[路飞]_前端算法第六十七弹-设计推特

155 阅读6分钟

设计一个简化版的推特(Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近 10 条推文。

实现 Twitter 类:

  • Twitter() 初始化简易版推特对象
  • void postTweet(int userId, int tweetId) 根据给定的 tweetIduserId 创建一条新推文。每次调用此函数都会使用一个不同的 tweetId
  • List<Integer> getNewsFeed(int userId) 检索当前用户新闻推送中最近 10 条推文的 ID 。新闻推送中的每一项都必须是由用户关注的人或者是用户自己发布的推文。推文必须 按照时间顺序由最近到最远排序
  • void follow(int followerId, int followeeId) ID 为 followerId 的用户开始关注 ID 为 followeeId 的用户。
  • void unfollow(int followerId,int followeeId) ID 为 followerId 的用户不再关注 ID 为 followeeId 的用户。

示例:

输入
["Twitter", "postTweet", "getNewsFeed", "follow", "postTweet", "getNewsFeed", "unfollow", "getNewsFeed"]
[[], [1, 5], [1], [1, 2], [2, 6], [1], [1, 2], [1]]
输出
[null, null, [5], null, null, [6, 5], null, [5]]

解释
Twitter twitter = new Twitter();
twitter.postTweet(1, 5); // 用户 1 发送了一条新推文 (用户 id = 1, 推文 id = 5)
twitter.getNewsFeed(1);  // 用户 1 的获取推文应当返回一个列表,其中包含一个 id 为 5 的推文
twitter.follow(1, 2);    // 用户 1 关注了用户 2
twitter.postTweet(2, 6); // 用户 2 发送了一个新推文 (推文 id = 6)
twitter.getNewsFeed(1);  // 用户 1 的获取推文应当返回一个列表,其中包含两个推文,id 分别为 -> [6, 5] 。推文 id 6 应当在推文 id 5 之前,因为它是在 5 之后发送的
twitter.unfollow(1, 2);  // 用户 1 取消关注了用户 2
twitter.getNewsFeed(1);  // 用户 1 获取推文应当返回一个列表,其中包含一个 id 为 5 的推文。因为用户 1 已经不再关注用户 2

这是第一道我做的需要设计的题目。我们先看他的几个需求。

  • 首先我们需要将文章与用户联系起来。
  • 每个用户的每篇文章都要有时间标识,并且按时间顺序排序。
  • 用户可以关注其他用户,可以查看关注用户的文章。
  • 用户可以取消关注

数组+哈希

我们需要建立两个变量,一个是文章列表,一个是用户列表,还需要一个用于判断的函数。

var Twitter = function() {
    this.article = [];//所有用户及其推文[[用户id,推文id],[],[]]
    this.user = new Map();//所有用户及其关注的user[]
    this.tfUser = function (userId) { // 用户不存在就新增用户
        if(!this.user.has(userId)) this.user.set(userId, []);
    }
};

postTweet方法我们将所有文章都存入到article里面,并关联其作者

Twitter.prototype.postTweet = function(userId, tweetId) {
	this.tfUser(userId);
  this.article.unshift([userId, tweetId]); // 最新的文章在最前面
};

getNewsFeed方法是查询文章,我们遍历所有文章,取出包含我们用户的,和用户关注的用户的文章,取出前十个

Twitter.prototype.getNewsFeed = function(userId) {
    this.tfUser(userId);
    let arr = this.user.get(userId);
    arr.push(userId);
    let res = [];
    this.article.forEach((values) => {
        if(arr.includes(values[0])) {
            res.push(values[1]);
        }
    })
    if(res.length > 10) res = res.slice(0,10);
    return res;
};

follow关注用户,我们将需要关注的用户ID,加入到我们的用户的数组中

Twitter.prototype.follow = function(followerId, followeeId) {
    this.tfUser(followerId);
    this.user.get(followerId).push(followeeId);
};

取消关注用户,只需要在用户关注的列表里找到需要取消的用户,然后delete

Twitter.prototype.unfollow = function(followerId, followeeId) {
    this.tfUser(followerId);
    let index = this.user.get(followerId).indexOf(followeeId);
    if(index !== -1) {
        this.user.get(followerId).splice(index,1);
    }
};

这种方法每次都需要从所有文章中,遍历出所需文章,耗时较大

链表+哈希

我们新建两个类,一个是推文类,一个是用户的类。在此之前我们需要创建一个全局变量timer,用于记录发文的顺序。

let time = 0; //维护一个全局的time


//推文
function Tweet (id){
    this.id = id;
    this.time = time;
    this.next = null;
}

推文类是一个链表类,我们需要记录推文的时间和id,创建链表结构。

//用户类
function User(id) {
    this.id = id;
    this.tweet = null; //推文放在链表;
    this.followee = new Set(); //关注者,放hashSet; 方便删除查找
    this.follow = (followeeId)=>{
        //不能关注自己
        if(followeeId != this.id){
            this.followee.add(followeeId);
        }
        return this;
    }
    this.unfollow = function unfollow(followeeId){
        //不能取关自己
        if(this.followee.has(followeeId) && followeeId!=this.id){
            this.followee.delete(followeeId);
        }
        return this;
    }
    //发表文章
    this.post = function post(tweetId){
        //插入到链表的头部
        time+=1;
        let tweet = new Tweet(tweetId);
        tweet.next = this.tweet;
        this.tweet = tweet;
        return this;
    }
}

用户类需要记录用户的id,用户的最新推文,用户的关注列表,我们还实现了三个方法,关注和取消关注,还有发文。

现在我们需要一个大顶堆,当需要获取自己和自己关注的用户的文章时,我们需要用大顶堆来对多个链表的文章时间进行排序, 从而找到其中的前十篇文章。对于完全二叉树和大顶堆,前面的文章有提及。

//大二根堆;
class BinaryHeap {
    constructor() {
        //数组存储完全二叉树;
        //从索引0开始;
        this.heap = [];
    }
    swap(i, j) {
        let temp = this.heap[i];
        this.heap[i] = this.heap[j];
        this.heap[j] = temp;
    }
    isEmpty() {
        return this.heap.length == 0;
    }
    top () {
        return this.heap[0];
    }
    push(node) {
        //插入尾部;向上跟父节点做交换,直到满足堆性质;
        this.heap.push(node);
        //从最后一个索引开始;
        this.heapifyUp(this.heap.length - 1);
    }
    pop() {
        //删除堆顶1.头尾元素交换,2.删除尾部,3.此时不满足堆性质,所以向下跟子元素做交换;
        let res = this.heap[0];
        this.swap(0, this.heap.length-1);
        this.heap.pop();
        
        this.heapifyDown(0);
        //返回被删除的元素;
        return res;
    }
    heapifyUp(p) {
        //1.一直向上做交换,直到根节点或者大于父节点(小根堆)2.父节点为(p-1)>>1 右位移一位
        while(p>0) {
            let fa = (p-1) >> 1;   //father的索引
            
            if(this.heap[p].val>this.heap[fa].val){
                this.swap(p, fa);
                p = fa;
            }
            else break;
        }
    }
    heapifyDown(p) {
        //1. 左子节点索引p*2+1, 右子节点p*2+2;
        //2. 小根堆,对比子节点小的作交换;
        let child = p * 2+1;
        let len = this.heap.length;
        //出界停止;
        while(child < len) {
            let otherChild = p * 2 + 2;
            //比较当前的两个节点;
            if(otherChild<len && this.heap[otherChild].val>this.heap[child].val){
                child = otherChild;
            }
            if(this.heap[p].val<this.heap[child].val){
                this.swap(p, child);
                p = child;
                child = p * 2 + 1;
            }
            else break;
        }
         
    }
}

准备工作做完了之后,我们就要着手推特的实现了,首先是初始化的过程,初始化需要创建一个存放用户信息的一个哈希表。

var Twitter = function() {
    //所有的用户;
    this.userMap = new Map();
};

postTweet用户发文,我们需要判断用户是否存在,不存在则新建一个用户。并推送发文

Twitter.prototype.postTweet = function(userId, tweetId) {
    //1.查看用户是否建立
    if(!this.userMap.has(userId)){
        this.userMap.set(userId, new User(userId));
    }
    //2. 新建一条post;
    this.userMap.set(userId, this.userMap.get(userId).post(tweetId));

};

getNewsFeed获取最新文章,查看用户是否存在,建立一个大顶堆heap,将自己的推文push进大顶堆heap中,并且查看关注列表,将关注列表中的用户的推文也加入到大顶堆中。取出堆中的前十个数据,组成前十条推文。

};
Twitter.prototype.getNewsFeed = function(userId) {
    //1.查看用户是否建立
    if(!this.userMap.has(userId)){
        return [];
    }
    //合并k个链表 前10条
    let user = this.userMap.get(userId);
    //声明一个大根堆
    let heap = new BinaryHeap();
    //以time为比较的依据
    //添加自己的推文
    if(user.tweet != null){
        heap.push({val: user.tweet.time, listNode: user.tweet});
    }
    //如果有关注者
        for(let followeeId of user.followee.values()){
            if(this.userMap.get(followeeId)){
                let tweet = this.userMap.get(followeeId).tweet;
                //qi'y
                if(tweet!=null){
                    heap.push({val: tweet.time, listNode: tweet});
                }
            }
        }
    let ans = [];
    while(!heap.isEmpty()){
        let node = heap.pop();
        //先存储当前的值;
        ans.push(node.listNode.id);
        if(ans.length == 10){ return ans;}
        //添加下一个到堆;
        let nextNode = node.listNode.next;
        if(nextNode!=null){
            heap.push({val: nextNode.time, listNode: nextNode});
        }
    }
    return ans;    
};

follow关注用户,需要查看关注的用户和被关注的用户是否存在,不能关注自己和关注已经关注过的用户。

Twitter.prototype.follow = function(followerId, followeeId) {
    //1.查看用户是否建立
    if(!this.userMap.has(followerId)){
        this.userMap.set(followerId, new User(followerId));
    }
    //1.查看关注者是否建立
    if(!this.userMap.has(followeeId)){
        this.userMap.set(followeeId, new User(followeeId));
    }
    this.userMap.set(followerId, this.userMap.get(followerId).follow(followeeId));
};

unfollow取消关注用户,需要查看用户和被关注用户是否存在,不能取消关注自己和未关注的用户和不存在的用户。

Twitter.prototype.unfollow = function(followerId, followeeId) {
    //1.查看用户是否建立
    if(!this.userMap.has(followerId)){
        this.userMap.set(followerId, new User(followerId));
    }
    this.userMap.set(followerId, this.userMap.get(followerId).unfollow(followeeId));
};