Start learning free

LESSON 408 OF 573 · HEAP

Design Twitter in TypeScript: a Max-Heap Merge of Each User's Tweets

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

    TYPESCRIPT · HEAP

    Design Twitter in TypeScript, explained

    Problem overview

    The task is to build a small social feed. Users post tweets, follow and unfollow each other, and ask for a news feed: the 10 most recent tweets posted by the user and by the people the user follows, newest first. The class is Twitter, with four methods:

    • postTweet(userId, tweetId) posts a tweet. Each call is later than all the calls before it.
    • getNewsFeed(userId) returns up to 10 tweet ids, newest first. A feed with fewer tweets returns all of them, and an empty feed returns an empty array.
    • follow(followerId, followeeId) and unfollow(followerId, followeeId) change who sees whose tweets. Following is one way. Following someone you already follow, following yourself, and unfollowing someone you don't follow change nothing. Your own tweets are always in your feed.

    Here is a short session on a new Twitter, with what each getNewsFeed(1) returns:

    const t = new Twitter();
    t.postTweet(1, 5);
    console.log(t.getNewsFeed(1).join(" "));   // 5
    t.follow(1, 2);
    t.postTweet(2, 6);
    console.log(t.getNewsFeed(1).join(" "));   // 6 5
    t.unfollow(1, 2);
    console.log(t.getNewsFeed(1).join(" "));   // 5

    Tweet 6 came later than tweet 5, so it comes first. After the unfollow, user 2's tweets leave user 1's feed again. Tweet ids say nothing about age: in a longer run where users 1 and 2 take turns posting 101 to 106 and 201 to 206, user 1's feed is:

    206 106 205 105 204 104 203 103 202 102

    Tweet 106 is newer than tweet 201, so a clock must decide the order, not the ids.

    Approach: a clock, per-user lists and a heap

    The simple approach gathers all tweets of the user and everyone followed, sorts them by time and takes the first 10. That does work proportional to every tweet ever posted by those people, and it grows with each new tweet.

    The better idea uses two facts. Each user's own tweets are already sorted by time, because tweets are appended as they are posted. And a feed needs only 10 tweets. So the feed is a merge of sorted lists, the same idea as merging k sorted linked lists: put the newest tweet of each person into a max-heap, take the top (the newest tweet overall), then replace it with that person's next older tweet. After at most 10 such steps the feed is complete, and older tweets are never touched.

    The data kept by the class:

    • time: a global clock that goes up by 1 for each tweet posted, so a newer tweet always has a bigger time.
    • tweets: for each user id, an array of [time, tweetId] pairs, oldest first.
    • following: for each user id, a Set of the ids they follow. A set ignores a second follow of the same person, and adds and deletes in constant time.

    The lesson provides a ready-to-use MaxHeap. You create it with new MaxHeap(compare), where the compare function works as in sort, and it has insert, extract, peek, size and isEmpty.

    Step by step

    1. Declare the three fields. time starts at 0. tweets is an empty object typed { [userId: number]: number[][] }, an index signature meaning any number can be a key and each value is an array of number arrays. following is typed the same way with Set<number> values.
    2. postTweet: add 1 to this.time, create the user's array if !(userId in this.tweets) (the parentheses make ! apply to the whole in test), and push [this.time, tweetId]. Times only grow, so each array stays sorted with the newest tweet at the end.
    3. getNewsFeed, who is in the feed: start an array users with userId, and if the user follows anyone, spread that set into it with users.push(...this.following[userId]). follow never stores a user following themselves, so nobody appears twice.
    4. Prepare the heap: new MaxHeap((a, b) => a[0] - b[0]) compares entries by their first number, the time, so the newest tweet is on top. Each entry is [time, user, index]: the tweet's time, its author, and its position in that author's array.
    5. Seed it: for each person in users who has posted, insert the entry for their last tweet, at index length - 1. People who never posted have nothing to show.
    6. Merge: while the heap isn't empty and feed has fewer than 10 ids, extract the top entry, unpack it with const [time, user, index] = ..., and push that tweet's id, this.tweets[user][index][1], onto feed.
    7. Refill: if the author has an older tweet (index > 0), insert it as [time of that tweet, user, index - 1]. It is now that person's newest tweet not yet shown. Then return feed.
    8. follow: return at once when the two ids are equal (===), create a new Set for the follower if needed, and add the followee.
    9. unfollow: if the follower has a set, delete the followee from it. Deleting a value that isn't in a set does nothing.

    The solution in TypeScript

    class Twitter {
        time: number = 0;
        tweets: { [userId: number]: number[][] } = {};
        following: { [userId: number]: Set<number> } = {};
    
        postTweet(userId: number, tweetId: number): void {
            this.time++;
            if (!(userId in this.tweets)) {
                this.tweets[userId] = [];
            }
            this.tweets[userId].push([this.time, tweetId]);
        }
    
        getNewsFeed(userId: number): number[] {
            const users: number[] = [userId];
            if (userId in this.following) {
                users.push(...this.following[userId]);
            }
            const newest = new MaxHeap((a, b) => a[0] - b[0]);
            for (const user of users) {
                if (user in this.tweets) {
                    const index = this.tweets[user].length - 1;
                    newest.insert([this.tweets[user][index][0], user, index]);
                }
            }
            const feed: number[] = [];
            while (!newest.isEmpty() && feed.length < 10) {
                const [time, user, index] = newest.extract();
                feed.push(this.tweets[user][index][1]);
                if (index > 0) {
                    newest.insert([this.tweets[user][index - 1][0], user, index - 1]);
                }
            }
            return feed;
        }
    
        follow(followerId: number, followeeId: number): void {
            if (followerId === followeeId) {
                return;
            }
            if (!(followerId in this.following)) {
                this.following[followerId] = new Set();
            }
            this.following[followerId].add(followeeId);
        }
    
        unfollow(followerId: number, followeeId: number): void {
            if (followerId in this.following) {
                this.following[followerId].delete(followeeId);
            }
        }
    }

    TypeScript details:

    • The index signatures and Set<number> are only types; at run time tweets and following are plain objects, and their keys become strings. userId in this.tweets works because in converts the number to a string key.
    • new Set() without a type argument is accepted here, because the field's declared type tells the compiler what it holds.
    • Spreading a Set with ... gives its values, one after another, which is what push(...set) needs.
    • The unpacked time in the merge loop isn't used afterwards; the entry carries it only so the heap can order by it.

    Complexity

    • postTweet, follow and unfollow are O(1): an increment, a key check, a push, and a set add or delete.
    • getNewsFeed builds users with F entries, where F is the number of people the user follows, plus the user. The heap never holds more than one entry per person, so each insert or extract is O(log F). Seeding takes at most F inserts, and the merge loop runs at most 10 times, so the total is O(F log F).
    • Space is O(T + P) overall for T tweets and P follow pairs, and O(F) extra per feed call.

    Edge cases and common mistakes

    • An empty feed. On a new Twitter, getNewsFeed(3) returns [].
    • Following yourself. After follow(3, 3) and postTweet(3, 7), the feed is 7, once. Without the early return, user 3 would be in users twice and tweet 7 would appear twice.
    • Unfollow then follow before any post. unfollow(3, 4) on someone you don't follow changes nothing. After follow(3, 4) and user 4 posting tweet 8, user 3's feed is 8 7.
    • Ordering by tweet id. Ids are arbitrary, so the clock is what orders tweets.
    • Pushing every tweet into the heap. Only one tweet per person goes in at first; an older tweet enters only after the newer one is taken. This keeps the work independent of the number of tweets.
    • Forgetting users who never posted. Without the user in this.tweets check, reading this.tweets[user].length would fail for a user with no array.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free