news 2026/9/17 13:48:40

排序、搜索与算法设计范式精讲:maths-cs-ai-compendium 第14章实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
排序、搜索与算法设计范式精讲:maths-cs-ai-compendium 第14章实战指南

排序、搜索与算法设计范式精讲:maths-cs-ai-compendium 第14章实战指南

【免费下载链接】maths-cs-ai-compendiumBecome a cracked AI/ML researcher/engineer with this unconventional textbook covering maths, computing, and ML with intuition.项目地址: https://gitcode.com/GitHub_Trending/mat/maths-cs-ai-compendium

排序与搜索是计算机科学中最基础的算法操作,也是算法面试的高频主战场。本文以开源教科书Maths, CS & AI Compendium的 第14章第05节「Sorting and Search」 为主体,系统讲解七大经典排序算法的复杂度与稳定性、二分查找的四种进阶形态、以及贪心、动态规划、回溯三大设计范式的识别方法与可运行代码。读完本文,你将掌握一套"识别范式 → 套用模板 → 规避陷阱"的完整解题方法论,能够直接应对从 Easy 到 Hard 的排序搜索类面试题。

每一类算法都会给出完整、可直接运行的 Python 实现、复杂度分析、最容易犯的边界错误,以及配套的课后练习清单,与本仓库 MCP 服务(mcp/src/index.ts)暴露的read_section检索能力呼应,方便读者在本地仓库中随时回溯原文。


排序算法:复杂度、稳定性与比较排序下界

排序是计算机科学中被研究得最透彻的问题之一。理解排序算法是建立递归、分治与复杂度分析直觉的最佳起点。下表汇总了七种经典排序算法在最好、平均、最坏三种情况下的时间复杂度和空间复杂度:

算法最好平均最坏空间稳定?
冒泡排序$O(n)$$O(n^2)$$O(n^2)$$O(1)$
插入排序$O(n)$$O(n^2)$$O(n^2)$$O(1)$
归并排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(n)$
快速排序$O(n \log n)$$O(n \log n)$$O(n^2)$$O(\log n)$
堆排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(1)$
计数排序$O(n + k)$$O(n + k)$$O(n + k)$$O(k)$
基数排序$O(d(n + k))$$O(d(n + k))$$O(d(n + k))$$O(n + k)$

稳定(stable)意味着相等元素的相对顺序在排序后保持不变。这在多关键字排序时至关重要:例如先按姓名排序、再按分数排序,若第二个排序不稳定,第一关键字的顺序就会被破坏。归并排序、插入排序、计数排序、基数排序是稳定的;快速排序与堆排序不稳定。

比较排序的下界是 $\Omega(n \log n)$。证明思路用到决策树:任何比较排序都必须能够区分所有 $n!$ 种排列,因此至少需要 $\log_2(n!) = \Omega(n \log n)$ 次比较。计数排序和基数排序之所以能突破这个下界,正是因为它们不比较元素——而是利用键值的数值结构直接定位。这一下界分析依赖的离散数学与复杂度基础,参见本仓库 第13章第01节「Discrete Maths」 与 第14章第00节「Foundations」。

归并排序:稳定的 $O(n \log n)$ 分治

把数组对半切分,递归排序两个子数组,再合并两个有序半区。无论输入如何,复杂度都是 $O(n \log n)$,代价是需要 $O(n)$ 额外空间。

def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: # <= for stability result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result

关键陷阱:合并时如果使用<而非<=,相等元素会来自右半区排在左半区之前,破坏稳定性。<=保证相等时优先取左半区元素,从而维持原数组的相对顺序。合并过程本身是两个有序序列的双指针归并,这一技巧在后续双指针模式中还会反复出现。

快速排序:平均 $O(n \log n)$、最坏 $O(n^2)$ 的原地分治

选取一个基准(pivot),把元素划分为"小于基准"和"大于基准"两个分区,递归排序每个分区。平均 $O(n \log n)$;当基准始终是最大或最小元素时退化为 $O(n^2)$。

