常见排序算法总结

部分转自 http://blog.csdn.net/whuslei/article/details/6442755

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

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

image

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

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

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

一,冒泡排序:

1、简介:冒泡排序是最简单的排序,是刚学c语言时最早接触到的一个算法。

他的思想就是,对待排序元素的关键字从后往前进行多遍扫描,遇到相邻两个关键字次序与排序规则不符时,就将这两个元素进行交换。这样关键字较小的那个元素就像一个泡泡一样,从最后面冒到最前面来

  2、时间复杂度 
     最好情况下:
正序有序,则只需要比较n次。故,为O(n) 
      最坏情况下:  逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)

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

代码

 1 void BubbleSort(int a[], int n)
 2 {
 3     for(int i = 0 ; i < n; i++)
 4     {
 5         for(int j = n-1; j > i; j--)
 6         {
 7             if(a[j] < a[j - 1])
 8             {
 9                 swap(a[j], a[j - 1]);
10             }
11         }
12     }
13 }

二、插入排序

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

image

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

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

代码

 1 void InsertSort(int a[], int n)
 2 {
 3    int i , j;
 4    for(i = 1 ; i < n; i++)
 5    {
 6        int tmp = a[i];
 7        for(j = i - 1; j >= 0; j--)
 8        {
 9            if(tmp < a[j])
10            {   // 向后移动一位,因为a[i]的值赋给了k,所以直接赋值即可
11                a[j+1] = a[j];
12            }
13            else break;
14        }
15        a[j+1] = tmp;
16    }
17 }

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

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

     例如:将 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排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)

代码:

 1 void ShellSort(int a[], int n, int d)
 2 {
 3    int i,j,k,len;
 4    for( len = d; len >= 1; len--)// 步长
 5    {
 6        for( k = 0 ; k < len  ; k++)// 分组的起始位置
 7        {
 8           // 下面就是插入排序
 9           for(i = k + len ; i < n; i+=len)
10           {
11               int tmp = a[i];
12               for(j = i - len; j >= 0; j-=len)
13               {
14                   if( tmp < a[j])
15                      a[j+len] = a[j];
16                   else break;
17               }
18               a[j+len] = tmp;
19           }
20        }
21    }
22 }

 四、快速排序

快速排序采用了分治算法策略,它是冒泡排序的一种改进。

基本思路是:把待排列的数据分为两个子列,从数列中挑出一个数作为基准,遍历其他数据,把小于它的放前面,大的放在基准的后面。之后,通过递归,将各个子序列划分为更小的序列,直到把小于基准值元素的子数列和大于基准值元素的字数列排序。

快速排序示意图:

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<math.h>
 4 #include<algorithm>
 5 #include<iostream>
 6 using namespace std ;
 7 int a[100];
 8 int LEN;
 9 void QuikSort(int a[], int l, int r)
10 {
11     if(l < r )
12     {
13         int k = a[l];// 取第一个值为标量
14         int head = l;
15         int tail = r;
16         while(head < tail)
17         {
18             // a[tail] >= k 必须是大于等于,否则当含有大量相等元素时,如(2,2,2,2,2)会出现死循环
19             while(head < tail && a[tail] >= k)tail--;
20             a[head] = a[tail];
21             while(head <  tail && a[head] <= k)head++;
22             a[tail] = a[head];
23 
24         }
25         a[head] = k;
26 
27         QuikSort(a, l, head - 1);
28         QuikSort(a, head+1, r);
29 
30     }
31 }
32 int main()
33 {
34 
35     while(~scanf("%d",&LEN))
36     {
37         for(int i  = 0 ; i < LEN; i++)
38             scanf("%d", &a[i]);
39         QuikSort(a, 0, LEN - 1);
40 
41         for(int i = 0; i < LEN; i++)
42             printf("%d ", a[i]);
43 
44     }
45 
46 
47 }

 五、堆排序

建堆是一个从下往上进行“筛选”的过程 (首先要把底部的建成小堆,前面调整是因为只有堆顶,其它都已经是堆了。当我建堆到堆顶是也是从堆顶往下筛选)(所以说建堆大范围是从下往上筛选,在添加该结点时,还得从该节点往下筛选确保添加该节点后还是堆)。
如下图建堆过程:  从97 开始->65->38 ->49这是从下往上(大范围从下往上)。第二个图到65时又 65与13 调整了(从上往下调整)。当到49时也是49<-> 13  <-> 27所以也是从上之下调整(为了确保加入该结点后还是堆)。


 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<math.h>
 4 #include<algorithm>
 5 #include<iostream>
 6 using namespace std ;
 7 int a[100];
 8 int LEN;
 9 // 从底向上逐步调节每一个非叶子节点,使其符合大顶堆或者小顶堆的结构
