基于MoreWindows整理的基础排序的个人理解

本文详细介绍了七种经典的排序算法:冒泡排序、插入排序、希尔排序、选择排序、归并排序、快速排序和堆排序。文章不仅解释了每种算法的基本原理,还深入探讨了它们的稳定性和效率,并提供了具体的实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    春节期间拜读了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;
由于堆排序真正是通过移动节点达到排序的目的,不关注实际的数据的先后顺序,本身是不稳定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值