排序算法总结

本文详细介绍了排序算法,包括插入排序(直接插入排序、折半插入排序)、Shell排序、冒泡排序、快速排序和选择排序(简单选择排序、堆排序)。其中,快速排序平均时间复杂度为O(nlog2n),适用于大规模数据;而Shell排序和冒泡排序效率较低,适合数据量较小的场景。此外,还提到了稳定性和辅助空间的要求。

总结一下数据结构经典排序算法,加深印象。

序号

分类

算法

1

插入排序

1)直接插入排序;2)折半插入排序;3)希尔排序

2

交换排序

1)冒泡排序;2)快速排序

3

选择排序

1)简单选择排序;2)堆排序

4

归并排序

 

5

基数排序

 

 

 

一、插入排序(InsertSort)
1)直接插入排序

时间复杂度:平均情况—O(n2)  最坏情况—O(n2) 辅助空间:O(1)  稳定性:稳定

插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。

 

01  void InsertSort(SqList &L) {

02    // 对顺序表L作直接插入排序。

03    inti,j;

04    for(i=2; i<=L.length; ++i)

05      if(LT(L.r[i].key, L.r[i-1].key)) {

06        //"<"时,需将L.r[i]插入有序子表

07       L.r[0] = L.r[i];                // 复制为哨兵

08       for (j=i-1;  LT(L.r[0].key,L.r[j].key);  --j)

09         L.r[j+1] = L.r[j];             // 记录后移

10       L.r[j+1] = L.r[0];              // 插入到正确位置

11      }

12  } // InsertSort

 

2)折半插入排序

时间复杂度:平均情况—O(n2)  稳定性:稳定

 

01  void BInsertSort(SqList &L) {

02    // 对顺序表L作折半插入排序。

03    inti,j,high,low,m;

04    for(i=2; i<=L.length; ++i) {

05     L.r[0] = L.r[i];       // 将L.r[i]暂存到L.r[0]

06      low= 1;   high = i-1;

07     while (low<=high) {    // 在r[low..high]中折半查找有序插入的位置

08        m= (low+high)/2;                           // 折半

09        if(LT(L.r[0].key, L.r[m].key)) high = m-1; // 插入点在低半区

10       else  low = m+1;                             // 插入点在高半区

11      }

12      for(j=i-1; j>=high+1; --j) L.r[j+1] = L.r[j]; // 记录后移

13     L.r[high+1] = L.r[0];                           // 插入

14    }

15  } // BInsertSort

 

3)Shell排序(ShellSort)

时间复杂度:理想情况—O(nlog2n)     最坏情况—O(n2)    稳定性:不稳定


Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。

Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。

 

01  void ShellInsert(SqList &L, int dk) {

02    // 对顺序表L作一趟希尔插入排序。本算法对算法10.1作了以下修改:

03   //     1. 前后记录位置的增量是dk,而不是1;

04   //     2. r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。

05    inti,j;

06    for(i=dk+1; i<=L.length; ++i)

07      if(LT(L.r[i].key, L.r[i-dk].key)) { // 需将L.r[i]插入有序增量子表

08       L.r[0] = L.r[i];                  // 暂存在L.r[0]

09       for (j=i-dk; j>0 && LT(L.r[0].key, L.r[j].key); j-=dk)

10         L.r[j+dk] = L.r[j];             // 记录后移,查找插入位置

11       L.r[j+dk] = L.r[0];               // 插入

12      }

13  } // ShellInsert

14 

15  void ShellSort(SqList &L, int dlta[], intt) {

16     // 按增量序列dlta[0..t-1]对顺序表L作希尔排序。

17     for(int k=0;k<t;k++)

18       ShellInsert(L, dlta[k]);  // 一趟增量为dlta[k]的插入排序

19  } // ShellSort

 

二、交换排序(ExchangeSort)

1)冒泡排序(BubbleSort)

时间复杂度:平均情况—O(n2)  最坏情况—O(n2)辅助空间:O(1) 稳定性:稳定

冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。

 

