Start learning free

LESSON 408 OF 573 · HEAP

Design Twitter in Go: News Feed with a Max-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 25
    Ctrl + Enter to check

    GO · HEAP

    Design Twitter in Go, explained

    Problem overview

    The lesson asks for a small social network. 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 Twitter struct has five parts: Constructor creates an empty one, PostTweet(userId, tweetId) posts a tweet, GetNewsFeed(userId) returns up to 10 tweet ids, newest first, Follow(followerId, followeeId) starts following and Unfollow(followerId, followeeId) stops. Following goes one way: if user 1 follows user 2, user 1 sees user 2's tweets, old and new, but not the other way round. Following someone twice, following yourself, and unfollowing someone you don't follow all 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 now [6 5], because 6 is newer. After the unfollow it is [5] again.

    Approach: merge the users' lists with a heap

    Collecting every tweet of everyone in the feed and sorting them works, but its cost grows with every tweet ever posted, even though only 10 are shown. The statement asks for 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 data is arranged to make that possible:

    • A global clock counts every tweet ever posted, so a newer tweet always has a bigger time. Tweet ids can't tell which tweet is newer (in the second example tweet 106 is newer than 201), but the clock can.
    • Each user has a list of tweets, oldest first. Because the clock only grows, appending keeps each list sorted, and the newest tweet is at the end.
    • Each user has a set of the people they follow.

    The feed is then a merge of several sorted lists, the same idea as merging sorted linked lists. A max-heap holds one candidate per person: that person's newest tweet not yet shown. The top of the heap is the newest tweet overall. Take it, add it to the feed, and push the same person's previous tweet in its place. After at most 10 rounds the feed is done. An older tweet of a person can't be in the feed before their newer one, so one entry per person is enough, and the heap never holds more than F + 1 entries.

    The lesson provides the maxHeap type for container/heap. Its entries are slices of ints ordered by their first number, biggest on top.

    Step by step

    1. Declare the clock time as an int.
    2. Declare tweets as a map[int][][]int: for each user id, a list of tweets, each a []int holding its time and its id.
    3. Declare following as a map[int]map[int]bool. Go has no set type, so a map with true values plays the role of a set: adding twice changes nothing, and adding and removing are O(1).
    4. Make both maps in Constructor. Storing into a nil map stops the program, so they must be created first. time starts at 0.
    5. In PostTweet, advance the clock with this.time++, so every tweet has its own time.
    6. Append []int{this.time, tweetId} to the user's list and store the result back. A missing key reads as a nil slice, and append works on nil.
    7. In GetNewsFeed, start users as a slice holding userId, because your own tweets are always in your feed.
    8. Add the followees with a range loop over this.following[userId]. Ranging over a map with one variable gives its keys, and over a nil map it gives nothing. Follow never stores a user following themselves, so nobody is in users twice.
    9. Create the heap newest as &maxHeap{}. Each entry is []int{time, user, index}: the tweet's time, its author and its position in that author's list.
    10. Push each author's newest tweet. For every user who has posted, the newest tweet is at the last index, len(this.tweets[user]) - 1. Users who never posted have nothing to show.
    11. Loop while the heap isn't empty and feed has fewer than 10 ids. A for with only a condition is Go's while.
    12. Pop the top entry with heap.Pop(newest) and convert it with .([]int), because heap.Pop returns an any.
    13. Read the user and the index from the entry and append that tweet's id to feed. Tweets come out newest first, so the order is right.
    14. If the user has an older tweet (index > 0), push it: its time, the user and index - 1.
    15. Return feed.
    16. Follow returns at once when both ids are equal, makes the inner map if it is nil, and sets the entry to true.
    17. Unfollow calls delete, which does nothing when the key or the whole map is missing.

    The solution in Go

    type Twitter struct {
        time int
        tweets map[int][][]int
        following map[int]map[int]bool
    }
    
    func Constructor() Twitter {
        return Twitter{tweets: map[int][][]int{}, following: map[int]map[int]bool{}}
    }
    
    func (this *Twitter) PostTweet(userId int, tweetId int) {
        this.time++
        this.tweets[userId] = append(this.tweets[userId], []int{this.time, tweetId})
    }
    
    func (this *Twitter) GetNewsFeed(userId int) []int {
        users := []int{userId}
        for followee := range this.following[userId] {
            users = append(users, followee)
        }
        newest := &maxHeap{}
        for _, user := range users {
            if len(this.tweets[user]) > 0 {
                index := len(this.tweets[user]) - 1
                heap.Push(newest, []int{this.tweets[user][index][0], user, index})
            }
        }
        feed := []int{}
        for newest.Len() > 0 && len(feed) < 10 {
            top := heap.Pop(newest).([]int)
            user, index := top[1], top[2]
            feed = append(feed, this.tweets[user][index][1])
            if index > 0 {
                heap.Push(newest, []int{this.tweets[user][index-1][0], user, index - 1})
            }
        }
        return feed
    }
    
    func (this *Twitter) Follow(followerId int, followeeId int) {
        if followerId == followeeId {
            return
        }
        if this.following[followerId] == nil {
            this.following[followerId] = map[int]bool{}
        }
        this.following[followerId][followeeId] = true
    }
    
    func (this *Twitter) Unfollow(followerId int, followeeId int) {
        delete(this.following[followerId], followeeId)
    }
    

    The Go details worth a closer look:

    • Map iteration order is random. The order of users changes from run to run. That is harmless, because the heap orders the tweets by time, and every tweet has a different time.
    • Nil maps. Reading from a nil map is safe and gives the zero value, so this.tweets[user] for a user who never posted is a nil slice of length 0. Writing to a nil map stops the program, which is why Follow creates the inner map before storing in it.
    • delete on a missing map or key does nothing, so Unfollow needs no checks.
    • Type assertion. heap.Pop(newest).([]int) asserts that the any value holds a []int. It would panic on any other type.
    • Empty feeds. feed := []int{} is an empty, non-nil slice, so an empty feed is returned as [].

    Complexity

    Time. PostTweet, Follow and Unfollow are O(1) on average: a counter step, an append and a few map operations. GetNewsFeed builds the list of F + 1 users, pushes up to F + 1 entries and then runs at most 10 rounds of one pop and one push. The heap never holds more than F + 1 entries, so the call takes O(F log F), however many tweets exist.

    Space. The tweets take O(T) for T tweets posted, plus the follow sets. A feed call needs O(F) extra space for the heap.

    Edge cases and common mistakes

    • Using the tweet id as the time. Ids are unrelated to age. In the second example, tweet 201 is older than tweet 106 although its id is bigger; only the clock gives the order.
    • An empty feed. Before anyone posts, GetNewsFeed(3) returns [].
    • Following yourself. After Follow(3, 3) and PostTweet(3, 7), the feed is [7], with tweet 7 once. Storing the self-follow would put user 3 in users twice and show the tweet twice.
    • Following someone before they post. After Follow(3, 4) and then PostTweet(4, 8), the feed is [8 7]. Following is stored as a relationship, not as a copy of tweets.
    • Following twice. A second Follow(3, 4) leaves one entry in the set.
    • Pushing every tweet into the heap. It works, but then the heap grows with all tweets of everyone in the feed. One entry per person keeps it small.
    • More than 10 tweets. With 12 tweets in a feed, only the 10 newest are returned.

    Testing

    The first example, run on a new Twitter:

    [5]
    [6 5]
    [5]
    

    Users 1 and 2 each post six tweets, taking turns (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]: 12 tweets exist and the two oldest are cut. User 2 follows nobody, so user 2's feed is [206 205 204 203 202 201].

    Starting from a new Twitter, GetNewsFeed(3) gives []. Then Follow(3, 3) and PostTweet(3, 7) give [7], then Unfollow(3, 4), Follow(3, 4) and Follow(3, 4) again still give [7] with one followee stored. After PostTweet(4, 8) the feed is [8 7].

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