几种常见的排序算法

几种常见的排序算法

冒泡排序(Bubble Sort):

  冒泡排序是一种计算机科学领域的较简单的排序算法。以数字排序为例,冒泡排序让相连的两个数字进行比较,将比较大的数字放在右边。假设最大的数字N在最左边。第一趟排序的时候,N每次和右边的数字做对比,都将比右边的数字大,然后将N一直往右移动只到最右。情形犹如冒泡,因此称为冒泡排序。

  以下为冒泡排序的一次范例:

    

冒泡排序示例
原始数据 10 1 66 20 19 3
第1次排序 1 10 20 19 3 66
第2次排序 1 10 19 3 20 66
第3次排序 1 10 3 19 20 66
第4次排序 1 3 10 19 20 66
第5次排序 1 3 10 19 20 66

  第一趟排序对数组的0、1位置进行排序,10>1,将1放在10之后;10<66,不动;66>20,对换20跟66的位置;66>19;对换66跟19的位置;66>3,对换66跟3的位置。从而获取到了最大值66。

  第二趟排序进行跟上面一样的对比,只是66可以不参与比较。获取第二大的数字20

  第三趟排序获取第三大的数字19。

  。。。。。。

  重复步骤,完成数字排序。

  以java代码为例,进行编码:

  

    public static void bubbleSorted(Integer[] list) {
        Integer temp;
        for (int i = 0; i < list.length; i++) {
            //如果没有进行数据调换,则说明排序完成
            boolean didSwap = false;
            for (int j = 0; j < list.length - i - 1; j++) {
                if (list[j] > list[j + 1]) {
                    temp = list[j];
                    list[j] = list[j + 1];
                    list[j + 1] = temp;
                    didSwap = true;
                }
            }
            if (didSwap == false) {
                return;
            }
        }
    }

 

  分析:冒泡排序每一次都进行了N次迭代,因此排序的时间复杂度为O(n²)。它过于简单了,以至于可以毫不费力地写出来。然而当数据量很小的时候它会有些应用的价值,数据量比较小的时候其实也有其他更优的选择,所以一般不会使用冒泡排序。

 

 

 

 

插入排序(insertion Sort):

 

  插入排序是一种最简单的排序算法,假设待排序的数组长度为N,插入排序必须进行N-1趟排序。插入排序的第M趟排序,保证数组的0位置开始到M位置处于已排序状态。

      以下为插入排序的一次范例:

插入排序示例
原始数据 10 1 66 20 19 3
第1次排序 1 10 66 20 19 3
第2次排序 1 10 66 20 19 3
第3次排序 1 10 20 66 19 3
第4次排序 1 10 19 20 66 3
第5次排序 1 3 10 19 20 66

  第一趟排序对数组的0、1位置进行排序,10>1,将1放在10之后。

  第二趟排序对数组的0、1、2位置进行排序,1<10<66,不进行数据移动。

  第三趟排序对数组的0、1、2、3位置进行排序,1<10<19<20。移动数据。

  。。。。。。

  重复上述步骤,直到数组中的所有元素排序完成。

 

  以java代码为例,进行编码:

  

 1     public static void insertionSorted(Integer[] list) {
 2         for (int i = 1; i < list.length; i++) {
 3             Integer temp = list[i];
 4             int j;
 5             for (j = i; j > 0 && list[j - 1] > temp; j--) {
 6                 list[j] = list[j - 1];
 7             }
 8             list[j] = temp;
 9         }
10     }

  分析:由于插入排序每一次都进行了N次迭代,因此插入排序的时间复杂度为O(n²)。而且在数组的数量比较小的情况下,插入排序的速度是很可观的。插入排序跟冒泡排序经常会拿起来做比较,看下方数据比较:

冒泡排序和插入排序对比
算法名称 最差时间复杂度 平均时间复杂度 最优时间复杂度 空间复杂度 稳定性
冒泡排序 O(N²) O(N²) O(N) O(1) 稳定
插入排序 O(N²) O(N²) O(N) O(1) 稳定

   

  两者在数据上的表现非常一致。但是插入排序可能因为循环不成立而退出,从而减少了比较的次数。因此一般情况下,插入排序是相比冒泡排序更优秀的排序算法,小数据量的情况下一般优先选择插入排序。

 

 

 

