数据结构-排序

排序

考虑大规模堆数据排序

只考虑内部排序:内存充分大,所有数据都能加载到内存中

排序的稳定性:任意两个相等的数据,排序前后的相对位置不发生改变

没有一种排序是任何情况下都是表现最好的

逆序对

  • 对于下标i<j,如果a[i]>a[j],则(i,j)为逆序对(inversion)
  • 交换两个相邻元素正好消去一个逆序对
  • 任意N个不同元素组成对序列平均具有 N ( N − 1 ) / 4 N(N-1)/4 N(N1)/4个逆序对。
  • 任意一个以交换两个相邻元素对排序算法,平均时间复杂度为 Ω ( N 2 ) \Omega(N^2) Ω(N2)(下界,最好情况)
  • 要提高算法效率,每次需要消去不止一个逆序对,每次要交换两个较远的元素。

以下默认int型升序排序

冒泡排序

int i,j;
for(i=0;i<N;i++){
   for(j=0;j<N-i-1;j++){
      ......
   }
}

for(i=N-1;i>=0;i--){
   for(j=0;j<i;j++){
       if(a[j]>a[j+1]){
           swap(a[j],a[j+1]);
           flag = 1;
       }
       if(!flag)// 如果一趟下来没有交换,则已经全部排好。
         break;
   }
}

最好 T = O ( n ) T=O(n) T=O(n),最坏 T = O ( n 2 ) T=O(n^2) T=O(n2) ,稳定
后面是有序的,前面无序。
便于链表排序

插入排序

for(i=1;i<N;i++){//默认一个元素是有序的
tem = a[i];
  for(j=i;j>0 && a[j-1]>tem;j--){
     a[j] = a[j-1];
  }
  a[j] = tem;
}

最好 T = O ( n ) T=O(n) T=O(n),最坏 T = O ( n 2 ) T=O(n^2) T=O(n2) ,稳定
前面是有序的,后面无序。比冒泡排序交换次数少。

若序列基本有序,则插入简单高效

希尔排序(shell)

为了提高算法效率,将每次交换的元素间隔拉大。

  • N间隔排序:对i,i+N,i+2N,…,i+nN这些元素进行排序,然后再对i+1做上述操作,直到i=N。每次调整的是整个序列的一个子序列。然后将N变小,重复上述操作。为了保证序列有序,最后N必须等于1。
  • N间隔排序后,在进行(N/2)间隔排序后,依然保持N间隔有序。
//原始希尔排序
for(i=N/2;i>0;i/=2){// i为间隔
   for(j=i;j<N;j++){ // 插入排序
       tem = a[j];
       for(k=j;k>=0 && a[k-i]>tem;k-=i){
           a[k]=a[k-i];
       }
       a[k]=tem;
   }
}

最坏情况 T = θ ( N 2 ) T=\theta(N^2) T=θ(N2) (既是上界又是下界,即等价)
由于间隔序列不互斥,会导致小的间隔无效(即无元素交换)
Hibbard增量序列
D k = 2 k − 1 D_k=2^k-1 Dk=2k1相邻元素互斥
swdgewick增量序列
4 i − 3 ∗ 2 i + 1 4^i-3*2^i+1 4i32i+1
效果较好

选择排序

for(i=0;i<M;i++){
  for(j=i;j<N;j++)
     if(a[j]<min)
        min = a[j];
     swap(a[i],min);
}

T = θ ( N 2 ) T=\theta(N^2) T=θ(N2)
选择排序在交换两元素时不一定是交换相邻两个元素,故破坏了稳定性

堆排序

  1. 建堆 T ( N ) = O ( N l o g N ) T(N)=O(NlogN) T(N)=O(NlogN) ,需要额外空间 O ( n ) O(n) O(n),且复制元素需要时间
  2. 建最大堆,将最大元素放到当前堆末尾。平均比较次数 2 N l o g N − O ( N l o g l o h N ) 2NlogN-O(NloglohN) 2NlogNO(NloglohN)

实际效果不如swdgewick增量序列堆希尔排序

归并排序

在这里插入图片描述

核心:有序子列堆归并
T ( N ) = O ( N ) T(N)=O(N) T(N)=O(N),但元素需要不停但复制,且需要额外的空间,不常用于内排序,常用于外排序。
稳定的排序

// 递归  

void M(int a[],int tem[],int l,int r,int e){
    int tem = l,m = r-1,beg= l;
    while(l<= m && r<= e){
      if(a[l]  <= a[r]){ //保证稳定性
         tem[in]=a[l];
         l++
      }elsr{
         tem[in]= a[r];
         r++;
      }
       in++;
    }
 while(l<=m){
   tem[in]=a[l];
   in++;
   l++;
 }
 while(r<=e){
   tem[in]=a[r];
   r++;
   in++;
 }

    for(i=beg;i<=e;i++)// 将数据倒回原来倒数组
       a[i]=tem[i];
}

