数据结构-排序

》排序:使一组任意排列的对象变成一组按关键字线性有序的对象。

        排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。

        -内部排序:整个排序过程不需要访问外存便能完成。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。

        -外部排序:参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

        -数据表(Data List):待排序的数据对象的有限集合。

        -关键字(Key):作为排序依据的数据对象中的属性域。

        -主关键字(Primary Key):是表中的一个或多个字段,它的值用于唯一地标识表中的某一条记录。

        -排序算法的稳定性:关键字相同的数据对象在排序过程中是否保持前后次序不变,不变则表示排序是稳定的,反之是不稳定的。

        -排序的时间开销:它是衡量算法好坏的最重要的标志。通常用算法执行中的数据比较次数和数据移动次数来衡量。

        -评价排序算法好坏的标准:时间复杂度和空间复杂度。另外算法本身的复杂程度也是需要考虑的一个因素。排序算法所需要的附加空间一般都不大,矛盾并不突出。而排序是一种经常执行的运算,往往属于系统的核心部分,因此排序的时间开销是算法好坏的最重要标志。

        -插入排序:将无序子序列中的一个或几个记录“插入"到有序序列中,从而增加记录的有序子序列的长度。

        -交换排序:通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将其加入到有序子序列中,以此方法增加记录的有序子序列的长度。

        -选择排序:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。

        -归并排序:通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。

》直接插入排序 (稳定,需要一个记录的辅助空间)   

        -时间性能分析:

        最好情况下(正序):比较次数>n-1    移动次数>0    时间复杂度为O(n)。

        最坏情况下(逆序):比较次数>(n+2)(n-1)/2    移动次数>(n+4)(n-1)/2    时间复杂度O(n^2)。

        直接插入排序算法简单、容易实现,适用于待排序记录基本有序或待排序记录较小时。当待排序的记录个数较多时,大量的比较和移动操作使直接插入排序算法的效率降低。

        -基本思想:

               当插入第i个对象时,前面的V[0],V[1],…,V[i-1]已经排好序,此时,用v[i]的关键字与V[i-1], V[i-2],…的关键字顺序进行比较,找到插入位置即将V[i]插入,原来位置上对象向后顺移。

        -一趟插入排序步骤:

                在R[1..i-1]中查找R[i]的插入位置, R[1..j].key≤R[i].key< R[j+1..i-1].key;
                将R[j+1..i-1]中的所有记录均后移一个位置;
                将R[i] 插入(复制)到R[j+1]的位置上。

                

       - 构造初始的有序序列:将第1个记录看成是初始有序表,然后从第2个记录起依次插入到这个有序表中,直到将第n个记录插入。

        -查找待插入记录的插入位置:在i-1个记录的有序区r[1] ~ r[i-1]中插入记录r[i],首先顺序查找r[i]的正确插入位置,然后将r[i]插入到相应位置。

        r[0]=r[i];j=i-1;    //r[0]充当哨兵

        while(r[0<r[j]]){

                 r[j+1]=r[j];    j--;  

        }//while


》表插入排序

        为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。


》折半插入排序(稳定,需要初始队列已经有序)

        因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。


》希尔排序(插入类,不稳定)

        又称缩小增量排序,在插入排序中,只比较相邻的结点,一次比较最多把结点移动一个位置。如果对位置间隔较大距离的结点进行比较,使得结点在比较以后能够一次跨过较大的距离,这样就可以提高排序的速度。希尔排序开始时增量较大,每个子序列中的记录个数较少,从而排序速度较快;当增量较小时,虽然每个子序列中记录个数较多,但整个序列已基本有序,排序速度也较快。
        希尔排序算法的时间性能是所取增量的函数,而到目前为止尚未有人求得一种最好的增量序列。
        研究表明,希尔排序的时间性能在O(n2)和O(nlog2n)之间。当n在某个特定范围内,希尔排序所需的比较次数和记录的移动次数约为O(n1.3 ) 。

        -基本思想:将整个待排序记录分割成若干个子序列,在子序列内分别进行直接插入排序,待整个序列中的记录基本有序时,对全体记录进行直接插入排序。

        -需要解决的关键问题:

        应如何分割待排序记录,才能保证整个序列逐步向基本有序发展?

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

            算法描述:for(d=n/2;d>=1;d=d/2){以d为增量,进行组内直接插入排序;}

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

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

            