希尔排序(Shellsort):

  希尔排序的名称来自他的发明者DonaldShell,希尔排序是第一批冲破二次时间屏障的算法之一。它的算法思路是通过比较一定距离的元素,逐渐缩减,直到比较距离为1的元素。所以希尔排序也叫缩减增量排序。

以下为希尔排序的一次范例:

希尔排序示例
原始数据 10 1 66 20 19 3
第1次排序(间距为3) 10 1 3 20 19 66
第2次排序(间距为2) 3 1 10 20 19 66
第3次排序(间距为1) 1 3 10 19 20 66

  

  第一趟排序间距为3,将10与20比较;1跟19比较;66跟3比较,交换66跟3的位置。

  第二趟排序间距为2,将10与3比较,交换10个3的位置;20跟1比较;10跟19比较;66跟20比较;

  第三趟排序间距为1,将1跟3比较,交换1跟3的位置;3跟10比较;10跟20比较;20跟19比较,交换19跟20的位置。19再跟10比较;20跟66比较;

 

  以java代码为例,进行编程:

  

 1     public static void shellSorted(Integer[] list) {
 2         for (int gap = 3; gap > 0; gap--) {
 3             for (int i = gap; i < list.length; i++) {
 4                 Integer temp = list[i];
 5                 int j;
 6                 for (j = i; j - gap >= 0 && list[j - gap] > temp; j -= gap) {
 7                     list[j] = list[j - gap];
 8                 }
 9                 list[j] = temp;
10             }
11         }
12     }

  分析:上述代码中,gap为3、2、1,称为增量序列。希尔排序的效率依赖于增量序列的选择。希尔排序的问题在于,增量未必是互素的。

  Hibbard提出一个增量序列,称为Hibbard增量序列。Hibbard增量序列形如1、3、7、.....、2^k-1。因此增量之间没有公因子,所以Hibbard增量的最坏运行情形时间为O(N^3/2),平均运行时间为O(N^5/4)。

  Sedgewick提出的几种增量序列,最坏运行时间为O(N^4/3),平均运行时间为O(N^7/6),其中最好的序列为{1,5,19,41,109....},该序列9*4^i-9*2^i+1,或者4^i-3*2^i+1的形式。

 

堆排序(Heapsort):

  堆排序是一种使用堆为结构来进行排序的算法。利用二叉堆作为优先队列,进行排序,每次获取堆中最大值或者最小值,放入新序列中。实现了队列的排序。

 

堆排序例子:

 

  第一步获取最大值66;

  第二步获取最大值20;

  第三部获取最大值19;

     。。。。。

    以上述方式获取新的排序队列。

 

    以java代码为例,进行编程:

    

 1     private static int leftChild(int i) {
 2         return 2 * i + 1;
 3     }
 4 
 5     private static void perDown(Integer[] list, int i, int n) {
 6         int child;
 7         Integer temp;
 8         for (temp = list[i]; leftChild(i) < n; i = child) {
 9             child = leftChild(i);
10             if (child != n - 1 && list[child] < list[child + 1]) {
11                 child++;
12             }
13             if (temp < list[child]) {
14                 list[i] = list[child];
15             } else {
16                 break;
17             }
18         }
19         list[i] = temp;
20     }
21 
22     public static void heapSort(Integer[] list) {
23         Integer temp;
24 
25         for (int i = list.length / 2 - 1; i >= 0; i--) {
26             perDown(list, i, list.length);
27         }
28         for (int i = list.length - 1; i > 0; i--) {
29             temp = list[0];
30             list[0] = list[i];
31             list[i] = temp;
32             perDown(list, 0, i);
33         }
34     }

    分析:堆排序种,第一阶段构建堆最多用到2N次比较,第二阶段,第i次deleteMax最多用到2Logi次比较,总共最多2N*LogN-O(N)次比较。堆排序也是一个非常稳定的算法。他的比较平均只比最坏情况指出的情况略少。

    

