讨论记录之排序

来源:互联网 发布:php include 变量 编辑:程序博客网 时间:2024/06/06 19:44

ParticipantsCPPZY ,LFHZP

Date08-09-20  7:20PM

Recorder: LFHZP

本次讨论重点讨论了 快速排序,推排序,基数排序,计数排序算法和比较树模型;

依次介绍各种算法,为了使得记录的完整性,加入了没有讨论的一些简单算法,如果发现错误请及时通知免得贻笑大方

由于待排序的记录数量不同,使得排序过程中设计的存储器不同,可将排序算法分为两大类:一类是内部排序,一类是外部排序;

PS:讨论完了还有橘子吃,突然感觉就进入了共产主义社会!呵呵

 直接插入排序

算法描述:

直接插入排序是一种简单的排序算法,它的基本操作是将一个记录插入到已排序的有序表中,从而得到一个新的、记录数加1 的有序表;

伪代码:

  1. Void InsertSort( int a[])
  2. {
  3.     for  int i= 1 to a.size
  4.     int  j = i -1;
  5.     int temp = a[i];
  6.     if temp< a[j]
  7.     {
  8.         for ( j; temp>a[j]  and j>= 0;j--)
  9.         {
  10.             a[j+1] = a[j];
  11.         }
  12.         a [j+1] = temp;
  13.     }
  14. }


效率分析:

时间复杂度:首先最外层循环要执行 n-1次,因为要将待排序列依次插入有序表中;对于第n次插入,至少要比较一次,最多要比较n次,最少要移动元素0次,最多要移动元素n+2次,所以算法最好的时间复杂度为On)(即已经排序好的序列)最坏的时间复杂度为On^2,也就是元素刚好逆序的时候;平均时间复杂度约为n2/4

空间复杂度:只需要一个元素的辅助空间,用于元素的位置交换O(1)

稳定性:稳定

结构复杂性和使用范围:此种排序算法适用于顺序存储结构,数组通过移动实现插入,链表这通过修改指针达到目的;适用于序列的元素不多的情况;

算法衍生:在直接插入排序中元素的移动频率和比较次数很高,有人提出用2 路插入排序算法改进;第一种:折半插入排序,利用了折半搜索算法的的思想,每次和已排好序列的中间元素比较大小这样来减少比较的次数;

希尔排序

算法描述:

       基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个增量的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

       增量序列一般都是经验数组,没有很证明最优的选法:{……9 5 3 2 1} { 40 13 4 1}

伪代码:

  1. Void  ShellSort ( int A[], int dlta[])  // A 待排序列  dlta 增量序列
  2. {
  3.     for int i = 0; i<dlat.size; i++
  4.        ShellInsert ( int A[], int i);  // i 为增量
  5. }
  6. Void ShellInsert ( int A[],int n)
  7. {
  8.     for  int i = 0 to i-1
  9.     {
  10.         for  int j = i+n; j<A.size ; j+n;
  11.         {
  12.             int tmp = A[j];
  13.             If  tmp< A[i]
  14.             {
  15.                    for ( int  k = i; tmp<A[i]; k = k-n )
  16.                        A [k+n] = A[k];
  17.             }
  18.             A[k+n] = tmp;
  19.          }
  20.     }
  21. }

效率分析:

       Shell排序的执行时间依赖于增量序列。
   
 好的增量序列的共同特征:
   最后一个增量必须为1(否则不能保证算法的正确性)
   应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
   
 有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.251.6n1.25之间。

时间复杂度:O( n3/2  )

       希尔排序由于直接插入排序的原因:

n        当文件基本有序时直接插入排序算法需要的比较次数和移动次数较少;

n        n较小时,n n2的差别较小,即最好时间复杂度和最坏时间复杂度的差别不大;

n        当增量大时,分组数目多,但组内元素少,这样直接插入排序速度较快,当增量小时,由于数组已经基本有序,此时直接插入排序算法也有很好的效率;

空间复杂度:

只需要一个零时变量空间;

稳定性:不稳定

冒泡排序

算法描述:

