Problem overview
You write a class Twitter with four methods. postTweet(userId, tweetId) records 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 written by the user or by anyone the user follows, newest first, as a List<Int>.
The rules around the edges: following is one way; following someone twice, following yourself, or unfollowing someone you don't follow changes nothing; your own tweets are always in your feed; and an empty feed is an empty list. Tweet ids say nothing about age. A call posts later than all calls before it, so "newest" means "posted last", even if its id is smaller.
Approach
The simple solution stores every tweet in one list with its time. To build a feed, it scans the list, keeps the tweets whose author is in the user's group, and takes the last 10. Or it collects the group's tweets and sorts them. Either way the cost of getNewsFeed grows with every tweet ever posted, including the tweets of people nobody in the feed follows.
The better design stores each user's tweets in their own list, oldest first. Because time only grows, a new tweet always goes at the end, so each list is already sorted. A feed is then a merge of several sorted lists, each read from its newest end, stopping after 10 items. This is the standard way to merge k sorted lists: a heap holds the next candidate of each list.
Concretely, the heap starts with the newest tweet of each person in the feed. The top is the newest tweet overall, so it goes into the feed. Then that person's next older tweet takes its place in the heap. After 10 rounds, or when the heap is empty, the feed is complete. The heap never holds more than one entry per person, and at most 10 tweets are ever taken out.
Step by step
- Three fields.
timeis a global clock (avar, starting at 0);tweetsmaps each user id to a list ofIntArraypairs holding a tweet's time and id, oldest first;followingmaps each user id to the set of ids they follow. A set ignores repeated follows and adds and removes in O(1). - Posting.
postTweetincreasestime, creates the user's list if this is their first tweet, and appendsintArrayOf(time, tweetId). Each user's list stays sorted by time without any sorting. - Who is in the feed.
getNewsFeedstarts a listuserswith the user, since your own tweets are always shown, and adds everyone the user follows. - Seed the heap.
newestis aPriorityQueue<IntArray>built withcompareByDescending { it[0] }, so the entry with the biggest time is on top. For each person who has posted, an entryintArrayOf(time, user, index)describes their newest tweet: its time, its author and its position in the author's list. People who never posted are skipped. - Take up to 10. While the heap is not empty and the feed has fewer than 10 ids, poll the top entry, unpack it with
val (_, user, index) = …, and add that tweet's id tofeed. - Refill from the same author. If
index > 0, the author has an older tweet. Push it withindex - 1: it is now their newest tweet not yet shown. - Return the feed. It is already newest first.
- Follow and unfollow.
followreturns at once when the two ids are equal, creates the follower's set when needed, and adds the followee.unfollowremoves the followee when the follower has a set at all. Removing a missing value does nothing.
The solution in Kotlin
class Twitter {
var time = 0
val tweets = mutableMapOf<Int, MutableList<IntArray>>()
val following = mutableMapOf<Int, MutableSet<Int>>()
fun postTweet(userId: Int, tweetId: Int) {
time++
if (!tweets.containsKey(userId)) {
tweets[userId] = mutableListOf()
}
tweets.getValue(userId).add(intArrayOf(time, tweetId))
}
fun getNewsFeed(userId: Int): List<Int> {
val users = mutableListOf(userId)
if (following.containsKey(userId)) {
users.addAll(following.getValue(userId))
}
val newest = PriorityQueue<IntArray>(compareByDescending { it[0] })
for (user in users) {
if (tweets.containsKey(user)) {
val index = tweets.getValue(user).size - 1
newest.add(intArrayOf(tweets.getValue(user)[index][0], user, index))
}
}
val feed = mutableListOf<Int>()
while (newest.isNotEmpty() && feed.size < 10) {
val (_, user, index) = newest.poll()
feed.add(tweets.getValue(user)[index][1])
if (index > 0) {
newest.add(intArrayOf(tweets.getValue(user)[index - 1][0], user, index - 1))
}
}
return feed
}
fun follow(followerId: Int, followeeId: Int) {
if (followerId == followeeId) {
return
}
if (!following.containsKey(followerId)) {
following[followerId] = mutableSetOf()
}
following.getValue(followerId).add(followeeId)
}
fun unfollow(followerId: Int, followeeId: Int) {
if (following.containsKey(followerId)) {
following.getValue(followerId).remove(followeeId)
}
}
}
Kotlin details worth knowing:
getValueversus[]. For a map,map[key]gives a nullable value (nullfor a missing key).getValue(key)returns the plain value and throws if the key is missing. The code uses it only after acontainsKeycheck.- Mutable collections.
mutableMapOf,mutableListOfandmutableSetOfcreate collections that can change. Avalfield still always points at the same map, but the map's contents grow. The emptymutableListOf()andmutableSetOf()need no type argument, because the map's declared type tells Kotlin what they hold. - Destructuring an array.
val (_, user, index) = newest.poll()takes the first three elements of theIntArray. The underscore skips the time, which has already done its job. - The comparator.
compareByDescending { it[0] }compares two entries by their first number, largest first. Only times decide the order, and every tweet has a different time, so ties do not occur. - Method names. Kotlin's
PriorityQueueis Java's, sopoll()removes the top and returns it, andisNotEmpty()works on it like on any collection.
Seeing it run
This code uses the class the way the lesson's tests do: it creates a Twitter, posts, follows and prints feeds.
fun main() {
val twitter = Twitter()
twitter.postTweet(1, 5)
println(twitter.getNewsFeed(1))
twitter.follow(1, 2)
twitter.postTweet(2, 6)
println(twitter.getNewsFeed(1))
twitter.unfollow(1, 2)
println(twitter.getNewsFeed(1))
val ids = Twitter()
ids.postTweet(1, 900)
ids.postTweet(2, 5)
ids.follow(3, 1)
ids.follow(3, 2)
println(ids.getNewsFeed(3))
val busy = Twitter()
busy.follow(1, 2)
for (i in 1..6) {
busy.postTweet(1, 100 + i)
busy.postTweet(2, 200 + i)
}
println(busy.getNewsFeed(1))
println(busy.getNewsFeed(2))
val odd = Twitter()
println(odd.getNewsFeed(3))
odd.follow(3, 3)
odd.postTweet(3, 7)
println(odd.getNewsFeed(3))
odd.unfollow(3, 4)
odd.follow(3, 4)
odd.postTweet(4, 8)
println(odd.getNewsFeed(3))
}
It prints:
[5]
[6, 5]
[5]
[5, 900]
[206, 106, 205, 105, 204, 104, 203, 103, 202, 102]
[206, 205, 204, 203, 202, 201]
[]
[7]
[8, 7]
The first three lines show a follow and an unfollow: tweet 6 appears first because it is newer, then disappears again. The fourth line shows that the clock decides the order: tweet 5 was posted after tweet 900, so it comes first. Lines five and six show the 10-tweet cut: user 1 sees 12 tweets but only the 10 newest, while user 2, who follows nobody, sees the 6 tweets they wrote. The last three lines cover the odd cases: an empty feed prints [], following yourself changes nothing, and following someone before they post is fine, because their later tweet shows up.
Complexity
postTweet,follow,unfollow: O(1) on average. Each is a few hash lookups plus a list append or a set change.getNewsFeed: with F the number of people in the feed, building the heap takes F insertions of O(log F) each, and at most 10 rounds of one removal and one insertion add 10 · O(log F). The total is O(F log F), whatever the number of tweets.- Space: O(T + L) for T tweets and L follow relations, plus O(F) for one feed call.
Edge cases and common mistakes
- Ordering by tweet id. Ids are arbitrary. Only the clock tells which tweet is newer.
- Following yourself. Without the early return, the user would appear twice in
usersand their tweets would be listed twice. - Fewer than 10 tweets. The loop ends when the heap is empty, so the feed is shorter, or empty.
- Merging everything and sorting. It gives correct answers but slows down with every tweet in the system, not just with the feed's size.
- Pushing every tweet of every author into the heap. One entry per author is enough, because an author's older tweet cannot be shown before their newer one.
- Unfollowing without a set. A user who never followed anyone has no set. The
containsKeycheck avoids touching a missing one.
