插⼊排序
插⼊排序(Insertion Sort)类似于玩扑克牌插牌过程,每次将⼀个待排序的元素按照其关键字⼤⼩插⼊到前⾯已排好序的序列中,按照该种⽅式将所有元素全部插⼊完成即可
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
void insert_sort()
{
// 依次枚举待排序的元素
for(int i = 2; i <= n; i++) // 第⼀个位置默认就是有序的
{
int key = a[i];
// 前⾯⽐ key ⼤的,统⼀右移
int j = i - 1;
while(j >= 1 && a[j] > key)
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
insert_sort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
时间复杂度
- 当整个序列有序的时候,插⼊排序最优,此时时间复杂度为O(n) 。
- 当整个序列逆序的时候,每个元素都要跑到最前⾯,时间复杂度为O(n2)
选择排序
选择排序(Selection Sort)是⼀种特别直观的排序算法。每次找出未排序序列中最⼩的元素,然后放进有序序列的后⾯
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
void selection_sort()
{
for(int i = 1; i < n; i++) // 待排序区间的⾸位置
{
// [i, n] 区间就是待排序的区间
int pos = i;
for(int j = i + 1; j <= n; j++) // 查找待排序区间最⼩的元素的下标
{
if(a[j] < a[pos])
{
pos = j;
}
}
swap(a[i], a[pos]);
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
selection_sort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
【时间复杂度】
- ⽆论数组有序还是⽆序,在未排序区间内找最⼩值的时候,都需要遍历⼀遍待排序的区间,因此时间复杂度为O(n2)
冒泡排序
冒泡排序(Bubble Sort)也是⼀种简单的排序算法。它的⼯作原理是每次检查相邻两个元素,如果前⾯的元素与后⾯的元素满⾜给定的排序条件,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。
由于在算法的执⾏过程中,较⼤的元素像是⽓泡般慢慢浮到数列的末端,故叫做冒泡排序
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
// 优化后的冒泡排序
void bubble_sort()
{
// 依次枚举待排序区间的最后⼀个元素
for(int i = n; i > 1; i--)
{
bool flag = false;
// [1, i] 就是待排序区间
for(int j = 1; j < i; j++)
{
if(a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
flag = true;
}
}
if(flag == false) return;
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
bubble_sort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
时间复杂度
- 如果数组有序,只会扫描⼀遍,时间复杂度为O(n) 。
- 如果逆序,所有元素都会向后冒⼀次到合适位置,时间复杂度为O(n2)
堆排序
堆排序(Heap Sort)是指利⽤堆这种数据结构所设计的⼀种排序算法。本质上是优化了选择排序算法,如果将数据放在堆中,能够快速找到待排序元素中的最⼩值或最⼤值。
堆排序的过程分两步:
- 建堆。升序建⼤堆,降序建⼩堆。
建堆过程,从倒数第⼀个⾮叶⼦节点开始,倒着⼀直到根结点位置,每个结点进⾏向下调整。 - 排序。删除堆顶的逻辑。
每次将堆顶元素与堆中最后⼀个元素交换,然后将堆顶元素往下调整。直到堆中剩余⼀个元素,所有元素就都有序了。
因此,堆排序仅需⽤到向下调整算法
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
void down(int parent, int len)
{
int child = parent * 2;
while(child <= len)
{
if(child + 1 <= len && a[child + 1] > a[child]) child++;
if(a[parent] >= a[child]) return;
swap(a[parent], a[child]);
parent = child;
child = parent * 2;
}
}
void heap_sort()
{
// 1. 建堆
for(int i = n / 2; i >= 1; i--)
{
down(i, n);
}
// 2. 排序
for(int i = n; i > 1; i--) // 枚举堆⾥⾯最后⼀个元素的位置
{
swap(a[1], a[i]);
down(1, i - 1);
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
heap_sort();
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
时间复杂度
时间复杂度需要计算两部分:⼀部分是建堆的时间复杂度,另⼀部分是排序。
- 排序部分的时间复杂度很好估计,每次都是从堆中选择⼀个最⼤值,然后与最后⼀个元素交换,然后调整。⼀次选择的时间复杂度为log n ,⼀共执⾏n 次,⼤概就是n log n 级别。
- 建堆的时间复杂度:
计算向下调整算法建堆时间复杂度
则需要移动结点总的移动步数为:每层结点个数*向下调整次数
T ( h ) = 2 h − 1 − h T(h) = 2^h-1-h T(h)=2h−1−h
根据⼆叉树的性质:n = 2^h - 1 和h = log2 (n + 1)
T ( n ) = n − log 2 ( n + 1 ) ≈ n T(n) = n - \log_{2}(n+1) \approx n T(n)=n−log2(n+1)≈n
综上所述,堆排序的时间复杂度主要取决于排序的过程,也就是n log n
快速排序
快速排序(Quick Sort),既然敢起这样的名字,说明它是常⻅排序算法中较为优秀的。事实上,在很多情况下,快排确实是效率较⾼的算法
算法原理
常规快排:在待排序区间中任取⼀个元素作为枢轴(pivot,或称基准值,通常选取区间⾸元素),然后按照基准值⼤⼩将区间中元素分割成左右两部分,左侧区间中元素⼩于基准值,右侧区间中元素⼤于等于基准值,即基准值已经放在其该放的位置上,该过程称为⼀次划分。划分结束后,再递归排基准值左侧,递归排基准值右侧即可
优化⼀:三路划分
三路划分优化:其实可以做到按照基准元素,将所有元素分成三个区间。左部分全部⼩于pivot,中间部分全部等于pivot,右部分全部⼤于pivot。然后中间部分就不⽤管了,直接递归处理左右部分
从区间中任取一个元素作为基准值,一般取区间最左侧元素,然后按照基准值对区间中元素进行划分
递归排基准值左侧;递归排基准值右侧
⽽这个三路划分,就是典型的数组分三块算法
优化⼆:随机选择基准元素
选择基准元素的⽅式:
- 每次选择待排序序列的最左边元素
那么,当整个序列有序的时候,每次递归就会划分出特别⻓的⼀段右区间,递归的层数是n次,每次要遍历整个数组⼀遍,时间复杂度就退化成n^2 。
每次选择最右边元素也是同理。 - 每次选择待排序序列的中间元素
可以构造⼀个特殊的序列,使得每次取中间元素的时候都会取到最⼩值,依旧会退化成n^2。 - 随机选择基准元素
每次选择基准元素的时候,都从待排序的序列中随机选择⼀个数。在随机性的前提下,可以证明算法的时间复杂度趋近于nlogn 。
因此,每次选择基准元素时,都使⽤随机函数选择
C++中的随机函数
C++提供了函数srand和rand,搭配使⽤可以返回⼀个随机值
#include <iostream>
#include <ctime>
using namespace std;
int main()
{
srand(time(0)); // 种下⼀个随机数种⼦
for(int i = 1; i <= 10; i++)
{
cout << rand() << endl; // 每次⽣成⼀个随机值
}
return 0;
}
left和right指向第一个和最后一个元素,假设p等于4
i从第一个元素往最后一个元素遍历
现在ai是4,和p一样大,i++
ai是2,比p小,交换到前面,++l和i交换,i++
现在ai是4,和p一样大,i++
现在ai是5,比p大,交换到最右边,i和–r交换
i这是下标为4,ai为1,比p小,++l和i交换,i++
这是i等于5,r也是5,遍历结束,此时p=4,两个4已经在正确的位置上
中间区域也就是相等的区域,是l+1到r-1
左边比p小的区域是left到l,右边比p大的区域是r到right
接着遍历left到l和r到right
#include <iostream>
#include <ctime>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
// 优化⼀:随机选择基准元素
int get_random(int left, int right)
{
return a[rand() % (right - left + 1) + left];
}
void quick_sort(int left, int right)
{
if(left >= right) return;
// 1. 选择⼀个基准元素
int p = get_random(left, right);
// 2. 数组分三块 - 荷兰国旗问题
int l = left - 1, i = left, r = right + 1;
while(i < r)
{
if(a[i] < p) swap(a[++l], a[i++]);
else if(a[i] == p) i++;
else swap(a[--r], a[i]);
}
// [left, l] [l + 1, r - 1] [r, right]
quick_sort(left, l);
quick_sort(r, right);
}
int main()
{
srand(time(0));
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
quick_sort(1, n);
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
时间复杂度
- 如果每次基准元素都选择得当,数组划分的比较均匀,时间复杂度=递归层数
*
N*
logN - 如果划分不当,数组分布比较极端,时间复杂度退化成N^2
归并排序
归并排序(Merge Sort)是⽆论数据有什么特性,时间复杂度就能稳定O(n log n) 的排序算法
归并排序⽤的是分治思想,不知道分治是啥也没关系,后续算法课会讲到。它的主要过程分两步:
- 将整个区间从中间⼀分为⼆,先把左边和右边排排序;
- 然后将左右两个有序的区间合并在⼀起。
其中,如何让左右两边有序,就继续交给归并排序,因此归并排序是⽤递归来实现的;合并两个有序区间的操作,在顺序表中讲过类似的算法题
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
int tmp[N]; // 辅助归并排序时,合并两个有序数组
void merge_sort(int left, int right)
{
if(left >= right) return;
// 1. 先⼀分为⼆
int mid = (left + right) >> 1;
// [left, mid] [mid + 1, right]
// 2. 先让左右区间有序
merge_sort(left, mid);
merge_sort(mid + 1, right);
// 3. 合并两个有序数组
int cur1 = left, cur2 = mid + 1, i = left;
// [left, mid] [mid + 1, right]
while(cur1 <= mid && cur2 <= right)
{
if(a[cur1] <= a[cur2]) tmp[i++] = a[cur1++];
else tmp[i++] = a[cur2++];
}
while(cur1 <= mid) tmp[i++] = a[cur1++];
while(cur2 <= right) tmp[i++] = a[cur2++];
for(int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
merge_sort(1, n);
for(int i = 1; i <= n; i++) cout << a[i] << " ";
return 0;
}
【时间复杂度】
每次递归都是标准的⼀分为⼆,因此时间复杂度为O(n log n)