def quicksort(arr, lo=0, hi=None): if hi is None: hi = len(arr) - 1 if lo >= hi: return pivot_idx = partition(arr, lo, hi) quicksort(arr, lo, pivot_idx - 1) quicksort(arr, pivot_idx + 1, hi) def partition(arr, lo, hi): pivot = arr[hi] # Lomuto: pivot is last element i = lo for j in range(lo, hi): if arr[j] < pivot: arr[i], arr[j] = arr[j], arr[i] i += 1 arr[i], arr[hi] = arr[hi], arr[i] return i

上面的partition使用Lomuto 分区方案:以最后一个元素为基准,i维护"小于基准"区域的边界,j扫描剩余元素,遇到小于基准的元素就与边界处交换。快排的最坏情况 $O(n^2)$ 出现在已排序数组配合首/尾基准时——此时每次分区都极度不均衡。

基准选择策略:取最后一个元素(最简单,但对已排序输入很差)、随机选取(期望复杂度 $O(n \log n)$)、三数取中 median-of-three(工程上最实用的选择)。面试中建议优先讨论随机基准,以避免最坏情况的纠缠。空间复杂度 $O(\log n)$ 来自递归栈深度,这也是第14章第00节强调"递归调用栈也要计入空间复杂度"的典型例子。

计数排序:突破 $O(n \log n)$ 的非比较排序

当所有值都是已知范围 $[0, k)$ 内的整数时,统计每个值的出现次数再重建数组,时间复杂度为 $O(n + k)$。它不是基于比较的,因此可以击败 $O(n \log n)$ 下界。

def counting_sort(arr, k): count = [0] * k for x in arr: count[x] += 1 result = [] for val in range(k): result.extend([val] * count[val]) return result

适用时机:当范围 $k$ 与 $n$ 同量级时,$k = O(n)$,整体为线性 $O(n)$。若 $k \gg n$(例如在 $[0, 10^9]$ 范围内排序 10 个数),计数数组会浪费大量内存,此时应改用比较排序。注意上面是"不稳定"的简化实现——如需稳定版本,需要累积计数(cumulative counts)后从后向前放置元素。基数排序则对每一位执行稳定计数排序,复杂度为 $O(d(n + k))$,其中 $d$ 是位数。

工程视角:语言内建排序通常远比手写排序精细。原文档的陷阱总结表明确提示"使用稳定排序(归并排序、Python 的sorted)",因此在多关键字排序等稳定性敏感的场景中,优先依赖语言内建实现。


模式一:二分查找——在单调条件上搜索

二分查找在有序数组中用 $O(\log n)$ 时间找到目标,方法是反复将搜索空间减半。但二分查找远不止"在有序数组中找一个数",其通用模式是:在单调条件(monotonic condition)上做搜索

标准模板(规避 off-by-one 错误)

def binary_search(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = lo + (hi - lo) // 2 # avoids overflow in other languages if arr[mid] == target: return mid elif arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1 # not found

mid = lo + (hi - lo) // 2而非(lo + hi) // 2,在 C/C++/Java 中可避免lo + hi的整数溢出;Python 中无溢出风险,但该写法作为习惯保留。

下界:第一个 $\geq$ target 的元素

def lower_bound(arr, target): lo, hi = 0, len(arr) while lo < hi: mid = (lo + hi) // 2 if arr[mid] < target: lo = mid + 1 else: hi = mid return lo

注意这里hi初始化为len(arr)右开区间),循环条件是lo < hi,收缩时hi = mid而非mid - 1。这正是陷阱所在:lo <= hilo < hi的区别、hi = midhi = mid - 1的区别,决定了你找到的是精确匹配还是边界位置。拿一个只有 2 个元素的数组手动画一遍,是最可靠的验证方式。lower_bound返回的索引可以直接作为 C++std::lower_bound的语义:arr[lo] >= target的第一个位置。

Medium:搜索旋转排序数组

问题:一个有序数组在某个支点处被旋转(rotated),在其中搜索目标值。

模式:每一步中,总有一半是排序好的。判断哪一半有序,再看目标是否落在该半区。