01  void BubbleSort(SeqList R) {

02    int i,j;

03    Boolean exchange; //交换标志

04    for(i=1;i<n;i++){exchange="FALSE;" j="n-1;j">=i;j--) //对当前无序区R[i..n]自下向上扫描

05              if(R[j+1].key< R[j].key){//交换记录

06                  R[0]=R[j+1]; //R[0]不是哨兵,仅做暂存单元

07                  R[j+1]=R[j];

08                 R[j]=R[0];

09                  exchange=TRUE; //发生了交换,故将交换标志置为真

10              }

11              if(!exchange) //本趟排序未发生交换,提前终止算法

12              return;

13    } //endfor(外循环)

14  } //BubbleSort</n;i++){>

 

2)快速排序(QuickSort)

时间复杂度:平均情况—O(nlog2n)  最坏情况—O(n2) 辅助空间:O(log2n) 稳定性:不稳定


快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。

(1) 如果不多于1个数据,直接返回。
(2) 一般选择序列最左边的值作为支点数据。
(3) 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
(4) 对两边利用递归排序数列。

快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。

 

01  int Partition(SqList &L, int low, inthigh) {

02  // 交换顺序表L中子序列L.r[low..high]的记录,使枢轴记录到位,

03     // 并返回其所在位置,此时,在它之前(后)的记录均不大(小)于它

04    KeyType pivotkey;

05    RedType temp;

06    pivotkey = L.r[low].key;     // 用子表的第一个记录作枢轴记录

07     while(low < high) {           // 从表的两端交替地向中间扫描

08       while (low < high && L.r[high].key>=pivotkey) --high;

09       temp=L.r[low];

10       L.r[low]=L.r[high];

11       L.r[high]=temp;           // 将比枢轴记录小的记录交换到低端

12       while (low  < high &&L.r[low].key < =pivotkey) ++low;

13       temp=L.r[low];

14       L.r[low]=L.r[high];

15       L.r[high]=temp;           // 将比枢轴记录大的记录交换到高端

16     }

17    return low;                  // 返回枢轴所在位置

18  } // Partition      

19 

20  int Partition(SqList &L, int low, inthigh) {

21  // 交换顺序表L中子序列L.r[low..high]的记录,使枢轴记录到位,

22     // 并返回其所在位置,此时,在它之前(后)的记录均不大(小)于它

23    KeyType pivotkey;

24    L.r[0] = L.r[low];            // 用子表的第一个记录作枢轴记录

25    pivotkey = L.r[low].key;      // 枢轴记录关键字

26     while(low < high) {            // 从表的两端交替地向中间扫描

27       while (low < high && L.r[high].key>=pivotkey) --high;

28       L.r[low] = L.r[high];      // 将比枢轴记录小的记录移到低端

29       while (low  < high &&L.r[low].key  < =pivotkey) ++low;

30       L.r[high] = L.r[low];      // 将比枢轴记录大的记录移到高端

31     }

32    L.r[low] = L.r[0];            // 枢轴记录到位

33    return low;                   // 返回枢轴位置

34  } // Partition      

35 

36  void QSort(SqList &L, int low, int high) {

37    // 对顺序表L中的子序列L.r[low..high]进行快速排序

38    intpivotloc;

39    if(low  <  high) {                      // 长度大于1

40     pivotloc = Partition(L, low, high); // 将L.r[low..high]一分为二

41     QSort(L, low, pivotloc-1); // 对低子表递归排序,pivotloc是枢轴位置

42     QSort(L, pivotloc+1, high);         // 对高子表递归排序

43    }

44  } // QSort   

45 

46  void QuickSort(SqList &L) {  // 算法10.8

47     // 对顺序表L进行快速排序

48    QSort(L, 1, L.length);

49  } // QuickSort

 

三、选择排序(SelectSort)

1)简单选择排序

时间复杂度:平均情况—O(n2)     最坏情况—O(n2)     辅助空间:O(1)      稳定性:不稳定

 

