排序小结——4.选择排序

选择排序

 

     选择排序(Selection Sort)的基本思想是:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。
     常用的选择排序方法有直接选择排序和堆排序。

直接选择排序(Straight Selection Sort)

1、直接选择排序的基本思想

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

2、直接选择排序的过程
  对初始关键字为49、38、65、97、76、13、27和49的文件进行直接选择排序的过程【参见动画演示

3、算法描述
  直接选择排序的具体算法如下:


4、算法分析
(1)关键字比较次数
     无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需做n-i次比较,因此,总的比较次数为:
     n(n-1)/2=0(n2)

(2)记录的移动次数
     当初始文件为正序时,移动次数为0
     文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。
     直接选择排序的平均时间复杂度为O(n2)。

(3)直接选择排序是一个就地排序

(4)稳定性分析
     直接选择排序是不稳定的
   【例】反例[2,2,1]

 

 

堆排序

1、 堆排序定义

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

     若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
【例】关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足堆性质(1)和(2),故它们均是堆,其对应的完全二叉树分别如小根堆示例和大根堆示例所示。

 

2、大根堆和小根堆
     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。
     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。
  注意:
     ①堆中任一子树亦是堆。
     ②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。

3、堆排序特点
     堆排序(HeapSort)是一树形选择排序。
     堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系【参见二叉树的顺序存储结构】,在当前无序区中选择关键字最大(或最小)的记录。

4、堆排序与直接插入排序的区别
     直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
     堆排序可通过树形结构保存部分比较结果,可减少比较次数。

5、堆排序
    堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

(1)用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
    ……
直到无序区只有一个元素为止。

(2)大根堆排序算法的基本操作:
① 初始化操作:将R[1..n]构造为初始堆;
② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
  注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

(3)堆排序的算法:

 

(4) BuildHeap和Heapify函数的实现
 因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现。
① Heapify函数思想方法
 每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,只须调整以R[low]为根的树即可。
"筛选法"调整堆
  R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足堆性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为"筛选法"。
    具体的算法【参见教材】

②BuildHeap的实现
  要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。
  显然只有一个结点的树是堆,而在完全二叉树中,所有序号 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为 -1,…,1的结点作为根的子树都调整为堆即可。
     具体算法【参见教材】。

5、大根堆排序实例
     对于关键字序列(42,13,24,91,23,16,05,88),在建堆过程中完全二叉树及其存储结构的变化情况参见【动画演示】。

6、 算法分析
     堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
     堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。
     由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
     堆排序是就地排序,辅助空间为O(1),
     它是不稳定的排序方法。

 

 

下一页

四、归并排序

     归并排序(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是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

2、归并算法
 

 

归并排序

     归并排序有两种实现方法:自底向上和自顶向下。

1、 自底向上的方法
(1) 自底向上的基本思想
     自底向上的基本思想是:第1趟归并排序时,将待排序的文件R[1..n]看作是n个长度为1的有序子文件,将这些子文件两两归并,若n为偶数,则得到 个长度为2的有序子文件;若n为奇数,则最后一个子文件轮空(不

参与归并)。故本趟归并完成后,前 个有序子文件长度为2,但最

后一个子文件长度仍为1;第2趟归并则是将第1趟归并所得到的 个有

序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。
     上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为"二路归并排序"。类似地有k(k>2)路归并排序。

(2) 二路归并排序的全过程
  【参见动画演示

(3) 一趟归并算法
 
分析:
      在某趟归并中,设各子文件长度为length(最后一个子文件的长度可能小于length),则归并前R[1..n]中共有 个有序的子文件:R

[1..length],R[length+1..2length],…,
注意:
     调用归并操作将相邻的一对子文件进行归并时,必须对子文件的个数可能是奇数、以及最后一个子文件的长度小于length这两种特殊情况进行特殊处理:
  ① 若子文件个数为奇数,则最后一个子文件无须和其它子文件归并(即本趟轮空);
  ② 若子文件个数为偶数,则要注意最后一对子文件中后一子文件的区间上界是n。

  具体算法如下:
 
(4)二路归并排序算法

注意:
     自底向上的归并排序算法虽然效率较高,但可读性较差。

 

2、自顶向下的方法
     采用分治法进行自顶向下的算法设计,形式更为简洁。

(1)分治法的三个步骤
     设归并排序的当前区间是R[low..high],分治法的三个步骤是:
①分解:将当前区间一分为二,即求分裂点
                

②求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序;
③组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]。
  递归的终结条件:子区间长度为1(一个记录自然有序)。

