经典排序算法
1.冒泡排序
- 依次将未排序的最大元素放到序列尾部,就像水中的泡泡付出水面那样
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
void bubble_sort(vector<int> &q) {
for(int i = q.size() - 1; i > 0; i -- ) {
bool flag = false; //本轮循环是否进行过交换操作,如果没有进行过,则说明已经有序,结束排序过程
for(int j = 0; j + 1 <= i; j ++ )
if(q[j] > q[j + 1]) {
swap(q[j], q[j + 1]);
flag = true;
}
}
}
//主函数(后续排序算法不再写出)
int main()
{
int n;
vector<int> q;
//处理输入
cin >> n;
for(int i = 0, t; i < n; i ++ ) {
cin >> t;
q.push_back(t);
}
//排序
bubble_sort(q);
//输出
for(auto x : q) cout << x << ' ';
cout << endl;
return 0;
}
2.选择排序
void selectionSort(vector<int>& q) {
for(int i = 0;i < q.size(); i ++ ) {
for(int j = i + 1; j < q.size(); j ++ ) {
if(q[i] > q[j])
swap(q[i], q[j]);//内层循环相当于从[i, end]中找到最小值并放置到就绪序列的下一个位置
}
}
}
3. 插入排序
void insertionSort(vector<int> &q) {
for(int i = 1; i < q.size(); i ++ ) {
int t = q[i];
for(int j = i - 1; j >= 0; j -- ) {//依次与已经有序的序列比较
if(q[j] > t)
q[j + 1] = q[j];
else break;
}
q[j + 1] = t;
}
}
4. 希尔排序
- 缩小增量排序
- 预先设定好一些增量,比如将增量设置成t,则将整个待排序序列分割为t个子序列分别进行直接插入排序
- 第一个突破n^2的排序算法
//省略
5. 归并排序(熟练掌握)
- 分治思想
- 很多其他算法思想的基础
- 可以用归并思想解决很多问题,比如“求数组逆序对的数目”
//自顶向下 或 自底向上
//链表 或 数组
//交叉有四种实现
void merge_sort(vector<int>& q, int l, int r) {
if(l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
static vector<int> w; //辅助数组,用来合并当前层的左右两部分; 开成静态空间可以提高效率
w.clear();
//两个指针分别从两部分从小到大比较遍历,归并
int i = l, j = mid + 1;
while(l <= mid && j <= r) {
if(q[i] <= q[j])
w.push_back(q[i ++ ]);
else w.push_back(q[j ++ ]);
}
//将最后剩下的尾部接上去
while(i <= mid) w.push_back(q[i ++ ]);
while(j <= r) w.push_back(q[j ++ ]);
for(int i = l, j = 0; j < w.size(); i ++, j ++ ) q[i] = w[j];//将归并完成的部分存到原数组的空间中
}
//求逆序对
int merge_sort(vector<int>& q, int l, int r) {
if(l >= r) return 0;
int res = 0; //当前区间的逆序对个数
int mid = l + r >> 1;
res += merge_sort(q, l, mid);
res += merge_sort(q, mid + 1, r);
static vector<int> w; //辅助数组,用来合并当前层的左右两部分; 开成静态空间可以提高效率
w.clear();
//两个指针分别从两部分从小到大比较遍历,归并
int i = l, j = mid + 1;
while(l <= mid && j <= r) {
if(q[i] <= q[j])
w.push_back(q[i ++ ]);
else {
w.push_back(q[j ++ ]);
res += mid - i + 1 //从i到mid的所有数都比q[j]大
}
}
//将最后剩下的尾部接上去
while(i <= mid) w.push_back(q[i ++ ]);
while(j <= r) w.push_back(q[j ++ ]);
for(int i = l, j = 0; j < w.size(); i ++, j ++ ) q[i] = w[j];//将归并完成的部分存到原数组的空间中
}
6. 快速排序(必须掌握)
- 分治思想:从数组中选择一个基准值,把其他数按照与基准值的大小关系分到基准值的左右两边
- 要想用快速排序实现稳定排序,只需要将单关键字编程双关键子,比如给每个元素附一个下标,将其变成pair
void quick_sort(vector<int>& q, int l, int r) {
if(l >= r) return;//只有一个数直接返回
int i = l - 1, j = r + 1, x= q[l + r >> 1]; //选择中点为基准值
while(i < j) {
//左右指针技巧
do i ++; while(q[i] < x);//从左边找一个小于x的数
do j ++; while(q[j] > x);//从右边找一个大于x的数
if(i < j) swap(q[i], q[j]);//i和j还没有相遇,分别指向两个不属于它所在位置的数,互相交换
else quick_sort(q, l, j), quick_sort(q, j + 1, r);//注意这儿有一个思维误区,取中间点为基准值并不是说该点一直会在中点,而是按照该基准值对依次对区间内的数做分拣
}
}
//基于链表的快速排序
7. 堆排序
- 堆是一棵完全二叉树,同时每个根节点的值都比左右儿子的值大/小
- 堆排序的思想,先在原数组的基础之上,将堆建出来,根节点就是当前最小值,将根节点取出来,然后将整个堆的最后一个元素插入根节点位置,再维护堆(堆化),每次堆化的时间复杂度是O(logn), 依次类推
- 堆存储时所有元素按照层级序列存在一个数组中, i的左儿子2i,右儿子2i + 1
- 堆的常用操作:
- push_down: 如果某个时刻除了根节点之外,所有节点都是满足堆的定义的,则通过一系列比较和交换将当前根节点移动到属于它的位置,使得整棵二叉树满足堆的定义; 每一层只会有一组操作,整棵树的层数是logn量级的,所以时间复杂度是O(logn);
- 类似地,有push_up操作,时间复杂度同样是O(logn)
void push_down(vector<int>& heap, int size, int u) {
int t = u, left = u * 2, right = u * 2 + 1;
if(left <= size && heap[left] > heap[t]) t = left;
if(right <= size && heap[right] > heap[t]) t = right;
if(t != u) {
swap(heap[u], heap[t]);
push_down(heap, size, t);
}
}
void push_up(vector<int>& heap, int size, int u) {
while(u /2 && heap[u / 2] < heap[u]) {
swap(heap[u / 2], heap[u]);
u /= 2;
}
}
//插入一个元素
void insert(vector<int>& heap, int size, int x) {
heap[ ++ size] = x;
push_up(heap, size, x);
}
//删除一个元素
void remove_top(vector<int>& heap, int& size) {
heap[1] = heap[size];
size -- ;
push_down(heap, size, 1);
}
/*改变中间某个节点的值,执行下面两句,顺序无关
//push_up(x);
push_down(x);*/
//建堆
void heap_sort(vector<int>& q, int size) {
for(int i = 1; i <= n; i ++ ) push_up(q, i);//建堆,时间复杂度为O(nlogn)
for(int i = 1; i <= n; i ++) {
swap(q[1], q[size]);
size -- ; //相当与将最大的数拿出来
push_down(q, size, 1);
}
}
int main() {
vector<int> q;
int n;
cin >> n;
q.resize(n + 1);
for(int i = 1; i <= n; i ++ ) cin >> q[i];
heap_sort(q, n);
for(int i = 0; i <= n; i++ ) cout << q[i] << ' '; cout << endl;
return 0;
}
8.计数排序
- 要求数据范围和数的个数差不多,能达到线性时间复杂度
- 应用该算法的题目的典型特征类似这样“给定10^7个数,每个数在0~9999999之间”
- 算法思想应用于后缀数组
void counting_sort(vector<int>& q, int n) {
vector<int> cnt(101, 0);
for(int i = 0; i <= n; i ++ ) cnt[q[i]] ++ ; //统计每个数出现了多少次
for(int i = 1, k = 1; i <= 100; i ++ ) //从前往后遍历
while(cnt[i]) {
q[k ++ ] = i;
cnt[i] --;
}
}
9. 桶排序
- 将元素均匀分到(按照某种方式映射)一定数量的桶中,在桶内部再做排序,桶和桶之间是有序的
//省略
10. 基数排序
- 在桶排序思想下按照数字的位来划分桶,就是基数排序
- 比sort慢很多,除法和取模大大增大了常数项
//假设所有的数都在三位以内
int get(int x, int i) {
while(i -- ) x /= 10;
return x%10;
}
void radix_sort(vector<int>& q, int n) {
vector<vector<int>> cnt(10);//定义桶
for(int i = 0; i < 10; i ++ ) { //依次按照
for(int j = 0; j < 10; j++ ) cnt[j].clear();
for(int j = 1; j <= n; j ++ )
cnt[get(q[j], i)].push_back(q[j]);
for(int j = 0, k = 1; j < 10; j ++ )
for(int x : cnt[j])
q[k ++ ] = x;
}
}