Start learning free

LESSON 404 OF 569 · HEAP

Design Twitter in Python: Per-User Lists and a Heap Merge

Practice O(F log F) Read the full explanation

Design a small version of Twitter. 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.

Implement the class Twitter:

  • Twitter() creates an empty Twitter: no users, no tweets.
  • postTweet(userId, tweetId) posts a new tweet with the id tweetId by the user userId. Each call posts later than all the calls before it.
  • getNewsFeed(userId) returns the ids of the 10 most recent tweets in the user's news feed, newest first. A feed with fewer than 10 tweets returns all of them, and an empty feed returns no ids at all.
  • follow(followerId, followeeId): the user followerId starts following the user followeeId.
  • unfollow(followerId, followeeId): the user followerId stops following the user followeeId.

Following is one way: when user 1 follows user 2, user 1 sees user 2's tweets, old and new, but not the other way round. Following someone you already follow, following yourself and unfollowing someone you don't follow change nothing; your own tweets are always in your feed.

The input is a list of operations, applied in order to one new Twitter. The output has one line for each getNewsFeed call: the tweet ids it returns, separated by spaces, or an empty line for an empty feed. The starter code spells these names the way your language does.

 

Example 1:

Input:
    postTweet(1, 5)
    getNewsFeed(1)
    follow(1, 2)
    postTweet(2, 6)
    getNewsFeed(1)
    unfollow(1, 2)
    getNewsFeed(1)
Output:
5
6 5
5
Explanation:
User 1's first feed holds their own tweet 5.
User 1 then follows user 2, who posts tweet 6: it is newer than 5, so it comes first.
After the unfollow, user 2's tweets leave user 1's feed again.

Example 2:

Input:
    follow(1, 2)
    postTweet(1, 101)
    postTweet(2, 201)
    postTweet(1, 102)
    postTweet(2, 202)
    ...
    postTweet(1, 106)
    postTweet(2, 206)
    getNewsFeed(1)
    getNewsFeed(2)
Output:
206 106 205 105 204 104 203 103 202 102
206 205 204 203 202 201
Explanation:
Users 1 and 2 take turns posting 6 tweets each (101 to 106 and 201 to 206).
User 1's feed has all 12, but shows only the 10 newest: 101 and 201 are left out.
User 2 doesn't follow user 1, so user 2's feed holds only user 2's own tweets.

Example 3:

Input:
    getNewsFeed(3)
    follow(3, 3)
    postTweet(3, 7)
    getNewsFeed(3)
    unfollow(3, 4)
    follow(3, 4)
    getNewsFeed(3)
    postTweet(4, 8)
    getNewsFeed(3)
Output:

7
7
8 7
Explanation:
The first line of the output is empty: nobody has posted yet, so user 3's feed is empty.
follow(3, 3) changes nothing: tweet 7 appears once. unfollow(3, 4) changes nothing either: user 3 didn't follow user 4.
User 3 follows user 4 before user 4 posts anything; once tweet 8 arrives, it is in user 3's feed.

 

Constraints:

  • 1 <= userId, followerId, followeeId <= 500
  • 0 <= tweetId <= 109
  • All the tweets have different ids.
  • At most 105 calls in total are made to postTweet, getNewsFeed, follow and unfollow.

Note:

postTweet, follow and unfollow should run in O(1) time, and getNewsFeed in O(F log F) time, where F is the number of people the user follows. Collecting and sorting all the tweets of those people at every getNewsFeed would take time that grows with every tweet ever posted.

