用“歸併”改進“快速排序” (轉)

worldblog發表於2007-08-17
用“歸併”改進“快速排序” (轉)[@more@]

  排序和搜尋是我們時最常用到的兩種演算法了,C++員們幸運一些,因為在C++標準庫中就有各種通用的排序;而程式設計師就只有TList.Sort和TStringList.Sort可用了,所以Delphi程式設計師通常都把排序函式加入到自己的常用函式單元中。

  作為“通用排序函式”,自然要在“基於比較”的排序演算法中選擇,而其中的“排序”以其優勢常使它成為程式設計師的首選。但快速排序也有一個常常另人擔心的問題……它在最差情況下的時間複雜度是O(N2)。因此,悲觀的程式設計師們通常會改用“堆排序”(在《C++標準程式庫》一書中作者就推薦使用partial_sort,因為sort可能是用“快速排序”實現的而partial_sort則通常是用“堆排序實現的”)。

  為了能“既吃到魚又不丟了熊掌”,很多程式設計師都在想辦法改良“快速排序”,在不影響它的優秀的平均的前提下使它在最差情況下也可以獲得可以忍受的效能,其中最成功的應該算是“Intro Sort”,而我在這裡談的是另一種方法。

  我們知道,“快速排序”的主要思想就是遞迴地對序列進行劃分,用一個樞值把序列分成“大序列”和“小序列”。如果這個劃分每次都是平均地“一分為二”,這時它就獲得了最佳的效能;而如果每次都是“畸形劃分”——把序列分成長度為1和N-1的兩部分,它就退化成了遞迴實現的“氣泡排序”,效能可想而知。要想對它進行改良,就要針對這種情況進行處理。一種思路是對劃分進行改良,讓它不產生“畸形劃分”;另一種思路是對劃分結果進行檢查,如果是畸形劃分則進行特殊處理。我這裡的方法是第二種。

  基本思路就是,對劃分結果產生的兩個子序列的長度進行檢查,如果其中一個與另一個的長度比超過某一界限,則認為這是一個“畸形劃分”,對較短的子序列繼續使用“快速排序”,而把較長的子序列平分為兩個子序列分別排序,然後再進行一次合併。兩個有序序列的合併是可以實現為線性的時間複雜度的,因此可以在每次都是畸形劃分時仍然獲得O(N*LogN)的時間複雜度。

  基本演算法描述如下:

procedure Sort(Data, Size);

begin

  M := Partition(Data, Size);

  MoreData := @Data[M + 1];

  MoreSize := Size - M - 1;

  Size := M;

  if Size > M then

  begin

  S(MoreData, Data);

  Swap(MoreSize, Size);

  end;

  Sort(Data, Size);

  if MoreSize div MAX_RATIO > Size then

  begin

  Sort(MoreData, MoreSize div 2);

  Sort(@MoreData[MoreSize div 2], MoreSize - MoreSize div 2);

  Merge(MoreData, MoreSize div 2, MoreSize);

  end

  else

  Sort(MoreData, MoreSize);

end;

 

  其中Partition就是眾所周知的用於“快速排序”的劃分子程式,Merge(Data, First,Size)把Data中[0,First)和[First, Size)兩個有序列合併為一個有序序列並存放在Data中。上面的演算法認為Partition劃分的位置M處的值就是劃分的樞值,也就是說序列可以分成[0,M-1]、[M,M]和[M+1,Size-1]三部分。如果Partition的實現不能保證這一點,則MoreData應為Data[M],而MoreSize也應為Size - M。

  現在簡單分析一下這個排序,最好情況是在Partition每次劃分都是平均劃分時,這時合併不會發生,它等同於“快速排序”。而在最差情況下,每次Partition都會把序列分成長度為1和N-1的兩部分,這時它就變成了遞迴實現的“二路歸併排序”,但在每次合併前都進行了約N次比較,作用只是分出一個元素不參與合併。因此這個排序的時間複雜度仍然是O(N*LogN),比較次數大約是歸併排序的二倍。

  關於MAX_RATIO的選擇,根據我的,應該是選擇4-16之間的數字比較好,我選擇的是8。

  我實際上實現時比上面說的要複雜一些,一是在序列長度不大於16時改用插入排序,二是去掉了一個遞迴。然後作了一些效率上的測試,資料是500,000個長度為10的字串,大致如下:

 

隨機分佈,500000個元素:
Sort:時間:3.31;比較次數:10843175。
QuickSort:時間:3.28;比較次數:10936357。
IntroSort:時間:3.35;比較次數:10958355。
MergeSort:時間:4.20;比較次數:13502620。

正序分佈,500000個元素:
Sort:時間:1.71;比較次數:8401712。
QuickSort:時間:1.91;比較次數:9262161。
IntroSort:時間:1.80;比較次數:8401712。
MergeSort:時間:1.72;比較次數:4766525。