基本思想:在待排序列的无序区内,如果选择最大的一个元素将其“浮出水面”,对待排序列进行n-1 次冒泡,则序列完成排序过程;这种算法是进行相邻元素进行比较,然后将较大的元素往上挪动;

伪代码:

  1. Void bubbleSort(  int A[])
  2.     for  int i=0; i<A.size-1 ; i++         //n-1趟 i<A.size-1
  3.     {
  4.          for int j = 1; j<A.size-i ; j++
  5.          if A[j] < A[j-1]
  6.          {
  7.              int tmp = A[j];  
  8.              A[j]= A[j-1]; 
  9.              A[j-1] = tmp; 
  10.           }
  11.      }
  12. }

效率分析:

时间复杂度:n次循环,第i次循环进行n-i次比较,比较次数不能减少,最少移动0次,当序列已经有序的时候,最多移动(3+6+9+……+3n)次;所以时间复杂都最好最坏 平均复杂度都是一样的为On^2

空间复杂度:只需要一个零时变量空间;O1

稳定性:稳定

算法衍生:

       如果对冒泡算法进行改进,可以设定一个标记为,标记这一趟排序是否进行了交换,若没有交换则整个算法结束;这种情况下,最好只要进行一次遍历,(已经有序)也就是On)的时间复杂度,空间复杂度为0;若待排序列为逆序,则达到最坏时间复杂度和空间复杂度。如上面分析;

快速排序(讨论重点)

算法描述:

基本思想:每一趟排序将待排记录分为两部分,其中一部分的关键字小于另一部分的所有关键字,然后分别对两部分进行排序;

伪代码:

  1. Void QuickSort ( int A[], int low, int high )
  2. {
  3.     If  (low < high)
  4.     {
  5.       pivotLoc = partition(A,low,high);
  6.       QuickSort(A,low,pivotLoc-1);
  7.       Quicksort(A,pivot+1,high);
  8.     }  
  9. }
  10. Int partition(int A[], int low, int high)
  11. {
  12.     Pivotkey = A[low];
  13.     While(low<high)
  14.     {
  15.        while(low<high and A[high] > =pivotkey ) { high--; }
  16.        A[low] = A[high];
  17.        While(  low<high  and A[low] <= pivotkey ) { low++ }
  18.        A[high] = A[low]
  19.     }
  20.     A[low] = pivotkey;
  21.     Return low;
  22. }

效率分析:

快速排序的中心思想是分治法和递归

快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。

时间复杂度:最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。
   
 因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:
              Cmax = n(n-1)/2=O(n
2)
   
 如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。此时退化为冒泡排序;

在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:
        0(nlgn)

空间复杂度:快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)

稳定性:不稳定

算法衍生:

算法改进:

A.       改进中枢元素的选取方法:

a)        平均法:采用low high 和中间元素的平均值作为中枢值

b)       中间元素法:采用low high,和中间元素之中 元素值处于中间的值为中枢值

c)       随即法:随机选取序列中的一个元素值作为中枢值;

B.       与其他排序算法想结合:由于在待排序列元素数目较少的情况下,直接插入排序有其很好的优势,所以可以将直接插入算法与快速排序想结合,即当待排序列数目小于特定值时就采用直接插入算法。当然也可以考虑其他简单算法结合;

比较树:为什么O(nlgn) 是比较排序算法的最好时间复杂度)

这个我描述不清楚,这个大家参考 算法导论2nd 165 

我想说的时通过比较树模可以证明任意一个比较排序算法在最坏情况下,都需要做O(nlgn)次比较,也就是说这个比较排序算法的最好时间复杂度的下届;

       尝试论述比较树模型:对于任意可比较序列,这个序列不同的排序可能性总共是n!,而比较树就是一个包含了所有可能的树模型!(树形状就不画了!)对于任何一种排序的可能性,都有一条路径表达这种关系!

       形状考虑一颗高度为h,具有m个可达叶节点的决策树,它对应于对n个元素所做的比较排序。因为n个输入元素共有n!种排列,每一一种都作为一个叶子节点出现在树中,故有n<= L 。又由于一颗高为h的二叉树中,叶子的数目不多于2h ,则有

 n<= L<= 2h

取对数,得到 h>=lg(n!) = O(nlgn)  #

