(排序) 各种排序算法汇总

排序

前言说明

本文均以升序为例

表格分析

算法名称平均时间最优时间最差时间空间排序方式稳定性
冒泡 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)内部稳定
选择 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)内部不稳定
插入 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)内部稳定
希尔 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( 1 ) O(1) O(1)内部不稳定
快速 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n l o g n ) O(nlogn) O(nlogn)内部不稳定
归并 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n)外部稳定
O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( 1 ) O(1) O(1)内部不稳定

专业性介绍

衡量一个排序的参数,一般为 时间复杂度,空间复杂度,稳定性,

稳定性: 指数列中相同的元素,在排序完成后相对顺序是否改变

如(这里用下标表示):原来数列 [ 2 0 , 1 0 , 1 1 ] [2_0, 1_0, 1_1] [20,10,11]

稳定: [ 1 0 , 1 1 , 2 0 ] [1_0, 1_1, 2_0] [10,1120]

不稳定: [ 1 1 , 1 0 , 2 0 ] [1_1, 1_0, 2_0] [11,10,20]

分类介绍

  • 按时间复杂度分类

    • O ( n 2 ) {O(n^2)} O(n2)
    • O ( n l o g n ) {O(nlogn)} O(nlogn)
    • O ( n ) {O(n)} O(n)
  • 按照排序方式分类

    • 比较型
    • 非比较型
  • 按辅助空间分类

    • 内部排序
    • 外部排序

题集

牛客:简单的排序

数据范围:

1 < = n < = 1000 1<=n<=1000 1<=n<=1000

适合练习简单的排序


洛谷: P1177 【模板】快速排序

数据范围:

对于 20 % 20\% 20% 的数据,有 N ≤ 1 0 3 N\leq 10^3 N103

对于 100 % 100\% 100% 的数据,有 N ≤ 1 0 5 N\leq 10^5 N105


力扣: 912. 排序数组

数据范围:

1 < = n u m s . l e n g t h < = 5 ∗ 1 0 4 1 <= nums.length <= 5 * 10^4 1<=nums.length<=5104

− 5 ∗ 1 0 4 < = n u m s [ i ] < = 5 ∗ 1 0 4 -5 * 10^4 <= nums[i] <= 5 * 10^4 5104<=nums[i]<=5104

时间复杂度 O ( n 2 ) {O(n^2)} O(n2)

冒泡排序 (bubble sort)

不断比较相邻的两个数据,使得较大(或较小)的数据不断往一边靠

有冒泡和沉底两种思路,但核心都差不多

void bubbleSort(vector<int>& arr) {
    int n = arr.size();
    // 确定一遍的i个数据已成为有序
    for (int i = 0; i < n; i++) {
        // 不断比较相邻数据 -i可以免去再次比较已经排好的数据
        for (int j = 0; j < n-1-i; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j], arr[j+1]);
            }
        }
    }
}

选择排序 (select sort)

不断在未排好的区间内找到最大(最小)值

将该值与已排序的边缘外一个元素交换

void selectSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n; i++) {
        int minn = i;
        for (int j = i; j < n; j++) {
            if (arr[minn] > arr[j]) {
                minn = j;
            }
        }
        swap(arr[minn], arr[i]);
    }
}

插入排序 (insert sort)

循序枚举每个元素,以一个方向去判断大小

若相对大小不合格,则逆向覆盖掉来向的数据,最后记得存储一个枚举的数据

写的时候还有不少细节,需要注意

注意: 插入排序没最后排好之前,无法保证任何元素是否在最终位置。这点与冒泡和选择不同

void insertSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n; i++) {
        // 暂存需要插入的数据
        int cur = arr[i];
        // 从该数据的前一个位置开始试着判断插入
        int j = i-1;
        for (j = i-1; j >= 0 && cur < arr[j]; j--) {
            // 若符合则往后覆盖
            arr[j+1] = arr[j];
        }
        // 退出循环只有两个情况
        // 1.j==-1, 2.j处比cur还小
        // 因此是j的后一个位置要被cur覆盖
        arr[j+1] = cur;
    }
}

时间复杂度 O ( n l o g n ) {O(nlogn)} O(nlogn)

希尔排序 (shell sort)

时间复杂度约 O ( n 1.3 ) O(n^{1.3}) O(n1.3) 并非严格的 O ( n l o g n ) O(nlogn) O(nlogn)

希尔排序是插入排序的一种改良版,是一种缩小增量排序

朴素的插入排序的每次比较的数值是相邻的数值,差值为1

