两句话中的不常见单词:NeetCode 哈希表计数解法全解析
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
本篇技术指南聚焦 LeetCode 经典题目「Uncommon Words from Two Sentences(两句话中的不常见单词)」,以本仓库 articles/uncommon-words-from-two-sentences.md 的解题思路为核心骨架,系统讲解"单词频率统计"这一高频面试套路:通过哈希表合并计数,一行判据筛出只出现一次的单词。读完本文,你将掌握该题的两种哈希表实现、九种主流语言的等价写法、严格的复杂度推导,以及面试中最容易踩中的"不常见"定义陷阱。
题目本质:什么才是"不常见"单词
给定两个字符串s1和s2,返回所有不常见单词的列表。题目对"不常见"的定义是:
一个单词在两个句子合并后的整体中出现恰好一次(across both sentences combined)。
这个定义是解题的唯一准绳,它隐含了三个要点:
- 一个单词只在其中一句话中出现过一次,但另一句话完全没有——属于不常见;
- 一个单词在同一句话里出现了两次(例如
"apple apple")——不属于不常见,因为合并后频次为 2; - 一个单词在两句话里各出现一次——不属于不常见,因为合并后频次为 2。
因此,正确解法不是"检查单词是否只存在于某一句话",而是合并两句话后统计全局频次,频次恰好为 1 的单词即为答案。
前置知识
在动手实现前,建议先具备以下两个基础:
- Hash Maps(哈希表 / 字典):使用字典或哈希表统计元素频次,是本题的核心数据结构。Python 的
defaultdict、Java 的HashMap、C++ 的unordered_map、Go 的map等均可胜任; - String Manipulation(字符串处理):将句子按空格切分为单词列表,并遍历处理。注意
s1与s2拼接时中间必须补一个空格,否则边界处的单词会错误地粘在一起。
本仓库的整体解题框架与文章写作规范可参见 articles/README.md,其中明确要求每篇文章至少包含与视频讲解一致的核心解法,并附上时间与空间复杂度分析——本文严格遵循这一约定。
解法一:Hash Map 标准计数
核心直觉
不常见单词 = 合并后全局频次恰好为 1 的单词。把两句话的所有单词放进同一个哈希表计数,统计结束后遍历哈希表,把频次为 1 的单词收集进结果数组即可。
算法步骤
- 将
s1与s2分别按空格切分,合并成完整的单词列表; - 用哈希表统计每个单词的频次(
key为单词,value为出现次数); - 遍历哈希表,收集所有频次等于
1的单词; - 返回结果列表。
多语言实现
class Solution: def uncommonFromSentences(self, s1: str, s2: str) -> List[str]: count = defaultdict(int) for w in s1.split(" ") + s2.split(" "): count[w] += 1 res = [] for w, cnt in count.items(): if cnt == 1: res.append(w) return respublic class Solution { public String[] uncommonFromSentences(String s1, String s2) { String[] words = (s1 + " " + s2).split(" "); Map<String, Integer> count = new HashMap<>(); for (String w : words) { count.put(w, count.getOrDefault(w, 0) + 1); } List<String> res = new ArrayList<>(); for (Map.Entry<String, Integer> entry : count.entrySet()) { if (entry.getValue() == 1) { res.add(entry.getKey()); } } return res.toArray(new String[0]); } }class Solution { public: vector<string> uncommonFromSentences(string s1, string s2) { unordered_map<string, int> count; istringstream ss(s1 + " " + s2); string w; while (ss >> w) { count[w]++; } vector<string> res; for (auto& [word, freq] : count) { if (freq == 1) { res.push_back(word); } } return res; } };class Solution { /** * @param {string} s1 * @param {string} s2 * @return {string[]} */ uncommonFromSentences(s1, s2) { const words = (s1 + ' ' + s2).split(' '); const count = new Map(); for (const w of words) { count.set(w, (count.get(w) || 0) + 1); } const res = []; for (const [w, c] of count.entries()) { if (c === 1) { res.push(w); } } return res; } }public class Solution { public string[] UncommonFromSentences(string s1, string s2) { string[] words = (s1 + " " + s2).Split(' '); Dictionary<string, int> count = new Dictionary<string, int>(); foreach (string w in words) { if (count.ContainsKey(w)) { count[w]++; } else { count[w] = 1; } } List<string> res = new List<string>(); foreach (var entry in count) { if (entry.Value == 1) { res.Add(entry.Key); } } return res.ToArray(); } }func uncommonFromSentences(s1 string, s2 string) []string { words := strings.Split(s1 + " " + s2, " ") count := make(map[string]int) for _, w := range words { count[w]++ } res := []string{} for w, c := range count { if c == 1 { res = append(res, w) } } return res }class Solution { fun uncommonFromSentences(s1: String, s2: String): Array<String> { val words = "$s1 $s2".split(" ") val count = mutableMapOf<String, Int>() for (w in words) { count[w] = count.getOrDefault(w, 0) + 1 } val res = mutableListOf<String>() for ((w, c) in count) { if (c == 1) { res.add(w) } } return res.toTypedArray() } }class Solution { func uncommonFromSentences(_ s1: String, _ s2: String) -> [String] { let words = (s1 + " " + s2).split(separator: " ").map { String($0) } var count = [String: Int]() for w in words { count[w, default: 0] += 1 } var res = [String]() for (w, c) in count { if c == 1 { res.append(w) } } return res } }impl Solution { pub fn uncommon_from_sentences(s1: String, s2: String) -> Vec<String> { let mut count = HashMap::new(); for w in s1.split(' ').chain(s2.split(' ')) { *count.entry(w).or_insert(0) += 1; } count.into_iter() .filter(|&(_, c)| c == 1) .map(|(w, _)| w.to_string()) .collect() } }复杂度分析
- 时间复杂度:$O(n + m)$,其中 $n$ 和 $m$ 分别是
s1与s2的长度。切分、计数与最终遍历均线性扫描,哈希表的读写平均为 $O(1)$; - 空间复杂度:$O(n + m)$,哈希表在最坏情况下需要存储两句话中所有不同的单词。
解法二:Hash Map 简洁版(内置计数工具)
核心直觉
解法二是对解法一的纯语法精简:逻辑完全一致,只是借助语言内置的计数/分组函数(如 Python 的Counter、Kotlin 的groupingBy、Java 的 Stream API)减少样板代码。它更利于面试中快速书写,可读性与一脉相承。
算法步骤
- 合并并切分两句话得到单词列表;
- 使用内置计数器或分组函数统计单词频次;
- 过滤出频次恰好为
1的单词; - 返回过滤结果。
多语言实现
class Solution: def uncommonFromSentences(self, s1: str, s2: str) -> List[str]: return [w for w, cnt in Counter(s1.split(" ") + s2.split(" ")).items() if cnt == 1]public class Solution { public String[] uncommonFromSentences(String s1, String s2) { String[] words = (s1 + " " + s2).split(" "); Map<String, Integer> count = new HashMap<>(); for (String w : words) { count.put(w, count.getOrDefault(w, 0) + 1); } return count.entrySet() .stream() .filter(e -> e.getValue() == 1) .map(Map.Entry::getKey) .toArray(String[]::new); } }class Solution { public: vector<string> uncommonFromSentences(string s1, string s2) { unordered_map<string, int> count; istringstream ss(s1 + " " + s2); string w; while (ss >> w) { count[w]++; } vector<string> res; for (auto& [w, c] : count) { if (c == 1) { res.push_back(w); } } return res; } };class Solution { /** * @param {string} s1 * @param {string} s2 * @return {string[]} */ uncommonFromSentences(s1, s2) { const words = (s1 + ' ' + s2).split(' '); const count = new Map(); for (const w of words) { count.set(w, (count.get(w) || 0) + 1); } return [...count.entries()].filter(([_, c]) => c === 1).map(([w]) => w); } }public class Solution { public string[] UncommonFromSentences(string s1, string s2) { string[] words = (s1 + " " + s2).Split(' '); Dictionary<string, int> count = new Dictionary<string, int>(); foreach (string w in words) { if (count.ContainsKey(w)) { count[w]++; } else { count[w] = 1; } } return count.Where(e => e.Value == 1) .Select(e => e.Key) .ToArray(); } }func uncommonFromSentences(s1 string, s2 string) []string { words := strings.Split(s1 + " " + s2, " ") count := make(map[string]int) for _, w := range words { count[w]++ } res := []string{} for w, c := range count { if c == 1 { res = append(res, w) } } return res }class Solution { fun uncommonFromSentences(s1: String, s2: String): Array<String> { val words = "$s1 $s2".split(" ") val count = words.groupingBy { it }.eachCount() return count.filter { it.value == 1 } .keys .toTypedArray() } }class Solution { func uncommonFromSentences(_ s1: String, _ s2: String) -> [String] { let words = (s1 + " " + s2).split(separator: " ").map { String($0) } var count = [String: Int]() for w in words { count[w, default: 0] += 1 } return count.filter { $0.value == 1 }.map { $0.key } } }impl Solution { pub fn uncommon_from_sentences(s1: String, s2: String) -> Vec<String> { let mut count = HashMap::new(); for w in s1.split(' ').chain(s2.split(' ')) { *count.entry(w).or_insert(0) += 1; } count.into_iter() .filter(|&(_, c)| c == 1) .map(|(w, _)| w.to_string()) .collect() } }复杂度分析
- 时间复杂度:$O(n + m)$;
- 空间复杂度:$O(n + m)$;
其中 $n$ 和 $m$ 分别是字符串
s1与s2的长度。虽然写法更简洁,但两版解法的渐进复杂度完全相同。
常见陷阱
陷阱一:误解"不常见"的定义
"不常见"指的是单词在两句话合并后的整体中恰好出现一次,而不是"只出现在其中一句话里"。
- 错误理解:检查单词出现在一句话而没出现在另一句话;
- 错误推论:同一句话中出现两次的单词被误判为不常见(例如
s1 = "apple apple",apple合并后频次为 2,绝不是不常见)。
只有把两句话合并统一计数,才能避免这一系统性错误。
陷阱二:错误的字符串切分方式
切分句子时要注意边界情况,例如多个连续空格或空串。虽然题目保证单词之间是单个空格,但拼接s1与s2时若忘记在中间补空格(例如直接写s1 + s2),会导致边界处的两个单词粘连成一个错误单词,例如"hello"与"world"被拼成"helloworld",从而得到错误答案。
# 错误:s1 与 s2 边界单词会粘连 words = (s1 + s2).split(" ") # 正确:中间显式补一个空格 words = (s1 + " " + s2).split(" ")小结与延伸
「两句话中的不常见单词」是典型的"哈希表统计频次"入门题:把多来源数据合并、统一计数、再按频次过滤。该模式可平滑迁移到仓库中同类型的计数类问题(如统计数组元素频次、判断字符串构成等场景),也是后续学习更复杂计数优化(如前缀和、差分数组、滑动窗口维护频次)的基础。
本文中的两种解法均来源于 articles/uncommon-words-from-two-sentences.md,完整仓库的题解总览见 README.md。建议读者先独立推导"合并计数"这一关键步骤,再对照九种语言的实现,重点体会内置计数工具(PythonCounter、KotlingroupingBy、Java Stream)如何在不改变算法本质的前提下压缩代码量。
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考