常见排序算法小结

 排序算法经过了很长时间的演变,产生了很多种不同的方法。对于初学者来说,对它们进行整理便于理解记忆显得很重要。每种算法都有它特定的使用场合,很难通用。因此,我们很有必要对所有常见的排序算法进行归纳。

     我不喜欢死记硬背,我更偏向于弄清来龙去脉,理解性地记忆。比如下面这张图,我们将围绕这张图来思考几个问题。

image

     上面的这张图来自一个PPT。它概括了数据结构中的所有常见的排序算法。现在有以下几个问题:

     1、每个算法的思想是什么?
     2、每个算法的稳定性怎样?时间复杂度是多少?
     3、在什么情况下,算法出现最好情况 or 最坏情况?
     4、每种算法的具体实现又是怎样的?

     这个是排序算法里面最基本,也是最常考的问题。下面是我的小结。

一、直接插入排序(插入排序)。

     1、算法的伪代码(这样便于理解):    

     INSERTION-SORT (A, n)             A[1 . . n]
     for j ←2 to n
          do key ← A[ j]
          i ← j – 1
          while i > 0 and A[i] > key
               do A[i+1] ← A[i]
                    i ← i – 1
          A[i+1] = key

     2、思想:如下图所示,每次选择一个元素K插入到之前已排好序的部分A[1…i]中,插入过程中K依次由后向前与A[1…i]中的元素进行比较。若发现发现A[x]>=K,则将K插入到A[x]的后面,插入前需要移动元素。

image

     3、算法时间复杂度。 
        最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n) 
        最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n­2
        平均情况下:O(n­2)

     4、稳定性。 
     理解性记忆比死记硬背要好。因此,我们来分析下。稳定性,就是有两个相同的元素,排序先后的相对位置是否变化,主要用在排序时有多个排序规则的情况下。在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面(没有必要插到K1的前面,这样做还需要移动!!),因此,插入排序是稳定的。

     5、代码(java版)
          

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.yonyou.test;
 
/**
  * 内部排序算法之直接插入排序
  * 默认按照从小到大进行排序操作
  * @author 小浩
  * @创建日期 2015-3-27
  */
public class Test{
     public static void main(String[] args) {
    //需要进行排序的数组
     int [] array= new int []{ 8 , 3 , 2 , 1 , 7 , 4 , 6 , 5 };
      //输出原数组的内容
     printResult(array);
     //直接插入排序操作
     directInsertSort(array);
     //输出排序后的相关结果
     printResult(array);
     }
     
     
    
     /**
      * 直接插入排序的方法
      * @param array
      */
     private static void directInsertSort( int [] array) {
        for ( int i= 0 ;i<array.length;i++)
        {
             for ( int j= 0 ;j<i;j++)
             {
                 if (array[i]<array[j])
                 {
                   int temp=array[i];
                   System.arraycopy(array,j,array,j+ 1 ,i-j);
                   array[j]=temp;
                 }
             }
        }
     }
     
 
 
     
 
     /**
      *                                       
      * 输出相应数组的结果
      * @param array
      */
     private static void printResult( int [] array) {
        for ( int value:array)    
            System.out.print( " " +value+ " " );
       System.out.println();
     }
 
     /**
      * 交换数组中两个变量的值
      * @param array
      * @param i
      * @param j
      */
     private static void swap( int [] array, int i, int j){
         int temp=array[i];
         array[i]=array[j];
         array[j]=temp;
     }
}

二、希尔排序(插入排序)

     1、思想:希尔排序也是一种插入排序方法,实际上是一种分组插入方法。先取定一个小于n的整数d1作为第一个增量,把表的全部记录分成d1个组,所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序;然后,取第二个增量d2(<d1),重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。     <="" font="">

     例如:将 n 个记录分成 d 个子序列:
       { R[0],   R[d],     R[2d],…,     R[kd] }
       { R[1],   R[1+d], R[1+2d],…,R[1+kd] }
         …
       { R[d-1],R[2d-1],R[3d-1],…,R[(k+1)d-1] }

     image
     说明:d=5 时,先从A[d]开始向前插入,判断A[d-d],然后A[d+1]与A[(d+1)-d]比较,如此类推,这一回合后将原序列分为d个组。<由后向前>

     2、时间复杂度。 
     最好情况
