文章目录
介绍
更多算法模板见 github :https://github.com/surtr1/Algorithm/tree/main/BasicAlgorithm/Sort
持续更新中。。。
冒泡排序
原理
它的工作原理是每次检查相邻两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。
经过 i i i 次扫描后,数列的末尾 i i i 项必然是最大的 i i i 项,因此冒泡排序最多需要扫描 n − 1 n - 1 n−1 遍数组就能完成排序。
是一种稳定的排序算法,时间复杂度为 O ( n 2 ) O(n^2) O(n2) , 辅助空间复杂度: O ( 1 ) O(1) O(1)
cpp
template<class T>
void BubbleSort(vector<T> & a, int l, int r) {
int len = r - l;
for (int k = 1; k <= len ; k++) {
bool swaped = false;
for (int i = l + 1; i <= r; i++) {
if (a[i] < a[i - 1]) {
std::swap(a[i], a[i - 1]);
swaped = true;
}
}
if (!swaped) break;
}
}
希尔排序
原理
希尔排序(英语:Shell sort),也称为缩小增量排序法,是 插入排序 的一种改进版本。
过程:
- 排序对不相邻的记录进行比较和移动:将待排序序列分为若干子序列(每个子序列的元素在原始数组中间距相同);
- 对这些子序列进行插入排序;
- 减小每个子序列中元素之间的间距,重复上述过程直至间距减少为 1。
是一种不稳定的排序算法,平均时间复杂度为 O ( n 1.3 ) O(n^{1.3}) O(n1.3), 辅助空间复杂度: O ( 1 ) O(1) O(1)
插入排序的工作原理为将待排列元素划分为「已排序」和「未排序」两部分,每次从「未排序的」元素中选择一个插入到「已排序的」元素中的正确位置。
cpp
template<class T>
void ShellSort(vector<T> & a, int l, int r) {
int len = r - l + 1;
for (int gap = len >> 1; gap >= 1; gap >>= 1 ){
for (int i = l + gap; i <= r; i++) {
T key = a[i];
int j = i;
while (j >= l + gap && a[j - gap] > key) {
a[j] = a[j - gap];
j -= gap;
}
a[j] = key;
}
}
}
题目练习
题目链接:https://www.luogu.com.cn/problem/P1177
cpp
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
ShellSort(a, 1, n);
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << "\n";
}
堆排序
原理
堆排序(英语:Heapsort)是指利用 二叉堆 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。
过程
堆排序的本质是建立在堆上的选择排序。
排序
- 首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;
- 之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;
- 以此类推,在第 n - 1 次操作后,整个数组就完成了排序。
是一种不稳定的排序算法,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) , 辅助空间复杂度: O ( 1 ) O(1) O(1)
cpp
/*
基于 0 -idx 的堆排序
Parent(i) = (i - 1) / 2;
LeftChild(i) = 2 * i + 1;
RightChild(i) = 2 * i + 2;
*/
/*
start: 当前堆在数组中的起始绝对下标 (即 l)
end: 当前堆的结束绝对下标 (即 r 或 i-1)
parent: 当前要下沉的节点的绝对下标
*/
template<class T>
void sift_down(vector<T> & a, int l, int r, int parent) {
int child = l + 2 * (parent - l) + 1;
while (child <= r) {
//找最大的儿子
if (child + 1 <= r && a[child + 1] > a[child]) {
child++;
}
if (a[child] < a[parent]) return ;
std::swap(a[parent], a[child]);
parent = child;
child = l + 2 * (parent - l) + 1;
}
}
template<class T>
void HeapSort(vector<T> & a, int l, int r) {
// 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)
int sz = r - l + 1;
for (int i = sz / 2 - 1; i >= 0; i--) {
int parent = l + i; //偏移量 + 起点 就是真实值
sift_down(a, l, r, parent);
}
//先将第一个元素和已经排好的元素前一位做交换,
//再重新调整(刚调整的元素之前的元素),直到排序完毕
for (int i = r; i > l; i--) {
std::swap(a[i], a[l]);
sift_down(a, l, i - 1, l); //剩余区间 [l, i - 1]
}
}
题目练习
题目链接:https://www.luogu.com.cn/problem/P1177
cpp
//https://www.luogu.com.cn/problem/P1177
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
HeapSort(a, 1, n);
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << "\n";
}
归并排序
原理
归并排序基于分治思想将数组分段排序后合并,时间复杂度在最优、最坏与平均情况下均为 O ( n l o g n ) O(n logn) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)。
归并排序可以只使用 O ( 1 ) O(1) O(1) 的辅助空间,但为便捷通常使用与原数组等长的辅助数组。
最重要的步骤 合并
归并排序最核心的部分是合并(merge)过程:将两个有序的数组 a [ i ] a[i] a[i] 和 b [ j ] b[j] b[j] 合并为一个有序数组 c [ k ] c[k] c[k]。
从左往右枚举 a [ i ] a[i] a[i] 和 b [ j ] b[j] b[j],找出最小的值并放入数组 c [ k ] c[k] c[k];重复上述过程直到 a [ i ] a[i] a[i] 和 b [ j ] b[j] b[j] 有一个为空时,将另一个数组剩下的元素放入 c [ k ] c[k] c[k]。
为保证排序的稳定性,前段首元素小于或等于后段首元素时( a [ i ] < = b [ j ] a[i] <= b[j] a[i]<=b[j])而非小于时( a [ i ] < b [ j ] a[i] < b[j] a[i]<b[j])就要作为最小值放入 c [ k ] c[k] c[k]。
分治法实现归并排序
-
当数组长度为 1 时,该数组就已经是有序的,不用再分解。
-
当数组长度大于 1 时,该数组很可能不是有序的。此时将该数组分为两段,再分别检查两个数组是否有序(用第 1 条)。如果有序,则将它们合并为一个有序数组;否则对不有序的数组重复第 2 条,再合并。
用数学归纳法可以证明该流程可以将一个数组转变为有序数组。
cpp
template<class T>
void _MergeSort(vector<T>& a, int l, int r, vector<T>& tmp) {
if (l >= r) return ;
int mid = l + ((r - l) >> 1);
_MergeSort(a, l, mid, tmp), _MergeSort(a, mid + 1, r, tmp);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (i = l; i <= r; i++) {
a[i] = tmp[i];
}
}
template<class T>
void MergeSort(vector<T>& a, int l, int r) {
if(l < r && !a.empty()) {
vector<T> tmp(a.size());
_MergeSort(a, l, r, tmp);
}
}
题目练习
题目链接:https://www.luogu.com.cn/problem/P1177
cpp
//https://www.luogu.com.cn/problem/P1177
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
MergeSort(a, 1, n);
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << "\n";
}
快速排序
原理
快速排序的工作原理是通过 分治 的方式来将一个数组排序。
快速排序分为三个过程:
- 将数列划分为两部分(要求保证相对大小关系);
- 递归到两个子序列中分别进行快速排序;
- 不用合并,因为此时数列已经完全有序。
和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数 𝑚
m 来当做两个子数列的分界。
之后,维护一前一后两个指针 i 和 j,依次考虑当前的数是否放在了应该放的位置,i 从前往后找到第一个大于等于 m的数,j 从后往前找到第一个小于等于 m 的数,那么可以交换 i 和 j 位置上的数,再移动指针继续处理,直到两个指针相遇。
快速排序的最优时间复杂度和平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
cpp
template<class T>
void QuickSort(vector<T>& a, int low, int high) {
if (low >= high) return;
static std::mt19937 gen(std::random_device{}());
std::uniform_int_distribution<int> dis(low, high);
T pivot = a[dis(gen)];
//双指针划分
int i = low, j = high;
while (i <= j) {
while (a[i] < pivot) i++;
while (a[j] > pivot) j--;
if (i <= j) {
std::swap(a[i], a[j]);
i++;
j--;
}
}
QuickSort(a, low, j);
QuickSort(a, i, high);
}
template<class T>
void QuickSort(vector<T>& a) {
if(!a.empty()) {
QuickSort(a, 0, (int)a.size() - 1);
}
}
题目练习
题目链接:https://www.luogu.com.cn/problem/P1177
cpp
//https://www.luogu.com.cn/problem/P1177
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
QuickSort(a, 1, n);
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << "\n";
}