01  void SelectSort(SqList &L) {

02    // 对顺序表L作简单选择排序。

03    inti,j;

04    for(i=1; i < L.length; ++i) { // 选择第i小的记录,并交换到位

05      j =SelectMinKey(L, i);  // 在L.r[i..L.length]中选择key最小的记录

06      if(i!=j) {                //L.r[i]←→L.r[j];   与第i个记录交换

07       RedType temp;

08       temp=L.r[i];

09       L.r[i]=L.r[j];

10       L.r[j]=temp;

11      }

12    }

13  } // SelectSort

 

2)堆排序(HeapSort)

时间复杂度:平均情况—O(nlog2n)     最坏情况—O(nlog2n)     辅助空间:O(1)      稳定性:不稳定

 

堆排序适合于数据量非常大的场合(百万数据)。
堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。

堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。

 

01  void HeapAdjust(HeapType &H, int s, int m){

02    // 已知H.r[s..m]中记录的关键字除H.r[s].key之外均满足堆的定义,

03    // 本函数调整H.r[s]的关键字,使H.r[s..m]成为一个大顶堆

04    // (对其中记录的关键字而言)

05    int j;

06   RedType rc;

07    rc =H.r[s];

08    for(j=2*s; j < =m; j*=2) {   // 沿key较大的孩子结点向下筛选

09      if(j < m && H.r[j].key < H.r[j+1].key) ++j; // j为key较大的记录的下标

10      if(rc.key >= H.r[j].key) break;        // rc应插入在位置s上

11     H.r[s] = H.r[j];  s = j;

12    }

13    H.r[s]= rc;  // 插入

14  } // HeapAdjust  

15 

16  void HeapSort(HeapType &H) {

17     // 对顺序表H进行堆排序。

18     inti;

19    RedType temp;

20     for(i=H.length/2; i>0; --i)  // 把H.r[1..H.length]建成大顶堆

21       HeapAdjust ( H, i, H.length );

22       for (i=H.length; i>1; --i) {

23          temp=H.r[i];

24          H.r[i]=H.r[1];

25          H.r[1]=temp;  // 将堆顶记录和当前未经排序子序列Hr[1..i]中

26                         // 最后一个记录相互交换

27          HeapAdjust(H, 1, i-1);  // 将H.r[1..i-1] 重新调整为大顶堆

28        }

29  } // HeapSort

 

四、归并排序

1)归并排序(MergeSort)

时间复杂度:平均情况—O(nlog2n)      最坏情况—O(nlog2n)      辅助空间:O(n)      稳定性:稳定

归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。

 

01  void Merge (RedType SR[], RedType TR[], int i,int m, int n) {

02     // 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]

03     intj,k;

04     for(j=m+1, k=i;  i < =m && j <=n;  ++k) {

05        //将SR中记录由小到大地并入TR

06        ifLQ(SR[i].key,SR[j].key) TR[k] = SR[i++];

07       else TR[k] = SR[j++];

08     }

09     if (i< =m)  // TR[k..n] = SR[i..m];  将剩余的SR[i..m]复制到TR

10       while (k < =n && i < =m)TR[k++]=SR[i++];

11     if (j< =n)  // 将剩余的SR[j..n]复制到TR

12       while (k < =n &&j  <=n) TR[k++]=SR[j++];

13  } // Merge  

14 

15  void MSort(RedType SR[], RedType TR1[], int s,int t) {

16     // 将SR[s..t]归并排序为TR1[s..t]。

17     intm;

18    RedType TR2[20];

19     if(s==t) TR1[t] = SR[s];

20     else{

21       m=(s+t)/2;            // 将SR[s..t]平分为SR[s..m]和SR[m+1..t]

22       MSort(SR,TR2,s,m);    // 递归地将SR[s..m]归并为有序的TR2[s..m]

23       MSort(SR,TR2,m+1,t);  // 将SR[m+1..t]归并为有序的TR2[m+1..t]

24       Merge(TR2,TR1,s,m,t); // 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]

25     }

26  } // MSort  

27 

28  void MergeSort(SqList &L) {

29    // 对顺序表L作归并排序。

30   MSort(L.r, L.r, 1, L.length);

31  } // MergeSort

 

