部分转自 http://blog.csdn.net/whuslei/article/details/6442755
排序算法经过了很长时间的演变,产生了很多种不同的方法。对于初学者来说,对它们进行整理便于理解记忆显得很重要。每种算法都有它特定的使用场合,很难通用。因此,我们很有必要对所有常见的排序算法进行归纳。
我不喜欢死记硬背,我更偏向于弄清来龙去脉,理解性地记忆。比如下面这张图,我们将围绕这张图来思考几个问题。
上面的这张图来自一个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]的后面,插入前需要移动元素。
2、算法时间复杂度。
最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n)
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n2)
平均情况下:O(n2)
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] }
说明: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 }
六、归并排序
合并排序:
合并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(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 }
,桶排序:
假定:输入是由一个随机过程产生的[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}。这个算法由以下步骤组成:
- 初始化一个计数数组,大小是输入数组中的最大的数。
- 遍历输入数组,遇到一个数就在计数数组对应的位置上加一。例如:遇到5,就将计数数组第五个位置的数加一。
- 把计数数组直接覆盖到输出数组(节约空间)。
- 例子
输入{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}