切换到宽版
  • 7806阅读
  • 6回复

几种排序法 [复制链接]

上一主题 下一主题
离线181818181818
 
只看楼主 倒序阅读 0 发表于: 2007-07-02
1、冒泡排序 Bubble Sort
最简单的排序方法是冒泡排序方法。这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。在冒泡排序算法中我们要对这个“气泡”序列处理若干遍。所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确。如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置。显然,处理一遍之后,“最轻”的元素就浮到了最高位置;处理二遍之后,“次轻”的元素就浮到了次高位置。在作第二遍处理时,由于最高位置上的元素已是“最轻”元素,所以不必检查。一般地,第i遍处理时,不必检查第i高位置以上的元素,因为经过前面i-1遍的处理,它们已正确地排好序。这个算法可实现如下。
procedure Bubble_Sort(var L:List);
var
i,j:position;
begin
1 for i:=First(L) to Last(L)-1 do
2 for j:=First(L) to Last(L)-i do
3   if L[j]>L[j+1] then
4     swap(L[j],L[j+1]);   //交换L[j]和L[j+1]
end;

[Copy to clipboard]

上述算法将较大的元素看作较重的气泡,每次最大的元素沉到表尾。其中First(L)和Last(L)分别表示线性表L的第一个元素和最后一个元素的位置,swap(x,y)交换变量x,y的值。上述算法简单地将线性表的位置当作整数用for循环来处理,但实际上线性表可能用链表实现;而且上述算法将线性表元素的值当作其键值进行处理。不过这些并不影响表达该算法的基本思想。今后如果不加说明,所有的算法都用这种简化方式表达。

容易看出该算法总共进行了n(n-1)/2次比较。如果swap过程消耗的时间不多的话,主要时间消耗在比较上,因而时间复杂性为O(n2)。但是如果元素类型是一个很大的纪录,则Swap过程要消耗大量的时间,因此有必要分析swap执行的次数。

显然算法Bubble_Sort在最坏情况下调用n(n-1)/2次Swap过程。我们假设输入序列的分布是等可能的。考虑互逆的两个输入序列L1=k1,k2,..,kn和L2=kn,kn-1,..,k1。我们知道,如果ki>kj,且ki在表中排在kj前面,则在冒泡法排序时必定要将kj换到ki前面,即kj向前浮的过程中一定要穿过一次ki,这个过程要调用一次Swap。对于任意的两个元素ki和kj,不妨设ki>kj,或者在L1中ki排在kj前面,或者L2在中ki排在kj前面,两者必居其一。因此对于任意的两个元素ki和kj,在对L1和L2排序时,总共需要将这两个元素对调一次。n个元素中任取两个元素有Cn2 种取法,因此对于两个互逆序列进行排序,总共要调用Cn2 =n(n-1)/2次Swap,平均每个序列要调用n(n-1)/4次Swap。那么算法Bubble_Sort调用Swap的平均次数为n(n-1)/4。

可以对冒泡算法作一些改进,如果算法第二行的某次内循环没有进行元素交换,则说明排序工作已经完成,可以退出外循环。可以用一个布尔变量来记录内循环是否进行了记录交换,如果没有则终止外循环。

冒泡法的另一个改进版本是双向扫描冒泡法(Bi-Directional Bubble Sort)。设被排序的表中各元素键值序列为:

483 67 888 50 255 406 134 592 657 745 683

对该序列进行3次扫描后会发现,第3此扫描中最后一次交换的一对纪录是L[4]和L[5]:

50 67 255 134 | 406 483 592 657 683 745 888

显然,第3次扫描(i=3)结束后L[5]以后的序列都已经排好序了,所以下一次扫描不必到达Last(L)-i=11-4=7,即第2行的for 循环j不必到达7,只要到达4-1=3就可以了。按照这种思路,可以来回地进行扫描,即先从头扫到尾,再从尾扫到头。这样就得到双向冒泡排序算法:

CODE:

