参考链接
排序分类(9)
1. 插入排序
直接插入排序
-
难度:简单 必须掌握
-
思路:将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表
-
图示:
-
算法复杂度:O(n^2) ;稳定算法
-
代码:
void InsertSort(int a[],int n) { for(int i=1;i<n;i++)//若第i个元素大于i-1元素,直接插入;小于的话,移动有序表后插入 { if(a[i]<a[i-1]) { int j=i-1;//记录哨兵前角标 int x=a[i];//复制为哨兵,即存储待排序元素 a[i] = a[i-1];//先后移一个元素 while(x<a[j] && j>=0)//查找在有序表的插入位置 { a[j+1]=a[j];//元素后移 j--; } a[j+1]=x;//插入到正确位置 } } }
-
记忆要点:
- for if while结构
- 记录哨兵前项角标-复制哨兵-先移动一个-查找插入位置-元素后移-插入正确位置
希尔排序(缩小增量排序)
-
难度:较易 操作方法必须掌握
-
思路:直接插入排序的变种;先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序
-
操作方法:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;简单增量序列:d = {n/2 ,n/4, n/8 …1} n为要排序数的个数
- 按增量序列个数k,对序列进行k趟排序
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序;仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度
-
算法复杂度:O(n^(1.3—2)) ;非稳定算法
-
代码:
void ShellInsertSort(int a[],int n,int dk) { for(int i=dk;i<n;i++) { if(a[i]<a[i-dk]) { int j=i-dk; int x=a[i]; a[i] = a[i-dk]; while(x<a[j]) { a[j+dk]=a[j]; j-=dk;; } a[j+dk]=x; } } } void ShellSort(int a[],int n) { int dk = n/2; while(dk>=1) { ShellInsertSort(a,n,dk); dk/=2; } }
-
记忆要点:
- 直接插入排序结构
- 与直接插入排序对比,仅仅是把直接插入排序中的增量1改为了通用增量dk
- 增加增量序列
2. 选择排序
简单选择排序
-
难度:简单 必须掌握
-
思路:在要排序的一组数中,找第一大与第一个元素交换、第二大与第二个元素交换、第三大与第三个元素交换,依次类推…)
-
操作方法:
-
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
-
第二趟,从第二个记录开始的n-1个记录中再选出关键码最小的记录与第二个记录交换;
-
第 i 趟,则从第i 个记录开始的n-i+1个记录中选出关键码最小的记录与第i 个记录交换,
直到整个序列按关键码有序
-
-
图示:
-
算法复杂度:O(n^2) 虽然选择排序和冒泡排序的时间复杂度一样,但实际上,选择排序进行的交换操作很少,最多会发生n-1次交换,而冒泡排序最坏的情况下要发生[n
^
2/2]交换操作。从这个意义上讲,交换排序的性能略优于冒泡排序;非稳定算法 -
代码:
//最小值键值 int SelectMinKey(int a[],int n,int i) { int k=i; for(int j=i+1;j<n;j++) if(a[k]>a[j]) k=j; return k; } void SelectSort(int a[],int n) { int key,tmp; for(int i=0;i<n;i++) { key = SelectMinKey(a,n,i);//选择最小的元素 if(key!=i)//最小元素与第i位置元素互换 { tmp=a[i]; a[i]=a[key]; a[key]=tmp; } } }
-
记忆要点:
- 找最值:for if结构
- 交换:for swap结构
-
改进—二元选择排序
-
思路:改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数;改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可;复杂度仍然是O(n^2)
-
代码:
void SelectSort(int a[],int n) { int i,j,mintmp,maxtmp,max,min; for(i=0;i<n/2;i++) { min=i,max=i;//先将最小值与最大值下标指向未排序的第一个数。 for (j=i+1;j<n-i;j++) { if (a[j]<a[min]) { min = j; continue; } if (a[j]>a[max]) max = j; } //最小值是否已经在正确的位置上了 if (min != i) { mintmp = a[min]; a[min] = a[i]; a[i] = mintmp; } //可能出现最大值存储在a[i],那么经过上一步交换,a[i]上存储的最大值已经被换到了a[min]所在位置. if (max == i) max = min; //最大值是否已经在正确的位置上了 if (max != n-i-1) { maxtmp = a[max]; a[max] = a[n-i-1]; a[n-i-1] = maxtmp; } } } //最后的三种情况推理比较重要
-
堆排序
-
难度:较难 需要掌握
-
思路:堆排序是一种树形选择排序,是对直接选择排序的有效改进;需要掌握二叉树;
-
操作方法:
-
图示:
-
算法复杂度:
-
代码:
-
记忆要点:
3. 交换排序
冒泡排序
-
难度:简单 必须掌握
-
思路:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换
-
图示:
-
算法复杂度:O(n^2);稳定算法
-
代码:
void swap(int *m,int *n) { *m = *m^*n; *n = *m^*n; *m = *m^*n; } void BubbleSort(int a[], int n) { for(int j=0;j<n-1;j++)// 每次最大元素就像气泡一样"浮"到数组的最后 { for(int i=0;i<n-1-j;i++)// 依次比较相邻的两个元素,使较大的那个向后移 { if (a[i]>a[i+1]) swap(&a[i],&a[i+1]); } } } //外侧循环j加一次,内侧循环i就要少一次
-
记忆要点:
- for for if swap结构
-
冒泡改进
- 传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) ,从而使排序趟数几乎减少了一半
void BubbleSort(int a[],int n) { int low = 0; int high = n-1; int j; while(low<high) { for(j=low;j<high;j++) if(a[j]>a[j+1]) swap(&a[j],&a[j+1]); --high; for(j=high;j>low;j--) if(a[j]<a[j-1]) swap(&a[j],&a[j-1]); ++low; } }
- 记忆要点:while: for if for if结构
快速排序
-
难度:较难 必须掌握
-
思路:
-
选择一个基准元素,通常选择第一个元素或者最后一个元素
-
通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的元素值比基准值大
-
此时基准元素在其排好序后的正确位置
-
然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序
-
-
图示:
-
算法复杂度:O(nlogn);不稳定
-
代码:
void swap(int *m,int *n) { *m = *m^*n; *n = *m^*n; *m = *m^*n; } void QuickSort(int a[],int left,int right) { int i,j,tmp; if(left>right) return;//递归基 tmp=a[left];//基准数取最左边 i=left; j=right; while(i!=j) { //顺序很重要,要先从右边开始找(最后交换基准时换过去的数要保证比基准小) while(a[j]>=tmp && i<j) j--; //再找右边的 while(a[i]<=tmp && i<j) i++; //交换位置 if(i<j) swap(&a[i],&a[j]); } //最终将基准数归位 a[left]=a[i]; a[i]=tmp; QuickSort(a,left,i-1);//继续处理左边 QuickSort(a,i+1,right);//继续处理右边 } void AdjustQuickSort(int a[],int n) { QuickSort(a,0,n-1); }
-
记忆要点:
- 记基准值+while{while;while;if}结构
4. 归并排序
-
难度:较难
-
思路:归并排序法是将两个或两个以上有序表合并成一个新的有序表;即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序子序列合并为整体有序序列
-
操作方法:
-
图示:
-
算法复杂度:
-
代码:
-
记忆要点:
5. 基数排序
- 难度:很难
- 思路:
- 操作方法:
- 图示:
- 算法复杂度:
- 代码:
- 记忆要点:
6. 对比总结
- 算法复杂度对比
-
排序算法的稳定性
-
若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的
-
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较
-
只有直接插入、冒泡、归并和基数排序算法是稳定的!
-
-
各算法特点
- 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序:如果内存空间允许且要求稳定性时使用;
- 归并排序:它有一定数量的数据移动,所以我们可能与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高
-
排序算法选择
- 设待排序元素的个数为n
-
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序;
-
当n较大,内存空间允许,且要求稳定性选择归并排序
-
当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
-
一般不使用或不直接使用传统的冒泡排序
-
基数排序 它是一种稳定的排序算法,但有一定的局限性:
- 关键字可分解
- 记录的关键字位数较少,如果密集更好
- 如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序
-
菜鸟必须掌握的算法:插入排序(直接&希尔)+ 一般选择排序 + 交换排序(冒泡+快排)
较难算法后续再完善!