快速了解桶排序

桶排序(箱排序)原理及其时间复杂度详解

排序充斥着我们的生活,比如站队、排队买票、考试排名、公司业绩排名、将电子邮件按时间排序、QQ 好友列表中的会员红名靠前,等等。

这里先举个例子,通过这个例子让我们接触第 1 个算法。

在某个期末考试中,老师要把大家的分数排序,比如有 5 个学生,分别考 5、9、5、1、6 分(满分 10 分),从大到小排序应该是 9、6、5、5、1,大家有没有办法写一段程序随机读取 5 个数,然后对它们排序呢?

看到这个问题,我们用 5 分钟想一下该怎么办。办法当然很多,这里使用桶排序的思想来处理。

我们找到 11 个桶,分别编号为 0-10,对应 0-10 分,如图 1 所示。

在这里插入图片描述

图 1准备 11 个桶并编号

接着我们把这些分数按照桶的编号放入桶中,如图 2 所示。

在这里插入图片描述

图 2 把分数都放入桶中

接着我们从最大编号的桶到最小编号的桶依次输出每个桶中的分数,分别是 9、6、5、5、1 了。是不是很轻松地完成排序了呢?这就是桶排序的思想。
什么是桶排序
桶排序,也叫作箱排序,是一个排序算法,也是所有排序算法中最快、最简单的排序算法。其中的思想是我们首先需要知道所有待排序元素的范围,然后需要有在这个范围内的同样数量的桶,接着把元素放到对应的桶中,最后按顺序输出。

这实际上是简易版的桶排序,我们想象一下,如果考试分数的范围是 0~100 万该怎么办?弄 100 万个桶吗?

实际上在这种情况下,一个桶并不总是放同一个元素,在很多时候一个桶里可能会放多个元素,这是不是与散列表有点相似呢?其实真正的桶排序和散列表有一样的原理。

除了对一个桶内的元素做链表存储,我们也有可能对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。
桶排序的实现
我们怎么在代码中实现桶排序呢?其实很简单,使用数组就好了。比如有 11 个桶,我们只需要声明一个长度为 11 的数组,然后每把一个元素往桶中放时,就把数组指定位置的值加 1,最终倒序输出数组的下标,数组每个位置的值为几就输出几次下标,这样就可以实现桶排序了。

前k个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:

输入: nums = [1], k = 1
输出: [1]

来源:力扣(LeetCode347)
链接:https://leetcode-cn.com/problems/top-k-frequent-elements

解决方法

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> counts; // 用来记录数字出现的个数
        int maxCounts = 0;
        for (const int &num : nums) {// 求解最大频率并记录每个数字出现的频率
            maxCounts = max(maxCounts, ++counts[num]);
        }

        vector<vector<int>> bucket(maxCounts + 1); // 用二维数组bucket[i][]存储出现i次的数字
        for (const auto &p : counts) { // 将出现相对频率的数字放在对应的桶里面
            bucket[p.second].push_back(p.first);
        }

        vector<int> ans;
        for (int i = maxCounts; i >= 0 && ans.size() < k; --i) {
            for (const int &num : bucket[i]) { // 从桶里面将频率高的(i)放入答案数组里面
                ans.push_back(num);
            }
            if (ans.size() == k) break;
        }
        return ans;
    }
};

桶排序的时间复杂度

桶排序实际上只需要遍历一遍所有的待排序元素,然后依次放入指定的位置。如果加上输出排序的时间,那么需要遍历所有的桶,时间复杂度就是 O(n+m),其中,n 为待排序的元素的个数,m 为桶的个数。这是相当快速的排序算法,但是对于空间的消耗来说有点太大了。

比如我们对 1、10、100、1000 这四个元素排序,那么我们需要长度为 1001 的数组用来排序,如果是对 1、1000、100000 排序呢?我们发现,当元素的跨度范围越大时,空间的浪费就越大,即使只有几个元素,但是这个范围才是空间的大小。所以桶排序的空间复杂度是 O(m),其中 m 为桶的个数,待排序元素分布越均匀,也就是说当元素能够非常均匀地填满所有的桶时,这个空间的利用率是最好的。不过这种情况并不多见,在多数情况下,数据并不会均匀地分布。

通过上面的性能分析,我们可以知道桶排序的特点,那就是速度快、简单,但是也有相应的弱点,那就是空间利用率低,如果数据跨度过大,则空间可能无法承受,或者说这些元素并不适合使用桶排序算法。

桶排序的适用场景

桶排序的适用场景非常明了,那就是在数据分布相对比较均匀或者数据跨度范围并不是很大时,排序的速度还是相当快且简单的。

但是当数据跨度过大时,这个空间消耗就会很大;如果数值的范围特别大,那么对空间消耗的代价肯定也是不切实际的,所以这个算法还有一定的局限性。同样,由于时间复杂度为 O(n+m),如果 m 比 n 大太多,则从时间上来说,性能也并不是很好。

但是实际上在使用桶排序的过程中,我们会使用类似散列表的方式去实现,这时的空间利用率会高很多,同时时间复杂度会有一定的提升,但是效率还不错。

我们在开发过程中,除了对一些要求特别高并且数据分布较为均匀的情况使用桶排序,还是很少使用桶排序的,所以即使桶排序很简单、很快,我们也很少使用它。

桶排序更多地被用于一些特定的环境,比如数据范围较为局限或者有一些特定的要求,比如需要通过哈希映射快速获取某些值、需要统计每个数的数量。但是这一切都需要确认数据的范围,如果范围太大,就需要巧妙地解决这个问题或者使用其他算法了。
转载: http://data.biancheng.net/view/115.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值