procedure Bi-Directional_Bubble_Sort(var L:List);
var
low,up,t,i:position;
begin
1 low:=First(L);up:=Last(L);
2 while up>low do
begin
3   t:=low;
4   for i:=low to up-1 do
5   if L>L[i+1] then
    begin
6     swap(L,L[i+1]);
7     t:=i;
    end;
8   up:=t;
9   for i:=up downto low+1 do
10   if L< L[i-1] then
    begin
11     swap(L,L[i-1]);
12     t:=i;
    end;
13   low:=t;  
end;
end;

[Copy to clipboard]

算法利用两个变量low和up记录排序的区域L[low..up],用变量t 记录最近一次交换纪录的位置,4-7行从前向后扫描,9-12行从后向前扫描,每次扫描以后利用t所记录的最后一次交换记录的位置,并不断地缩小需要排序的区间,直到该区间只剩下一个元素。

直观上来看,双向冒泡法先让重的气泡沉到底下,然后让轻的气泡浮上来,然后再让较大气泡沉下去,让较轻气泡浮上来,依次反复,直到排序结束。

双向冒泡排序法的性能分析比较复杂,目前暂缺,那位朋友知道请告诉我。

冒泡排序法和双向冒泡排序法是原地置换排序法,也是稳定排序法,如果算法Bubble_Sort中第3行的比较条件L[j]>L[j+1]改为L[j]>= L[j+1],则不再是稳定排序法。

2、选择排序 Selection Sort
选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第i遍处理是将L[i..n]中最小者与L交换位置。这样,经过i遍处理之后,前i个记录的位置已经是正确的了。

选择排序算法可实现如下。

procedure Selection_Sort(var L:List);
var
i,j,s:position;
begin
1 for i:=First(L) to Last(L)-1 do
begin
2     s:=i;
3     for j:=i+1 to Last(L) do
4     if L[j]< L[s] then
5       s:=j;       //记录L[i..n]中最小元素的位置
6     swap(L,L[s]);   //交换L,L[s]
  end;
end;

[Copy to clipboard]

算法Selection_Sort中里面的一个for循环需要进行n-i次比较,所以整个算法需要 次比较。
显而易见,算法Selection_Sort中共调用了n-1次swap过程。选择排序法是一个原地置换排序法,也是稳定排序法。

3、插入排序 Insertion Sort
插入排序的基本思想是,经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L插入L[1..i-1]的适当位置,使得L[1..i]又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L和L[i-1],如果L[i-1]≤ L,则L[1..i]已排好序,第i遍处理就结束了;否则交换L与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。图1演示了对4个元素进行插入排序的过程,共需要(a),(b),(c)三次插入。
[Copy to clipboard]





CODE:
图1 对4个元素进行插入排序

在下面的插入排序算法中,为了写程序方便我们可以引入一个哨兵元素L[0],它小于L[1..n]中任一记录。所以,我们设元素的类型ElementType中有一个常量-∞,它比可能出现的任何记录都小。如果常量-∞不好事先确定,就必须在决定L是否向前移动之前检查当前位置是否为1,若当前位置已经为1时就应结束第i遍的处理。另一个办法是在第i遍处理开始时,就将L放入L[0]中,这样也可以保证在适当的时候结束第i遍处理。下面的算法中将对当前位置进行判断。

插入排序算法如下:

procedure Selection_Sort(var L:List);
var
i,j:position;
v:ElementType;
begin
1 for i:=First(L)+1 to Last(L) do
begin
2   v:=L;
3   j:=i;
4   while (j<>First(L))and(L[j-1]< v) do //循环找到插入点
  begin
5     L[j]:=L[j-1]; //移动元素
6     j:=j-1;
  end;
7   L[j]:=v;   //插入元素
end;
end;
下面考虑算法Insertion_Sort的复杂性。对于确定的i,内while循环的次数为O(i),所以整个循环体内执行了∑O(i)=O(∑i),其中i从2到n。即比较次数为O(n2)。如果输入序列是从大到小排列的,那么内while循环次数为i-1次,所以整个循环体执行了∑(i-1)=n(n-1)/2次。由此可知,最坏情况下,Insertion_Sort要比较Ω(n2)次。


