zoukankan      html  css  js  c++  java
  • 每周一道数据结构(二)排序总结

    排序

     所谓排序,就是要整理文件中的记录,使之按关键字递增(或递减)次序排列起来。

      排序是数据处理中经常使用的一种重要运算。在计算机及其应用系统中,花费在排序上的时间在系统运行时间中占有很大比重,并且排序本身对推动算法分析的发展也起很大作用。目前已有上百种排序方法,但并没有一个万能的排序方法来解决所有问题,接下来介绍几种常用的排序方法,并对它们进行分析和比较。

    分类

    1.按是否涉及数据的内、外存交换

    •   内排序
      •   在排序过程中,若整个文件都是放在内存中处理,排序时不涉及数据的内、外存交换,则称之为内部排序。
    •   外排序
      •   若排序过程中要进行数据的内、外存交换,则称之为外部排序。

    2.按策略划分内部排序方法

    •     插入排序
    •     选择排序
    •     交换排序
    •     归并排序
    •     分配排序

    排序算法性能评价

    评价排序算法好坏的标准主要有两条:

    1. 执行时间和所需的辅助空间
    2. 算法本身的复杂程度

    排序算法的时间复杂度:

      大多数排序算法的时间开销主要是关键字之间的比较和记录的移动。有的排序算法其执行时间不仅依赖于问题的规模,还取决于输入实例中数据的状态。

    排序算法的空间复杂度:

      若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间是O(1),则称之为就地排序(In-PlaceSou)。非就地排序一般要求的辅助空间为O(n)。

    插入排序

      插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。

      常用的插入排序:直接插入排序 希尔排序

    1.直接插入排序

       排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。

      时间复杂度为O(n^2),空间复杂度为O(1),稳定的。

    伪码:

    //直接插入排序
    void straightSelectSort(Record Array[], int n)
    {
        for(i = 0~n-1)
            Small = Array[i];
            for(j = i+1~n-1)
                if(Small > Array[j])
                    Small=Array[j];
            swap(Small,Array[i]);
    }

    2.希尔排序

       希尔排序(Shell Sort)又称为“缩小增量排序”。是1959年由D.L.Shell提出来的。该方法先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的。

      在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。

      希尔排序是不稳定的。

    伪码:

    void Insort(int a[],int n, int x)//x为步长,n为序列长度
    {
        for (int i=x;i<n;i+=x){//对子序列中第i个元素进行插入排序(由于步长为x,故从第二个元素开始,也就是x开始)
            for(int j=i;j>=x;j-=x){
                if(a[j]<a[j-x]){
                    int tmp=a[j];
                    a[j]=a[j-x];
                    a[j-x]=tmp;
                }
            }
        }
    }
    
    void shellsort(int a[],int n)
    {
        for (int i=n/2;i>0;i/=2){//设置增量为2,也就是步长每次减少2
            for (int j=0;j<i;++j){//对步长为i的每个序列进行直接插入排序
                Insort(&a[j],n-j,i);
            }
        }
    }

    交换排序

       交换排序是通过两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。
         应用交换排序基本思想的主要排序方法有:冒泡排序快速排序

    1.冒泡排序

      将被排序的记录数组S[1..n]垂直排列,每个记录s[i]看作是重量为s[i]的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组s:凡扫描到违反本原则的轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。

      平均时间复杂度为O(n^2),空间复杂度为O(1),由于是交换式的所以稳定。

    代码:

    void BubbleSort(SeqList R)
    { //采用自下向上扫描,对R做冒泡排序
         int i,j;
         Boolean exchange; //交换标志
         for(i=1;i<n;i++){ //最多做n-1趟排序
           exchange=FALSE; //本趟排序开始前,交换标志应为假
           for(j=n-1;j>=i;j--) //对当前无序区R[i..n]自下向上扫描
            if(R[j+1].key<R[j].key){//交换记录
              R[0]=R[j+1]; //R[0]不是哨兵,仅做暂存单元
              R[j+1]=R[j];
              R[j]=R[0];
              exchange=TRUE; //发生了交换,故将交换标志置为真
             }
           if(!exchange) //本趟排序未发生交换,提前终止算法
                 return;
         } //endfor(外循环)
        } //BubbleSort

    2.快速排序

      设当前待排序的无序区为S[low..high],利用分治法可将快速排序的基本思想描述为:
      ①分解: 
        
     在S[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间S[low..pivotpos-1)和S[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。
        注意:
         划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=S[pivotpos]):
         S[low..pivotpos-1].keys≤S[pivotpos].key≤S[pivotpos+1..high].keys
                      其中low≤pivotpos≤high。
      ②求解: 
         
    通过递归调用快速排序对左、右子区间S[low..pivotpos-1]和S[pivotpos+1..high]快速排序。
      ③组合: 
        
     因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。

      在当前无序区中选取划分的基准关键字是决定算法性能的关键。

      由于快速排序的交换思想,可知它是不稳定的

      它的平均复杂度可以计算出为O(nlogn),空间复杂度为O(1)。

    代码:

     int Partition(SeqList R,int i,int j)
     {//调用Partition(R,low,high)时,对R[low..high]做划分,
         //并返回基准记录的位置
          ReceType pivot=R[i]; //用区间的第1个记录作为基准 '
          while(i<j){ //从区间两端交替向中间扫描,直至i=j为止
            while(i<j&&R[j].key>=pivot.key) //pivot相当于在位置i上
              j--; //从右向左扫描,查找第1个关键字小于pivot.key的记录R[j]
            if(i<j) //表示找到的R[j]的关键字<pivot.key
                R[i++]=R[j]; //相当于交换R[i]和R[j],交换后i指针加1
            while(i<j&&R[i].key<=pivot.key) //pivot相当于在位置j上
                i++; //从左向右扫描,查找第1个关键字大于pivot.key的记录R[i]
            if(i<j) //表示找到了R[i],使R[i].key>pivot.key
                R[j--]=R[i]; //相当于交换R[i]和R[j],交换后j指针减1
           } //endwhile
          R[i]=pivot; //基准记录已被最后定位
          return i;
        } //partition
    
    
    void QuickSort(SeqList R,int low,int high)
       { //对R[low..high]快速排序
         int pivotpos; //划分后的基准记录的位置
         if(low<high){//仅当区间长度大于1时才须排序
            pivotpos=Partition(R,low,high); //对R[low..high]做划分
            QuickSort(R,low,pivotpos-1); //对左区间递归排序
            QuickSort(R,pivotpos+1,high); //对右区间递归排序
          }
    } //QuickSort

    选择排序

      选择排序通过每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。
        常用的选择排序方法有直接选择排序堆排序

    1.直接选择排序

    n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:

       在无序区S[1..n]中选出关键字最小的记录S[k],将它与无序区的第1个记录S[1]交换,使S[1..1]和S[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。第i趟排序开始时,当前有序区和无序区分别为S[1..i-1]和S[i..n](1≤i≤n-1)。该趟排序从当前无序区中选出关键字最小的记录S[k],将它与无序区的第1个记录S[i]交换,使S[1..i]和S[i+1..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
         这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。

      平均时间复杂度为O(n^2),空间复杂度为O(1)。

      由于这个选择的关系,有可能会把相同元素的相对位置改变,故为不稳定的。

    代码:

    void SelectSort(SeqList R)
    {
       int i,j,k;
       for(i=1;i<n;i++){//做第i趟排序(1≤i≤n-1)
         k=i;
         for(j=i+1;j<=n;j++) //在当前无序区R[i..n]中选key最小的记录R[k]
           if(R[j].key<R[k].key)
             k=j; //k记下目前找到的最小关键字所在的位置
           if(k!=i){ //交换R[i]和R[k]
             R[0]=R[i];R[i]=R[k];R[k]=R[0]; //R[0]作暂存单元
            } //endif
         } //endfor
      } //SeleetSort

    2.堆排序

    1.堆的定义

     n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):
         (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤  )

    2.堆的分类

       堆可分为大根堆小根堆

       根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。
         根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。

    3.基本思想

      通常堆是通过一数组来实现的。在起始数组为 0 的情形中:

    • 堆的根节点(即堆积树的最大值)存放在数组位置 1 的地方;

        注意:不使用位置 0,否则左子树永远为 0[2]

    • 父节点i的左子节点在位置 (2*i);
    • 父节点i的右子节点在位置 (2*i+1);
    • 子节点i的父节点在位置 floor(i/2);

      在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:

    • 最大堆调整(Max_Heapify):将堆的末端子结点作调整,使得子结点永远小于父结点
    • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
    • 堆排序(HeapSort):移除位在第一个数据的根结点,并做最大堆调整的递归运算

    4.算法复杂度

       堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
         堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。

     代码:

    //筛选法
     void sift(int E[],int j,int length)
     {
         int i=j;
         int c = 2*i+1;//数据从0开始
     
         while(c < length)
         {
             if((c+1<length)&&(E[c]<E[c+1]))//左孩子<右孩子 时,取大的(右孩子)
                 c++;
             if(E[i]>E[c]) 
                 break;//此节点数据已经比孩子节点数据大 则停止循环
             else
             {
                 int t=E[i];
                 E[i]=E[c];
                 E[c]=t;
     
                 i=c;//继续重复上述操作,直到孩子节点小于此节点或到数的最后一层
                 c = 2*i+1;
             }
         }
     }
     //堆排序
     void HeapSort(int E[],int n)//第二个参数是数组长度
     {
         //初始化堆
         for(int i=n/2;i>=0;i--)//i=n/2是从倒数第二行开始
             sift(E,i,n);
     
         for(int i=0;i<n;i++)//所有的元素
         {
             //数组的0号位置与堆内剩余的数据中最后一个交换位置
             int t=E[n-i-1];
             E[n-i-1]=E[0];
             E[0]=t;
     
             sift(E,0,n-i-1);//每次都是数组的0号位置
         }
     }

    归并排序

      归并排序(Merge Sort)是利用"归并"技术来进行排序。归并是指将若干个已排序的子文件合并成一个有序的文件。

     1、算法基本思路

         设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。

    (1)合并过程

         合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
         重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。

    (2)动态申请R1

         实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

      平均时间复杂度为O(nlogn),空间复杂度为O(n),稳定的。

     代码:

    void Merge(SeqList R,int low,int m,int high)
    {//将两个有序的子文件R[low..m)和R[m+1..high]归并成一个有序的
         //子文件R[low..high]
         int i=low,j=m+1,p=0//置初始值
         RecType *R1; //R1是局部向量,若p定义为此类型指针速度更快
         R1=(ReeType *)malloc((high-low+1)*sizeof(RecType));
         if(! R1) //申请空间失败
           Error("Insufficient memory available!");
         while(i<=m&&j<=high) //两子文件非空时取其小者输出到R1[p]上
           R1[p++]=(R[i].key<=R[j].key)?R[i++]:R[j++];
         while(i<=m) //若第1个子文件非空,则复制剩余记录到R1中
           R1[p++]=R[i++];
         while(j<=high) //若第2个子文件非空,则复制剩余记录到R1中
           R1[p++]=R[j++];
         for(p=0,i=low;i<=high;p++,i++)
           R[i]=R1[p];//归并完成后将结果复制回R[low..high]
    } //Merge

     分配排序

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

      分配排序包括桶排序基数排序

    1.桶排序

      桶排序(Bucket Sort),其基本思想是:设置若干个桶,依次扫描待排序的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个桶里(分配),然后按序号依次将各非空的箱子首尾连接起来(收集)。

      箱子的个数取决于关键字的取值范围。

      桶排序的平均时间复杂度是线性的,即O(n)。但最坏情况仍有可能是O(n^2)。空间复杂度为O(m+n)。(m为每个桶的值域)

    伪码:

    void BucketSon(R)
    { //对R[0..n-1]做桶排序,其中0≤R[i].key<1(0≤i<n)
          for(i=0,i<n;i++) //分配过程.
            将R[i]插入到桶B[「n(R[i].key)」]中; //可插入表头上
          for(i=0;i<n;i++) //排序过程
            当B[i]非空时用插人排序将B[i]中的记录排序;
          for(i=0,i<n;i++) //收集过程
            若B[i]非空,则将B[i]中的记录依次输出到R中;
    }

    2.基数排序

       基数排序(Radix Sort)是当每个桶的值域区间很大时对桶排序的改进。

      将一个记录的值即排序码拆分为多个部分来进行比较。例如如果要对0~9999之间的整数进行排序,可以先按照千位数字进行桶排序,将所有数字分配到10个桶中,接下来,继续按照桶排序的方法对百位、十位、个位进行排序,这样,可以完成排序。这种将排序码拆分为多个字码分别来进行排序的方法就是基数排序。

    代码:  具体参见这里

    //数组实现
    #include<iostream>
    using namespace std;
    
    int data[10]={73, 22, 93, 43, 55, 14, 50, 65, 39, 81};
    int tmp[10];
    int count[10];
    int maxbit(int data[],int n)//取数据位数
    {
        int d=1;
        for(int i=0;i<n;i++)
        {
            int c=1;
            int p=data[i];
            while(p/10)
            {
                p=p/10;
                c++;
            }
            if(c>d)
                d=c;
        }
        return d;
    }
    
    void RadixSort(int data[],int n)
    {    
        int d=maxbit(data,n);//获取数据最大位数
            int r=1;
        for(int i=0;i<d;i++)
        {
        
            for(int i=0;i<10;i++)//装桶之前要先清桶--10个桶(0~9)
                count[i]=0;
            for(int i=0;i<n;i++) //记录每个桶的记录数
            {
                int k=data[i]/r;
                int q=k%10;
                count[q]++;//记录
            }
            for(int i=1;i<10;i++)//计算位置
            {
                count[i]+=count[i-1];
                //cout<<count[i]<<" ";
            }
            for(int j=n-1;j>=0;j--)
            {
                int p=data[j]/r;
                int s=p%10;
                tmp[count[s]-1]=data[j];//由于如果此位相同的数字有两个 那计数是从0开始的,所以它的位置就应该-1
                count[s]--;
                //cout<<data[j]<<" ";
            }
            for(int i=0;i<n;i++)
            {
                data[i]=tmp[i];
                //cout<<tmp[i]<<" ";
            }
        //    cout<<endl;
            r=r*10;//不断循环
    
        }
    
    }
    int main()
    {
        cout<<"基数排序c++实现"<<endl;
        //cout<<maxbit(data,10)<<endl;
        cout<<"排序之前的数值:";
        for(int i=0;i<10;i++)
            cout<<data[i]<<" ";
        cout<<endl;
        RadixSort(data,10);
        cout<<"排序之前的数值:";
            for(int i=0;i<10;i++)
            cout<<data[i]<<" ";
        cout<<endl;
    
    
        return 0;
    }

    导航:

  • 相关阅读:
    383. Ransom Note
    598. Range Addition II
    453. Minimum Moves to Equal Array Elements
    492. Construct the Rectangle
    171. Excel Sheet Column Number
    697. Degree of an Array
    665. Nondecreasing Array
    视频网站使用H265编码能提高视频清晰度吗?
    现阶段的语音视频通话SDK需要解决哪些问题?
    企业远程高清会议平台视频会议系统在手机端使用的必备要求有哪些?
  • 原文地址:https://www.cnblogs.com/coder2012/p/3088242.html
Copyright © 2011-2022 走看看