目录
1、题目描述
2、解体关键知识点: 统计思维,统计法只是一种跳过过程、直接数最终状态的技巧
4、统计思维方法:
5、对比
题目描述
学校食堂里有一排 圆形(0)和 方形(1)三明治。每个学生都有自己喜欢的三明治形状。
食堂按照栈的顺序提供三明治,而学生按照队列的顺序取餐。
- 如果队首的学生喜欢当前最上面的三明治,他就吃掉它并离开队列。
- 否则,他会放到队列的最后,并让队列中的下一个学生决定是否吃掉当前的三明治。
目标:计算最终无法吃到午餐的学生数量。
解体关键知识点: 统计思维,统计法只是一种跳过过程、直接数最终状态的技巧
-
统计法是“跳脱模拟”的思维
- 你是在按部就班地模拟整个过程,这很自然,很多人第一反应都会这么做(包括很多面试官)。
- 但统计法的思维是:“我不关心过程,我只关心最终状态”,这种思维方式需要更多的抽象能力,需要训练。
-
统计法更像是数学推理
- 它把问题转化为数学计数,直接看哪些人永远吃不到三明治,跳过过程,直达结果。
- 你更习惯从具体的操作入手,而不是直接思考最终状态,这不是笨,而是思维方式的不同。
模拟思维解题:模拟整个过程
func countStudents(students []int, sandwiches []int) int {
lens := len(students)
cal := lens
for len(sandwiches) > 0 && cal >= 0 {
s := students[0]
students = students[1:]
sw := sandwiches[0]
if s == sw {
sandwiches = sandwiches[1:]
lens--
cal = lens
} else {
students = append(students, s)
}
//fmt.Println(students, sandwiches, cal)
cal--
}
return len(sandwiches)
}
完整地模拟了队列操作,通过 append
把不吃当前三明治的学生放到队列尾部,并用 cal
变量来检查是否出现循环但无人吃三明治的情况,逻辑很清晰。
不过,这种方法的时间复杂度为 O(n²),因为 students = students[1:]
每次都会重新创建切片,导致数据移动,性能会受到影响。
模拟法优化:
func countStudents(students []int, sandwiches []int) int {
lens := len(students)
cal := lens
front := 0 // 记录队列的起始索引
for front < lens && cal > 0 {
if students[front] == sandwiches[0] { // 能吃掉三明治
sandwiches = sandwiches[1:]
lens--
cal = lens
} else { // 不能吃,移动到队列尾部
students = append(students, students[front])
}
front++
cal--
}
return len(sandwiches)
}
优化点
- 使用
front
作为队列的头指针,避免students = students[1:]
产生 O(n²) 的切片复制。 - 减少切片创建,只需移动
front
而不是删除元素。 - 时间复杂度优化为 O(n),适合大数据量。
统计思维方法:
思路:
- 直接用
count[0]
和count[1]
统计喜欢每种三明治的学生数,避免不必要的队列操作。 - 逐个处理三明治,如果当前的三明治没人喜欢,直接返回剩余学生数量。
- 时间复杂度 O(n),空间复杂度 O(1),高效解决问题。 🚀
package main
import "fmt"
func countStudents(students []int, sandwiches []int) int {
count := [2]int{0, 0} // count[0] 记录喜欢 0 的人数,count[1] 记录喜欢 1 的人数
for _, s := range students {
count[s]++
}
for _, s := range sandwiches {
if count[s] == 0 { // 如果当前三明治没人喜欢,直接返回剩余的学生数量
return count[0] + count[1]
}
count[s]-- // 有人吃掉了当前的三明治
}
return 0 // 全部学生都能吃到
}
func main() {
students1 := []int{1, 1, 0, 0}
sandwiches1 := []int{0, 1, 0, 1}
fmt.Println(countStudents(students1, sandwiches1)) // 输出: 0
students2 := []int{1, 1, 1, 0, 0, 1}
sandwiches2 := []int{1, 0, 0, 0, 1, 1}
fmt.Println(countStudents(students2, sandwiches2)) // 输出: 3
}
对比
方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
模拟过程法 | O(n²) | O(n) | 适用于小数据量 |
优化版本(双指针) | O(n) | O(n) | 适用于大数据量 |
统计计数法(最优解) | O(n) | O(1) | 最快,适用于任何数据量 |
如何训练统计思维?
需要更习惯跳过“过程”,直接思考“状态”:
-
每次遇到模拟类问题,想一想:
- 有没有“不用完整模拟”的方法?
- 我是否只需要关心某个最终数量?
-
多做一些类似的问题,刻意训练:
- 1221. 分割平衡字符串(统计L和R的数量)
- 696. 计数二进制子串(数相邻 0 和 1 的数量)
- 2264. 字符串中最大的 3 位相同数字(直接统计)
-
复盘自己的代码
- 你现在的
O(n²)
方法 已经是正确的,但可以问自己:- 哪些地方可以少做?
- 哪些地方是重复的?
- 是否可以用“统计”方式优化?
- 你现在的