七大经典排序算法及其心得体会
经典排序算法简介
以内排序(排序整个过程中,待排序的所有记录全部被放置在内存中)的七大经典排序算法为例子,就时间性能、辅助空间、算法复杂性来具体阐述。
排序算法分类
按照算法复杂程度分类
- 简单排序:冒泡排序、简单选择排序、直接插入排序
- 复杂排序:希尔排序、堆排序、归并排序、快速排序
按照算法类别分类
- 交换类:冒泡排序、快速排序
- 选择类:简单选择排序、堆排序
- 插入类:直接插入排序、希尔排序
- 归并类:归并排序
排序准备
建立一个排序用的顺序表结构L,用于下面七种经典排序
1 #define MAXSIZE 10
2 typedef struct SqList
3 {
4 int r[MAXSIZE+1];
5 int length;
6 }SqList;
/*排序的交换函数*/
7 void swap(SqList *L,int i,int j)
8 {
9 int temp=L->r[i];
10 L->[i]=L->[j];
11 L->[j]=temp;
12 }
冒泡排序
冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
- 中心思想:两两比较,符合要求交换。
算法
1 void BubbleSort(SqList *L)
2 {
3 int i,j;
4 for(int i=1;i<L->length;i++)
5 {
6 for(int j=L->length-1;j>=i;j--)
7 if(L->r[j+1]<L->[j])
8 swap(L,j+1,j);
9 }
10 }
时间复杂度
- 平均运行时间:所有情况中最有意义的,因为它是期望的运行时间
- 最坏时间复杂度:一般在没有特殊说明的情况下,都是指最坏时间复杂度
冒泡排序最坏时间复杂度是逆序,需要比较下面这么多次
∑
i
=
2
n
(
i
−
1
)
=
1
+
2
+
3
+
⋅
⋅
⋅
+
(
n
−
1
)
=
n
(
n
−
1
)
2
\sum_{i=2}^n{\left( i-1 \right) =1+2+3+\cdot \cdot \cdot +\left( n-1 \right) =\frac{n\left( n-1 \right)}{2}}
i=2∑n(i−1)=1+2+3+⋅⋅⋅+(n−1)=2n(n−1)
并做出同等数量级的交换,总的时间复杂度为
o
(
n
2
)
o( n^2 )
o(n2)
简单选择排序
简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<i<n)个记录交换之。
- 中心思想:先比较选择,后记录交换
算法
1 void SelectSort(SqList *L)
2 {
3 int i,j,m;
4 for(i=1;i<L->length;i++)
5 {
6 min=i;
7 for(j=i+1;j<=L->length;j++)
8 if(L->r[j]<L->r[min])
9 min=j;
10 if(i!=min)/**需要判断i和min,减少交换次数*/
11 swap(L,i,min);
12 }
13 }
时间复杂度
选择排序最坏时间复杂度是主要是比较选择过程,需要比较下面这么多次
∑
i
=
1
n
−
1
(
n
−
i
)
=
n
−
1
+
n
−
2
+
⋅
⋅
⋅
+
1
=
n
(
n
−
1
)
2
\sum_{i=1}^{n-1}{\left( n-i \right) =n-1+n-2+\cdot \cdot \cdot +1=\frac{n\left( n-1 \right)}{2}}
i=1∑n−1(n−i)=n−1+n−2+⋅⋅⋅+1=2n(n−1)
而交换次数最多为
n
−
1
n-1
n−1,因此总的时间复杂度为
o
(
n
2
)
o( n^2 )
o(n2)
直接插入排序
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
- 中心思想:设置额外空间,增加序列移动
算法
1 void InsertSort(SqList *L)
2 {
3 int i,j;
4 for(i=2;i<=L->length;i++)
5 {
6 if(L->r[i]<L->r[i-1])
7 {
8 L->r[0]=L->r[i];/*设置哨兵*/
9 for(j=i-1;L->r[j]>L->r[0];j--)/*>符号的原则,相等不交换*/
10 L->r[j+1]=L->r[j];
11 L->r[j+1]=L->r[0];
12 }
13 }
14 }
时间复杂度
直接插入排序最坏时间复杂度是逆序,需要比较下面这么多次
∑
i
=
2
n
i
=
1
+
2
+
3
+
⋅
⋅
⋅
+
n
=
(
n
+
2
)
(
n
−
1
)
2
\sum_{i=2}^n{i=1+2+3+\cdot \cdot \cdot +n=\frac{\left( n+2 \right) \left( n-1 \right)}{2}}
i=2∑ni=1+2+3+⋅⋅⋅+n=2(n+2)(n−1)
而记录的移动次数同时最大下面这么多次
∑
i
=
2
n
i
+
1
=
2
+
3
+
4
+
⋅
⋅
⋅
+
n
+
1
=
(
n
+
4
)
(
n
−
1
)
2
\sum_{i=2}^n{i+1=2+3+4+\cdot \cdot \cdot +n+1=\frac{\left( n+4 \right) \left( n-1 \right)}{2}}
i=2∑ni+1=2+3+4+⋅⋅⋅+n+1=2(n+4)(n−1)
因此,我们得出直接插入排序法的时间复杂度为
0
(
n
2
)
0(n^2)
0(n2)。
希尔排序
在直接插入排序的基础上,我们需要采取跳跃分割的策略:
增加increment,increment=increment/3+1
- 中心思想跳跃分割,直接插入
算法
1 void ShellSort(SqList *L)
2 {
3 int i,j;
4 int increment=L->length;
5 do
6 {
7 increment=increment/3+1;
8 for(i=increment+1;i<=L->length;i++)
9 {
10 if(L->r[i]<L->r[i-increment])
11 {
12 L->r[0]=L->r[i];/*设置哨兵*/
13 for(j=i-increment;i>0&&L->r[j]>L->[0];j-=increment)
14 L->r[j+increment]=L->r[j];
15 L->r[j+increment]=L->r[0];
16 }
17 }
18 } while (increment>1);
19 }
时间复杂度
希尔排序的时间复杂度由于跳跃式的移动,研究表明当
d
l
t
a
[
k
]
=
2
t
−
k
+
1
−
1
(
0
⩽
k
⩽
t
⩽
⌊
log
2
(
n
+
1
)
⌋
)
dlta\left[ k \right] =2^{t-k+1}-1\left( 0\leqslant k\leqslant t\leqslant \lfloor \log _2\left( n+1 \right) \rfloor \right)
dlta[k]=2t−k+1−1(0⩽k⩽t⩽⌊log2(n+1)⌋)
其空间复杂度
o
(
n
3/
2
)
o\left( n^{\text{3/}2} \right)
o(n3/2),要好于直接排序的
o
(
n
2
)
o(n^2)
o(n2)
堆排序
堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
- 中心思想:不断的构成大顶堆的过程(二叉树)
算法
1 void HeapSort(SqList *L)
2 {
3 int i;
4 for(i=L->r/2;i>0;i--)
5 HeapAdjust(L,i,L->length);/*把L中的r构建成一个大顶堆*/
6 for(i=L->length;i>1;i--)
7 {
8 swap(L,1,i);
9 HeapAdjust(L,1,i-1);/*将L->r[1..1-1]重新调整为大顶堆*/
10 }
11 }
/*本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆*/
12 void HeapAdjust(SqList *L,int s,int m)
13 {
14 int j,temp;
15 temp=L->r[s];
16 for(j=2*s;j<=m;j*=2)
17 {
18 if(j<m&&L->r[j]<L->r[j+1])/*j<m是判断是否有右孩子*/
19 j++;
20 if(temp>=L->r[j])
21 break;
22 L->r[s]=L->r[j];
23 s=j;
24 }
25 L->r[s]=temp;/*s为全局变量,temp包含两个意思一个是直接break另外一个是L->r[j]*/
26 }
时间复杂度
堆排序时间复杂度包含构建堆和正式排序
- 构建堆的时间复杂度 o ( n ) o(n) o(n)
- 正式排序时,第i次取堆顶记录重建堆需要用 o ( l o g i ) o(logi) o(logi)的时间(完全二又树的某个结点到根结点的距离为 ⌊ log 2 i ⌋ + 1 \lfloor \log _2i \rfloor +1 ⌊log2i⌋+1,并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为 o ( n l o g n ) o(nlogn) o(nlogn)。
总的来说算时间复杂度为 o ( n l o g n ) o(nlogn) o(nlogn)。
归并排序
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 ⌈ n / 2 ⌉ \lceil n/2 \rceil ⌈n/2⌉( ⌈ x ⌉ \lceil x \rceil ⌈x⌉)表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
- 中心思想:先拆分在合并,子序列合并排序
算法
/*递归调用,向外封装了一个函数*/
1 void MergeSort(SqList *L)
2 {
3 MSort(L->r,L->r,1,L->length);
4 }
/*将SR[s..t]归并排序为TR1[s..t]*/
5 void MSort(int SR[],int TR1[],int s,int t)
6 {
7 i--nt m;
8 int TR2[MAXSIZE+1];
9 if(s==t)
10 TR1[s]=SR[s];
11 else
12 {
13 m=(s+t)/2;
14 MSort(SR,TR2,s,m);
15 MSort(SR,TR2,m+1,t);
16 Merge(TR2,TR1,s,m,t);
17 }
18 }
/*将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]*/
19 void Merge(int SR[],int TR[],int i,int m,int n)
20 {
21 int j,k,l;
22 for(j=m+1,k=i;i<=m&&j<=n;k++)/*这里不用||代替&&原因是为了防止数组越界,由于i++和j++*/
23 {
24 if(SR[i]<SR[j])
25 TR[k]=SR[i++];
26 else
27 TR[k]=SR[j++];
28 }
29 if(i<=m)/*29-38不可省,主要为了防止数组越界,导致i和j在SR数组赋值到TR上出现混乱*/
30 {
31 for(l=0;l<=m-i;l++)/*这里是L不是数字1*/
32 TR[k+l]=SR[i+l];
33 }
34 if(j<=n)
35 {
36 for(l=0;l<=n-j;l++)
37 TR[k+l]=SR[j+l];
38 }
39 }
时间复杂度
归并排序时间复杂度包括遍历和整体归并
- 遍历:排序序列中的所有记录扫描一遍,因此耗费 o ( n ) o(n) o(n)时间
- 整体归并:由完全二叉树的深度可知,整个归并排序需要进行 ⌈ log 2 n ⌉ \lceil \log _2n \rceil ⌈log2n⌉次,时间复杂度为 o ( n l o g n ) o(nlogn) o(nlogn)
因此归并排序总的时间复杂度要 o ( n l o g n ) o(nlogn) o(nlogn)
快速排序
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
- 枢轴分小大两部分,递归调用记录枢轴
算法
/*向外封装一个函数*/
1 void QuickSort(SqList *L)
2 {
3 QSort(L,1,L->length);
4 }
/*对顺序表工中的子序列L->r[1ow..high]作快速排序*/
5 void QSort(SqList *L,int low,int high)
6 {
7 int pivot;/*设置枢轴*/
8 if(low<high)
9 {
10 pivot=Partition(L,low,high)
11 QSort(L,low,pivot-1);
12 QSort(L,pivot+1,high);
13 }
14 }
/*使枢轴记录到位,并返回其所在位置*/
15 int Partition(SqList *L,int low,int high)
16 {
17 int pivotkey;
18 pivotkey=L->r[low];
19 while(low<high)
20 {
21 while(low<high&&L->r[high]>=pivotkey)/*>=原则相等不交换*/
22 high--;
23 swap(L,low,high);
24 while(low<high&&L->r[low]<=pivotkey)
25 low++;
26 swap(L,low,high);
27 }
28 return low;/*返回枢轴所在位置*/
29 }
时间复杂性
快速排序的时间复杂性最坏的情况为正序和逆序(即一棵斜树),包括递归调用和比较找枢轴
- 递归调用:n-1次的递归调用,时间复杂度为 o ( n ) o(n) o(n)
- 比较找枢轴:第i次划分需要经过n-i次关键字的比较才能找到第i个记录,也就是枢轴的位置,比较找的次数为 ∑ i = 1 n − 1 n − i = n − 1 + n − 2 + ⋅ ⋅ ⋅ + 1 = n ( n − 1 ) 2 \sum_{i=1}^{n-1}{n-i=n-1+n-2+\cdot \cdot \cdot +1=\frac{n\left( n-1 \right)}{2}} ∑i=1n−1n−i=n−1+n−2+⋅⋅⋅+1=2n(n−1),则时间复杂度为 o ( n 2 ) o(n^2) o(n2)
总的时间复杂度为 o ( n 2 ) o(n^2) o(n2)
总结
- 经过优化的快排是目前最好的排序算法,特别是应用于大数据(n>50)
- 小数据的时候简单排序更胜一筹(直接插入排序)
- 归并排序的稳定性最好
参考文献
[1] 程杰. 大话数据结构[M]. 清华大学出版社, 2011.
[2] 啊哈磊. 啊哈! 算法[M]. 人民邮电出版社, 2014.