如果元素类型是一个很大的纪录,则算法第5行要消耗大量的时间,因此有必要分析移动元素的次数。经过分析可知,平均情况下第5行要执行n(n-1)/4次,分析方法与冒泡排序的分析相同。

如果移动元素要消耗大量的时间,则可以用链表来实现线性表,这样Insertion_Sort可以改写如下(当然前一个算法同样也适用于链表,只不过没下面这个好,但是下面算法这个比较复杂):

注意:在下面的算法中链表L增加了一个哨兵单元,其中的元素为-∞,即线性表L的第一个元素是L^.next^

procedure Selection_Sort_II(var L:PList);
var
i,j,tmp:Position;
begin
1 if L^.next=nil then exit; //如果链表L为空则直接退出
2 i:=L^.next; //i指向L的第一个元素,注意,L有一个哨兵元素,因此L^.next^才是L的第一个元素
3 while i^.next<>nil do
begin
4   tmp:=i^.next; //tmp指向L的下一个位置
5   j:=L;
6   while (j<>i)and(tmp^.data>=j^.next^.data) do //从前向后找到tmp的位置,tmp应该插在j后面
7   j:=j^.next;
8   if j<>i then //j=i说明不需要改变tmp的位置
    begin            
9     i^.next:=tmp^.next; //将tmp从i后面摘除
10     tmp^.next:=j^.next; //在j后面插入tmp
11     j^.next:=tmp;
    end
12   else i:=i^.next; //否则i指向下一个元素
end;
end;
上述改进算法主要是利用链表删除和插入元素方便的特性,对于数组则不适用。

插入排序法是一个原地置换排序法,也是一个稳定排序法。插入法虽然在最坏情况下复杂性为θ(n2),但是对于小规模输入来说,插入排序法是一个快速的原地置换排序法。许多复杂的排序法,在规模较小的情况下,都使用插入排序法来进行排序,比如快速排序和桶排序。
4、快速排序 Quick Sort
我们已经知道,在决策树计算模型下,任何一个基于比较来确定两个元素相对位置的排序算法需要Ω(nlogn)计算时间。如果我们能设计一个需要O(n1ogn)时间的排序算法,则在渐近的意义上,这个排序算法就是最优的。许多排序算法都是追求这个目标。

下面介绍快速排序算法,它在平均情况下需要O(nlogn)时间。这个算法是由C.A.R.Hoare发明的。

算法的基本思想
快速排序的基本思想是基于分治策略的。对于输入的子序列L[p..r],如果规模足够小则直接进行排序,否则分三步处理:

分解(Divide):将输入的序列L[p..r]划分成两个非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大于L[q+1..r]中任一元素的值。
递归求解(Conquer):通过递归调用快速排序算法分别对L[p..q]和L[q+1..r]进行排序。
合并(Merge):由于对分解出的两个子序列的排序是就地进行的,所以在L[p..q]和L[q+1..r]都排好序后不需要执行任何计算L[p..r]就已排好序。
这个解决流程是符合分治法的基本步骤的。因此,快速排序法是分治法的经典应用实例之一。

算法的实现
算法Quick_Sort的实现:

注意:下面的记号L[p..r]代表线性表L从位置p到位置r的元素的集合,但是L并不一定要用数组来实现,可以是用任何一种实现方法(比如说链表),这里L[p..r]只是一种记号。

procedure Quick_Sort(p,r:position;var L:List);
const
e=12;
var
q:position;
begin
1 if r-p<=e then Insertion_Sort(L,p,r)//若L[p..r]足够小则直接对L[p..r]进行插入排序
else begin
2       q:=partition(p,r,L);//将L[p..r]分解为L[p..q]和L[q+1..r]两部分
3       Quick_Sort(p,q,L); //递归排序L[p..q]
4       Quick_Sort(q+1,r,L);//递归排序L[q+1..r]      
    end;
