排序技术

排序的基本概念

排序:给定一组记录的集合{r1, r2, ……, rn},其相应的关键码分别为{k1, k2, ……, kn},排序是将这些记录排列成顺序为{rs1, rs2, ……, rsn}的一个序列,使得相应的关键码满足ks1≤ks2≤……≤ksn(称为升序)或ks1≥ks2≥……≥ksn(称为降序)。
正序:待排序序列中的记录已按关键码排好序。
逆序(反序):待排序序列中记录的排列顺序与排好序的顺序正好相反。
:在排序过程中,将待排序的记录序列扫描一遍称为一趟。
通常,一次排序过程需要进行多趟扫描才能完成
排序算法的稳定性:
假定在待排序的记录集中,存在多个具有相同键值的记录,
若经过排序,这些记录的相对次序仍然保持不变,
即在原序列中,ki=kj且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;
而对于稳定的排序算法,必须对算法进行分析从而证明稳定的特性。
需要注意的是,排序算法是否为稳定的是由具体算法决定的。
不稳定的算法在某种条件下可以变为稳定的算法,
而稳定的算法在某种条件下也可以变为不稳定的算法。
例:
冒泡排序是稳定的排序方法
5,3,3,4
如果规则是 A[i]>a[i+1],则是稳定
如果规则是A[i]>=a[i+1],则是不稳定的排序方法
快速排序原本是不稳定的排序方法,
但若待排序记录中只有一组具有相同关键码的记录,而选择的轴值恰好是这组相同关键码中的一个,此时的快速排序就是稳定的。
排序的分类-根据排序数据在内存中还是在外存中:
内排序:在排序的整个过程中,待排序的所有记录全部被放置在内存中
外排序:由于待排序的记录个数太多,不能同时放置在内存,而需要将一部分记录放置在内存,另一部分记录放置在外存上,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。
排序的分类-根据排序过程中所进行的基本操作分:

  1. 基于比较:基本操作——关键码的比较和记录的移动,其最差时间下限已经被证明为O(nlog2n)。
  2. 基于比较的内排序
  3. 插入排序
  4. 交换排序
  5. 选择排序
  6. 归并排序
  7. 不基于比较:根据关键码的分布特征。比如,桶式排序,基数排序(多关键字排序
    不基于比较的内排序
  8. 分配排序
  9. 桶式排序
    3.基数排序
    性能分析
    1.时间复杂性:基本操作。
    内排序在排序过程中的基本操作:
    ⑴比较:关键码之间的比较;
    ⑵移动:记录从一个位置移动到另一个位置。
    2.空间复杂性: 辅助存储空间。
    辅助存储空间是指在数据规模一定的条件下,除了存放待排序记录占用的存储空间之外,执行算法所需要的其他存储空间。
    排序算法的存储结构
    从操作角度看,排序是线性结构的一种操作,待排序记录可以用顺序存储结构或链接存储结构存储。

插入排序

插入排序的主要操作是插入,
其基本思想是:
每次将一个待排序的记录按其关键码的大小插入到一个已经排好序的有序序列中,直到全部记录排好序为止。

直接插入排序

基本思想:在插入第 i(i>1)个记录时,前面的 i-1个记录已经排好序。


void  insertSort (int  r[ ], int n){ 
   for (i=2; i<=n; i++)   { 
       r[0]=r[i]; j=i-1;//r[0]存入待插入元素
       while (r[0]<r[j])       {  
           r[j+1]=r[j];//遇到大于待插入元素的将其后移 
  j=j-1;//j继续向前推进 
        }
       r[j+1]=r[0]; //找打了待插入元素应该在的位置,赋值
    }
}

时间性能

在这里插入图片描述
在这里插入图片描述
空间性能:需要一个记录的辅助空间。
直接插入排序算法是一种稳定的排序算法。
直接插入排序算法简单、容易实现,适用于待排序记录基本有序或待排序记录个数较小的情况。
当待排序的记录个数较多时,大量的比较和移动操作使直接插入排序算法的效率降低。

希尔排序

改进的依据:
(1)若待排序记录按关键码基本有序时,直接插入排序的效率可以大大提高;
(2)由于直接插入排序算法简单,则在待排序记录数量n较小时效率也很高。
基本思想:
将整个待排序记录分割成若干个子序列,
在子序列内分别进行直接插入排序,
待整个序列中的记录基本有序时,对全体记录进行直接插入排序。

1、应如何分割待排序记录,才能保证整个序列逐步向基本有序发展?
分割的目的:
1、减少待排序记录个数
2、使整个序列向基本有序发展期
如何分割:

解决方法:
将相隔某个“增量”的记录组成一个子序列。
增量应如何取?
希尔最早提出的方法是d1=n/2,di+1=di/2。

2、子序列内如何进行直接插入排序?

解决方法:
在插入记录r[i]时,自r[i-d]起往前跳跃式(跳跃幅度为d)搜索待插入位置,并且r[0]只是暂存单元,不是哨兵。当搜索位置<0,表示插入位置已找到。
在搜索过程中,记录后移也是跳跃d个位置。
在整个序列中,前d个记录分别是d个子序列中的第一个记录,所以从第d+1个记录开始进行插入。

void shellsort(baishu r[],int n){
  for(int d=n/2;d>=1;d=d/2)//分割
   {   for(int i=d+1;i<=n;i++)对一个分割进行直接插入排序
      {
      int j=i-d;
      r[0]=r[i];
      while(j>0&&r[0]<r[j])
      {
       r[j+d]=r[j];
       j=j-d;
      }
      r[j+d]=r[0];
      }
   }
}
      

时间性能分析
希尔排序算法的时间性能是所取增量的函数,而到目前为止尚未有人求得一种最好的增量序列。
研究表明,希尔排序的时间性能在O(n2)和O(nlog2n)之间。当n在某个特定范围内,希尔排序所需的比较次数和记录的移动次数约为O(n1.3 ) 。

交换排序

交换排序的主要操作是交换,其主要思想是:在待排序列中选两个记录,将它们的关键码相比较,如果反序(即排列顺序与排序后的次序正好相反),则交换它们的存储位置。

冒泡排序

基本思想:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止。
在这里插入图片描述
⑴ 在一趟起泡排序中,若有多个记录位于最终位置,应如何记载?
⑵ 如何确定起泡排序的范围,使得已经位于最终位置的记录不参与下一趟排序?
⑶ 如何判别起泡排序的结束?

void BubbleSort(int r[],int n)
{
    int exchange=n;
    while(exchange)//最后一次交换的位置
    {
        int bound=exchange;
        exchange=0;//先让它取零,防止全部有序无法跳出循环。
        for(int j=1;j<bound;j++)
        {
            if(r[j]>r[j+1])
            {
                int t=r[j+1];
                r[j+1]=r[j];
                r[j]=t;
                exchange=j;//更新位置标记
            }
        }
    }
}

时间性能分析
最好情况(正序):
比较次数:n-1;
移动次数:0;
时间复杂度Q(n);
在这里插入图片描述平均情况:时间复杂度Q(n^2)
继续提高性能
双向起泡排序
在这里插入图片描述

int temp;
int    exchange1,exchange2;
int bound1,bound2;
exchange1=n-1; 
exchange2=0; 
while (exchange1) {
    exchange1=0;  
    bound1=exchange1;
    bound2=exchange2;
    for (int j=bound2; j<bound1; j++) 
        if (r[j]>r[j+1]){
 temp=r[j];
 r[j]=r[j+1];
 r[j+1]=temp;
 exchange1=j; //更新排序右截至点
 }
         bound1=exchange1;    
          for ( j=bound1; j>bound2; j--) 
     if (r[j]<r[j+1]) {
    temp=r[j];
    r[j]=r[j+1];
    r[j+1]=temp;
    exchange2=j; //更新排序左起点
    }
            bound2=exchange2;
}

快速排序

⑴如何选择轴值?
选择轴值的方法:
1.使用第一个记录的关键码;
2.选取序列中间记录的关键码;
3.比较序列中第一个记录、最后一个记录和中间记录的关键码,取关键码居中的作为轴值并调换到第一个记录的位置;
4.随机选取轴值。
选取不同轴值的后果:
决定两个子序列的长度,子序列的长度最好相等。
⑵如何实现分割(称一次划分)?
解决方法:
设待划分的序列是r[s] ~ r[t],设参数i,j分别指向子序列左、右两端的下标s和t,令r[s]为轴值,将轴值放入r[0]中。
(1)j从后向前扫描,直到r[j]<r[0],将r[j]移动到r[i]的位置,使关键码小(同轴值相比)的记录移动到前面去;
(2)i从前向后扫描,直到r[i]>r[0],将r[i]移动到r[j]的位置,使关键码大(同轴值比较)的记录移动到后面去;
(3)重复上述过程,直到i==j。
⑶如何处理分割得到的两个待排序子序列?
⑷如何判别快速排序的结束?

int Partition(int r[],int first,int end)
{//实现一次分割
    int i=first,j=end;
    r[0]=r[i];
    while(i<j)
    {
        while(i<j&&r[0]<=r[j]) j--;
        if(i<j)
        {
            r[i]=r[j];//将小的记录交换到前面
            i++;
        }
        while(i<j&&r[i]<=r[0]) i++;
        if(i<j)
        {
            r[j]=r[i];//将大的记录交换到后面
            j--;
        }
    }
    r[i]=r[0];//此时i=j,这个点前的数据全部小于这个点,这个点后的数据全部大于这个点
    return i;
}
void QuickSort(int r[],int first,int end)
{    if(first<end){//else递归出口
    int pivotpos=Partition(r,first,end);
    QuickSort(r,first,pivotpos-1);
    QuickSort(r,pivotpos+1,end);
}
}

时间性能分析
最好情况:每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。
最坏情况:每次划分只得到一个比上一次划分少一个记录的子序列(另一个序列为空),也就是说,总共要进行n-1趟排序,每趟排序又要进行n-i次比较,O(n2)。
平均情况:为O(nlog2n)。

选择排序

选择排序的主要操作是选择,其主要思想是:每趟排序在当前待排序序列中选出关键码最小的记录,添加到有序序列中。

简单选择排序

基本思想:第i 趟在n-i+1(i=1,2,…,n-1)个记录中选取关键码最小的记录作为有序序列中的第i个记录。
⑴如何在待排序序列中选出关键码最小的记录?
⑵如何确定待排序序列中关键码最小的记录在有序序列中的位置?

void SeletSort(int r[],int n)
{
    for(int i=1;i<=n;i++)
    {
      int index=i;//利用index变量记录关键码最小记录所在位置
        for(int j=i+1;j<=n;j++)
        {
            if(r[j]<r[index]) index=j;//找出关键码最小记录的位置
        }
        if(index!=i)
        {   int t=r[i];
            r[i]=r[index];
            r[index]=t;
        }
    }
}

时间性能分析
移动次数:
最好情况(正序):0次;
最坏情况:
在这里插入图片描述
简单排序的时间复杂度O(n2)
空间性能分析
空间性能:需一个辅助空间。
稳定性:根据上述算法,简单选择排序是不稳定的。(如果采用另外一个相同大小的存储空间,用来存储排序结果,则是稳定算法)

堆排序

改进着眼点:减小关键码间的比较次数,查找最小值的同时,找出较小值。
堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(称为小根堆),或每个结点的值都大于或等于其左右孩子结点的值(称为大根堆)。
堆和序列的关系:将堆用顺序存储结构来存储,则堆对应一组序列。
基本思想:
1.首先将待排序的记录序列构造成一个堆(大顶堆),
2.此时,选出了堆中所有记录的最大者,然后将它从堆中移走,
3.将剩余的记录再调整成堆,
4.这样又找出了次大的记录,以此类推,直到堆中只有一个记录。
⑴如何由一个无序序列建成一个堆(即初始建堆)?
⑵如何处理堆顶记录?
⑶如何调整剩余记录,成为一个新堆(即重建堆)?

void sift(int r[],int k,int m)
{//此时要调整的堆,起始编号K,结尾编号m;
    int i=k;int j=2*i;int temp=r[i];
    while(j<=m)
    {
        if(j<m&&r[j]<r[j+1]) j++;//选出左右孩子最大的
        if(temp>r[j]) break;//堆顶已经最大,完成选出最大值任务,退出循环。
        else{
            r[i]=r[j]; i=j; j=2*i;//给堆顶赋值,更新堆顶,更新左孩子。(因为本来就是堆顶的左右孩子就是堆,所以更新操作是为了方便,在下一次循环退出)
        }
    }
    r[i]=temp;
}
void HeapSort(int r[],int n)
{
    for(int i=n/2;i>=1;i--)
        sift(r,i,n);//又无序序列建立一个堆
    for(int i=1;i<n;i++)
    {
        int t=r[n-i+1];
        r[n-i+1]=r[1];//将堆顶取出
        r[1]=t;
        sift(r,1,n-i);//重新进行堆调整
    }
}

性能分析
总时间复杂度O(n)=nlogn;
空间代价:O(1);
堆排序不稳定

归并排序

归并:将两个或两个以上的有序序列合并成一个有序序列的过程。
归并排序的主要操作是归并,其主要思想是:将若干有序序列逐步归并,最终得到一个有序序列。

二路归并排序

基本思想:
将一个具有n个待排序记录的序列看成是n个长度为1的有序序列,
然后进行两两归并,
得到n/2个长度为2的有序序列,
再进行两两归并,得到n/4个长度为4的有序序列,
……,
直至得到一个长度为n的有序序列为止。
思考:
⑴如何将两个有序序列合成一个有序序列?

void Merge(int r[],int rl[],int s,int m,int t)
{
    int i=s;//第一个有序序列起点
    int j=m+1;//第二个有序序列起点
    int k=s;//装载序列起点
    while(i<=m&&j<=t)//按大小装载两个有序序列,其中一个有序序列装载完成就跳出循环
    {
        if(r[i]<=r[j]) rl[k++]=r[i++];
        else rl[k++]=r[j++];//cout+=j-k;cout初始等于0,这样cout就是逆序数个数,推导自己画图分析。
    }
    if(i<=m)//检查未完成装载的有序序列,并赋值到装载序列中
    {
        while(i<=m)
        {
            rl[k++]=r[i++];
        }
    }
    else
    while(j<=t)
        {
          rl[k++]=r[j++];
        }
}

⑵怎样完成一趟归并?

void Mergepass(int r[],int rl[],int n,int h)
{
    int i=1;
    while(i<=n-2*h+1)//两两归并有序序列
    {
        Merge(r,rl,i,i+h-1,i+2*h-1);
        i+=2*h;
    }
     if(i<n-h+1) Merge(r,rl,i,i+h-1,n);//剩下两个有序序列,最后一个有序序列长度不足h
    else for(int k=i;k<=n;k++)//剩下一个有序序列
    {
        rl[k]=r[k];
    }
}
    

⑶如何控制二路归并的结束?

void MergeSort(int r[],int rl[],int n)
{
int     h=1;
    while(h<n)
    {
        Mergepass(r,rl,n,h);
        h=2*h;
        Mergepass(rl,r,n,h);
        h=2*h;
    }
}

时间性能:
一趟归并操作是将r[1]r[n]中相邻的长度为h的有序序列进行两两归并,并把结果存放到r1[1]r1[n]中,这需要O(n)时间。整个归并排序需要进行 趟,因此,总的时间代价是O(nlog2n)。这是归并排序算法的最好、最坏、平均的时间性能。
空间性能:
算法在执行时,需要占用与原始记录序列同样数量的存储空间,因此空间复杂度为O(n)。

分配排序

基于分配和收集
基本思想
先将数据分配到不同的桶中
再将桶中的数据收集到一起
两种方法

桶式排序(单关键字排序)

假设待排序的记录的值在0~m-1之间
设置m个桶,
依次扫描待排序的记录,R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个箱子里(分配),
然后按序号依次将各非空的箱子首尾连接起来(收集)。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
 int b[1001],n,i,j,m=0,x;
 memset(b,0,sizeof(b));
 cin>>n;
 for(i=1;i<=n;i++)
 {cin>>x;
 b[x]++;//记录x出现次数
 }
 for(i=0;i<=1000;i++)
 if(b[i]>0) cout<<j<<" "<<b[j]<<endl;
 cout<<endl;
 return 0;

链式基数排序(多关键字排序)

如何表示桶?即,如何存储具有相同关键字的记录

struct Node{//结点
   int key;
   Node * next;
}
struct head//桶
{
 Node *first, *rear;
};

如何进行分配操作?

void distribute(Node *first, int n, head *list){//分配操作
 Node *p,*q;//node是一条包含所有元素的单链表
 p=first; int data;
 while(p) {
  data=p->data;  //桶的编号
  q=p->next;   
  if(list[data].first)//桶中已有元素
      list[data].rear->next=p;
  else//桶中放入第一个元素
      list[data].first=p;
  list[data].rear=p;//rear下移
  list[data].rear->next=NULL;
  p=q;//p下移
 } 
}

如何进行收集操作?

void collect( head *list, Node *&first,int m){
 int i=0,j;
 while(list[i].first==NULL)  i++;  //寻找第一个非空的桶
 if(i>m) return;
 first=list[i].first;//收集表的表头
    while(i<=m) {
           j=i+1;
            while(list[j].first==NULL) j++;//寻找下一个非空的桶
            if(j>m) return;
            list[i].rear->next=list[j].first;
            i=j;
         }
}

实例:

int  main(){
 Node *s,*first; head *list;
 int m;cin>>m;
 list=new head[m];
 for (int i=0;i<=m;i++){
  list[i].first=NULL; list[i].rear=NULL;}
 int n;
 cin>>n;
 first=NULL;
 for( i=0;i<n;i++)
 { s=new Node; cin>>s->data; s->next=first; first=s; }
 distribute(first,n,list);
 collect(list,first,m);
 Node *p=first;
 while(p){
  cout<<p->data<<"\t"; p=p->next; }
 cout<<endl;
 return 0;
}

特点:
需要较多的桶
时间复杂性
一次分配,O(n)//n结点数
一次收集,O(m)//桶数
O(n+m)
空间复杂性
O(m)
稳定性
稳定

基数排序

“花色”优先及高于“面值”
方法一:
先按花色分成4堆;
然后,每堆再按“面值”排;
最后,收成一堆。
“花色”优先及高于“面值”
方法二?
先按面值分成13堆;
收成一堆。
再按花色分成四堆,
最后,收成一堆。
基数排序是典型的LSD排序方法,利用“分配”和“收集”两种运算对单关键码进行排序。
基本思想
从关键字的最“低位”开始,将关键字分配到r(基数)个堆(桶)中;
按桶的编号将关键字收集起来;
然后,以“次低位”将关键字又分配到r个桶中;再收集,……,重复直到“最高位”为止,这时,以按关键字有序。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

void distribute(Node *first, int n, head *list,int d){
 Node *p,*q; p=first; int data, s,t;
  while(p) {
  data=p->data;
  s=pow(10,d);t=s/10;data=data%s;data=data/t;
  q=p->next;
  if( list[data].first)
  {list[data].rear->next=p; list[data].rear=p;}
  else
   list[data].first=list[data].rear=p;
  list[data].rear->next=NULL;
  p=q;
 }
}
//……
    int d;
 cout<<"Please input the size of a data:";
 cin>>d;
 for(i=1;i<=d;i++) //处理每一“位”(个位、十位...)
 {
  distribute(first,n,list ,i);
  collect(list,first,m);
  for (int i=0;i<=m;i++)
  {//每处理一遍就再初始化桶
   list[i].first=NULL; 
   list[i].rear=NULL; 
  }
 }

时间复杂性
多次分配,多次收集
一次分配的时间复杂性O(n)
一次收集的时间复杂性O(radix)
分配与收集的次数:数的位数(d)
O(d(n+radix))
空间复杂性
O(radix)
稳定性
稳定

分配排序特点
排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序
它们的时间复杂度可达到线性阶:O(n)。

在这里插入图片描述
在这里插入图片描述
稳定性比较
所有排序方法可分为两类,
(1)一类是稳定的,包括直接插入排序、起泡排序、和归并排序,基数桶式排序;
(2)另一类是不稳定的,包括直接选择排序、希尔排序、快速排序和堆排序。
简单些比较
从算法简单性看,
(1)一类是简单算法,包括直接插入排序、直接选择排序和起泡排序,
(2)另一类是改进后的算法,包括希尔排序、堆排序、快速排序、归并排序和基数排序,这些算法都很复杂

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值