今日言:有朋自远方来,不亦乐乎!总有一天,我要让我正在学习的知识成为我的“朋”!
😋前言
今天介绍八大排序。
1. 排序的概念及其运用💬
1.1 排序的分类
2. 常见的7个比较排序💬
2.1 插入排序
2.1.1 直接插入排序
思路:
(以升序为例)将待排序的数据逐个插入到一个已经有序的序列当中,直到所有数据插入完成。我们认为第一个数是有序的,那么从第二个数开始往前插入。就像我们平时玩的扑克牌,我们一张一张地抓牌、理牌就是直接插入排序的思想。
图解:
总结:
- 对于直接插入排序而言,序列越接近有序,那么它的时间效率越高。
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性1:稳定
代码实现
// 直接插入排序
void InsertSort(int* a, int sz)
{
for (int i = 1; i < sz; ++i)
{
// 单趟排序
int tmp = a[i];
int end = i - 1;
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
}
}
2.1.2 希尔排序
思路:
(以升序为例)与直接插入排序类似,但不同的是:希尔排序先将序列分为n个小序列,然后分别将n个小序列以直接插入排序的方法排完序,再回头将这已经有序的n个小序列整合到一起。需要注意的是,小序列内的每两个数的间隔是gap,序列内数据并非左右紧密相连的。
图解:
总结:
- 希尔排序是直接插入排序的优化
- gap>1时的排序都是预排序,目的是让序列更加接近于有序。最后gap=1时的排序是真正让序列有序的。我们从直接插入排序的知识可以知道,当序列接近于有序的时候,其时间效率极高,因此希尔排序的性能很好。
- 时间复杂度:希尔排序的时间复杂度没有定论,因为对于gap的取值方法并没有最优解,这涉及数学问题。Shell的取法是gap=n/2,gap=gap/2,直到gap为1。后来Knuth提出取gap=gap/3+1,直到gap>1。还有很多取法,这些都可以,我们使用Knuth的方法,经过统计,它的时间复杂度从O(N^1.25)到O(1.6 *N^ 1.25)之间。大概可以记为O(N^1.3)。
- 稳定性:不稳定
代码实现:
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
// 单趟排序
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
}
2.2 选择排序
2.2.1 直接选择排序
思路:
(以升序为例)从一个序列中找出最小的和最大的那两个数,将其分别放在首位和末位,然后这两个数不再参与排序,在剩下的n-2个数中找出最小的和最大的那两个,将其放在首位和末位。以此类推…直到最终排序完成。(下图仅演示找最小的数)
图解:
总结:
- 效率不是很高,实际应用中不多。
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:不稳定
代码实现:
void Swap(int* i, int* j)
{
int tmp = *i;
*i = *j;
*j = tmp;
}
// 选择排序
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int min = left;
int max = right;
for (int i = left; i <= right; i++)
{
if (a[i] < a[min])
min = i;
if (a[i] > a[max])
max = i;
}
Swap(&a[min], &a[left]);
// 防止max在left的位置,被交换走了
if (max == left)
max = min;
Swap(&a[max], &a[right]);
left++;
right--;
}
}
2.2.2 堆排序
思路:
(以升序为例)充分利用堆的性质,建立大堆,就是选出最大的数,将其与最后一个数交换位置,就完成了一个数的排序。再重新建立大堆,以此类推…最终就可以得出一个升序序列。
图解:
总结:
-
效率较高
-
时间复杂度:O(N*log2N)
-
空间复杂度:O(1)
-
稳定性:不稳定
代码实现:
// 向下调整
void AdjustDown(HeapDataType* a, int size, int parent)
{
int child = parent * 2 + 1;// 左孩子
while (child < size)
{
// 判断左右孩子哪个大,大孩子是chlid
if (child + 1 < size && a[child] < a[child + 1])
{
++child;
}
// 判断大孩子和父亲哪个大
if (a[child] > a[parent])
{
Swap(a, child, parent);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
// 堆排序
void HeapSort(int* a, int n)
{
// 向下调整建大堆
for (int i = (n - 2) / 2; i > 0; --i)
AdjustDown(a, n, i);
// 堆排序
for (int i = n - 1; i > 0; --i)
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
}
}
2.3 交换排序
2.3.1 冒泡排序
思路:
(以升序为例)左右两个数比较交换,从头到尾,每遍历完一次后,就会将一个最大数放置在最后。重复遍历,即可完成排序。其特点是将数值较大的向序列的尾部移动,数值较小的向序列的前部移动。
图解:
总结:
- 易理解,但效率低
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
代码实现:
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j + 1] < a[j])
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
2.3.2 快速排序
思路:
(以升序为例)在序列中任意取一个值,将其作为基准值key,以key为中心分为左右两个序列,使key的左边全部比key小,key的右边全部比key大。再分别对左右两个序列同样操作。以此类推…直到最终排序完成。
图解:
快速排序有三种实现方法,分别是:
Hoare:
前后指针:
挖坑法:
总结:
- 快排存在一些问题,比如当我想要排升序,但序列是降序,此时快排的性能极低,因为每次取的key是序列的第一个,因为它本来是降序,key最后会被放在最后一个位置,也就是说这趟排序仅仅排好了key一个数的位置,它无法划分出左右两个子序列,因此效率很低。这种情况是可以有解决方案的,详细代码见下方代码实现区。
- 我们使用的是递归的方法,但是假若递归到一定程度(比如序列剩十几个数),此时若继续递归,将会很繁琐,尽管电脑程序自己运行,不用我们一步一步繁琐地走,但其会影响运行效率。因此,在序列剩余的数不多时,我们可以使用直接插入排序来代替剩下的递归。
- 快速排序的性能很好,使用场景也很多。
- 时间复杂度:O(N*log2N)
- 空间复杂度:O(log2N)
- 稳定性:不稳定
代码实现:
Hoare:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
// 一定要先走右边,后走左边,这样能保证key左侧小于key,右侧大于key
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi] && left < right)
right--;
while (a[left] <= a[keyi] && left < right)
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
前后指针:
// 快排前后指针
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
int keyi = left;
int prev = left;
int cur = left;
while (cur++ < right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
挖坑法:
void QuickSort3(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
// 第一个数存在临时变量中,形成一个坑位
int keyi = left;
int key = a[left];
int hole = left;
while (left < right)
{
// 右边找小
while (a[right] >= key && left < right)
right--;
a[hole] = a[right];
hole = right;
// 左边找大
while (a[left] <= key && left < right)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
keyi = hole;
QuickSort3(a, begin, keyi - 1);
QuickSort3(a, keyi + 1, end);
}
快排优化:
// 方案一:随机选keyi
int randi = left + (rand() % (right - left));
Swap(&a[randi], &a[left]);
// 三数取中
int GetMidi(int* a, int left, int right)
{
int mid = (right + left) / 2;
if (a[left] < a[mid])
{
if (a[left] > a[right])
return left;
else if (a[mid] < a[right])
return mid;
else
return right;
}
else // a[left] >= a[mid]
{
if (a[left] < a[right])
return left;
else if (a[mid] > a[right])
return mid;
else
return right;
}
}
// 方案二:三数取中
int Midi = GetMidi(a, left, right);
Swap(&a[Midi], &a[left]);
快排的非递归:
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int BEGIN = begin;
int END = end;
// Hoare
int keyi = begin;
while (begin < end)
{
while (a[end] >= a[keyi] && begin < end)
end--;
while (a[begin] <= a[keyi] && begin < end)
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
keyi = begin;
if (keyi + 1 < END)
{
STPush(&st, END);
STPush(&st, keyi + 1);
}
if (BEGIN < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, BEGIN);
}
}
STDestory(&st);
}
2.4 归并排序
基本思想:分治
2.4.1 归并排序
思路:
(以升序为例)我们设想一个方案:将一个序列的左右子序列分别排序至有序,然后再将两个子序列合并为一个总体有序的序列。这就是归并排序。其实就是将序列无限划分,例如8个数分为4+4,4又分为2+2,2再分为1+1,然后让让1和1合并成一个有序的2个数,再让2和2合并为一个有序的4个数,以此类推…就可以实现整个序列的排序。
图解:
总结:
- 缺点是需要O(N)的空间复杂度,它更多是用来解决在磁盘中的外排序问题。
- 时间复杂度:O(N*log2N)
- 空间复杂度:O(N)
- 稳定性:稳定
代码实现:
递归版本:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// 分割
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
_MergeSort(a, begin1, end1, tmp);
_MergeSort(a, begin2, end2, tmp);
// 合并
int j = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)
tmp[j++] = a[begin1++];
while (begin2 <= end2)
tmp[j++] = a[begin2++];
memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}
// 归并排序
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (NULL == tmp)
{
perror("Malloc fail!");
return;
}
_MergeSort(a, 0, n - 1, tmp);
}
非递归版本:
// 非递归归并
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (NULL == tmp)
{
perror("malloc fail!");
return;
}
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 (end1 >= n || begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)
tmp[j++] = a[begin1++];
while (begin2 <= end2)
tmp[j++] = a[begin2++];
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
}
3. 非比较排序💬
3.1 计数排序
思路:
统计每个数出现的次数,然后从该序列的最小数值到最大数值遍历,将出现次数不为0的数依次填入序列中。
图解:
代码实现:
// 计数排序
void CountSort(int* a, int n)
{
// 1. 统计每个数据出现的次数
int max = a[0], min = 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* countA = (int*)calloc(range, sizeof(int));
if (!countA)
{
perror("calloc fail\n");
return;
}
memset(countA, 0, sizeof(int) * range);
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
// 2. 排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
}
4. 排序总结💬
各大排序的时间复杂度、空间复杂度、稳定性对比:
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(nlog2n) ~ O(n2) | O(n1.3) | O(n2) | O(1) | 不稳定 |
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(nlog2n) | O(n2) | O(nlog2n) ~ O(n2) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
稳定性解释:所谓稳定,就是指两个数据,在排序前后它们之间的先后次序不变。 ↩︎