逆序分佈,500000個元素:
Sort:時間:2.38;比較次數:11737937。
QuickSort:時間:2.54;比較次數:12619014。
IntroSort:時間:2.38;比較次數:11293745。
MergeSort:時間:1.69;比較次數:4192495。

相同值,500000個元素:
Sort:時間:1.41;比較次數:8401712。
QuickSort:時間:1.47;比較次數:9262161。
IntroSort:時間:1.40;比較次數:8401712。
MergeSort:時間:1.43;比較次數:4766525。

波形分佈(波長1000),500000個元素:
Sort:時間:2.52;比較次數:10658948。
QuickSort:時間:2.97;比較次數:12971845。
IntroSort:時間:3.02;比較次數:12672744。
MergeSort:時間:2.71;比較次數:7978745。

峰形分佈(前半段為正序,後半段為前半段的逆轉),500000個元素:
Sort:時間:2.42;比較次數:10401407。
IntroSort:時間:5.13;比較次數:19211813。
MergeSort:時間:1.88;比較次數:5176855。

谷形分佈(峰形分佈的逆轉),500000個元素:
Sort:時間:2.29;比較次數:10944792。
IntroSort:時間:5.29;比較次數:17898801。
MergeSort:時間:1.90;比較次數:5282136。

  由於這個排序的最差分佈不是很好尋找,我修改了一下Partition函式,使正序分成成為它的不利分佈,然後在同一臺機器上測了一下:


正序分佈,500000個元素:
Sort:時間:2.77;比較次數:12011738。
IntroSort:時間:4.31;比較次數:19212426。
MergeSort:時間:1.73;比較次數:4766525。

  據我分析,這時排序可以分成QuickSort和MergeSort兩部分,兩部分花的時間相互獨立,所以我把這個時間與MergeSort的時間相減,然後加上隨機分佈情況的MergeSort的時間(取4.20),結果為5.24,應該是差不多的。
從這個結果來看,和IntroSort的最差情況基本上是差不多的。

  在這裡感謝CSDN的LeeMaRS和ZhanYv,他們在論壇上和我對排序的改進進行了很多討論,並給了我很大幫助。LeeMaRS為我提供了一個優秀的Partition函式。

下面給出實現的完整程式碼:

type
  TPointerList = array[0..32767] of Pointer;
  PPointerList = ^TPointerList;
  PPointer = ^ Pointer;
  TLessThenProc = function(Left, Right: Pointer): Boolean;

 

const

  SORT_MAX = 16;

  MAX_RATIO = 8;

 

{**************************************************************************
函式:Partition
功能:將一個序列劃分成兩個子序列,後一子序列所有值都不大於前一子序列任意值。
   返回子序列分割處。
引數:
  Data: PPointerList,源序列。
  Size: Integer, 序列長度。
  LessThen: TLessThenProc,用於定義順序的比較函式
說明:
  用於“快速排序”
  樞值策略:選擇0、0.5、1三處的值的中間值
  返回值保證:
  A < Result 則必然 not LessThen(Data[Result], Data[A]);
  A > Result 則必然 not LessThen(Data[A], Data[Result]);
**************************************************************************}
function Partition(Data: PPointerList; Size: Integer;LessThen: TLessThenProc): Integer;
var
  M: Integer;
  Value: Pointer;
begin
  M := Size div 2;
  Dec(Size);
  if LessThen(Data[0], Data[M]) then
  Swap(Data[M], Data[0]);
  if LessThen(Data[Size], Data[0]) then
  Swap(Data[Size], Data[0]);
  if LessThen(Data[0], Data[M]) then
  Swap(Data[M], Data[0]);
  Value := Data[0];
  Result := 0;
  while Result < Size do
  begin
  while (Result < Size) and LessThen(Value, Data[Size]) do
  Dec(Size);
  If Result < Size then
  begin
  Data[Result] := Data[Size];
  Inc(Result);
  end;
  while (Result < Size) and LessThen(Data[Result], Value) do
  Inc(Result);
  If Result < Size then
  begin
  Data[Size] := Data[Result];
  Dec(Size);
  end;
  end;
  Data[Result] := Value;
end;

{**************************************************************************
函式:Merge
功能:將兩個有序序列合併為一個有序列序。
引數:
  SrcFirst, SrcSecond: PPointerList,兩個源序列。合併時如果有相同的值,
  SrcSecond的值將排在SrcFirst的值的後面。
  Dest:PPointerList,存放合併結果的序列,必須有足夠的空間。
  SizeFirst, SizeSecond: Integer, 兩個源序列長度
  LessThen: TLessThenProc,用於定義順序的比較函式
**************************************************************************}

procedure Merge(SrcFirst, SrcSecond, Dest: PPointerList;
SizeFirst, SizeSecond: Integer; LessThen: TLessThenProc);
var
  I: Integer;
  Iirst: Boolean;