void sort(int a[],int tem[],int b,int e){
   int m = (b+e)/2;
   if(b<e){
       sort(a,tem,b,m);
       sort(a,tem,m+1,e);
       m(a,tem,a,m+1,e);
   }
}

// 非递归
// 合并子列
void M1(int a[],int tem[],int l,int r,int e){
     int m=r-1,in= l;
     while(l<=m && r<=e){
       if(a[r]>a[l]){
          tem[in] = a[r];
          r++;
       }else{
         tem[in] = a[l];
         l++;
       }
       in++;
     }
     while(r<=e){
       tem[in]= a[r];
       in++;
       r++;
     }
     while(l<=m){
      tem[in]= a[l];
      in++;
      l++;
     }
}
// 合并长度为len的全部子列
void M_pass(int a[],int tem[],int n,int len){
   for(i=0;i<n-2*len;i+=2*len)
      M1(a,tem,i,i+len,i+2*len-1);
    if(i+len <n)//最后剩一个完整倒和一个不完整的字串
      M1(a,in,i,i+len,n-1);
     else// 只剩一个不完整的字串
       for(j=i;j<n;j++)
         tem[j] = a[j];
}

void sort(int a[],int N){
   int tem[N],len=1;
   while(len<N){
      M_pass(a,tem,N,len);
      len *=2;
      M_pass(a,tem,N,len);
   }
}

快排

不是永远都是最好的。
且若实现的不好,则很慢

分而治之

选主元 pivot ,即选出中间数

当最好情况是主元是中位数,最坏是有序的。
选主元可以从头,中,尾三个数中取中位数。

快排快的原因之一,是主元直接放到了最终的位置上。

快排是用递归,不适合小规模数据。对于N<100,可能不如插入。
定义一个cutoff,小于该阈值,使用其他排序,如简单排序

//利用首,中,尾三个元素选主元
int median3(int a[],int l,int r){
    int m = (r+l)/2;
    // 保证左面最小,右面最大,实现排序
    if(a[l]>a[m])
      swap(a[l],a[m]);
    if(a[l]>a[r])
      swap(a[l],a[r]);
    // 以上保证左面最小,以下保证右面最大
    if(a[m]>a[r])
      swap(a[m],a[r]);
    swap(a[m],a[r-1]);
    // 下一步划分子集时,只需要考虑l+1,r-2之间的元素
    return a[r-1];
}
void q_sort(int a[],int l,int r){
int p;
   if(r-l >cutoff){// 使用快排
       p = median3(a,r,l); //选主元
       i=l ;
       j = r -1; 
       // 待划分的区域是i+1到r-2
        for(;;){
          while(a[++i]<p){}
          while(a[--j]>p){}
          if(i<j)
             swap(a[i],a[j]);
           else 
             break;
       }
   }else{ //规模太小,使用其他排序,如插入排序
        other_sort(a,l,r);
   }
}

void sort(int a,int l){
   q_sort(a,0,l-1);
}

表排序

对比较复杂对元素进行排序,如复杂的结构体,不能不考虑交换元素之间的时间。只是移动对应的指针。是间接排序,利用一个新数组做为表(table),表中存放实际元素对应的指针,只需将指针排序即可。

若需要将元素按顺序排,而不仅仅是排指针,则需要移动元素,如何使移动元素所消耗时间最短是要解决的问题。下面给出线型时间复杂度对移动算法。
N个数字的排列是由若干个环组成的,如下图,红色为一个环,绿色为一个环,蓝色为一个环。
在这里插入图片描述
每次移动只对一个环内元素进行移动,首先需要保存环上第一个元素对应对数据,将其他元素调到对应对位置,每次移动一个元素后,将其table值设为对应对下标,即table[i]=i;当遇到table[i]=i时,一个环结束。

最好情况,有序
最坏情况:每个环有两个元素。

基数排序

实现某种情况下的线性时间复杂度。

桶排序

n个数据排序,只有m种取值,且n>>m,建立n个桶,扫描一次n个数据,将数据放到对应的桶中,近似线性。

当m>>n时,需要基数排序,实现近似线性复杂度。
当n=10,m=1000,使用次位优先(Least Significant digit)。用最低位进行排序,几位数决定需要排几次,每次拍完后,按序去取出在这里插入图片描述
也可以对应多种排序方式

总结

在这里插入图片描述
一次后位置固定:冒泡,插入

reference

浙大 数据结构 mooc https://www.icourse163.org/course/ZJU-93001

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值