而希尔排序通过不断的将差值从大到小递减(直到1),来使一些元素相对位置提前排好便于后面的排序能减少插入次序


当n较小的时候,与 O ( n 2 ) O(n^2) O(n2) 差别不大

此处展示的是增量2倍递减的形式

上述的 力扣 5e4 可过; 洛谷 1e5可过

void insertSort(vector<int>& arr, const int step) {
    int n = arr.size();
    for (int i = step; i < n; i++) {
        int cur = arr[i];
        int j = i-step;
        for (j = i-step; j >= 0 && cur < arr[j]; j -= step) {
            arr[j+step] = arr[j];
        }
        arr[j+step] = cur;
    }
}
void shellSort(vector<int>& arr) {
    int n = arr.size();
    for (int step = n>>1; step > 0; step >>= 1) {
        insertSort(arr, step);
    }
}

快速排序 (quick sort)

大名鼎鼎的快速排序

各种平台为了卡特殊情况,设置了长段有序数列,从而让快排的时间复杂度退化到了 O ( n 2 ) O(n^2) O(n2)

为了解决这个问题,要随机获得一个序列的值,以该值来快排,可以避免一直出现有序而效率退化的问题

加了随机后 力扣可过,洛谷最后一个测试样例还是不可过

实现思路:

传入区间是闭区间 [ s t a r t , e n d ] [start, end] [start,end]

  • 在该区间内获得一个随机值
  • 将该值交换到序列的某一端处(此处交换到最左端)
  • 记录此时(交换后的)的最左端值 int pivot = arr[left];
  • 此时arr[left]是一个空位
  • ====================前置准备========================
  • 因为此时是左边有空置位置,所以先搜索右边
  • 右边搜索完,与左边的空置位置交换,此时空置位置到了右边
  • 开始搜索左边
  • 。。。循环操作。。。
  • 当letf == right时终止
  • 将最后的空置位置存储最开始存储的pivot
  • ==================一轮排序完毕======================
  • 此时letf 就是分割点
  • 分治递归该分割点的两边
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand((unsigned)time(NULL));
        quickSort(nums, 0, nums.size()-1);
        return nums;
    }
private:
    // 范围0~n-1
    void quickSort(vector<int>& arr, const int start, const int end) {
        if (start >= end) {
            return ;
        }

        int left = start;
        int right = end;
        
        // 区间内随机捕获的一个数值
        int pivotIdx = rand()%(right-left+1) + left;
        // 将该随机获得的数值靠到边界
        swap(arr[pivotIdx], arr[left]);
        
        // 获取对照值
        int pivot = arr[left];
        while (left < right) {
            // left处可存值,因此先搜索右边
            while (left < right && arr[right] >= pivot) {
                right--;
            }
            arr[left] = arr[right];
            // 右边搜索完毕,left空位存值完毕,而right处变为可供存值
            while (left < right && arr[left] <= pivot) {
                left++;
            }
            arr[right] = arr[left];
        }
        // left == right 退出,此处就是用来存最后的pivot
        arr[left] = pivot;

        pivotIdx = left;
        quickSort(arr, start, pivotIdx-1);
        quickSort(arr, pivotIdx+1, end);
    }

};

归并排序 (merge sort)

直观的解释,就是合并两个有序序列

与快排有点类似,不断分割小区间,将每次分割的两个区间有序合并

若当前的两个小区间均有序,则合并的大区间必然有序


  • 递归return 条件左右指针相碰
  • 先均分当前的区间
  • 递归两段分割的区间,使得两段成为有序序列
  • 合并两段有序序列,达到当前大区间排序好
  • 从合并的格外空间抄回原数组
class Solution {
private:
    vector<int> tmp;
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize(nums.size());
        mergeSort(nums, 0, nums.size()-1);
        return nums;
    }
private:
    void mergeSort(vector<int>& arr, const int start, const int end) {
        if (start >= end) {
            return ;
        }

        int mid = (end-start)/2 + start;
        // 先将两部分进行排序
        mergeSort(arr, start, mid);
        mergeSort(arr, mid+1, end);

        // 到了这步,就是说明两边已经有序了
        // 注意:这里的边界要与前面递归的相同
        int leftIdx = start;
        int rightIdx = mid+1;
        int tmpIdx = 0;
        // 合并两个有序序列
        while (leftIdx <= mid && rightIdx <= end) {
            if (arr[leftIdx] <= arr[rightIdx]) {
                tmp[tmpIdx++] = arr[leftIdx++];
            } else {
                tmp[tmpIdx++] = arr[rightIdx++];
            }
        }
        while (leftIdx <= mid) {
            tmp[tmpIdx++] = arr[leftIdx++];
        }
        while (rightIdx <= end) {
            tmp[tmpIdx++] = arr[rightIdx++];
        }

        // 注意:这里不能直接用tmp把arr覆盖,因为每次都是一个规定的闭区间的操作
        for (int i = 0; i < tmpIdx; i++) {
            arr[i+start] = tmp[i];
        }
        return ;
    }
};