快速排序的思想应用:线性时间内选择第k大的元素

伪代码:

  1. Int RandomSelect (A,p,r,i )
  2. {  if p== r
  3.       Return;
  4.    q = RandomPartition(A,p,r);
  5.    k = q-p+1;
  6.    if k==i
  7.           then return A[i];
  8.    else i<k
  9.           return RandomSelect(A,p,q-1,i);
  10.    else
  11.           return RandomSelect( A,q+1,r,i-k);
  12. }

简单注释:和快速排序一样,RandomPartition(A,p,r)是对待排序列进行划分,返回值为中枢元素的位置(q),中枢位置前的元素都小于中枢值,中枢位置后的元素都大于中枢值,换句话说,q位置的元素就是第q大的元素。这样如果k恰好等于q,则返回,如果q<k;则说明第k大的元素在q元素后面的第k-q个位置上,如果q>k,则在第k大元素在前面的第k个位置。

现在需要证明的是这个过程是线性时间的:证明见算法导论p111

简单选择排序

算法描述:

每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。

伪代码:

Void SelectSort( int A[] )

{ for int i= 0 to A.size

      j = SelectMin(A,i+1);  // 选择i后面最小的元素

       A[i] A[j] 交换   

}

效率分析:

时间复杂度:无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需做n-i次比较,因此,总的比较次数也是O(n2)

       待排序列如果是已经排好序的,则移动次数为0.,如果是逆序的,则每次都需要交换,也就是移动次数为3n-1

空间复杂度:如果需要交换则为O1),否则为0

稳定性:不稳定

结构复杂性和使用范围:直接选择排序的用处不大,他的用处主要是用于引出树形排序和堆排序

堆排序

算法描述:n个关键字序列KlK2Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质)
   
 (1) ki≤K2iki≤K2i+1 (2)Ki≥K2iki≥K2i+1(1≤i≤n/2)
   
 若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。

       堆一般用数组实现;

 

基本思想: 先将初始文件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]调整为堆。
    ……
直到无序区只有一个元素为止。

其中最为重要的两个步骤为 建堆 buildHeap调整堆 heapify

下面分别介绍上面2个步骤:

建堆: 要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。
  显然只有一个结点的树是堆,而在完全二叉树中,所有序号i>n/2 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为floor(n/2) floor(n/2)-11的结点作为根的子树都调整为堆即可。

调整堆:对于每一个非叶子节点,如果节点元素值大于子节点则退出,如果该节点值小于其子节点的值,则将其值与子节点中值最大的元素交换,然后考虑该子结点是否满足堆的定义,如果不满足,则调整以该子结点为起点的堆;

伪代码:

Void HeapSort(Heap H)       // 这里堆元素的下标从1开始

{     for( i = H.length/2;i>0;i--)

       { HeapAdjust(H,i,H.lenght)   }  // 建大顶堆的过程

       For( i= H.length,i>1;i--)

       {     H[1] ßà H[i] }  //与堆顶元素交换

       HeapAdjust(H,1,i-1);

}

Void HeapAdjust(Heap H, int i, int j)

{     if( H[i]>=H[i].left and H[i] >= H[i].right )

       {     return      }

        H[max] = selectFindMaxChild(H[i]); // 找到元素值最大的子节点,max 为节点位置

       H[i]ß à H[max]  // 交换

       VoidHeapAdjust( H, max, j);

}

       Ps:在堆调整过程中一直没有用到intj,这个值的意思是在找最大子结点的时候必须在H[1j]之中寻找子结点,而不能超过这个边界

效率分析:

时间复杂度:

       对于n个节点的堆,堆的高度为lgn,堆排序就是每次从堆顶取一个元素,然后进行堆的一次调整,调整的复杂度就是堆的高度lgn,由于堆每一个元素都有一次调整的过程,所以总的时间复杂度为Onlgn

空间复杂度:堆排序是就地排序,辅助空间为O(1)

稳定性:不稳定

结构复杂性和使用范围:堆排序对于记录数较少的文件排序并不值得提倡,但对于n较大的文件还是很有效的。

归并排序

算法描述:

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

