1. 冒泡排序

两两交换,第一趟使最大的元素位于最后,第二趟使次大的处于倒数第二位,每一趟处理,都会有一个元素处于最终位置,时间复杂度为O(N^2)

排序后同值元素的相对位置保持不变,具有稳定性(大学考试可能会考,实际上没什么卵用)
冒泡.gif

  1. void bubbleSort(vector<int>& v) {
  2. for (int i = v.size() - 1; i > 0; i--) {
  3. bool flag = true; // 标记是否发生交换,如果没有发生交换,则已经有序,直接结束循环
  4. for (int j = 0; j < i; j++) {
  5. if (v[j] > v[j + 1]) { // swap
  6. int t = v[j];
  7. v[j] = v[j + 1];
  8. v[j + 1] = t;
  9. flag = false;
  10. }
  11. }
  12. if (flag)
  13. break;
  14. }
  15. }

2. 选择排序

第一趟选择所有元素中最小的,和第一位交换;第二趟选择第二位及以后的元素中最小的,和第二位交换;以此类推.(选择最大的和最后一位交换也行,逻辑是一样的)
每一趟处理,都会有一个元素处于最终位置,时间复杂度为O(N^2)

选择排序.gif

  1. void selectionSrot(vector<int>& v) {
  2. for (int i = 0; i < v.size(); i++) {
  3. int minIndex = i;
  4. for (int j = i + 1; j < v.size(); j++) { // 查找最小元素
  5. if (v[j] < v[minIndex]) {
  6. minIndex = j;
  7. }
  8. }
  9. // 交换
  10. int t = v[i];
  11. v[i] = v[minIndex];
  12. v[minIndex] = t;
  13. }
  14. }

3. 插入排序

思路:假设某元素之前的子序列有序,该元素如果大于子序列末端元素则可继续下一个元素;如果该元素小于子序列末端,则往前插入到指定位置
第一趟:第一个元素已经有序
第二趟:第二个元素往前插
第三趟:第三个元素往前两个元素插
……
时间复杂度:O(n²)=1+2+3+…+n-1
空间复杂度:O(1)
原址排序
稳定性:由于是从后往前,后续元素如果存在前面相等的,无法越过,相对位置不会发生变化

插入排序.gif
算法第四版上插入的思路,通过交换,值得学习

  1. void insertSort(vector<int>& v) {
  2. for (int i = 0; i < v.size(); i++) {
  3. for (int j = i; j > 0 && v[j - 1] > v[j]; j--) {
  4. // 通过交换将元素插入合适的位置
  5. int t = v[j];
  6. v[j] = v[j - 1];
  7. v[j - 1] = t;
  8. }
  9. }
  10. }

4. 希尔排序

希尔排序是插入排序的一种。
也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法

思路:如序列 9 8 7 6 5 4 3 2 1
确定一个增量序列,如 4(length/2) 2 1 ,从大到小使用增量
使用第一个增量,将序列划分为若干个子序列,下标组合为0-4-8,1-5,2-6,3-7
依次对子序列使用直接插入排序法;
使用第二个增量,将序列划分为若干个子序列(0-2-4-6-8),(1-3-5-7)
依次对子序列使用直接插入排序法:
使用第三个增量1,这时子序列就是元序列(0-1-2-3-4-5-6-7-8),使用直接插入法
完成排序。
时间复杂度:不太确定在O(nlogn)~O(n²)之间
空间复杂度:O(1)
原址排序
稳定性:由于相同的元素可能会被划分至不同子序列单独排序,因此稳定性是无法保证的——不稳定
希尔排序.gif

  1. void shellSort(vector<int>& v) {
  2. int len = v.size();
  3. int interval = len / 2; //增量,一般初始设为长度的一半
  4. // while (h < len / 3)
  5. // h = 3 * h + 1;
  6. while (interval >= 1) {
  7. // 第一个循环,改变间隔的大小
  8. for (int i = interval; i < len; i++) {
  9. // i向前推进的同时,用j来对每个子序列进行插入排序,很秒
  10. for (int j = i; j >= interval && v[j] < v[j - interval]; j -= interval) {
  11. int t = v[j];
  12. v[j] = v[j - interval];
  13. v[j - interval] = t;
  14. }
  15. }
  16. interval /= 2;
  17. }
  18. }