归并排序(mergeSort):

   归并排序的用法于上面几种排序不同,归并排序使用场景是存在已经排序完成的队列A和队列B,现要将已经排序完成A、B两个队列合并成队列C;归并排序以O(N*logN)的最坏情形时间运行,而使用的比较时间几乎是最优的。它是递归算法的一个好的事例应用。

  归并排序例子:

  

    A、B为已经排序的数组,C为新建数组。A、B数组一开始有个指针指向第一个元素。

    第一步,比较A、B第一个元素1<2。将1写入C中,A的指针往右移动。

    第二部,比较2<4,将2写入C中,B的指针往右移动。

    .....

    同上,A或者B全部写入后,将另外的数组剩余的数字也写入C中。

 

    在单个数组进行排序的时候使用归并排序,则是将单个数组拆分成两个数组,将这两个数组分别排序完成后,再进行归并。

    以java代码为例,进行编程:

    

 1     public static void mergeSort(Integer[] list) {
 2         Integer[] tempArray = new Integer[list.length];
 3         mergeSort(list, tempArray, 0, list.length - 1);
 4     }
 5 
 6     private static void mergeSort(Integer[] list, Integer[] tempArray, int left, int right) {
 7         if (left >= right) {
 8             return;
 9         }
10         int center = (left + right) / 2;
11         mergeSort(list, tempArray, left, center);
12         mergeSort(list, tempArray, center + 1, right);
13         merge(list, tempArray, left, center + 1, right);
14     }
15 
16     private static void merge(Integer[] list, Integer[] tempArray, int leftPos, int rightPos, int rightEnd) {
17         int leftEnd = rightPos - 1;
18         int tempPos = leftPos;
19         int numberElements = rightEnd - leftPos + 1;
20 
21         while (leftPos <= leftEnd && rightPos <= rightEnd) {
22             if (list[leftPos] <= list[rightPos]) {
23                 tempArray[tempPos++] = list[leftPos++];
24             } else {
25                 tempArray[tempPos++] = list[rightPos++];
26             }
27         }
28 
29         while (leftPos <= leftEnd) {
30             tempArray[tempPos++] = list[leftPos++];
31         }
32 
33         while (rightPos <= rightEnd) {
34             tempArray[tempPos++] = list[rightPos++];
35         }
36 
37         for (int i = 0; i < numberElements; i++, rightEnd--) {
38             list[rightEnd] = tempArray[rightEnd];
39         }
40     }

    

    分析:归并排序的运行时间为O(N*logN),但是归并排序需要线性附加内存。整个算法在花费了数据拷贝再拷回来这些附加工作,减慢了排序速度。这种拷贝可以通过list和tempArray进行角色交换来避免。在java中归并排序的数据移动是很省时的,因为归并排序只是移动了对象的引用。在java类库中泛型排序使用的就是归并算法。

    

快速排序(mergeSort):

  快速排序是实践中的一种快速排序算法,在Java中对基本类型的排序特别有用,也是面试时候最容易被问道的排序算法。快排的平均运行时间为O(N*logN),最坏情形为O(N^2),但是经过稍许努力就能让最坏情况很难出现。通过快排和堆排序进行结合,由于堆排序的最坏为O(N*logN),我们可以对几乎所有的输入都能达到快速排序的快速运行时间。

  

  快排例子:

  第一步,随机获取枢纽元,上例中为19。

  第二步,将其他数字跟19做比较,一组比19小,一组等于19,一组大于19。

  第三步,将大于19和小于19的两组进行排序。

  第四步,按照顺序将他们写入数组中。

 

  以java代码为例,进行编程:

  

 1     public static void quickSort(Integer[] list) {
 2         quickSort(list, 0, list.length - 1);
 3     }
 4 
 5     private static void quickSort(Integer[] list, int left, int right) {
 6         if (left + CUTOFF <= right) {
 7             Integer pivot = media3(list, left, right);
 8             int i = left + 1, j = right - 2;
 9             while (true) {
10                 //=pivot,防止list[i]=list[j]=pivot,进入死循环
11                 while (i<list.length-1&&list[i] <=pivot) {
12                     i++;
13                 }
14                 while (j>0&&list[j] >= pivot) {
15                     j--;
16                 }
17                 if (i < j) {
18                     swapReferences(list, i, j);
19                 }else {
20                     break;
21                 }
22             }
23             //分组完成再把中值拎回来放在中间
24             swapReferences(list, i, right - 1);
25 
26             //防止数组中数字一样进入死循环
27             if(left<i-1&&i-1<right){
28                 quickSort(list, left, i - 1);
29             }
30             if(left<i+1&&i+1<right){
31                 quickSort(list, i + 1, right);
32             }
33         } else {
34             insertionSorted(list, left, right);
35         }
36     }
37 
38     private static Integer media3(Integer[] list, int left, int right) {
39         int center = (left + right) / 2;
40         if (list[center] < list[left]) {
41             swapReferences(list, left, center);
42         }
43         if (list[right] < list[left]) {
44             swapReferences(list, left, right);
45         }
46         if (list[right] < list[center]) {
47             swapReferences(list, center, right);
48         }
49         //先把中值拎出去,方便交换操作
50         swapReferences(list, center, right - 1);
51         return list[right - 1];
52     }
53 
54     private static void swapReferences(Integer[] list, int i, int j) {
55         Integer temp = list[i];
56         list[i] = list[j];
57         list[j] = temp;
58     }

 

 

 

 

  分析:枢纽元的选择对快排的效率有很大影响,随机选取枢纽元是很比较靠谱的做法。比较不好的选取方式选取第一个或者最后一个,上述代码的枢纽元用的三数中值分割法,取数组第一位,中间位,和最后一位,从三个数中去中间值作为枢纽元。因为快排是递归的分隔数组进行排序,当数组被切割得比较小的时候(N<=20),可以使用插入排序来完成,效率会更高。快速排序的最坏情况为O(N^2),最好情况为O(NlogN),平均情况也是O(NlogN)。

 

