6
头图

前言

在我重新复习我创建的代码段集合网站,我复习到了桶排序算法的实现,它的代码如下所示:

const bucketSort = (arr, size = 5) => {
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  const buckets = Array.from(
    { length: Math.floor((max - min) / size) + 1 },
    () => []
  );
  arr.forEach(val => {
    buckets[Math.floor((val - min) / size)].push(val);
  });
  return buckets.reduce((acc, b) => [...acc, ...b.sort((a, b) => a - b)], []);
};

咋一看代码实现思路,我并不是很清楚为什么这样写。我产生了如下几个疑惑点:

  1. 为什么要取最小值和最大值?
  2. Math.floor((max - min) / size) + 1,这是一个公式,代表什么意义?并且这个公式如何得来的?
  3. Math.floor((val - min) / size),这也是一个公式,这个又是指什么?
特别说明:代码段集合网站是我为了收集来自网上又或者是平时开发的一些简短的代码片段,方便自己在用到的时候可以翻阅查找,代码片段包括但不限于html,css,js,node.js,ts,php,git等。其中,对于react和vue,我可能会分开,例如react代码段是一个单独的网站,vue目前还没有总结到。目前这一系列代码片段集合已经是很庞大了,可以看到,我基本上每天都会添加一个代码段,目前包含有typescript代码段,html代码段,css代码段,js代码段,node.js代码段,git代码段。

带着这几个疑问,我重新复习了一下桶排序算法。

桶排序的定义

桶排序(Bucket Sort)是一种基于分配和排序的排序算法,它的基本思想是将元素分配到多个桶中,然后对每个桶内的元素进行排序,最后将所有桶中的元素按顺序合并起来。

桶排序的基本思想

桶排序分成3个思想步骤,如下所示:

  1. 划分桶:根据待排序数组中元素的范围,将数组的元素分配到多个桶中。每个桶可以容纳一定范围的值。
  2. 排序桶内的元素:对每个桶内的元素进行排序,常用的排序方法可以是插入排序或其他适合桶内元素个数较少的排序算法。
  3. 合并桶:将每个桶排序后的元素按顺序依次取出,得到最终排序的结果。

桶排序的特点

  • 桶排序的时间复杂度与元素的分布情况密切相关。如果元素分布均匀,桶排序的时间复杂度接近 O(n),但如果元素分布极不均匀,桶排序的效率会退化为 O(n^2)(相当于每个桶内的元素都比较多,排序起来比较耗时)。
  • 桶排序需要额外的空间来存储桶,因此空间复杂度为 O(n)

根据基本思想和步骤来分析前面的代码

此时再来看前面的实现代码,我们可以根据3个步骤将代码划分成3个部分来分析:

划分桶。

首先我们需要划分桶,划分桶包含了创建桶和分配元素到桶中。即:

const bucketSort = (arr, size = 5) => {
  // 第一步:划分桶,包含创建桶和分配元素到每一个桶中
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  // 根据最小值和最大值来创建桶的数量
  const buckets = Array.from(
    { length: Math.floor((max - min) / size) + 1 },
    () => []
  );
  // 分配元素到桶中
  arr.forEach(val => {
    buckets[Math.floor((val - min) / size)].push(val);
  });
};

排序桶与合并桶

前面的代码实际上将排序桶与合并桶给融合到一起了,也就是说我们先使用js提供的sort方法对桶中的元素进行排序,然后再使用reduce创建一个新的数组,并将桶中的元素合并到新的数组中并返回。

const bucketSort = (arr, size = 5) => {
  //...
  // 排序桶与合并桶
  return buckets.reduce((acc, b) => [...acc, ...b.sort((a, b) => a - b)], []);
};

接下来我们来看以上代码的时间复杂度和空间复杂度。

时间复杂度和空间复杂度

  • 时间复杂度

    • 分配元素到桶的时间复杂度是 O(n),其中 n 是元素的数量。
    • 每个桶内的排序时间复杂度,假设每个桶内的元素大致均匀分布,每个桶的最大元素数为 O(n / k),其中 k 是桶的数量。如果每个桶内的排序使用了合适的排序算法(比如插入排序),其时间复杂度是 O((n / k)²),因此整体的时间复杂度为 O(n + k * (n / k)²),在最优情况下接近 O(n)
  • 空间复杂度

    • 空间复杂度是 O(n),主要用于存储桶。

好了,以上只是分析了代码的整体大概意思,接下来还要解决前面我们提到的3个问题。

