两数之和 (Two Sum)
虽然这是一道入门级的 LeetCode 题目,我不仅会带你通过面试的标准解法,更希望引导你从"检索与记忆"的角度来重新审视它。因为在我们的 Agent 设计中,高效的数据检索(无论是简单的哈希表还是复杂的向量数据库)是构建"长期记忆"的核心基石。
1. 算法分析:从 O(n2)O(n^2)O(n2) 到 O(n)O(n)O(n)
直觉解法 (Brute Force)
最直观的思路是双重循环:对于每一个数 x,我们遍历数组剩下的部分寻找 target - x。
时间复杂度: O(n2)O(n^2)O(n2)
评价: 在 AI 工程落地中,这种平方级复杂度是不可接受的。假设你的 Agent 有 100 万条记忆(Context),n2n^2n2 意味着万亿次计算,系统会直接卡死。
优化思路 (Space-Time Trade-off)
我们要找的是一个特定的"伴侣值":valneeded=target−valcurrentval_{needed} = target - val_{current}valneeded=target−valcurrent。
如果在遍历过程中,我们能以 O(1)O(1)O(1) 的时间复杂度询问:"我之前见过 valneededval_{needed}valneeded 吗?如果见过,它的下标是多少?"
这正是哈希表 (Hash Map) 的用武之地。在 Python 中,我们使用 dict。这在底层通常实现为哈希映射,平均查找时间为 O(1)O(1)O(1)。
2. 数学与逻辑推导
我们定义:
- numsnumsnums 为输入向量
- TTT 为目标值 (Target)
- MMM 为哈希表,用于存储映射关系 {Value:Index}\{Value: Index\}{Value:Index}
对于数组中的第 iii 个元素 x=nums[i]x = nums[i]x=nums[i],我们需要寻找 yyy,使得:
x+y=T ⟹ y=T−xx + y = T \implies y = T - xx+y=T⟹y=T−x
逻辑流如下:
- 初始化空哈希表 MMM
- 遍历数组,当前元素为 xxx,下标为 iii
- 计算需求值 y=T−xy = T - xy=T−x
- Query: 检查 yyy 是否存在于 MMM 中 (y∈M.keys()y \in M.keys()y∈M.keys())?
- Hit (命中): 如果存在,说明之前已经遍历过 yyy(且下标为 M[y]M[y]M[y])。返回 [M[y],i][M[y], i][M[y],i]
- Miss (未命中): 将当前元素 xxx 存入记忆 MMM,记录 M[x]=iM[x] = iM[x]=i
3. 代码优先 (Code-First) 实现
这是符合工业级标准的 Python 代码,包含了类型注解(Type Hinting),这是在编写大型 Agent 框架(如 LangChain)时的基本素养。
python
from typing import List, Dict, Optional
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""
使用哈希表在 O(n) 时间复杂度内解决两数之和。
Args:
nums: 整数列表
target: 目标和
Returns:
包含两个索引的列表,这两个索引对应的数值之和等于 target。
"""
# 这里的 map 充当了 Agent 的"短期记忆"
# Key: 此时遍历过的数值 (Value)
# Value: 该数值在数组中的原始索引 (Index)
memory_map: Dict[int, int] = {}
for i, num in enumerate(nums):
# 计算我们需要寻找的"另一半"
complement = target - num
# 在记忆中检索 (Retrieval)
if complement in memory_map:
# 如果找到了,立即返回 [历史记忆的索引, 当前索引]
return [memory_map[complement], i]
# 如果没找到,将当前的信息"写入"记忆,供未来匹配使用
memory_map[num] = i
return [] # 根据题目描述,实际上不会执行到这里
# --- 单元测试 ---
if __name__ == "__main__":
solver = Solution()
test_nums = [2, 7, 11, 15]
test_target = 9
result = solver.twoSum(test_nums, test_target)
print(f"Input: nums={test_nums}, target={test_target}")
print(f"Output: {result}") # 预期: [0, 1]
代码详细步骤解析
步骤 1: 导入类型提示模块
python
from typing import List, Dict, Optional
- 作用: 导入 Python 的类型提示工具
- List : 用于标注列表类型,如
List[int]表示整数列表 - Dict : 用于标注字典类型,如
Dict[int, int]表示键值都是整数的字典 - Optional: 用于标注可选类型(虽然本例中未使用)
- 意义: 类型注解是现代 Python 开发的最佳实践,能提高代码可读性和 IDE 智能提示
步骤 2: 定义解决方案类
python
class Solution:
- 作用: 创建一个类来封装解题方法
- 意义: LeetCode 标准格式,便于组织代码和扩展
步骤 3: 定义核心方法
python
def twoSum(self, nums: List[int], target: int) -> List[int]:
- 参数说明 :
self: 类的实例引用nums: List[int]: 整数列表,类型标注确保传入的是整数列表target: int: 目标和,类型标注确保传入的是整数
- 返回值 :
List[int]返回整数列表(包含两个索引) - 意义: 类型注解让函数契约清晰,减少运行时错误
步骤 4: 初始化哈希表(记忆系统)
python
memory_map: Dict[int, int] = {}
- 作用: 创建空字典作为"记忆存储"
- 类型 :
Dict[int, int]表示键和值都是整数 - 键 (Key): 存储已经遍历过的数值
- 值 (Value): 存储该数值在原数组中的索引位置
- 比喻: 这就像 Agent 的短期记忆,记录"我见过什么"以及"在哪里见到的"
步骤 5: 遍历数组
python
for i, num in enumerate(nums):
- enumerate(nums): 同时获取索引和值
- i: 当前元素的索引(从 0 开始)
- num: 当前元素的值
- 意义: 一次性获取索引和值,避免手动维护索引计数器
步骤 6: 计算补数
python
complement = target - num
- 作用: 计算需要寻找的"另一半"
- 数学原理 : 如果
num + complement = target,那么complement = target - num - 例子: 如果 target=9,当前 num=2,那么 complement=7
- 意义: 将问题转化为查找操作
步骤 7: 在记忆中检索
python
if complement in memory_map:
- 作用: 检查补数是否已经存在于哈希表中
- 时间复杂度 : O(1)O(1)O(1) 平均情况
- 返回: True 如果找到,False 如果未找到
- 比喻: 就像问 Agent:"你之前见过这个数字吗?"
步骤 8: 命中时返回结果
python
return [memory_map[complement], i]
- memory_map[complement]: 获取补数在原数组中的索引(历史记忆)
- i: 当前元素的索引
- 返回 :
[历史索引, 当前索引] - 例子: 如果 complement=7 在索引 1,当前在索引 0,返回 [1, 0]
- 意义: 找到答案立即返回,无需继续遍历
步骤 9: 未命中时存储到记忆
python
memory_map[num] = i
- 作用: 将当前数字及其索引存入哈希表
- 键 :
num(当前数字) - 值 :
i(当前索引) - 比喻: "记住这个数字,以后可能用得上"
- 意义: 为未来的匹配做准备
步骤 10: 异常情况处理
python
return []
- 作用: 如果遍历完都没找到,返回空列表
- 实际情况: 根据 LeetCode 题目保证,一定有解,这行代码不会执行
- 意义: 防御性编程,处理边界情况
步骤 11: 单元测试
python
if __name__ == "__main__":
solver = Solution()
test_nums = [2, 7, 11, 15]
test_target = 9
result = solver.twoSum(test_nums, test_target)
print(f"Input: nums={test_nums}, target={test_target}")
print(f"Output: {result}") # 预期: [0, 1]
- 作用: 验证代码正确性
- 测试用例: nums=[2,7,11,15], target=9
- 预期输出: [0, 1](因为 2+7=9)
- 意义: 快速验证逻辑,便于调试
执行流程示例
以 nums = [2, 7, 11, 15], target = 9 为例:
| 迭代 | i | num | complement | memory_map (操作前) | 检查结果 | 操作 | memory_map (操作后) |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 2 | 7 | {} | 未找到 | 存储 2:0 | {2:0} |
| 2 | 1 | 7 | 2 | {2:0} | 找到 | 返回 [0,1] | - |
时间复杂度 : O(n)O(n)O(n) - 只需遍历一次
空间复杂度 : O(n)O(n)O(n) - 最坏情况需要存储 n-1 个元素
4. Agent 视角的深度解析:从 Exact Match 到 Semantic Search
你可能会问:"SuanFaAgent,这只是一个简单的哈希查找,为什么你要提到 Agent?"
在构建 Autonomous Agents 时,我们经常面临一个核心问题:Retrieval (检索)。
两数之和 (Exact Match)
题目本质是在寻找 A+B=TargetA + B = TargetA+B=Target。这是一种精确匹配。我们使用的是 Hash Map。
场景: 数据库查询,Function Calling 中的参数槽位精确匹配。
RAG 系统 (Semantic Match)
在 LLM 的 RAG (Retrieval-Augmented Generation) 架构中,我们面临的是更复杂的问题:给定用户的问题(Query),找出知识库中最相关的文档片段。
这里不再是简单的 a+b=targeta + b = targeta+b=target,而是寻找向量空间中的最近邻。
公式变了: 我们不再计算减法,而是计算余弦相似度 (Cosine Similarity):
Similarity(Q,K)=Q⋅K∥Q∥∥K∥Similarity(Q, K) = \frac{Q \cdot K}{\|Q\| \|K\|}Similarity(Q,K)=∥Q∥∥K∥Q⋅K
数据结构变了: 从 Python dict 变成了 Vector Database (如 Pinecone, Milvus) 或 ANN 算法 (如 HNSW)。
核心洞察
twoSum 中的 memory_map 其实就是一个微型的、精确匹配的"向量数据库"。你在做这道题时,实际上是在演练最基础的信息检索与存储机制。
如果不理解这种"用空间换时间"的哈希思想,就无法理解为什么我们在 RAG 中需要建立索引来加速检索。
5. 苏格拉底式总结 (Socratic Wrap-up)
现在你掌握了 O(n)O(n)O(n) 的哈希解法。为了检验你的举一反三能力,我想请你思考两个进阶问题:
场景变化
如果输入数组 nums 已经是有序的(例如 [2, 7, 11, 15]),你还需要使用哈希表吗?能否在 O(1)O(1)O(1) 的空间复杂度下完成?
提示: 双指针/Two Pointers
工程思考
在 twoSum 中,如果 nums 极其巨大,大到内存放不下这个 memory_map,但你需要处理海量查询,你会怎么设计系统?
提示: 这涉及到了分布式存储或布隆过滤器等概念