FIRST:
是我们最方便的快速排序,使用时sort即可,快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称快排,是一种被广泛运用的排序算法,
快速排序的最优时间复杂度和平均时间复杂度为 O(n log n),最坏时间复杂度为 O(n²)。
//快排
//sort();
struct Range {
int start, end;
Range(int s = 0, int e = 0) { start = s, end = e; }
};
template
void quick_sort(T arr[], const int len) {
if (len <= 0) return;
Range r[len];
int p = 0;
r[p++] = Range(0, len - 1);
while (p) {
Range range = r[--p];
if (range.start >= range.end) continue;
T mid = arr[range.end];
int left = range.start, right = range.end - 1;
while (left < right) {
while (arr[left] < mid && left < right) left++;
while (arr[right] >= mid && left < right) right--;
std::swap(arr[left], arr[right]);
}
if (arr[left] >= arr[range.end])
std::swap(arr[left], arr[range.end]);
else
left++;
r[p++] = Range(range.start, left - 1);
r[p++] = Range(left + 1, range.end);
}
}
SECOND:
选择排序(英语:Selection sort)是一种简单直观的排序算法。它的工作原理是每次找出第 小的元素(也就是 中最小的元素),然后将这个元素与数组第 个位置上的元素交换。
具有非凡的稳定性。
选择排序的最优时间复杂度、平均时间复杂度和最坏时间复杂度均为 O(n²)。
//选择排序
void selection_sort(int* a, int n) {
for (int i = 1; i < n; ++i) {
int ith = i;
for (int j = i + 1; j <= n; ++j) {
if (a[j] < a[ith]) {
ith = j;
}
}
std::swap(a[i], a[ith]);
}
}
THIRD:
冒泡排序(英语:Bubble sort)是一种简单的排序算法。由于在算法的执行过程中,较小的元素像是气泡般慢慢「浮」到数列的顶端,故叫做冒泡排序。
它的工作原理是每次检查相邻两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。
经过 次扫描后,数列的末尾 项必然是最大的 i项,因此冒泡排序最多需要扫描 i遍数组就能完成排序。
在序列完全有序时,冒泡排序只需遍历一遍数组,不用执行任何交换操作,时间复杂度为O(n) 。
在最坏情况下,冒泡排序要执行(n(n-1))/2 次交换操作,时间复杂度为 O(n²)。
冒泡排序的平均时间复杂度为O(n²)。
//冒泡排序
void bubble_sort(int *a, int n) {
bool flag = true;
while (flag) {
flag = false;
for (int i = 1; i < n; ++i) {
if (a[i] > a[i + 1]) {
flag = true;
int t = a[i];
a[i] = a[i + 1];
a[i + 1] = t;
}
}
}
}
FORTH:
插入排序(英语:Insertion sort)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为“已排序”和“未排序”两部分,每次从“未排序的”元素中选择一个插入到“已排序的”元素中的正确位置。一个与插入排序相同的操作是打扑克牌时,从牌桌上抓一张牌,按牌面大小插到手牌后,再抓下一张牌。
同时也具有稳定性,
插入排序的最优时间复杂度为 O(n),在数列几乎有序时效率很高。插入排序的最坏时间复杂度和平均时间复杂度都为O(n²) 。
//插入排序
void insertion_sort(int* a, int n) {
// 对 a[1],a[2],...,a[n] 进行插入排序
for (int i = 2; i <= n; ++i) {
int key = a[i];
int j = i - 1;
while (j > 0 && a[j] > key) {
a[j + 1] = a[j];
--j;
}
a[j + 1] = key;
}
}
FIFTH:
计数排序(英语:Counting sort)是一种线性时间的排序算法。
计数排序的工作原理是使用一个额外的数组 C,其中第 i个元素是待排序数组 A中值等于i 的元素的个数,然后根据数组 C 来将 A 中的元素排到正确的位置。
它的工作过程分为三个步骤:
计算每个数出现了几次;
求出每个数出现次数的 前缀和;
利用出现次数的前缀和,从右至左计算每个数的排名。
计数排序的时间复杂度为O(n+w) ,其中 w 代表待排序数据的值域大小。
//计数排序
const int N = 100010;
const int W = 100010;
int n, w, a[N], cnt[W], b[N];
void counting_sort() {
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; ++i) ++cnt[a[i]];
for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}
SIXTH:基数排序(英语:Radix sort)是一种非比较型的排序算法,最早用于解决卡片排序的问题。
它的工作原理是将待排序的元素拆分为 k 个关键字(比较两个元素时,先比较第一关键字,如果相同再比较第二关键字……),然后先对第 k 关键字进行稳定排序,再对第 k-1关键字进行稳定排序,再对第 k-2 关键字进行稳定排序……最后对第一关键字进行稳定排序,这样就完成了对整个待排序序列的稳定排序。 一般来说,如果每个关键字的值域都不大,就可以使用 计数排序 作为内层排序,此时的复杂度为 ,其中w[i] 为第 i 关键字的值域大小。如果关键字值域很大,就可以直接使用基于比较的 排序而无需使用基数排序了。 基数排序的空间复杂度为O(k+n) 。
//基数排序
const int N = 100010;
const int W = 100010;
const int K = 100;
int n, w[K], k, cnt[W];
struct Element {
int key[K];
bool operator<(const Element& y) const {
// 两个元素的比较流程
for (int i = 1; i <= k; ++i) {
if (key[i] == y.key[i]) continue;
return key[i] < y.key[i];
}
return false;
}
} a[N], b[N];
void counting_sort(int p) {
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; ++i) ++cnt[a[i].key[p]];
for (int i = 1; i <= w[p]; ++i) cnt[i] += cnt[i - 1];
// 为保证排序的稳定性,此处循环i应从n到1
// 即当两元素关键字的值相同时,原先排在后面的元素在排序后仍应排在后面
for (int i = n; i >= 1; --i) b[cnt[a[i].key[p]]--] = a[i];
memcpy(a, b, sizeof(a));
}
void radix_sort() {
for (int i = k; i >= 1; --i) {
// 借助计数排序完成对关键字的排序
counting_sort(i);
}
}
SEVENTH:
归并排序(merge sort)是高效的基于比较的稳定排序算法。
归并排序基于分治思想将数组分段排序后合并,时间复杂度在最优、最坏与平均情况下均为 O(n log n),空间复杂度为 O(n)。
归并排序可以只使用 O(1) 的辅助空间,但为便捷通常使用与原数组等长的辅助数组
由于已分段排序,各非空段的首元素的最小值即是数组的最小值,不断从数组中取出当前最小值至辅助数组即可使其有序,最后将其从辅助数组复制至原数组。
为保证排序的正确性,应注意从数组中取出当前最小值可能导致非空段变为空,后段为空时(j == r)前段的首元素是当前最小值,否则在前段非空时(i < mid)比较前后段的首元素。
为保证排序的稳定性,前段首元素小于或等于后段首元素时(a[i] <= a[j])而非小于时(a[i] < a[j])就要作为最小值。
为保证排序的复杂度,通常将数组分为尽量等长的两段(mid=(l+r)/2)。
//归并排序
void merge(int l, int r) {
if (r - l <= 1) return;
int mid = l + ((r - l) >> 1);
merge(l, mid), merge(mid, r);
for (int i = l, j = mid, k = l; k < r; ++k) {
if (j == r || (i < mid && a[i] <= a[j])) tmp[k] = a[i++];
else tmp[k] = a[j++];
}
for (int i = l; i < r; ++i) a[i] = tmp[i];
}
EIGHTH:
内省排序(英语:Introsort 或 Introspective sort)是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为O(n log n) 。
内省排序将快速排序的最大递归深度限制为 log² n,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为 O(n²)。
从 2000 年 6 月起,SGI C++ STL 的 stl_algo.h 中 sort() 函数的实现采用了内省排序算法。
//内省排序
// 模板的 T 参数表示元素的类型,此类型需要定义小于(<)运算
template
// arr 为查找范围数组,rk 为需要查找的排名(从 0 开始),len 为数组长度
T find_kth_element(T arr[], int rk, const int len) {
if (len <= 1) return arr[0];
// 随机选择基准(pivot)
const T pivot = arr[rand() % len];
// i:当前操作的元素
// j:第一个等于 pivot 的元素
// k:第一个大于 pivot 的元素
int i = 0, j = 0, k = len;
// 完成一趟三路快排,将序列分为:
// 小于 pivot 的元素 | 等于 pivot 的元素 | 大于 pivot 的元素
while (i < k) {
if (arr[i] < pivot)
swap(arr[i++], arr[j++]);
else if (pivot < arr[i])
swap(arr[i], arr[--k]);
else
i++;
}
// 根据要找的排名与两条分界线的位置,去不同的区间递归查找第 k 大的数
// 如果小于 pivot 的元素个数比k多,则第 k 大的元素一定是一个小于 pivot 的元素
if (rk < j) return find_kth_element(arr, rk, j);
// 否则,如果小于 pivot 和等于 pivot 的元素加起来也没有 k 多,
// 则第 k 大的元素一定是一个大于 pivot 的元素
else if (rk >= k)
return find_kth_element(arr + k, rk - k, len - k);
// 否则,pivot 就是第 k 大的元素
return pivot;
}
NINETH:
三路快速排序(英语:3-way Radix Quicksort)是快速排序和 基数排序 的混合。它的算法思想基于 荷兰国旗问题 的解法。
与原始的快速排序不同,三路快速排序在随机选取分界点 m 后,将待排数列划分为三个部分:小于m 、等于m 以及大于m 。这样做即实现了将与分界元素相等的元素聚集在分界元素周围这一效果。
三路快速排序在处理含有多个重复值的数组时,效率远高于原始快速排序。其最佳时间复杂度为O(n) 。
三路快速排序实现起来非常简单,下面给出了一种三路快排的 C++ 实现。
//三路快速排序
// 模板的T参数表示元素的类型,此类型需要定义小于(<)运算
template
// arr 为需要被排序的数组,len 为数组长度
void quick_sort(T arr[], const int len) {
if (len <= 1) return;
// 随机选择基准(pivot)
const T pivot = arr[rand() % len];
// i:当前操作的元素
// j:第一个等于 pivot 的元素
// k:第一个大于 pivot 的元素
int i = 0, j = 0, k = len;
// 完成一趟三路快排,将序列分为:
// 小于 pivot 的元素| 等于 pivot 的元素 | 大于 pivot 的元素
while (i < k) {
if (arr[i] < pivot)
swap(arr[i++], arr[j++]);
else if (pivot < arr[i])
swap(arr[i], arr[--k]);
else
i++;
}
// 递归完成对于两个子序列的快速排序
quick_sort(arr, j);
quick_sort(arr + k, len - k);
}
TENTH:
堆排序(英语:Heapsort)是指利用 二叉堆 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。本质是建立在堆上的选择排序。
首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;
之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;
以此类推,在第 n-1 次操作后,整个数组就完成了排序。
堆排序的最优时间复杂度、平均时间复杂度、最坏时间复杂度均为O(n log n) 。
由于可以在输入数组上建立堆,所以这是一个原地算法。
// 堆排序
void sift_down(int arr[], int start, int end) {
// 计算父结点和子结点的下标
int parent = start;
int child = parent * 2 + 1;
while (child <= end) { // 子结点下标在范围内才做比较
// 先比较两个子结点大小,选择最大的
if (child + 1 <= end && arr[child] < arr[child + 1]) child++;
// 如果父结点比子结点大,代表调整完毕,直接跳出函数
if (arr[parent] >= arr[child])
return;
else { // 否则交换父子内容,子结点再和孙结点比较
swap(arr[parent], arr[child]);
parent = child;
child = parent * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
// 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)
for (int i = (len - 1 - 1) / 2; i >= 0; i--) sift_down(arr, i, len - 1);
// 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
sift_down(arr, 0, i - 1);
}
}
ELEVENTH:
桶排序(英文:Bucket sort)是排序算法的一种,适用于待排序数据值域较大但分布比较均匀的情况。
桶排序按下列步骤进行:
设置一个定量的数组当作空桶;遍历序列,并将元素一个个放到对应的桶中;对每个不是空的桶进行排序;从不是空的桶里把元素再放回原来的序列中。
桶排序的平均时间复杂度为 O(n+n²/k+k)(将值域平均分成 n块 + 排序 + 重新合并元素),当k约等于n 时为 O(n)。1
桶排序的最坏时间复杂度为O(n²) 。
//桶排序
const int N = 100010;
int n, w, a[N];
vector
void insertion_sort(vector
for (int i = 1; i < A.size(); ++i) {
int key = A[i];
int j = i - 1;
while (j >= 0 && A[j] > key) {
A[j + 1] = A[j];
--j;
}
A[j + 1] = key;
}
}
void bucket_sort() {
int bucket_size = w / n + 1;
for (int i = 0; i < n; ++i) {
bucket[i].clear();
}
for (int i = 1; i <= n; ++i) {
bucket[a[i] / bucket_size].push_back(a[i]);
}
int p = 0;
for (int i = 0; i < n; ++i) {
insertion_sort(bucket[i]);
for (int j = 0; j < bucket[i].size(); ++j) {
a[++p] = bucket[i][j];
}
}
}
TVELVETH:
希尔排序(英语:Shell sort),也称为缩小增量排序法,是 插入排序 的一种改进版本。希尔排序以它的发明者希尔(英语:Donald Shell)命名。
排序对不相邻的记录进行比较和移动:将待排序序列分为若干子序列(每个子序列的元素在原始数组中间距相同);对这些子序列进行插入排序;减小每个子序列中元素之间的间距,重复上述过程直至间距减少为 1。
!!!希尔排序不是一个稳定的排序!!!
希尔排序的最优时间复杂度为O(n) 。
希尔排序的平均时间复杂度和最坏时间复杂度与间距序列的选取(就是间距如何减小到 1)有关,比如「间距每次除以 3」的希尔排序的时间复杂度是 。已知最好的最坏时间复杂度为 。希尔排序的空间复杂度为 。
//希尔排序
template
void shell_sort(T array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
std::swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
THIRTEENTH:
锦标赛排序(英文:Tournament sort),又被称为树形选择排序,是 选择排序 的优化版本,堆排序 的一种变体(均采用完全二叉树)。它在选择排序的基础上使用优先队列查找下一个该选择的元素。
该算法的名字来源于单败淘汰制的竞赛形式。在这种赛制中有许多选手参与比赛,他们两两比较,胜者进入下一轮比赛。这种淘汰方式能够决定最好的选手,但是在最后一轮比赛中被淘汰的选手不一定是第二好的——他可能不如先前被淘汰的选手。
以 最小锦标赛排序树 为例:
待排序元素是叶子节点显示的元素。红色边显示的是每一轮比较中较小的元素的胜出路径。显然,完成一次"锦标赛"可以选出一组元素中最小的那一个。
每一轮对 n 个元素进行比较后可以得到 n/2 个“优胜者”,每一对中较小的元素进入下一轮比较。如果无法凑齐一对元素,那么这个元素直接进入下一轮的比较。
完成一次“锦标赛”后需要将被选出的元素去除。直接将其设置为 无限(这个操作类似 堆排序),然后再次举行“锦标赛”选出次小的元素。
之后一直重复这个操作,直至所有元素有序。
锦标赛排序的最优时间复杂度、平均时间复杂度和最坏时间复杂度均为O(n log n) 。它用 O(n)的时间初始化“锦标赛”,然后用 O(log n) 的时间从 n个元素中选取一个元素。
锦标赛排序的空间复杂度为O(n) 。
//锦标赛排序
int n, a[maxn], tmp[maxn << 1];
int winner(int pos1, int pos2) {
int u = pos1 >= n ? pos1 : tmp[pos1];
int v = pos2 >= n ? pos2 : tmp[pos2];
if (tmp[u] <= tmp[v]) return u;
return v;
}
void creat_tree(int &value) {
for (int i = 0; i < n; i++) tmp[n + i] = a[i];
for (int i = 2 * n - 1; i > 1; i -= 2) {
int k = i / 2;
int j = i - 1;
tmp[k] = winner(i, j);
}
value = tmp[tmp[1]];
tmp[tmp[1]] = INF;
}
void recreat(int &value) {
int i = tmp[1];
while (i > 1) {
int j, k = i / 2;
if (i % 2 == 0 && i < 2 * n - 1)
j = i + 1;
else
j = i - 1;
tmp[k] = winner(i, j);
i = k;
}
value = tmp[tmp[1]];
tmp[tmp[1]] = INF;
}
void tournament_sort() {
int value;
creat_tree(value);
for (int i = 0; i < n; i++) {
a[i] = value;
recreat(value);
}
}
FOURTEENTH:
//排序作用
理解数据特点
降低时间复杂度
作为查找的预处理