昨晚群里有人甩了个“设计推特”,我一看就笑了:这题吧,表面是社交产品,骨子里是“怎么把你关注的那一堆人里,最新的10条糊你脸上”。你要真按产品思路写,能写成PRD;但算法题嘛,它只关心你别把自己写进 O(n²) 的坑里,然后线上一抖你就开始怀疑人生…我懂,我也经常。
我一般会把每个用户的推文当成一条“单向链表”,最新的在头上,时间戳递增(用全局自增就行,别整什么真实时间,面试官不想听你时区事故)。关注关系用个 set。取 feed 的时候呢,就把“我关注的人”的链表头都丢进一个最大堆(Go里自己实现 heap,别偷懒),每次弹出最新那条,再把它的 next 推回去,最多取10条,收工。这个东西像不像你在便利店门口挑夜宵:先看最显眼的,再顺手翻下一层,最多挑10个,钱包不允许你继续翻。
代码我就按这个来,核心就这几坨,够你把题过了,也够你后面扩展成“只保留最近N条”之类的骚操作。
package mainimport ("container/heap")type Tweet struct { id int time int next *Tweet}type User struct { follows map[int]bool head *Tweet}funcnewUser() *User {return &User{follows: map[int]bool{}}}type Twitter struct { time int users map[int]*User}funcConstructor()Twitter {return Twitter{users: map[int]*User{}}}func(tw *Twitter)ensure(uid int) *User { u, ok := tw.users[uid]if !ok { u = newUser() tw.users[uid] = u }// 默认关注自己,不然很多人会漏自己的推…这坑我见过太多次 u.follows[uid] = truereturn u}func(tw *Twitter)PostTweet(userId int, tweetId int) { u := tw.ensure(userId) tw.time++ t := &Tweet{id: tweetId, time: tw.time, next: u.head} u.head = t}func(tw *Twitter)Follow(followerId int, followeeId int) { f := tw.ensure(followerId) tw.ensure(followeeId) f.follows[followeeId] = true}func(tw *Twitter)Unfollow(followerId int, followeeId int) { f := tw.ensure(followerId)// 不让取关自己,省得 feed 变“我看不到我自己”那种尴尬if followerId == followeeId {return }delete(f.follows, followeeId)}// --- 堆:按时间戳做最大堆 ---type item struct { tw *Tweet}type maxHeap []itemfunc(h maxHeap)Len()int { returnlen(h) }func(h maxHeap)Less(i, j int)bool { return h[i].tw.time > h[j].tw.time } // 大的在前func(h maxHeap)Swap(i, j int) { h[i], h[j] = h[j], h[i] }func(h *maxHeap)Push(x any) { *h = append(*h, x.(item)) }func(h *maxHeap)Pop()any { old := *h; n := len(old); x := old[n-1]; *h = old[:n-1]; return x }func(tw *Twitter)GetNewsFeed(userId int) []int { u := tw.ensure(userId) h := &maxHeap{} heap.Init(h)for fid := range u.follows {if fu, ok := tw.users[fid]; ok && fu.head != nil { heap.Push(h, item{tw: fu.head}) } } res := make([]int, 0, 10)for h.Len() > 0 && len(res) < 10 { it := heap.Pop(h).(item) res = append(res, it.tw.id)if it.tw.next != nil { heap.Push(h, item{tw: it.tw.next}) } }return res}
你看这实现,核心复杂度就卡得很舒服:发推 O(1),关注取关 O(1),拉 feed 是 O(F log F + 10 log F),F 是关注人数。面试官要是追问“为啥不是把所有推都拉出来排序”,你就说:哥,我不是不想,我是怕把CPU风扇干到起飞,然后产品经理以为我们在挖矿…反正你就记住一句:别全量排序,堆合并链表,稳得一批。