八大排序算法
时间复杂度 | 空间复杂度 | 稳定性 | |
直接插入排序 | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^(1.3—2)) | O(1) | 不稳定 |
简单选择排序 | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(logn) | 不稳定 |
归并排序 | O(nlogn) | O(n) | 稳定 |
基数排序 | O(d(n+r)) | O(r) | 稳定 |
直接插入排序
长度为n的序列,从左到右,将第 i 个数按照其值有序地插入到前 i-1 个数中。
实现
1 void InsertSort(int A[],int n) 2 { 3 for(int i=1;i<n;i++) 4 { 5 int temp=A[i];//将第i个关键字存于temp中 6 int j=i-1; 7 while(j>=0&&temp<A[j])//从后往前扫描前i-1个数,如果大于temp,则后移一位 8 { 9 A[j+1]=A[j]; 10 j--; 11 } 12 A[j+1]=temp;//找到插入位置,插入 13 } 14 }
性能分析
时间复杂度:O(n^2)
最好的情况,整个序列有序,不会进入内层循环,O(n)。最坏的情况,整个序列逆序,内层循环的执行次数为1+2+3+...+(n-1)=n(n-1)/2,O(n^2)。综上,O(n^2)。
空间复杂度:O(1)
不需要辅存,O(1)。
稳定性:稳定
希尔排序
又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。
1,9,7,6,2,4,8,3,5
1、先将要排序的数分组,第1、1+n、1+2n...个数为一组;第2、2+n、2+2n...个数为一组...,一次类推;n为间隔,一般开始时取数组长度的一半。
例:第一轮:n取4。1、2、5为一组,9、4为一组,7、8为一组,6、3为一组;(写的这些数是值,不是下标)
2、分别对每一组进行插入排序(组内部排序)
例:第一轮:排好后为:1,4,7,3,2,9,8,6,5
3、间隔n取上个间隔的一半(也就是4/2=2),再从1步骤开始:分组、每组插入排序、间隔缩小一半(直至n取至1为止)
第二轮:n取2。1、7、2、8、5为一组,4、3、9、6为一组
第二轮:排好后为:1,3,2,4,5,6,7,9,8
第三轮:排好后为:1,2,3,4,5,6,7,8,9
实现
1 void ShellSort(int A[],int n) 2 { 3 for(int interval=n/2;interval>0;interval/=2) 4 { 5 for(int i=interval;i<n;i++)//对每组进行插入排序,代码和直接插入排序代码类似 6 { 7 int temp=A[i]; 8 int j=i-interval; 9 while(j>=0&&temp<A[j])//从后往前扫描间隔为inteval的那一组数,如果大于temp,则后移interval位 10 { 11 A[j+interval]=A[j]; 12 j-=interval; 13 } 14 A[j+interval]=temp; 15 } 16 } 17 }
性能分析
时间复杂度:O(n^(1.3—2))
希尔排序的时间复杂度与增量选取有关,目前最好的增量使时间复杂度最坏可以达到O(n^1.3)。
空间复杂度:O(1)
不需要辅存,O(1)。
稳定性:不稳定
简单选择排序
从头到尾扫描序列,选出最小的关键字,与第一个关键字交换,接着从剩下的序列中选出最小的关键字,与第二个关键字交换,以此类推。
实现
1 void SelectSort(int A[],int n) 2 { 3 for(int i=0;i<n;i++) 4 { 5 int k=i;//记录最小关键字的下标 6 for(int j=i+1;j<n;j++) 7 { 8 if(A[j]<A[k]) 9 k=j; 10 } 11 int temp=A[i]; 12 A[i]=A[k]; 13 A[k]=temp; 14 } 15 }
性能分析
时间复杂度:O(n^2)
两层循环的执行次数与初始序列没有关系,内层循环的执行次数是(n-1)+(n-2)+...+1=n(n-1)/2,O(n^2)。
空间复杂度:O(1)
不需要辅存,O(1)。
稳定性:不稳定
堆排序
堆是一种数据结构,可以看成是一颗完全二叉树。这颗完全二叉树,如果任何一个非叶结点的值都不大于其左右孩子结点的值,称这样的结构为小顶堆;如果任何一个非叶结点的值都不小于其左右孩子结点的值,称这样的结构为大顶堆。
根据堆的定义可知,代表堆的这颗完全二叉树的根节点的值是最大(或最小的),因此将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样,有序序列关键字增加1个,无序序列关键字减少1个,对新的无序序列重复这样的操作,就实现了排序。这就是堆排序的思想。
实现
1 /*注: 2 1.使用大顶堆 3 2.实现对数组A[]的从小到大排序 4 3.数组的下标也就是堆的下标从0开始 5 */ 6 //对A[low]到A[high]这段的堆顶A[low]进行下沉调整 7 void downAdjust(int A[],int low,int high) 8 { 9 int temp=A[low]; 10 int i=low,j=2*i+1; //A[j]是A[i]的左孩子结点 11 while(j<=high) 12 { 13 if(j<high&&A[j]<A[j+1])//若右孩子大,则j指向右孩子 14 j++; 15 if(temp<A[j]) 16 { 17 A[i]=A[j]; //将A[j]调整到双亲结点的位置上 18 i=j; //修改i,j的值,以便继续向下调整 19 j=2*i; 20 } 21 else 22 break; 23 } 24 A[i]=temp; //被调整结点的值放入最终位置 25 } 26 //建堆 27 void buildHeap(int A[],int n) 28 { 29 for(int i=n/2-1;i>=0;i--)//从最后个非叶结点开始,依次下沉调整 30 { 31 downAdjust(A,i,n-1); 32 } 33 } 34 //堆排序 35 void heapSort(int A[],int n) 36 { 37 buildHeap(A,n);//建堆,初始时A[0]为最大关键字 38 for(int i=n-1;i>=1;i--) 39 { 40 int temp=A[0];//每次将无序序列中的堆顶A[0](也就是最大值)和无序序列的最后一项交换 41 A[0]=A[i]; 42 A[i]=temp; 43 downAdjust(A,0,i-1);//对减少了一个关键字的无序序列进行新的调整,寻找新的堆顶。 44 } 45 }
性能分析
时间复杂度:O(nlogn)
对于downAdjust()函数,j走了一条从当前结点到叶子节点的路径,完全二叉树的高度为⌈log(n+1)⌉,即对每个结点调整的时间复杂度为O(logn)。对于heapSort()函数,建堆花费了O(logn)*n/2,循环花费了O(logn)*(n-1),总的基本操作执行次数即为O(logn)*n/2+O(logn)*(n-1),化简,O(nlogn)。
空间复杂度:O(1)
不需要辅存,O(1)。
稳定性:不稳定
冒泡排序
首先第一个关键字与第二个关键字比较,如果第一个大,则二者交换,否则不交换;然后第二个关键字和第三个关键字比较,如果第二个大,则交换,否则不交换......。一直按这种方式进行下去,最终最大的那个关键字被交换到了最后,一趟冒泡排序完成。经过多趟这样的排序,最终使整个序列有序。
实现
1 void BubbleSort(int A[],int n) 2 { 3 int flag;//flag用来标记每趟排序是否发生交换 4 for(int i=n-1;i>=1;i--) 5 { 6 flag=0; 7 for(int j=1;j<=i;j++) 8 { 9 if(A[j-1]>A[j]) 10 { 11 int temp=A[j]; 12 A[j]=A[j-1]; 13 A[j-1]=temp; 14 flag=1; 15 } 16 } 17 if(flag==0)//如果一趟排序没有发生交换,那么证明序列此时已经有序 18 return; 19 } 20 }
性能分析
时间复杂度:O(n^2)
最好的情况,整个序列有序,不会进入内层循环,O(n)。最坏的情况,整个序列逆序,内层循环的执行次数为(n-1)+(n-2)+...+1=n(n-1)/2,O(n^2)。综上,O(n^2)。
空间复杂度:O(1)
不需要辅存,O(1)。
稳定性:稳定
快速排序
每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为枢纽,将子序列中比枢纽小的移到枢纽左边,比枢纽大的移到枢纽右边;当本趟所有子序列都被枢纽以上述规则划分完毕后会得到新的一组更短的子序列,它们成为下一趟划分的初始序列集。
实现
1 void QuickSort(int A[],int l,int r) 2 { 3 int temp; 4 int i=l,j=r; 5 if(l<r) 6 { 7 temp=A[l];//枢纽 8 while(i<j) 9 { 10 while(i<j&&A[j]>=temp)j--;//从右往左扫描,找到一个小于temp的关键字 11 if(i<j) 12 { 13 A[i]=A[j]; //放到temp左边 14 i++; //i右移一位 15 } 16 while(i<j&&A[i]<temp)i++;//从左往右扫描,找到一个大于temp的关键字 17 if(i<j) 18 { 19 A[j]=A[i]; //放到temp右边 20 j--; //j左移一位 21 } 22 } 23 A[i]=temp; //将temp放在最终位置 24 QuickSort(A,l,i-1); //递归地对temp左边的关键字排序 25 QuickSort(A,i+1,r); //递归地对temp右边的关键字排序 26 } 27 }
性能分析
时间复杂度:O(nlogn)
最好情况下,待排序序列接近无序,为O(nlogn)。最坏情况下,待排序序列接近有序,为O(n^2),平均情况下时间复杂度为O(nlogn)。
空间复杂度:O(logn)
快速排序是递归进行的,递归需要栈的辅助。
稳定性:不稳定
归并排序
归并排序算法完全依照了分治模式
-
分解:将n个元素分成各含n/2个元素的子序列
-
解决:对两个子序列递归地排序
-
合并:合并两个已排序的子序列以得到排序结果
归并排序的重点在于合并,而快速排序的重点在于划分。
实现
1 //辅助空间 2 int helper[1000]; 3 //合并[l,mid]和[mid+1,r]两个序列 4 void merge(int A[],int l,int mid,int r) 5 { 6 7 for(int i=l;i<=r;i++) //将原数组复制到辅助空间的相同位置 8 helper[i]=A[i]; 9 10 int left=l,right=mid+1; //辅助数组的两个指针 11 12 int cur=l; //原始数组的指针 13 while(left<=mid&&right<=r) 14 { 15 if(helper[left]<=helper[right]) 16 A[cur++]=helper[left++]; 17 else 18 A[cur++]=helper[right++]; 19 } 20 21 while(left<=mid) //左边指针没到头,右边到头 22 A[cur++]=helper[left++]; 23 //左边指针到头,右边没到头也没关系(右边和原数组完全一致)。 24 } 25 //排序 26 void mergeSort(int A[],int l,int r) 27 { 28 if(l<r) { 29 int mid=(l+r)>>1; 30 mergeSort(A, l, mid);//对左侧排序 31 mergeSort(A, mid+1, r);//对右侧排序 32 merge(A,l,mid,r);//合并 33 } 34 }
性能分析
时间复杂度:O(nlogn)
划分了O(logn)次,对每次划分的子序列合并的规模是O(n),因此是O(nlogn)。
空间复杂度:O(n)
需要用到一个辅助空间。
稳定性:稳定
基数排序
前面七种排序都是按整个关键字的值来排序的,而基数排序先将关键字分成多个部分,按照关键字的一部分资讯进行排序,然后再通过一部分资讯进行排序,接着再通过一部分资讯进行排序,然后,,,接着,,,最后就整体有序了。
例:
待排序序列:278,109,063,930,589,184,505,269,008,083
关键字每一位都是由“0~9”的数字组成,所以准备10个桶来放关键字。
进行第一趟分配和收集,按照个位。(注意,关键字从桶的上面进入)
分配完成后如图
收集的过程是这样的:按桶0到9的顺序收集。(注意关键字从桶的下面出)
桶0:930
桶1:没关键字,不收集
桶2:没关键字,不收集
...
桶8:278,008
桶9:109,589,269
将每桶收集的关键字以次排开,所以第一趟收集后的结果为:
930,063,083,184,505,278,008,109,589,269
注意观察,最低位有序了,这就是第一趟基数排序后的结果。
进行第二趟分配和收集,按照十位。
分配完成后如图
第二趟收集后的结果为:505,008,109,930,063,269,278,083,184,589
此时中间位有序了,并且中间位相同的数字,其最低位也是有序的。
进行第三趟分配和收集,按照百位。
分配完成后如图
第三趟收集后的结果为:008,063,083,109,184,269,278,505,589,930
于是整个序列有序了。
性能分析
时间复杂度:O(d(n+r)) n为关键字个数,d为关键字位数,r为关键字基的个数,这里的基指的是构成关键字的符号
每一趟基数排序都有分配和收集,分配是对n个关键字,收集是对r个桶,于是一趟分配和收集是n+r,有d趟分配和收集,所以一共花费了d(n+r),O(d(n+r))。
空间复杂度:O(r)
r个桶嘛。
稳定性:稳定