假期第一篇 \(Blog\) 是鸽了一学期的排序233
下面将介绍一些常见的排序及各种优化~
- 插入排序 \((Insertion\) \(Sort)\)
- 选择排序 \((Selection\) \(Sort)\)
- 冒泡排序 \((Bubble\) \(Sort)\)
- 希尔排序 \((Shell\) \(Sort)\)
- 堆排序 \((Heap\) \(Sort)\)
- 快速排序 \((Quick\) \(Sort)\)
- 归并排序 \((Merge\) \(Sort)\)
- 基数排序 \((Radix\) \(Sort)\)
- 基于中序遍历的平衡树排序 \((BBST\) \(Sort)\)
关于排序原理,首先,关于这方面网上有太多的参考资料了,我在这里给出的都是用自己的语言组织的、尽可能精简的版本;其次,这篇 \(Blog\) 仅是为了总结这些排序算法,要说重点那也是落在排序优化上。因此,读者需要对这些排序有一个初步了解为好。
关于代码,参考 \(STL\) 的 \(sort\) 函数
函数均以
sort(int *head,int *tail,bool cmp(int,int))
的形式声明
表示对 \([head,tail)\) 内的元素进行排序
其他类似函数也表示对这种半闭半开区间的序列的操作
默认的 \(cmp\) 函数与 \(STL\) 保持一致
inline bool cmp(int a,int b){return a<b;}
默认宏定义
#define rep(i,l,u,d) for(register int i=(l);i<=(u);i+=(d)) #define reb(i,u,l,d) for(register int i=(u);i>=(l);i-=(d))
此外,一些功能较为简单的子函数此处就不给代码了 其实是懒
关于偏序关系,为避免麻烦,默认为 \(<\)
其他地方也采用通俗的描述,如:“最小元”直接叫“最小值”
插入排序、选择排序、冒泡排序这三种是最基本的排序算法
原理各一话概括,不再赘述:
插入排序:不断将无序序列的元素插入到有序序列中
选择排序:不断选择最小(大)值
冒泡排序:不断交换相邻的两个无序元素(无序序偶)
复杂度和稳定性分析:
排序方式 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
插入排序 | \(O(n^2)\) | \(O(1)\) | 稳定 |
选择排序 | \(O(n^2)\) | \(O(1)\) | 不稳定 |
冒泡排序 | \(O(n^2)\) | \(O(1)\) | 稳定 |
Code
插入排序
void insertion_sort(int *head,int *tail,bool (*cmp)(int,int)){ for(int *i=head+1;i<tail;i++){ int *p=i-1; while(p>=head&&cmp(*i,*p)) p--; int tmp=*i; for(int *j=i;j>p+1;j--) *j=*(j-1); p[1]=tmp; } }
找插入位置时可以二分查找
p=upper_bound(head,i,*i)-1;
选择排序
void selection_sort(int *head,int *tail,bool (*cmp)(int,int)){ for(int *i=head;i<tail-1;i++){ int *p=i; for(int *j=i+1;j<tail;j++) if(cmp(*j,*p)) p=j; if(i<p) swap(*i,*p); } }
冒泡排序
void bubble_sort(int *head,int *tail,bool (*cmp)(int,int)){ for(int *i=tail-1;i>head;i--) for(int *j=head;j<i;j++) if(cmp(j[1],*j)) swap(*j,j[1]); }
希尔排序
原理:按增量(或者说步长)分组进行插入排序,不断减小增量至一(此时相当于进行一次插入排序),因此希尔排序也叫缩小增量排序
希尔排序增量的减小方式会影响其复杂度,下面给出几种常见的增量序列
- \(Shell\) 增量:\({1,2,4,...,2^n}\)
- \(Hibbard\) 增量:\({1,3,7,...,2^n-1}\)
- \(Knuth\) 增量:\({1,4,13,...,\frac{3^n-1}{2}}\)
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(Shell\) 增量:\(O(n^2)\) \(Hibbard\) 增量:\(O(n^{\frac{3}{2}})\) \(Knuth\) 增量:\(O(n^{\frac{3}{2}})\) |
\(O(1)\) | 不稳定 |
Code
void shell_sort(int *head,int *tail,bool (*cmp)(int,int)){ //Hibbard增量:n=max{x|2^x-1<=tail-head},for(int i=2^n-1;i;(--i)>>=1) //Knuth增量:n=max{x|(3^x-1)/2<=tail-head},for(int i=3^n-1;i;(--i)/=3) //Shell增量 for(int i=(tail-head)>>1;i;i>>=1) for(int *j=head;j<head+i;j++) insertion_sort(j,tail,i); }
堆排序
原理:大(小)根堆不断取根。
堆排序一般采用数组建堆的方式,无需额外的辅助空间,需要的操作是 \(heapify\) ,可以理解为一般的堆的下沉操作。建堆时从最后一个父节点开始往前 \(heapify\) ,建完后每次取根时,将根与数组尾(头)交换,再 \(heapify\) 一次即可
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(O(nlogn)\) | \(O(1)\) | 不稳定 |
Code
void heap_sort(int *head,int *tail,bool (*cmp)(int,int)){ reb(i,(tail-head)>>1,1,1) heapify(head,tail,head+i-1); for(int *i=tail-1;i>head;i--) swap(*head,*i),heapify(head,i,head); } void heapify(int *head,int *tail,int *rt){ int far=rt-head,son=(far<<1)+1,tmp=*rt; while(son<tail-head){ if(son+1<tail-head&&cmp(head[son],head[son+1])) son++; if(cmp(head[son],tmp)) break; head[far]=head[son],far=son,son=(far<<1)+1; } head[far]=tmp; }
快速排序
原理:取无序序列中一元素为基准数,将小于它的元素移到它的左边,其它元素移到它的右边,然后再对它两边的序列递归进行此操作
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(O(n^2)\) 平均情况:\(O(nlogn)\) |
\(O(n)\) 最好情况:\(O(logn)\) |
不稳定 |
Code
void quick_sort(int *head,int *tail,bool (*cmp)(int,int)){ if(head>=tail-1) return; int *lef=head,*rig=tail; while(true){ while(cmp(*(++lef),*head)&&lef<tail-1); while(!cmp(*(--rig),*head)&&rig>head); if(lef>=rig) break; swap(*lef,*rig); } if(head<rig) swap(*head,*rig); quick_sort(head,rig),quick_sort(rig+1,tail); }
当序列元素小于一定数量时改用其它排序会更具优势,如插入排序
if(tail-head<=bound){insertion_sort(head,tail);return;}
默认取序列首为基准数
在此基础上,可以用随机取数的方法
基准数可以随机选择序列中任一元素
int p=rand()%(tail-head);swap(*head,head[p]);
或者用三数取中的方法
取序列的首、尾、中间三个数,排序后再取中间的数为基准数
\(sorted \_ median\) 函数:将传入的三个指针指向元素排序,返回中间元素的指针
int *mid=head+((tail-head)>>1)-1; int *p=sorted_median(head,mid,tail); swap(*(head+1),*p),lef=head+1,rig=tail-1;
尾递归优化,可以在数据较极端时表现更好
\(partition\) 函数:选取基准数并划分好左右序列,返回基准数右边的数的指针(此时基准数在左右序列之间)
void quick_sort(int *head,int *tail,bool (*cmp)(int,int)){ if(head>=tail-1) return; while(head<tail-1){ int *pivot=partition(head,tail); if(pivot-head<tail-pivot) quick_sort(head,pivot),head=pivot; else quick_sort(pivot,tail),tail=pivot; } }
三向切分优化,每次将和基准数相等的元素都放到中间,再对两边的序列进行递归操作
void quick_sort(int *head,int *tail,bool (*cmp)(int,int)){ if(head>=tail-1) return; int *lef=head,*rig=tail-1,*p=head+1,val=*lef; while(p<=rig){ if(cmp(*p,val)) swap(*p,*lef),p++,lef++; else if(cmp(val,*p)) swap(*p,*rig),rig--; else p++; } quick_sort(head,lef),quick_sort(rig+1,tail); }
归并排序
原理:将两个有序序列每次取序列首较小者,合并成一个有序序列,递归完成该过程
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(O(nlogn)\) | \(O(n)\) | 稳定 |
Code
\(merge\) 函数:将左右两个有序序列合并成一个有序序列
void merge_sort(int *head,int *tail,bool (*cmp)(int,int)){ if(head>=tail-1) return; int *pivot=head+((tail-head)>>1); merge_sort(head,pivot),merge_sort(pivot,tail),merge(head,pivot,tail); } void merge(int *lef,int *mid,int *rig,bool (*cmp)(int,int)){ int *tmp=new int[rig-lef+1]; int *i=lef,*j=mid,p=0; while(i<mid&&j<rig) if(!cmp(*j,*i)) tmp[++p]=*(i++); else tmp[++p]=*(j++); while(i<mid) tmp[++p]=*(i++); while(j<rig) tmp[++p]=*(j++); rep(i,1,p,1) lef[i-1]=tmp[i]; delete[] tmp; }
非递归优化,序列的不断合并实际上是序列的长度倍增的过程,因此可以写成非递归形式
void merge_sort(int *head,int *tail,bool (*cmp)(int,int)){ int i=2,*j; for(;i<tail-head;i<<=1){ for(j=head;j<=tail-i;j+=i) merge(j,j+(i>>1),j+i); if(j>tail-i) merge(j,j+((tail-j)>>1),tail); } merge(head,head+(i>>1),tail); }
三重反转优化,左右两序列的合并过程,可以看作将右边序列中较小的那些元素整块地插入到左边的序列,这个过程可以用三重反转实现
比如要将 \([b,c]\) 插入到 \(a\) 的前面 (\(a<b\)) ,我们可以先反转 \([a,b-1]\) 和 \([b,c]\) ,再反转 \([a,c]\),将区间 \([a,b-1]\) 和 \([b,c]\) 对调
\(triple \_ reverse\) 函数:三重反转,对调左右区间
void merge(int *lef,int *mid,int *rig,bool (*cmp)(int,int)){ int *i=lef,*j=mid,*idx; while(i<j&&j<rig){ while(!cmp(*j,*i)) i++; if(i==j) break; idx=j; while(cmp(*j,*i)) j++; triple_reverse(i,idx,j),i+=j-idx; } }
基数排序
原理:将元素按部分关键码分配到有序的桶中,将桶之间排好序,并保持元素之间的这个相对位置继续进行排序,直至关键码全部相同
如果我们直接将基数取为比序列中最大值还要大的数,那么就相当于进行一次鸽巢排序(也就是平时说的桶排序,但实际上桶排序也可以指另一种排序)
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(O(d(n+r))\) 一般情况:\(d=digit(max(array)),r=10\) |
\(O(n+r)\) | 稳定 |
Code
const int radix=10; void radix_sort(int *head,int *tail,bool (*cmp)(int,int)){ int *tmp=new int[tail-head+1],*cnt=new int[radix]; int p=1,d=maxbit(head,tail); rep(i,1,d,1){ rep(j,0,radix-1,1) cnt[j]=0; for(int *j=head;j<tail;j++) cnt[*j/p%radix]++; rep(j,1,radix-1,1) cnt[j]+=cnt[j-1]; for(int *j=tail-1;j>=head;j--) tmp[cnt[*j/p%radix]--]=*j; rep(j,1,tail-head,1) head[j-1]=tmp[j]; p*=10; } delete[] tmp,cnt; }
关键码可以最低位优先 \((LSD)\) ,也可以最高位优先 \((MSD)\)
基于中序遍历的平衡树排序
原理:平衡树建好后(小于父节点 \(value\) 的在左子树,其他在右子树),中序遍历的顺序即是排序的顺序
复杂度和稳定性分析:
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
\(O(nlogn)\) | \(O(n)\) | 不稳定 |
这里用的是 \(AVL\) 树
Code
typedef struct _node{ int val,dep; _node *son[2]; _node(int v,int d,_node *lef=nullptr,_node *rig=nullptr); }node; node::_node(int v,int d,node *lef,node *rig):val(v),dep(d),son{lef,rig}{ } void bbst_sort(int *head,int *tail,bool (*cmp)(int,int)){ node *rt=nullptr; for(int *i=head;i<tail;i++) insert(rt,*i); int p=-1; mid_order(rt,head,p); tree_free(rt); } void insert(node *&rt,int val){ if(rt==nullptr){rt=new node(val,1);return;} int x=0; if(!cmp(val,rt->val)) x=1; insert(rt->son[x],val); if(depth(rt->son[x])-depth(rt->son[!x])==2) if((!x&&cmp(val,rt->son[0]->val))||(x&&!cmp(val,rt->son[1]->val))) rotate(rt,!x); else binary_rotate(rt,x); update(rt); } void mid_order(node *rt,int *ary,int &p){ if(rt==nullptr) return; mid_order(rt->son[0],ary,p),ary[++p]=rt->val,mid_order(rt->son[1],ary,p); } void tree_free(node *rt){ if(rt==nullptr) return; tree_free(rt->son[0]),tree_free(rt->son[1]),delete rt; } inline int depth(node *rt){ return rt?rt->dep:0; } inline void update(node *rt){ rt->dep=max(depth(rt->son[0]),depth(rt->son[1])); } inline void rotate(node *&rt,int x){ node *tmp=rt->son[!x]; rt->son[!x]=tmp->son[x],tmp->son[x]=rt; update(rt),update(tmp),rt=tmp; } inline void binary_rotate(node *&rt,int x){ rotate(rt->son[!x],x),rotate(rt,!x); }
以上qwq
下学期数据结构好像也要整这些东西,这里算是预习 复习 一下
接下来打算整理并查集和网络流的相关知识~敬请期待 可能会咕,反正没人看
来源:https://www.cnblogs.com/MakiseVon/p/11173661.html