排序的基本概念
排序:重新排列,使表中元素满足按关键字有序的过程。
根据元素在排序过程是否完全在内存中,将排序分为(1)内部排序(2)外部排序
内部排序两种操作:(1)比较(2)移动 (基数排序不基于比较)
下面介绍几种基本的内部排序算法思路,代码实现(大多为伪代码),性能分析。
插入排序
基本思想:每次将一个待排序的记录按关键字大小插入前面已经排好的序列。
三类插入排序算法:直接插入排序,折半插入排序,希尔排序。
直接插入排序
1)查找L(i)在L[1…i-1]中的插入位置k;
2) 将L[k…i-1]中所以元素依次后移;
3) 将L(i)复制到L[k];
伪代码:
void insertSort(ElemType A[],int n){
int i,j;
for(int i=1;i<n;i++){ //将A[1]~A[n-1]依次插入到前面的有序序列
ElemType temp=A[i]; //记录待排序的值
for(j=i-1;j>=0&&temp<=A[j];j--) A[j+1]=A[j]; //从后往前作比较,并挪位
A[j+1]=temp; //复制到插入位置
}
}
性能分析:
空间效率:使用常数辅助单元,O(1)。
时间效率:插入操作n-1趟,每趟比较最少1次,最多i次。最好时间复杂度O(n),最差O(
n
2
n^2
n2),平均时间复杂度O(
n
2
n^2
n2)。
稳定性:稳定。每次插入从后向前先比较再移动。
折半插入排序
对直接插入排序算法做改进,查找有序子表用折半查找来实现。
伪代码:
void InsertSort(ElemType A[],int n){
int i,j,low,high;
for(int i=1;i<n;i++){
//折半查找
ElemType temp=A[i];
low=0;high=i-1;
while(low<=high){
mid=(low+high)/2;
if(A[mid]>temp) high=mid-1; //查找左半子表
else low=mid+1; //查找右半子表
}
//移位,插入
for(j=i-1;j>high+1;j--){
A[j+1]=A[j];
}
A[high+1]=temp;
}
}
性能分析:
空间和稳定性同直接插入算法。
时间:减少比较元素的次数,约为O(
n
l
o
g
2
n
nlog_2n
nlog2n),该比较次数仅取决于元素个数n。元素移动次数并未改变,它依赖于表的初始状态,故时间复杂度仍为O(
n
2
n^2
n2)。
希尔排序
基本思想:将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的子表,对各个子表进行插入排序。
步长:
d
1
<
n
,
d
2
<
d
1
,
.
.
.
,
d
t
=
1
d_1<n,d_2<d_1,...,d_t=1
d1<n,d2<d1,...,dt=1
伪代码:
void ShellSort(ElemType A[],int n){
for(dk=n/2;dk>0;dk=dk/2) //步长变化
for(int i=dk+1;i<=n;i++){ //从子表中的第二个元素进行比较
if(A[i]<A[i-dk]){
A[0]=A[i];
for(j=i-dk;j>0&&A[0]<A[j];j=j-dk) A[j+dk]=A[j]; //记录后移
A[j+dk]=A[0]; //插入
}
}
}
性能分析:
空间:仍为O(1)。
时间:复杂度依赖于增量序列的函数,最坏情况O(
n
2
n^2
n2)。
稳定性:不稳定。相同关键字划分到不同子表时,可能改变相对次序。
交换排序
基本思想:根据元素关键字的比较结果对换两个记录在序列中的位置。
冒泡排序
从后往前(从前往后)依次比较相邻两个元素的值,若为逆序,则交换,直到比较完。这是第一趟冒泡,结果使最小元素到第一个位置(或最大元素到最后一个位置)。前一趟排序确定的最小元素(或最大)不再参与比较,每趟排序把序列中的一个元素放到序列的最终位置。最多n-1趟把所以元素排好序。
伪代码:
void BubbleSort(ElemType A[],int n){
for(int i=0;i<n-1;i++){
flag=false;
for(int j=n-1;j>i;j--){
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);
flag=true;
}
}
if(flag==false) return; //本趟遍历没有发生交换,说明表已经有序
}
}
空间:常数辅助单元O(1)。
时间:有序,一趟排序,比较n-1,交换0,最好时间复杂度O(n)。逆序,n-1趟排序,每趟n-i次比较,每次比较移动元素3次,这种情况下最坏时间复杂度O(
n
2
n^2
n2)。平均时间复杂度O(
n
2
n^2
n2)。
稳定性:稳定。
快速排序
基本思想:基于分治。在待排序表中选取一个元素pivot作为枢轴(或基准),通过一趟排序将待排序表分为独立的两部分L[1…k-1] (所有元素小于pivot) 和L[k+1…n] (所有元素大于pivot)。这个过程称为一趟快速排序。然后分别对两个子表重复上述过程,直至每一部分内只有一个元素或空为止。
伪代码:
void QuickSort(ElemType A[],int low,int high){
if(low<high){
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
int Partition(ElemType A[],int low,int high){
ElemType 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; //返回最终位置
}
空间:需要借助递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大深度一致。最好情况O(
l
o
g
2
n
log_2n
log2n),最坏情况O(n),平均为O(
l
o
g
2
n
log_2n
log2n)。
时间:运行时间与划分是否对称有关。最坏情况下时间复杂度为O(
n
2
n^2
n2)。在理想情况下,划分对称,时间复杂度为O(
n
l
o
g
2
n
nlog_2n
nlog2n)。
稳定性:不稳定。右端两个相同关键字且均小于基准值的记录,在交换到左端点区间后,它们的相对位置会发生变化。
选择排序
基本思想:每一趟(如第i趟)在后面n-i+1个待排元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完。
简单选择排序
第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换。
伪代码:
void SelectSort(ElemType A[],int n){
for(int i=0;i<n-1;i++){ //一共n-1趟
int min=i; //记录最小元素位置
for(j=i+1;j<n;j++){
if(A[j]<A[min]) min=j; //更新最小元素位置
}
if(min!=i) swap(A[i],A[min]);
}
}
性能分析:
空间:常数个辅助单元O(1);
时间:移动-[0,3(n-1)]次,元素间比较次数与序列初始状况无关,始终是n(n-1)/2次,时间复杂度O(
n
2
n^2
n2)。
稳定性:不稳定。在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变。
堆排序
大根堆:L(i)>=L(2i)且L(i)>=L(2i+1)
小根堆:L(i)<=L(2i)且L(i)<=L(2i+1)
堆排序思路:
(1)构造初始堆
(2)输出堆顶元素,将剩余元素调整为新的堆
性能分析:
空间:O(1)
时间:建堆时间为O(n),之后n-1次向下调整,每次调整O(h),故堆排序的时间复杂度(
n
l
o
g
2
n
nlog_2n
nlog2n)
稳定性:不稳定
归并排序
归并:将两个或两个以上的有序表组合成一个新的有序表
void Merge(ElemType A[],int low,int mid,int high){
for(int k=low;k<=high;k++)
B[k]=A[k];
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
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[1…n]中前后相邻长度为h的有序段进行两两归并整个归并需要进行
⌈
l
o
g
2
n
⌉
\lceil log_2n \rceil
⌈log2n⌉趟。
递归形式的2路算法是基于分治的。
void MergeSort(ElemType A[],int low,int high){
if(low<high){
int mid=(low+high)/2;
MergeSort(A,low,high);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high);
}
}
性能分析:
空间:辅助数组B,故为O(n)。
时间:每趟归并O(n),共
⌈
l
o
g
2
n
⌉
\lceil log_2n \rceil
⌈log2n⌉趟,时间复杂度
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。
稳定性:稳定
基数排序
基于关键字各位的大小进行排序。
通常有两种方法:(1)最高位优先法(MSD)(2)最低位优先法(LSD)
性能分析:
空间:需要辅助存储空间r个队列,所以空间复杂度O®。
时间:进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(d(n+r))。
稳定性:稳定。按位排序时必须是稳定的。
各种内部排序算法的比较
排序算法的主要步骤还是比较和交换,比较和交换的方法会影响其空间,时间复杂度。分析复杂度,也主要从算法比较和交换的角度分析。
空间复杂度
O
(
1
)
O(1)
O(1):简单选择排序,插入排序(直接or折半),希尔排序,冒泡排序,堆排序仅需借助常数个辅助空间(基于插入的排序算法借助一个temp临时变量存储待排序元素,和其他元素作比较;基于交换的排序算法使用swap()做交换时,借助临时变量)。
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n):快速排序(使用一个小的辅助栈,用于实现递归,最坏情况可能达到
O
(
n
)
O(n)
O(n))
O
(
n
)
O(n)
O(n):2路归并排序在合并操作时,需要借助辅助空间,用于元素复制。
O
(
r
)
O(r)
O(r):基数排序,辅助存储空间r个队列。
时间复杂度
O
(
n
2
)
O(n^2)
O(n2):简单选择,直接插入,冒泡排序平均情况下。直接插入和冒泡最好情况可以达到
O
(
n
)
O(n)
O(n)(顺序)。简单选择与序列初始状态无关。
希尔排序:作为插入排序的拓展,对较大规模的排序可以达到很高的效率,未得其精确渐进时间。
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n):堆排序,快速排序,归并排序。堆排序使用堆的数据结构,快排和归并都基于分治的思想。但快排最坏情况可以达到
O
(
n
2
)
O(n^2)
O(n2),归并最好,最坏和平均都是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。
稳定性
稳定:插入排序,冒泡排序,归并排序,基数排序
不稳定:简单选择,快速排序,希尔排序和堆排序
参考:
王道数据结构考研复习指导