begin
  IsFirst := True;
  if (SizeFirst = 0) or (LessThen(SrcSecond[0], SrcFirst[0])) then
  begin
  Swap(Pointer(SrcFirst), Pointer(SrcSecond));
  Swap(SizeFirst, SizeSecond);
  IsFirst := not IsFirst;
  end;
  while SizeFirst > 0 do
  begin
  if SizeSecond = 0 then
  I := SizeFirst
  else
  begin
  I := 0;
  while (I < SizeFirst) and
  ((IsFirst and not LessThen(SrcSecond[0], SrcFirst[I]))
  or (not IsFirst and LessThen(SrcFirst[I], SrcSecond[0]))) do
  Inc(I);
  end;
  Move(SrcFirst^, Dest^, Sizeof(Pointer) * I);
  Dec(SizeFirst, I);
  SrcFirst := @SrcFirst[I];
  Dest := @Dest[I];
  Swap(Pointer(SrcFirst), Pointer(SrcSecond));
  Swap(SizeFirst, SizeSecond);
  IsFirst := not IsFirst;
  end;
end;

{**************************************************************************
函式:SortInsert
功能:向有序序列中插入一個值,保證插入後仍然有序。
引數:
  Data: PPointerList,有序序列,必須可容納Size + 1個元素
  Size: Integer, 原序列長度
  Value: 新插入的值
  LessThen: TLessThenProc,用於定義順序的比較函式
**************************************************************************}
procedure SortInsert(Data: PPointerList; Size: Integer; Value: Pointer; LessThen: TLessThenProc);
var
  J: Integer;
begin
  if LessThen(Value, Data[0]) then
  J := 0
  else
  begin
  J := Size;
  while (J > 0) and LessThen(Value, Data[J - 1]) do
  Dec(J);
  end;
  Move(Data[J], Data[J +1], Sizeof(Pointer) * (Size - J));
  Data[J] := Value;
end;

{**************************************************************************
函式:MergePart
功能:將序列中的兩個鄰近有序子序列合併為一個有序子序列並存放在原處。
引數:
  Data: PPointerList,源序列。
  PartSize: Integer, 第一個有序子序列長度。
  Size: Integer, 序列總長度。
  LessThen: TLessThenProc,用於定義順序的比較函式
說明:
  如果自由空間足夠,Merge實現,否則呼叫SortInsert。
**************************************************************************}

procedure MergePart(Data: PPointerList; First: Integer; Size: Integer;LessThen: TLessThenProc);
var
  Buffer: PPointerList;
  I: Integer;
begin
  Buffer := AllocMem(Size * Sizeof(Pointer));
  if Buffer <> nil then
  begin
  Move(Data^, Buffer^, Size * Sizeof(Pointer));
  Merge(@Buffer[0], @Buffer[First], Data, First, Size-First, LessThen);
  FreeMem(Buffer);
  end
  else
  begin
  Dec(Size);
  for I := PartSize to Size do
  SortInsert(Data, I, Data[I], LessThen);
  end;
end;

{**************************************************************************
函式:InsertionSort
功能:簡單插入排序
引數:
  Data: PPointerList,源序列。
  Size: Integer, 序列長度。
  LessThen: TLessThenProc,用於定義順序的比較函式
**************************************************************************}
procedure InsertionSort(Data: PPointerList; Size: Integer;
LessThen: TLessThenProc);
var
  I: Integer;
begin
  Dec(Size);
  for I := 1 to Size do
  SortInsert(Data, I, Data[I], LessThen);
end;

{**************************************************************************
函式:Sort
功能:排序
引數:
  Data: PPointerList,源序列。
  Size: Integer, 序列長度。
  LessThen: TLessThenProc,用於定義順序的比較函式
說明:
  使用快速排序。
  當子序列長度不大於SORT_MAX時使用插入排序。
  當子序列長度比大於MAX_RATIO時將長子序列一分為二分別排序,然後合併。
**************************************************************************}
procedure Sort(Data: PPointerList; Size: Integer; LessThen: TLessThenProc);
var
  M: Integer;
  OtherData: PPointerList;
  OtherSize: Integer;
begin
  Assert(Data <> nil);
  while Size > SORT_MAX do
  begin
  M := Partition(Data, Size, LessThen);
  if (M <= Size div 2) then
  begin
  OtherData := @Data[M + 1];
  OtherSize := Size - M - 1;
  Size := M;
  end
  else
  begin
  OtherData := Data;
  OtherSize := M;
  Data := @OtherData[M + 1];
  Size := Size - M - 1;
  end;

  if (OtherSize div MAX_RATIO > Size) then
  begin
  M := OtherSize div 2;
  Sort(OtherData, M, LessThen, MaxRatio);
  Sort(@OtherData[M], OtherSize - M, LessThen, MaxRatio);
  MergePart(OtherData, M, OtherSize, LessThen);
  end
  else
  Sort(OtherData, OtherSize, LessThen, MaxRatio);
  end;
  InsertionSort(Data, Size, LessThen);
end;


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-963449/,如需轉載,請註明出處,否則將追究法律責任。

相關文章