堆排序 (heap sort)

个人认为very nice 的一个排序

将数组当作一颗完全二叉树


大顶堆获得递增序列

  • 建堆(大顶堆)
    • 因为是完全二叉树,所以后一半的叶子节点可以直接掠过
    • 自底向上调整,可以保证大数值能不断向顶部跑
  • 不断交换首元素和未排序的末尾元素,使得最后元素称为未排序的最大元素
  • 每次交换完后调整堆:调整顶点
  • 堆的调整
    • 退出条件:该点的概念上的孩子均超出最大范围
    • 若左右孩子中有一个比当前点大,则交换
    • 并重新调整交换位置的子树
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        heapSort(nums);
        return nums;
    }

private:
    void modifyHeap(vector<int>& arr, int cur, int last) {
        // 这是一个完全二叉树
        // 该点在未排序范围内还有孩子
        while ((cur<<1)+1 <= last) {
            // 数组下标0~n-1
            // 所以左右孩子是 2*i+1;2*i+2
            int leftSon = (cur<<1)+1;
            int rightSon = (cur<<1)+2;
            int large = cur;

            // 分别判断左右是否还有孩子,且比父节点大
            if (leftSon <= last && arr[leftSon] > arr[large]) {
                large = leftSon;
            }
            if (rightSon <= last && arr[rightSon] > arr[large]) {
                large = rightSon;
            }
            // 所比父节点大
            if (large != cur) {
                // 交换,保证父节点大于孩子
                swap(arr[cur], arr[large]);
                // 交换的那个孩子节点继续调整
                cur = large;
            } else {
                // 无更新,则直接退出循环
                break;
            }
        }
    }

    void buildMaxHeap(vector<int>& arr) {
        int last = arr.size()-1;
        // 逆序初始化大顶堆
        // 完全二叉树,直接略过后一半的叶子节点
        for (int i = last/2; i >= 0; i--) {
            modifyHeap(arr, i, last);
        }
    }

    void heapSort(vector<int>& arr) {
        // 建堆
        buildMaxHeap(arr);

        for (int last = arr.size()-1; last >= 1; last--) {
            // 将最大值交换到最后
            swap(arr[0], arr[last]);
            // last位置有序不可用了
            // 从顶部开始调整
            modifyHeap(arr, 0, last-1);
        }

        return ;
    }

};

时间复杂度 O ( n ) {O(n)} O(n)

时间复杂度O(n) 的大都是一些特殊情况的排序,或者说是针对特殊情况的排序。不通用

如对数值型有位数特点的基数排序,数值可以作为下标的计数排序等等

计数排序(count sort)

根据下标统计出现次数

map写法 快于O(nlogn)

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        countSort(nums);
        return nums;
    }

private:
    void countSort(vector<int>& arr) {
        map<int, int> mp;
        for (auto it : arr) {
            mp[it] ++;
        }

        int sum = 0;
        for (auto [key, val] : mp) {
            fill(arr.begin()+sum, arr.begin()+sum+val, key);
            sum += val;
        }
    }

};

数组偏移量版

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        countSort(nums);
        return nums;
    }

private:
    void countSort(vector<int>& arr) {
        // 根据数据范围
        const int OFFEST = 5e4;
        vector<int> hash(2*OFFEST + 1);

        for (auto it : arr) {
            hash[it + OFFEST] ++;
        }

        int sum = 0;
        for (int i = 0; i <= 2*OFFEST; i++) {
            int key = i - OFFEST;
            int cnt = hash[i];
            fill(arr.begin()+sum, arr.begin()+sum+cnt, key);
            sum += cnt;
        }
    }

};

基数排序(radix sort)

按照个位十位百位一次排序,则从低位的有小到大通过计数排序后,最终的顺序均可以相对正确

二维数组版

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        radixSort(nums);
        return nums;
    }

