排序是数据结构中非常重要的内容。排序会涉及到时间复杂度和空间复杂度的计算;排序会运用到数据结构中的基础结构,例如栈、堆等;很多排序算法使用递归会更加容易实现,总之,排序可以说是对数据结构基础内容的综合应用。这篇博客中将会介绍一些常用的排序算法,对于每个算法的复杂度,思路作详细描述。
插入排序
直接插入排序
思想介绍
基本思想:对于一个集合中的元素,顺序地把待排序的元素根据值的大小插入到已排序的序列中。将已排序的元素看成一个子集合,排序刚开始时子集合中只有一个元素,随着排序逐次增大,直到子集合大小和集合大小一致。 举例:假设要对数组a[11] = { 4,7,6,8,2,9,5,3,1,3,10 } 排升序,将数组中的所有元素看成一个集合,初始时子集合就是{ 4 },4可以看作是有序序列;接下来就是往子集合中插入数据,按照数组中元素的顺序,要把7根据大小插入子集合,插入后子集合变为{4,7};再接下来要根据大小插入6,插入后子集合变为{4, 6, 7}; …… 依次将未排序的元素都插入到子集合中,这样就和得到按照升序排列的序列。
代码实现
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int tmp = a[i+1];
int end = i;
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end+1] = tmp;
}
}
算法性能评价
时间复杂度
- 最好情况:原始数据已经排好序。此时算法内层的while循环的循环次数每次都是0,外层for循环执行n-1次,for循环内部有3个赋值语句和1个比较语句。因此最好情况下的时间复杂度为
O
(
N
)
O(N)
O(N);
- 最坏情况:最坏情况是原始数据的顺序和要排的顺序相反。此时内存while循环每次执行i次,所以for循环中的比较次数和赋值语句的执行次数为等差数列的前n项和,大致是
n
2
n^2
n2量级的。因此最坏情况下的时间复杂度为
O
(
N
2
)
O(N^2)
O(N2).
空间复杂度
直接插入排序的空间复杂度为
O
(
1
)
O(1)
O(1)
算法稳定性 稳定。 算法稳定性是指排好序的元素之间的相对顺序和原始数据相比保持不变。假设数组a中,a[0]和a[7]中的数值相等,a[0]中的数在a[7]前面,使用某个算法排完序后a[0]中的数还在a[7]前面,这就说明该算法是稳定的,否则就是不稳定的。
希尔排序
希尔排序又称为缩小增量排序。增量指的是原始数据分成的小组中元素的个数。
思想介绍
直接插入排序的时间复杂度在
O
(
N
)
O(N)
O(N)和
O
(
N
2
)
O(N^2)
O(N2)之间,原始数据越接近有序,直接插入排序的时间效率就越高,希尔排序就是利用这一点提出的。
基本思想:把待排序的数据元素分成若干个小组,对同一小组的内的数据元素 用直接插入排序;小组的个数逐次减少,当所有数据都在同一小组内排序后,排序过程结束。 解释:分成多个小组之后小组内的数据元素更少,然后对小组内的元素使用插入排序,使得小组内的数据有序,这样做是因为小数目数据使用插入排序需要移动的数据更少;然后减少小组的个数,进行直接插入排序,直到所有数据元素都在同一个小组,最后使用直接插入排序。这样做的时间复杂度是优于直接使用插入排序的。
代码实现
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = (gap / 3) + 1;
for (int i = 0; i < n - gap; i += gap)
{
int end = i;
int tmp = a[i + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
算法性能评价
时间复杂度
希尔排序的时间复杂度会收到gap取值的影响,根据上述代码中的gap取值方式,时间复杂度应该在
O
(
N
1
.
25
)
O(N^1.25)
O(N1.25)和
O
(
1.6
×
N
1
.
25
)
O(1.6\times N^1.25)
O(1.6×N1.25)之间。
空间复杂度
希尔排序的空间复杂度为
O
(
1
)
O(1)
O(1)
算法稳定性
不稳定。分组排序时可能会打乱很多元素的相对位置。
选择排序
直接选择排序
一般情况下,选择排序就是指直接选择排序。
思想介绍
基本思想:每次从待排序的数据元素中集合中选取值最小的元素,放到数据元素集合最前面(或最后面),数据元素集合不断缩小,当数据元素集合为空时,排序结束。 实现方式:排升序时,每次从待排序的数据元素集合中选取最小的数放在数据元素集合最前面,最大的数放在最后面。
代码实现
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int MinIndex = left, MaxIndex = left;
for (int i = left; i <= right; i++)
{
if (a[i] < a[MinIndex])
{
MinIndex = i;
}
if (a[i] > a[MaxIndex])
{
MaxIndex = i;
}
}
Swap(&a[left], &a[MinIndex]);
if (MaxIndex == left)
{
MaxIndex = MinIndex;
}
Swap(&a[MaxIndex], &a[right]);
left++;
right--;
}
}
算法性能评价
时间复杂度
直接选择排序算法最外层while循环大概会执行n/2次,内层for循环大概执行n+(n-2)+ … +0次,循环内部的赋值语句和比较语句都是常数个,所以总的时间复杂度应该是
O
(
N
2
)
O(N^2)
O(N2).
空间复杂度
直接选择排序不需要额外开辟空间,所以空间复杂度为
O
(
1
)
O(1)
O(1).
算法稳定性
稳定。在待排序的数据元素集合中选择最小或者最大的时候,选择的是遇到的第一个最小的数或者最大的数,因此不会打乱数据元素的相对位置。
堆排序
堆排序也是选择排序的一种。
思想介绍
基本思想:排升序建大堆,排降序建小堆。每次选择堆中根结点位置的元素放到待排序元素集合的最后一个位置。
堆排序分为建堆过程和排序过程,向下调整算法是核心。
代码实现
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[root])
{
Swap(&a[child], &a[root]);
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
for (int i = n-1; i > 0; i--)
{
Swap(&a[i], &a[0]);
AdjustDwon(a, i, 0);
}
}
算法性能评价
时间复杂度
-
使用向下调整算法建堆的时间复杂度 堆是完全二叉树,为了方便计算,我们直接计算跟堆层数相等的满二叉树建堆的时间复杂度。在最坏的情况下,满二叉树中所有非叶结点都要使用向下调整算法来调整,假设树的高度为
h
h
h,那么第
1
1
1层有
2
0
2^0
20个结点,需要向下移动
h
?
1
h-1
h?1层;第
2
2
2层有
2
1
2^1
21个结点,每个结点需要向下移动
h
?
2
h-2
h?2层;以此类推第
h
?
1
h-1
h?1层有
2
h
?
2
2^h-2
2h?2个几点,每个结点需要向下移动
1
1
1层;那么总的移动次数为:
T
(
n
)
=
2
0
×
(
h
?
1
)
+
2
1
×
(
h
?
2
)
+
2
2
×
(
h
?
3
)
+
.
.
.
+
2
h
?
2
×
1
T(n)=2^0\times (h-1)+2^1\times (h-2)+2^2\times (h-3)+...+2^{h-2}\times 1
T(n)=20×(h?1)+21×(h?2)+22×(h?3)+...+2h?2×1 对于上式可以采用错位相减法计算:
2
×
T
(
n
)
?
T
(
n
)
=
2
1
+
2
2
+
.
.
.
.
+
2
h
?
2
+
2
h
?
1
?
2
0
×
(
h
?
1
)
=
2
(
1
?
2
h
?
1
)
1
?
2
?
h
+
1
=
2
h
?
h
?
1.
\begin{aligned} 2\times T(n)-T(n)&=2^1+2^2+ ....+2^{h-2}+2^{h-1}-2^0\times (h-1)\\ &=\frac {2(1-2^{h-1})} {1-2}-h+1\\ &=2^h-h-1. \end{aligned}
2×T(n)?T(n)?=21+22+....+2h?2+2h?1?20×(h?1)=1?22(1?2h?1)??h+1=2h?h?1.? 根据满二叉树结点个数和树的深度关系
2
h
?
1
=
N
2^h-1=N
2h?1=N可得
T
(
N
)
=
N
?
L
o
g
2
N
T(N) = N-Log_2N
T(N)=N?Log2?N 用大O表示法可得建堆的时间复杂度为
O
(
N
)
O(N)
O(N) -
排序过程的时间复杂度 每次交换根结点元素和最后一个位置的元素,交换完之后再调整堆,假设待排序元素个数为N,第一次排序后需要调整
L
o
g
2
(
N
?
1
)
Log_2{(N-1)}
Log2?(N?1)次,第二次排序后要调整
L
o
g
2
(
N
?
2
)
Log_2{(N-2)}
Log2?(N?2)次,…,最后一次排序后要调整0次,则一共需要调整的次数为
L
o
g
2
(
N
?
1
)
+
L
o
g
2
(
N
?
2
)
+
.
.
.
+
L
o
g
2
(
1
)
=
L
o
g
2
(
n
?
1
)
!
≤
N
×
L
o
g
2
N
Log_2(N-1) + Log_2(N-2) +...+Log_2(1) = Log_2(n-1)!\leq N\times Log_2N
Log2?(N?1)+Log2?(N?2)+...+Log2?(1)=Log2?(n?1)!≤N×Log2?N 用大O表示法为
O
(
N
×
L
o
g
2
N
)
O(N\times Log_2N)
O(N×Log2?N)
综上堆排序的时间复杂度为
O
(
N
×
L
o
g
2
N
+
N
)
=
O
(
N
L
o
g
2
N
)
O(N\times Log_2N+N)=O(NLog_2N)
O(N×Log2?N+N)=O(NLog2?N)
空间复杂度 堆排序的空间复杂度为
O
(
1
)
O(1)
O(1)
算法稳定性 堆排序过程中会打乱原始数据的顺序,所以堆排序是不稳定的。
交换排序
冒泡排序
思想介绍
基本思想:设数组a中存放了n个数据元素,循环进行n-1趟如下排序:从a[0]开始依次比较相邻两个数的值,如果逆序就交换,否则不交换。这样的话第1趟排序结束后,数值最大的数据元素就会被放在a[n-1]中;第2趟结束后次大的数会被放到a[n-2]中;n-1趟结束之后序列就有序了。
代码实现
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
算法性能评价
时间复杂度
O
(
N
2
)
O(N^2)
O(N2)
空间复杂度
O
(
1
)
O(1)
O(1)
算法稳定性 稳定
快速排序
思想介绍
基本思想 递归法:假设待排序的数据元素存放在数组a中,选取最左边的数据元素a[0]作为关键值key,然后从待排序数据元素左边left和右边right开始(left和right指数组下标,初始时left=0,right=n-1),先从右边往左遍历找比key值小的数据元素low,然后从左边往右边遍历找比key值大的数据元素high,找到后交换low和high;然后接着找,直到left大于或等于right,这时候交换key和a[left],此时,key左边的数都比key小,右边都比key大,然后按照上面的思路递归对key的左右两边区间进行排序。 概括解释:快排简单来说就是,在要排序的数据中选取一个基本值,将这个基本值放到属于它的位置上(排好序时它的位置),然后这个值的左边都是小于它的数,右边都是大于它的数;接下来对于这个基本值左右两边的区间采用同样的思路进行排序,然后一直递归,直到区间中的数据个数为1,此时整个序列就有序了。
非递归法:利用栈来实现。将要排序的区间左右下标依次入栈,然后出栈,找到基本值的位置,如果基本值左右两边区间依旧存在,那么将左右两边区间边界下标依次入栈,然后再出栈,一直重复这个过程直到左右区间不存在。
要点
- 确定基本值:我们期望我们选择的基本值是中位数,这样的话基本值处于序列中间,可以将原数组均分为两个子数组,递归总次数为
L
o
g
2
N
Log_2N
Log2?N量级;否则,如果我们选取的基本值在序列左边或者右边,那么递归总次数就是
N
2
N^2
N2量级。所以对于基本值的选取,我们采用三数取中法来实现。
- 确定基本值的位置:对于快排来说,找到基本值的位置是最关键的,这个步骤一共有三种方法来寻找。
上面两个要点将在代码中体现:三数取中由函数GetMidIndex实现,确定基本值得位置分别用PartSort1、PartSort2和PartSort3实现。
代码实现
递归法
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[mid] < a[right])
{
if (a[mid] > a[left])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[right] > a[left])
{
return right;
}
else if (a[left] > a[mid])
{
return mid;
}
else
{
return left;
}
}
}
int PartSort1(int* a, int left, int right)
{
int keyi = left;
int MidIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[MidIndex]);
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
int PartSort2(int* a, int left, int right)
{
int MidIndex = GetMidIndex(a, left, right);
Swap(&a[MidIndex], &a[left]);
int key = a[left];
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[left] = a[right];
while (left < right && a[left] <= key)
{
left++;
}
a[right] = a[left];
}
a[left] = key;
return left;
}
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = keyi;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
prev++;
if (prev != cur)
{
Swap(&a[prev], &a[cur]);
}
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int meeti = PartSort1(a, left, right);
QuickSort(a, left, meeti - 1);
QuickSort(a, meeti + 1, right);
}
非递归法
#include"Stack.h"
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
int left, right;
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (right > keyi + 1)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
}
算法性能评价
时间复杂度
O
(
N
L
o
g
2
N
)
O(NLog_2N)
O(NLog2?N)。主要是算递归的次数。
空间复杂度
O
(
L
o
g
2
N
)
O(Log_2N)
O(Log2?N) (不同写法可能会不一样)
算法稳定性 不稳定
归并排序
思想介绍
基本思想:设数组a中存放了n个数据元素,初始时把它们看成n个长度为1的有序子数组,然后从第一个有序子数组开始,把相邻的有序子数组两两合并,得到[n/2]个长度为2的新的有序数组(n为奇数时,最后一个新的有序子数组的长度为1)。对这些新的有序子数组在进行两两归并。如此重复直到得到一个长度为n的有序数组为止。
代码实现
void _mergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1;
_mergeSort(a, left, mid, tmp);
_mergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_mergeSort(a, 0, n - 1, tmp);
free(tmp);
}
此外,归并排序还有非递归实现方法,代码如下:
void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int left = begin1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
for (int j = left; j <= end2; j++)
{
a[j] = tmp[j];
}
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 >= n)
break;
if (end2 >= n)
{
end2 = n - 1;
}
_MergeSortNonR(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2;
}
free(tmp);
}
算法性能评价
时间复杂度
O
(
N
L
o
g
2
N
)
O(NLog_2N)
O(NLog2?N)
空间复杂度
O
(
N
)
O(N)
O(N)
算法稳定性 稳定。
非比较排序
计数排序
思想介绍
基本思想:对于待排序的一组数,统计这组数所处在的区间和相同的数出现次数,然后根据统计结果将数按照顺序放回原数组中。
下图是对计数排序算法的形象展示
解释 我把统计待排序数据相同元素个数的数组称为统计数组,
统计数组大小
=
m
a
x
?
m
i
n
+
1
统计数组大小=max-min+1
统计数组大小=max?min+1 上图中max=19,min=11,所以统计数组大小为19-11+1=9 ,开辟一个大小为9的数组a,a[0]位置存11的个数,a[1]位置存12的个数,… ,a[8]位置存19的个数,上图中元素和它的个数按照颜色对应。
代码实现
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range);
for (int i = 0; i < range; i++)
{
count[a[i] - min]++;
}
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
算法性能评价
时间复杂度 计数排序统计最大值和最小值时需要遍历一遍待排序数据,时间复杂度为O(N);接着需要遍历一遍统计数组,所以时间复杂度为O(统计数组大小)。因此计数排序的时间复杂度为
O
(
M
A
X
(
N
,
统计数组大小
)
)
O(MAX(N,统计数组大小))
O(MAX(N,统计数组大小))
空间复杂度 统计数组是额外开辟的空间,所以空间复杂度为
O
(
统计数组大小
)
O(统计数组大小)
O(统计数组大小)
稳定性 稳定。
|