Problem overview
The class Twitter is a small version of the service. Users post tweets and follow each other, and each user has a news feed: the 10 most recent tweets posted by the user and by the people the user follows, newest first. The methods are:
postTweet(userId, tweetId): post a tweet. Each call is later than all the calls before it.getNewsFeed(userId): return the ids of the 10 newest tweets in the user's feed, newest first. A feed with fewer than 10 tweets returns all of them, and an empty feed returns an empty vector.follow(followerId, followeeId)andunfollow(followerId, followeeId): start or stop following. Following is one way. Following someone twice, following yourself and unfollowing someone you do not follow change nothing, and your own tweets are always in your feed.
In the first example, user 1 posts tweet 5, so the feed is 5. User 1 follows user 2, who posts tweet 6; the feed is 6 5. After the unfollow it is 5 again.
The statement asks for O(1) time for postTweet, follow and unfollow, and O(F log F) for getNewsFeed, where F is the number of people the user follows.
Approach: a clock, per-user lists and a heap that merges them
The simple solution collects every tweet of everyone in the feed, sorts them by time and takes 10. That work grows with every tweet ever posted, even though the feed shows only 10.
The class stores three things:
- a clock,
time, that counts every tweet. Tweet ids do not tell which tweet is newer (tweet 106 can be newer than 201), but the clock does; tweets: for each user, a vector of{time, tweetId}pairs, oldest first. Times only grow, so a new tweet always goes at the end, and the newest tweet of a user is the last one;following: for each user, a set of the ids that user follows. A set ignores a second follow and removes inO(1).
With that, postTweet, follow and unfollow are each a map lookup and one cheap operation.
For getNewsFeed, each person's tweets are already a sorted list, newest at the end. Building the feed means merging those sorted lists and stopping after 10 tweets. A max-heap does that: it holds the newest tweet not yet shown of each person. Take the top of the heap, which is the newest tweet among everyone, add it to the feed, then put that person's previous tweet into the heap, because it is now their newest tweet not shown. The heap never holds more than one entry per person, so each push and pop costs O(log F), and at most 10 tweets are taken out.
Take a small case: user 1 follows user 2, and the posts in order are 101 (user 1), 201 (user 2), 102 (user 1), 202 (user 2), 103 (user 1). The clock gives them the times 1 to 5. The heap starts with the newest tweet of each person, and the feed is built like this:
start: heap size 2
take time 5 (user 1, id 103), push user 1's older tweet; heap size 2
take time 4 (user 2, id 202), push user 2's older tweet; heap size 2
take time 3 (user 1, id 102), push user 1's older tweet; heap size 2
take time 2 (user 2, id 201); heap size 1
take time 1 (user 1, id 101); heap size 0
feed: 103 202 102 201 101
Step by step
- Declare the clock
time, anintset to 0. - Declare
tweetsas anunordered_map<int, vector<pair<int, int>>>: per user, the{time, tweetId}pairs, oldest first. - Declare
followingas anunordered_map<int, unordered_set<int>>: per user, the ids that user follows. - In
postTweet, advance the clock withtime++, so every tweet gets a later time than all before. - Push
{time, tweetId}ontotweets[userId]. The end of the vector is the newest position. - In
getNewsFeed, make the vectorusersholdinguserId: the people whose tweets are in the feed. You cannot follow yourself, so the user is added by hand. - Insert
following[userId]at the end ofusers. - Declare
newest, apriority_queue<array<int, 3>>of entries{time, user, index}. Apriority_queueis a max-heap, and arrays compare their first element first, so the newest tweet is on top. - Loop over
users. - Skip people who never posted, with
if (tweets.count(user)).countonly looks;tweets[user]would create an empty vector. - Compute
index, the position of the user's newest tweet:tweets[user].size() - 1. - Push the entry with the tweet's time, the user and the index.
- Declare
feed, an emptyvector<int>. - Loop while
newestis not empty andfeedhas fewer than 10 ids. - Copy the top entry into
top, thenpop().pop()returns nothing, so the entry is read first. - Read
userandindexfromtop[1]andtop[2]. The time has done its job. - Add the tweet's id,
tweets[user][index].second, tofeed. - If the user has an older tweet (
index > 0), push it: its time,userandindex - 1. This is the merge step. - Return
feed. - In
follow, return at once if the two ids are equal. Storing a self-follow would put the user inuserstwice. - Insert
followeeIdintofollowing[followerId]. A set ignores a repeated follow. - In
unfollow, erasefolloweeIdfromfollowing[followerId]. Erasing a value that is not in the set does nothing. The followee's tweets stay where they are.
The solution in C++
class Twitter {
public:
int time = 0;
unordered_map<int, vector<pair<int, int>>> tweets;
unordered_map<int, unordered_set<int>> following;
void postTweet(int userId, int tweetId) {
time++;
tweets[userId].push_back({time, tweetId});
}
vector<int> getNewsFeed(int userId) {
vector<int> users = {userId};
users.insert(users.end(), following[userId].begin(), following[userId].end());
priority_queue<array<int, 3>> newest;
for (int user : users) {
if (tweets.count(user)) {
int index = tweets[user].size() - 1;
newest.push({tweets[user][index].first, user, index});
}
}
vector<int> feed;
while (!newest.empty() && feed.size() < 10) {
array<int, 3> top = newest.top();
newest.pop();
int user = top[1], index = top[2];
feed.push_back(tweets[user][index].second);
if (index > 0) {
newest.push({tweets[user][index - 1].first, user, index - 1});
}
}
return feed;
}
void follow(int followerId, int followeeId) {
if (followerId == followeeId) {
return;
}
following[followerId].insert(followeeId);
}
void unfollow(int followerId, int followeeId) {
following[followerId].erase(followeeId);
}
};
The C++ details worth a closer look:
- Arrays in a heap.
array<int, 3>is a fixed-size array that compares element by element, like a tuple. The heap therefore orders entries by time first. The times are all different, so the user and the index never decide the order; they only say where the tweet is. - Why the index, not the tweet. Keeping
indexin the entry makes the previous tweet of the same user easy to find: it is atindex - 1. countagainst[].tweets.count(user)reads without inserting. Aftercount(9)on an empty map, the map has 0 entries, and aftertweets[9]it has 1. IngetNewsFeedthe lookupfollowing[userId]does create an empty set for a user who follows nobody; that is harmless.size()is unsigned.tweets[user].size() - 1is safe because thecountcheck means the vector has at least one tweet. The result is stored in anint.- Brace initialisation.
{time, tweetId}makes apairand{a, b, c}makes anarray<int, 3>for thepushcall. - Several variables in one line.
int user = top[1], index = top[2];declares twoints. Thisuseris a new variable inside the loop and is unrelated to the loop variable of the earlierfor.
Running the second example, where users 1 and 2 take turns posting 101 to 106 and 201 to 206 and user 1 follows user 2, user 1's feed is 206 106 205 105 204 104 203 103 202 102. Tweets 101 and 201 are left out. User 2, who does not follow user 1, gets 206 205 204 203 202 201.
Complexity
Time. postTweet is amortized O(1): a hash lookup and a push_back. follow and unfollow are O(1) on average, being hash-set operations. getNewsFeed is O(F log F): building the list of users takes O(F), putting up to F + 1 entries into the heap takes O(F log F), and the loop takes at most 10 entries out and puts at most 10 in, each O(log F). The work does not depend on how many tweets were ever posted.
Space: O(T + S) for T tweets and S follow relations, plus O(F) for the heap and the users list during one getNewsFeed call.
Edge cases and common mistakes
- Ordering by tweet id. Ids are arbitrary. User 1 posts 900, user 2 posts 50, user 1 posts 10: user 3, who follows both, sees
10 50 900, ordered by posting time, not by id. - Your own tweets. A user who follows nobody still sees their own tweets. An empty feed, for a user who has not posted and follows nobody who has, is an empty vector.
- Following yourself. Without the early return, the user would appear twice in
users, and every tweet would show up twice in the feed. Afterfollow(3, 3)andpostTweet(3, 7), the feed is just7. - Unfollowing someone you do not follow. Nothing happens, and the feed does not change.
- Tweets that predate the follow. Following gives access to the person's old tweets too. After
postTweet(2, 1),follow(1, 2)shows1in user 1's feed. After an unfollow it is gone, and after following again it is back. - Following before the first tweet. User 3 follows user 4 who has not posted yet. Once tweet 8 arrives, user 3's feed is
8 7. - More than 10 tweets. A user with 20 tweets, numbered 1 to 20, gets
20 19 18 17 16 15 14 13 12 11. - Pushing every tweet into the heap. It works, but costs time in the number of tweets. Pushing only the newest of each person and refilling from the same person keeps the heap at
Fentries.
Testing
postTweet(1, 5),getNewsFeed(1),follow(1, 2),postTweet(2, 6),getNewsFeed(1),unfollow(1, 2),getNewsFeed(1)gives the three feeds5,6 5,5.getNewsFeed(3)on a newTwittergives an empty feed.follow(3, 3),postTweet(3, 7),getNewsFeed(3)gives7. Thenunfollow(3, 4),follow(3, 4),postTweet(4, 8)andgetNewsFeed(3)give8 7.- Twenty tweets by user 1, ids 1 to 20, then
getNewsFeed(1)gives20 19 18 17 16 15 14 13 12 11.
