浅析8种常用排序

1、引言

  排序作为一种常用算法,时常活跃在我们的工作学习中,为了进一步加深对常用算法的理解,故做此博文与大家共享...

2、常用排序算法的种类

  冒泡排序,简单选择排序,插入排序,希尔排序,快速排序,归并排序,堆排序,基数排序

3、常用排序算法的一些特征

  (1)排序的稳定性(什么是稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,

且在之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的)

  •    稳定排序:冒泡排序、插入排序、归并排序、基数排序
  •      非稳定排序:选择排序、快速排序、希尔排序、堆排序

  (2)排序的维度(即占用内存的大小及运行所用时间的复杂度)

4、各种排序 

  ①快速排序

为了帮助大家理解下面引用《啊哈!算法》一书中的经典实例进行说明(《啊哈!算法》很好的一本书将枯燥的算法趣味化,建议大家看看)

  假设我们现在对“6  1  2 7  9  3  4  5 10  8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数。为了方便,我们一般取第一个数作为基准书。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列。
      3  1  2 5  4  6  9 7  10  8
 

方法其实很简单:分别从初始序列“6  1  2 7  9  3  4  5 10  8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。

094811yilrz1tkzkvlrriz.png
      首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j--),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
095430axy0qkhxxkktkktk.png
095437kdandfxhbtokk2qh.png


      现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下。

       6  1  2  5  9 3  4  7  10  8
095448k1kevwlz41373e7k.png
095458ejza15wscjv7iw5c.png
       到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下。
       6  1  2 5  4  3  9  7 10  8
 
       第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下。
3  1 2  5  4  6  9 7  10  8
095506uz7e1uuukcblhkxv.png
095514cag5fumuqqg5jnsw.png
095530e0jf6p0y6aaaw2ir.png
       到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。这样就进行了一轮排序,类似于二分法,下面将以6为二分点,将数据分为3、1、2、5、4和9、7、10、8两部分,重复上面的步骤进行递归,直到排序完成。
/**
* 快速排序,从小到大排
*/
public void quickSort(int arr[],int low,int high){
  if(low>high){
    return;//左边数大于右边数,程序结束
  } 
  int key = arr[low];//将第一个数设为关键字
  int i = low;
  int j = high;
  //左边找到的数与右边找到的数不相等时就一直找
  while(i!=j){
  //先从右边开始找,不能从左边开始下面会解释
  while(i<j&&arr[j]>=key){
    j--;
  }
  //再找左边的
  while(i<j&&arr[j]<=key){
    i++;
  }
  //交换低位和高位上找到的数
  if(i<j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }
}
  //当i=j时,即交换完毕,将该数与基本数交换,此步破坏了快速排序的稳定性
  arr[low] = arr[i];
  arr[i] = key;
  quickSort(arr,low,i-1);//经过基本数筛选后,基本数左边所有数都小于基本数,基本数右边所有数都大于基本数
  quickSort(arr,i+1,high);
}

@Test
public void test(){
  int[] arr = {6,1,2,7,9,3,4,5,10,8};

  //用一个循环完成所有递推确保排序完成,现在还没弄明白最少循环多少次能保证排序完成
  for(int i=1;i<arr.length;i++){    
    quickSort(arr,0,arr.length-1);
  }
  System.out.println(Arrays.toString(arr));
}

有必要解释下为什么一定要从右边开始找,其实原因很简单,如果先从左边开始的话,不能保证与基准数交换的数会是一个小于基准数的值,可以自己debug看看,实践见真章!   



  

②冒泡排序

  冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行 直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。由于对于相等的相邻两个数,无需进行交换,换句话说原来相等的两个数左右顺序不会发生改变,故该算法是一种稳定算法。又因为排序要经过多次循环反复对相邻元素进行排序,所以该算法的效率是比较低的,时间复杂度达到O(n2)。

  冒泡排序算法的运作如下:

  1、 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

  2、 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

  3、 针对所有的元素重复以上的步骤,除了最后一个。

  4、 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

  用个小实例图解冒泡过程:

/**
* 冒泡排序,从小到大排
*/
public void bubbleSort(int[] arr){
  for(int i=0;i<arr.length;i++){
    for(int j=0;j<arr.length-i-1;j++){
      if(arr[j]>arr[j+1]){
        int temp = arr[j];
        arr[j] = arr[j+1];
        arr[j+1] = temp;
      }
    }
  }
  System.out.println(Arrays.toString(arr));
}

@Test
public void testBubbleSort(){
  bubbleSort(new int[]{6,1,2,7,9,3,4,5,10,8});
}