def search_rotated(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid # left half is sorted if nums[lo] <= nums[mid]: if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 # right half is sorted else: if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1

关键陷阱nums[lo] <= nums[mid]中的<=(而非<)至关重要。当区间只剩 2 个元素时lo == mid,必须用<=才能正确识别哪一半是有序的;写成<会把"左半区已排序"错误判定为"右半区已排序"。旋转数组的经典变形"寻找旋转数组最小值"也是同一思路:二分寻找拐点。

Hard:两个有序数组的中位数

问题:在 $O(\log(m + n))$ 时间内找到两个有序数组的中位数。

模式:对较短的数组二分搜索分割点。分割把两个数组都一分为二,使得左侧所有元素都小于右侧所有元素。

def find_median(nums1, nums2): if len(nums1) > len(nums2): nums1, nums2 = nums2, nums1 # ensure nums1 is shorter m, n = len(nums1), len(nums2) lo, hi = 0, m half = (m + n + 1) // 2 while lo <= hi: i = (lo + hi) // 2 # partition point in nums1 j = half - i # partition point in nums2 left1 = nums1[i - 1] if i > 0 else float('-inf') right1 = nums1[i] if i < m else float('inf') left2 = nums2[j - 1] if j > 0 else float('-inf') right2 = nums2[j] if j < n else float('inf') if left1 <= right2 and left2 <= right1: # correct partition if (m + n) % 2 == 1: return max(left1, left2) return (max(left1, left2) + min(right1, right2)) / 2 elif left1 > right2: hi = i - 1 else: lo = i + 1

这是最难的二分查找问题之一。核心洞察是:你搜索的不是某个值,而是一个满足条件的"分割点"(partition point)。先在较短数组上枚举分割位置 $i$,再通过j = half - i推导另一个数组的分割位置,用边界哨兵float('-inf')/float('inf')优雅处理越界情况。两个分割点共同满足left1 <= right2 and left2 <= right1时,中位数即可由两侧最靠近分割点的四个数计算得出。

元模式:二分答案

很多看起来与二分查找无关的问题,可以通过对答案做二分解决。如果答案是某个数值 $x$,并且你能写出一个单调的可行性判定函数is_feasible(x)(对所有 $x \geq$ 最优值恒为 True,或对所有 $x \geq$ 最优值恒为 False),那么就可以对 $x$ 进行二分。

经典例子:"一艘船至少需要多大的容量,才能在 $d$ 天内运完所有包裹?"对容量做二分。对每个候选容量,用贪心法检查是否能在 $d$ 天内运完:

def ship_within_days(weights, days): lo, hi = max(weights), sum(weights) while lo < hi: mid = (lo + hi) // 2 # can we ship with capacity mid in <= days? current_load, num_days = 0, 1 for w in weights: if current_load + w > mid: num_days += 1 current_load = 0 current_load += w if num_days <= days: hi = mid else: lo = mid + 1 return lo

判定函数单调性明显:容量越大,所需天数越少或不变。二分的下界是单件最大重量max(weights)(容量必须装得下最重的包裹),上界是总重量sum(weights)(一天运完)。判定函数内部本质是贪心装箱:只要当前包裹放不进就新开一天。类似的"二分答案"题目还有 Koko 吃香蕉(每小时吃 k 根能否在 h 小时内吃完)等,这类题在课后练习中会进一步巩固。


模式二:贪心算法——局部最优通往全局最优

贪心算法在每一步做出当前看起来最优的选择,希望由此得到全局最优解。贪心成立需要两个性质:

  • 贪心选择性质(greedy choice property):局部最优选择能通向全局最优解;
  • 最优子结构(optimal substructure):全局最优解包含子问题的最优解。

贪心的最大陷阱是"没有证明就用贪心"——很多问题局部最优并不能导出全局最优(典型反例见 动态规划一节 的硬币组合)。下界问题都经过证明:只要维护正确的"局部状态",贪心就是正确的。

Medium:跳跃游戏

问题:给定数组numsnums[i]是在位置 $i$ 能跳的最大长度,判断能否到达最后一个下标。

def can_jump(nums): max_reach = 0 for i, jump in enumerate(nums): if i > max_reach: return False # cannot reach this position max_reach = max(max_reach, i + jump) return True

为什么贪心成立:我们只需要知道"最远可达位置"。如果当前位置已经超出最远可达位置,说明卡死了;否则不断更新最远可达位置即可。不需要回溯、不需要 DP,因为可达性具有单调扩张性质:只要某个位置可达,它之前的所有位置都可达。贪心的核心就是把状态压缩到最少必要信息——在这里就是"最远可达位置"这一个变量。

Medium:合并区间

问题:合并所有重叠的区间。

def merge_intervals(intervals): intervals.sort(key=lambda x: x[0]) merged = [intervals[0]] for start, end in intervals[1:]: if start <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], end) else: merged.append([start, end]) return merged