:由于希尔排序的好坏和步长d的选择有很多关系,因此,目前还没有得出最好的步长如何选择(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。 
     最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。 
     平均情况下:O(N*logN)

     3、稳定性。 
     由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)

     4、代码(java版)

  1. package sort;  
  2.   
  3. public class ShellSortTest {  
  4.     public static int count = 0;  
  5.   
  6.     public static void main(String[] args) {  
  7.   
  8.         int[] data = new int[] { 536219487 };  
  9.         print(data);  
  10.         shellSort(data);  
  11.         print(data);  
  12.   
  13.     }  
  14.   
  15.     public static void shellSort(int[] data) {  
  16.         // 计算出最大的h值  
  17.         int h = 1;  
  18.         while (h <= data.length / 3) {  
  19.             h = h * 3 + 1;  
  20.         }  
  21.         while (h > 0) {  
  22.             for (int i = h; i < data.length; i += h) {  
  23.                 if (data[i] < data[i - h]) {  
  24.                     int tmp = data[i];  
  25.                     int j = i - h;  
  26.                     while (j >= 0 && data[j] > tmp) {  
  27.                         data[j + h] = data[j];  
  28.                         j -= h;  
  29.                     }  
  30.                     data[j + h] = tmp;  
  31.                     print(data);  
  32.                 }  
  33.             }  
  34.             // 计算出下一个h值  
  35.             h = (h - 1) / 3;  
  36.         }  
  37.     }  
  38.   
  39.     public static void print(int[] data) {  
  40.         for (int i = 0; i < data.length; i++) {  
  41.             System.out.print(data[i] + "\t");  
  42.         }  
  43.         System.out.println();  
  44.     }  
  45.   
  46. }  
          

三、冒泡排序(交换排序)

       1、基本思想:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小的记录如气泡一般逐渐往上“漂浮”直至“水面”。
       
image      2、时间复杂度 
     最好情况下:
正序有序,则只需要比较n次。故,为O(n) 
      最坏情况下:  逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)

      3、稳定性 
      排序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的

      4、代码(java版)

package 冒泡排序;
import java.util.Arrays;
/**
 * 冒泡排序改进版
 * @author mmz
 *
 */
public class BubbleSort1 {
    public static void BubbleSort(int[] arr) {
        boolean flag = flase;
        while(!flag){
            int temp;//定义一个临时变量
            for(int i=0;i<arr.length-1;i++){//冒泡趟数,n-1趟
                for(int j=0;j<arr.length-i-1;j++){
                    if(arr[j+1]<arr[j]){
                        temp = arr[j];
                        arr[j] = arr[j+1];
                        arr[j+1] = temp;
                        flag = true;
                    }
                }
                if(!flag){
                    break;//若果没有发生交换,则退出循环
                }
            }
        }
    }
    public static void main(String[] args) {
        int arr[] = new int[]{1,6,2,2,5};
        BubbleSort.BubbleSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

四、快速排序(交换排序)

     1、思想:它是由冒泡排序改进而来的。在待排序的n个记录中任取一个记录(通常取第一个记录),把该记录放入适当位置后,数据序列被此记录划分成两部分。所有关键字比该记录关键字小的记录放置在前一部分,所有比它大的记录放置在后一部分,并把该记录排在这两部分的中间(称为该记录归位),这个过程称作一趟快速排序。

image           说明:最核心的思想是将小的部分放在左边,大的部分放到右边,实现分割。       
     2、算法复杂度 
      最好的情况下
:因为每次都将序列分为两个部分(一般二分都复杂度都和logN相关),故为 O(N*logN) 
      最坏的情况下:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)

      3、稳定性 
      由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的!

      4、代码(java版)

public class QuickSort {
    public static void sort(int a[], int low, int hight) {
        int i, j, index;
        if (low > hight) {
            return;
        }
        i = low;
        j = hight;
        index = a[i]; // 用子表的第一个记录做基准
        while (i < j) { // 从表的两端交替向中间扫描
            while (i < j && a[j] >= index)
                j--;
            if (i < j)
                a[i++] = a[j];// 用比基准小的记录替换低位记录
            while (i < j && a[i] < index)
                i++;
            if (i < j) // 用比基准大的记录替换高位记录
                a[j--] = a[i];
        }
        a[i] = index;// 将基准数值替换回 a[i]
        sort(a, low, i - 1); // 对低子表进行递归排序
        sort(a, i + 1, hight); // 对高子表进行递归排序

    }

    public static void quickSort(int a[]) {
        sort(a, 0, a.length - 1);
    }

    public static void main(String[] args) {

        int a[] = { 49, 38, 65, 97, 76, 13, 27, 49 };
        quickSort(a);
        System.out.println(Arrays.toString(a));
    }
}

五、直接选择排序(选择排序)

      1、思想:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。具体做法是:选择最小的元素与未排序部分的首部交换,使得序列的前面为有序。 
image      2、时间复杂度。
      最好情况下:
交换0次,但是每次都要找到最小的元素,因此大约必须遍历N*N次,因此为O(N*N)。减少了交换次数!
      最坏情况下,平均情况下:O(N*N)