5. 快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

pivot的选择:

  • 最左边元素
  • 最右边元素
  • 中间元素

选择两端的元素为pivot如果数组本身有序的话,可能会TLE

快排的一般写法,这段代码没有把分区单独封装

  1. void quickSort(vector<int>& v, int l, int r) {
  2. // 子数组长度为 1 时终止递归
  3. if (l >= r) return;
  4. // 哨兵划分操作(以 arr[l] 作为基准数)
  5. int i = l, j = r;
  6. while (i < j) {
  7. while (i < j && arr[j] >= arr[l]) j--; // 找到第一个小于枢轴的数
  8. while (i < j && arr[i] <= arr[l]) i++; // 找到第一个大于枢轴的数
  9. swap(arr[i], arr[j]); // 交换
  10. }
  11. swap(arr[i], arr[l]);
  12. // 递归左(右)子数组执行哨兵划分
  13. quickSort(arr, l, i - 1);
  14. quickSort(arr, i + 1, r);
  15. }

划分的时候有单向扫描分区法,双指针双向扫描分区法,还有三点中值法使得选择的枢轴尽量是中值

快速排序初始数组越乱,效率越高,平均效率为O(nlogN)

👌模板:

  1. #include <iostream>
  2. #include <cstring>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 100010;
  6. int x[N];
  7. void quickSort(int x[], int l, int r) {
  8. if (l >= r) return; // 没有元素或者只有一个
  9. int i = l - 1, j = r + 1, pivot = x[l + r >> 1]; // 去中间元素为枢轴 样例有有序大数组,选择两端会TLE
  10. while (i < j) {
  11. while (x[++i] < pivot);
  12. while (x[--j] > pivot);
  13. if (i < j) swap(x[i], x[j]);
  14. }
  15. quickSort(x, l, j);
  16. quickSort(x, j + 1, r);
  17. }
  18. int main() {
  19. int n;
  20. cin >> n;
  21. for (int i = 0; i < n; ++i) cin >> x[i];
  22. quickSort(x, 0, n - 1);
  23. for (int i = 0; i < n; ++i) cout << x[i] << " ";
  24. return 0;
  25. }

快速选择算法查找第k个数

快速排序的步骤:

  • 选择pivot
  • 划分为左右两个区间
  • 递归处理左右两个区间

如果pivot前面有left个元素,假如left > k,说明排序后第k个元素位于枢轴左边,递归处理左区间,反之递归处理右区间。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int x[N];

int quickSelect(int x[], int l, int r, int k) {
    if (l >= r) return x[l];
    int i = l - 1, j = r + 1, pivot = x[l + r >> 1];
    while (i < j) {
        while (x[++i] < pivot);
        while (x[--j] > pivot);
        if (i < j) swap(x[i], x[j]);
    }
    int len = j - l + 1; // pivot前面的元素个数
    if (len >= k) return quickSelect(x, l, j, k); // 处理左区间
    else return quickSelect(x, j + 1, r, k - len); // 处理有区间,找右区间的第k - len个元素
}

int main() {
    int n, k;
    cin >> n >> k;
    for (int i = 0; i < n; ++i) cin >> x[i];

    cout << quickSelect(x, 0, n - 1, k) << endl;
    return 0;
}

快速排序划分的应用,求排序数组第k大的数

6. 归并排序

归并排序使用的是分治的思想,先让部分有序,然后进行归并,最终使整体有序
归并排序需要一个辅助空间,空间复杂度为O(N),时间复杂度为 O(NlogN)
归并排序.gif
步骤:

  1. 确定分界点(中间点),将数组分为左右两部分
  2. 递归排序左右两部分
  3. merge

版本1:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int a[N], tmp[N];

