Problem overview
You write the class Twitter with four methods. PostTweet(userId, tweetId) posts a tweet. Follow(followerId, followeeId) and Unfollow(followerId, followeeId) change who sees whose tweets. GetNewsFeed(userId) returns the ids of the 10 most recent tweets posted by the user and by the people the user follows, newest first, as a List<int>.
The rules around the edges: following is one way; following someone twice, following yourself, and unfollowing someone you do not follow change nothing; your own tweets are always in your feed; a feed with fewer than 10 tweets returns all of them, and a feed with none returns an empty list.
Here is the first example in use. User 1 posts tweet 5, follows user 2, who posts tweet 6, and then unfollows. The feed of user 1 after each stage:
5
6 5
5
Tweet ids do not tell which tweet is newer: in the second example, tweet 106 is newer than tweet 201. Newness comes from the order of the PostTweet calls.
Approach: merge, don't sort everything
The simple solution is to collect every tweet of every person in the feed, sort them by time, and take 10. That costs time that grows with every tweet ever posted, and a user who follows a prolific account pays for its whole history on every call, even though only 10 tweets are shown.
The better idea uses one fact: each user's tweets are already in order, because they are appended as they are posted. Finding the 10 newest tweets across several sorted lists is a merge, the same shape as merging k sorted lists. A heap holds the next candidate from each list:
- Put the newest tweet of each person in the feed into a max-heap ordered by time.
- The top is the newest tweet of all. Take it out and add its id to the feed.
- That person's next older tweet is now their newest tweet not shown yet, so push it in.
- Stop after 10 tweets or when the heap is empty.
The heap never holds more than one entry per person, and at most 10 tweets are taken out. With F people in the feed, GetNewsFeed costs O(F log F) at most to build the heap and a further O(10 log F) for the merge, regardless of how many tweets exist. PostTweet, Follow and Unfollow are O(1).
To compare tweets you need a clock. The class keeps one counter that goes up with every tweet, and stores each tweet as the pair (time, id).
Step by step
- Declare
time, the global clock, starting at 0. Every tweet gets the next value, so a later tweet always has a bigger time. - Declare
tweets, aDictionary<int, List<int[]>>: for each user, the list of their tweets, oldest first, each anint[]holding the time and the id. A user gets a list only when they first post. - Declare
following, aDictionary<int, HashSet<int>>: for each user, the set of people they follow. A set ignores a repeated follow and adds and removes in O(1). PostTweetmoves the clock on, creates the user's list if it is their first tweet, and addsnew int[]{time, tweetId}to the end of it.GetNewsFeedfirst buildsusers: the user themself, then everyone they follow, added withAddRange. The user is added separately becauseFollownever stores a user following themself.- It creates the heap
newest, aPriorityQueue<int[], int>. Its comparerComparer<int>.Create((a, b) => b - a)reverses the order of the priorities, so the biggest time comes out first. Each element is{user, index}, whereindexis the position of a tweet in that user's list, and its priority is the tweet's time. - For each person who has posted, the newest tweet (the last in the list) goes in.
- The loop runs while the heap is not empty and the feed has fewer than 10 ids. It takes the top, adds that tweet's id to the feed, and if the person has an older tweet (
index > 0) it pushes that one in with its own time. Followreturns at once if both ids are equal, creates the set if needed, and adds the followee.Unfollowremoves the followee if the follower has a set.
The solution in C#
class Twitter {
int time = 0;
Dictionary<int, List<int[]>> tweets = new Dictionary<int, List<int[]>>();
Dictionary<int, HashSet<int>> following = new Dictionary<int, HashSet<int>>();
public void PostTweet(int userId, int tweetId) {
time++;
if (!tweets.ContainsKey(userId)) {
tweets[userId] = new List<int[]>();
}
tweets[userId].Add(new int[]{time, tweetId});
}
public List<int> GetNewsFeed(int userId) {
List<int> users = new List<int>();
users.Add(userId);
if (following.ContainsKey(userId)) {
users.AddRange(following[userId]);
}
PriorityQueue<int[], int> newest = new PriorityQueue<int[], int>(Comparer<int>.Create((a, b) => b - a));
foreach (int user in users) {
if (tweets.ContainsKey(user)) {
int index = tweets[user].Count - 1;
newest.Enqueue(new int[]{user, index}, tweets[user][index][0]);
}
}
List<int> feed = new List<int>();
while (newest.Count > 0 && feed.Count < 10) {
int[] top = newest.Dequeue();
int user = top[0], index = top[1];
feed.Add(tweets[user][index][1]);
if (index > 0) {
newest.Enqueue(new int[]{user, index - 1}, tweets[user][index - 1][0]);
}
}
return feed;
}
public void Follow(int followerId, int followeeId) {
if (followerId == followeeId) {
return;
}
if (!following.ContainsKey(followerId)) {
following[followerId] = new HashSet<int>();
}
following[followerId].Add(followeeId);
}
public void Unfollow(int followerId, int followeeId) {
if (following.ContainsKey(followerId)) {
following[followerId].Remove(followeeId);
}
}
}
C# details worth knowing: the heap stores the index of a tweet rather than the tweet itself, which is what makes "the same user's previous tweet" easy to find, at index - 1. The comparer subtracts priorities; that is safe here because times are small positive counters (at most 100,000 calls), so b - a cannot overflow. PriorityQueue orders only by priority, so the times, which are all different, decide everything. HashSet<int>.Remove returns false and does nothing when the value is absent, which is why unfollowing a stranger needs no check.
Complexity
- PostTweet, Follow, Unfollow: O(1) each, apart from the occasional growth of a list or dictionary.
- GetNewsFeed: O(F log F) for F people in the feed. The heap is built by F pushes, and at most 10 pops and 10 pushes follow.
- Space: O(T + L) for T tweets and L follow links. A feed call uses O(F) extra for the heap.
Testing
The second example has users 1 and 2 taking turns posting 6 tweets each (101 to 106, and 201 to 206), with user 1 following user 2. The two feeds:
206 106 205 105 204 104 203 103 202 102
206 205 204 203 202 201
User 1 has 12 tweets in their feed and sees the newest 10, so 101 and 201 are left out. User 2 follows nobody, so only their own tweets appear.
A third check with three users. User 1 posts 10 and 11, user 2 posts 20 and 21, user 3 posts 30, interleaved as 10, 20, 11, 30, 21. User 1 follows users 2 and 3. The feeds of user 1 and user 2:
21 30 11 20 10
21 20
The edge cases from the third example behave as described: a brand-new user with no tweets gets an empty list, Follow(3, 3) leaves tweet 7 appearing once, Unfollow(3, 4) on a stranger changes nothing, and following user 4 before they post still works, so after user 4 posts tweet 8 the feed of user 3 is 8 7.
Edge cases and common mistakes
- Ordering by tweet id. Ids are arbitrary; use the clock.
- Following yourself. If it were stored, your own tweets would be collected twice and show up twice in the feed.
- Pushing all of a person's tweets. Only the newest goes in first; the older ones wait their turn. Pushing everything turns the method back into a full sort.
- Reading the last tweet of a user who never posted. People who never posted have no entry in
tweets, so checkContainsKeybefore reading their last tweet. - A reversed comparer. A default
PriorityQueueis a min-heap; without the comparer the feed would come out oldest first.