STEP 1 OF 23
    Ctrl + Enter to check

    PYTHON · HEAP

    Design Twitter in Python, explained

    Problem overview

    The class Twitter models a tiny social feed with four operations:

    • postTweet(userId, tweetId) records a new tweet. Every call posts later than all the calls before it.
    • getNewsFeed(userId) returns the ids of the 10 most recent tweets written by the user and by the people the user follows, newest first. A shorter feed returns all its tweets, and an empty feed returns an empty list.
    • follow(followerId, followeeId) and unfollow(followerId, followeeId) change who sees whose tweets.

    Following goes one way. If user 1 follows user 2, user 1 sees user 2's tweets, old and new, but user 2 does not see user 1's. Following someone twice, following yourself and unfollowing someone you don't follow all change nothing, and your own tweets are always in your feed.

    Example: user 1 posts tweet 5, so the feed is [5]. User 1 follows user 2, who posts tweet 6. The feed is now [6, 5], because 6 is newer. After unfollow(1, 2) it is [5] again.

    Approach: merge sorted lists with a heap

    The simple approach keeps all tweets in one list. To build a feed, it scans every tweet ever posted, keeps those whose author is in the user's group, sorts them by time and takes 10. That costs time that grows with every tweet in the system, even if the user follows only two quiet people.

    A better design keeps each user's tweets in their own list. Because the clock only goes up, each list is already sorted from oldest to newest, with no sorting needed. A feed is then a merge of a few sorted lists, and only the 10 newest tweets matter. The newest overall tweet is the newest tweet of one of the people involved, so the merge starts with just one candidate per person. Taking the newest candidate and replacing it with that same person's next older tweet gives the next newest overall, and so on, at most 10 times. A max-heap on time holds the candidates.

    The stated costs are O(1) for postTweet, follow and unfollow and O(F log F) for getNewsFeed, where F is the number of people the user follows. The heap holds at most one entry per person, so each push and pop costs O(log F), and a feed does at most F pushes at the start and 10 more pops and pushes after that.

    Step by step

    1. In __init__, set self.time to 0. This global clock counts every tweet ever posted. Tweet ids can't serve as the clock, because an id says nothing about when the tweet was posted.
    2. Set self.tweets to an empty dictionary: for each user id, a list of (time, tweetId) pairs, oldest first. A user gets a list on their first tweet.
    3. Set self.following to an empty dictionary: for each user id, the set of ids that user follows. A set ignores a second follow of the same person.
    4. In postTweet, add 1 to self.time, so each tweet has a unique, larger time than the one before.
    5. If the user has no list yet, create an empty one.
    6. Append (self.time, tweetId). Times only grow, so the list stays sorted and the newest tweet is at the end.
    7. In getNewsFeed, start a list users with userId, then extend it with everyone the user follows, if anyone. follow never stores a user as following themselves, so nobody appears twice.
    8. Build the heap newest. For each person who has posted, push one entry (-time, user, index): the time of their newest tweet with a minus sign, the person, and the position of that tweet in their list. heapq is a min-heap, so negating the time puts the newest tweet on top.
    9. Create feed, an empty list. Loop while newest isn't empty and feed has fewer than 10 ids.
    10. Pop the top entry, unpack it into _, user and index, and append that tweet's id to feed. The time has done its job, so it goes into _.
    11. If the person has an older tweet (index > 0), push it the same way, with index - 1. That tweet is now their newest not yet shown.
    12. Return feed.
    13. In follow, return at once if the two ids are equal. Otherwise create the follower's set if needed and add the followee.
    14. In unfollow, if the follower has a set, call discard on it. Unlike remove, discard does nothing when the value is missing, so unfollowing a stranger is harmless. The followee's tweets stay stored; they are just no longer collected.

    The solution in Python

    class Twitter:
        def __init__(self):
            self.time = 0
            self.tweets = {}
            self.following = {}
    
        def postTweet(self, userId: int, tweetId: int) -> None:
            self.time += 1
            if userId not in self.tweets:
                self.tweets[userId] = []
            self.tweets[userId].append((self.time, tweetId))
    
        def getNewsFeed(self, userId: int) -> List[int]:
            users = [userId]
            if userId in self.following:
                users.extend(self.following[userId])
            newest = []
            for user in users:
                if user in self.tweets:
                    index = len(self.tweets[user]) - 1
                    heapq.heappush(newest, (-self.tweets[user][index][0], user, index))
            feed = []
            while newest and len(feed) < 10:
                _, user, index = heapq.heappop(newest)
                feed.append(self.tweets[user][index][1])
                if index > 0:
                    heapq.heappush(newest, (-self.tweets[user][index - 1][0], user, index - 1))
            return feed
    
        def follow(self, followerId: int, followeeId: int) -> None:
            if followerId == followeeId:
                return
            if followerId not in self.following:
                self.following[followerId] = set()
            self.following[followerId].add(followeeId)
    
        def unfollow(self, followerId: int, followeeId: int) -> None:
            if followerId in self.following:
                self.following[followerId].discard(followeeId)
    

    To see the merge at work, let user 1 follow user 2, and let them post four tweets in turn:

    t = Twitter()
    t.follow(1, 2)
    t.postTweet(1, 10)
    t.postTweet(2, 20)
    t.postTweet(2, 21)
    t.postTweet(1, 11)
    print(t.tweets)
    print(t.following)
    print(t.getNewsFeed(1))
    
    {1: [(1, 10), (4, 11)], 2: [(2, 20), (3, 21)]}
    {1: {2}}
    [11, 21, 20, 10]
    

    Each list is sorted by time. The heap starts with the newest tweet of each person, 11 (time 4) and 21 (time 3). Popping 11 pushes user 1's older tweet 10. The next pop is 21, which pushes 20. Then 20 comes out, and then 10.

    Python details worth knowing:

    • Heap entries are tuples, and tuples compare element by element. The times are all different, so the comparison is always settled by the first element; the user and the index just travel along to say where the tweet is.
    • set() makes an empty set. {} would be an empty dictionary.
    • users.extend(a_set) appends every element of the set. Sets have no order, which doesn't matter here because the heap orders the tweets.
    • The lesson's editor already has heapq and List available. In a file of your own, add import heapq and from typing import List.

    Complexity

    Time: postTweet, follow and unfollow are a few dictionary and set operations, so O(1) on average. getNewsFeed pushes at most one entry per person, F + 1 in all, and then does at most 10 pop-and-push rounds on a heap of at most F + 1 entries. That gives O(F log F), and it does not depend on how many tweets exist.

    Space: the object stores every tweet once and every follow relation once. A feed needs O(F) extra space for the heap and at most 10 ids for the result.

    Edge cases and common mistakes

    • An empty feed. If nobody in the group has posted, the heap stays empty and the result is [].
    • Following yourself. Storing it would put the user in users twice and show each of their tweets twice. The early return in follow prevents it.
    • Following before posting. The followee has no list yet, so the loop skips them. After they post, their tweets show up in the feed.
    • Using tweet ids as time. The ids don't say which tweet is newer. In the lesson's second example tweet 106 is newer than tweet 201.
    • More than 10 tweets. The loop stops once the feed holds 10 ids.
    • Unfollowing someone you don't follow. discard makes it a no-op, while remove would raise a KeyError.

    Testing

    Two users post six tweets each, taking turns, and user 1 follows user 2:

    t2 = Twitter()
    t2.follow(1, 2)
    for i in range(1, 7):
        t2.postTweet(1, 100 + i)
        t2.postTweet(2, 200 + i)
    print(t2.getNewsFeed(1))
    print(t2.getNewsFeed(2))
    
    [206, 106, 205, 105, 204, 104, 203, 103, 202, 102]
    [206, 205, 204, 203, 202, 201]
    

    User 1's feed holds 12 tweets and shows the newest 10. User 2 does not follow user 1, so their feed holds only their own six.

    The special cases, on a fresh object:

    t3 = Twitter()
    print(t3.getNewsFeed(3))
    t3.follow(3, 3)
    t3.postTweet(3, 7)
    print(t3.getNewsFeed(3), t3.following)
    t3.unfollow(3, 4)
    t3.follow(3, 4)
    t3.postTweet(4, 8)
    print(t3.getNewsFeed(3))
    
    []
    [7] {}
    [8, 7]
    

    Following yourself stores nothing, the tweet appears once, and user 3 sees user 4's tweet even though they followed before it was posted.

    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free