排序算法合集

126 阅读4分钟

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:

image.png

parent = (i - 1) / 2 = 1

c1 = 2i + 1 = 7, c2 = 2i + 2 = 8

讲解视频

堆排序算法

代码

  1. 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;
}

运行结果

image.png

树的变化如下图所示,

需要注意的是,父节点堆化后,还需对被更换的子节点进行堆化(图2→图3)。

image.png image.png image.png
  1. 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;
}

运行结果

image.png

  1. 完整代码
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、 将两个排序好的子序列合并成一个最终的排序序列。

图解

202205072324650.gif

时间复杂度

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)

稳定性

如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

代码

  1. 未优化版本
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;
  1. 优化版本

假如从开始的第一对到结尾的最后一对,相邻的元素之间都没有发生交换的操作,这意味着右边的元素总是大于等于左边的元素,此时的数组已经是有序的了,我们无需再对剩余的元素重复比较下去了。

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、从最低位开始对每个位进行计数排序(利用计数排序适用于小范围数的特点)。

视频讲解

  1. 思路讲解
  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;
}

运行结果

image.png

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;
}

综合比较

img