》冒泡排序(交换类,稳定)

        基本思想:假设待排序的n个对象的序列为r[0],r[1],..., r[n-1],起始时排序范围是从r[0]到r[n-1]。在当前的排序范围之内,自右至左对相邻的两个结点依次进行比较,让值较大的结点往下移(下沉),让值较小的结点往上移(上冒)。每趟起泡都能保证值最小的结点上移至最左边,下一遍的排序范围为从下一结点到r[n-1]。在整个排序过程中,最多执行(n-1)遍。但执行的遍数可能少于(n-1),这是因为在执行某一遍的各次比较没有出现结点交换时,就不用进行下一遍的比较。
        

        实现算法:

                Bubble_Sort(rectype r[]){

                        int i,j,noswap;

                        rectype temp;

                        for(i=0;i<n-2;i++){

                                noswap=TRUE;

                                for(j=n-1;j>=i;j--){

                                      if(r[i+1].key<r[j]){

                                            temp=r[j+1];    r[j+1]=r[j];    r[j]=temp;    noswap=FALSE;

                                       }//if

                                       if(noswap) break;

                                 }//for

                        }//for

                }//Bubble_Sort

        时间性能分析:

            最好情况:初始状态是递增有序的,一趟扫描就可完成排序,关键字的比较次数为n-1,没有记录移动。

            最坏情况:若初始状态是反序的,则需要进行n-1趟扫描,每趟扫描要进行n-i次关键字的比较,且每次需要移动记录三次,因此,最大比较次数和移动次数分别为:(最大比较次数>O(n^2))(最大移动次数>O(n^2))。


》快速排序(交换类,不稳定)

        -基本思想:首先选一个轴值(即比较的基准),通过一趟排序将待排序记录分割成独立的两部分,前一部分记录的关键码均小于或等于轴值,后一部分记录的关键码均大于或等于轴值,然后分别对这两部分重复上述方法,直到整个序列有序。

        -选择轴值的方法:

                使用第一个记录的关键码;
                选取序列中间记录的关键码;
                比较序列中第一个记录、最后一个记录和中间记录的关键码,取关键码居中的作为轴值并调换到第一个记录的位置;
                随机选取轴值。

        -选取不同轴值的效果:决定两个子序列的长度,子序列的长度最好相等。

        -如何实现一次划分:

                取第一个记录的关键字值作为基准,将第一个记录暂存于temp中,设两个变量i,j分别指示将要划分的最左、最右记录的位置。
                将j指向的记录关键字值与基准值进行比较,如果j指向的记录关键字值大,则j前移一个位置;重复此过程,直到j指向的记录关键字值小于基准值;若i<j,则将j指向的记录移到i所指位置。
                将i指向的记录关键字值与基准值进行比较,如果i指向的记录关键字值小,则i后移一个位置;重复此过程,直到i指向的记录关键字值大于基准;若i<j,则i指向的记录移到j所指位置。
                 重复②、③步,直到i=j。

        -如何处理分割得到的两个待排序子序列:对分割得到的两个子序列递归的执行快速排序。

        -时间性能分析:

                最好情况:每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。

                最坏情况:每次划分只得到一个比上一次划分少一个记录的子序列(另一个子序列为空),为 O(n^2)。

                平均情况:O(nlog2n)。

  

》直接选择排序(不稳定)

        -基本思想:在一组对象V[i]到V[n-1]中选择具有最小关键字的对象。若它不是这组对象中的第一个对象,则将它与这组对象中的第一个对象对调。删除具有最小关键字的对象,在剩下的对象中重复第(1)、(2)步,直到剩余对象只有一个为止。

        

        -算法实现:

            Select_Sort(rectype r[]){

                    int i,j,k;

                    rectype temp;

                    for(i=0;i<n-1;i++){

                          k=i;

                          for(j=i+1;j<n;j++) if(r[j].key<r[k].key) k=j;

                          if(k!=i){

                               temp=r[i];    r[i]=r[k];    r[k]=temp;

                          }//if

                    }//for

            }//Select_Sort

        -时间性能分析:

            无论初始状态如何,在第i趟排序中选择最小关键字的记录,需做n-i次比较,因此总的比较次数为O(n^2)。

            当文件为正序时,移动次数为0,文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。

》堆排序(选择类,不稳定)

        -基本思想:直接选择排序的改进,研究如何减少关键码间的比较次数。若能利用每趟比较后的结果,也就是在找出键值最小记录的同时,也找出键值较小的记录,则可减少后面的选择中所用的比较次数,从而提高整个排序过程的效率。

        -堆的定义:

        

         在完全二叉树上,双亲和左右孩子之间的编号就是i和2i、2i+1的关系。因此一个序列可以和一棵完全二叉树对应起来,用双亲其左、右孩子之间的关系可以直观的分析是否符合堆的特性。

         

        -建堆的过程:对原始序列建堆过程,就是一个反复进行筛选的过程。通过对应的完全二叉树分析:对n个结点的完全二叉树,可以认为:以叶子为根的子树(只有它自己)已满足堆特性,因此从最后一个分支结点开始,把每棵子树调整为堆,直到根结点为止,整棵树成为堆。

        设初始排序序列:30  24  85 16  36 53 91 47 ,建成大顶堆。

        

        -输出堆元素:输出堆顶元素后,将堆底元素送入堆顶(或将堆顶元素与堆底元素交换),堆可能被破坏。破坏的情况仅是根结点和其左右孩子之间可能不满足堆的特性,而其左右子树仍然是局部的堆。在这种情况下,将其R1 … Ri整理成堆。(i=n-1..1)

        

        -怎样将剩余的n-1个元素按其关键码调整为一个新堆:

            假设有一个大根堆,当输出堆顶元素(根结点)后,以堆中最后一个元素替代它。此时根结点的左子树和右子树均为堆,则只需自上而下进行调整即可
             首先将堆顶元素与其左、右子树根结点的值进行比较,如果堆顶元素比它的两个子结点都大,则已经是堆;否则,让堆顶元素与其中较大的孩子结点交换,先让堆顶满足堆的性质。

             可能因为交换,使交换后的结点为根的子树不再满足堆的性质,则重复向下调整,当调整使新的更小子树依旧满足堆的性质时,重新建堆的过程结束。这种自上而下的建堆过程称为结点向下的“筛选”。

  

        -时间性能分析:

            对深度为 k 的堆,“筛选”所需进行的关键字比较的次数至多为2(k-1);

            对 n 个关键字,建成深度为h=log2n+1的堆,所需进行的关键字比较的次数至多4n;

            调整“堆顶” n-1 次,总共进行的关键字比较的次数不超过2(log2(n-1)+log2(n-2)+ …+log22)< 2n(log2n) ,因此,堆的时间复杂度O(nlogn)。