③插入排序

  插入排序的基本思想:每步将一个待排序的记录,按其关键字的大小插入前面已经排好序的文件中适当的位置上,直到全部插入完为止。

  i、直接插入排序

    直接插入的排序的基本思想:把待排序的记录按其关键字的大小逐个插入到一个已经排序好的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

  用个小实例图表解释直接插入排序的过程:(参考资料:《算法与数据结构》 清华大学出版社 陈媛版)

注意:用方括号括起来的部分表示已经排序好的部分
i=1 [6] 1 2 7 9 3 4 5 10 8
i=2 [1,6] / 2 7 9 3 4 5 10 8
i=3 [1,2,6] / / 7 9 3 4 5 10 8
i=4 [1,2,6,7] / / / 9 3 4 5 10 8
i=5 [1,2,6,7,9] / / / / 3 4 5 10 8
i=6 [1,2,3,6,7,9] / / / / / 4 5 10 8
i=7 [1,2,3,4,6,7,9] / / / / / / 5 10 8
i=8 [1,2,3,4,5,6,7,9] / / / / / / / 10 8
i=9 [1,2,3,4,5,6,7,9,10] / / / / / / / / 8
i=10 [1,2,3,4,5,6,7,9,10] / / / / / / / / /
/**
*代码示例,直接插入排序,从小到大 
*/
public void immediateInsertSort(int[] arr){
  for(int i=1;i<arr.length;i++){//假定第一个元素是已经排好序的,故只需要进行n-1次插入排序(n为要排序的元素个数)
    int temp = arr[i];//将要插入的数,存入一个名为‘监视哨’的临时变量中,目的是防止被插入的数越界 
    int j = i-1;//插入的数从第i个数的前一个空位开始查起,并进行比对
    while(j>=0&&temp<arr[j]){

      //这里要注意不要想当然的把arr[j]写成arr[i-1],我就犯过这样的错误,i-1,中的i只有在for循环中才会变的
      arr[j+1] = arr[j];//如果插入的数比比对数小,将比对数右移一位
      j--;
    }
    arr[j+1] = temp;
  }
  System.out.println(Arrays.toString(arr));
}

@Test
public void testImmediateInsertSort(){
  immediateInsertSort(new int[]{6,1,2,7,9,3,4,5,10,8});
}

 

  ii、折半插入排序

  折半排序的基本思想:

   1)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置 到中间值的位置,这样很简单的完成了折半;

     2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围,不停的折半,范围依次缩小为 1/2  1/4  1/8 .......快速的确定出第 i  个元素要插在什么地方;

       3)确定位置之后,将整个序列后移,并将元素插入到相应位置。

 1 /**
 2      * 折半插入排序,从小到大
 3      */
 4     public void halfInsertSort(int[] arr){
 5         for(int i=1;i<arr.length;i++){
 6             int insertNode = arr[i];//将本轮排序的最后一位假定为要插入排序的数
 7             int low = 0;
 8             int high = i-1;//高位为数组倒数第二位数
 9             while(low<=high){//不断的折半二分
10                 int middle = (low+high)/2; 
11                 //要插入的数小于中间值,说明要排序插入的值位于中间值左边,那么中间值减1,继续二分,缩小查找范围
12                 if(insertNode<arr[middle]){
13                     high = --middle;
14                 }else{
15                     low = ++middle;
16                 }
17             }
18             int  j = i;
19             while(j>low){
20                 arr[j] = arr[j-1];//如果插入的数比比对数小,将比对数右移一位
21                 j--;
22             }
23             arr[low] = insertNode;
24         }
25         System.out.println(Arrays.toString(arr));
26     }
27     
28     @Test            
29     public void testHalfInsertSort(){
30         int[] arr = new int[]{6,1,2,7,9,3,4,5,10,8};
31             halfInsertSort(arr);
32     }

iii、希尔排序

  希尔排序的基本思想:先将整个待排序的数分成若干组,分别对每个小组进行直接插入排序,然后再缩减步长继续进行排序,待整个序列基本有序时(插入排序对基本有序的元素排序效率极高),对全部元素进行一次直接插入排序,至此排序完成,希尔排序效率非常高,其效率高于快速排序

  下面仍以6,1,2,7,9,3,4,5,10,8进行排序,分析希尔排序的排序过程

  第一次:步长gap=10/2,分组为(6,3)(1,4)(2,5)(7,10)(9,8),排序后结果(3,6)(1,4)(2,5)(7,10)(8,9)

第二次:步长gap=5/2,分组为(3,2,8,4,10)(1,7,6,5,9),排序后结果(2,3,4,8,10)(1,5,6,7,9)

 

