排序有内部排序和外部排序之分,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。我们这里说的八大排序算法均为内部排序。
下图为排序算法体系结构图:
1. 直接插入排序(Straight Insertion Sort )
基本思想:将待排序的无序数列看成是一个仅含有一个元素的有序数列和一个无序数列,将无序数列中的元素逐次插入到有序数列中,从而获得最终的有序数列。
算法流程:
1)初始时, a[0]自成一个有序区, 无序区为a[1, ... , n-1], 令i=1;
2)将a[i]并入当前的有序区a[0, ... , i-1];
3)i++并重复2)直到i=n-1, 排序完成。
时间复杂度:O(n^2)。
示意图:初始无序数列为 49, 38, 65, 97, 76, 13, 27 ,49
说明:如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
C++实现源码:
//直接插入排序,版本1 void StraightInsertionSort1(int a[], int n) { int i, j, k; for(i=1; i<n; i++) { //找到要插入的位置 for(j=0; j<i; j++) if(a[i] < a[j]) break; //插入,并后移剩余元素 if(j != i) { int temp = a[i]; for(int k=i-1; k>=j; k--) a[k+1] = a[k]; a[j] = temp; } } PrintDataArray(a, n); }
两种简化版本,推荐第三版本。
//直接插入法,版本2:搜索和后移同时进行 void StraightInsertionSort2(int a[], int n) { int i, j, k; for(i=1; i<n; i++) if(a[i] < a[i-1]) { int temp = a[i]; for(j=i-1; j>=0 && a[j]>temp; j--) a[j+1] = a[j]; a[j+1] = temp; } PrintDataArray(a, n); } //插入排序,版本3:用数据交换代替版本2的数据后移(比较对象只考虑两个元素) void StraightInsertionSort3(int a[], int n) { for(int i=1; i<n; i++) for(int j=i-1; j>=0 && a[j]>a[j+1]; j--) Swap(a[j], a[j+1]); PrintDataArray(a, n); }
2. 希尔排序(Shells Sort)
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序
基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法流程:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
时间复杂度:O(n^2),在元素基本有序的情况下,效率很高。希尔排序是一种不稳定的排序算法。
希尔排序的示例:
C++实现源码:
//希尔排序void ShellSort(int a[], int n) { int i, j, gap; //分组 for(gap=n/2; gap>0; gap/=2) //直接插入排序 for(i=gap; i<n; i++) for(j=i-gap; j>=0 && a[j]>a[j+gap]; j-=gap) Swap(a[j], a[j+gap]); PrintDataArray(a, n); }
通过源代码我们也能看出来,希尔排序就是在直接插入排序的基础上加入了分组策略。
3. 直接选择排序(Straight Selection Sort)
基本思想:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
算法流程:
1)初始时,数组全为无序区a[0, ... , n-1], 令i=0;
2)在无序区a[i, ... , n-1]中选取一个最小的元素与a[i]交换,交换之后a[0, ... , i]即为有序区;
3)重复2),直到i=n-1,排序完成。
时间复杂度分析:O(n^2),直接选择排序是一种稳定的排序算法。
直接选择排序的示例:
C++实现源码:
//直接选择排序 void StraightSelectionSort(int a[], int n) { int i, j, minIndex; for(i=0; i<n; i++) { minIndex=i; for(j=i+1; j<n; j++) if(a[j]<a[minIndex]) minIndex=j; Swap(a[i], a[minIndex]); } PrintDataArray(a, n); }
4. 堆排序(Heap Sort)
5. 冒泡排序(Bubble Sort)
基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。每一趟排序后的效果都是讲没有沉下去的元素给沉下去。
算法流程:
1)比较相邻的两个元素,如果前面的数据大于后面的数据,就将两个数据进行交换;这样对数组第0个元素到第n-1个元素进行一次遍历后,最大的一个元素就沉到数组的第n-1个位置;
2)重复第2)操作,直到i=n-1。
时间复杂度分析:O(n^2),冒泡排序是一种不稳定排序算法。
冒泡排序的示例:
C++实现源码:
//冒泡排序 void BubbleSort(int a[], int n) { int i, j; for(i=0; i<n; i++) //j的起始位置为1,终止位置为n-i for(j=1; j<n-i; j++) if(a[j]<a[j-1]) Swap(a[j-1], a[j]); PrintDataArray(a, n); }
6. 快速排序(Quick Sort)
基本思想:快速排序算法的基本思想为分治思想。
1)先从数列中取出一个数作为基准数;
2)根据基准数将数列进行分区,小于基准数的放左边,大于基准数的放右边;
3)重复分区操作,知道各区间只有一个数为止。
算法流程:(递归+挖坑填数)
1)i=L,j=R,将基准数挖出形成第一个坑a[i];
2)j--由后向前找出比它小的数,找到后挖出此数a[j]填到前一个坑a[i]中;
3)i++从前向后找出比它大的数,找到后也挖出此数填到前一个坑a[j]中;
4)再重复2,3),直到i=j,将基准数填到a[i]。
时间复杂度:O(nlog(n)),但若初始数列基本有序时,快排序反而退化为冒泡排序。
快速排序的示例:
(a)一趟排序的过程:
(b)排序的全过程
C++实现源码:
//快速排序 void QuickSort(int a[], int L, int R) { if(L<R) { int i=L, j=R, temp=a[i]; while(i<j) { //从右向左找小于基准值a[i]的元素 while(i<j && a[j]>=temp) j--; if(i<j) a[i++]=a[j]; //从左向右找大于基准值a[i]的元素 while(i<j && a[i]<temp) i++; if(i<j) a[j--]=a[i]; } //将基准值填入最后的坑中 a[i]=temp; //递归调用,分治法的思想 QuickSort(a, L, i-1); QuickSort(a, i+1, R); } }
7. 归并排序(Merge Sort)
基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
算法流程:(迭代+两个有序数列合并为一个有序数列)
时间复杂度:O(nlog(n)),归并算法是一种稳定排序算法。
归并排序示例:
C++实现源码:
//merge两个有序数列为一个有序数列 void MergeArr(int a[], int first, int mid, int last, int temp[]) { int i = first, j = mid+1; int m = mid, n = last; int k=0; //通过比较,归并数列a和b while(i<=m && j<=n) { if(a[i]<a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } //将数列a或者b剩余的元素直接插入到新数列后边 while(i<=m) temp[k++] = a[i++]; while(j<=n) temp[k++] = a[j++]; for(i=0; i<k; i++) a[first+i] = temp[i]; } //归并排序 void MergeSort(int a[], int first, int last, int temp[]) { if(first<last) { int mid = (first+last)/2; MergeSort(a, first, mid, temp); MergeSort(a, mid+1, last, temp); MergeArr(a, first, mid, last, temp); } }
8. 桶排序(Bucket Sort)
9. 各种排序算法性能比较
1)各种排序的稳定性,时间复杂度和空间复杂度总结:
我们比较时间复杂度函数的情况:
2)时间复杂度来说:
(1)平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
3)稳定性:排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较。
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
4)选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据:
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序:如果内存空间允许且要求稳定性的;
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2)当n较大,内存空间允许,且要求稳定性:归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
4)一般不使用或不直接使用传统的冒泡排序。
5)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。