五、基数排序

1)基数排序(RadixSort)

时间复杂度:平均情况—O(d(n+rd))   最坏情况—O(d(n+rd))   辅助空间:O(rd)      稳定性:稳定


基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。

 

01  void Distribute(SLList &L, int i, ArrType &f,ArrType &e) {

02    // 静态链表L的r域中记录已按(keys[0],...,keys[i-1])有序,

03    // 本算法按第i个关键字keys[i]建立RADIX个子表,

04    // 使同一子表中记录的keys[i]相同。f[0..RADIX-1]和e[0..RADIX-1]

05    // 分别指向各子表中第一个和最后一个记录。

06    int j,p;

07    for(j=0; j < RADIX; ++j) f[j] = 0;     //各子表初始化为空表

08    for(p=L.r[0].next;  p;  p=L.r[p].next) {

09      j =L.r[p].keys[i]-'0';  // 将记录中第i个关键字映射到[0..RADIX-1],

10      if(!f[j]) f[j] = p;

11      elseL.r[e[j]].next = p;

12      e[j]= p;                // 将p所指的结点插入第j个子表中

13    }

14  } // Distribute   

15 

16  void Collect(SLList &L, int i, ArrType f,ArrType e) {

17    // 本算法按keys[i]自小至大地将f[0..RADIX-1]所指各子表依次链接成

18    // 一个链表,e[0..RADIX-1]为各子表的尾指针

19    intj,t;

20    for(j=0; !f[j]; j++);  // 找第一个非空子表,succ为求后继函数: ++

21   L.r[0].next = f[j];  // L.r[0].next指向第一个非空子表中第一个结点

22    t =e[j];

23    while(j < RADIX) {

24      for(j=j+1; j < RADIX && !f[j]; j++);       // 找下一个非空子表

25      if(j < RADIX) // 链接两个非空子表

26        {L.r[t].next = f[j];  t = e[j]; }

27    }

28   L.r[t].next = 0;   // t指向最后一个非空子表中的最后一个结点

29  } // Collect  

30 

31  void RadixSort(SLList &L) {

32     // L是采用静态链表表示的顺序表。

33     // 对L作基数排序,使得L成为按关键字自小到大的有序静态链表,

34     //L.r[0]为头结点。

35     inti;

36    ArrType f, e;

37     for(i=1; i < L.recnum; ++i) L.r[i-1].next = i;

38    L.r[L.recnum].next = 0;     // 将L改造为静态链表

39     for(i=0; i < L.keynum; ++i) {

40        //按最低位优先依次对各关键字进行分配和收集

41       Distribute(L, i, f, e);    // 第i趟分配

42       Collect(L, i, f, e);       // 第i趟收集

43       print_SLList2(L, i);

44     }

45  } // RadixSort

 

下面是一个总的表格,总结了我们常见的排序算法的特点:

排序法

 平均时间

最差情形

稳定度

额外空间

备注

冒泡

 O(n2)

  O(n2)

 稳定

O(1)

n小时较好

交换

  O(n2)

  O(n2)

不稳定

O(1)

n小时较好

选择

 O(n2)

 O(n2)

不稳定

O(1)

n小时较好

插入

 O(n2)

 O(n2)

稳定

O(1)

大部分已排序时较好

基数

O(logRB)

O(logRB)

稳定

O(n)

B是真数(0-9),

R是基数(个十百)

Shell

O(nlogn)

O(ns) 1<2

不稳定

O(1)

s是所选分组

快速

O(nlogn)

O(n2)

不稳定

O(nlogn)

n大时较好

归并

O(nlogn)

O(nlogn)

稳定

O(1)

n大时较好

O(nlogn)

O(nlogn)

不稳定

O(1)

n大时较好

 

Reference:

http://www.whhpaccp.com/hpaccp/studydb_10164123.html

http://www.shilin-blog.com/73.html

http://www.cnblogs.com/ziyiFly/archive/2008/09/10/1288516.html

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值