diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index d9703dea6..df606817a 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -12,9 +12,11 @@ comments: true 最直接地,考虑借助「元素入堆」方法,先建立一个空堆,**再将列表元素依次入堆即可**。 +设元素数量为 $n$ ,则最后一个元素入堆的时间复杂度为 $O(\log n)$ ,在依次入堆时,堆的平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。 + ### 基于堆化操作实现 -然而,**存在一种更加高效的建堆方法**。设元素数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。 +有趣的是,存在一种更加高效的建堆方法,时间复杂度可以达到 $O(n)$ 。我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。 === "Java" @@ -155,25 +157,25 @@ comments: true ## 8.2.2. 复杂度分析 -对于第一种建堆方法,元素入堆的时间复杂度为 $O(\log n)$ ,而平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。 - -那么,第二种建堆方法的时间复杂度是多少呢?我们来展开推算一下。 +第二种建堆方法的时间复杂度为什么是 $O(n)$ 呢?我们来展开推算一下。 - 完全二叉树中,设结点总数为 $n$ ,则叶结点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此在排除叶结点后,需要堆化结点数量为 $(n - 1)/2$ ,即为 $O(n)$ ; - 从顶至底堆化中,每个结点最多堆化至叶结点,因此最大迭代次数为二叉树高度 $O(\log n)$ ; -将上述两者相乘,可得时间复杂度为 $O(n \log n)$ 。然而,该估算结果仍不够准确,因为我们没有考虑到 **二叉树底层结点远多于顶层结点** 的性质。 +将上述两者相乘,可得时间复杂度为 $O(n \log n)$ 。这个估算结果不够准确,因为我们没有考虑到 **二叉树底层结点远多于顶层结点** 的性质。 -下面我们来尝试展开计算。为了减小计算难度,我们假设树是一个「完美二叉树」,该假设不会影响计算结果的正确性。设二叉树(即堆)结点数量为 $n$ ,树高度为 $h$ 。上文提到,**结点堆化最大迭代次数等于该结点到叶结点的距离,而这正是“结点高度”**。因此,我们将各层的“结点数量 $\times$ 结点高度”求和,即可得到所有结点的堆化的迭代次数总和。 - -$$ -T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 -$$ +下面我们来展开计算。为了减小计算难度,我们假设树是一个「完美二叉树」,该假设不会影响计算结果的正确性。设二叉树(即堆)结点数量为 $n$ ,树高度为 $h$ 。上文提到,**结点堆化最大迭代次数等于该结点到叶结点的距离,而这正是“结点高度”**。 ![完美二叉树的各层结点数量](build_heap.assets/heapify_operations_count.png)
Fig. 完美二叉树的各层结点数量
+因此,我们将各层的“结点数量 $\times$ 结点高度”求和,即可得到 **所有结点的堆化的迭代次数总和**。 + +$$ +T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 +$$ + 化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得 $$ diff --git a/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md new file mode 100644 index 000000000..4275706e8 --- /dev/null +++ b/chapter_sorting/bucket_sort.md @@ -0,0 +1,16 @@ +## 拓展到桶排序 + +如果我们把上述 `bucket` 中的每个索引想象成一个桶,那么可以将计数排序理解为把 $n$ 个元素分配到对应的桶中,再根据桶与桶之间天然的有序性来实现排序。 + +以上解读便是「桶排序 Bucket Sort」的核心思想。具体地,桶排序考虑将 $n$ 个元素根据大小范围均匀地分配到 $k$ 个桶中,由于桶之间是有序的,**因此仅需在每个桶内部执行排序**,最终按照桶之间的大小关系将元素依次排列,即可得到排序结果。 + +假设使用「快速排序」来排序各个桶内的元素,每个桶内元素数量为 $\frac{n}{k}$ ,则排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 接近 $n$ 时,时间复杂度则趋向于 $O(n)$** 。 + +(图) + +理论上桶排序的时间复杂度是 $O(n)$ ,**但前提是需要将元素均匀分配到各个桶中**,而这是不太容易做到的。假设我们要把淘宝中的 $100$ 万件商品根据价格范围平均分配到 $100$ 个桶中,由于商品价格不是均匀分布的,比如 $1$ ~ $100$ 元的商品非常多、$1$ 万元以上的商品非常少等,因此难以简单地设定各个桶的价格分界线。解决方案有: + +- 先初步设置一个分界线,将元素分配完后,**把元素较多的桶继续划分为多个桶**,直至每个桶内元素数量合理为止;该做法一般使用递归实现; +- 如果我们提前知道商品价格的概率分布,**则可以根据已知分布来设置每个桶的价格分界线**;值得说明的是,数据分布不一定需要 case-by-case 地统计,有时可以采用一些常见分布来近似,例如自然界的正态分布; + +(图) \ No newline at end of file diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index 73f2b526d..6c0b32b85 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -294,7 +294,7 @@ comments: true 举个例子,给定数组 `[0, 0, 0, 0, 1]` ,如果先“从左向右查找”,哨兵划分后数组为 `[1, 0, 0, 0, 0]` ,这个结果是不对的。 - 再往深想一步,如果我们选择 `nums[right]` 为基准数,那么正好反过来,必须先“从左往右查找”。 + 再深想一步,如果我们选择 `nums[right]` 为基准数,那么正好反过来,必须先“从左往右查找”。 ## 11.4.1. 算法流程