首页
/ 《Hello 算法》导读:算法无处不在——从查字典、理牌到找零,理解二分查找、插入排序与贪心算法

《Hello 算法》导读:算法无处不在——从查字典、理牌到找零,理解二分查找、插入排序与贪心算法

2026-09-07 14:09:10作者:钟日瑜

谈到"算法",很多人第一反应是复杂的数学公式。但实际上,大量算法的核心并不依赖高深数学,而是建立在简单的基础逻辑之上,它们就藏在日常生活的各种操作中。本文基于《Hello 算法》英文版开篇章节 en/docs/chapter_introduction/algorithms_are_everywhere.md,通过"查字典""整理扑克牌""超市找零"三个再熟悉不过的生活场景,带你零基础认识二分查找、插入排序与贪心算法三大经典主题,并结合仓库内真实的代码实现与专题文档,讲清它们背后的数据结构与算法本质。读完本文,你不仅能准确说出这些算法的原理、适用场景与复杂度特征,还能在仓库中找到对应的一键可运行源码,为后续系统学习打好"生活化"的认知基础。

一个反直觉的事实:你早就"会"算法了

在正式学习算法之前,章节首先抛出一个有趣的观点:很多人在不知不觉中已经学会并使用了不少算法,只是没有用术语称呼它们。

这一观点揭示了学习数据结构和算法的正确姿态——不要把这些概念当成陌生的、抽象的数学对象,而应从可感知的日常行为出发。文档指出,查字典这种小学生必备技能,本质就是著名的"二分查找"算法;而只要理解了"有序数组 + 逐步缩小范围"的关系,就同时建立起了数据结构(字典 ≈ 有序数组)与算法(查找过程 ≈ 二分查找)之间的桥梁。这正是《Hello 算法》全书反复强调的"从生活切入、从代码印证"的学习路径。

场景一:查字典 = 二分查找(Binary Search)

假设在英文词典中查找一个以字母 rr 开头的单词,我们通常不会从第一页逐页翻找,而是这样做:

  1. 把词典翻开到大约一半的位置,查看该页第一个单词,假设它以字母 mm 开头;
  2. 因为 rr 在字母表中排在 mm 之后,所以可以直接忽略前半本,把搜索范围缩小到后半本;
  3. 重复执行第 1、2 步,直到翻到以 rr 开头的单词所在的那一页。

二分查找之查字典全过程

这个过程正是对"搜索区间减半"思想的反复应用:每次比较中间位置,就能排除掉一半不可能包含目标的数据。从数据结构角度看,词典可视为一个已按字母排序的"数组";从算法角度看,这套查词动作就是著名的二分查找。

源码印证。 仓库 en/codes/c/chapter_searching/binary_search.c 给出了两种区间写法的完整实现:

/* Binary search (closed interval on both sides) */
int binarySearch(int *nums, int len, int target) {
    int i = 0, j = len - 1;
    while (i <= j) {
        int m = i + (j - i) / 2;      // 计算中点下标 m
        if (nums[m] < target)         // 目标在区间 [m+1, j]
            i = m + 1;
        else if (nums[m] > target)    // 目标在区间 [i, m-1]
            j = m - 1;
        else
            return m;                 // 找到目标,返回下标
    }
    return -1;                        // 未找到,返回 -1
}

文件还给出了另一种"左闭右开区间"写法 binarySearchLCRO,循环退出条件变为 i < j、收缩区间时执行 j = m。两种写法对应教材中经典的闭区间与左闭右开区间约定,可对照阅读(参见 binary_search.c)。该文件对应的专题文档 en/docs/chapter_searching/binary_search.md 会进一步推导其时间复杂度与适用前提(数据有序、支持随机访问)。理解这个例子后你会发现,二分查找并不神秘——它只是把人类查词典的本能动作形式化了。

场景二:整理扑克牌 = 插入排序(Insertion Sort)

打牌时把手中的牌按从小到大排列,通常的做法是:

  1. 把手中的牌划分为"有序区"与"无序区",初始时假设最左侧一张牌已经有序;
  2. 从无序区取出一张牌,插入到有序区中的正确位置;插入后,最左侧两张牌即处于有序状态;
  3. 重复执行第 2 步,直到所有牌都排好序。