模式:按开始时间排序,然后贪心合并。如果当前区间与已合并的最后一个区间重叠,就扩展它;否则开启新的合并区间。这里排序是 $O(n \log n)$ 的预处理,排序后单次线性扫描即可完成合并,整体 $O(n \log n)$。

关键陷阱:合并时要用merged[-1][1] = max(merged[-1][1], end)而不是merged[-1][1] = end。一个区间可能被另一个完全包含(例如 [1, 10] 和 [2, 5]),若直接赋值end会把已合并区间的右端点错误地缩回去。这一"取 max 而非覆盖"的细节是合并类问题最普遍的 bug。进阶变形(插入区间、无重叠区间)可参见课后练习清单,它们都是在"排序 + 贪心扫描"骨架上做文章。


模式三:动态规划——重叠子问题只算一次

动态规划(DP)通过把问题分解为重叠的子问题(overlapping subproblems),每个子问题只求解一次并存储结果来避免重复计算。DP 适用需要两个条件:最优子结构(全局最优可由子问题最优构造)和重叠子问题(递归树中同一子问题反复出现)。

两种实现方式

  • 自顶向下(记忆化 memoisation):先写出自然的递归解法,再把结果缓存到字典中;
  • 自底向上(表格法 tabulation):从最小的子问题开始向上构建表格。

如何识别 DP:问题求最优值(min/max)、计数或存在性,且当前决策依赖之前的决策。画出递归树如果看到重复子问题,就是 DP。这里与第14章第00节的 Fibonacci 例子一脉相承:朴素递归 $O(2^n)$,记忆化后变为 $O(n)$。

Easy:爬楼梯

问题:$n$ 级台阶,每次可以爬 1 或 2 级,有多少种不同爬法?

这正是 Fibonacci:$f(n) = f(n-1) + f(n-2)$。

def climb_stairs(n): if n <= 2: return n a, b = 1, 2 for _ in range(3, n + 1): a, b = b, a + b return b

$O(n)$ 时间、$O(1)$ 空间。因为每个状态只依赖前两个状态,完整的记忆化表并不需要——这是 DP 空间优化(滚动变量)的最简单示范。

Medium:零钱兑换

问题:给定硬币面额和目标金额,求凑出该金额所需的最少硬币数。

  • 状态dp[amount]= 凑出amount所需的最少硬币数;
  • 转移dp[amount] = min(dp[amount - coin] + 1)(对每种硬币);
  • 基础情形dp[0] = 0
def coin_change(coins, amount): dp = [float('inf')] * (amount + 1) dp[0] = 0 for a in range(1, amount + 1): for coin in coins: if coin <= a and dp[a - coin] + 1 < dp[a]: dp[a] = dp[a - coin] + 1 return dp[amount] if dp[amount] != float('inf') else -1

关键陷阱:用float('inf')(而非 0 或 -1)初始化。最小值比较只有在"不可达状态是无穷大"时才成立;如果初始化成 0,所有状态都会被错误地判定为"用 0 个硬币凑出"。这也是一个典型的无界背包(unbounded knapsack)——每种硬币可以用任意多次,所以内层循环可以正序使用dp[a - coin]。当题目要求每种物品最多用一次时,就退化为下面的 0/1 背包,迭代方向必须反过来。