伪代码:

  1. Void MergeSort( int a[],int low, int mld, int high)
  2. {     for ( i =low, j = mld+1,k = i; i<mld and j<high ; k++)
  3.       If ( a[i]<a[j])
  4.       {
  5.           ta[k] = a[i++]
  6.       }
  7.       Else
  8.       { 
  9.           ta[k] = a[j++]
  10.       }
  11.       If i<low
  12.           ta[k…n] = a[i…m];
  13.       if j< n
  14.          ta[k…n] = a[j…n];
  15. }

效率分析:

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

空间复杂度:需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n)

稳定性:稳定

结构复杂性和使用范围:

此算法有两种设计方法:

l        自底向上:第一次排序时,将待排文件两两结合,这样第一次之后就形成n/2个子序列,然后在对产生的子序列再两两结合;

l        自底向上:设长度为n的待排序列,先将序列划分为两个n/2长度的子序列,然后递归的进行归并排序。如果序列长度小于等于2,则停止划分,(其中序列长度为2时将小元素放在前,如果长度为1时,序列不变)

桶排序

算法描述:

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

伪代码:

  1. Void binSort( int a[] )
  2. {   
  3.     for ( i = 0;i<a.size;i++)
  4.     {  
  5.         将a[i] 插入到个个桶之中 }
  6.         for ( i = 0, i< bin.size ,i++)
  7.         {
  8.           对每一个桶内排序
  9.         }
  10.         for ( i = 0, i< bin.size ,i++)
  11.         {
  12.             按桶次序收集桶
  13.         }
  14.     }
  15. }

效率分析:

时间复杂度:分配过程的时间是O(n);收集过程的时间为O(m) (采用链表来存储输入的待排序记录)或O(m+n)。因此,箱排序的时间为O(m+n)。若箱子个数m的数量级为O(n),则箱排序的时间是线性的,即O(n)

空间复杂度:需要m个箱子,箱子的总容量是n,用来暂存待排序列的元素,所以空间复杂度为O(n)

稳定性:为保证排序是稳定的,分配过程中装箱及收集过程中的连接必须按先进先出原则进行。

结构复杂性和使用范围:箱排序只适用于关键字取值范围较小的情况,否则所需箱子的数目m太多导致浪费存储空间和计算时间。箱排序实用价值不大,仅适用于作为基数排序的一个中间步骤。

基数排序

算法描述 基数排序的基本思想是:从低位到高位依次对Kj(j=d-1d-20)进行箱排序。在d趟箱排序中,所需的箱子数就是基数rd,这就是"基数排序"名称的由来。

设单关键字的每个分量的取值范围均是:
      C0≤kj≤Crd-1(0≤j<d)
可能的取值个数rd称为基数。
   
 基数的选择和关键字的分解因关键宇的类型而异:
(1)
若关键字是十进制整数,则按个、十等位进行分解,基数rd=10C0=0C9=9d为最长整数的位数;
(2)
若关键字是小写的英文字符串,则rd=26Co='a'C25='z'd为字符串的最大长度。

要保证基数排序是正确的,就必须保证除第一趟外各趟箱排序是稳定的。否则可能打乱已经排好的顺序;

效率分析:

时间复杂度:基数排序的时间是线性的(O(n))

空间复杂度:基数排序所需的辅助存储空间为O(n+rd)

稳定性 : 稳定

计数排序

这个算法我不知道怎么描述,具体算法见算法导论p99,如果哪天我觉得可以描述清楚了我再加上来!不好意思

各种内部排序方法比较

按平均时间将排序分为四类:
1)平方阶(O(n2))排序
   
 一般称为简单排序,例如直接插入、直接选择和冒泡排序;
2)线性对数阶(O(nlgn))排序
   
 如快速、堆和归并排序;

3O(n1+)阶排序

     £是介于01之间的常数,即0<<1,如希尔排序;
4)线性阶(O(n))排序
   
 如桶和基数排序

不同条件下,排序方法的选择

1.        n较小(n≤50),可采用直接插入或直接选择排序。
   
 当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。

2.        若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;

3.        n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
   
 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
   
 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
   
 若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的  排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。

 



全文由HZP完成

 

原创粉丝点击