解答疑问

为什么要有最大值与最小值

首先我们来看第一个问题。对于一个待排序的数组,我们会将每个元素添加到桶中,而桶存储的元素就需要有一个区间范围,这个区间范围就是由数组元素中的最小值和最大值以及桶的大小来决定的。

计算桶的数量的公式是如何得来的

公式 桶的数量 = (max-min) / size 用于计算桶排序中所需的桶的总数量。这个公式的含义是通过已知的最小值 (min)、最大值 (max) 和桶的大小 (size),来确定将整个数据范围划分成多少个桶。

在桶排序中,目标是将待排序的元素分配到多个桶里,桶的范围是根据 minmaxsize 来确定的。每个桶会存储一个区间范围内的元素,桶的大小 (size) 决定了每个桶的范围。

为了合理地将整个数据范围 [min, max] 划分成多个桶,我们需要知道:

  • 最小值 (min) 和最大值 (max) 之间的差距,即整个数据的跨度。
  • 每个桶所代表的范围大小,也就是桶的大小 size

假设我们希望将整个数据范围 [min, max] 平均地划分成若干个桶,并且每个桶的大小都相等。每个桶的大小由 size 决定。

  • 如果 min 是数组中的最小值,max 是数组中的最大值,整个数据的范围就是 max - min
  • 每个桶的宽度或大小是 size

因此,桶的数量就是将整个数据范围 max - min 按照每个桶的大小 size 来划分的数量。

为了计算桶的数量,我们将数据范围 (max-min) 除以每个桶的大小 size,得到的结果就是需要的桶数:

桶的数量 = max - min / size

以上公式每个部分代表的含义:

  • max - min:表示整个数据的范围或跨度,即所有元素的值之间的差距。
  • size:表示每个桶所能容纳的值的大小。它决定了桶的宽度(或桶的区间范围)。
  • 桶的数量:这个公式给出了需要多少个桶来覆盖整个数据的范围。

我们以示例来推导这个公式,如下:

假设有一个数组 arr = [0.1, 0.3, 0.45, 0.6, 0.8, 0.9],最小值 min = 0.1,最大值 max = 0.9,桶的大小 size = 0.2

  • 数据范围:max - min = 0.9 - 0.1 = 0.8
  • 每个桶的大小:size = 0.2
  • 所需桶的数量:桶的数量 = (max-min) / size = 0.8 / 0.2 = 4

所以,我们需要 4 个桶来覆盖整个数据范围 [0.1, 0.9],并且这 4 个桶的范围可以分别是:

  • 桶 1:[0.1, 0.3)
  • 桶 2:[0.3, 0.5)
  • 桶 3:[0.5, 0.7)
  • 桶 4:[0.7, 0.9]

对于创建桶的数量,我们还需要注意以下2点:

  • 如果 max - min 无法被 size 整除,通常会向上取整。比如如果 (max-min) = 0.95size = 0.3,那么桶数应该是 Math.ceil(0.95 / 0.3) = Math.ceil(3.1667) = 4
  • 如果 size 大于 (max-min),则只需要一个桶即可覆盖整个数据范围。

根据以上分析,我们就明白了原来公式 桶的数量 = (max-min) / size 是通过将数据的总范围(max - min)除以每个桶的大小(size)来得出的。这个公式帮助我们估算出需要多少个桶来覆盖整个数据范围。如果这个除法结果是一个非整数,通常会取上整值(即使用 Math.ceil()),以确保每个数据点都有一个桶可以容纳。

分配元素的公式如何得来的

Math.floor((val - min) / size) 的目的是确定数组中元素 val 应该被分配到哪个桶中。桶排序的核心思想是将待排序的元素分到不同的桶里。为了做到这一点,我们需要计算每个元素应该放到哪个桶。

  • 假设我们已经知道了待排序数组的最小值 min 和最大值 max,以及每个桶的大小(即桶的范围)size。根据这些信息,我们希望能够将待排序的元素均匀地分配到多个桶中。

我们需要先明确桶的范围。假设 min 是数组中的最小值,max 是数组中的最大值,桶的大小是 size。桶的范围可以理解为一个区间,它表示每个桶容纳的值的范围。

  • 比如:如果 min = 0max = 100,而我们设置 size = 10,那么我们就可以把整个区间 [0, 100] 分成 10 个桶,桶的范围分别是:

    • 桶 1(索引值为0,后面依此类推): [0, 9]
    • 桶 2: [10, 19]
    • 桶 3: [20, 29]
    • 桶 10: [90, 99]

