370,103 个单词的排序、哈希与草图算法
Sorting, hashing, and sketches on 370,103 words

原始链接: https://stochastic.blog/sorting-hashing-and-sketches-on-370-103-words/

这篇文章探讨了核心 Python 数据结构与算法在实际应用中的表现。作者使用了一份包含 370,103 个英语单词的清洗数据集,通过评估各种排序方法、哈希策略以及概率算法(如 HyperLogLog)来分析它们在时间和内存方面的效率。 文章将大 O 表示法、大 Θ 表示法及摊还分析等理论概念与具体实现联系起来。通过对排序(Timsort)和列表操作等基准测试,文章展示了选择合适工具的重要性;例如,高效的列表末尾追加与代价高昂的索引零位插入之间的对比,突显了渐进分析的意义。 最终,本文旨在成为高性能搜索引擎底层机制的“实地指南”。通过从零开始构建这些结构,作者清晰地展现了现代系统如何管理大数据集,证明了即便像 HyperLogLog 这样简单高效的算法,也能以极少的资源实现极高的准确性。

一位 Hacker News 的评论者强烈批评了一篇题为《关于 370,103 个单词的排序、哈希和草图算法》的博文,并驳斥了作者的技术分析。 该评论者认为,作者混淆了经验性的耗时测量与形式化的时间复杂度。具体来说,评论者指出了两个主要缺陷: 1. **误解复杂度**:作者仅根据单一输入的基准测试就推断时间复杂度,这在方法论上是站不住脚的。 2. **术语不严谨**:作者将列表追加操作称为“均摊 O(1)”却未给出理由或证明,未能区分真正的均摊复杂度和常数时间操作。 评论者总结称,该文章缺乏严谨的分析,且对算法基础的理解也很匮乏,导致文章不可信,因此决定不再阅读。
相关文章

原文

In Post 2 we built the foundations with Python lists, dicts, sets, and recursion, learning how the language's core containers behave under load. Now we put those tools through their paces on a real dataset: 370,103 English words, one per line, drawn from the dwyl/english-words repository. By the end of this post we will have sorted them six ways, hashed them into four different structures, and sketched them with four probabilistic algorithms, all while tracking what each approach costs in time and memory. The headline: HyperLogLog estimates the vocabulary size with just 2.71 percent error using only 4,096 registers.

Think of this post as a field guide to the algorithms that keep modern systems fast. Every time you type a query into a search box, the engine is sorting, hashing, and sketching behind the scenes. We will build each of these mechanisms from scratch, measure them on real words, and see which ones earn their complexity.

The dataset

The word list arrives as a single column of lowercase strings, about 21.63 MB in memory. The raw file contains duplicates and missing values; cleaning leaves 370,103 unique words. Two missing values appear in the raw file, which we strip and clean away. The vocabulary settles at 370,103 unique words, with no duplicate rows to worry about. Word lengths skew right with a long tail past 15 characters, and the out-of-vocabulary rate on a 20 percent holdout hits 1.0, meaning every word in the test split is unseen. That last number matters: it tells us membership structures will face nothing but novel queries.

Figure 1 shows the word-length distribution for the cleaned vocabulary.

Word-length frequency curve

Figure 1: Word lengths cluster between 3 and 10 characters and fall off steadily on a log scale in the long tail.

This shape drives our later choices: the long tail means tries will have deep paths, and the high OOV rate means hash tables will face constant misses.

Complexity

Before we sort anything, we need a language for talking about cost. Big-O notation gives an upper bound on growth, Big-Theta notation pins the exact asymptotic class, and amortized analysis measures cost across a sequence of operations rather than a single call. We measure all three on the operations the rest of the post uses.

The built-in sort on 50,000 words takes 0.0011 seconds, on 100,000 it takes 0.0022, and on 200,000 it takes 0.0053. Doubling the input roughly doubles the time, the signature of an n log n algorithm. Timsort, Python's default, sits in Big-Theta(n log n). List append tells a different story: 200,000 appends complete in 0.0071 seconds total, about 4e-08 seconds per operation, which is O(1) amortized. List insert at position zero is the cautionary tale. We only insert 5,000 items, forty times fewer than the appends, yet the operation takes 0.0019 seconds, still slower per operation at 3.8e-07 seconds. That gap is the whole lesson: asymptotic analysis predicts real behavior when the constants stay honest.

Sorting

Binary search needs a sorted array, so we start there. We sample 5,000 words with a fixed seed, sort them, and confirm that binary search finds a known word at index 1234 while returning -1 for a nonsense string. Then we implement the classic sorts by hand.

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)
联系我们 contact @ memedata.com