简单选择排序流程图_排序算法:直接插入排序,希尔排序,冒泡排序,快速排序,简单选择排序,归并排序,堆排序...

今天来总结一下常用的排序算法。排序算法们需要掌握的知识点大概有:算法的原理,算法的编码实现,算法的时空复杂度的计算和记忆,何时出现最差时间复杂度,以及是否稳定,何时不稳定。

目录

  • 整体记忆

  • 快速排序

  • 堆排序

    • 建堆时间复杂度推导

    • 建堆

    • 删除

    • 插入

    • 堆排序

  • 归并排序

  • 冒泡排序

  • 直接插入排序

  • 简单选择排序

  • 希尔排序

整体记忆

名称时间复杂度何时最差是否稳定
快速排序平均O(nlogn),最坏O(n^2),最好O(nlogn)数组本身有序不稳定
堆排序都O(nlogn)-不稳定
归并排序都O(nlogn) 空间O(n)-稳定
冒泡排序平均O(n2),最坏O(n2),最好O(n)数组逆序稳定
直接插入排序平均O(n2),最坏O(n2),最好O(n)数组逆序稳定
简单选择排序都O(n^2)-不稳定
希尔排序O(n1.3)-O(n2)-不稳定

快速排序

本文不从分类顺序和发展角度出发,而从重要程度和面试出现概率总结各算法。
那首当其冲的就是快速排序了。
快速排序,是交换排序的一种,思想是每轮把一个数放到它该在的位置(左边的元素都比它小,右边都比它大)。即左右各一个指针,先从右边往左找,找到第一个比pivot(基准)小的值,停下,左指针往右找第一个比pivot大的元素,停下,交换两个元素,之后继续,直到两个指针相遇,右指针的位置的元素应小于pivot的值(右指针先走的原因),即为pivot应在的位置,右指针元素与pivot交换即可。具体:

	public static void QuickSort(int[] a) {
Partition(a,0,a.length-1);
}
private static void Partition(int[] a , int start ,int end) {
if(end<=start) return;
int l = start+1;
int r = end;
while(l<=r) {
while(r>=l&&a[r]>=a[start]) r--;
while(l<=r&&a[l]<=a[start]) l++;
if(lint temp = a[l];a[l]=a[r];a[r]=temp;}else {int temp = a[r];a[r]=a[start];a[start]=temp;}
}
Partition(a,start,r-1);
Partition(a,r+1,end);
}
def quickSort(nums):
if nums is None or len(nums)<1:
return []
partition(nums,0,len(nums)-1)
return nums
def partition(nums,start,end):
if start>=end:
return
l = start+1
r = end
while l<=r:
while l<=r and nums[r]>=nums[start]:
r-=1
while l<=r and nums[l]<=nums[start]:
l+=1
if l<r:
temp = nums[l];nums[l]=nums[r];nums[r]=temp
else:
temp = nums[start];nums[start]=nums[r];nums[r]=temp
partition(nums,start,r-1)
partition(nums,r+1,end)

时间复杂度平均情况O(nlogn),最好O(nlogn),最坏O(n^2)。证明见快速排序时间复杂度为O(n×log(n))的证明。
其中最好情况出现在,每次划分都二分,这样的话递归树的深度就是logn,每层都要遍历所有的元素,故O(nlogn)。
最坏情况出现在,数组本身就有序,这样递归树是一棵斜树,深度n-1。
不稳定。因为右指针从右往左找到第一个比pivot小的数,也就是比pivot小的数里最靠右的数,这时如果与pivot交换,就会不稳定。如:211113456。2会和1交换,1就跑到了和它相等的一众1的左边。

堆排序

堆(heap)又被为优先队列(priority queue)。尽管名为优先队列,但堆并不是队列。堆是一种数据结构,在堆中,我们取出的元素是堆中最小/最大的(小根堆/大根堆)。堆的经典实现方法是使用完全二叉树(由于建堆和插入删除操作都可以保证堆的平衡性,所以堆一直会是一棵完全二叉树),完全二叉树又可以用数组来替代。
堆有三种基本操作:建堆、插入和删除,删除就是弹出堆中最小/最大的元素。