      3、稳定性
      由于每次都是选取未排序序列A中的最小元素x与A中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!

      4、代码(java版)

package Algorithm;

public class InsertSort {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] a={11,10,55,78,100,111,45,56,79,90,345,1000};//创建一个数组a
        System.out.println("排序之前:");
        InsertSort.output(a);
        System.out.println();
        InsertSort.Sort(a);
        System.out.println("排序之后:");
        InsertSort.output(a);
    }
    //插入排序
    public static void Sort(int[] arr){
        for(int i=1;i<arr.length;i++){
            int tempdata = arr[i];
            int j;
            for(j=i-1;j>=0;j--){
                 if(arr[j]>tempdata){
                     arr[j+1] = arr[j];
                 }else{
                     break;
                 }
            }
            arr[j+1] = tempdata;
        }
    }
    //输出打印
    public static void output(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+",");
        }
    }
}

六、堆排序

     1、思想:利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的记录。也就是说,以最小堆为例,根节点为最小元素,较大的节点偏向于分布在堆底附近。
image      2、算法复杂度
         最坏情况下,接近于最差情况下:O(N*logN),因此它是一种效果不错的排序算法。

      3、稳定性
         堆排序需要不断地调整堆,因此它是一种不稳定的排序

      4、代码(java版,看代码后更容易理解!)     

  1. public class HeapSortTest {  
  2.   
  3.     public static void main(String[] args) {  
  4.         int[] data5 = new int[] { 536219487 };  
  5.         print(data5);  
  6.         heapSort(data5);  
  7.         System.out.println("排序后的数组:");  
  8.         print(data5);  
  9.     }  
  10.   
  11.     public static void swap(int[] data, int i, int j) {  
  12.         if (i == j) {  
  13.             return;  
  14.         }  
  15.         data[i] = data[i] + data[j];  
  16.         data[j] = data[i] - data[j];  
  17.         data[i] = data[i] - data[j];  
  18.     }  
  19.   
  20.     public static void heapSort(int[] data) {  
  21.         for (int i = 0; i < data.length; i++) {  
  22.             createMaxdHeap(data, data.length - 1 - i);  
  23.             swap(data, 0, data.length - 1 - i);  
  24.             print(data);  
  25.         }  
  26.     }  
  27.   
  28.     public static void createMaxdHeap(int[] data, int lastIndex) {  
  29.         for (int i = (lastIndex - 1) / 2; i >= 0; i--) {  
  30.             // 保存当前正在判断的节点  
  31.             int k = i;  
  32.             // 若当前节点的子节点存在  
  33.             while (2 * k + 1 <= lastIndex) {  
  34.                 // biggerIndex总是记录较大节点的值,先赋值为当前判断节点的左子节点  
  35.                 int biggerIndex = 2 * k + 1;  
  36.                 if (biggerIndex < lastIndex) {  
  37.                     // 若右子节点存在,否则此时biggerIndex应该等于 lastIndex  
  38.                     if (data[biggerIndex] < data[biggerIndex + 1]) {  
  39.                         // 若右子节点值比左子节点值大,则biggerIndex记录的是右子节点的值  
  40.                         biggerIndex++;  
  41.                     }  
  42.                 }  
  43.                 if (data[k] < data[biggerIndex]) {  
  44.                     // 若当前节点值比子节点最大值小,则交换2者得值,交换后将biggerIndex值赋值给k  
  45.                     swap(data, k, biggerIndex);  
  46.                     k = biggerIndex;  
  47.                 } else {  
  48.                     break;  
  49.                 }  
  50.             }  
  51.         }  
  52.     }  
  53.   
  54.     public static void print(int[] data) {  
  55.         for (int i = 0; i < data.length; i++) {  
  56.             System.out.print(data[i] + "\t");  
  57.         }  
  58.         System.out.println();  
  59.     }  
  60.   
  61. }  
          

七、归并排序

      1、思想:多次将两个或两个以上的有序表合并成一个新的有序表。
image       2、算法时间复杂度
          最好的情况下
