Problem overview
The struct Twitter supports five operations: Twitter::new() creates an empty service, post_tweet(user_id, tweet_id) posts a tweet, follow(follower_id, followee_id) and unfollow(follower_id, followee_id) change who sees whose tweets, and get_news_feed(user_id) returns a Vec<i32> with the ids of the 10 newest tweets by the user and the people the user follows, newest first.
Following goes one way only. Following someone twice, following yourself, or unfollowing someone you don't follow changes nothing, and your own tweets are always in your feed. A feed with fewer than 10 tweets returns them all, and an empty feed returns an empty vector.
For example, user 1 posts tweet 5, follows user 2, and user 2 posts tweet 6. User 1's feed is then [6, 5], because 6 is newer. After unfollow(1, 2) the feed is [5] again.
The statement asks for O(1) time for post_tweet, follow and unfollow, and O(F log F) for get_news_feed, where F is the number of people the user follows.
Approach: merge each person's tweets with a heap
The simple approach gathers every tweet of the user and of everyone they follow, sorts the lot by time and takes the first 10. That cost grows with every tweet ever posted, even though the feed shows only 10.
Two facts make it cheaper. First, each person's tweets are already in order, because they are appended as they are posted. Second, the feed needs only the 10 newest, so it never has to look far into anyone's history. This is the same merge as combining sorted lists: the heap holds the next candidate of each list, and the largest candidate is the next tweet of the feed.
Tweet ids don't tell which tweet is newer: in the second example, tweet 106 is newer than tweet 201, and ids can be arbitrary anyway. So the struct keeps a global clock. Every tweet gets the next time, so a bigger time always means a newer tweet.
The structure has three parts: the clock, a map from each user to a vector of (time, tweet_id) pairs (oldest first), and a map from each user to the set of ids they follow. A set makes a repeated follow harmless and makes both follow and unfollow O(1).
To build a feed, put the newest tweet of each person into a max-heap, as a tuple (time, user, index) where index is the tweet's position in that user's vector. Tuples compare field by field, and times are all different, so the top is always the newest tweet not yet shown. Pop it, add its id to the feed, and push the same person's previous tweet, at index - 1, if there is one. Stop at 10 ids or when the heap is empty.
Take the second example: users 1 and 2 take turns posting 101 to 106 and 201 to 206, and user 1 follows user 2. The vectors hold times 1, 3, 5, 7, 9, 11 for user 1 and 2, 4, 6, 8, 10, 12 for user 2. The heap starts with each user's newest tweet, and the first three rounds go like this:
start: [(11, 1, 5), (12, 2, 5)]
take tweet 206
heap: [(10, 2, 4), (11, 1, 5)]
take tweet 106
heap: [(9, 1, 4), (10, 2, 4)]
take tweet 205
heap: [(8, 2, 3), (9, 1, 4)]
The heap lists are printed in ascending order, so the entry on top is the last one.
Step by step
- Declare the clock
timeas ani32. - Declare
tweets, aHashMap<i32, Vec<(i32, i32)>>from user id to that user's(time, tweet_id)pairs. - Declare
following, aHashMap<i32, HashSet<i32>>from user id to the ids the user follows. - In
new, return the struct with the clock at 0 and two empty maps. - In
post_tweet, advance the clock by 1. - Push the pair
(self.time, tweet_id)onto the user's vector, creating the vector on the user's first post withentry(...).or_insert(Vec::new()). Times only grow, so each vector stays ordered from oldest to newest. - In
get_news_feed, start a vector of users withuser_iditself. - Extend it with the followed ids, if the user follows anyone. No one appears twice, because
follownever stores a user following themselves. - Create the max-heap
newest. - Loop over the users.
- Skip the users who never posted, by testing
self.tweets.get(&user)withif let. - Find the newest tweet's index: the vector's length minus 1.
- Push
(time, user, index)into the heap, with the time read from the pair's field.0. - Create the vector
feed. - Loop while the heap has entries and the feed has fewer than 10 ids.
- Pop the top tuple and unpack it, ignoring the time.
- Push that tweet's id, the pair's field
.1, onto the feed. - Push the same user's previous tweet if
index > 0, so the heap again holds one candidate per person. - Return
feed. - In
follow, return early when a user would follow themselves. - Insert the followee into the follower's set, creating the set on first use.
- In
unfollow, remove the followee from the follower's set if the follower has one. Removing an id that isn't there does nothing.
The solution in Rust
use std::collections::{BinaryHeap, HashMap, HashSet};
struct Twitter {
time: i32,
tweets: HashMap<i32, Vec<(i32, i32)>>,
following: HashMap<i32, HashSet<i32>>,
}
impl Twitter {
fn new() -> Twitter {
Twitter { time: 0, tweets: HashMap::new(), following: HashMap::new() }
}
fn post_tweet(&mut self, user_id: i32, tweet_id: i32) {
self.time += 1;
self.tweets.entry(user_id).or_insert(Vec::new()).push((self.time, tweet_id));
}
fn get_news_feed(&self, user_id: i32) -> Vec<i32> {
let mut users = vec![user_id];
if let Some(followees) = self.following.get(&user_id) {
users.extend(followees);
}
let mut newest = BinaryHeap::new();
for user in users {
if let Some(user_tweets) = self.tweets.get(&user) {
let index = user_tweets.len() - 1;
newest.push((user_tweets[index].0, user, index));
}
}
let mut feed = Vec::new();
while !newest.is_empty() && feed.len() < 10 {
let (_, user, index) = newest.pop().unwrap();
feed.push(self.tweets[&user][index].1);
if index > 0 {
newest.push((self.tweets[&user][index - 1].0, user, index - 1));
}
}
feed
}
fn follow(&mut self, follower_id: i32, followee_id: i32) {
if follower_id == followee_id {
return;
}
self.following.entry(follower_id).or_insert(HashSet::new()).insert(followee_id);
}
fn unfollow(&mut self, follower_id: i32, followee_id: i32) {
if let Some(followees) = self.following.get_mut(&follower_id) {
followees.remove(&followee_id);
}
}
}
Using it on the lesson's examples gives:
post_tweet(1, 5) feed(1) = [5]
follow(1, 2)
post_tweet(2, 6) feed(1) = [6, 5]
unfollow(1, 2) feed(1) = [5]
With users 1 and 2 posting in turns as above, user 1's feed is 206 106 205 105 204 104 203 103 202 102 and user 2's feed, which includes only user 2's own tweets, is 206 205 204 203 202 201.
The Rust details worth a closer look:
entry(...).or_insert(...). It returns a mutable reference to the value stored for the key, inserting the given value first when the key is missing. That is why.push(...)and.insert(...)can be chained onto it.&selfversus&mut self.get_news_feedonly reads, so it takes&self, and the heap it builds is a local variable.post_tweet,followandunfollowchange the maps and take&mut self.getandget_mutreturn anOption.if let Some(followees) = self.following.get(&user_id)runs its block only when the key is there.get_mutlends the set mutably, whichremoveneeds.users.extend(followees).followeesis a&HashSet<i32>, which iterates over&i32. A vector ofi32can be extended from references toi32, becausei32isCopy.- Tuples in a
BinaryHeap.BinaryHeapis a max-heap, and tuples compare field by field, so the tuple with the largest time is on top. The heap's element type is inferred from the firstpush. - Indexing a map.
self.tweets[&user]panics when the key is missing. It is safe here, because the heap only holds users who have a vector. - The subtraction
len() - 1. It is ausizeand would wrap or panic for an empty vector. A vector exists only after a first post, so it always has at least one element.
Complexity
Time. post_tweet advances the clock and appends to a vector: O(1) on average. follow and unfollow are one set operation each, also O(1) on average. get_news_feed pushes at most F + 1 starting entries into the heap, which costs O(F log F), and then does at most 10 rounds of one pop and one push, each O(log F). The total is O(F log F), and it does not depend on how many tweets exist.
Space. The maps hold every tweet and every follow relation, so memory is O(T + R) for T tweets and R follow relations. A single feed call needs O(F) extra space for the user list and the heap, plus the 10 ids it returns.
Edge cases and common mistakes
- Using tweet ids as times. After posting 900, 3 and 500 as user 1, the feed is
[500, 3, 900]: newest first by posting order, not by id. - An empty feed. A user who has not posted and follows nobody gets an empty vector.
- Following yourself. Without the early return, the user's id would be in the list twice and each of their tweets would show up twice. After
follow(3, 3)andpost_tweet(3, 7), the feed is just[7]. - Following someone before they post. It works, because the feed reads tweets at the time of the call. After
follow(3, 4)and a later post of tweet 8 by user 4, user 3's feed is[8, 7]. - Repeated follows and useless unfollows. A second
follow(3, 4)leaves the feed at[8, 7], andunfollow(3, 4)before any follow does nothing. - Pushing every tweet into the heap. It works but costs more with every tweet ever posted. Pushing one candidate per person is what bounds the cost.
- Forgetting to push the previous tweet. Without it, each person would contribute at most one tweet to the feed.
- More than 10 tweets. A user with 30 tweets (ids 0 to 29) gets
29 28 27 26 25 24 23 22 21 20.
Testing
- The first example gives the feeds
5,6 5and5. - The second example gives
206 106 205 105 204 104 203 103 202 102for user 1 and206 205 204 203 202 201for user 2. - The third example gives an empty line, then
7,7and8 7.