建堆

建堆是一个递归的过程:
0.我们的数据构成数组a[0,1,..,n-1],共n个元素
1.我们用该数组构建一个完全二叉树(用数组实现的完全二叉树的话其实不用变动)
2.从树的右下角的第一个非叶子节点开始从右下向左上进行从上至下的调整(从(n-1-1)/2到0进行从上至下调整),每次的调整都是从上至下的(因为对(n-1-1)/2调整后,下面的子树可能不满足堆的性质,需要继续调整)。
3.具体的,对节点k的调整为,(默认小根堆,java中优先队列就默认小根堆):a[k]和两个叶子节点a[2k+1],a[2k+2],若a[k]最小,停止调整;否则,和a[2k+1],a[2k+2]中较小的值互换。若互换,则k移位到了叶子上,继续对新的三个节点递归进行以上调整,直到无需调整或叶子节点。
建堆时间复杂度O(n)。

建堆时间复杂度推导

假如有N个节点,那么高度为H=logN,最后一层每个父节点最多只需要下调1次,倒数第二层最多只需要下调2次,顶点最多需要下调H次,而最后一层父节点共有2(H-1)个,倒数第二层公有2(H-2),顶点只有1(2^0)个,所以总共的时间复杂度为s = 1 * 2^(H-1) + 2 * 2^(H-2) + ... + (H-1) * 2^1 + H * 2^0
将H代入后s= 2N - 2 - log2(N),近似的时间复杂度就是O(N)。
另外一个更细致的解答:
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
S = 2^(k-2) * 1 + 2(k-3)*2.....+2*(k-2)+2(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)
除最后一项外,就是一个等比数列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q);
S = 2^k -k -1;又因为k为完全二叉树的深度,所以 (2^k) <= n < (2^k -1 ),总之可以认为:k = logn (实际计算得到应该是 log(n+1) < k <= logn );
综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)
时间复杂度分析参考

删除

直接弹出堆顶元素,之后把堆尾元素置于堆顶,对这个新的堆定元素进行从上至下的调整。(下沉)
时间复杂度很直观,就是O(logn)。

插入

将插入元素放于堆尾,进行从下至上(这里和上面的调整不同)的调整(上浮)。
1.对于该节点k,找到其双亲结点(k-1)/2,若a[k]时间复杂度很直观,就是O(logn)。
插入删除参考

堆排序

堆排序就是利用堆进行的排序算法,每次将堆顶元素和堆尾元素互换,然后堆长度减一,之后做类似删除操作的下沉重新建堆,递归即可得到降序排列的数组。
时间复杂度都是O(nlogn)。
不稳定。因为(小根堆)建堆调整时可能会将更靠右的相等元素调整至堆顶,从而导致更靠右的先输出。
具体代码有空再写,可以参考:堆排序(这个就挺好)或堆排序或堆排序。

归并排序

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。(快排的Partition也使用了分治的思想)e9d7cff238da4505b38e59bba033abf4.png
即在“分”的阶段,把数组逐步分解为单个元素组成的数组,那么这个数组就必然是有序的,这个过程在代码中也就是一个递归调用的过程。然后再逐步合成大的有序数组。
这样每层都对比n次,一共logn层,所以各种情况下时间复杂度都是O(nlogn)。
同时空间复杂度为O(n)。
稳定。

	public static void MergeSort(int[] a) {
int[] temp = new int[a.length];
MergeSortHelper(a,temp,0,a.length-1);
}
private static void MergeSortHelper(int[] a,int[] temp,int l,int r) {
if(lint m = (l+r)/2;
MergeSortHelper(a,temp,l,m);
MergeSortHelper(a,temp,m+1,r);
//merge
int i=l,j=m+1,t=0;
while(i<=m && j<=r) {
if(a[i]<=a[j]) temp[t++]=a[i++];
else temp[t++]=a[j++];
}
while(i<=m) temp[t++]=a[i++];
while(j<=r) temp[t++]=a[j++];
t=l;
while(l<=r) a[t++]=temp[t++];
}
}
def mergeSort(nums):
if nums is None or len(nums)<1:return []copy = nums[:]mergeSortHelper(nums,copy,0,len(nums)-1)return numsdef mergeSortHelper(nums,copy,l,r):if l<r:m = (l+r)//2mergeSortHelper(nums,copy,l,m)mergeSortHelper(nums,copy,m+1,r)i=l;j=m+1;t=lwhile i<=m and j<=r:if nums[i]<=nums[j]:copy[t]=nums[i]t+=1;i+=1else:copy[t]=nums[j]t+=1;j+=1while i<=m:copy[t]=nums[i]t+=1;i+=1while j<=r:copy[t]=nums[j]t+=1;j+=1t=lwhile t<=r:nums[t]=copy[t]t+=1

