各种排序复杂度
类别 |
排序算法 | 时间复杂度 |
空间复杂度 |
稳定性 | ||
平均情况 | 最好情况 | 最坏情况 | ||||
插入 | 直接插入 | O( | O(n) | O( | O(1) | 稳定 |
| 希尔排序 | 不好说 | O(1) | 不稳定 | ||
交换 | 冒泡排序 | O( | O(n) | O( | O(1) | 稳定 |
| 快速排序 | O() | O() | O( | O() | 不稳定 |
选择 | 简单选择 | O( | O( | O( | O(1) | 不稳定 |
| 堆排序 | O() | O() | O() | O(1) | 不稳定 |
归并排序 | O() | O() | O() | O(n) | 稳定 | |
基数排序 | O(d(r+n)) | O(d(r+n)) | O(d(r+n)) | O(r) | 稳定 |
基本概念
l 并不是所有的内部排序都是基于比较的,基数排序就不是基于比较的
l 对于任意n个关键字排序的比较次数至少为
插入排序
插入排序总共有三种:直接插入、折半插入和希尔排序
直接插入
l 算法:从头开始进行遍历,每一次都将访问元素插入到前面已经有序的子序列中。对于具有n个元素的序列表而言,需要进行n-1次操作。在前面有序的子序列中找到需要插入的地方k,将k~i-1的所有元素向后移动一位,最后将i处的复制到k的位置。
l 空间效率:因为是就地进行操作,所有空间复杂度为O(1)
l 时间效率:时间复杂度为O(),时间效率取决于比较和移动的次数。最好的情况下,不用移动,只用比较n-1次;最坏的情况下,比较的次数为 = n(n-1)/2,移动的次数同上。
l 稳定性:稳定。因为查找插入位置的时候是寻找比当前元素小的地方,所以不会改变相等元素的前后位置。
l 适用性:适用于基本有序的排序表且数据量不会很大。适用于顺序存储和链式存储。
//直接插入算法
voidInsertSort(int[] A){
for(int i = 1; i < A.length; i++){
if(A[i-1] > A[i]){
int temp = A[i];
for(int j = i - 1; j >= 0; j--){
if(A[j] < temp){
A[j] = temp;
break;
}else{
A[j+1] = A[j];
}
}
}
}
}
折半插入
l 算法思想:和前面直接插入是类似的,不同的只是缩短了在有序子序列中查找插入位置的时间。用的是折半查找的方法。
l 空间效率:O(1)
l 时间效率:O()
l 稳定性:稳定
l 适用性:仅适用于顺序存储的情况
//直接插入算法
voidInsertSort(int[] A){
for(int i = 1; i < A.length; i++){
if(A[i-1] > A[i]){
int temp = A[i];
int low = 0;
int high = i - 1;
while(low <= high){
int mid = (low + high)/2;
if(A[mid] > temp){
high = mid - 1;
}else{
low = mid + 1;
}
}
for(int j = i - 1; j >= high +1; j--){
A[j+1] = A[j];
}
A[high + 1] = temp;
}
}
}
希尔排序(缩小增量排序)
l 算法思想:将排序表分为距离相等的部分,距离相等部分进行直接插入排序的方法。然后不断缩小距离增量,直至d = 1为止。常用的方法是, = n/2, ,直到为1。
l 空间效率:O(1)
l 时间效率:不好说,但是最坏情况下为O()
l 稳定性:不稳定,因为可能会根据不同d导致相同元素的排序颠倒
l 适用性:仅适用于顺序存储的情况
交换排序
常用的算法为:冒泡排序和快速排序
冒泡排序
l 算法思想:从前向后或者从后向前,相邻两位两两比较,如果为逆序,交换两者。这样一趟下来,最小的元素会被放置在最开始的位置。一共进行n-1次完成排序。
l 空间效率:常数个辅助单元,所以为O(1)
l 时间效率:因为有设置一个标志为来表示这一趟是否进行了交换操作。所以最好情况下,比较n-1次,移动0次,为O(n)。最坏情况下,比较次数为: = n(n-1)/2,每一次都移动3次,那么时间为3n(n-1)/2. 即O()。平均情况下为:O()
l 稳定性:因为移动的前提条件是大或小于,因而相等的时候不会有变化,是稳定的算法。
l 注:冒泡排序中已经产生的有序子序列一定是全局有序的(不同于选择排序),每一趟的排序都会将一个元素放置在最终的位置上。
//冒泡排序
voidBubbleSort(int[] a){
for(int i = 0; i < a.length - 1; i++){
int flag = 0;
for(int j = a.length - 1; j > i;j--){
if(a[j] < a[j-1]){
swap(a[j],a[j-1]);
flag = 1;
}
}
if(flag == 0){
return;
}
}
}
快速排序
l 算法思想:基本思想是分治法。在待排序表L[1…n]中任意选取一个元素pivot作为基准。通过一趟排序将待排序表划分为独立的两部分L[1…k]和L[k+1…n],使得L[1…k-1]全部小雨pivot,L[k+1…n]全部大于pivot,将pivot放在了最终的位置L(k)上,这个过程是一趟排序。之后递归的对两个表重复上述过程,直到每部分只有一个元素或空为止。快速排序算法的关键在于划分操作。
l 空间效率:因为用的是递归,所以需要借助一个递归工作栈。最好情况下是+1,最坏的情况是n-1次,所以平均情况下为O()
l 时间效率:最好的情况为O(),最坏情况为O()。平均情况下接近最好的情况。
l 稳定性:不稳定。因为如果右边区间存在两个关键字相同,且均小于pivot,那么交换到左边区间之后会打乱顺序。
l 注:在快速排序算法中,并不产生有序子序列,但每一趟排序之后都会将一个元素放到最终的位置上。
void QuickSort(int[] A,int low, int high){
if(low < high){
int pivotpos = Partation(A,low,high);
QuickSort(A, low, pivotpos - 1);
QuickSort(A, pivotpos + 1, high);
}
}
int Partation(int[] A,int low, int high){
int pivot = A[low];
while(low< high){
while(low < high &&A[high] >= pivot){
high--;
}
A[low] = A[high];
while(low < high && A[low]<= pivot){
low++;
}
A[high] = A[low];
}
A[low] = pivot;
return low;
}
选择排序
主要由简单选择排序和堆排序构成,两者都是不稳定的算法。主要思想是:每一趟在后面待排序的元素中选取关键字最小的元素,作为有序子序列的第i个元素,总共n-1趟即可完成排序。每次排序都会将一个元素放在最终的位置。
简单选择排序
l 算法思想:假设排序表为L[1…n],第i趟排序即从L[i…n]中寻找最小的元素,和L(i)交换,每一趟排序可以确定一个元素的最终位置。经过n-1次就可以使得整个排序表有序。
l 空间效率:常数个辅助空间,所以空间复杂度为O(1).
l 时间效率:移动次数很少,最多也就是3(n-1)次,但是比较的次数是固定的。都为n(n-1)/2次,所以时间复杂度始终为:O(n2)
l 稳定性:可能将后面的相同元素移动到前面,因此是不稳定的算法。
堆排序
是一种树形排序方法,堆满足一定的条件,分为大根堆和小根堆两种。有两大步骤,首先是建堆过程,然后是排序过程。
l 建堆过程:从最后一个非叶结点开始进行调整,逆序调整到根结点为止。中间调整过程中,会对向下的部分造成影响,因此需要用相同的方法构造下级的堆。
l 排序过程:首先将n个元素构造成初始堆(以大根堆为例),堆顶元素就是最大值。输出堆顶元素之后,将堆底元素送入堆顶,从堆顶向下继续调整,再输出堆顶元素。如此重复n-1次,直到堆中仅剩下一个元素为止。整个过程中涉及到堆顶元素的删除,就是前面的先交换堆顶和堆底元素,然后从堆顶元素向下调整使其继续保持堆的性质。
l 堆的插入:先将该元素放在堆的末端,然后从这个新结点执行向上调整操作。
l 空间效率:使用了常数个辅助空间,所以为O(1)
l 时间效率:常数个辅助单位,O(1)
l 空间效率:初始建堆为O(n),后面n-1次向下调整和树的高度h有关,所以在最好、最坏和平均之下,时间复杂度都是:O()
l 稳定性:是不稳定的算法
归并排序
l 归并:指的是将两个或两个以上有序的子序列合并一个新的有序的序列。
l 过程:开始的时候,将带排序表看为有n个有序的子表,每个字表长度为1,然后两两归并。得到向上取整的n/2个有序的表,然后继续两两归并…直到最终表的长度为n为止。这种排序方法称为“2-路归并”
l 归并的过程:merge()算法先把两个有序的子表[low,mid],[mid+1,high]复制到一个新的数组中,然后对这个数组的两部分进行遍历,将较小的元素放入最终合并的数组A中。当某个部分到头,就将另一个数组剩余部分全部复制到A中即可。
voidMerge(int[] A, int low, int mid, int high){
int[] B = new int[A.length];
for(int i = low; i <= high; i++){
B[i] = A[i];
}
int i = low;
int j = mid + 1;
while(i <= mid && j <= high){
if(B[i] < B[j]){
A[k++] = B[i++];
}else{
A[k++] = B[j++];
}
}
while(i <= mid){
A[k++] = B[i++];
}
while(j <= high){
A[k++] = B[j++];
}
}
l 一趟二路归并排序,会进行趟。对于k路归并,那么需要进行次。递归形式的归并排序算法是基于分治的,先分解再合并。分解:将含有n个元素的待排序表分成各含n/2的子表,然后对两个子表递归。合并,就是上面的merge算法。
voidMergeSort(int[] A, int low, int high){
if(low < high){
int mid = (low + high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high);
}
}
l 空间效率:因为在Merge的时候,需要一个复制数组,所以为O(n)
l 时间效率,依次Merge为O(n),一共进行趟,其分割子序列和初始序列是无关的。所以时间效率为O(n)
l 稳定性:稳定的
基数排序
l 不是基于比较的算法,是根据多关键字排序思想,借助分配和收集两种操作对单逻辑关键字进行排序。基数排序分为最高为优先和最低为优先。以r为基数,则需要r个队列进行辅助。每个结点为d元组,那么需要进行d趟。
l 空间效率:因为需要r个队列进行辅助,所以空间复杂度为O(r)
l 时间效率:一共进行d趟,每一趟有分配和收集两个步骤。分配时间为O(n),收集为O(r),因此时间复杂度为O(d(r+n))
l 稳定性:基数排序一定是稳定的