end;

对线性表L[1..n]进行排序,只要调用Quick_Sort(1,n,L)就可以了。算法首先判断L[p..r]是否足够小,若足够小则直接对L[p..r]进行排序,Sort可以是任何一种简单的排序法,一般用插入排序。这是因为,对于较小的表,快速排序中划分和递归的开销使得该算法的效率还不如其它的直接排序法好。至于规模多小才算足够小,并没有一定的标准,因为这跟生成的代码和执行代码的计算机有关,可以采取试验的方法确定这个规模阈值。经验表明,在大多数计算机上,取这个阈值为12较好,也就是说,当r-p<=e=12即L[p..r]的规模不大于12时,直接采用插入排序法对L[p..r]进行排序(参见 Sorting and Searching Algorithms: A Cookbook)。当然,比较方便的方法是取该阈值为1,当待排序的表只有一个元素时,根本不用排序(其实还剩两个元素时就已经在Partition函数中排好序了),只要把第1行的if语句该为if p=r then exit else ...。这就是通常教科书上看到的快速排序的形式。

注意:算法Quick_Sort中变量q的值一定不能等于r,否则该过程会无限递归下去,永远不能结束。因此下文中在partition函数里加了限制条件,避免q=r情况的出现。

算法Quick_Sort中调用了一个函数partition,该函数主要实现以下两个功能:

在L[p..r]中选择一个支点元素pivot;
对L[p..r]中的元素进行整理,使得L[p..q]分为两部分L[p..q]和L[q+1..r],并且L[p..q]中的每一个元素的值不大于pivot,L[q+1..r]中的每一个元素的值不小于pivot,但是L[p..q]和L[q+1..r]中的元素并不要求排好序。
快速排序法改进性能的关键就在于上述的第二个功能,因为该功能并不要求L[p..q]和L[q+1..r]中的元素排好序。

函数partition可以实现如下。以下的实现方法是原地置换的,当然也有不是原地置换的方法,实现起来较为简单,这里就不介绍了。

function partition(p,r:position;var L:List):position;
var
pivot:ElementType;
i,j:position;
begin
1 pivot:=Select_Pivot(p,r,L); //在L[p..r]中选择一个支点元素pivot
2 i:=p-1;
3 j:=r+1;
4 while true do
begin
5   repeat j:=j-1 until L[j]<=pivot; //移动左指针,注意这里不能用while循环
6   repeat i:=i+1 until L>=pivot; //移动右指针,注意这里不能用while循环
7   if i< j then swap(L,L[j]) //交换L和L[j]
8       else if j<>r then return j   //返回j的值作为分割点
9             else return j-1;   //返回j前一个位置作为分割点
end;
end;

该算法的实现很精巧。其中,有一些细节需要注意。例如,算法中的位置i和j不会超出A[p..r]的位置界,并且该算法的循环不会出现死循环,如果将两个repeat语句换为while则要注意当L=L[j]=pivot且i<j时i和j的值都不再变化,会出现死循环。

另外,最后一个if..then..语句很重要,因为如果pivot取的不好,使得Partition结束时j正好等于r,则如前所述,算法Quick_Sort会无限递归下去;因此必须判断j是否等于r,若j=r则返回j的前驱。

以上算法的一个执行实例如图1所示,其中pivot=L[p]=5:
离线181818181818
只看该作者 1 发表于: 2007-07-02
好!
离线447389831
只看该作者 2 发表于: 2007-08-11
好好好~~~~~~
离线sm-star
只看该作者 3 发表于: 2007-08-19
我只喜欢快排。。。
离线clwxzh57
只看该作者 4 发表于: 2007-08-20
这还不错.
离线liuichou
只看该作者 5 发表于: 2007-08-20
随机快排够了
离线kai^f^p^kai
只看该作者 6 发表于: 2007-11-13
八错八错
快速回复
限100 字节
 
上一个 下一个