void merge_sort(int a[], int l, int r) {
    if (l >= r) return;

    int mid = (l + r) >> 1;
    merge_sort(a, l, mid);
    merge_sort(a, mid + 1, r);

    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];
        else tmp[k++] = a[j++];
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];
    for (int i = l; i <= r; ++i) a[i] = tmp[i];
}


int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
    merge_sort(a, 0, n - 1);
    for (int i = 0; i < n; ++i) printf("%d ", a[i]);
     return 0;
}

版本2:

void merge(vector<int>& v, int left, int mid, int right) { // 左闭右闭
    // 相当于将两个数组合并成一个有序数组
    int n = right - left + 1;
    vector<int> helper(v.begin(), v.end());

    int cur = left;
    int p1 = left, p2 = mid + 1;
    while (p1 <= mid && p2 <= right) {
        if (helper[p1] < helper[p2]) {
            v[cur++] = helper[p1++];
        } else {
            v[cur++] = helper[p2++];
        }
    }
    while (p1 <= mid) { // 右边没处理完的话,不用处理
        v[cur++] = helper[p1++];
    }

}

void mergeSort(vector<int>& v, int left, int right) { // 左闭右闭
    if (left < right) {
        int mid = left + ((right - left) >> 1);
        mergeSort(v, left, mid); // 处理左半部分
        mergeSort(v, mid + 1, right); // 处理右半部分
        merge(v, left, mid, right); // 归并
    }
}

归并排序的应用,求逆序对的个数
归并的过程中,如果左半边的数组出现了大于右半边数组中对应位置的元素,那么左半边数组中剩余的所有元素都比右半边这个元素大,那么逆序对的个数加 mid - i + 1,即左边剩余元素的个数

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 100010;
int a[N], tmp[N];

ll mergeSort(int a[], int l, int r) {
    if (l >= r) return 0;
    int mid = (l + r) >> 1;

    long long res = mergeSort(a, l, mid) + mergeSort(a, mid + 1, r);

    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];
        else {
            tmp[k++] = a[j++];
            res += mid - i + 1;
        }
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];

    for (int i = l; i <= r; i++) a[i] = tmp[i];
    return res;
}

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    cout << mergeSort(a, 0, n - 1) << endl;
    return 0;
}

还有一个常考的题,求小数和

#include<iostream>
using namespace std;
const int N = 100010;
int n;
int a[N], tmp[N];
long long res;

void merge_sort(int a[], int l, int r) {
    if (l >= r) return;
    int mid = (l + r) >> 1;
    merge_sort(a, l, mid);
    merge_sort(a, mid + 1, r);

    // merge
    int k = l, i = l, j = mid + 1;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) { // 找到a[j]左边比它小的数
            res += a[i] * (r - j + 1); // a[i]比a[j]右边的数也小
            tmp[k++] = a[i++];
        } else {
            tmp[k++] = a[j++];
        }
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];

    for (int i = l; i <= r; i++) a[i] = tmp[i];
}



int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];
    merge_sort(a, 0 , n - 1);
    cout << res << endl;
    return 0;
}

7. 计数排序

计数排序:创建一个辅助数组,以数据元素为索引,统计出现的次数,然后按照辅助数组下标和计数进行输出,得到一个有序(升)数列
优点:快
缺点:数组数据范围较大时,浪费空间
适用范围:数据范围较小,额外空间能接受

计数排序.gif

void countSort(vector<int>& v) {
    int maxNum = INT32_MIN;
    for (auto num : v) {
        maxNum = max(maxNum, num);
    }
    vector<int> helper(maxNum + 1, 0); // 辅助数组用于计数
    // 统计出现的次数
    for (auto num : v) {
        helper[num]++;
    }

    // 根据辅助数组来进行排序
    int cur = 0;
    for (int i = 0; i < helper.size(); i++) {
        while (helper[i]) {
            v[cur++] = i;
            helper[i]--;
        }
    }
}

计数排序是一个稳定?的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

8. 桶排序

桶排序是计数排序的升级版。计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效
image.png
桶排序需要用链表(动态数组也行)来存储每个桶中的元素