private:
    void radixSort(vector<int>& arr) {
        // 根据数据范围
        const int OFFEST = 5e4;
        // 预处理,化为非负数
        for (auto &it : arr) {
            it += OFFEST;
        }

        // 获得最大值的长度
        int maxx = *max_element(arr.begin(), arr.end());
        int maxLen = to_string(maxx).length();

        vector<vector<int>> radix(10);
        // 预处理第一轮,构造起始的二维数组
        for (auto it : arr) {
            int cnt = it%10;
            radix[cnt].push_back(it);
        }
        // 个位处理过了从十位开始基数排序
        for (int len = 2; len <= maxLen; len++) {
            int preBase = pow(10, len-1);

            vector<vector<int>> nexRadix(10);
            for (int i = 0; i < 10; i++) {
                for (int j = 0; j < radix[i].size(); j++) {
                    // 消掉前一轮的低位,在进行计数排序
                    int cnt = radix[i][j]/preBase;
                    cnt %= 10;
                    nexRadix[cnt].push_back(radix[i][j]);
                }
            }

            radix = nexRadix;
        }

        int sum = 0;
        for (int i = 0; i < 10; i++) {
            // 将每组数据顺序拷贝到一维数组中
            copy(radix[i].begin(), radix[i].end(), arr.begin()+sum);
            sum += radix[i].size();
        }

        // 取消预处理的偏移
        for (auto &it : arr) {
            it -= OFFEST;
        }
    }

};

桶排序(bucket sort)

桶排序的关键在于如何建桶,然后再对桶内进行排序。个人认为有点鸡肋的感觉

  • 先从数据范围规定桶的大小

  • 选出最大最小值,是得一定程度的离散化‘

  • 算出桶的数量

  • 依次将数据根据容量入桶

  • 每个桶内分别排序

  • 合并所有桶

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        bucketSort(nums);
        return nums;
    }

private:
    void bucketSort(vector<int>& arr) {
        int n = arr.size();
        // 根据数据范围
        const int OFFEST = 5e4;
        // 预处理,化为非负数
        for (auto &it : arr) {
            it += OFFEST;
        }

        // 设定桶的大小
        const int BUCKETSIZE = 1000;

        // 计算桶数量
        int maxx = *max_element(arr.begin(), arr.end());
        int minn = *min_element(arr.begin(), arr.end());
        int bucketCount = (maxx - minn) / BUCKETSIZE + 1;
        vector<vector<int>> bucket(bucketCount);

        // 入桶
        for (auto it : arr) {
            // 计算在第几个桶中
            int cnt = (it-minn)/BUCKETSIZE;
            bucket[cnt].push_back(it);
        }

        // 每个桶自身排序
        for (int i = 0; i < bucketCount; i++) {
            sort(bucket[i].begin(), bucket[i].end());
        }

        // 将桶装回原数组
        int sum = 0;
        for (int i = 0; i < bucketCount; i++) {
            copy(bucket[i].begin(), bucket[i].end(), arr.begin()+sum);
            sum += bucket[i].size();
        }

        // 取消预处理的偏移
        for (auto &it : arr) {
            it -= OFFEST;
        }
    }

};

其他有趣的排序

猴子排序 (monkey sort)

随机打乱,那只要打乱次数的基数大,理论上就有可能排序成功

void monkeyShuffle(const int n) {
    vector<int> arr(n);
    iota(arr.begin(), arr.end(), 0);

    int cnt = 0;
    random_shuffle(arr.begin(), arr.end());
    while(!is_sorted(arr.begin(), arr.end()) ) {
        cnt++;
        printf("第%05d次猴子打乱\t", cnt);
        random_shuffle(arr.begin(), arr.end());
        for (int i = 0; i < n; i++) {
            cout << arr[i] << " \n"[i == n-1];
        }
    }

    return ;
}

睡眠排序(sleep sort)

顾名思义,拿个线程睡觉,谁先睡醒了就输出

let sleepSort = function (arr) {
    for (let num of arr) {
        setTimeout(() => {
            console.log(num);
        }, 1000 * num)
    }
}

let arr = [2, 4, 1, 5, 1, 5, 2, 6, 3];
sleepSort(arr);

煎饼排序 (pancake sort)

固定首位置,不断寻找未排序的最大值

先将[0, maxx] 翻转,使得0位置最大

在将0位置的最大值反转到尾部

力扣:969. 煎饼排序

class Solution {
public:
    vector<int> pancakeSort(vector<int>& arr) {
        int n = arr.size();

        vector<int> ans;
        for (int i = n-1; i >= 0; i--) {
            int idx = max_element(arr.begin(), arr.begin()+i+1) - arr.begin();
            reverse(arr.begin(), arr.begin()+idx+1);
            ans.push_back(idx+1);
            reverse(arr.begin(), arr.begin()+i+1);
            ans.push_back(i+1);
        }

        return ans;
    }
};



END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值