汇总:
数据结构笔记|C++
1.三个时间复杂度为O(n2)的排序算法
1.1插入排序
1.1.1直接插入排序
//插入排序
void insertSort(int* d){//递增
int N=sizeof(d)/sizeof(int);
for(int i=0;i<N;i++){
for(int j=i;j<0;j--){
if(d[j]<d[j-1]){
swap(d[j],d[j-1]);
break;
}
}
}
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(n2) | O(n2) | 初始逆序 | O(n) | 初始正序 | n2 | n2 | n | O(1) | 稳定 | 全局有序 |
1.2冒泡排序
使最小的元素一直上浮
//冒泡排序
void bubbleSort(int* d){
int N=sizeof(d)/sizeof(int);
for(int i=0;i<N;i++){
for(int j=N-1;j>i;j++){
if(d[j-1]>d[j])
swap(d[j-1],d[j]);
}
}
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(n2) | O(n2) | 初始反序 | O(n) | 初始正序 | n2 | n2 | n | O(1) | 稳定 | 全局有序 |
1.3选择排序
每步从待排序的元素中选出关键字最小的元素,放到已排序的序列的最后
//选择排序
void selectSort(int* d){
int N=sizeof(d)/sizeof(int);
for(int i=0;i<N;i++){//待放的位置
int minIdx=i;
for(int j=i+1;j<N;j++){
if(d[j]<d[minIdx]){
minIdx=j;
}
}
swap(d[i],d[minIdx]);
}
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(n2) | O(n2) | 无 | O(n2) | 无 | n2 | n | n | O(1) | 不稳定 | 全局有序 |
- 简单选择排序的效率与初始数据的顺序性无关(就算正序逆序,都需要每次比较剩余全部未比较元素),每进行一趟排序归位一个元素
- 由于每次都在选,所以不稳定,eg.
排序前:
2,4,4*,3
排序后:
2,3,4*,4
2.Shell排序
incr:增量
分组(组内下标差=增量)->增量/2直到1
//shellSort
void insertSort(int* d,int begin,int n,int incr){
for(int i=begin;i<n;i+=incr){
for(int j=i-incr;j>=begin;j-=incr){
if(d[i]<d[j])
swap(d[i],d[j]);
}
}
}
void shellSort(int* d,int n){
for(int i=n/2;i>2;i/=2){
for(int j=0;j<i;j++){
insertSort(d,j,n-j,i);
}
}
insertSort(d,0,n,1);
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(n1.5) | O(n2) | 增量序列为[2,4,8,…,22] | O(n1.5) | 增量序列[3,6,12,…,3k] | ❓n2 | ❓n2 | log2n | O(1) | 不稳定 | – |
3.归并排序
算法核心:分治法
重要的两个操作:
- 如何合并
- 复制子序列->原序列
经典实现:
//Standard implementation
void mergeSort(int* d,int* temp,int left,int right){
if(left==right)
return;
int mid=(left+right)/2;
mergeSort(d,temp,left,mid);
mergeSort(d,temp,mid+1,right);
for(int i=left;i<right;i++){
temp[i]=d[i];
}
int i1=left;int i2=right;
for(int curr=left;curr<right;curr++){
}
}
优化实现
这个算法在复制时,把第二个子数组中的元素的顺序颠倒了一下。现在两个子数组从两端开始运行,向中间推进,使得这两个子数组的两端互为另一个数组的监视哨(sentinel)
从而不用像上面的算法那样需要检查子序列被处理完的情况
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(nlog2n) | O(nlog2n) | 无 | O(nlog2n) | 无 | 不确定 | 不确定 | log2n | O(n) | 稳定 | 不全局有序 |
特点:
- 时间效率与待排序数据无关
- 最好、最坏、平均时间复杂度一致
4.快速排序
由冒泡排序改进
原理:
选定基准值(privot,一般为第一个或者中间一个),将比基准值大的放到后面区间,比基准值小的放的前面区间,这个叫做一趟划分(partition)。
再对前后区间进行划分,直到区间只有一个元素为止。
注意:
如果l从i-1开始,循环条件:l<r
如果l从i开始,循环条件:l<=r
int partition(int* arr,int l,int r,int i,int pivot){
while(l<=r){
while(arr[l]<pivot)
l++;
while(arr[r]>=pivot)
r--;
swap(arr[l],arr[r]);
}
swap(arr[i],arr[r]);
return r;
}
void quickSort(int* arr,int i,int j){
if(j<=i) return;//0 or 1 element
int pivot=arr[i];
int l=i+1;
int r=j;
int k=partition(arr,l,r,i,pivot);
quickSort(arr,i,k-1);
quickSort(arr,k+1,j);
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(nlog2n) | O(n2) | 初始正序(每个privot轴值都未能把数组划分好) | O(nlog2n) | 随机 | 不确定 | 不确定 | log2n | O(log2n) | 不稳定 | 无 |
- 适用于n较大时,不适用于n较小时
- 其他分析(优化改进、时间复杂度的数学依据):快速排序|数据结构|C++|原理分析及一些优化
5.堆排序
5.1堆的实现:
template<typename E>
class heap{
private:
E* Heap;
int maxsize;
int n;
//helper function to put element in its correct place
void siftdown(int pos){
while(!isLeaf(pos)){
int lc=leftChild(pos);
int rc=rightChild(pos);
//三值的比较
if(rc<n&&Heap[rc]>Heap[lc])
lc=rc;//最大的孩子赋给rc
if(Heap[pos]>Heap[rc])return;
swap(Heap[pos],Heap[rc]);
pos=rc;
}
}
void siftup(int pos){
while(pos!=0&&(Heap[parent(pos)]<Heap[pos])){
swap{Heap[parent(pos)],Heap[pos]};
pos=parent(pos);
}
}
public:
heap(E* e,int num,int max){
Heap=h;n=num;maxsize=max;buildHeap();
}
int size(){return n;}
bool isLeaf(int pos)const{
return (pos>=n/2)&&pos<n;//(n-1)/2为leaf
}
int leftChild(int pos)const{
return 2*pos+1;
}
int rightChild(int pos)const{
return 2*pos+2;
}
int parent(int pos)const{
return (pos-1)/2;
}
void buildHeap(){
for(int i=0n/2-1;i>=0;i--)siftdown(i);
}
void insert(const E&it){
assert(n<maxsize);
Heap[n++]=it;//append to the last
siftup(n);
}
E removefirst(){
assert(n>0);
swap(Heap[0],Heap[--n]);//第一个与最后一个交换
if(n>0)siftdown(0);
return Heap[n];//注意n为最后一个元素的后一个元素的下标
}
E remove(int pos){
assert(pos>=0&&pos<n);
if(pos==(n-1)) n--;//最后一个元素,无需操作,只需要减一
else{
swap(Heap[pos],Heap[--n]);
siftup(pos);
siftdown(pos);
}
return Heap[n];
}
};
5.2堆排序
template<typename E>
void heapSort(E A[],int n){
E maxval;
heap<E> H(A,n,n);
for(int i=0;i<n;i++){
maxval=H.removefirst();
}
}
5.3补充:C++中priority_queue的使用
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
– | – | – | – | – | – | – | – | – | – | – |
6.计数排序
非比较排序
根据出现的次数和数组的逻辑结构进行排序
6.1原理
6.2实现
void countSort(int* arr,int N){
int max=*max_element(arr,arr+N);//max_element返回数组的最大值的指针
int min=*min_element(arr,arr+N);
int n = max-min+1;
int count[n]{0};//储存出现的次数,下标x为arr对应元素x,count[x]为出现次数
int temp[N];//储存正确排序数组
for(int i=0;i<N;i++)
count[arr[i]]++;
for(int i=1;i<N;i++)//更新count[i]为前缀和
count[i]=count[i-1]+count[i];
for(int i=N-1;i>=0;i--){
temp[count[arr[i]]]=arr[i];
count[arr[i]]--;
}
for(int i=0;i<N;i++)//复制正确数组到原数组
arr[i]=temp[i];
}
6.2分析
- 非比较排序
- 根据conut由后向前遍历,每输出一次count[x]-=1
- 适用于在一定范围内的整数(max与min差距不要过大)
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(n+k) | – | – | – | – | – | – | – | O(k) | 稳定 | – |
6.4应用
7.分配排序和基数排序
分配排序
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
– | – | – | – | – | – | – | – | – | – | – |
基数排序
LSD法:从低位到高位依次排序
int getMax(int* a,int n){
int idx=0;
int max=a[0];
for(int i=0;i<n;i++){
if(max<a[i])
max=a[i];
}
return max;
}
int getIndex(int a,int exp){
return a/exp;
}
int countSort_LSD(int* a,int n,int exp){
int temp[n];
int max=a[0],min=a[0];
int N=max-min+1;
int count[9]{};//每一位只有九个桶
for(int i=0;i<n;i++){//count[i]=检索位数exp为i的数字个数
count[getIndex(a[i],exp)]++;
}
for(int i=1;i<9;i++){//count[i]=前缀和
count[i]=count[i]+count[i-1];
}
for(int i=n-1;i>=0;i--){//temp:排好序的数组,count[]--,每输出一个,对应位置减一个
temp[count[getIndex(a[i],exp)]--]=a[i];
}
for(int i=0;i<n;i++){
a[i]=temp[i];
}
}
void radix_sort(int* a,int n){
int max=getMax(a,n);
for(int exp=1;max/exp>0;exp*=10){
countSort_LSD(a,n,exp);
}
}
平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|
O(d*n) | O(d*n) | 无 | O(d*n) | 无 | 无 | 无 | d | O(n+k) | 稳定 | 不一定 |
- d:最大位数,n:排序个数,k:桶的个数
- 稳定条件:从后向前输出
- 非比较排序
8.对各种排序算法的实验比较
总结
算法名称 | 平均 | 最坏 | 最坏条件 | 最好 | 最好条件 | 比较次数 | 移动次数 | 排序趟数 | 空间复杂度 | 稳定性 | 有序区 |
---|---|---|---|---|---|---|---|---|---|---|---|
插入 | – | – | – | – | – | – | – | – | – | – | – |
冒泡 | – | – | – | – | – | – | – | – | – | – | – |
选择 | – | – | – | – | – | – | – | – | – | – | – |
Shell | – | – | – | – | – | – | – | – | – | – | – |
归并 | – | – | – | – | – | – | – | – | – | – | – |
快排 | – | – | – | – | – | – | – | – | – | – | – |
堆 | – | – | – | – | – | – | – | – | – | – | – |
分配 | – | – | – | – | – | – | – | – | – | – | – |
基数 | – | – | – | – | – | – | – | – | – | – | – |
8.排序问题的下限
8.1
O ( n l o g n ) O(nlogn) O(nlogn)
8.2说明
8.2.1上限和下限问题
- 一个问题的上限定义为已知算法中速度最快的渐进时间代价
- 下限为解决这个问题所有算法的最佳可能效率,包括那些未设计出来的算法
8.2.2估计问题下限的一种简单方法
- 计算必须读入的输入长度及必须写出的输出长度。任何算法的时间代价当然都不可能小于它的I/O时间
8.2.3排序问题的下限
- 没有任何一种基于关键码比较的排序算法可以把最差执行时间降低到O(nlogn)以下
8.3证明
证明:在最差情况下,任何一种基于比较的算法都需要Ω(nlogn)的时间代价
8.3.1判定树(decision tree)
一棵可以模拟任何判定程序的二叉树
高度:h
比较元素个数:n
叶结点:n!(如果是完全二叉树,最多 2 h 2^h 2h)(二叉树第i层最多有 2 i 2^i 2i个结点,i=0,1,2,…)
8.3.2证明
2 h ≥ n ! h ≥ l o g 2 n ! ≥ l o g ( n e ) n = n l o g n − n l o g e ≈ n l o g n 2^h \geq n! \\ h \geq log_2 n! \geq log( \frac{n}{e})^n\\ = nlogn-nloge \approx nlogn 2h≥n!h≥log2n!≥log(en)n=nlogn−nloge≈nlogn