数据结构 第八章学习小结

数据结构第八章学习小结

  • 排序
    • 8.1 基本概念和排序方法概述
      • 排序的基本概念
        1. 排序:是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作
        2. 排序的稳定性:当排序记录中的关键字Ki(i = 1, 2, …, n) 都不相同时, 则任何一个记录的无序序列经排序后得到的结果唯一 ;反之, 当待排序的序列中存在两个或两个以上关键字相等的记录时, 则排序所得的结果不唯一。假设 Ki = Kj (1≤i≤n, 1 ≤j≤n, i≠j),且在排序前的序列中 Ri领先于千 Rj (即i<j)。若在排序后的序列中Ri仍领先于Rj丿,则称所用的排序方法是稳定的;反之, 若可能使排序后的序列中RJ领先千R;, 则称所用的排序方法是不稳定的。
        3. 内部排序和外部排序:一类是内部排序, 指的是待排序记录全部存放在计算机内存中进行排序的过程;另一类是外部排序,指的是待排序记录的数量很大,以致内存一次不能容纳全部记录, 在排序过程中尚需对外存进行访问的排序过程。
        View Code
      • 内部排序方法的分类
        内部排序的过程是一个逐步扩大记录的有序序列长度的过程。在排序的过程中, 可以将排序
        记录区分为两个区域:有序序列区和无序序列区。
        使有序区中记录的数目增加一个或几个的操作称为一趟排序。
        根据逐步扩大记录有序序列长度的原则不同, 可以将内部排序分为以下几类。
        (1) 插入类:将无序子序列中的一个或几个记录 "插入” 到有序序列中, 从而增加记录的有
        序子序列的长度。 主要包括直接插入排序、折半插入排序和希尔排序。
        (2) 交换类:通过 “交换“ 无序序列中的记录从而得到其中关键字最小或最大的记录,并将
        它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括冒泡排序和快速排序。
        (3) 选择类:从记录的无序子序列中 ”选择“ 关键字最小或最大的记录,并将它加入到有序子序
        列中, 以此方法增加记录的有序子序列的长度。 主要包括简单选择排序、树形选择排序和堆排序。
        (4) 归并类:通过 “归并“ 两个或两个以上的记录有序子序列, 逐步增加记录有序序列的长
        度。2-路归并排序是最为常见的归并排序方法。
        (s)分配类:是唯一一类不需要进行关键字之间比较的排序方法, 排序时主要利用分配和收
        集两种基本操作来完成。基数排序是主要的分配类排序方法。
        View Code
      • 待排序记录的存储方式
        (1) 顺序表:记录之间的次序关系由其存储位置决定, 实现排序需要移动记录。
        (2) 链表:记录之间的次序关系由指针指示,实现排序不需要移动记录,仅需修改指针即可。这种排序方式称为链表排序。
        (3) 待排序记录本身存储在一组地址连续的存储单元内, 同时另设一个指示各个记录存储位
        置的地址向量,在排序过程中不移动记录本身,而移动地址向撒中这些记录的 “地址”
        ,在排序结束之后再按照地址向量中的值调整记录的存储位置。这种排序方式称为地址排序。
        View Code
        • 待排序记录的数据类型定义
          #define MAXSIZE 20  //顺序表的最大长度
          typedef int KeyType; //定义关键字类型为整型
          typedef struct{ 
            KeyType key; //关键字项
            InfoType otherinfo; //其他数据项
          ) RedType; //记录类型
          
          typedef struct{ 
            RedType r[MAXSIZE+1]; //r[OJ闲置或用做哨兵单元
            int length; //顺序表长度
          ) SqList; //顺序表类型
          View Code
      • 排序算法效率的评价指标
        (1) 执行时间:
        对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上(这里, 只考虑以顺序表方式存储待排序记录),排序算法的时间复杂度由这两个指标(比较次数和移动次数)决定。
        (2)辅助空间:
        空间复杂度由排序算法所需的辅助空间决定。辅助空间是除了存放待排序记录占用的空间之外,执行算法所需要的其他存储空间。理想的空间复杂度为 0(1), 即算法执行期间所需要的辅助空间与待排序的数据量无关。
        View Code
    • 8.2 插入排序
      • 直接插入排序:
        每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
        • 算法
          void InsertSort(SqList &L) 
          {//对顺序表L做直接插入排序
           for(i=2;i<=L.length;++i) 
            if(L.r[i) .key<L.r[i-1] .key) //"<", 需将 r[i]插人有序子表
           { 
              L.r[O]=L.r[i);  //将待插人的记录暂存到监视哨中
              L.r[i]=L.r[i-1]; //r[i-1]后移
              for(j=i-2; L.r[O) .key<L.r[j] .key; --j)//从后向前寻找插入位置
                  L.r[j+l]=L.r[j];//记录逐个后移,直到找到插人位置
                  L.r[j+l]=L.r[O];//将 r[O]即原r习,插人到正确位置
             }
          }
          View Code
        • 算法分析
          (1)时间复杂度
          从时间来看, 排序的基本操作为: 比较两个关键字的大小和移动记录。
          1.在最好情况(正序:待排序序列中记录按关键字非递减有序排列)下,比较1次,不移动; 
          2.在最坏情况(逆序:待排序序列中记录按关键字非递增有序排列)
          下,比较i次(依次同前面的i-1个记录进行比较,并和哨兵比较1次),移动计1次(前面的i-1个记录依次向后移动,另外开始时将待插入的记录移动到监视哨中,最后找到插入位置,又从监视哨中移过去)。
          对于整个排序过程需执行 n-1趟,最好情况下,总的比较次数达最小值 n-1, 记录不需移
          动;最最坏情况下,总的关键字比较次数KCN和记录移动次数RMN均达到最大值,分别均为n^2/2。若待排序序列中出现各种可能排列的概率相同,则可取上述最好情况和最坏情况的平均情况。在平均情况下,直接插入排序关键字的比较次数和记录移动次数均约为n2
          /4。
          由此,直接插入排序的时间复杂度为0(n^2)。
          (2)空间复杂度
          直接插入排序只需要一个记录的辅助空间,[O]'所以空间复杂度为0(1)。
          (3)算法特点
          1.稳定排序。
          2.算法简便,且容易实现。
          3.也适用千链式存储结构,只是在单链表上无需移动记录,只需修改相应的指针。
          4.更适合于初始记录基本有序(正序)的情况,当初始记录无序, n较大时,此算法时间复杂度较高,不宜采用。
          View Code
      • 折半插入排序(二分查找)
        • 算法
          void Binsert Sort(SqList &L) 
          {//对顺序表L做折半插入排序
             for (i=2; i < =L. length; ++i)
             {
                L.r[O)=L.r[i); //将待插人的记录暂存到监视哨中
                low=l;high=i-1; //置查找区间初值
                while(low<=high) //在r[low .. high]中折半查找插入的位置
                {
                  m=(low+high)/2; //折半
                  if(L.r[O) .key<L.r[m) .key) high=m-1; //插入点在前一子表
                  else low=m+l;//插入点在后一子表
                 }
                 for (j=i一l;j>=high+l; --j) L.r[j+l]=L.r(j]; //记录后移
                 L.r[high+l]=L.r[O];//将r[O]即原r[i], 插入到正确位置
                }
          }
          View Code
        • 算法分析
          (1) 时间复杂度
          从时间上比较, 折半查找比顺序查找快,就平均性能来说,折半插入排序优于直接插入
          排序。
          折半插入排序所需要的关键字比较次数与待排序序列的初始排列无关,仅依赖于记录的个数。
          折半插入排序的对象移动次数与直接插入排序相同, 依赖千对象的初始排列。
          在平均情况下,折半插入排序 仅减少了关键字间的比较次数,而记录的移动次数不变。因此,折半插入排序的时间复杂度仍为 O(n^2)。
          (2) 空间复杂度
          折半插入排序所需附加存储空间和直接插入排序相同,只需要一个记录的辅助空间 r[O], 所
          以空间复杂度为O(1)。
          (3) 算法特点
          1.稳定排序。
          2.因为要进行折半查找, 所以只能用于顺序结构,不能用于链式结构。
          3. 适合初始记录无序、n较大时的情况。
          View Code
      • 希尔排序(缩小增量排序):
        实质上是采用分组插入的方法。先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。
        • 算法
          void Shellinsert(SqList &L, int dk) 
          {//对顺序表L做一趟增量是dk的希尔插入排序
              for(i=dk+l;i<=L.length;++i) 
              if(L.r[i) .key<L.r[i-dk) .key) //需将L.r[i]插入有序增扯子表
            {
                  L.r[O]=L.r[i];//暂存在L.r[O]
                  for(j=i-dk; j >O&& L.r[O] .key<L.r[j] .key;j-=dk)
                     L.r[j+dk]=L.r[j]; //记录后移, 直到找到插入位置
                  L.r[j+dk]=L.r[O];//将 r[O]即原r[i], 插入到正确位置
               }
          }
          void ShellSort(SqList &L,int dt[],int t) 
          {//按增批序列 dt[O .. t-1]对顺序表 L作t 趟希尔排序
            for (k=O;k<t;++k) 
            Shellinsert(L,dt[k]);//一趟增址为 dt[t]的希尔插入排序
          }
          View Code
        • 算法分析
          (1) 时间复杂度
          当 n 在某个特定范围内,希尔排序所需的比较和移动次数约为 nu ' 当 n-> oo时,可减少到 n(log2n)^2
          (2) 空间复杂度
          从空间来看, 希尔排序和前面两种排序方法一样,也只需要一个辅助空间r[O], 空间复杂度
          为0(1)。
          (3) 算法特点
          1.记录跳跃式地移动导致排序方法是不稳定的。
          2.只能用千顺序结构,不能用于链式结构。
          3.增量序列可以有各种取法,但应该使增量序列中的值没有除 l 之外的公因子,并且最后
          一个增量值必须等于1。
          4.记录总的比较次数和移动次数都比直接插入排序要少,n越大时,效果越明显。所以适
          合初始记录无序、n较大时的情况。
          View Code
    • 8.3 交换排序
      • 冒泡排序
        • 算法
          void BubbleSort(SqList &L) 
          {//对顺序表L做冒泡排序
          m=L.length-l;flag=l;  //flag用来标记某一趟排序是否发生交换
          while ((m>O) && (flag==1)) 
          { 
              flag=O; //flag置为0, 如果本趟排序没有发生交换,则不会执行下一趟排序
              for (j =1; j<=m; j ++)
                  if(L.r[j] .key>L.r[j+l].key)
                  {
                    flag=l; //flag置为1, 表示本趟排序发生了交换
                    t=L.r[j); L.r[j)=L.r[j+l); L.r[j+l)=t; 
                  }
                --m;
             }
          }  
          View Code
        • 算法分析
          (1)时间复杂度
          在平均情况下,冒泡排序关键字的比较次数和记录移动次数分别约为n^2/4 和 3n^2
          /4, 时间复杂度为O(n^2)
          (2)空间复杂度
          冒泡排序只有在两个记录交换位置时需要一个辅助空间用做暂存记录,所以空间复杂度
          为0(1)。
          (3)算法特点
          1. 稳定排序。
          2. 可用于链式存储结构。
          3.移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时,
          此算法不宜采用。
          View Code
      • 快速排序
        • 算法
          int Partition(SqList &L, int low, int high) 
          {//对顺序表1中的子表r[low .. high)进行一趟排序,返回枢轴位置 图8.5 快速排序的递归树
          L.r[O)=L.r[low]; //用子表的第一个记录做枢轴记录
          pivotkey=L.r[low] .key; //枢轴记录关键字保存在pivotkey中
          while(low<high) //从表的两端交替地向中间扫描
          {
          while(low<high&&L.r[high) .key>=pivotkey) --high;
          L.r[low)=L.r[high); //将比枢轴记录小的记录移到低端
          while(low<high&&L.r[low].key<=pivotkey) ++low;
          L.r[high)=L.r[low] ; //将比枢轴记录大的记录移到高端
          }
          L.r[low) =L.r[O];//枢轴记录到位
          return low;//返回枢轴位置
          }
          void QSort(SqList &L,int low,int high) 
          (//调用前置初值: low=l; high=L.length;
          //对顺序表L中的子序列L.r[low .. high]做快速排序
          if(low<high) { //长度大于1
          pivotloc=Partition (L, low, high); //将L.r[low.. high] 一分为二,pivotloc是枢轴位置
          QSort(L, low, pivotloc-1); //对左子表递归排序
          QSort (L, pivotloc+l, high); //对右子表递归排序
          }
          }
          void QuickSort(SqList &L) 
          {//对顺序表L做快速排序
          QSort(L,1,L.length);
          }
          View Code
        • 算法分析
          (1)时间复杂度
          平均情况下,快速排序的时间复杂度为O(nlog2 n)。
          (2)空间复杂度
          快速排序是递归的,执行时需要有一个栈来存放相应的数据。最大递归调用次数与递归树的
          深度一致,所以最好情况下的空间复杂度为O(log2n),最坏情况下为O(n)。
          (3)算法特点
          1.记录非顺次的移动导致排序方法是不稳定的。
          2.排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用千链式结构。
          3. 当n较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适
          合初始记录无序、 n较大时的情况。
          View Code
    • 8.4 选择排序
      • 简单选择排序(直接选择排序)
        • 算法
          void SelectSort(SqList &L) 
          {//对顺序表L做简单选择排序
          for(i=l;i<L.length;++i)//在L.r[i. .L. length]中选择关键字最小的记录
          {
            k=i; 
            for (j=i+l; j<=L. length; ++j) 
              if(L.r[j) .key<L.r[k) .key) k=j; //k指向此趟排序中关键字最小的记录
            if (k!=i) 
             {t=L.r[i); L.r[i)=L.r[k]; L.r[k)=t;}//交换r[i]与r[kl
            }
          }
          View Code
        • 算法分析
          (1)时间复杂度
          简单选择排序过程中,所需进行记录移动的次数较少。最好情况(正序):不移动;最坏情况逆序):移动3(n-1)次。无论记录的初始排列如何,所需进行的关键字间的比较次数相同,均为n^2/2。简单选择排序的时间复杂度也是O(n讥
          (2)空间复杂度
          同冒泡排序一样,只有在两个记录交换时需要一个辅助空间,所以空间复杂度为 0(1)。
          (3)算法特点
          1.就选择排序方法本身来讲,它是一种稳定的排序方法,但图 8.6 所表现出来的现象是不
          稳定的,这是因为上述实现选择排序的算法采用 “交换记录” 的策略所造成的,改变这个策略,可以写出不产生 ”不稳定现象” 的选择排序算法。
          2. 可用于链式存储结构。
          3.移动记录次数较少,当每一记录占用的空间较多时,此方法比直接插入排序快。
          View Code
      • 树形选择排序(锦标赛排序)
      • 堆排序
        • 筛选法调整堆算法
          void HeapAdjust(SqList &L,int s,int m) 
          {//假设r[s+l. .m)已经是堆 , 将r[s.. m)调整为以r[s)为根的大根堆
              rc=L.r[s];
              for (j= 2*s; j<=m; j*=2) //沿key较大的孩子结点向下筛选
              {
                if(j<m&&L.r[j].key<L.r[j+l] .key) ++j; //j为key较大的记录的下标
                if(rc.key>=L.r[j] .key) break; //re应插入在位置s上
                L.r[s]=L.r[j] ;s=j;
              }
              L.r[s]=rc; //插入
          }
          View Code
        • 建初堆算法
          void CreatHeap(SqList &L) 
          {//把无序序列L.r[l..n)建成大根堆
          n=L.length; 
          for(i=n/2;i>O; --i) //反复调用HeapAdjust
          HeapAdjust(L,i,n);
          }
          View Code
        • 堆排序算法
          void HeapSort(SqList &L) 
          {//对顺序表L进行堆排序
          CreatHeap(L); //把无序序列L.r [ 1.. L. length]建成大根堆
          for( i=L.length;i>l;--i)
          {
          x=L.r[l]; //将堆顶记录和当前未经排序子序列L.r[l..i]中最后一个记录互换
          L.r[l] =L.r[i];
          L.r[i]=x;
          HeapAdjust(L, l, i-1);//将L.r[l..1-l]重新调整为大根堆
          }
          }
          View Code
        • 算法分析
          (1) 时间复杂度
          堆排序在最坏的情况下,其时间复杂度也为O(nlog2n)。
          (2) 空间复杂度
          仅需一个记录大小供交换用的辅助存储空间,所以空间复杂度为0(1) 。
          (3) 算法特点
          (1. 是不稳定排序。
          2.只能用于顺序结构,不能用于链式结构 。 
          3.初始建堆所需 的比较次数较多,因此 记录数较少时不宜采用。堆排序在最坏情况下时间复杂度为O(nlog2n), 相对千快速排序最坏情况下的O(n^2)而言是一个优点,当记录较多时较为高效。
          View Code
    • 8.5 归并排序(内外部排序)
      • 算法
        1.相邻两个有序子序列的归并
        void Merge{RedType R[],RedType &T[],int low,int mid,int high) 
        {//将有序表 R[low.. mid]和R[mid+l. .high]归并为有序表 T[low.. high]
           i=low;j=mid+l;k=low; 
           while(i<=mid&&j<=high) //将R中记录由小到大地并入T中
        if (R[i). key<=R[j). key) T[k) =R[i++);
             else T[k)=R[j++);
            }
           while(i<=mid) T[k++]=R[i++]; //将剩余的 R[low.. mid]复制到 T中
           while (j<=high) T [k++]=R[j++];//将剩余的 R[j.high]复制到 T 中
        }
        
        2.归井排序
        void MSort(RedType R[],RedType &T[],int low,int high) 
        {//R [low .. high]归并排序后放人 T[low.. high]中
           if(low==high) T[low]=R[low]; 
           else 
          {
            mid=(low+high)/2; //将当前序列一分为二, 求出分裂点 mid
            MSort(R,S,low,mid); //对子序列 R[low.. mid]递归归并排序, 结果放入S[low .. mid]
            MSort(R,S,mid+l,high);//对子序列 R[mid+l.. high]递归归并排序, 结果放人 S[mid+ 1. . high]
            Merge(S,T,low,mid,high);//将S[low .. mid]和S[mid+l. .high]归并到 T[low .. high]
           }
        }
        
        void MergeSort(SqList &L) 
        {//对顺序表 L 做归并排序
           MSort(L.r,L.r,1,L.length);
        }
        View Code
      • 算法分析
        (1) 时间复杂度
        当有 个记录时, 需进行「log2nl趟归并排序, 每一趟归并, 其关键字比较次数不超过n, 元素移动次数都是n, 因此, 归并排序的时间复杂度为O(nlog2n)。
        (2) 空间复杂度
        用顺序表实现归并排序时, 需要和待排序记录个数相等的辅助存储空间, 所以空间复杂度为O(n)。
        (3) 算法特点
        1.是稳定排序。
        2.可用于链式结构, 且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
        View Code
    • 8.6 基数排序(分配类排序)
      • 多关键字排序
        • (1)最高位优先法
        • (2)最低位优先法
      • 链式基数排序
        • 类型定义及算法
          #define MAXNUM_KEY 8 //关键字项数的最大值
          #define RADIX 10 //关键字基数, 此时是十进制整数的基数
          #define MAX_SPACE 10000 
          typedef struct 
          { 
            KeysType keys [MAXNUM_KEY] ; //关键字
            InfoType otheritems; //其他数据项
            int next; 
          ) SLCell; //静态链表的结点类型
          typedef struct 
          { 
            SLCell r [MAX_SPACE]; //静态链表的可利用空间, r[OJ 为头结点
            int keynum; //记录的当前关键字个数
            int recnum; //静态链表的当前长度
          ) SLList; //静态链表类型
          typedef int ArrType[RADIX] //指针数组类型
          
          void Distribute(SLCell &r,int i,ArrType &f,ArrType &e) 
          {//静态链表 L 的 r 域中记录巳按 (keys[OJ, …, keys [i-1)) 有序
          //本算法按第 i 个关键字 keys[i)建立 RADIX 个子表,使同一子表中记录的 keys[i)相同
          //f[O .. RADIX-1)和 e[O.. RADIX-1)分别指向各子表中第一个和最后一个记录
          for(j=O;j<RADIX;++j) f[j)=O; //各子表初始化为空表
          for(p=r[O) .next;p;p=r[p) .next)
          { 
          j=ord(r[p] .keys[i]); //lord 将记录中第 J. 个关键字映射到[0 .. RADIX-1]
          if(!f(j]) f[j]=p; 
          else r[e[j]] .next=p; 
          e[j]=p; //将 p 所指的结点插人第 J 个子表中
          }
          }
          
          void Collect (SLCell &r, int i, ArrType f, ArrType e) 
          {//本算法按 keys[i)自小至大地将 f[O.. RADIX-1)所指各子表依次链接成一个链表
          //e[O .. RADIX-1)为各子表的尾指针
          for(j=O; !f[j); j=succ(j)); //找第一个非空子表, succ 为求后继函数
          r[OJ .next=f[j); t=e[j); //r[O] .next 指向第一个非空子表中第一个结点
          while{护:RADIX) 
          { 
             for {j=succ {j); j<RADIX-1&& ! f [j]; j=succ {j)); //找下一个非空子表
              if{f[j]) {r[t].next=f[j]; t=e[j];} //链接两个非空子表
          }//while 
             r[t] .next=O; //t指向最后一个非空子表中的最后一个结点
          }
          
          void RadixSort(SLList &L) 
           {//L是采用静态链表表示的顺序表
          //对 L 做基数排序,使得 L 成为按关键字自小到大的有序静态链表, L.r[O]为头结点
            for(i=O;i<L.recnum;++i) L.r[i] .next=i+l;
            L.r[L.recnum] .next = O; //将 L 改造为静态链表
            for(i=O;i<L.keynum;++i) 
            {  //按最低位优先依次对各关键字进行分配和收集
            Distribute(L.r,i,f,e);  //第i趟分配
            Collect(L.r,i,f,e);  //第i趟收集
            }
          }
          View Code
        • 算法分析
          (1) 时间复杂度
          对千n个记录(假设每个记录含d个关键字,每个关键字的取值范围为rd个值)进行链式基
          数排序时, 每一趟分配的时间复杂度为O(n), 每一趟收集的时间复杂度为O(rd), 整个排序需进行d趟分配和收集, 所以时间复杂度为O(d(n+ rd))。
          
          (2) 空间复杂度
          所需辅助空间为2rd个队列指针,另外由于需用链表做存储结构,则相对于其他以顺序结
          构存储记录的排序方法而言, 还增加了n个指针域的空间, 所以空间复杂度为O(n + rd)。
          
          (3) 算法特点
          1.是稳定排序。
          2.可用千链式结构, 也可用于顺序结构。
          3.时间复杂度可以突破基千关键字比较一类方法的下界 O(nlog2n), 达到 O(n)。
          4.基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值
          范围。
          View Code
    • 8.7 外部排序
      • 外部排序的基本方法
      • 多路平衡归并的实现
      • 置换-选择排序 
      • 最佳归井树 
    • 小结
  • 心得体会
    • 这一章很多排序方法,需要比较清晰的分析每一个排序所对应的时间复杂度、稳定性及算法等等,会有点难,但是其实了解了每一个的特点后分析就会很简单明了。最后,大一这年就快结束了,感觉很短,但是遇到xm老师教学好又认真而且人又nice真的很幸运![玫瑰] 期末也要加加油!好好考试,争取高分~ 最后的最后,希望老师以后也能多找我们玩,爱你!

 

posted @ 2020-07-12 21:27  iiiiiiiiH  阅读(346)  评论(0编辑  收藏  举报