我们希望能够知道每个元素应该放入哪个桶。为此,我们需要基于该元素的值来计算它属于哪个桶。

  • 每个桶的大小是 size,桶的索引从 0 开始。
  • 我们需要将元素值按桶的大小进行归类,使得元素值小于 size 的元素进入桶 0,元素值介于 size2 * size 之间的元素进入桶 1,依此类推。

对于每个元素 val,我们可以通过以下步骤来确定它的桶索引:

  1. 元素值相对最小值的偏移:首先,我们需要将 val 的值映射到一个与桶范围相关的值域。因为桶的划分是基于相对最小值 min 的,所以首先计算 val - min,得到元素值相对于最小值的偏移量。
  2. 将偏移量除以桶的大小:接下来,我们通过 size 来划分偏移量,使其映射到对应的桶。用 (val - min) / size 来计算元素 val 应该进入的桶的一个“浮动”位置(即它落在桶的哪个位置)。这个值表示该元素在桶中可能的位置。
  3. 向下取整:由于桶的索引是离散的整数值,我们需要对计算结果取整。使用 Math.floor() 来向下取整,得到最接近的桶索引。

这里还有一点我们需要搞明白,那就是为什么需要使用 Math.floor()而不是Math.ceil或者Math.round

  • Math.floor() 将浮动的位置向下取整,确保所有元素都被分配到正确的桶中。例如,假设桶的大小是 10,元素值 val 为 25,min 为 0,那么 (val - min) / size = 25 / 10 = 2.5Math.floor(2.5) = 2,因此该元素会被分配到桶索引为 2 的桶中,即 [20, 29] 的桶。

因此,公式 Math.floor((val - min) / size) 用于计算了元素 val 应该被分配到哪个桶中。

  • val - min:将元素值从最小值 min 归一化为一个相对偏移量。
  • / size:将这个偏移量映射到桶的大小上,得到桶的“浮动位置”。
  • Math.floor():将该浮动位置向下取整,得到实际的桶索引。

我们来看一个示例,如下所示:

假设我们有一个待排序数组:arr = [0.42, 0.32, 0.23, 0.56, 0.78, 0.91, 0.12],最小值 min = 0,最大值 max = 1,桶的大小 size = 0.2,那么桶的划分如下:

  • 桶 0: [0, 0.2)
  • 桶 1: [0.2, 0.4)
  • 桶 2: [0.4, 0.6)
  • 桶 3: [0.6, 0.8)
  • 桶 4: [0.8, 1.0)

接着,我们根据公式 Math.floor((val - min) / size) 来确定每个元素应该放入哪个桶:

  • 对于 arr[0] = 0.42(0.42 - 0) / 0.2 = 2.1Math.floor(2.1) = 2,所以它应该放入桶 2。
  • 对于 arr[1] = 0.32(0.32 - 0) / 0.2 = 1.6Math.floor(1.6) = 1,所以它应该放入桶 1。
  • 对于 arr[2] = 0.23(0.23 - 0) / 0.2 = 1.15Math.floor(1.15) = 1,所以它应该放入桶 1。
  • 对于 arr[3] = 0.56(0.56 - 0) / 0.2 = 2.8Math.floor(2.8) = 2,所以它应该放入桶 2。
  • 对于 arr[4] = 0.78(0.78 - 0) / 0.2 = 3.9Math.floor(3.9) = 3,所以它应该放入桶 3。
  • 对于 arr[5] = 0.91(0.91 - 0) / 0.2 = 4.55Math.floor(4.55) = 4,所以它应该放入桶 4。
  • 对于 arr[6] = 0.12(0.12 - 0) / 0.2 = 0.6Math.floor(0.6) = 0,所以它应该放入桶 0。

根据以上分析,总算知道了公式 Math.floor((val - min) / size)的含义和由来,它的含义是通过将元素值归一化到桶的范围,并将其映射到具体的桶索引,实现了桶排序中元素的桶分配。

将以上的所有分析整合起来,就得到了我们最终的桶排序算法的实现。

总结

桶排序算法主要包含划分桶,分配桶,排序桶以及合并桶,其中排序桶可以与合并桶整合到一起。并且我们还需要知道划分桶,我们需要根据最大值与最小值和桶的大小来确定范围,然后根据最小值与元素以及桶大小来决定元素是分配到哪个桶区间。


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。