355. Design Twitter
Problem Description
This problem simulates a basic form of Twitter functionality. It involves designing a system that allows users to post tweets, follow other users, unfollow users, and view the 10 most recent tweets in their news feed. Each tweet has a unique ID, and the news feed must display these tweets in order from most to least recent. The following functionalities are to be implemented:
Twitter()
: a constructor to initialize the Twitter object.postTweet(userId, tweetId)
: allows a user to post a tweet.getNewsFeed(userId)
: retrieves the 10 most recent tweets from the user and the users they follow.follow(followerId, followeeId)
: allows a user to follow another user.unfollow(followerId, followeeId)
: allows a user to unfollow another user.
Intuition
The solution to this problem requires efficient ways to store and retrieve tweets and follow relationships. Here's the intuition behind each part of the solution:
-
Initialisation: Use dictionaries (
defaultdict
) to track tweets and follow relationships for each user. Theuser_tweets
dictionary keeps a list of a user's tweets,user_following
keeps track of who each user is following, andtweets
maintains the timestamp order of each tweet. -
Posting Tweets: Increment a global time variable each time a tweet is posted to keep track of the order. Save each tweet with the current time into
user_tweets
to build a user's twitter history andtweets
dictionary to preserve the global order. -
Getting News Feed: Retrieve the 10 most recent tweets from the user and users they follow. This requires merging and sorting tweets from multiple users. The naive approach could be costly due to sorting each time the news feed is requested, so the solution uses a heap (
nlargest
function) to efficiently get the top 10 tweets. -
Following: Modify the
user_following
dictionary to add the followeeId to the followerId's set. -
Unfollowing: Check if the
followerId
is followingfolloweeId
and remove it from the follow set if present.
By combining these data structures and methods, the system manages user activity in an efficient and streamlined manner, optimizing for the frequent action of getting the news feed which requires sorting and collation of tweets.
Learn more about Linked List and Heap (Priority Queue) patterns.
Solution Approach
The solution utilizes a combination of data structures such as dictionaries, sets, and heaps to implement the required functionality efficiently. Here’s how each functionality is implemented:
-
Data Structures Used:
defaultdict(list)
foruser_tweets
: This stores tweets for each user. The key is theuserId
and the value is a list oftweetId
s.defaultdict(set)
foruser_following
: This maintains the following relationships. The key is thefollowerId
and the value is a set offolloweeId
s to facilitate quick add (follow) and delete (unfollow) operations.defaultdict()
fortweets
: This keeps track of when each tweet was posted. It mapstweetId
to a timestamp based on thetime
variable.
-
Posting Tweets (
postTweet
method):- Increment the
time
each time a new tweet is posted to create a unique timestamp. - Append
tweetId
to theuser_tweets
list for the givenuserId
. - Associate
tweetId
with the currenttime
in thetweets
dictionary to keep track of when the tweet was posted.
- Increment the
-
Getting News Feed (
getNewsFeed
method):- First, gather all user IDs that the news feed should include – the user itself and all user IDs they follow, which are obtained from
user_following
. - Create a combined list of tweets by retrieving the lists of recent tweets from
user_tweets
for all these users and flattening them into a single list. Only the most recent 10 tweets per user are considered. - Use the
nlargest
function from theheapq
module to efficiently retrieve the 10 most recent tweets from the combined list, utilizing timestamps stored in thetweets
dictionary as the key for sorting. - The
nlargest
function takes advantage of the fact that tweets are stored with a monotonically increasing timestamp, thereby efficiently implementing a max heap to find the top tweets without needing to sort the entire list.
- First, gather all user IDs that the news feed should include – the user itself and all user IDs they follow, which are obtained from
-
Following a user (
follow
method):- Add the
followeeId
to thefollowerId
's set inuser_following
.
- Add the
-
Unfollowing a user (
unfollow
method):- Check if the
followeeId
is in thefollowerId
's set inuser_following
and remove it if present.
- Check if the
This approach takes advantage of the properties of sets for quick modifications in follow/unfollow operations and leverages the ordering of a timestamp to avoid full sorts when retrieving the news feed.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simple example to illustrate how the solution approach works:
-
Initialization: Assume we initialize our Twitter system.
twitter = Twitter()
Now, our
user_tweets
,user_following
, andtweets
are all initialized, but they are empty. -
Post Tweets:
-
User 1 posts a tweet:
twitter.postTweet(1, 5)
The tweet with id 5 from user 1 is stored, and the timestamp is increased.
user_tweets
now has{1: [5]}
, andtweets
has{5: timestamp}
. -
User 2 posts two tweets:
twitter.postTweet(2, 6) twitter.postTweet(2, 7)
User 2’s tweets are stored with increasing timestamps. Now
user_tweets
has{1: [5], 2: [6, 7]}
, andtweets
includes{5: timestamp1, 6: timestamp2, 7: timestamp3}
.
-
-
Follow:
- User 1 decides to follow user 2:
twitter.follow(1, 2)
user_following
updates to{1: {2}}
indicating that user 1 is now following user 2.
- User 1 decides to follow user 2:
-
Get News Feed:
- User 1 wants to view their news feed:
Since user 1 follows user 2, the feed should include tweets from both users. The system gathers all tweet IDs from user 1 and user 2, which aretwitter.getNewsFeed(1)
[5, 6, 7]
. Thenlargest
function is used with the tweets' timestamps to get the top tweets, resulting in a list of tweets[7, 6, 5]
showing the most recent tweets first.
- User 1 wants to view their news feed:
-
Unfollow:
- User 1 decides to unfollow user 2:
Nowtwitter.unfollow(1, 2)
user_following
is updated to{1: {}}
, which means user 1 is not following anyone anymore.
- User 1 decides to unfollow user 2:
By following these simple steps, the system is capable of supporting different operations efficiently, catering to the functionalities similar to that of Twitter. The use of appropriate data structures such as defaultdict(list)
, defaultdict(set)
, and heaps allows for optimal performance for each operation, whether it be posting, following/unfollowing, or retrieving the news feed.
Solution Implementation
1from collections import defaultdict
2from heapq import nlargest
3from typing import List
4
5class Twitter:
6 def __init__(self):
7 """
8 Initialize your data structures here.
9 """
10 self.user_tweets = defaultdict(list) # Maps user ids to their tweets (respecting the chronological order)
11 self.user_following = defaultdict(set) # Maps user ids to the set of user ids they follow
12 self.tweets = {} # Maps tweet ids to timestamps
13 self.timer = 0 # A timestamp to order tweets
14
15 def post_tweet(self, user_id: int, tweet_id: int) -> None:
16 """
17 Compose and post a new tweet.
18 """
19 self.timer += 1 # Increment the timer to record the time of the tweet
20 self.user_tweets[user_id].append(tweet_id) # Append the new tweet to the user's tweets
21 self.tweets[tweet_id] = self.timer # Record the timestamp for the new tweet
22
23 def get_news_feed(self, user_id: int) -> List[int]:
24 """
25 Retrieve the 10 most recent tweet ids in the user's news feed.
26 Each item in the news feed must be posted by users whom the user follows or by the user themselves.
27 Tweets must be ordered from most recent to least recent.
28 """
29 following = self.user_following[user_id] # Get the set of user ids the current user follows
30 users = following.union({user_id}) # Combine with the current user to get all potential sources of tweets
31 # Collect up to the 10 most recent tweets from each user
32 tweets = [self.user_tweets[u][-10:][::-1] for u in users] # Retrieve, reverse, and limit to 10 tweets
33 all_tweets = sum(tweets, []) # Flatten the list of tweets
34 # Get the 10 most recent tweets among all the tweets collected, based on timestamps
35 return nlargest(10, all_tweets, key=lambda tweet: self.tweets[tweet])
36
37 def follow(self, follower_id: int, followee_id: int) -> None:
38 """
39 Follower follows a followee. If the operation is invalid, it should be a no-op.
40 """
41 self.user_following[follower_id].add(followee_id) # Add the followee to the follower's following set
42
43 def unfollow(self, follower_id: int, followee_id: int) -> None:
44 """
45 Follower unfollows a followee. If the operation is invalid (e.g., not following), it should be a no-op.
46 """
47 if followee_id in self.user_following[follower_id]: # If the follower is actually following the followee
48 self.user_following[follower_id].remove(followee_id) # Remove the followee from the set
49
50
51# The Twitter object can be instantiated and used as follows:
52# obj = Twitter()
53# obj.post_tweet(user_id, tweet_id)
54# feed = obj.get_news_feed(user_id)
55# obj.follow(follower_id, followee_id)
56# obj.unfollow(follower_id, followee_id)
57
1import java.util.*;
2
3public class Twitter {
4 private Map<Integer, List<Integer>> userTweetListMap;
5 private Map<Integer, Set<Integer>> userFollowsMap;
6 private Map<Integer, Integer> tweetIdToTimestampMap;
7 private int timestamp;
8
9 /** Initialize your data structure for the Twitter class. */
10 public Twitter() {
11 userTweetListMap = new HashMap<>();
12 userFollowsMap = new HashMap<>();
13 tweetIdToTimestampMap = new HashMap<>();
14 timestamp = 0;
15 }
16
17 /**
18 * Compose a new tweet.
19 * @param userId The ID of the user posting the tweet.
20 * @param tweetId The ID of the tweet.
21 */
22 public void postTweet(int userId, int tweetId) {
23 // Add tweet to the user's list of tweets
24 userTweetListMap.computeIfAbsent(userId, k -> new ArrayList<>()).add(tweetId);
25 // Map the tweetId to the current timestamp
26 tweetIdToTimestampMap.put(tweetId, ++timestamp);
27 }
28
29 /**
30 * Retrieve the 10 most recent tweet IDs in the user's news feed. Each item
31 * in the news feed must be posted by users who the user followed or by the
32 * user themself. Tweets must be ordered from most recent to least recent.
33 * @param userId The ID of the user whose news feed is retrieved.
34 * @return A list of tweet IDs in the news feed.
35 */
36 public List<Integer> getNewsFeed(int userId) {
37 // Get the set of all users the given user is following
38 Set<Integer> following = userFollowsMap.getOrDefault(userId, new HashSet<>());
39 // Include the user's own ID to include their tweets as well
40 following.add(userId);
41
42 // Priority Queue to store tweets by recency
43 PriorityQueue<Integer> minHeap = new PriorityQueue<>(
44 (tweetId1, tweetId2) -> tweetIdToTimestampMap.get(tweetId2) - tweetIdToTimestampMap.get(tweetId1)
45 );
46
47 // Collect the tweets from each user's tweet list, but restrict to the 10 most recent tweets per user
48 for (Integer user : following) {
49 List<Integer> tweets = userTweetListMap.get(user);
50 if (tweets != null) {
51 int tweetCount = tweets.size();
52 for (int i = tweetCount - 1, tweetLimit = 10; i >= 0 && tweetLimit > 0; --i, --tweetLimit) {
53 minHeap.offer(tweets.get(i));
54 }
55 }
56 }
57
58 // Retrieve the 10 most recent tweets for the news feed
59 List<Integer> feed = new ArrayList<>();
60 int feedSize = 10;
61 while (!minHeap.isEmpty() && feed.size() < feedSize) {
62 feed.add(minHeap.poll());
63 }
64
65 return feed;
66 }
67
68 /**
69 * Follower follows a followee. If the operation is invalid (i.e., already following), it should be a no-op.
70 * @param followerId The ID of the follower.
71 * @param followeeId The ID of the followee.
72 */
73 public void follow(int followerId, int followeeId) {
74 userFollowsMap.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId);
75 }
76
77 /**
78 * Follower unfollows a followee. If the operation is invalid (i.e., not following), it should be a no-op.
79 * @param followerId The ID of the follower.
80 * @param followeeId The ID of the followee.
81 */
82 public void unfollow(int followerId, int followeeId) {
83 if (userFollowsMap.containsKey(followerId)) {
84 userFollowsMap.get(followerId).remove(followeeId);
85 }
86 }
87}
88
89/**
90 * Your Twitter object will be instantiated and called as such:
91 * Twitter obj = new Twitter();
92 * obj.postTweet(userId, tweetId);
93 * List<Integer> param_2 = obj.getNewsFeed(userId);
94 * obj.follow(followerId, followeeId);
95 * obj.unfollow(followerId, followeeId);
96 */
97
1#include <unordered_map>
2#include <unordered_set>
3#include <vector>
4#include <queue>
5#include <functional>
6#include <utility>
7using namespace std;
8
9class Twitter {
10private:
11 unordered_map<int, vector<int>> userTweetIdsMap; // Maps user ID to a list of tweet IDs
12 unordered_map<int, unordered_set<int>> userFollowsMap; // Maps user ID to a set of user IDs they follow
13 unordered_map<int, int> tweetIdToTimestampMap; // Maps tweet ID to the timestamp it was posted
14 int timestamp; // A global timestamp representing the order in which tweets are posted
15
16public:
17 /** Initialize your data structure for the Twitter class. */
18 Twitter() : timestamp(0) { }
19
20 /**
21 * Compose a new tweet.
22 * @param userId The ID of the user posting the tweet.
23 * @param tweetId The ID of the tweet.
24 */
25 void postTweet(int userId, int tweetId) {
26 // Increment the global timestamp and post the tweet
27 tweetIdToTimestampMap[tweetId] = ++timestamp;
28 // Add the tweet to the user's list of tweets
29 userTweetIdsMap[userId].push_back(tweetId);
30 }
31
32 /**
33 * Retrieve the 10 most recent tweet IDs in the user's news feed. Each item
34 * in the news feed must be posted by users who the user followed or by the user themselves.
35 * Tweets must be ordered from most recent to least recent.
36 *
37 * @param userId The ID of the user whose news feed is retrieved.
38 * @return A list of tweet IDs in the news feed.
39 */
40 vector<int> getNewsFeed(int userId) {
41 // Include the user themselves into the following list to see their own tweets in the news feed
42 unordered_set<int> following = userFollowsMap[userId];
43 following.insert(userId);
44
45 auto cmp = [&](const int a, const int b) {
46 return tweetIdToTimestampMap[a] < tweetIdToTimestampMap[b];
47 };
48
49 // Use a min heap to keep the 10 most recent tweets
50 priority_queue<int, vector<int>, decltype(cmp)> minHeap(cmp);
51
52 for (int user : following) {
53 // Iterate over the tweets posted by the user
54 const vector<int>& tweets = userTweetIdsMap[user];
55 for (int tweetId : tweets) {
56 minHeap.push(tweetId);
57 // If we have more than 10 tweets, pop the least recent
58 if (minHeap.size() > 10) {
59 minHeap.pop();
60 }
61 }
62 }
63
64 // Now we have up to 10 tweets in the min heap, sorted by recency
65 vector<int> feed;
66 while (!minHeap.empty()) {
67 feed.push_back(minHeap.top());
68 minHeap.pop();
69 }
70 // The most recent tweet is at the end, so we reverse the vector
71 reverse(feed.begin(), feed.end());
72 return feed;
73 }
74
75 /**
76 * Follower follows a followee. If the operation is invalid (i.e., already following), it should be a no-op.
77 * @param followerId The ID of the follower.
78 * @param followeeId The ID of the followee.
79 */
80 void follow(int followerId, int followeeId) {
81 // Add the followee to the follower's set of follows
82 userFollowsMap[followerId].insert(followeeId);
83 }
84
85 /**
86 * Follower unfollows a followee. If the operation is invalid (i.e., not following), it should be a no-op.
87 * @param followerId The ID of the follower.
88 * @param followeeId The ID of the followee.
89 */
90 void unfollow(int followerId, int followeeId) {
91 // Remove the followee from the follower's set of follows, if it exists
92 userFollowsMap[followerId].erase(followeeId);
93 }
94};
95
1// Declare the type for the maps to be used globally
2type UserTweetListMapType = Map<number, number[]>;
3type UserFollowsMapType = Map<number, Set<number>>;
4type TweetIdToTimestampMapType = Map<number, number>;
5
6// Declare the global variables equivalent to the class properties in Java
7let userTweetListMap: UserTweetListMapType = new Map();
8let userFollowsMap: UserFollowsMapType = new Map();
9let tweetIdToTimestampMap: TweetIdToTimestampMapType = new Map();
10let timestamp: number = 0;
11
12// Function to post a tweet associated with a user
13function postTweet(userId: number, tweetId: number): void {
14 // If the user is not found in the map, initialize their tweet list
15 const tweetList = userTweetListMap.get(userId) ?? [];
16
17 // Add the new tweet's ID to the user's tweet list
18 tweetList.push(tweetId);
19 userTweetListMap.set(userId, tweetList);
20
21 // Increment the timestamp and associate it with the tweet
22 tweetIdToTimestampMap.set(tweetId, ++timestamp);
23}
24
25// Function to get the 10 most recent tweets in the user's news feed
26function getNewsFeed(userId: number): number[] {
27 // Get the user's own tweets and the ones from the users they follow
28 const following = userFollowsMap.get(userId) ?? new Set();
29 following.add(userId);
30
31 // Use a min-heap to keep track of the tweets' timestamps
32 const minHeap: number[] = [];
33
34 // Helper function to sort the heap by timestamp
35 const byTimestamp = (a: number, b: number) => tweetIdToTimestampMap.get(b)! - tweetIdToTimestampMap.get(a)!;
36
37 following.forEach(user => {
38 const tweets = userTweetListMap.get(user) || [];
39 // Take at most the 10 most recent tweets from each user
40 tweets.slice(-10).forEach(tweetId => {
41 minHeap.push(tweetId);
42 });
43 });
44
45 // Sort the collected tweets by their timestamp using the min-heap
46 minHeap.sort(byTimestamp);
47
48 // Slice to get only the 10 most recent tweets
49 return minHeap.slice(0, 10);
50}
51
52// Function for a user to follow another user
53function follow(followerId: number, followeeId: number): void {
54 const followsSet = userFollowsMap.get(followerId) ?? new Set();
55 followsSet.add(followeeId);
56 userFollowsMap.set(followerId, followsSet);
57}
58
59// Function for a user to unfollow another user
60function unfollow(followerId: number, followeeId: number): void {
61 userFollowsMap.get(followerId)?.delete(followeeId);
62}
63
Time and Space Complexity
Time Complexity
__init__
: O(1) - Constant time for initialization since no calculations are being done.postTweet
: O(1) - Adding a tweet involves a constant number of operations, incrementing time, appending a tweet to a user's list and storing the timestamp, all of which are O(1).getNewsFeed
: O(users * tweets + KlogK) - Whereusers
is the number of followed users, andtweets
is the number of tweets in their feed and K is the total number of tweets fetched to sort. Specifically, O(users * 10) for collecting the tweets since we are obtaining at most 10 recent tweets from each user, and O(KlogK) for sorting these tweets, where K is min(users*10, 10) since we only return the 10 most recent tweets. In the worst case, this can be O(NlogN) for the sorting step ifN
is the number of tweets considered.follow
: O(1) - The act of following simply adds to a set which is an O(1) operation.unfollow
: O(1) - Unfollowing checks if a followeeId is in the set and removes it which are both O(1) operations.
Space Complexity
__init__
: O(1) - Initialization of dictionaries and variables uses constant space.postTweet
: O(T) - Where T is the number of tweets. Each tweet increases the space needed to store the tweet and its timestamp.getNewsFeed
: O(users * tweets) - Additional space needed to collect up to 10 latest tweets from each user followed. Thus space required grows with the number of users being followed and their tweets, although this space is temporary just for the duration of the method call.follow
andunfollow
: O(U + F) - Where U is the number of users and F is the total number of follow relations since each user may have a set of followers and followees. Each follow/unfollow action potentially adds/removes an element in a user's following set, so it doesn't increase the space complexity beyond what is needed to store all follow relationships.
Learn more about how to find time and space complexity quickly using problem constraints.
How does merge sort divide the problem into subproblems?
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
https algomonster s3 us east 2 amazonaws com cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Want a Structured Path to Master System Design Too? Don’t Miss This!