桶排序(bucketSorted):

  当输入的数据由仅小于M的正整数来组成,可以使用桶排序来完成。假设有一个存在一个整数数组,数组中的元素不超过10。对这个数组进行排序,创建一个长度为11的数组count,count中的每个元素都为0,当读入数据为i的时候,count(i)加上1,等全部读取完毕之后根据count数将数组排列出来。

  桶排序例子:

    

    原数组进行迭代,读到的数据在在count数组中对应下标的地方进行+1操作。等原数组迭代完成后,将count数组读出就是排序完成的数组。

    

    java代码如下:

    

 1     public static void bucketSorted(Integer[] list) {
 2         Integer[] count = new Integer[10];
 3         for (int i = 0; i < count.length; i++) {
 4             count[i] = 0;
 5         }
 6 
 7         for (Integer value : list) {
 8             count[value] += 1;
 9         }
10         int i = 0;
11         for (int k = 0; k < count.length - 1; k++) {
12             for (int j = 1; j <= count[k]; j++) {
13                 list[i++] = k;
14             }
15         }
16     }

 

    

  分析:桶排序比较简单,然而在业务中很少有机会能使用到。当你的数字M上限比较高的时候,可以使用基数排序来实现。算法的用时为O(M+N),M为桶的个数。如果M为O(N),则总时间为O(N)。

 

    

 

基数排序(radix sort):

 

基数排序又叫做卡片排序,假设我们有大小在0~999之间数据进行排序,如果使用桶排序就不大合适。此时我们就可以使用基数排序,按照个位、十位、百位数进行三次排序。

  

基数排序例子:

  

    第一步,根据个位数排序,排序完成依次读取。

    第二部,根据十位数排序,排序完成依次读取。

    第三部,根据百位数排序,排序完成依次读取。

    

    由于每一次排序的过程都保留了上一次排序的顺序,所以最后数字能按照大小进行排序。

    

    以java代码为例,进行编程:

  

 1     public static void radixSort(Integer[] list, int numberLen) {
 2 
 3         //初始化
 4         final int BUCKETS = 10;
 5         ArrayList<Integer>[] buckets = new ArrayList[BUCKETS];
 6         for (int i = 0; i < BUCKETS; i++) {
 7             buckets[i] = new ArrayList<Integer>();
 8         }
 9 
10         //排序
11         for (int i = 0; i < numberLen; i++) {
12 
13             for (Integer num : list) {
14                 buckets[(int) ((num / Math.pow(10, i)) % 10)].add(num);
15             }
16 
17             int j = 0;
18             for (ArrayList<Integer> bucket : buckets) {
19                 for (Integer number : bucket) {
20                     list[j++] = number;
21                 }
22                 bucket.clear();
23             }
24         }
25     }

    

  分析:基数排序的运行时间为O(p(N+b)),p是排序的趟数,对应上述代码的numberLen。b为桶的个数,对应上述代码中的BUCKETS。桶排序和基数排序实现了线性时间完成排序。但是桶排序和基数排序的条件相比其他排序苛刻。

 

