从零开始的排序

假期第一篇 \(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值