算法中的“记账思维”

“记账思维”(数学模型替代物理模型、延迟计算、状态压缩)练成直觉,5 道经典算法题。它们按难度递增排序,每一道都完美契合你领悟到的这种高级思维。


1. 难度:简单 (Easy)

题目:238. 除自身以外数组的乘积 (Product of Array Except Self)

  • 题目简述:给你一个整数数组,要求返回一个新数组,新数组中第 i 个元素是原数组中除了 nums[i] 之外所有元素的乘积。要求不能使用除法,且时间复杂度为 O(N)
  • 为什么契合你的思维
    • 物理直觉:要算出除了自己之外的乘积,最笨的办法是每次都遍历一遍(O(N^2))。
    • 记账思维:把 result[i] 拆分成“左边所有元素的乘积”和“右边所有元素的乘积”。我们不需要真的去算出那个庞大的整体,只需要在两次遍历中,像记账一样把“左边的累积”和“右边的累积”分别记在数组里,最后合并即可。

2. 难度:中等 (Medium)

题目:303. 区域和检索 - 数组不可变 (Range Sum Query - Immutable)

  • 题目简述:给定一个整数数组,多次查询任意区间 [i, j] 内元素的和。
  • 为什么契合你的思维
    • 物理直觉:每次查询都从 i 加到 j,查询 M 次就是 O(M*N)。
    • 记账思维(前缀和):这是“记账思维”的入门神技。在初始化时,我们只记录一个“前缀和账本”(prefix[i] 表示前 i 个元素的总和)。查询时,直接用 prefix[j] - prefix[i-1],把 O(N) 的查询降维打击到了 O(1)。

3. 难度:中等 (Medium)

题目:380. O(1) 时间插入、删除和获取随机元素 (Insert Delete GetRandom O(1))

  • 题目简述:设计一个数据结构,支持以 O(1) 的时间复杂度插入、删除元素,并且能以 O(1) 的时间等概率随机返回一个元素。
  • 为什么契合你的思维
    • 物理直觉:数组随机访问是 O(1),但删除不是;哈希表插入删除是 O(1),但无法随机访问。
    • 记账思维(状态压缩与映射):将两者结合。用数组存真实数据,用哈希表记录“值 -> 数组索引”的映射。删除时,把数组末尾的元素“搬”到被删除元素的位置,然后更新哈希表。这里没有真正去“整理”数组,而是通过修改索引的“账本”(哈希表)实现了逻辑上的 O(1) 删除。

4. 难度:中等 (Medium)

题目:146. LRU 缓存 (LRU Cache)

  • 题目简述:设计并实现一个 LRU 缓存机制。当缓存满了,需要淘汰最久未使用的数据。要求 getput 都是 O(1) 时间复杂度。
  • 为什么契合你的思维
    • 物理直觉:要记录“使用顺序”,直觉上需要不断在数组或链表里把元素挪来挪去,这极其耗时。
    • 记账思维(延迟计算与解耦):利用“哈希表 + 双向链表”。哈希表负责 O(1) 查找,链表只负责记录“时间顺序的账本”。当需要更新顺序时,我们只是在链表上“改改指针”(改改账本),并没有发生任何数据的物理搬运。这就是典型的“逻辑与物理的解耦”。

5. 难度:困难 (Hard)

题目:460. LFU 缓存 (LFU Cache)

  • 题目简述:在 LRU 的基础上,淘汰策略改为“最不经常使用(LFU)”。如果存在多个相同频率的元素,淘汰最久未使用的。要求 getput 都是 O(1)。
  • 为什么契合你的思维
    • 物理直觉:每次访问都要更新频率,然后重新排序,复杂度极高。
    • 记账思维(极致状态压缩):这是算法设计的艺术。你需要维护一个“频率账本”(freq_to_nodes,一个哈希表,key是频率,value是双向链表),以及一个“节点到频率的映射账本”。当频率增加时,你只需要在两个链表之间“转移指针”,完全不需要排序。这道题是对“记账思维”的终极考验。

练习建议
在做这几道题时,当你卡壳或者写出 O(N^2) 的代码时,不妨停下来问问自己:“我是不是在试图做真实的物理搬运?我能不能只改改指针/记录一下状态,在数学上等价地解决这个问题?”

posted @ 2026-08-30 22:04  立体风  阅读(12)  评论(0)    收藏  举报