LeetCode 355中等堆 / 优先队列
设计推特 图解题解
这道题到底在问什么
支持 postTweet(发推) / follow(关注) / unfollow(取关) / getNewsFeed(取最近10条动态,含自己与关注者,按时间倒序)。
- 输入
- 小赵关注小钱/小孙/小李;四人交错发推;getNewsFeed(小赵)
- 输出
- 按发推时间从新到旧取最近10条
最优解:一步一步想明白
- 3记住这条「各人推文按时间存·入堆·弹最新·补下一条」,下面每一步都在套它。
- 4postTweet 给当前全局时间戳 +1 并把新推文插到该用户链头;follow 把对方加入关注集。t 越大表示越新。
- 5getNewsFeed(小赵):相关链有 4 条(自己 + 关注的 3 人)。堆容量 4,空位占位。先把每条链的链头(各自最新一条)入堆。
- 6把 @小赵 的最新推文103(t9) 入堆: 先放到堆末尾(下标0),再按「时间越新越靠堆顶」上浮。
- 7把 @小钱 的最新推文203(t8) 入堆: 先放到堆末尾(下标1),再按「时间越新越靠堆顶」上浮。
- 8t8 不比父 t9 新,停止上浮,就位(标绿)。堆顶 t9 仍是当前最新。
- 9把 @小孙 的最新推文303(t10) 入堆: 先放到堆末尾(下标2),再按「时间越新越靠堆顶」上浮。
- 10比较 t10 与父 t9:t10 更新(更晚),上浮——和父交换。
- 11交换完成,上浮到下标0。
- 12把 @小李 的最新推文403(t11) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 13比较 t11 与父 t8:t11 更新(更晚),上浮——和父交换。
- 14交换完成,上浮到下标1。
- 15比较 t11 与父 t10:t11 更新(更晚),上浮——和父交换。
- 16交换完成,上浮到下标0。
- 17堆顶 t11(@小李 推文403) 是当前所有链头里最新的,弹出接入新闻流。
- 18推文403 接入新闻流(第1条)。把堆末元素 t8 暂放堆顶(标紫),再下沉。
- 19比较 t8 与更新的子节点 t10:子节点更新,下沉——交换。
- 20交换完成,t8 沉到下标1。
- 21t8 已不比子节点新,下沉结束(标绿),堆顶 t10 又是当前最新。
- 22@小李 刚被取走一条,补它链上下一条 推文402(t7) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 23t7 不比父 t8 新,停止上浮,就位(标绿)。堆顶 t10 仍是当前最新。
- 24堆顶 t10(@小孙 推文303) 是当前所有链头里最新的,弹出接入新闻流。
- 25推文303 接入新闻流(第2条)。把堆末元素 t7 暂放堆顶(标紫),再下沉。
- 26比较 t7 与更新的子节点 t9:子节点更新,下沉——交换。
- 27交换完成,t7 沉到下标2。
- 28t7 已不比子节点新,下沉结束(标绿),堆顶 t9 又是当前最新。
- 29@小孙 刚被取走一条,补它链上下一条 推文302(t6) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 30t6 不比父 t8 新,停止上浮,就位(标绿)。堆顶 t9 仍是当前最新。
- 31堆顶 t9(@小赵 推文103) 是当前所有链头里最新的,弹出接入新闻流。
- 32推文103 接入新闻流(第3条)。把堆末元素 t6 暂放堆顶(标紫),再下沉。
- 33比较 t6 与更新的子节点 t8:子节点更新,下沉——交换。
- 34交换完成,t6 沉到下标1。
- 35t6 已不比子节点新,下沉结束(标绿),堆顶 t8 又是当前最新。
- 36@小赵 刚被取走一条,补它链上下一条 推文102(t5) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 37t5 不比父 t6 新,停止上浮,就位(标绿)。堆顶 t8 仍是当前最新。
- 38堆顶 t8(@小钱 推文203) 是当前所有链头里最新的,弹出接入新闻流。
- 39推文203 接入新闻流(第4条)。把堆末元素 t5 暂放堆顶(标紫),再下沉。
- 40比较 t5 与更新的子节点 t7:子节点更新,下沉——交换。
- 41交换完成,t5 沉到下标2。
- 42t5 已不比子节点新,下沉结束(标绿),堆顶 t7 又是当前最新。
- 43@小钱 刚被取走一条,补它链上下一条 推文202(t3) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 44t3 不比父 t6 新,停止上浮,就位(标绿)。堆顶 t7 仍是当前最新。
- 45堆顶 t7(@小李 推文402) 是当前所有链头里最新的,弹出接入新闻流。
- 46推文402 接入新闻流(第5条)。把堆末元素 t3 暂放堆顶(标紫),再下沉。
- 47比较 t3 与更新的子节点 t6:子节点更新,下沉——交换。
- 48交换完成,t3 沉到下标1。
- 49t3 已不比子节点新,下沉结束(标绿),堆顶 t6 又是当前最新。
- 50@小李 刚被取走一条,补它链上下一条 推文401(t4) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 51比较 t4 与父 t3:t4 更新(更晚),上浮——和父交换。
- 52交换完成,上浮到下标1。
- 53t4 不比父 t6 新,停止上浮,就位(标绿)。堆顶 t6 仍是当前最新。
- 54堆顶 t6(@小孙 推文302) 是当前所有链头里最新的,弹出接入新闻流。
- 55推文302 接入新闻流(第6条)。把堆末元素 t3 暂放堆顶(标紫),再下沉。
- 56比较 t3 与更新的子节点 t5:子节点更新,下沉——交换。
- 57交换完成,t3 沉到下标2。
- 58t3 已不比子节点新,下沉结束(标绿),堆顶 t5 又是当前最新。
- 59@小孙 刚被取走一条,补它链上下一条 推文301(t2) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 60t2 不比父 t4 新,停止上浮,就位(标绿)。堆顶 t5 仍是当前最新。
- 61堆顶 t5(@小赵 推文102) 是当前所有链头里最新的,弹出接入新闻流。
- 62推文102 接入新闻流(第7条)。把堆末元素 t2 暂放堆顶(标紫),再下沉。
- 63比较 t2 与更新的子节点 t4:子节点更新,下沉——交换。
- 64交换完成,t2 沉到下标1。
- 65t2 已不比子节点新,下沉结束(标绿),堆顶 t4 又是当前最新。
- 66@小赵 刚被取走一条,补它链上下一条 推文101(t1) 入堆: 先放到堆末尾(下标3),再按「时间越新越靠堆顶」上浮。
- 67t1 不比父 t2 新,停止上浮,就位(标绿)。堆顶 t4 仍是当前最新。
- 68堆顶 t4(@小李 推文401) 是当前所有链头里最新的,弹出接入新闻流。
- 69推文401 接入新闻流(第8条)。把堆末元素 t1 暂放堆顶(标紫),再下沉。
- 70比较 t1 与更新的子节点 t3:子节点更新,下沉——交换。
- 71交换完成,t1 沉到下标2。
- 72t1 已不比子节点新,下沉结束(标绿),堆顶 t3 又是当前最新。
- 73@小李 的推文链走空,无可补;堆里还剩 3 个链头(堆顶 t3,标蓝)继续。
- 74堆顶 t3(@小钱 推文202) 是当前所有链头里最新的,弹出接入新闻流。
- 75推文202 接入新闻流(第9条)。把堆末元素 t1 暂放堆顶(标紫),再下沉。
- 76比较 t1 与更新的子节点 t2:子节点更新,下沉——交换。
- 77交换完成,t1 沉到下标1。
- 78t1 已不比子节点新,下沉结束(标绿),堆顶 t2 又是当前最新。
- 79@小钱 刚被取走一条,补它链上下一条 推文201(t0) 入堆: 先放到堆末尾(下标2),再按「时间越新越靠堆顶」上浮。
- 80t0 不比父 t2 新,停止上浮,就位(标绿)。堆顶 t2 仍是当前最新。
- 81堆顶 t2(@小孙 推文301) 是当前所有链头里最新的,弹出接入新闻流。
- 82推文301 接入新闻流(第10条)。把堆末元素 t0 暂放堆顶(标紫),再下沉。
- 83比较 t0 与更新的子节点 t1:子节点更新,下沉——交换。
- 84交换完成,t0 沉到下标1。
- 85t0 已不比子节点新,下沉结束(标绿),堆顶 t1 又是当前最新。
- 86已取满 10 条(或堆空)。新闻流按时间从新到旧:推文403 → 推文303 → 推文103 → 推文203 → 推文402 → 推文302 → 推文102 → 推文401 → 推文202 → 推文301。getNewsFeed 完成。
⚠️ 容易写错的地方
✗ 错:把所有人的所有推文一次全排序
✓ 对:只把各链头入堆、弹一个补一个
K 路链已各自有序,堆合并只需 O(L·logK),全排序是 O(总推文数·log)
✗ 错:getNewsFeed 忘了带上自己
✓ 对:相关集合 = 关注集 ∪ {自己}
推特首页要看到自己发的推,漏了自己就少内容
✗ 错:unfollow 把自己也取关了
✓ 对:u==v 时不允许取关自己
取关自己会导致看不到自己的推文
完整代码(Java / Python / C++)
Java
class Twitter {
private int time = 0; // 全局时间戳
static class Tweet { int id, t; Tweet next; // 推文链节点
Tweet(int id, int t){ this.id=id; this.t=t; } }
private Map<Integer,Tweet> tweets = new HashMap<>(); // 用户→推文链头(最新)
private Map<Integer,Set<Integer>> follows = new HashMap<>(); // 用户→关注集
public void postTweet(int u, int id){
Tweet h = new Tweet(id, time++);
h.next = tweets.get(u); tweets.put(u, h); // 插到链头
}
public void follow(int u, int v){
follows.computeIfAbsent(u, k->new HashSet<>()).add(v); }
public void unfollow(int u, int v){
if(u!=v && follows.containsKey(u)) follows.get(u).remove(v); }
public List<Integer> getNewsFeed(int u){
// 最大堆:时间越新越靠堆顶
PriorityQueue<Tweet> pq = new PriorityQueue<>((a,b) -> b.t - a.t);
Set<Integer> src = new HashSet<>(follows.getOrDefault(u, new HashSet<>()));
src.add(u); // 包含自己
for(int id : src) if(tweets.get(id)!=null) pq.offer(tweets.get(id)); // 各链头入堆
List<Integer> res = new ArrayList<>();
while(!pq.isEmpty() && res.size() < 10){
Tweet t = pq.poll(); // 弹最新
res.add(t.id);
if(t.next != null) pq.offer(t.next); // 补该链下一条
}
return res;
}
}Python
import heapq
class Twitter:
def __init__(self):
self.time = 0
self.tweets = {} # uid -> [(-t, id), ...] 头部最新
self.follows = {} # uid -> set
def postTweet(self, u, tid):
self.tweets.setdefault(u, []).append((self.time, tid)); self.time += 1
def follow(self, u, v):
self.follows.setdefault(u, set()).add(v)
def unfollow(self, u, v):
if u != v: self.follows.setdefault(u, set()).discard(v)
def getNewsFeed(self, u):
src = set(self.follows.get(u, set())) | {u}
h = [] # 最大堆(用负时间戳)
for uid in src:
lst = self.tweets.get(uid)
if lst:
i = len(lst) - 1 # 该用户最新一条的下标
t, tid = lst[i]
heapq.heappush(h, (-t, tid, uid, i))
res = []
while h and len(res) < 10:
nt, tid, uid, i = heapq.heappop(h) # 弹最新
res.append(tid)
if i > 0: # 补该链下一条(更旧)
pt, ptid = self.tweets[uid][i-1]
heapq.heappush(h, (-pt, ptid, uid, i-1))
return resC++
class Twitter {
int time = 0;
struct Tweet { int id, t; Tweet* next; };
unordered_map<int, Tweet*> tweets; // uid -> 链头(最新)
unordered_map<int, unordered_set<int>> follows;
public:
void postTweet(int u, int id){
tweets[u] = new Tweet{id, time++, tweets.count(u)?tweets[u]:nullptr}; }
void follow(int u, int v){ follows[u].insert(v); }
void unfollow(int u, int v){ if(u!=v) follows[u].erase(v); }
vector<int> getNewsFeed(int u){
auto cmp = [](Tweet* a, Tweet* b){ return a->t < b->t; }; // 最大堆
priority_queue<Tweet*, vector<Tweet*>, decltype(cmp)> pq(cmp);
auto src = follows[u]; src.insert(u);
for(int id : src) if(tweets.count(id)) pq.push(tweets[id]);
vector<int> res;
while(!pq.empty() && res.size() < 10){
Tweet* t = pq.top(); pq.pop(); // 弹最新
res.push_back(t->id);
if(t->next) pq.push(t->next); // 补下一条
}
return res;
}
};复杂度
postTweet
O(1)
新推文插到该用户链头,常数时间
follow/unfollow
O(1)
关注集(哈希集合)增删一项
getNewsFeed
O(K + L·logK)
K=相关用户数,L=10(取的条数);各链头入堆 + 弹补各 O(logK)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 设计推特 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用链表存推文而不是普通数组?+
推文按时间天然倒序追加(新的插链头),getNewsFeed 顺着链就是从新到旧;用堆合并时只需各链头入堆、弹一个顺链取下一个,O(1) 拿后继,不必维护下标或重排。
关注者很多、每人推文很多时会慢吗?+
入堆是 O(K)(K=关注数)。若 K 极大,可只对「最近活跃」的关注者建堆,或缓存上次新闻流增量合并;核心仍是堆只取前 10、不全排序。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 设计推特 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。