10 void HeapAdjust(int a[], int i, int n)
11 {
12      int l = 2*i;
13      int r = 2*i + 1;
14      int largest = i;
15      if(l <= n&&a[i] < a[l])
16         largest = l;
17      if(r <= n && a[largest] < a[r])
18         largest = r;
19 
20      if(largest != i)
21      {
22          int tmp = a[i];
23          a[i] = a[largest];
24          a[largest] = tmp;
25          HeapAdjust(a, largest, n);
26 
27      }
28 
29 }
30 void BuilMaxHeap(int a[])
31 {
32     for(int i = LEN/2; i >= 1; i--)
33     {
34         HeapAdjust(a, i, LEN);
35     }
36     printf("first max heap is:
");
37     for(int  i = 1;  i <= LEN; i++)
38             printf("%d ", a[i]);
39     printf("
");
40 
41 }
42 void HeapSort(int a[])
43 {
44     BuilMaxHeap(a);
45     int n = LEN;
46     int tmp;
47     for(int i = LEN; i > 1; i--)
48     {
49         tmp = a[1];
50         a[1] = a[i];
51         a[i] = tmp;
52         HeapAdjust(a, 1, i - 1);
53     }
54 }
55 int main()
56 {
57 
58     while(~scanf("%d",&LEN))
59     {
60         for(int i = 1 ; i <= LEN ; i++)
61         {
62             scanf("%d",&a[i]);
63         }
64         HeapSort(a);
65         printf("after sort:
");
66         for(int  i = 1;  i <= LEN; i++)
67             printf("%d ", a[i]);
68     }
69 
70 
71 }
View Code

六、归并排序

合并排序:

 合并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。合并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。合并排序也叫归并排序。

代码:

 
 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<math.h>
 4 #include<algorithm>
 5 #include <stack>
 6 #include<iostream>
 7 #include <stdexcept>
 8 #define maxn 1200
 9 using namespace std ;
10 void MergeArray(int a[], int first, int mid, int last, int c[])
11 {
12     int i = first;
13     int j = mid + 1;
14     int k = first;
15     while(i <= mid && j <= last)
16     {
17         if(a[i] <= a[j]) c[k++] = a[i++];
18         else c[k++] = a[j++];
19     }
20 
21     while(i <= mid)c[k++] = a[i++];
22     while(j <= last)c[k++] = a[j++];
23 
24     for(int i = first; i <= last; i++)
25         a[i] = c[i];
26 }
27 void MergeSort(int a[], int l, int r, int c[])
28 {
29    if(l >= r )return ;
30    int mid = (l+r)/2;
31    MergeSort(a, l, mid, c);
32 
33    MergeSort(a, mid+1, r, c);
34 
35    MergeArray(a, l, mid, r, c);
36 
37 }
38 int main()
39 {
40     int a[] = {1,4,6,7,9,2,3,5,8,10};
41     int c[100];
42 
43     MergeSort(a, 0, 9, c);
44     for(int i = 0 ; i < 10; i++)
45     {
46         printf("%d ", a[i]);
47     }
48     printf("
");
49 
50 }
View Code

 ,桶排序:

假定:输入是由一个随机过程产生的[0, 1)区间上均匀分布的实数。将区间[0, 1)划分为n个大小相等的子区间(桶),每桶大小1/n:[0, 1/n), [1/n, 2/n), [2/n, 3/n),…,[k/n, (k+1)/n ),…将n个输入元素分配到这些桶中,对桶中元素进行排序,然后依次连接桶输入0 ≤A[1..n] <1辅助数组B[0..n-1]是一指针数组,指向桶(链表)。

他的排序过程如图:

桶排序只是用于关键字取值范围较小的情况,否则会因为所需箱子的数目太多而导致资源的浪费。这种排序的使用价值不大,他一般用于基数排序的一个中间过程。

2,基数排序:

基数排序是桶排序的一种改进和推广。它的基本思想是,先设立r个队列,队列编号分别为0~r-1,(r为关键字的基数),然后按照下面的规则对关键字进行“分配”和“收集”。

1, 按照最低有效位的值,把n个关键字分配到上述的r个队列里,然后从小到达将各队列中关键字收集起来。

2, 再按低次有效位的值把刚刚收集起来的关键字分配到r个队列中,重复收集工作。

3, 重复上述分配和收集工作,直到最高的有效位。(也就是说,如果数位为d,则需要重复进行d次。d由所有元素中最长的一个元素的位数计量。)

图示如下:

上图过程,就是先按个位分配然后收集,再按十位,百位分配和收集,最后就得出排序结果。

在C++中可以使用库函数lexicongraphical_compare()进行字典次序比较。

每一趟分配的时间是O(n),所以总时间的开销为O(d(n+r)) = O(n),通常d,r为常数。空间负复杂度为O(n+r)。基数排序使用于采用链式结构存储结构的排序。

计数排序:

计数排序非常基础,他的主要目的是对整数排序并且会比普通的排序算法性能更好。例如,输入{1, 3, 5, 2, 1, 4}给计数排序,会输出{1, 1, 2, 3, 4, 5}。这个算法由以下步骤组成:

  1. 初始化一个计数数组,大小是输入数组中的最大的数。
  2. 遍历输入数组,遇到一个数就在计数数组对应的位置上加一。例如:遇到5,就将计数数组第五个位置的数加一。
  3. 把计数数组直接覆盖到输出数组(节约空间)。
  • 例子

输入{3, 4, 3, 2, 1},最大是4,数组长度是5。

建立计数数组{0, 0, 0, 0}。

遍历输入数组:

{3, 4, 3, 2, 1} -> {0, 0, 1, 0}
{3, 4, 3, 2, 1} -> {0, 0, 1, 1}
{3, 4, 3, 2, 1} -> {0, 0, 2, 1}
{3, 4, 3, 2, 1} -> {0, 1, 2, 1}
{3, 4, 3, 2, 1} -> {1, 1, 2, 1}

原文地址:https://www.cnblogs.com/acSzz/p/5264056.html