LeetCode-Go 题解精讲:1078. Occurrences After Bigram 三元词序列匹配的 Go 实现
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
本篇技术指南以 LeetCode 第 1078 题《Occurrences After Bigram》(双词出现后的下一个词)为核心,结合 LeetCode-Go 仓库中的 Go 源码实现与配套测试,完整讲解题目的定义、示例、约束条件、线性扫描解法以及复杂度分析。读完本文,你将掌握「以空格切分文本 + 三元素滑动窗口匹配」这类字符串顺序匹配题的标准 Go 写法,并能直接复用仓库内的测试用例验证自己的实现。
题目概述
给定两个单词first和second,在文本text中考察形如"first second third"的出现:其中second紧随first之后,third紧随second之后。对于每一次这样的出现,将第三个词third加入答案并返回。
简单来说,本体的目标是从一段由空格分隔单词的文本中,找出所有「前两个词恰好为first second、第三个词紧随其后」的三元组,并收集其中的第三个词。这是一个典型的顺序敏感的顺序匹配问题,不涉及全局统计或排序,只需保持词在文本中的原始相对次序即可。
示例与输入输出
题目给出了两个标准示例,均已被完整收录在 leetcode/1078.Occurrences-After-Bigram/README.md 中:
示例 1:
Input: text = "alice is a good girl she is a good student", first = "a", second = "good" Output: ["girl","student"]文本中"a good"出现了两次:
- 第 1 次出现在
"... is a good girl ...",紧随其后的是girl; - 第 2 次出现在
"... is a good student",紧随其后的是student。
示例 2:
Input: text = "we will we will rock you", first = "we", second = "will" Output: ["we","rock"]这个示例非常关键,它揭示了出现位置可以重叠这一特性:
- 第 1 个三元组为
"we will we",即第一个"we will"后的下一个词是we; - 第 2 个三元组为
"we will rock",即第二个"we will"后的下一个词是rock。
两次匹配共用了中间的部分单词,说明扫描时每个位置都必须独立检查,不能因为某次匹配成功就跳过后续位置。
约束条件与边界分析
题目对输入做了如下约束(见 README.md):
1 <= text.length <= 1000text由空格分隔的单词组成,每个单词由小写英文字母构成1 <= first.length, second.length <= 10first和second均由小写英文字母构成
这些约束带来的直接影响:
- 数据规模极小(文本最长 1000 字符),任何线性复杂度的做法都绰绰有余;
- 文本中不会出现大写字母、数字、标点或连续多个空格,因此直接用
strings.Split(text, " ")切分是安全的; first与second均非空且仅含小写字母,与文本中的单词形态一致,比较时无需大小写归一化处理。
解题思路
原文档给出的思路非常直白,可以归纳为三步:
- 用空格把
text切分成单词切片words; - 从第三个单词开始(下标 2)依次遍历;
- 对每个位置
i,判断其前两个单词words[i-2]与words[i-1]是否分别等于first与second,若相等则将words[i]加入结果。
从数据结构角度看,这等价于在单词序列上滑动一个长度为 3 的固定窗口(words[i-2]、words[i-1]、words[i]),窗口每次右移一个单词,检查窗口前两个元素是否命中目标二元组,命中则收集窗口的第三个元素。由于窗口只向前移动、每个位置只比较一次,天然支持示例 2 中重叠出现的场景。
Go 源码实现逐行解析
仓库中的实现位于 leetcode/1078.Occurrences-After-Bigram/1078. Occurrences After Bigram.go(注意函数名findOcurrences沿用了原题目的拼写,未额外纠正常见的 "Occurrences" 双写):
package leetcode import "strings" func findOcurrences(text string, first string, second string) []string { var res []string words := strings.Split(text, " ") if len(words) < 3 { return []string{} } for i := 2; i < len(words); i++ { if words[i-2] == first && words[i-1] == second { res = append(res, words[i]) } } return res }逐行说明:
var res []string:声明结果切片,初始为nil;当没有命中时返回的将是空切片,符合题目对空结果的预期。words := strings.Split(text, " "):以单个空格为分隔符切分文本。由于约束保证单词之间恰好由单个空格分隔,这种切分是精确的;若存在连续空格,Split会产出空字符串元素,但在此题约束下不会发生。if len(words) < 3:提前返回。当单词总数不足 3 个时,任何位置都不可能构成first second third三元组,直接返回空切片,避免后续下标i-2越界的隐患(虽然循环本身从i = 2开始,天然不会越界,但提前返回使意图更清晰)。- 循环
for i := 2; i < len(words); i++:i指向当前窗口的第三个词。窗口左边界为i-2,中间为i-1,正好覆盖三元组。 if words[i-2] == first && words[i-1] == second:同时满足「前两个词分别等于first和second」才收集words[i]。注意 Go 中strings.Split返回的切片与字符串常量比较是逐字节相等比较,复杂度为 O(词长),而单词长度受1 <= first.length, second.length <= 10限制,比较开销极小。res = append(res, words[i]):将命中的第三个词追加到结果。由于窗口逐个位置扫描,示例 2 中重叠出现的两个三元组都会被独立捕获。
该实现与题目在 website/content/ChapterFour/1000~1099/1078.Occurrences-After-Bigram.md 中呈现的代码完全一致,是仓库中该题的唯一解法。
测试用例与验证
仓库为本题提供了完整的单元测试 leetcode/1078.Occurrences-After-Bigram/1078. Occurrences After Bigram_test.go,采用「参数 + 期望答案」的结构化表驱动测试风格(para1078/ans1078结构体):
qs := []question1078{ { para1078{"alice is a good girl she is a good student", "a", "good"}, ans1078{[]string{"girl", "student"}}, }, { para1078{"we will we will rock you", "we", "will"}, ans1078{[]string{"we", "rock"}}, }, { para1078{"a good", "a", "good"}, ans1078{[]string{}}, }, }除题目给出的两个示例外,测试还补充了一个边界用例:
text = "a good",first = "a",second = "good":单词总数只有 2 个,不足以构成三元组,期望输出为空切片。这个用例直接覆盖了源码中len(words) < 3的提前返回分支,确保边界处理正确。
测试通过fmt.Printf打印每组输入的参数与输出,便于人工核对:
【input】:{alice is a good girl she is a good student a good} 【output】:[girl student] 【input】:{we will we will rock you we will} 【output】:[we rock] 【input】:{a good a good} 【output】:[]运行该题测试只需在仓库根目录执行:
go test -v ./leetcode/1078.Occurrences-After-Bigram/...若希望统计整个题解库的覆盖率,仓库根目录的 gotest.sh 提供了现成脚本(以 atomic 模式生成覆盖文件coverage.txt):
go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...复杂度分析
设文本切分后的单词数量为n,则:
- 时间复杂度:O(n)。
strings.Split需遍历整个文本切分出单词,耗时 O(L)(L 为文本长度,L ≈ n × 平均词长);主循环对n个单词各做常数次字符串比较。整体为线性时间,在1 <= text.length <= 1000的限制下性能无压力。 - 空间复杂度:O(n)。
words切片保存了全部单词引用,结果切片最多保存n-2个词,两者均为 O(n)。
若希望进一步省去切分带来的空间开销,可以改用strings.Fields配合逐词流式扫描,或直接用strings.Split后复用同一份切片,在本题规模下差异可忽略;当前实现以清晰为优先,遵循了仓库「简单题用简单解」的风格。
边界情况与易错点小结
- 单词总数不足 3 个:如
"a good",无论first、second如何取值都不可能形成三元组,必须返回空结果。源码通过len(words) < 3提前兜底。 - 出现位置重叠:示例 2 中两次
"we will"出现仅相隔一个词,扫描时必须逐位推进窗口,不能在命中后跳过后续位置,否则会漏掉rock。 - 区分连续与相邻语义:题目要求的是「立即紧随」的相邻关系,而非文本中任意位置的包含关系,因此不能用子串搜索(如
strings.Contains)代替,必须按单词粒度精确比对。 - 返回空结果的形式:无命中时返回空切片而非
nil切片,保证与期望输出[]可比较、可序列化。
延伸:与同类字符串匹配题的联系
从本题可以归纳出一个通用的字符串顺序匹配模式:先将文本按分隔符拆分为词/字符序列,再在序列上以固定长度窗口或双指针进行顺序比对。仓库中同为 String 分类下的题目,如 028. Find the Index of the First Occurrence in a String、1455. Check If a Word Occurs As a Prefix of Any Word in a Sentence 等,都建立在类似的切分与定位思想上。掌握本题的「三词窗口」写法,再遇到需要提取固定上下文(如前缀、后缀、相邻词)的题目时,可以快速迁移。
总结
LeetCode 1078《Occurrences After Bigram》是一道典型的入门级字符串顺序匹配题,核心解法只有三步:空格切分、三词窗口、逐位比较收集。LeetCode-Go 仓库用 16 行 Go 代码给出了清晰实现,并配套覆盖两个官方示例与一个不足三词边界的表驱动测试。理解这道题的价值不在于算法难度,而在于建立「序列 + 定长窗口」的建模直觉——这种能力在后续处理更复杂的滑动窗口与子串匹配问题时会反复用到。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考