Medium:最长公共子序列

问题:给定两个字符串,求它们最长公共子序列(LCS)的长度。

  • 状态dp[i][j]=text1[:i]text2[:j]的 LCS 长度;
  • 转移:若text1[i-1] == text2[j-1],则dp[i][j] = dp[i-1][j-1] + 1;否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])
def longest_common_subsequence(text1, text2): m, n = len(text1), len(text2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if text1[i - 1] == text2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[m][n]

这是二维 DP的代表:两个索引分别对应两个字符串的前缀。dp表大小为 $(m+1) \times (n+1)$ 并让 0 行 0 列恒为 0,从而自然处理"空前缀"边界;因此访问字符串时要写成text1[i-1],这正是第14章第00节陷阱表中"2D DP 的 off-by-one:dp是 1 索引,访问text1[i-1]"所强调的点。编辑距离(Edit Distance)是同一结构换一个转移公式的经典变形。

Hard:0/1 背包

问题:给定若干物品的重量和价值,以及容量 $W$,在不超过 $W$ 的前提下最大化总价值。

  • 状态dp[i][w]= 使用前 $i$ 个物品、容量为 $w$ 时的最大价值;
  • 转移dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])(跳过或取物品 $i$)。
def knapsack(weights, values, capacity): n = len(weights) dp = [[0] * (capacity + 1) for _ in range(n + 1)] for i in range(1, n + 1): for w in range(capacity + 1): dp[i][w] = dp[i - 1][w] # skip item i if weights[i - 1] <= w: dp[i][w] = max(dp[i][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]) return dp[n][capacity]

空间优化:每一行只依赖上一行,因此可以用一维数组,并让 $w$从右往左迭代:

def knapsack_optimised(weights, values, capacity): dp = [0] * (capacity + 1) for i in range(len(weights)): for w in range(capacity, weights[i] - 1, -1): # right to left! dp[w] = max(dp[w], dp[w - weights[i]] + values[i]) return dp[capacity]

关键陷阱:一维版本如果从左往右迭代,dp[w - weights[i]]可能已经被本轮更新过,等于允许物品 $i$ 被多次使用(变成无界背包);从右往左保证每个物品至多使用一次。方向这一字之差,正是 0/1 背包与无界背包的分水岭。零钱兑换是无界背包、0/1 背包是"每物一次",两者在课后练习中经常结对出现,务必分清。


模式四:回溯——带剪枝的穷举搜索

回溯(backtracking)是带剪枝的穷举搜索:增量式构建解,一旦当前部分解不可能导向合法的完整解,就立即放弃(回溯)。递归三要素是选择(choose)、探索(explore)、撤销选择(unchoose)

通用模板

def backtrack(candidates, path, result): if is_solution(path): result.append(path[:]) # copy! return for candidate in get_candidates(path): if is_valid(candidate, path): path.append(candidate) # choose backtrack(candidates, path, result) # explore path.pop() # unchoose (backtrack)

撤销选择是回溯区别于普通递归的关键:没有path.pop(),状态会不断累积,后续候选看到的是被污染的旧状态。回溯的基础理论(三步骤、剪枝的作用、子集/排列的递归树)在第14章第00节有完整铺垫,N-Queens 的剪枝收益($n^n$ → 约 $n!$)也在那里给出了数量级对比。

Medium:子集

def subsets(nums): result = [] def backtrack(start, path): result.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1, path) path.pop() backtrack(0, []) return result

每个部分解都是一个合法子集,因此进入函数即记录。backtrack(i + 1, ...)保证不重复使用元素、且按start索引避免回头,从而生成全部 $2^n$ 个子集且无重复。

Medium:组合总和

问题:找出所有和为 target 的唯一组合(元素可以重复使用)。

def combination_sum(candidates, target): result = [] def backtrack(start, path, remaining): if remaining == 0: result.append(path[:]) return for i in range(start, len(candidates)): if candidates[i] > remaining: break # prune: sorted, so all further candidates are too large path.append(candidates[i]) backtrack(i, path, remaining - candidates[i]) # i, not i+1: reuse allowed path.pop() candidates.sort() # sort for pruning backtrack(0, [], target) return result