冒泡排序

冒泡排序的思想是,每次将最小的元素交换到队首。(对比左右元素,只要右元素小,就交换位置)。
时间复杂度:平均O(n2),最坏O(n2),最好O(n)。
最好发生在,数组本身有序。最坏发生在,数组本身逆序。
稳定。
具体算法:

    public static void bubbleSort(int[] a) {
int len = a.length;
int temp;
for(int i=0;i) {boolean flag=false; //表示本躺冒泡是否发生交换for(int j=len-1;j>i;j--) {if(a[j]1]) {  //稳定
temp=a[j];
a[j]=a[j-1];
a[j-1]=temp;
flag=true;
}
}
if(flag==false) return; //已经有序了,避免后续无用功
}
}

直接插入排序

直接插入排序的思想是,在序列前一部分有序时,将后一部分最前面的元素插入到有序部分,使插入后的序列依然有序。(找位置,找位置过程中移动元素腾出位置,然后插入)。
时间复杂度:平均O(n2),最坏O(n2),最好O(n)。
最好发生在,数组本身有序。最坏发生在,数组本身逆序。
稳定。
具体算法:

    public static void directInsertSort(int[] a) {
int len = a.length;
int temp;
for(int i=1;i//从第二个元素开始,因为一个元素时必然是有序的if(a[i]-1]) {
temp = a[i];int j=i-1;for(;j>=0;j--) {if(a[j]>temp) a[j+1]=a[j]; //不用>=是为了保证算法的稳定性,不然后出现的元素就会排在前出现的元素之前了,而且那样也会增加无谓的开销
}
a[j+1]=temp;
}
}
}

简单选择排序

简单选择排序的思想是,每轮找到一个最小的数,和无序部分的第一个元素交换位置。(遍历后交换位置,不存在略过的元素的移动)。
时间复杂度,都O(n^2)。因为没有像冒泡有提前终止的判断,或者直接插入排序本身有序就不需要挪动元素了,所有数组的操作都是一样的。
不稳定。首先由于交换位置的原因,那么第一个最小元素(出现在靠后的位置)就会跑到前面,故不稳定。如:222134会变成122234。
具体代码:

    public static void selectionSort(int[] a) {
int len=a.length;
int temp;
for(int i=0;ii++) {
int min = i;for(int j=i+1;jj++) {if(a[j]min]) min=j;
}if(min!=i) {
temp=a[i];
a[i]=a[min];
a[min]=temp;
}
}
}

希尔排序

希尔排序是升级版的直接插入排序,它引入了步长的概念,先对等步长的每个子序列进行直接插入排序,然后缩短步长,最后步长=1,进行最后一次排序。
希尔排序的分析是一个复杂的问题,以为它的时间是所取“增量”序列的函数,这涉及到一些数学上尚未解决的难题。直接记结论,最坏O(n2),最好O(n1.3)。
希尔排序比直接插入排序快的原因:
当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
当相同关键字的元素被分到不同的子表时,顺序可能会发生变化,故不稳定。

    public static void shellSort(int[] a) {
int len = a.length;
int temp;
for(int dk=len/2;dk>=1;dk/=2) { //步长变化
for(int i=dk;i<=len;i++) {
if(a[i]) {//如果需要插入
temp=a[i];
int j=i-dk;for(;j>=0;j-=dk) {if(a[j]>temp) {a[j+dk]=a[j];}
}
a[j+dk]=temp;
}
}
}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值