假期第一篇 \(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
下学期数据结构好像也要整这些东西,这里算是预习 复习 一下
接下来打算整理并查集和网络流的相关知识~敬请期待 可能会咕,反正没人看