关键陷阱backtrack(i, ...)允许重复使用同一元素;backtrack(i + 1, ...)则跳到下一个元素(不允许重复)。搞混这两者是回溯题最常见的 bug——前者对应"组合总和(可重复取)",后者对应"组合总和 II(每个元素只能用一次)"。此外,先排序再在candidates[i] > remainingbreak是剪枝的关键:既然已排序,后续候选只会更大,整棵子树都可以跳过。

Hard:N 皇后

问题:在 $n \times n$ 棋盘上放置 $n$ 个皇后,使任意两个皇后互不攻击。

def solve_n_queens(n): result = [] cols = set() pos_diag = set() # (row + col) is constant on / diagonals neg_diag = set() # (row - col) is constant on \ diagonals board = [['.' ] * n for _ in range(n)] def backtrack(row): if row == n: result.append([''.join(r) for r in board]) return for col in range(n): if col in cols or (row + col) in pos_diag or (row - col) in neg_diag: continue cols.add(col) pos_diag.add(row + col) neg_diag.add(row - col) board[row][col] = 'Q' backtrack(row + 1) cols.remove(col) pos_diag.remove(row + col) neg_diag.remove(row - col) board[row][col] = '.' backtrack(0) return result

关键洞察:对角线的编码方式。在/对角线上row + col是常数,在\对角线上row - col是常数。用三个集合(列、两条对角线)做冲突检测,使合法性检查降为 $O(1)$,配合逐行放置(天然保证同行无冲突),把 $O(n^n)$ 的暴力搜索剪枝到实用规模。


常见陷阱速查表

原文档将全文高频 bug 汇总为一张表,是面试前的"最后一页复习材料":