void bucketSort(vector<int>& v) {
    if (v.size() == 0)
        return;

    int maxNum = v[0], minNum = v[0];
    for (auto num : v) {
        maxNum = max(maxNum, num);
        minNum = min(minNum, num);
    }
    // 计算桶的数量
    int bucketNum = (maxNum - minNum) / v.size() + 1; //这个自己定义也行
    vector<vector<int>> buckets(bucketNum);
    // 元素入桶子
    for (auto num: v) {
        int n = (num - minNum) / v.size(); // 计算映射到哪个桶
        buckets[n].push_back(num);
        // 寻找正确的插入位置
        for (int i = buckets[n].size() - 1; i > 0 && buckets[n][i] < buckets[n][i - 1]; i++) {
            int temp = buckets[n][i];
            buckets[n][i] = buckets[n][i - 1];
            buckets[n][i - 1] = temp;
        }
    }

    int cur = 0;
    for (auto bucket : buckets) {
        for (int i = 0; i < bucket.size(); i++) {
            v[cur++] = bucket[i];
        }
    }
}

9. 基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

算法描述:

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

基数排序.gif

void radixSort(vector<int>& v) {
    if (v.size() == 0)
        return;

    // 寻找最大的元素,计算位数
    int maxNum = v[0];
    for (auto num : v) {
        maxNum = max(maxNum, num);
    }
    int pos = 0;
    while (maxNum) {
        pos++;
        maxNum /= 10;
    }

    const int BUCKETS = 10;
    vector<vector<int>> buckets(BUCKETS);


    //外层循环是根据数字的位数确定的
    for (int i = 0; i < pos; i++) {
        // i = 0, 表示个位数; i = 1, 表示十位数; 以此类推
        int denominator = static_cast<int>(pow(10, i)); // 取某一位数的时候需要用的分母
        for (int & tmp : v) // 按数字放入桶中
            buckets[(tmp / denominator) % 10].push_back(tmp);

        int index = 0;
        // 从桶中取出来,放入原来的source序列中,以备下次遍历时使用
        for (auto & theBucket : buckets) {
            for (int & k : theBucket)
                v[index++] = k;

            theBucket.clear();//一定要清空桶中数据
        }
    }
}

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

10. 堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

堆排序.gif

堆排序代码实现的流程:
建堆→将堆顶元素与数组未处理部分的最后一个元素交换→调整→将堆顶元素与数组未处理部分的最后一个元素交换→调整→…
如果要进行升序排序,可以建立小顶堆,每次输出堆顶(最大元素),将栈顶元素放在数组末尾,最后形成一个单调不减的数组
处理的过程中利用完全二叉树的性质,可以方便计算左右节点、父节点的下标

// 建立大顶堆
void makeHeap(vector<int>& v) {
    int n = v.size();
    for (int i = n / 2 - 1; i >= 0; i--) {
        fixDown(v, i, n);
    }
}

// 调整
void fixDown(vector<int>& v, int k, int n) {
    int left = 2 * k + 1;
    int right = 2 * k + 2;

    if (left >= n) { // 如果左孩子越界,当前节点是叶子结点,不用调整
        return;
    }
    int maxChild;
    if (right >= n) { // 右孩子越界
        maxChild = left;
    } else {
        maxChild = v[left] > v[right] ? left : right; // max指向较大的元素
    }

    if (v[k] >= v[maxChild])
        return; // 父节点比孩子大,不用交换

    // 父节点与最大的孩子交换
    int temp = v[k];
    v[k] = v[maxChild];
    v[maxChild] = temp;

    // 递归调整
    fixDown(v, maxChild, n);
}

void heapSort(vector<int>& v) {
    makeHeap(v); // 建堆
    for (int i = v.size() - 1; i >= 0; i--) {
        int temp = v[0];
        v[0] = v[i];
        v[i] = temp;
        fixDown(v, 0, i);
    }
}

时间复杂度: 堆化:一半的元素修复,修复是单分支的,所以整体堆化为 n*lgn / 2
排序:n个元素都要取出,因此调整n次,每次调整修复同上是lgn的,整体为O(nlgn)
空间复杂度:不需要开辟辅助空间
原址排序

参考链接