第三次:步长gap=2/2,分组为(2,1,3,5,4,6,8,7,10,9),排序后结果(1,2,3,4,5,6,7,8,9,10)

 1 /**
 2      * 希尔排序,小到大
 3      */
 4     public void shellSort(int[] arr){
 5         int n = arr.length;
 6         for(int gap=n/2;gap>0;gap/=2){//由希尔排序原理写出循环条件
 7             for(int i=gap;i<n;i++){//以gap为步长进行直接插入排序
 8                 int insertNode =  arr[i];
 9                 int j = i-gap;
10                 while(j>=0&&insertNode<arr[j]){
11                     arr[j+gap] = arr[j];
12                     j-=gap;
13                 }
14                 arr[j+gap] = insertNode;
15             }
16         }
17         System.out.println(Arrays.toString(arr));
18     }
19     
20     @Test            
21     public void testShellSort(){
22         int[] arr = new int[]{6,1,2,7,9,3,4,5,10,8};
23         shellSort(arr);
24     }

 



 

④选择排序
选择排序的基本思想:一个长度为N的无序数组中,在第一趟遍历N个数据,找出其中最小的数值与第一个元素交换,第二趟遍历剩下的N-1个数据,找出其中最小的数值与第二个元素交换,直到第N-1趟遍历剩下的2个数据,找出其中最小的数值与第N-1个元素交换,至此选择排序完成。
 1 /**
 2      * 选择排序,小到大
 3      */
 4     public void selectSort(int[] arr){
 5         for(int i=0;i<arr.length-1;i++){//最后一个数无需参与排序
 6             int index = i;//用来保存每轮最小值的索引
 7             for(int j=i+1;j<arr.length;j++){
 8                 if(arr[index]>arr[j]){//后面的数比前面的数小,则将较小值的索引保存在index中
 9                     index = j;
10                 }
11             }
12             //当前待排序的数与最小数交换,完成本次排序
13             int temp = arr[index];
14             arr[index] = arr[i];
15             arr[i] = temp;
16         }
17         System.out.println(Arrays.toString(arr));
18     }
19     
20     @Test
21     public void testSelectSort(){
22         int[] arr = {6,1,2,7,9,3,4,5,10,8};
23         selectSort(arr);
24     }


⑤归并排序

  归并排序:归并排序是一种稳定的排序方式,即归并排序不交换相同的元素,归并排序主要要做两件事 

  (1)“分解”:将序列每次折半划分

  (2)“合并”:将划分后的序列段两两合并后排序

    但要注意的分解和归并是同时进行的,并不是全部分解完了,再去合并,归并排序是一种高效的排序算法,可以毫不夸张的说在比较排序中,归并排序的效率是最高的,jdk的Arrays.sort()方法就是采用的归并排序,有兴趣的朋友可以参看jdk源码。

   下面以java代码为例,实现归并排序
  

 1 /**
 2      * 合并排好序的数组
 3      * @param arr 待排序的数组
 4      * @param first 数组的第一个数
 5      * @param mid 数组的中间数
 6      * @param last 数组的最后一位数
 7      * @param temp 临时数组,用于临时存放排好序的序列
 8      */
 9     public void mergeSort(int[] arr, int first, int mid, int last, int temp[]){
10         int i = first, j = mid + 1, m = mid, n = last, k = 0;        
11      while(i<=m&&j<=n){
12             if(arr[i]<=arr[j]){
13                 temp[k++] = arr[i++];
14             }else{
15                 temp[k++] = arr[j++];
16             }
17         }
18         while(i<=mid){
19             temp[k++] = arr[i++];
20         }
21         while(j<arr.length){
22             temp[k++] = arr[j++];
23         }
24         for(i=0;i<k;i++){
25             arr[i+first] = temp[i];
26         }
27     }
28     
29     /**
30      * 分解数组直到一个数一组,原理很简单就是一直递归
31      */
32     public void resolveSort(int arr[], int first, int last, int temp[])  
33     {  
34         if(first != last){//将数组分解为1个数为1组,很明显第一个数和最后一个数相等,就表明分解完了       
35             int mid = (first + last) / 2;  
36             resolveSort(arr, first, mid, temp);    //左边的数先分解
37             resolveSort(arr, mid + 1, last, temp); //右边的数分解
38             mergeSort(arr, first, mid, last, temp); //再将二个有序数列合并  
39         }  
40     }  
41     
42     /**
43      * 封装归并排序
44      */
45     public void mergeSort(int[] arr){
46         resolveSort(arr,0,arr.length-1,new int[arr.length]);
47     }
48     
49     @Test            
50     public void testMergeSort(){
51         int[] arr = new int[]{6,1,2,7,9,3,4,5,10,8};
52         mergeSort(arr);
53         System.out.println(Arrays.toString(arr));
54     }


 

希望各位大神,对文中的不足不吝赐教,共同学习,共同进步!!!
原文地址:https://www.cnblogs.com/gongli123/p/6927073.html