春节期间拜读了MoreWindows整理的白话经典算法之七大排序,受益匪浅。但正所谓纸上得来终觉浅,这两天一一重新写了一遍,将一些心得体会记录如下:
一、关于排序算法的稳定性:一个稳定的排序算法,就是相同值的左右位置经过排序之后不会更换。一个很好的例子是:一个学生的信息包含学号,年龄,按照年龄来进行由小到大的排序,最终学号的顺序也还是从小到大的。七种算法里面,冒泡,插入,归并排序是稳定的,而希尔、快速、选择、堆排序是不稳定的。
二、几种常用排序算法小结,完全的自我理解:
1.冒泡排序:
一个基于左右比较的排序算法,发现跟预定的规则不一致就交换,然后紧接着进行左右比较,然后一直进行下去,每一趟可以确定一个最大数,或者最小数,如果说进行一次从小到大的比较,而最大的元素恰好是第一个,那么第一趟的操作看起来就是第一个元素不断的向上滚动,类似于冒泡了。
从实现上看,外层循环最大为元素的个数,内存循环就是重复的左右比较。但是无论是外层循环,还是内层比较循环都是优化的空间:先说内层循环,最后发生交换的位置的后面都是有序的,所以下次左右比较的最后位置就是这里,如果一次比较过程都没有发生交换,那么说明目前的列表已经是有序的了,外层的循环也就可以终止了。
由于基于左右比较的,值相同的两个元素的左右位置自然不会发生交换,也就是稳定的了,基于冒泡排序的其他算法自然也是稳定的,比较插入排序。
两层循环的变量之前没有关联,虽然冒泡排序的时间复杂度为0(n^2),但是这种算法的思路,代码实现都很简单,特别是对于原始算法的优化的思考比较有意思,知道了如何实现还不够,只有对实现进行了理解才能优化,优化之后本身也会加深
基于pascal的代码:
procedure TAlgorithmTest.BubbleOrder(const ASourceData: TIntegerDynArray;
const IsAsc: Boolean);
var
LastFlag, CurrentFlag: Integer;
I: Integer;
begin
LastFlag := High(ASourceData);
while LastFlag > 0 do
begin
CurrentFlag := LastFlag;
LastFlag := 0;
for I := 1 to CurrentFlag do
begin
if NeedSwap(ASourceData[I - 1], ASourceData[I], IsAsc) then
begin
SwapData(ASourceData, I - 1, I);
LastFlag := I - 1;
end;
end;
end;
end;
2 插入排序
算法的基本思路:如果一个列表是有序的,每插入一个元素进行排序,那么子列表始终是有序的,当子列表的元素跟原始列表一样那就能保证整个列表已经处于有序的状态了。那么,这样一来就有了:外层循环从第二个元素开始(单个元素是有序的),自循环就是将后面的元素插入到当前的子列表中,子列表就在原始列表的前端,所以需要反向遍历查找位置,插入新的数值。所以,两层循环的变量是有关联关系的。如何维护子列表有三种思路:
2.1 对于新元素,先反向遍历之前的元素,找到合适的位置,然后直接将该元素移动那个位置
procedure TAlgorithmTest.InsertOrigin(const ASourceData: TIntegerDynArray;
const IsAsc: Boolean);
var
I, J, K: Integer;
TempVal: Integer;
begin
for I := Succ(Low(ASourceData)) to High(ASourceData) do
begin
J := I - 1;
while (j >= 0) do
begin
if IsAsc then
begin
if ASourceData[j] < ASourceData[I] then
Break;
end
else
begin
if ASourceData[j] > ASourceData[I] then
Break;
end;
Dec(J)
end;
if j <> I - 1 then
begin
TempVal := ASourceData[I];
k := I - 1;
while(k > j) do
begin
ASourceData[k + 1] := ASourceData[k];
Dec(k);
end;
ASourceData[k + 1] := TempVal;
end;
end;
end;
2.2 对于新元素,直接反向遍历子列表,然后找到合适的位置,将数值插入进去,相对于2.1,就是将查找位置和移动元素的工作结合起来了
procedure TAlgorithmTest.InsertOptimize(
const ASourceData: TIntegerDynArray; const IsAsc: Boolean);
var
I, J, k: Integer;
TempValue: Integer;
begin
for I := Succ(Low(ASourceData)) to High(ASourceData) do
begin
if IsAsc then
begin
if (ASourceData[I] < ASourceData[I - 1]) then
begin
TempValue := ASourceData[I];
K := I - 1;
while(k >= 0) and (ASourceData[k] > TempValue) do
begin
ASourceData[K + 1] := ASourceData[k];
Dec(k);
end;
ASourceData[K + 1] := TempValue;
end;
end
else
begin
if (ASourceData[I] > ASourceData[I - 1]) then
begin
TempValue := ASourceData[I];
k := I - 1;
while(K >= 0) and (ASourceData[k] < TempValue) do
begin
ASourceData[K + 1] := TempValue;
Dec(k);
end;
ASourceData[K + 1] := TempValue;
end;
end;
end;
end;
2.3 当前的元素尝试在子列表中冒泡一趟
procedure TAlgorithmTest.InsertOrder(const ASourceData: TIntegerDynArray;
const IsAsc: Boolean);
var
I, J: Integer;
begin
for I := Succ(Low(ASourceData)) to High(ASourceData) do
begin
J := I - 1;
while(j >= 0) and NeedSwap(ASourceData[J + 1], ASourceData[J], IsAsc) do
begin
SwapData(ASourceData, J + 1, J);
Dec(J);
end;
end;
end;
插入排序的代码实现其实是”反向冒泡“,所以,插入排序是稳定的。
插入算法思路很有意思,具体的3种实现方式各有特点,同时也是希尔排序的基础。
3 希尔排序
希尔排序是基于不断分组排序的一种算法,分组中的排序实现是使用插入排序实现。由于需要分组,所以,这种排序算法本身就是不稳定的。
希尔排序也有三种实现方法:
3.1 严格的按照定义:不断的分组,组间进行插入排序
procedure TAlgorithmTest.ShellOrder(const ASourceData: TIntegerDynArray;
const AIsAsc: Boolean);
var
DataCount: Integer;
Gap: Integer;
GroupIndex: Integer;
MemIndex: Integer;
TempValue: Integer;
K: Integer;
begin
DataCount := Length(ASourceData);
Gap := DataCount div 2;
while (Gap > 0) do
begin
for GroupIndex := 0 to Pred(Gap) do
begin
MemIndex := GroupIndex + Gap;
while (MemIndex < DataCount) do
begin
if AIsAsc then
begin
if ASourceData[MemIndex] < ASourceData[MemIndex - Gap] then
begin
TempValue := ASourceData[MemIndex];
k := MemIndex - Gap;
while(k >= GroupIndex) and (ASourceData[k] > TempValue) do
begin
ASourceData[k + Gap] := ASourceData[k];
k := k - Gap;
end;
ASourceData[k + Gap] := TempValue;
end;
end
else
begin
if ASourceData[MemIndex] > ASourceData[MemIndex - Gap] then
begin
TempValue := ASourceData[MemIndex];
k := MemIndex - Gap;
while(k >= GroupIndex) and (ASourceData[k] < TempValue) do
begin
ASourceData[k + Gap] := ASourceData[k];
Dec(k);
end;
ASourceData[k + Gap] := TempValue;
end;
end;
MemIndex := MemIndex + Gap;
end;
end;
Gap := Gap div 2;
end;
end;
3.2 相对于3.1,其实是一个裁剪代码的过程,确定了一个gap之后,从gap开始,然后增量是gap其实跟上面代码的含义一样
procedure TAlgorithmTest.ShellOrder_InterGroup(
const ASourceData: TIntegerDynArray; const AIsAsc: Boolean);
var
DataCount: Integer;
Gap: Integer;
MemIndex: Integer;
TempValue: Integer;
K: Integer;
begin
DataCount := Length(ASourceData);
Gap := DataCount div 2;
while(Gap > 0) do
begin
MemIndex := Gap;
while (MemIndex < DataCount) do
begin
if AIsAsc then
begin
if ASourceData[MemIndex] < ASourceData[MemIndex - Gap] then
begin
TempValue := ASourceData[MemIndex];
k := MemIndex - Gap;
while(k >= 0) and (ASourceData[k] > TempValue) do
begin
ASourceData[k + Gap] := ASourceData[k];
k := k - Gap;
end;
end;
end;
MemIndex := MemIndex + Gap;
end;
Gap := Gap div 2;
end;
end;
3.3 借鉴插入排序的直接反向冒泡,更加精简代码
procedure TAlgorithmTest.ShellOrder_InterGroupSwap(
const ASourceData: TIntegerDynArray; const AIsAsc: Boolean);
var
DataCount: Integer;
Gap: Integer;
MemIndex: Integer;
k: Integer;
begin
DataCount := Length(ASourceData);
Gap := DataCount div 2;
while(Gap >= 0) do
begin
MemIndex := Gap;
while(MemIndex < DataCount) do
begin
k := MemIndex;
while (k >= 0) do
begin
if NeedSwap(ASourceData[MemIndex], ASourceData[MemIndex - Gap], AIsAsc) then
SwapData(ASourceData, MemIndex, MemIndex);
k := k - Gap;
end;
MemIndex := MemIndex + Gap;
end;
Gap := Gap div 2;
end;
end;
4 选择排序
选择排序的思路就是按照排序的规则,找到当前位置的元素。思路很简洁,代码也很简单。
由于是选择,交换,所以这个过程中元素的顺序被打乱了,不再是一个左右比较交换,而是一个全局的比较交换。
外层控制了当前的位置,内层循环就是从当前位置的下个位置到结束的一个查找交换,所以,内外层有关系。
procedure TAlgorithmTest.SelectOrder(const ASourceData: TIntegerDynArray;
const IsAsc: Boolean);
var
I, J, DesiredIndex: Integer;
begin
for I := Low(ASourceData) to Pred(High(ASourceData)) do
begin
DesiredIndex := I;
for J := I + 1 to High(ASourceData) do
begin
if IsAsc then
begin
if ASourceData[DesiredIndex] > ASourceData[J] then
DesiredIndex := J;
end
else
begin
if ASourceData[DesiredIndex] < ASourceData[J] then
DesiredIndex := J;
end;
SwapData(ASourceData, I, DesiredIndex);
end;
end;
end;
5 归并排序
归并排序和快速排序使用了“分而治之”的思路,“分而治之”本身包含了由整体到部分的转换,包含一种迭代的思想在里面。这里的“分”是指将一个原始序列分为小块,就是从中间切,“治”就是一个将当前“分”的部分合起来,不断返回迭代的结果,最终保证了整个数据列是有需要的。
由于这种“分”是左右分,“治”也是按照两两比较,没有出现颠倒的数据元素,所以,这种排序是有序的。另外,由于这种算法的的实现在“治”的过程中需要申请额外的空间,所以,归并算法的效率比快速排序还高,应该有一种用空间换时间的概念在里面。
procedure TAlgorithmTest.MergeOrder(const ASourceData: TIntegerDynArray;
const AIsAsc: Boolean);
var
Temp: TIntegerDynArray;
procedure OrderPart(const ALow, AMid, AHigh: Integer);
var
i, j, k: Integer;
begin
i := ALow;
j := AMid + 1;
k := 0;
while(i <= AMid) and (j <= AHigh) do
begin
if AIsAsc then
begin
if ASourceData[i] < ASourceData[j] then
begin
Temp[k] := ASourceData[i];
Inc(k);
Inc(i);
end
else
begin
Temp[k] := ASourceData[j];
Inc(k);
Inc(j);
end;
end
else
begin
if ASourceData[i] > ASourceData[j] then
begin
Temp[k] := ASourceData[i];
Inc(k);
Inc(i);
end
else
begin
Temp[k] := ASourceData[j];
Inc(k);
Inc(j);
end;
end;
end;
while(i <= AMid) do
begin
Temp[k] := ASourceData[i];
Inc(k);
Inc(i);
end;
while(j <= AMid) do
begin
Temp[k] := ASourceData[j];
Inc(k);
Inc(j);
end;
for i := 0 to Pred(k) do
begin
ASourceData[ALow + i] := Temp[i];
end;
end;
procedure LoopOrder(const ALow, AHigh: Integer);
var
MidLocal: Integer;
begin
if ALow < AHigh then
begin
MidLocal := (ALow + AHigh) div 2;
LoopOrder(ALow, MidLocal);
LoopOrder(MidLocal + 1, AHigh);
OrderPart(ALow, MidLocal, AHigh);
end;
end;
begin
SetLength(Temp, Length(ASourceData));
LoopOrder(Low(ASourceData), High(ASourceData));
Temp := nil;
end;
6 快速排序
快速排序也使用了“分而治之”的思路,只是这种思路跟归并算法不一样。
快速排序的分是指,将一个数据列总是按照指定一个元素的方式,然后先从后往前找最小的,然后再从左往右找最大的一次填充,当左指针有有指针碰头了,然后再依据指针碰头的位置将数据列分成两个新的数据列,一次进行下去。
由于快速排序也是一种全局查找的算法,不再强调左右的元素位置,也是不稳定的算法。
procedure TAlgorithmTest.QuickOrder(const ASourceData: TIntegerDynArray;
const AIsAsc: Boolean);
procedure QuickLoop(const ALow, AHigh: Integer);
var
Seed, SeedIndex: Integer;
Left, Right: Integer;
begin
if ALow < AHigh then
begin
Seed := ASourceData[ALow];
Left := ALow;
Right := AHigh;
SeedIndex := ALow;
while(Left < Right) do
begin
While(Right > Left) and(ASourceData[Right] > Seed) do
begin
Dec(Right);
end;
if Right > Left then
begin
ASourceData[SeedIndex] := ASourceData[Right];
SeedIndex := Right;
Dec(Right);
end;
while(Right > Left) and(ASourceData[Left] < Seed) do
begin
Inc(Left);
end;
if Right > Left then
begin
ASourceData[SeedIndex] := ASourceData[Left];
SeedIndex := Left;
Inc(Left);
end;
end;
ASourceData[SendIndex] := Seed;
QuickLoop(ALow, Left - 1);
QuickLoop(Left + 1, AHigh);
end;
end;
begin
QuickLoop(Low(ASourceData), High(ASourceData));
end;
7 堆排序
对排序使用的是堆的特性来排序的,并不是将原始数据在内存中组织成一个对象。
这里的堆,是二叉堆。二叉堆的定义:父节点的键值总是大于或者等于任何一个子节点的键值,同时,每个节点的左子树和右子树也是二叉堆。
同时最小树是指任何节点的键值都小于其自己点的键值。
用到的特性:1)最小树根节点的键值最小 2)叶子集结点本身符合二叉堆的定义
用到的方式是下沉,下沉不关心本次的值,只会影响到之前的堆结构。
procedure TAlgorithmTest.MinHeapOrder(const ASourceData: TIntegerDynArray;
const AIsAsc: Boolean);
procedure FixdownHeap(const StartIndex: Integer; const MaxNodeCount: Integer);
var
I, J: Integer;
Temp: Integer;
begin
if StartIndex < 0 then
begin
Exit;
end;
I := StartIndex;
J := I * 2 + 1;
while(j < MaxNodeCount) do
begin
//Try to find the min node from left and right.
if ((j + 1) < MaxNodeCount) and (ASourceData[j + 1] < ASourceData[j]) then
begin
Inc(j);
end;
ASourceData[i] := ASourceData[j];
I := j;
j := I * 2 + 1;
end;
ASourceData[i] := Temp;
end;
procedure MakeMinHeap;
var
I: Integer;
Datalen: Integer;
begin
Datalen := Length(ASourceData);
//只需要从非叶子节点下沉即可
for I := Datalen div 2 - 1 downto 0 do
begin
FixdownHeap(I, Datalen);
end;
end;
procedure SwapData(const AFirstIndex, ASecondIndex: Integer);
var
Temp: Integer;
begin
if AFirstIndex = ASecondIndex then
begin
Exit;
end;
if ASourceData[AFirstIndex] = ASourceData[ASecondIndex] then
begin
Exit;
end;
Temp := ASourceData[AFirstIndex];
ASourceData[AFirstIndex] := ASourceData[ASecondIndex];
ASourceData[ASecondIndex] := Temp;
end;
procedure ReverseData;
var
Datalen: Integer;
I: Integer;
begin
Datalen := Length(ASourceData);
for I := 0 to Datalen div 2 do
begin
SwapData(I, Datalen - I - 1);
end;
end;
var
k: Integer;
begin
MakeMinHeap;
for k := High(ASourceData) downto 1 do
begin
SwapData(k, 0);
FixdownHeap(0, k);
end;
if AIsAsc then
ReverseData;
end;
由于堆排序真正是通过移动节点达到排序的目的,不关注实际的数据的先后顺序,本身是不稳定的。