扑克牌理牌排序过程

这就是"插入排序"的直观形态。文档特别指出,插入排序对规模较小的数据集非常高效,许多编程语言的内置排序实现内部就使用了插入排序

源码印证。en/codes/c/chapter_sorting/insertion_sort.c 中,可以看到与理牌动作一一对应的实现:

void insertionSort(int nums[], int size) {
    for (int i = 1; i < size; i++) {
        int base = nums[i], j = i - 1;
        // 将 base 插入到已排序区间 [0, i-1] 的正确位置
        while (j >= 0 && nums[j] > base) {
            nums[j + 1] = nums[j];    // 将元素右移腾出位置
            j--;
        }
        nums[j + 1] = base;           // 落位
    }
}

外层循环维护"已排序前缀 [0, i-1]",内层循环不断将更大的元素右移、为待插入元素 base 腾出空位——这正对应现实中"把某张牌抽出来,依次挪动前面的牌,再插回去"的整套动作。更完整的复杂度分析与优化讨论见 en/docs/chapter_sorting/insertion_sort.md

场景三:超市找零 = 贪心算法(Greedy Algorithm)

假设在超市消费了 6969 元,付给收银员 100100 元,则需要找回 3131 元。收银员会这样操作:

  1. 现有小于 3131 的可用面额为 115510102020
  2. 先取出其中最大的 2020,剩余应找 3120=1131 - 20 = 11
  3. 再从剩余可选面额中取出最大的 1010,剩余 1110=111 - 10 = 1
  4. 取出最大的 11,剩余 11=01 - 1 = 0
  5. 找零完成,解为 20+10+1=3120 + 10 + 1 = 31

超市找零的贪心过程

上述每一步都选择"当前看起来最优"的面额(取不大于剩余金额的最大者),这就是数据结构与算法中的贪心算法

源码印证与一个重要提醒。 仓库 en/codes/c/chapter_greedy/coin_change_greedy.c 给出了这一贪心策略的实现,其主程序同时演示了边界情况:当硬币面额为 {1, 5, 10, 20, 50, 100} 这类"彼此成倍数"的组合时,贪心可以保证得到全局最优解(例如找 186 元的最小硬币数);但当面额组合不合理(如 {1, 20, 50}60 元,或 {1, 49, 50}98 元)时,贪心反而给出较差的解——真正的局部最优不等于全局最优。这也解释了为什么贪心策略需要证明可行性,以及后续章节会引入动态规划等更普适的方法来求解此类问题。相关理论背景见 en/docs/chapter_greedy/greedy_algorithm.md

从生活到计算机:为什么要抽象"算法"?

从做饭到星际旅行,几乎所有问题的求解都蕴含着算法。而计算机的出现,让我们能够把数据结构存入内存,并编写代码驱动 CPU 与 GPU 去执行算法,从而把现实问题搬到计算机中,以更高效的方式解决各种复杂问题。这正是《Hello 算法》开篇要传达的认知闭环:

  • 数据结构回答"数据怎么组织"(如有序数组、链表、哈希表);
  • 算法回答"问题怎么求解"(如二分查找、排序、贪心策略);
  • 代码则把两者落地为可运行、可验证的步骤。

本书的每一章都延续这种"生活实例 → 图示推演 → 多语言代码"的写作范式。上文涉及的三个算法,仓库分别在 二分查找专题排序专题贪心专题 中展开;所有示例代码均可在 en/codes 目录下按语言(C、C++、Java、Python、Go、Rust、Swift、TypeScript、JavaScript 等)找到,例如 en/codes/python/chapter_searching/en/codes/java/chapter_sorting/en/codes/cpp/chapter_greedy/ 等,且多数直接附带可运行的 main 入口。

如果数据结构、算法、数组、二分查找等概念对你而言还只有模糊印象,不必担心——正如开篇所说,你早已在生活中熟练运用它们,接下来要做的,只是顺着这条从"生活常识"到"精确术语"的线索,系统进入数据结构和算法的世界。

登录后查看全文
热门项目推荐
相关项目推荐