七种排序算法的实现

最近在找工作时很多面试官都会问到排序算法的实现,所以趁着周末有时间就来总结一下七种排序算法实现。
算法的实现我使用的是java语言,其中为了增强算法的可复用性,我使用了泛型这一特性,在排序的数组元素都要实现Comparable接口,在排序时使用Comparable接口中的方法compareTo来对两个元素进行比较。

一、冒泡排序。

在当初大一时候学习C语言,当时教的两种排序算法冒泡排序、简单选择排序印象都很深刻,应该是我学过的排序算法中最简单的了。

冒泡排序的思想:

1.第一次排序时将序列[0 ~ n - 1]中从前往后进行两个相邻元素的比较,若前者较大则交换,比较n-1次;
当第一趟排序结束时,序列最大的元素就被交换到位置n-1上,就如同一个气泡,一步一步往后翻滚,直到最后一位。

2.重复步骤1,在第i趟时需要翻滚n-i-1次,每趟决定一个元素的位置,一共需要n-1趟。

比如,初始序列: [1, 5, 4, 3, 2]

第1趟: [1, 4, 3, 2 ] 5

第2趟: [1, 3, 2 ] 4, 5

......

public static <T extends Comparable<T>> void bubbleSort(T[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        for (int j = 0; j < a.length - i - 1; j++) {
            if (a[j].compareTo(a[j + 1]) > 0) {
                T temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

其实优化的冒泡排序应该有第3步,就是在一趟排序中未交换元素则说明子序列已经有序,不在进行下一趟排序;所以冒泡排序最多执行n-1次。

比如,初始序列:[1, 2, 3, 5, 4]

只需要一趟排序:[1, 2, 3, 4 ] 5

下面这个是我实现的优化冒泡算法,因为多了赋值语句和条件判断,所以在数组前面序列有序的情况下运行时间会有优化,但是如果数组元素分布均匀,大小随机则反而效率会低下。

public static <T extends Comparable<T>> void bubbleSortOpt(T[] a) {
    for (int i = 0; i < a.length; i++) {
        boolean isSwap = false;
        for (int j = 0; j < a.length - i - 1; j++) {
            if (a[j].compareTo(a[j + 1]) > 0) {
                T temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
                isSwap = true;
            }
        }
        i = isSwap ? i : a.length;
    }
}

而下面这个是在我的数据结构课本上的实现,不使用条件判断跳出循环,解决了效率问题。经过多次测试,下面的排序算法的运行时间确实比上面两个要好。

public static <T extends Comparable<T>> void bubbleSortOpti(T[] a) {
    int i = a.length - 1, j, last;
    while (i > 0) {
        last = 0;
        for (j = 0; j < i; j++) {
            if (a[j].compareTo(a[j + 1]) > 0) {
                T temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
                last = j;
            }
        }
        i = last;
    }
}

优化冒泡排序在已经有序的情况下只需进行一趟排序,n-1次比较。因此最好情况下的时间复杂度为O(n),无需移动元素;
最坏情况进行n-1趟排序,第i趟比较n-i次,移动元素3(n-i)次,这样总的比较次数为(1/2)n(n-1),移动元素次数为3n(n-1)/2。最坏情况下时间复杂度为O(n^2)。

二、简单选择排序。

简单选择排序的基本思想:

1.第一趟在初始序列[0 ~ n-1]中找最小值元素,并与位置为0的元素交换位置。

2.第i趟在序列[i-1, n-1]中进行,找到最小值元素与位置为i-1的元素交换位置。每次决定一个元素的位置,经过n-1趟后使得初始序列有序。

比如,初始序列:[1, 5, 4, 3, 2]

第1趟:1 [5, 4, 3, 2]

第2趟:1 2 [4, 3, 5]

......

public static <T extends Comparable<T>> void selectSort(T[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        int k = i;
        for (int j = i + 1; j < a.length; j++) {
            if (a[k].compareTo(a[j]) > 0) {
                k = j;
            }
        }
        if (k > i) {
            T temp = a[i];
            a[i] = a[k];
            a[k] = temp;
        }
    }
}

上面这种方法是标记了最小元素的位置,在一趟排序的最后交换序列首元素和序列最小元素。之前我还写过一种,在写这篇博客之前我一直以为是冒泡排序,后来才发现是选择排序。我贴出来对比一下,这种写法也是每次确定最小元素的位置但是没有标记而是在一趟排序时进行多次交换元素,这样做影响了效率不推荐这样做。

public static <T extends Comparable<T>> void selectSortE(T[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        for (int j = i + 1; j < a.length; j++) {
            if (a[i].compareTo(a[j]) > 0) {
                T temp = a[i];
                a[i] = a[j];
                a[j] = temp;
            }
        }
    }
}

简单选择排序和数组序列的初始排列无关,无聊排列如何都必须要执行n-1趟。总的比较次数为n(n-1)/2,交换元素(n-1)次,移动元素3(n-1)次。最好、最坏和平均时间复杂度都为O(n^2)。

三、直接插入排序

直接插入排序的基本思想:

1.向数组序列后面检索,当检索到后一个元素小于前一个元素时,记录下元素。从记录的元素位置开始往前检索,同时元素往后移找到比记录下元素要小的元素的位置插入。

2.重复1的步骤,经过n-1趟排序后即成为有序序列。

比如,初始序列:[1, 5, 4, 3, 2]

第1趟:[1, (4, 5), 3, 2]

第2趟:[1, (3, 4, 5), 2]

第3趟:[1, (2, 3, 4, 5)]

public static <T extends Comparable<T>> void insertSort(T[] a) {
    for (int i = 1; i < a.length; i++) {
        int j = i;
        T temp = a[i];
        for (; j > 0 && temp.compareTo(a[j - 1]) < 0; j--) {
            a[j] = a[j - 1];
        }
        a[j] = temp;
    }
}

直接插叙排序在有序的情况下,总的比较次数是n-1,移动元素次数是2(n-1),最好情况下时间复杂度就是O(n)。最坏情况下,一趟最多比较i次,移动元素i+2次,总的比较次数为n(n-1)/2,移动元素次数(n+4)(n-1)/2。最坏情况时间复杂度为O(n^2)。

四、快速排序。

快速排序可以说是冒泡排序的高级版本,冒泡排序每次都只能对相邻的两个数进行比较,而快速排序从两端同时检索保证每趟排序结束,基准数左边的元素都小于基准数,基准数右边的元素都大于基准数。代码中是选中序列第一个元素作为基准,对快排来说还有很多优化的方法,下次有时间的时候再总结一下。

快速排序的思想:

1.先从序列后端往前检索,找到小于基准数的元素,再从序列前端往后检索,找到大于基准数的数交换两个元素位置。直到从后往前和从前往后的指针相遇,
将基准数的位置和相遇位置元素交换结束一趟排序。这样就划分出两个序列,一个序列的元素比基准数都要小,一个序列的元素比基准数都要大。

2.将划分出的子序列按1的步骤继续排序,直到子序列只有一个元素时得到有序序列。

比如,初始序列:[1, 5, 4, 3, 6, 2]

第1趟:[1, (5, 4, 3, 6, 2)]

第2趟:[1, (2, 4, 3), 5, (6)]

第3趟:[1, 2, (4, 3), 5, 6]

第4趟:[1, 2, 3, 4, 5, 6]

public static <T extends Comparable<T>> void quickSort(T[] a) {
    quickSort(a, 0, a.length - 1);
}
public static <T extends Comparable<T>> void quickSort(T[] a, int left, int right) {
    if (left >= right) return;
    T temp = a[left];
    int i = left;
    int j = right;
    while (i < j) {
        while(a[j].compareTo(temp) >= 0 && i < j) j--;
        while(a[i].compareTo(temp) <= 0 && i < j) i++;
        if (i < j) {
            T t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }
    a[left] = a[j];
    a[j] = temp;
    quickSort(a, left, j - 1);
    quickSort(a, j + 1, right);
}

快排最坏情况是每次分割的两个子序列中有一个为空,机初始序列有序(顺序或逆序),这是快速排序效率最低,时间复杂度为O(n^2)。快排的平均时间复杂度为O(nlogn)。在最坏情况下快排需要的附加堆栈存储空间为O(n)。

五、堆排序。

使用数据结构堆来辅助我们排序,如果要构造非递减序列我们采用大根堆。大根堆是包含n个结点的完全二叉树,该树中每个节点的关键字值小于等于其双亲节点的关键字值。

堆排序的思想:

1.将初始序列构造成大根堆,从堆中第一个非叶子节点开始调用adjustDown方法将元素向下调整,直到堆顶。

2.第i趟排序将堆顶元素a[0]与堆底元素a[n-i]交换,然后将a[0]向下调整,直到堆中只剩最后一个元素。

比如,初始序列:[1, 5, 4, 3, 2]

建堆后:[5, 3, 4, 1, 2]

第1趟:[4, 3, 1, 2] 5

第2趟:[3, 1, 2] 4, 5

第3趟:[1, 2] 3, 4, 5

......

public static <T extends Comparable<T>> void heapSort(T[] a) {
    for (int i = (a.length - 2) / 2; i >= 0; i--) {
        adjustDown(a, i, a.length - 1);
    }
    for (int i = a.length - 1; i >= 0; i--) {
        T temp = a[0];
        a[0] = a[i];
        a[i] = temp;
        adjustDown(a, 0, i - 1);
    }
}
public static <T extends Comparable<T>> void adjustDown(T[] a, int r, int j) {
    T temp = a[r];
    int child = 2 * r + 1;
    while (child <= j) {
        if (child < j && a[child].compareTo(a[child + 1]) < 0) child++;
        if (temp.compareTo(a[child]) >= 0) break;
        a[(child - 1) / 2] = a[child];
        child = 2 * child + 1;
    }
    a[(child - 1) / 2] = temp;
}

构建堆得时间最多是O(nlogn),在每趟排序后都要向下调整一次最多花费O(nlogn),所以堆排序的时间复杂度为O(nlogn)。

六、希尔排序。

据百度百科,希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

希尔排序的思想:

1.第1趟以数组长度的一般作为分量d,后面的步骤与直接插入排序基本一样,只是循环移动的步长为d。

2.在一趟排序之后将增量除以2,继续执行插入排序,直到d为1。

比如,初始序列:[1, 5, 4, 3, 2]

第1趟:[1, 3, 4, 5, 2] d=2

第2趟:[1, 3, 2, 5, 4]

第3趟:[1, 2, 3, 5, 4] d=1

第4趟:[1, 2, 3, 4, 5]

public static <T extends Comparable<T>> void shellSort(T[] a) {
    int d = a.length / 2;
    while (d >= 1) {
        for (int i = d; i < a.length; i++) {
            int j = i;
            T temp = a[i];
            for (; j - d >= 0 && temp.compareTo(a[j - d]) < 0; j -= d) {
                a[j] = a[j - d];
            }
            a[j] = temp;
        }
        d /= 2;
    }
}

据百度百科,希尔排序的时间性能优于直接排序的原因:

  1. 当文件初态基本有序时,直接插入排序所需的比较和移动次数均较少。
  2. 当n值较小时,n和n2差别也小,即直接插入排序的最好时间复杂和最坏时间复杂度O(n2)差别不大。
  3. 在希尔排序开始时增量较大,分组较多,每组记录数目少,故组内直接插入较快后来增量减少,而组内的记录数目逐渐增多,
    但由于已经按2d最为距离排过序,使文件较接近于有序状态,所以一趟排序过程也快。

希尔排序的平均时间复杂度为O(n^1.3)。

七、归并排序。

归并排序思想:

将有n个元素的序列看成是n个长度为1的有序子序列,然后两两合并子序列,得到[n/2]个长度为2或1的有序子序列;
在两两合并,知道得到一个长度为n的有序序列时结束。

比如,初始序列:[1, 5, 4, 3, 2]

第1趟:[(1, 5), 4, (2, 3)]

第2趟:[(1, 4, 5), (2, 3)]

第3趟:[1, 2, 3, 4, 5]

public static <T extends Comparable<T>> void mergeSort(T[] a) {
    mergeSort(a, 0, a.length - 1);
}
public static <T extends Comparable<T>> void mergeSort(T[] a, int left, int right) {
    if (left >= right) return;
    int mid = (left + right) / 2;
    mergeSort(a, left, mid);
    mergeSort(a, mid + 1, right);
    merge(a, left, right, mid);
}
public static <T extends Comparable<T>> void merge(T[] a, int left, int right, int mid) {
    Object[] temp = new Object[right - left + 1];
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) {
        if (a[i].compareTo(a[j]) <= 0) {
            temp[k++] = a[i++];
        } else {
            temp[k++] = a[j++];
        }
    }
    while (i <= mid) temp[k++] = a[i++];
    while (j <= right) temp[k++] = a[j++];
    for (i = 0; i < temp.length; i++) {
        a[left + i] = (T) temp[i];
    }
}

两路合并算法的时间复杂度为o(nlogn),附加辅助空间为O(n)。

总结一下

|排序算法|平均时间复杂度|最好情况时间复杂度|最坏情况时间复杂度|最坏空间复杂度|稳定性|
|-|
|冒泡排序|O(n2)|O(n)|O(n2)|O(1)|稳定|
|简单选择排序|O(n2)|O(n2)|O(n^2)|O(1)|不稳定|
|直接插入排序|O(n2)|O(n)|O(n2)|O(1)|稳定|
|快速排序|O(nlogn)|O(nlogn)|O(n^2)|O(n)|不稳定|
|堆排序|O(nlogn)|O(nlogn)|O(nlogn)|O(1)|不稳定|
|希尔排序|O(n1.3)|O(n)|O(n2)|O(1)|不稳定|
|归并排序|O(nlogn)|O(nlogn)|O(nlogn)|O(n)|稳定|

最近开了一个个人博客网站,但是不懂怎么用wordpress=。=
欢迎前来提意见我的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容

  • 排序的基本概念 在计算机程序开发过程中,经常需要一组数据元素(或记录)按某个关键字进行排序,排序完成的序列可用于快...
    Jack921阅读 1,421评论 1 4
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,170评论 0 52
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,729评论 0 15
  • 一、 单项选择题(共71题) 对n个元素的序列进行冒泡排序时,最少的比较次数是( )。A. n ...
    貝影阅读 9,044评论 0 10
  • 概述排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的...
    Luc_阅读 2,259评论 0 35