三种基础排序算法及其拓展应用
排序是在算法竞赛中经常用到的操作,排序的算法有很多,大多数人的入门算法大多都是冒泡排序,插入排序等 O(n2) 的算法,当数据量比较大时,这个复杂度是不能容忍的。我们在竞赛中用到的最多的三种排序算法,分别是归并排序, 快速排序和堆排序,它们的复杂度都是 O(nlogn) ,这是基于交换的排序算法所能达到的复杂度下限。
一、归并排序
归并排序是《算法导论》中分治法的入门算法。
分治法,就是把大问题分解成小问题,解决小问题,然后再通过小问题的解得出大问题的解
归并排序:
分解:分解待排序的n个元素的序列各成具n/2个元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案
我们先来解释如何合并两个已经排好序的子序列:
因为两个子已经有序,所以最小值一定是这两个序列的第一个元素中较小的那一个。一直这样取就可以了。当其中一个序列已经取空,则直接依次取另一个序列的值。
再来看如何分解并解决子问题:
举个栗子:
假设a[]有16个元素,要a[0…15]将其排序,就需要先将a[0…7]和a[8…15]排序,然后合并a[0…7]和a[8…15]。
其中,要将a[0…7]排序,就需要将a[0…3]和a[4…7]排序,然后合并a[0…3]和a[4…7]。
其中,要将a[0…3]排序,就需要将a[0…1]和a[2…3]排序,然后合并a[0…1]和a[2…3]。
其中,要将a[0…1]排序,就需要将a[0]和a[1]排序已经排好啦,然后合并a[0]和a[1]。
最底层解决了,就可以一步一步向上合并了!
代码:
#include <iostream>
#define MAXN 100000
using namespace std;
int n, a[MAXN], tmp[MAXN];
void merge(int l, int m, int r) {
int k = l, i = l, j = m+1;
while (i <= m && j <= r) {
if (a[i] < a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while (i <= m) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (i = l; i <= r; i++) a[i] = tmp[i];
}
void mergeSort(int l, int r) {
if (l < r) {
int m = (l + r) / 2; //分解成两个子问题
mergeSort(l, m); //解决子问题
mergeSort(m+1, r);
merge(l, m, r); //合并
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
mergeSort(0, n-1);
for (int i = 0; i < n; i++)
cout << a[i] << " ";
cout << endl;
return 0;
}
应用:1. 逆序对
冒泡排序每次只能交换相邻的两个数字的位置,求用冒泡排序算法排序这个序列所需要的交换次数。
比如 9 1 0 5 4, 排序后为 1 0 4 5 9, 用冒泡排序最少需要交换6次。
限定数据范围 n <= 500000 , 即直接用冒泡排序 O(n2) 模拟是超时的。
思路:
可以分而治之,把长度为n的序列分成两个长度为n/2的子序列,分别求出这两个子序列内部需要的交换次数,再加上合并这两个子序列需要的交换次数。
代码:
POJ-2299 Ultra-QuickSort 逆序对,冒泡排序的交换次数
#include <iostream>
#include <cstring>
#define MAXN 500010
using namespace std;
long long n, a[MAXN], tmp[MAXN];
long long ans = 0;
void merge(int l, int m, int r) {
int k = l, i = l, j = m+1;
while (i <= m && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else {
ans += m - i + 1; // 添加一行,逆序对计数
tmp[k++] = a[j++];
}
}
while (i <= m) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (int i = l; i <= r; i++) a[i] = tmp[i];
}
void mergeSort(int l, int r) {
if (l < r) {
int m = (l + r) / 2;
mergeSort(l, m);
mergeSort(m+1, r);
merge(l, m, r);
}
}
int main() {
while (cin >> n) {
if (n == 0) break;
ans = 0;
memset(a, 0, sizeof(a));
memset(tmp, 0, sizeof(tmp));
for (int i = 0; i < n; i++)
cin >> a[i];
mergeSort(0, n-1);
cout << ans << endl;
}
return 0;
}
二、快速排序
快速排序用的也是分治法。
快速排序:
分解:数组 a[l…r] 被划分为两个(可能为空)字数组 a[l…p-1] 和 a[p+1…r] ,使得 a[l…p-1] 中的每一个元素都小于等于 a[p] ,而a[p]也小于等于 a[p+1…r] 中的每一个元素。其中,计算下标p也是划分过程的一部分。解决:通过递归调用快速排序,对子数组 a[l...p-1] 和 a[p+1...r] 进行排序。合并:因为子数组都是原址排序的,所以不需要合并操作:数组 a[l...r]$ 已经有序。
具体解释和证明参考《算法导论》
代码:
#include <iostream>
#include <cstdlib>
#define MAXN 500000
using namespace std;
int n, a[MAXN];
void swap(int &a, int &b) {
int t = a;
a = b;
b = t;
}
int partition(int l, int r) {
int x = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] <= x) {
i++;
swap(a[i], a[j]);
}
}
swap(a[i+1], a[r]);
return i+1;
}
int random_partition(int l, int r) {
int i = rand() % (r - l) + l;
swap(a[i], a[r]);
return partition(l, r);
}
void quickSort(int l, int r) {
if (l < r) {
int p = random_partition(l, r);
quickSort(l, p-1);
quickSort(p+1, r);
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
quickSort(0, n-1);
for (int i = 0; i < n; i++)
cout << a[i] << " ";
cout << endl;
return 0;
}
1. 第k大数字
代码:
#include <iostream>
#include <cstdlib>
#define MAXN 1000000
using namespace std;
int n, a[MAXN];
void swap(int &a, int &b) {
int t = a; a = b; b = t;
}
int partition(int l, int r) {
int x = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] < x) {
i++;
swap(a[i], a[j]);
}
}
swap(a[i+1], a[r]);
return i+1;
}
int random_partition(int l, int r) {
int i = rand() % (l - r) + l;
swap(a[i], a[r]);
return partition(l, r);
}
int random_select(int l, int r, int k) {
if (l == r)
return a[l];
int p = random_partition(l, r);
int t = p - l + 1;
if (t == k)
return a[p];
else if (k < t) return random_select(l, p-1, k);
else return random_select(p+1, r, k - t);
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
int k;
cin >> k;
cout << random_select(0, n-1, k) << endl;
}
三、堆排序
代码:
/*
ID: zachery1
PROG: sort3
LANG: C++
*/
#include <iostream>
#include <fstream>
#include <cstring>
#define MAXN 1010
#define LEFT(x) (x<<1)
#define RIGHT(x) ((x<<1)+1)
#define PARENT(x) (x>>1)
using namespace std;
int N;
int heap[MAXN];
int heapsize;
void swap(int &a, int &b) {
int t;
t = a; a = b; b = t;
}
void maxHeapify(int i) {
int l = LEFT(i);
int r = RIGHT(i);
int largest = i;
if (l <= heapsize && heap[l] > heap[largest])
largest = l;
if (r <= heapsize && heap[r] > heap[largest])
largest = r;
if (largest != i) {
swap(heap[i], heap[largest]);
maxHeapify(largest);
}
}
void buildHeap() {
for (int i = PARENT(heapsize); i > 0; i--) {
maxHeapify(i);
}
}
void heapSort() {
heapsize = N;
buildHeap();
for (int i = N; i > 0; i--) {
swap(heap[1], heap[i]);
heapsize--;
maxHeapify(1);
}
}
int main() {
cin >> N;
memset(heap, 0, sizeof(heap));
for (int i = 1; i <= N; i++) {
cin >> heap[i];
}
heapSort();
for (int i = 1; i <= N; i++)
cout << i << " "<< heap[i].v << " " << heap[i].idx << endl;
cout << ans << endl;
}
1. top-N
heapSort()的过程,只排序前N个就好了。
C++ STL 的实现: priority_queue(), 即优先队列。
算法可视化:http://www.cs.usfca.edu/~galles/visualization/Algorithms.html
待整理 排序的交换次数
POJ-2299 Ultra-QuickSort 逆序对 冒泡排序的交换次数
POJ-1674(n的一个全排列中要变成顺序需要几次交换)
USACO-Section2.1 Sorting A Three-Valued Sequence 含有重复元素的交换 简化版