(2)具体算法
    void MergeSortDC(SeqList R,int low,int high)
     {//用分治法对R[low..high]进行二路归并排序
       int mid;
       if(low<high){//区间长度大于1
          mid=(low+high)/2; //分解
          MergeSortDC(R,low,mid); //递归地对R[low..mid]排序
          MergeSortDC(R,mid+1,high); //递归地对R[mid+1..high]排序
          Merge(R,low,mid,high); //组合,将两个有序区归并为一个有序区
        }
     }//MergeSortDC

(3)算法MergeSortDC的执行过程
     算法MergeSortDC的执行过程如下图所示的递归树。

二、算法分析

1、稳定性

      归并排序是一种稳定的排序。

2、存储结构要求
     可用顺序存储结构。也易于在链表上实现。

3、时间复杂度
     对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

4、空间复杂度
     需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
  注意:
     若用单链表做存储结构,很容易给出就地的归并排序。具体【参见练习】。

 

 

下一页

四、归并排序

     归并排序(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是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

2、归并算法
 

 

归并排序

     归并排序有两种实现方法:自底向上和自顶向下。

1、 自底向上的方法
(1) 自底向上的基本思想
     自底向上的基本思想是:第1趟归并排序时,将待排序的文件R[1..n]看作是n个长度为1的有序子文件,将这些子文件两两归并,若n为偶数,则得到 个长度为2的有序子文件;若n为奇数,则最后一个子文件轮空(不

参与归并)。故本趟归并完成后,前 个有序子文件长度为2,但最

后一个子文件长度仍为1;第2趟归并则是将第1趟归并所得到的 个有

序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。
     上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为"二路归并排序"。类似地有k(k>2)路归并排序。

(2) 二路归并排序的全过程
  【参见动画演示

(3) 一趟归并算法
 
分析:
      在某趟归并中,设各子文件长度为length(最后一个子文件的长度可能小于length),则归并前R[1..n]中共有 个有序的子文件:R

[1..length],R[length+1..2length],…,
注意:
     调用归并操作将相邻的一对子文件进行归并时,必须对子文件的个数可能是奇数、以及最后一个子文件的长度小于length这两种特殊情况进行特殊处理:
  ① 若子文件个数为奇数,则最后一个子文件无须和其它子文件归并(即本趟轮空);
  ② 若子文件个数为偶数,则要注意最后一对子文件中后一子文件的区间上界是n。

  具体算法如下:
 
(4)二路归并排序算法

注意:
     自底向上的归并排序算法虽然效率较高,但可读性较差。

 

2、自顶向下的方法
     采用分治法进行自顶向下的算法设计,形式更为简洁。

(1)分治法的三个步骤
     设归并排序的当前区间是R[low..high],分治法的三个步骤是:
①分解:将当前区间一分为二,即求分裂点
                

②求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序;
③组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]。
  递归的终结条件:子区间长度为1(一个记录自然有序)。

(2)具体算法


(3)算法MergeSortDC的执行过程
     算法MergeSortDC的执行过程如下图所示的递归树。

二、算法分析

1、稳定性

      归并排序是一种稳定的排序。

2、存储结构要求
     可用顺序存储结构。也易于在链表上实现。

3、时间复杂度
     对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

4、空间复杂度
     需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
  注意:
     若用单链表做存储结构,很容易给出就地的归并排序。具体【参见练习】。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值