排序

算法稳定性

内部排序:在排序期间元素全部存放在内存中的排序

外部排序:在排序期间元素无法全部同时存放在内存中

大多数内部排序都更适用于顺序存储的线性表。

插入排序

直接插入排序

时间复杂度

具有稳定性。

折半插入排序

仅适用于顺序存储的线性表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void insertSort(int A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0]=A[i];
low=1;
high=-1;
while(low<=high){
mid=(low+high)/2;
if(A[mid]>A[0]) high=mid-1;
else low=mid+1;

}
for(j=i-1;j>=high+1;--j)
A[j+1]=A[j];

A[high+1]=A[0];

}
}

有稳定性,时间复杂度和直接插入排序一样。

希尔排序

取增量,然后不断缩小增量,直到增量为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void shellsort(int a[],int n){
int dk,i,j;
for(dk=n/2;dk>=1;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-=dk){
A[j+dk]=A[j];

}
A[j+dk]=A[0];
}
}

}

空间复杂度为

时间复杂度为

不稳定

交换排序

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
void bubblesort(int a[],int n){
for(int i=0;i<n-1;i++){
bool 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;
}
}
}

时间复杂度为

有稳定性

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void quick_sort(int q[],int l,int r){

if(l>=r) return ;
int i=l-1,j=r+1,x=q[l+r>>1];
while(i<j){
do i++;while(q[i]<x);
do j--;while(q[j]>x);
if(i<j) std::swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}

//int main(){
// int a[]={4,3,2,1};
// quick_sort(a,0,3);
// for(int i=0;i<=3;i++){
// cout<<a[i]<<" ";
// }
// return 0;
//}

快速排序是所有内部排序算法中平均性能最优的算法。

不具有稳定性

时间复杂度为

递归次数与每次划分后得到的分区处理顺序无关,与初始数据排列顺序有关

对n个元素进行第一趟快速排序之后,会确定一个基准元素,根据这个基准元素的位置有两个情况:

  1. 在数组的首端或者尾端,这样子第二次排序之后有两个位置确定。
  2. 不在数组首端或者尾端,这样两个子序列会至少有三个位置确定

选择排序

简单选择排序

每次都选最小的那个放到当前序列最前面的地方。

不稳定,时间复杂度

1
2
3
4
5
6
7
for(int i=0;i<n-1;i++){
int min=i;
for(int j=i+1;j<n;j++){
if(A[j]<A[min]) min=j;
}
if(min!=i) swap(A[min],A[i]);
}

堆排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void maxHeapify(vector<int>&a,int i,int heapSize){
int l=2*i+1;
int r=2*i+2;
int largest=i;
if(l<heapSize && a[l]>a[largest]){
l=largest;
}
if(r<heapSize && a[r]>a[largest]){
r=largest;
}
if(largest!=i){
swap(a[largest],a[i]);
maxHeapify(a,largest,heapSize);
}
}
// 建堆
void buildMaxHeap(vector<int>&a,int heapsize){
for(int i=heapsize/2;i>=0;i--){
maxHeapify(a,i,heapsize);
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapSize=nums.size();
buildMaxHeap(nums,heapSize);
for(int i=nums.size()-1;i>=nums.size()-k+1;i--){
swap(nums[0],nums[i]);
--heapSize;
maxHeapify(nums,0,heapSize);
}
return nums[0];
}

时间复杂度为

不稳定

若从一亿个数中选出前100个最大的数:

首先选择前100个数建立小顶堆

逐渐读入数据,如果小于堆顶就删除,如果大于就替换掉堆顶,然后重新调整堆。

归并排序,基数排序和计数排序

归并排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void merge(int a[],int b[],int low,int mid,int high){
// b是辅助数组
int i,j,k;
for(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++];
}

void mergeSort(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);
}
}

时间复杂度为

有稳定性

基数排序

d个关键字,以r为基数。

举个例子,每个关键字都是1000以下的正整数,基数r=10,先按照最低位子关键字(个位)来排序,然后是十位,最后是百位。

空间复杂度

空间复杂度

计数排序

对于每个待排序的元素x,统计小于x的元素的位置。

1
2
3
4
5
6
7
8
9
10
11
void CountSort(int a[],int b[],int n,int k){
int i,C[k];
for(i=0;i<k=i++) C[i]=0;
for(i=0;i<n;i++) C[A[i]]++;
for(i=1;i<n;i++) C[i]=C[i]+C[i-1];
for(int i=n-1;i>=0;i--){
B[--C[A[i]]]=A[i];
}
}
// B存放输出的排序序列
// C存放计数值

时间复杂度为

时算法为 ,k不是很大的时候算法时间复杂度不是很大。

外部排序

外部排序时间代价主要是考虑访问磁盘次数,即I/O次数

外部排序通常采用归并排序算法:

  1. 根据内存缓冲区大小,将外存上的文件分成若干长度为 的子文件,依次读入内存并利用内部排序算法对它们进行排序,并将排序后得到的子文件重新写回内存。
  2. 对这些归并段进行逐趟归并,使归并端逐渐从小变大,直到得到整个有序文件为止。

外部排序时间=内部排序时间+外存信息读/写时间+内部归并的时间。

多路平衡树和败者树

为了让内部归并不受k的增大的影响,引入败者树(完全二叉树)

k个叶节点分别存放k个归并段在归并过程中当前参与比较的元素,内部结点来记录左右子树中的失败者,胜利者向上比较,直到根节点。大的为失败者

置换-选择排序(生成初始归并段)

设初始待排文件为 FI ,初始归并段输出文件为 FO ,内存工作区为 WA,WA可以容纳w个记录。

就是在WA中选一个最小的(并且比FO中最大的还要大)输出到FO中,然后从FI中读入一个数据,继续循环。

直到WA中所有数据比FO最大的数据小,我们换到另一个FO。

最佳归并树

IO次数=2*WPL(树的带权路径长度)

设度为0的结点有 个,度为 的结点有 个,归并树的总结点有 个,则有:

(总结点数=所有结点度数之和+1)

所以严格k叉树有 ,于是有