目录
2 简单选择排序(Simple Selection Sort)
3 直接插入排序(Straight Insertion Sort)
4 希尔排序(Shell Sort)/缩小增量排序(Diminishing Increment Sort)
相关概念
- 稳定排序 & 非~
- 原地排序 & 非~
- 内排序 & 外排序
- 时间复杂度 & 空间复杂度
- 插入排序类:直接插入、希尔
- 选择排序类:简单选择、堆
- 交换排序类:冒泡、快速
图片来自菜鸟教程
1 冒泡排序 (Bubble Sort)
重复扫描元素列,依次比较相邻元素,顺序错误则交换,直到无需交换(排序完成)——把小(大)的元素往前(后)调
时间复杂度:
- 最好:
- 最坏 & 平均:
空间复杂度:
- 最坏:
稳定
代码实现
优化:增加标记变量flag,避免已经有序情况下的无意义循环判断
// C++
template<typename T>
void bubble_sort(T arr[], int len) {
int i, j; T temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
2 简单选择排序(Simple Selection Sort)
将元素列分为未排序序列、已排序序列:
在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
时间复杂度:
空间复杂度:
- 最坏:
不稳定
代码实现
通过 n-i 次关键字间的比较,从 n-i+1 个记录中选出关键字最小的记录,并和第 i(1≤i≤n)个记录交换
template<typename T>
void selection_sort(std::vector<T>& arr) {
for (int i = 0; i < arr.size() - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.size(); j++) {
if (arr[j] < arr[min])
min = j;
}
if (i != min)
std::swap(arr[i], arr[min]);
}
}
3 直接插入排序(Straight Insertion Sort)
将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表
抓扑克牌:手上已经抓到的扑克牌是按顺序排好的,待摸的是洗好的,即未排序的。
时间复杂度:
- 最好:
- 最坏 & 平均:
空间复杂度:
- 最坏:
稳定
代码实现
void insertion_sort(int arr[],int len) {
for(int i = 1; i < len; i++) {
int key = arr[i];
int j = i - 1;
while((j >= 0) && (key < arr[j])) { // 找到正确插入位置
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
4 希尔排序(Shell Sort)/缩小增量排序(Diminishing Increment Sort)
跳跃分割,将整个待排序元素序列分割成为若干子序列,分别进行直接插入排序;当整个序列都基本有序时,再对全体记录进行依次直接插入排序。
时间复杂度:
- 最好:
- 最坏 & 平均:
空间复杂度:
- 最坏:
不稳定
代码实现
template <typename T>
void insert_sort(T st, T ed, int delta) {
for(T i = st + delta; i < ed; i += delta) {
for(T j = i; j > st; j -= delta)
if(*j < *(j - delta)) std::swap(*j, *(j - delta));
else break;
}
}
template <typename T>
void shell_sort(T st, T ed) {
for(int delta = ed - st; delta; delta /= 2)
for(int i = 0; i < delta; i++)
insert_sort(st + i, ed, delta);
}
5 归并排序(Merging Sort )
分治思想:先使每个子序列有序,再使子序列段间有序。
or 归并思想:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 |n/2| 个长度为2或1的有序子序列;再两两归并……直至得到一个长度为n的有序序列为止(2路归并排序)
两种实现方法:
- 自上而下递归
- 自下而上迭代(所有递归都可以用迭代重写)
时间复杂度:
空间复杂度:
- 最坏:
稳定
6 快速排序(Quick Sort)
在冒泡排序基础上的递归分治法。
设定分界值(基准),按相对于分界值大小将元素列分为两部分;将这两部分分别重复上述操作。
时间复杂度:
- 最好 & 平均:
- 最坏:
空间复杂度:
- 最坏:
不稳定
代码实现
递归
优化:《大话数据结构》P645
- 基准选取:三数取中、九数取中法
- 优化不必要交换
- 优化小数组时的排序方案
- 优化递归
// 严蔚敏《数据结构》
using namespace std;
void Qsort(int arr[], int low, int high){
if (high <= low) return;
int i = low;
int j = high;
int key = arr[low];
while (true)
{
while (arr[i] <= key)
{
i++;
if (i == high){
break;
}
}
while (arr[j] >= key)
{
j--;
if (j == low){
break;
}
}
if (i >= j) break;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
arr[low] = arr[j];
arr[j] = key;
Qsort(arr, low, j - 1);
Qsort(arr, j + 1, high);
}
7 堆排序(Heap Sort )
将待排序的序列构造成一个堆,此时整个序列最值是堆顶的根节点,移走(将其与堆数组的末尾元素交换)后将剩余 n-1 个序列重新构造成堆……反复执行以得到有序序列
堆:完全二叉树、某个结点的值总是不大于或不小于其父结点的值。
堆操作:
- Heapify:将堆的末端子节点作调整,使得子节点永远小于(大于)父节点
- 堆排序:
- 升序:大顶堆
- 降序:小顶堆
时间复杂度:
空间复杂度:
- 最坏:
不稳定
不适合待排序序列个数少的情况
#include <algorithm>
using namespace std;
void max_heapify(int arr[], int start, int end)
{
int dad = start; // 建立父节点指标和子节点指标
int son = dad * 2 + 1;
while (son <= end) // 若子节点指标在范围内才做比较
{
if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比较两个子节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) // 如果父节点大於子节点代表调整完毕,直接跳出函数
return;
else // 否则交换父子内容再继续子节点和孙节点比较
{
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len)
{
// 初始化,i从最后一个父节点开始调整
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
// 先将第一个元素和已经排好的元素前一位做交换,再从新调整(刚调整的元素之前的元素),直到排序完毕
for (int i = len - 1; i > 0; i--)
{
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
8 计数排序(Counting Sort )
非比较型排序,要求输入的数据必须是有确定范围的整数。在此前提条件下,速度快于任何比较排序算法。
对于待排序元素列中的每一个元素 x,确定该序列中值小于 x 的元素的个数——并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定。对于相同元素需要设定统计数组。
要给一堆弹珠从小到大排列,拿一个模具,模具上有从小到大的孔洞,我弹珠放进孔洞中,最后只要从模具中从小到大的孔洞中倒出弹珠即可(思想来源于:航海家(小海))
算法步骤:
1. 找出待排序元素列 arr 中最大值max、最小值min,创建与 len(arr) 长度的结果数组resultArr、(max - min + 1)长度的数组Arr、统计数组countArr
- 数组 Arr 用来存放每个值的个数,countArr 存放最终位置;
- 节省内存,索引变为arr[] - min;
- 统计数组用来解决元素相同问题
2. 遍历元素列,统计值为 i (min~max)的元素个数,依次存入Arr[i - min]
- 数组索引转换
- 个数作为值
3. 遍历 Arr[],每个索引之前的计数依次累加,存入 countArr[] 相同索引位置
- 存放最终排序完成时元素的位置,解决元素相同问题
4. 反向遍历 arr[],【countArr[arr - min] - 1】即为arr[] 在排序完成的 resultArr 中的位置,同时将countArr[] 减1
时间复杂度:
k = max - min + 1
空间复杂度:
稳定
编程题
1. 选举学生会
学校正在选举学生会成员,有 名候选人,每名候选人编号分别从 1 到
,现在收集到了
张选票,每张选票都写了一个候选人编号。现在想把这些堆积如山的选票按照投票数字从小到大排序。
- 输入格式
输入 和
以及
个选票上的数字。
- 输出格式
排序后的选票编号
// 题解sort更方便,主要为了理解计数排序
#include<bits/stdc++.h>
using namespace std;
int a,n, m, b[1000];
int main() {
cin>>n>>m;
for(int i=0;i<m;i++)cin>>a,++b[a]; //记录票出现的次数
for(int i=0;i<1000;i++)while(b[i]--)cout<<i<<" "; //根据票出现的次数输出
return 0;
}
9 桶排序(Bucket Sort)/箱排序
非比较型排序,将 [max,min] 划分为大小相等的子区间(桶/箱)。
利用函数映射关系,将待排序元素列分到有限数量的桶里,每个桶内再排序(其他排序算法/递归)。
时间复杂度:
k为桶数
- 最好 & 平均:
- 最坏:
空间复杂度:
稳定
10 基数排序(Radix Sort)
非比较型排序
- MSD,最高位优先
- LSD,最低位优先
时间复杂度:
空间复杂度:
- 最坏:
稳定
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = newint[n];
int *count = newint[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete[]tmp;
delete[]count;
}
11 TimSort
把输入中有序的序列分区,逆序翻转后分区,使其成为基本单元(称为“run”)
- 归并:将两个 run 合并成一个 run。归并的结果保存到 run_stack 上
- 插入排序
时间复杂度:
- 最好:
- 最坏 & 平均:
空间复杂度:
- 最坏:
稳定
各算法比较
时空复杂度
(图片来源于菜鸟教程。In-place:占用常数内存,不占用额外内存;Out-place:占用额外内存)
- 平方阶
:冒泡、选择、插入排序
- 线性对数阶:快速、堆、归并排序
- 线性阶
:基数、桶排序
简单性
- 简单算法:冒泡、简单选择、直接插入
- 改进算法:希尔、堆、归并、快速
从平均情况来看,最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法
整体时间复杂度,堆/归并 > 快速
从最好情况看,冒泡和直接插入排序要更胜一筹(待排序序列总是基本有序时)
非常在乎排序稳定性:归并排序
待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适
对于数据量不是很大而记录的关键字信息量较大的排序要求,简单排序算法是占优的
其他
“桶”
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;