1、快速排序
算法思想
1、选取第一个数为基准;
2、将比基准小的数交换到前面,比基准大的数交换到后面;
3、对左右区间重复第二步。
时间复杂度
nlogn
视频讲解
代码
void quickSort(vector<int>& A, int low, int high)
{
if (low >= high) return; // 递归结束标志
int key = A[low]; // 以第一个数为基准
int i = low; // 低位下标
int j = high; // 高位下标
while (i < j)
{
// 从后往前走,将比第一个数小的移到前面
while (i < j && A[j] >= key)
{
--j;
}
if (i < j)
{
A[i] = A[j];
i++;
}
// 从前往后走,将比第一个数大的移到后面
while (i < j && A[i] <= key)
{
++i;
}
if (i < j)
{
A[j] = A[i];
j--;
}
}
A[i] = key;
// 前半递归
quickSort(A, low, i-1);
// 后半递归
quickSort(A, i+1, high);
}
quickSort(A, 0, A.size()-1);
for (auto i : A)
{
cout << i << endl;
}
eg:LeetCode 215. 数组中的第K个最大元素
class Solution {
public:
int findKthLargest(vector<int>& a, int k) {
int left = 0;
int right = a.size()-1;
int key = partition(a,left, right);
while(1){
if(key == k-1) return a[key];
else if(key < k-1){
key = partition(a, key + 1, right);
}else if(key > k-1){
key = partition(a, left, key - 1);
}
}
}
private:
int partition(vector<int>& a,int i,int j){
int key = a[i];
while(i < j){
while(i < j && a[j] <= key)
j--;
if (i < j)
a[i++] = a[j];
while(i < j && a[i] >= key)
i++;
if (i < j)
a[j--] = a[i];
}
a[i] = key;
return i;
}
};
2、堆排序
堆满足的两个条件
1、堆是完全二叉树
2、堆中父节点的值大于等于(或小于等于)子节点的值
时间复杂度
nlogn
数学关系
从 0 开始,对于索引为 i 的父节点:
1、其父节点为 (i - 1) / 2
2、其子节点分别为 2i + 1 和 2i + 2
比如下图的节点 3:
parent = (i - 1) / 2 = 1
c1 = 2i + 1 = 7, c2 = 2i + 2 = 8
讲解视频
代码
- heapify
功能:使树中的一个节点堆有序的操作叫做“heapify”,即堆化
代码:对索引为0(值为4)的节点进行heapify操作
void heapify(vector<int>& nums, int n, int i)
{
if (i >= n) return;
int l = 2 * i + 1, r = 2 * i + 2;
int max = i;
if (l < n && nums[l] > nums[max])
max = l;
if (r < n && nums[r] > nums[max])
max = r;
if (max != i)
{
swap(nums[max], nums[i]);
heapify(nums, n, max);
}
}
int main(){
vector<int> tree{4, 10, 3, 5, 1, 2};
int n = 6;
heapify(tree, n, 0);
int i;
for (int i = 0; i < n; i++){
cout << tree[i] << endl;
}
return 0;
}
运行结果:
树的变化如下图所示,
需要注意的是,父节点堆化后,还需对被更换的子节点进行堆化(图2→图3)。
- buildHeap
功能:将完全二叉树构造成堆,即建堆。建堆时需要用到堆化的操作
代码:
#include <bits/stdc++.h>
using namespace std;
void heapify(vector<int>& nums, int n, int i)
{
if (i >= n) return;
int l = 2 * i + 1, r = 2 * i + 2;
int max = i;
if (l < n && nums[l] > nums[max])
max = l;
if (r < n && nums[r] > nums[max])
max = r;
if (max != i)
{
swap(nums[max], nums[i]);
heapify(nums, n, max);
}
}
// 建立大根堆,从树的倒数第二层的最后一个非叶子节点开始
void buildHeap(vector<int>& nums, int n)
// 对每个节点进行 heapify 操作,然后向上走
{
int start = (n - 2) / 2;
for (int i = start; i >= 0; i--)
{
heapify(nums, n, i);
}
}
int main(){
vector<int> tree{2, 5, 3, 1, 10, 4};
int n = 6;
buildHeap(tree, n);
int i;
for (int i = 0; i < n; i++){
cout << tree[i] << endl;
}
return 0;
}
运行结果:
- 完整代码
void heapify(vector<int>& nums, int n, int i)
{
if (i >= n) return;
int l = 2 * i + 1, r = 2 * i + 2;
int max = i;
if (l < n && nums[l] > nums[max])
max = l;
if (r < n && nums[r] > nums[max])
max = r;
if (max != i)
{
swap(nums[max], nums[i]);
heapify(nums, n, max);
}
}
// 建立大根堆,从树的倒数第二层的最后一个非叶子节点开始
void buildHeap(vector<int>& nums, int n)
// 对每个节点进行 heapify 操作,然后向上走
{
int start = (n - 2) / 2;
for (int i = start; i >= 0; i--)
{
heapify(nums, n, i);
}
}
// 建立大根堆之后,每次交换最后一个节点和根节点(最大值),
// 对交换后的根节点继续进行 heapify (此时堆的最后一位是最大值,因此不用管他,n 变为 n-1)
void heapSort(vector<int>& nums, int n)
{
buildHeap(nums, n);
for (int i = 0; i < n; i++)
{
swap(nums[0], nums[n - i - 1]);
heapify(nums, n - i - 1, 0);
}
}
heapSort(A, A.size());
for (auto i : A)
{
cout << i << " ";
}
cout << endl;
3、归并排序
算法思想
1、把长度为 n 的输入序列分成两个长度为 n/2 的子序列;
2、对这两个子序列分别采用归并排序;
3、 将两个排序好的子序列合并成一个最终的排序序列。
图解
时间复杂度
nlogn
缺点
辅助数组所使用的额外空间和 n 的大小成正比。
代码
void mergeSortCore(vector<int>& data, vector<int>& dataTemp, int low, int high) {
// datatemp用来存放归并排序后的数组
if (low >= high) return;
int mid = (low + high) / 2;
int start1 = low, end1 = mid, start2 = mid + 1, end2 = high;
mergeSortCore(data, dataTemp, start1, end1);
mergeSortCore(data, dataTemp, start2, end2);
int index = low;
while (start1 <= end1 && start2 <= end2) {
dataTemp[index++] = data[start1] < data[start2] ? data[start1++] : data[start2++];
}
while (start1 <= end1) {
dataTemp[index++] = data[start1++];
}
while (start2 <= end2) {
dataTemp[index++] = data[start2++];
}
for (index = low; index <= high; ++index) {
data[index] = dataTemp[index];
}
}
void mergeSort(vector<int>& data) {
int len = data.size();
vector<int> dataTemp(len, 0);
mergeSortCore(data, dataTemp, 0, len - 1);
}
mergeSort(A);
for (auto i : A)
{
cout << i << endl;
}
快速、堆、归并比较
| 排序方法\复杂度 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 堆排序 | O(nlogn) | O(1) |
| 快速排序 | O(nlogn) | O(nlogn) |
| 归并排序 | O(nlogn) | O(n) |
4、选择排序
算法思想
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给二个元素选择第二小的,依次类推,直到第 n - 1 个元素,第 n 个元素不用选择了,因为只剩下它一个最大的元素了。
时间复杂度
O(n^2)
不稳定性
在一趟选择中,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么,交换后的稳定性就被破坏了。
比较拗口,举个例子,序列 5 8 5 2 9, 我们知道第一遍选择第 1 个元素 5 会和 2 交换,那么原序列中 2 个 5 的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
代码
void selectSort(vector<int>& nums)
{
int len = nums.size();
int minIndex = 0;
for (int i = 0; i < len; ++i)
{
minIndex = i;
for (int j = i + 1; j < len; ++j)
{
if (nums[j] < nums[minIndex]) minIndex = j;
}
swap(nums[i], nums[minIndex]);
}
}
selectSort(A);
for (auto i : A)
{
cout << i <<" ";
}
cout << endl;
5、冒泡排序
算法思想
冒泡排序就是把小的元素往前调或者把大的元素往后调,比较是相邻的两个元素比较,交换也发生在这两个元素之间。
时间复杂度
O(n^2)
稳定性
如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
代码
- 未优化版本
void bubbleSort(vector<int>& nums) {
int len = nums.size();
for (int i = 0; i < len; ++i) {
for (int j = 0; j < len - 1 - i; ++j) {
if (nums[j] > nums[j + 1])
swap(nums[j], nums[j + 1]);
}
}
}
bubbleSort(A);
for (auto i : A)
cout << i << " ";
cout << endl;
- 优化版本
假如从开始的第一对到结尾的最后一对,相邻的元素之间都没有发生交换的操作,这意味着右边的元素总是大于等于左边的元素,此时的数组已经是有序的了,我们无需再对剩余的元素重复比较下去了。
void bubbleSort(vector<int>& nums) {
int n = nums.size();
bool flag = false;
for (int i = 0; i < n - 1; ++i) {
flag = false;
for (int j = 0; j < n - 1 - i; ++j) {
if (nums[j] > nums[j + 1]) {
// 某一趟排序中,只要发生一次元素交换,flag 就从 false 变为了 true
// 也即表示这一趟排序还不能确定所剩待排序列是否已经有序,应继续下一趟循环
swap(nums[j], nums[j + 1]);
flag = true;
}
}
// 但若某一趟中一次元素交换都没有,即依然为 flag = false
// 那么表明所剩待排序列已经有序
// 不必再进行趟数比较,外层循环应该结束,即此时 if (!flag) break; 跳出循环
if (!flag) break;
}
}
bubbleSort(A);
for (auto i : A)
cout << i << " ";
cout << endl;
6、插入排序
算法思想
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。
当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。
所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
代码
void print(vector<int>& a, int n, int i) {
cout << "step" << i << ": ";
for (int j = 0; j < n; j++) {
cout << a[j] << " ";
}
cout << endl;
}
void insertionSort(vector<int>& a, int n) {
for (int i = 1; i < n; ++i) {
if (a[i] < a[i - 1]) { // 若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
int j = i - 1;
int x = a[i]; // 复制为哨兵,即存储待排序元素
while (j >= 0 && x < a[j]) { // 查找在有序表的插入位置,还必须要保证 j 是 >=0 的 因为 a[j] 要合法
a[j + 1] = a[j];
j--; // 元素后移
}
a[j + 1] = x; // 插入到正确位置
}
print(a, n, i); // 打印每趟排序的结果
}
}
insertionSort(A, A.size());
for (auto i : A)
{
cout << i << " ";
}
cout << endl;
7、希尔排序
算法思想
希尔排序可以说是插入排序的一种变种。无论是插入排序还是冒泡排序,如果数组的最大值刚好是在第一位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。
希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为 1 的元素有序,此时的数组就是有序的了。
视频讲解
代码
void shellSortCore(vector<int>& nums, int gap, int i) {
int inserted = nums[i];
int j;
// 插入的时候按组进行插入
for (j = i - gap; j >= 0 && inserted < nums[j]; j -= gap) {
nums[j + gap] = nums[j];
}
nums[j + gap] = inserted;
}
void shellSort(vector<int>& nums) {
int len = nums.size();
// 进行分组,最开始的时候,gap 为数组长度一半
for (int gap = len / 2; gap > 0; gap /= 2) {
// 对各个分组进行插入分组
for (int i = gap; i < len; ++i) {
// 将 nums[i] 插入到所在分组正确的位置上
shellSortCore(nums,gap,i);
}
}
for (auto a : nums) {
cout << a << " ";
}
cout << endl;
}
shellSort(A);
8、计数排序
算法思想
桶排序的思想就是把数据放到多个桶里,再对桶里的数据进行排序。
计数排序是基于桶排序思想的一种非比较排序。
1、找出待排序的数组中最大的元素;
2、统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
3、对所有的计数累加(从 C 中索引为 1 的元素开始,每一项和前一项相加);
4、向目标数组填充:将每个元素 i 放在新数组的第 i 项,每放一个元素就将 C[i] 减去 1;
适用范围
量大但是取值范围小
视频讲解
代码
#include <bits/stdc++.h>
using namespace std;
int getMax(vector<int>& arr, int size){
int maxx = arr[0];
for (int i = 1; i < size; i++){
if (arr[i] > maxx){
maxx = arr[i];
}
}
return maxx;
}
void countSort(vector<int>& arr, int size, int div){
vector<int> output(size);
vector<int> cnt(10, 0);
for (int i = 0; i < size; i++){
cnt[ (arr[i]/div)%10 ]++;
}
for (int i = 1; i < 10; i++){
cnt[i] += cnt[i - 1];
}
for (int i = size - 1; i >= 0; i--){
output[cnt[ (arr[i]/div)%10 ] - 1] = arr[i];
cnt[ (arr[i]/div)%10 ]--;
}
for (int i = 0; i < size; i++){
arr[i] = output[i];
}
}
void radixSort(vector<int>& arr, int size){
int m = getMax(arr, size);
for (int div = 1; m/div > 0; div *= 10){
countSort(arr, size, div);
}
}
int main(){
int size;
cout << "Enter the size of the array: " << endl;
cin >> size;
vector<int> arr(size);
cout << "Enter " << size << " integers in any order" << endl;
for (int i = 0; i < size; i++){
cin >> arr[i];
}
cout << endl << "Before Sorting: " << endl;
for (int i = 0; i < size; i++){
cout << arr[i] << " ";
}
radixSort(arr, size);
cout << endl << "After Sorting: " << endl;
for (int i = 0; i < size; i++){
cout << arr[i] << " ";
}
return 0;
}
9、基数排序
算法思想:
计数排序是基于桶排序思想的一种多关键字的非比较排序。
1、取得数组 arr 中的最大数;
2、从最低位开始对每个位进行计数排序(利用计数排序适用于小范围数的特点)。
视频讲解
代码
#include <bits/stdc++.h>
using namespace std;
int getMax(vector<int>& arr, int size){
int maxx = arr[0];
for (int i = 1; i < size; i++){
if (arr[i] > maxx){
maxx = arr[i];
}
}
return maxx;
}
void countSort(vector<int>& arr, int size, int div){
vector<int> output(size);
vector<int> cnt(10, 0);
for (int i = 0; i < size; i++){
cnt[ (arr[i]/div)%10 ]++;
}
for (int i = 1; i < 10; i++){
cnt[i] += cnt[i - 1];
}
for (int i = size - 1; i >= 0; i--){
output[cnt[ (arr[i]/div)%10 ] - 1] = arr[i];
cnt[ (arr[i]/div)%10 ]--;
}
for (int i = 0; i < size; i++){
arr[i] = output[i];
}
}
void radixSort(vector<int>& arr, int size){
int m = getMax(arr, size);
for (int div = 1; m/div > 0; div *= 10){
countSort(arr, size, div);
}
}
int main(){
int size;
cout << "Enter the size of the array: " << endl;
cin >> size;
vector<int> arr(size);
cout << "Enter " << size << " integers in any order" << endl;
for (int i = 0; i < size; i++){
cin >> arr[i];
}
cout << endl << "Before Sorting: " << endl;
for (int i = 0; i < size; i++){
cout << arr[i] << " ";
}
radixSort(arr, size);
cout << endl << "After Sorting: " << endl;
for (int i = 0; i < size; i++){
cout << arr[i] << " ";
}
return 0;
}
运行结果:
10、桶排序
算法思想
1、设置一个定量的数组当作空桶子;
2、寻访序列,并且把项目一个一个放到对应的桶子去;
3、对每个不是空的桶子进行排序;
4、从不是空的桶子里把项目再放回原来的序列中。
代码
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
void bucketSort(vector<float>& arr, int bucketSize)
{
// 创建 bucketSize 个空桶
vector<vector<float>> buckets(bucketSize);
// 将数组中的元素以一定的规则分配到不同的桶中
for (int i = 0; i < bucketSize; i++)
{
int bucketIndex = bucketSize * arr[i]; // 桶下标
buckets[bucketIndex].push_back(arr[i]);
}
// 将桶中的元素排序
int index = 0;
for (int i = 0; i < bucketSize; i++)
{
sort(buckets[i].begin(), buckets[i].end());
// 将桶中的元素分配到数组中
for (int j = 0; j < buckets[i].size(); j++)
{
arr[index++] = buckets[i][j];
}
}
}
int main()
{
vector<float> arr{0.22, 0.12, 0.98, 0.16, 0.83, 0.63, 0.24};
bucketSort(arr, arr.size());
for (auto f : arr)
{
cout << f << " ";
}
cout << endl;
}