【算法】桶排序

180 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情

一、前言

桶排序 (Bucket sort) 或所谓的箱排序 :工作的原理是将数组分到有限数量的桶子里。

  • 每个桶子再个别排序(插入排序)。
  • 桶排序是 鸽巢排序 的一种归纳结果。

桶排序-2022-08-1217-30-39.png

桶排序,是一个稳定排序: 适用于数据是均匀分布的情况,这样可以让分布到各个桶内的元素数量相当。

  • 最快情况:数据可以均匀分配到每一个桶中。
  • 最慢情况:数据被分配到同一个桶中。
# 举个栗子,有 100 200 300 ... 10000,总共 100 个且均匀分布
# 如果使用计数排序,则需要创建辅助数组长度为 10000
# 所以,面对数据是均匀分布的情况,使用桶排序

代码实现如下:

桶排序-2022-08-1217-30-40.png

public class BucketSort {
    private int bucketSize;

    public BucketSort(int bucketSize) {
        this.bucketSize = bucketSize;
    }

    // Time(avg): O(n+k), Time(worst): O(n^2), Space: O(n)
    public void sort(int[] arr) {
        if (arr == null || arr.length == 0) return;
        int max = arr[0], min = arr[0];
        // 计算最大值和最小值
        for (int num : arr) {
            if (num > max) max = num;
            if (num < min) min = num;
        }

        // 桶的数量 = 数组长度 / 桶大小
        int bucketCount = arr.length / bucketSize;
        List<List<Integer>> buckets = new ArrayList<>(bucketCount);
        for (int i = 0; i < bucketCount; ++i)
            buckets.add(new ArrayList<>());

        for (int num : arr) {
            // 计算放在哪个桶中
            int idx = (int) ((num - min) / (max - min + 1.0) * bucketCount);
            buckets.get(idx).add(num);
        }

        int idx = 0;
        for (List<Integer> bucket : buckets) {
            insertionSort(bucket);   // 桶内:插入排序
            for (int num : bucket) { // 再放入结果中
                arr[idx++] = num;
            }
        }
    }

    private void insertionSort(List<Integer> arr) {
        if (arr == null || arr.size() == 0) return;
        for (int i = 1; i < arr.size(); ++i) {
            int cur = arr.get(i);
            int j = i - 1;
            while (j >= 0 && arr.get(j) > cur) { // 先找一个合适的位置
                arr.set(j + 1, arr.get(j));
                --j;
            }
            // 找到合适的位置,放下
            arr.set(j + 1, cur);
        }
    }
}



二、题目

(1)前 K 个高频数字(中)

LeetCode 347

题干分析

这个题目说的是,给你一个不为空的整数数组,你要返回前 K 个出现频率最高的数字。假设给你的 K 总是有效的,也就是数组中一定包含至少 K 个不同的数字。

# 比如说,给你的数组是:
1, 2, 1, 2, 1, 4

# 给你的 K 是 2:
K = 2

# 在这个数组中,数字 1 出现了 3 次,数字 2 出现 2 次,数字 4 只出现 1 次。
# 因此,出现频率最高的两个数字是:
[1, 2]

注意,返回结果中,数字的顺序不重要,也就是说这里返回 [2, 1] 也是对的。

思路解法

思路方法有三: 暴力法(哈希表存储)、快速选择 和 桶排序

方法三:桶排序

  1. 用哈希表:统计数字出现的次数。
  2. 定义桶:把数字放入桶下。
  3. 从后向前遍历桶,取 k 个数组即可。

桶排序-2022-08-1217-30-42.png

// 方法三: 桶排序
// Time: O(n), Space: O(n), Faster: 97.25%
public int[] topKFrequentBucketSort(int[] nums, int k) {
    // 1. 哈希表:统计数字出现的次数
    Map<Integer, Integer> freqMap = new HashMap<>();
    for (int num: nums) {
        int freq = freqMap.getOrDefault(num, 0);
        freqMap.put(num, freq + 1);
    }

    // 2. 定义桶
    List<List<Integer>> buckets = new ArrayList<>(nums.length + 1);
    for (int i = 0; i <= nums.length; ++i) buckets.add(new ArrayList<>());
    for (Map.Entry<Integer, Integer> e: freqMap.entrySet()) {
        buckets.get(e.getValue()).add(e.getKey());
    }

    // 3. 从后往前取 k 个元素。
    List<Integer> result = new ArrayList<>();
    for (int i = buckets.size() - 1; i >= 0 && k > 0; --i) {
        List<Integer> bucket = buckets.get(i);
        for (int j = 0; j < bucket.size() && k > 0; ++j) {
            result.add(bucket.get(j));
            --k;
        }
    }
    int[] ans = new int[result.size()];
    for (int i = 0; i < result.size(); ++i) {
        ans[i] = result.get(i);
    }
    return ans;
}