》归并排序

       基本思想:将两个或两个以上的有序子序列“归并” 为一个有序序列。在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列。

       算法实现:

            //将有序的记录序列sr[i..m]和sr[m+1..n]归并为有序的记录序列tr[i..n]

            void Merge(rcdtype sr[],rcdtype &tr[],int i,int m,int n){

                    for(j=m+1,k=i;i<=m&&j<=n;k++){

                            if(sr[i].key<=sr[j].key)tr[k]=sr[i++];    //将sr中记录由小到大地并入tr

                            else tr[k]=sr[j++];

                    }//for

            }//Merge

        -时间性能分析:

             对n个记录进行归并排序的时间复杂度为O(nlogn)。即:每一趟归并的时间复杂度为O(n),总共需进行log2n趟。


》基数排序(多关键字排序)

            基数排序是一种借助“多关键字排序”的思想来实现“单关键字排序”的内部排序算法。

            -多关键字排序:

                    n个记录的序列{ R1, R2, …,Rn}对关键字 (Ki0,Ki1,…,Kid-1) 有序是指:对于序列中任意两个记录 Ri 和 Rj(1≤i<j≤n) 都满足下列(词典)有序关系:(Ki0, Ki1,…,Kid-1)< (Kj0, Kj1,…,Kjd-1);其中: K0被称为 “最主”位关键字,Kd-1  被称为 “最次”位关键字。

            -最高位优先MSD法:

                    先对K0进行排序,并按 K0 的不同值将记录序列分成若干子序列之后,分别对 K1 进行排序,...…, 依次类推,直至最后对最次位关键字排序完成为止。

            -最低位优先LSD法:

                    先对 Kd-1 进行排序,然后对 Kd-2   进行排序,依次类推,直至对最主位关键字 K0 排序完成为止。

            //当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。

            //排序过程中不需要根据 “前一个”关键字的排序结果,将记录序列分割成若干个(“前一个”关键字不同的)子序列。

             

》各种排序方法综合比较

        除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法。这类排序法可能达到的最快的时间复杂度为O(nlogn)。(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制。)

        -平均的时间性能:

                时间复杂度为 O(nlogn):快速排序、堆排序和归并排序

                时间复杂度为 O(n2):直接插入排序、冒泡排序和简单选择排序

                时间复杂度为 O(n):基数排序

        -当待排记录序列按关键字顺序有序时:

                直接插入排序和冒泡排序能达到O(n)的时间复杂度,快速排序的时间性能蜕化为O(n2) 。

        -简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

        -空间性能:

             归并排序所需辅助空间最多,其空间复杂度为 O(n);链式基数排序需附设队列首尾指针,则空间复杂度为 O(rd)。


》外部排序

        待排序的记录数量很大,不能一次装入内存,否则无法利用前几节讨论的排序方法 (否则将引起频繁访问内存);

对外存中数据的读/写是以“数据块”为单位进行的;

        读/写外存中一个“数据块”的数据所需要的时间为:

                  TI/O = tseek +tla + n´ twm

        其中 tseek 为寻查时间(查找该数据块所在磁道); tla  为等待(延迟)时间;  n´ twm 为传输数据块中n个记录的时间。

        -外部排序的基本过程:

               按可用内存大小,利用内部排序方法,构造若干( 记录的) 有序子序列,通常称外存中这些记录有序子序列为“归并段”;通过“归并”,逐步扩大 (记录的)有序子序列的长度,直至外存中整个记录序列按关键字有序为止。

  

        外排总的时间还应包括内部排序所需时间和逐趟归并时进行内部归并的时间,显然,除去内部排序的因素外,外部排序的时间取决于逐趟归并所需进行的“趟数”。

        例如,若对上述例子采用5-路归并,则只需进行2趟归并,总的访问外存的次数将压缩到100+2*100=300次。

        一般情况下,假设待排记录序列含 m 个初始归并段,外排时采用 k-路归并,则归并趟数为logkm,显然,随之k的增大归并的趟数将减少,因此对外排而言,通常采用多路归并。k 的大小可选,但需综合考虑各种因素。