陷阱示例修复
二分查找中lo <= hilo < hi混用边界 off-by-one根据hi是闭区间还是开区间选择
一维 0/1 背包从左到右迭代物品被多次使用0/1 背包必须从右往左迭代
回溯中不复制 pathresult.append(path)——所有条目指向同一列表result.append(path[:])path.copy()
backtrack(i)backtrack(i+1)混淆允许/禁止重复使用元素严格对照题目要求
已排序回溯中缺少break继续探索过大的候选排序 + 候选超过剩余值时break
DP 初始化错误dp[0]错 → 后续全部错仔细定义并验证基础情形
贪心未经证明贪心并不总是正确验证贪心选择性质
多关键字排序用了不稳定排序相等元素的相对顺序丢失用稳定排序(归并排序、Python 的sorted

这八条几乎覆盖了排序搜索类题目 90% 的失分点。结合前文各小节,可以总结出三条方法论:二分先画两元素数组验证边界;DP 先写状态定义、转移、基础情形再编码;回溯先确认i还是i + 1、path 是否深拷贝。


课后练习清单(按模式分组)

以下是原文档给出的配套练习路线,按模式分组、由易到难,每道题都在强化本文件的一个具体范式。建议每道题都先做模式识别("这道题属于哪个范式、为什么"),再动手写代码。

二分查找

  • Binary Search——标准模板
  • Search a 2D Matrix——展平矩阵上二分
  • Koko Eating Bananas——二分答案
  • Search in Rotated Sorted Array——识别有序半区
  • Find Minimum in Rotated Sorted Array——二分找拐点
  • Median of Two Sorted Arrays——基于分割点的二分

贪心

  • Jump Game——维护最远可达
  • Jump Game II——BFS 式层级跟踪
  • Merge Intervals——排序 + 合并
  • Insert Interval——定位重叠区域
  • Non-overlapping Intervals——按结束时间排序

动态规划

  • Climbing Stairs——Fibonacci DP
  • House Robber——取/跳过 DP
  • House Robber II——环形:跑两次
  • Coin Change——无界背包
  • Longest Common Subsequence——双字符串二维 DP
  • Word Break——集合查找 + DP
  • Longest Increasing Subsequence——$O(n^2)$ DP 或 $O(n \log n)$ 配合二分
  • Edit Distance——经典二维 DP
  • Partition Equal Subset Sum——0/1 背包变体

回溯

  • Subsets——枚举所有子集
  • Combination Sum——带复用的回溯
  • Permutations——used 集合回溯
  • Subsets II——跳过重复
  • Word Search——网格回溯
  • Palindrome Partitioning——回溯 + 回文判断
  • N-Queens——约束传播

练习时注意:本文件是第14章的收官之篇,前面的数组与哈希(双指针、滑动窗口、前缀和)、链表/栈/队列、树、图构成了完整模式库,排序搜索作为最后一个范式与其相互印证。


如何在仓库中深度使用本文内容

本仓库(maths-cs-ai-compendium)是 MkDocs 构建的开源教科书,本章节在导航配置 mkdocs.yml 中注册为chapter 14: data structures and algorithms/05. sorting and search.md,在 llms.txt 中登记的描述为"Merge/quick sort, binary search, greedy, DP, backtracking (with NeetCode problems)",与本文结构一一对应。

仓库还附带一个MCP 服务器(见 mcp/src/index.ts),让 Claude Code、Cursor、VS Code 等 AI 助手把整本教科书当作知识库使用。其核心能力:

  • list_topics:列出全部 20 章的章节结构(输入chapter参数可过滤到指定章,如第 14 章);
  • read_section:按章号与节号读取完整内容,例如chapter=14, section=5即可完整返回本节原文;
  • search:跨全部章节做关键词检索,返回命中上下文与行号;
  • recommend:解析llms.txt的描述文本与停用词表,基于学习目标推荐阅读顺序。

使用 MCP 服务器需要先在本地克隆仓库,然后配置 AI 客户端连接。阅读建议:先完整精读本文件对应的原文档,再对照 第14章第00节 Foundations 补足 Big O、递归、回溯、DP 的第一性原理,最后用上面的练习清单检验模式识别能力——这套"读原文 → 懂原理 → 练识别"的路径,正是本教科书"先直觉后公式"理念的落地方式。

【免费下载链接】maths-cs-ai-compendiumBecome a cracked AI/ML researcher/engineer with this unconventional textbook covering maths, computing, and ML with intuition.项目地址: https://gitcode.com/GitHub_Trending/mat/maths-cs-ai-compendium

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 13:47:38

Folo:AI驱动的下一代信息浏览器终极解决方案

Folo&#xff1a;AI驱动的下一代信息浏览器终极解决方案 在信息爆炸的时代&#xff0c;每天都有海量内容从各个渠道涌入我们的生活。你是否感到被各种APP推送淹没&#xff0c;有价值的信息总是被噪音掩盖&#xff1f;Folo作为一款革命性的AI信息浏览器&#xff0c;正是为了解决…

作者头像 李华
网站建设 2026/9/17 13:46:26

JS逆向实战:破解私募排行加密接口的完整流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 13:45:42

CloudBase-AI-ToolKit环境配置问题排查指南

CloudBase-AI-ToolKit环境配置问题排查指南 【免费下载链接】CloudBase-AI-Toolkit Backend for AI coding agents on CloudBase — database, auth, functions via Plugin, Skills & MCP. 项目地址: https://gitcode.com/gh_mirrors/cl/CloudBase-AI-Toolkit 在使用…

作者头像 李华
网站建设 2026/9/17 13:45:17

BERT多标签专利分类实践:IPC标签筛选与微调

简介&#xff1a;一份基于预训练模型的多标签专利分类研究文档&#xff0c;面向自然语言处理与专利文本挖掘方向的研究者&#xff0c;系统阐述如何利用BERT、RoBERTa和RBT3预训练模型解决大规模专利自动分类问题。文档将分类粒度细化到IPC“小类”级别&#xff0c;并通过高频标…

作者头像 李华
网站建设 2026/9/17 13:44:12

Ollama本地部署大模型,用Python打造隐私安全的翻译工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华