计数基数排序(counting radix sort):

  计数基数排序是基数排序的另外一个实现,他避免使用ArrayList,取而代之的是用一个计数器。当数组扫描的对应的数字的时候,计数器在对应的位置上+1。等所有排序完成之后,根据计算器的数据,将对应的数据翻出来。完成排序。

 

  

 

  

  第一步,遍历原数组,读取到值之后,在对应的计数器上+1,比如读取到5,则在计数器的5上加1。

  第二部,将计数器上数据进行叠加,获取对应数字的位置,计数器上1的位置上为0的数量+1的数量,2位置上的数量为0的数量+1的数量+2的数量。

  第三部,根据计数器,重排排序数组。

  

  以java代码为例,进行编程:

  

 1     public static Integer[] countingSort(Integer[] list) {
 2         Integer max = getMax(list);
 3 
 4         Integer min = getMin(list);
 5 
 6         Integer[] result = new Integer[list.length];
 7 
 8         int k = max - min + 1;
 9         int[] count = new int[k];
10         for (int i = 0; i < list.length; ++i) {
11             count[list[i] - min] += 1;
12         }
13         for (int i = 1; i < count.length; ++i) {
14             count[i] = count[i] + count[i - 1];
15         }
16         for (int i = list.length - 1; i >= 0; --i) {
17             result[--count[list[i] - min]] = list[i];//按存取的方式取出c的元素
18         }
19         return result;
20     }
21 
22     private static Integer getMax(Integer[] list) {
23         int b[] = new int[list.length];
24         int max = list[0];
25         for (int i : list) {
26             if (i > max) {
27                 max = i;
28             }
29         }
30         return max;
31     }
32 
33     private static Integer getMin(Integer[] list) {
34         int b[] = new int[list.length];
35         int min = list[0];
36         for (int i : list) {
37             if (i < min) {
38                 min = i;
39             }
40         }
41         return min;
42     }

   

    分析:计数排序也可以用来做字符串的排序。但是和桶排序有一个共同的问题是,当要进行排序的数字之间跨度非常大,或者字符串排序中字符串的长度特别长的时候,该算法的优势就没有那么明显,甚至完全消失。

 

 

外部排序(external sorting):

  以上的各种算法,都是将要排序的数组放到内存之中进行排序,但是在一些特殊场景下,需要被排序的内容数量太大,而没办法装载入内存的时候,我们就需要用到外部排序算法。下面简单介绍一种在磁盘上进行外部排序的算法,因为在磁带上访问一个元素需要把磁带转到正确的位置上,因此磁带需要被连续访问才能提高效率。我们通过部分排序的方式,在磁带上顺序得读入数据和写出数据来完成排序。假设现在我们有4个磁带,a1,a2,b1,b2,内存一次可以容纳3个数据,见下方模型:

 

 

  

   1、一开始数据并无排序,因为内存每次可以读入3个数据,每三个数据进行一次排序,保存12个数据中,有四队有序数据,b1两组,b2有两组。

   2、使用归并排序的套路,磁盘的磁头分别指向两组数据的开头部分,读入后进行比较,然后按顺序写入a1中。第三组数据和第四组数据写入a2中。

   3、跟上述方式一样,磁头指向a1、a2的开始部分,然后将比较后的的数据写入b1中。

   排序完成。

   

     分析:上方的模型比较简单,当我们有更多磁带,就可以进行多路合并,即同时比较多组数据来减少数据写入写出的次数。但是通过增加磁带来减少读写次数并不方便,还可以通过多相合并,即通过分配不同磁带的不同顺串的数量来完成上述工作。

  

 

   总结排序是最古老,被研究得最完备的问题之一。对于大部分内部排序的应用,不是插入、希尔、归并、就是快排。就主要取决于输入数量的大小和底层环境决定的。插入排序适用于非常少量的输入。中等规模用希尔是个不错的选择,只要增量序列合适,就能表现优异。归并排序最坏情况为O(NLogN),但是却需要额外的空间,但是比较的次数几乎是最优的。快排并不保证提供最坏的时间复杂度,并且编程麻烦。但是几乎可以肯定做到O(N logN),配合堆排序,就可以保证最坏情况为O(N logN)。基数排序可以在线性时间内排序,在特定环境下是相对比较的排序法更加实用。而外部排序则处理数据量大无法完全放入内存的情况。

  

posted on 2018-03-12 14:55  阿姆斯特朗回旋炮  阅读(1586)  评论(1编辑  收藏  举报

导航