第 02 站 · 线性结构
哈希计数与关系建模
边扫边查的哈希表 时间 O(n) · 空间 O(n)
- 先学
先做这道
LC 1两数之和卡住再看
边扫描边查询补数,区分先查询再写入以免重复使用同一下标。
要点
- 先查补数、再存自己:一遍扫描就能找到「之前出现过」的配对,也不会把同一个元素用两次。
- 存什么取决于问什么:要位置就存下标,要个数就存次数,要最早出现的位置就只在第一次出现时存。
- 键可以组合出来:排序后的字符串、计数元组、(行, 值) 对,都能把「同一类」放进同一个桶。
易错点
- 先插入再查询,target 恰好等于 2x 时会把同一个元素和自己配成一对。
- C++ 的 map[key] 读一个不存在的键会插入默认值;只想判断存在时用 find 或 count。
- Java 里两个 Integer 用 == 比较,超过 127 时可能不相等;用 equals 或先拆箱成 int。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
哈希表 · 拉链法与扩容
依次插入 12, 7, 4, 15, 9, 20, 3
换个输入
- 键数
- 0
- 桶数
- 4
- 负载因子
- 0.00
- 扩容次数
- 0
4 个空桶。键 k 放进第 k mod 桶数 号桶;同一个桶里的键串成链表(拉链法)。
始终成立键 k 总在第 k mod 桶数 号桶里;负载因子不超过 0.75,链表的平均长度就是常数。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 1。
Python
def two_sum(nums: list[int], target: int) -> tuple[int, int]:
seen: dict[int, int] = {} # value -> index of an earlier element
for i, x in enumerate(nums):
if target - x in seen: # look up before inserting: no self-pairing
return seen[target - x], i
seen[x] = i
return -1, -1C++
pair<int, int> twoSum(const vector<int>& nums, int target) {
unordered_map<int, int> seen; // value -> index of an earlier element
for (int i = 0; i < (int)nums.size(); i++) {
auto it = seen.find(target - nums[i]); // find() never inserts, unlike []
if (it != seen.end()) return {it->second, i};
seen[nums[i]] = i;
}
return {-1, -1};
}Java
class Solution {
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // value -> index of an earlier element
for (int i = 0; i < nums.length; i++) {
Integer j = seen.get(target - nums[i]); // look up before inserting: no self-pairing
if (j != null) return new int[] {j, i};
seen.put(nums[i], i);
}
return new int[] {-1, -1};
}
}Go
func twoSum(nums []int, target int) (int, int) {
seen := map[int]int{} // value -> index of an earlier element
for i, x := range nums {
if j, ok := seen[target-x]; ok { // look up before inserting: no self-pairing
return j, i
}
seen[x] = i
}
return -1, -1
}常见变体
这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
按规范键分组时间 O(n · L log L) · 空间 O(n · L)
要把「本质相同」的元素归到一起(字母异位词、移位字符串、同构):给每个元素算一个规范键,相同键进同一组。键可以是排序后的字母,也可以是 26 个计数拼成的字符串。LC 49 字母异位词分组、LC 249 移位字符串分组。
Python
def group_anagrams(words: list[str]) -> list[list[str]]:
"""Words grouped by anagram class, groups in order of first appearance."""
index: dict[str, int] = {} # canonical key -> position of its group
groups: list[list[str]] = []
for w in words:
key = "".join(sorted(w)) # anagrams share their sorted letters
if key not in index:
index[key] = len(groups)
groups.append([])
groups[index[key]].append(w)
return groupsC++
// Words grouped by anagram class, groups in order of first appearance.
vector<vector<string>> groupAnagrams(const vector<string>& words) {
unordered_map<string, int> index; // canonical key -> position of its group
vector<vector<string>> groups;
for (const string& w : words) {
string key = w;
sort(key.begin(), key.end()); // anagrams share their sorted letters
auto [it, fresh] = index.try_emplace(key, (int)groups.size());
if (fresh) groups.emplace_back();
groups[it->second].push_back(w);
}
return groups;
}Java
class Solution {
// Words grouped by anagram class, groups in order of first appearance.
List<List<String>> groupAnagrams(String[] words) {
Map<String, List<String>> groups = new LinkedHashMap<>(); // canonical key -> its words, in first-seen order
for (String w : words) {
char[] letters = w.toCharArray();
Arrays.sort(letters); // anagrams share their sorted letters
groups.computeIfAbsent(new String(letters), k -> new ArrayList<>()).add(w);
}
return new ArrayList<>(groups.values());
}
}Go
import "sort"
// groupAnagrams returns the words grouped by anagram class, in order of first appearance.
func groupAnagrams(words []string) [][]string {
index := map[string]int{} // canonical key -> position of its group
var groups [][]string
for _, w := range words {
letters := []byte(w)
sort.Slice(letters, func(i, j int) bool { return letters[i] < letters[j] })
key := string(letters) // anagrams share their sorted letters
at, ok := index[key]
if !ok {
at = len(groups)
index[key] = at
groups = append(groups, nil)
}
groups[at] = append(groups[at], w)
}
return groups
}哈希集合:只从连续段的起点往后数时间 O(n) · 空间 O(n)
要在 O(n) 里找最长的连续整数段(LC 128),不能排序:把数放进集合,只有 x - 1 不在集合里时 x 才是段的起点,从起点往后数。每个数最多被数两次,所以是线性的。
Python
def longest_consecutive(nums: list[int]) -> int:
"""Length of the longest run of consecutive integers, in O(n)."""
have = set(nums)
best = 0
for x in have:
if x - 1 in have:
continue # not a run start: the run is counted from its start
y = x
while y + 1 in have:
y += 1
best = max(best, y - x + 1)
return bestC++
// Length of the longest run of consecutive integers, in O(n).
int longestConsecutive(const vector<int>& nums) {
unordered_set<int> have(nums.begin(), nums.end());
int best = 0;
for (int x : have) {
if (have.count(x - 1)) continue; // not a run start: the run is counted from its start
int y = x;
while (have.count(y + 1)) y++;
best = max(best, y - x + 1);
}
return best;
}Java
class Solution {
// Length of the longest run of consecutive integers, in O(n).
int longestConsecutive(int[] nums) {
Set<Integer> have = new HashSet<>();
for (int x : nums) have.add(x);
int best = 0;
for (int x : have) {
if (have.contains(x - 1)) continue; // not a run start: the run is counted from its start
int y = x;
while (have.contains(y + 1)) y++;
best = Math.max(best, y - x + 1);
}
return best;
}
}Go
// longestConsecutive returns the length of the longest run of consecutive integers, in O(n).
func longestConsecutive(nums []int) int {
have := map[int]bool{}
for _, x := range nums {
have[x] = true
}
best := 0
for x := range have {
if have[x-1] {
continue // not a run start: the run is counted from its start
}
y := x
for have[y+1] {
y++
}
best = max(best, y-x+1)
}
return best
}为什么对
- 一遍扫描不会漏:任何一对答案 i < j,扫到 j 时 i 已经存进哈希表,所以一定在 j 处被查到;先查后存保证 j 不会和自己配对。
- 分组正确的条件是「键相同 ⇔ 属于同一类」。排序后的字符串、字母计数元组都满足这一点,所以每个桶里恰好是一类元素。
- 哈希表查找期望 O(1),所以整体 O(n);冲突很多时会退化,面试时可以提一句。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
我用哈希表把找配对从 O(n²) 降到 O(n)。从左往右扫,每到一个元素先查它需要的另一半是否已经出现过,再把它自己存进去。任意一对答案在扫到右边那个元素时,左边那个已经在表里,所以不会漏;先查后存也保证不会和自己配对。时间 O(n),空间 O(n)。I use a hash map to turn the pair search from O(n²) into O(n). Scanning left to right, I first look up the complement of the current element, then store the element itself. For any valid pair, the left element is already in the map when I reach the right one, so nothing is missed, and looking up before storing prevents pairing an element with itself. That is O(n) time and O(n) space.自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
5 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
分档练习
本站在 500 路里的 19 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
基础 5
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 1 | 两数之和 | 边扫描边查询补数,区分先查询再写入以免重复使用同一下标。 | |
| 219 | 存在重复元素 II | 维护值的最近出现位置,用下标差判定局部重复。 | |
| 409 | 最长回文串 | 用奇偶频次组织可构造回文的长度,区分中心与成对字符。 | |
| 266 | 回文排列 🔒 | 把回文排列的存在性归结为奇数频次的数量。 | |
| 1010 | 总持续时间可被 60 整除的歌曲 | 对余数互补分类计数,单独考虑余数零和中点自配对。 |
核心 13
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 49 | 字母异位词分组 | 为字符串构造稳定的等价类键,比较排序键与计数字段键。 | |
| 128 | 最长连续序列 | 只从连续区间的起点向右扩展,证明哈希扫描的总工作量。 | |
| 454 | 四数相加 II | 把四数组组合拆成两组配对和,利用计数哈希降低枚举维度。 | |
| 187 | 重复的DNA序列 | 固定长度子串去重与重复检测,比较直接字符串键和滚动编码。 | |
| 249 | 移位字符串分组 🔒 | 用相邻字符模差表示移位等价类,正确处理环绕与单字符。 | |
| 1679 | K 和数对的最大数目 | 用互补值配对消耗频次,保证每个元素只能被使用一次。 | |
| 2342 | 数位和相等数对的最大和 | 按数位和分组并维护组内前两大值,避免排序全部候选。 | |
| 2364 | 统计坏数对的数目 | 把下标差与数值差条件变形成相同键计数,再由总对数取补集。 | |
| 1814 | 统计一个数组中好对子的数目 | 把涉及反转数值的成对等式化为单元素键,统计相同键组合。 | |
| 1657 | 确定两个字符串是否接近 | 区分字符集合相同与频次数组可置换两项必要条件。 | |
| 554 | 砖墙 | 把跨越最少砖块转为内部缝隙位置的最大重合计数。 | |
| 1930 | 长度为 3 的不同回文子序列 | 按两端字符确定区间,再对中间字符去重统计长度三回文。 | |
| 2131 | 连接两字母单词得到的最长回文串 | 用反向单词配对并单独处理对称单词作为回文中心。 |
进阶 1
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 220 | 存在重复元素 III | 在受限下标窗口内维护值域邻居,比较有序集合与桶划分。 |
本站 OA 真题 268
- Count Powers of k in ReadingsMeta
- Longest Subarray With Value Frequency > ThresholdMeta
- Longest Contiguous-House Segment After Each BuildMeta
- Enumerating Narrative SectionsMeta
- Self-Sufficient SubstringAmazon
- Outlier ValueAmazon
- Get Total BalancedAmazon
- Find Maximum PackagesAmazon
- Find Max Health SumAmazon
- Find Score (p-Matching)Amazon