:一趟归并需要n次,总共需要logN次,因此为O(N*logN)
          最坏的情况下,接近于平均情况下,为O(N*logN)
          说明:对长度为n的文件,需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

      3、稳定性
         归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。
      4、缺点是,它需要O(n)的额外空间。但是很适合于多链表排序。
      5、代码(java版)

  1. public class MergeSortTest {  
  2.   
  3.     public static void main(String[] args) {  
  4.         int[] data = new int[] { 536219487 };  
  5.         print(data);  
  6.         mergeSort(data);  
  7.         System.out.println("排序后的数组:");  
  8.         print(data);  
  9.     }  
  10.   
  11.     public static void mergeSort(int[] data) {  
  12.         sort(data, 0, data.length - 1);  
  13.     }  
  14.   
  15.     public static void sort(int[] data, int left, int right) {  
  16.         if (left >= right)  
  17.             return;  
  18.         // 找出中间索引  
  19.         int center = (left + right) / 2;  
  20.         // 对左边数组进行递归  
  21.         sort(data, left, center);  
  22.         // 对右边数组进行递归  
  23.         sort(data, center + 1, right);  
  24.         // 合并  
  25.         merge(data, left, center, right);  
  26.         print(data);  
  27.     }  
  28.   
  29.     /** 
  30.      * 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序 
  31.      *  
  32.      * @param data 
  33.      *            数组对象 
  34.      * @param left 
  35.      *            左数组的第一个元素的索引 
  36.      * @param center 
  37.      *            左数组的最后一个元素的索引,center+1是右数组第一个元素的索引 
  38.      * @param right 
  39.      *            右数组最后一个元素的索引 
  40.      */  
  41.     public static void merge(int[] data, int left, int center, int right) {  
  42.         // 临时数组  
  43.         int[] tmpArr = new int[data.length];  
  44.         // 右数组第一个元素索引  
  45.         int mid = center + 1;  
  46.         // third 记录临时数组的索引  
  47.         int third = left;  
  48.         // 缓存左数组第一个元素的索引  
  49.         int tmp = left;  
  50.         while (left <= center && mid <= right) {  
  51.             // 从两个数组中取出最小的放入临时数组  
  52.             if (data[left] <= data[mid]) {  
  53.                 tmpArr[third++] = data[left++];  
  54.             } else {  
  55.                 tmpArr[third++] = data[mid++];  
  56.             }  
  57.         }  
  58.         // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)  
  59.         while (mid <= right) {  
  60.             tmpArr[third++] = data[mid++];  
  61.         }  
  62.         while (left <= center) {  
  63.             tmpArr[third++] = data[left++];  
  64.         }  
  65.         // 将临时数组中的内容拷贝回原数组中  
  66.         // (原left-right范围的内容被复制回原数组)  
  67.         while (tmp <= right) {  
  68.             data[tmp] = tmpArr[tmp++];  
  69.         }  
  70.     }  
  71.   
  72.     public static void print(int[] data) {  
  73.         for (int i = 0; i < data.length; i++) {  
  74.             System.out.print(data[i] + "\t");  
  75.         }  
  76.         System.out.println();  
  77.     }  
  78.   
  79. }  

八、基数排序

      1、思想:它是一种非比较排序。它是根据位的高低进行排序的,也就是先按个位排序,然后依据十位排序……以此类推。示例如下:
image
image        2、算法的时间复杂度
       分配需要O(n),收集为O(r),其中r为分配后链表的个数,以r=10为例,则有0~9这样10个链表来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即"分配-收集"的趟数。因此时间复杂度为O(d*(n+r))。

       3、稳定性
          基数排序过程中不改变元素的相对位置,因此是稳定的!

       4、适用情况:如果有一个序列,知道数的范围(比如1~1000),用快速排序或者堆排序,需要O(N*logN),但是如果采用基数排序,则可以达到O(4*(n+10))=O(n)的时间复杂度。算是这种情况下排序最快的!!

       5、代码(java版)

public  class  RadixSort
{
     public  static  void  sort( int [] number,  int  d)  //d表示最大的数有多少位
     {
         intk =  0 ;
         intn =  1 ;
         intm =  1 //控制键值排序依据在哪一位
         int [][]temp = newint[ 10 ][number.length];  //数组的第一维表示可能的余数0-9
         int []order = newint[ 10 ];  //数组orderp[i]用来表示该位是i的数的个数
         while (m <= d)
         {
             for (inti =  0 ; i < number.length; i++)
             {
                 intlsd = ((number[i] / n) %  10 );
                 temp[lsd][order[lsd]] = number[i];
                 order[lsd]++;
             }
             for (inti =  0 ; i <  10 ; i++)
             {
                 if (order[i] !=  0 )
                     for (intj =  0 ; j < order[i]; j++)
                     {
                         number[k] = temp[i][j];
                         k++;
                     }
                 order[i] =  0 ;
             }
             n *=  10 ;
             k =  0 ;
             m++;
         }
     }
     public  static  void  main(String[] args)
     {
         int []data =
         { 73 22 93 43 55 14 28 65 39 81 33 100 };
         RadixSort.sort(data,  3 );
         for (inti =  0 ; i < data.length; i++)
         {
             System.out.print(data[i] +  "" );
         }
     }
}

     总结: 每种算法都要它适用的条件,本文也仅仅是回顾了下基础。如有不懂的地方请参考课本。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值