发现失联多年好友的功能是怎么实现?

本文深入探讨了微博可能感兴趣的人推荐算法,分析了关注粉丝关系的图存储结构,对比了并查集、路径统计法与好友列表比对等方案,最终提出基于共同好友数量排序的可行算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者 | Cooper Song

责编 | 伍杏玲

出品 | 程序人生(ID:coder_life)

近期晚上失眠比较多,偶然发现在微博里打开别人的关注/粉丝列表可以找到可能感兴趣的人,点进去一看还果然都是小学、初中、高中时期的同学,我就开始思索背后的算法是怎么实现的,于是便有了这篇文章。

 

关注粉丝信息用什么数据结构存储呢?

 

当然是用图啦!我单方面关注了你我就成为了你的粉丝,但是你还不是我的粉丝,因此关注/粉丝关系需要使用有向图(Directed Graph)来存储,注意不一定是有向无环图(Directed Acyclic Graph,简称DAG),因为可能出现下图所示的我关注了你,你关注了他,他又关注了我这样的三角关系。

确定使用有向图这种数据结构后,还需确定采用何种方式存储,图的主流存储方式就两种,邻接矩阵和邻接表。由于微博或其他社交网络任意用户相互之间产生关注关系的并不多,采用邻接矩阵存储会耗费大量的空间,因此我们采用邻接表来存储,可以将某用户关注的人放在一条链表里接到该用户上,粉丝也放在一条链表里接到该用户上,当然链表也可以改为具备自动扩容功能的动态数组。

//结构体定义

struct node

{

    vector<int> follows;

    vector<int> followers;

    vector<int> unionSet;

};

//所有用户

node user[MAX_USER_NUM];

动态数组follows中存储的是某用户关注的人;followers存储的是某用户的粉丝;unionSet存储的是follows集和followers集的并集,表示与某用户有“认识”关系的人,可以在该用户关注/取消关注别人或者别人关注/取消关注该用户时自动插入/删除,也可以用到的时候利用follows集和followers集求交集。

如何表示“认识”这种关系?

 

这里我们认为,我的关注与我的粉丝,都与我存在“认识”或者”感兴趣“的关系。因此我们可以对关注的人follows与粉丝followers取一个并集放到一个新的关系人数组unionSet。

取并集算法有两种,一种的时间复杂度是O(m*n),比较暴力也比较容易理解;另一种的时间复杂度是O(m+n),利用了哈希算法。

取并集算法1(暴力)

vector<int> getUnion(vector<int> follows,vector<int> followers)

{

    vector<int> ret=followers;

    int len1=follows.size();

    int len2=followers.size();

    for(int i=0;i<len1;i++)

    {

        //用来标记关注的人是不是已经出现在粉丝中

        bool found=false;

        for(int j=0;j<len2;j++)

        {

            if(follows[i]==followers[j])

            {

                //关注的人已经出现在粉丝中了

                flag=true;

                break;

            }

        }

        if(!flag)

        {

            //关注的人没有出现在粉丝中

            ret.push_back(follows[i]);

        }

    }

    return ret;

}

先将follows集(关注的人)全部放入并集中,再遍历followers集(粉丝),对每一个粉丝检查是否在follows集中出现过,如果没有出现过,就将该粉丝加入并集。

取并集算法2(哈希算法)

vector<int> getUnion(vector<int> follows,vector<int> followers)

{

    vector<int> ret;

    int len1=follows.size();

    int len2=followers.size();

    //哈希表

    unordered_map<int,bool> mp;

    for(int i=0;i<len1;i++)

    {

        mp[follows[i]]=true;

        ret.push_back(follows[i]);

    }

    for(int i=0;i<len2;i++)

    {

        if(!mp[followers[i]])

        {

            ret.push_back(followers[i]);

        }

    }

    return ret;

}

先遍历follows集(关注的人),将follows集中的所有元素都放入并集中,并用哈希表标记其是否已经存在于并集unionSet,再遍历followers集(粉丝),对每一个粉丝检查哈希值是否为true,若为true则表明该元素已经存在于并集unionSet,若为false说明目前并集unionSet中还没有该元素,就将该元素加入并集,最后返回该并集。

并查集是否可行?

 

假如一共有MAX_USER_NUM个用户,我们开一个数组f[MAX_USER_NUM],将第i个元素的值初始化为i。

int f[MAX_USER_NUM];

for(int i=0;i<MAX_USER_NUM;i++)

{

    f[i]=i;

}

如果用户a关注了用户b或者用户b关注了用户a,则做如下操作。

f[findFather(a)]=findFather(b);

上述代码的含义是将a的父结点的父结点设为b的父结点,所谓findFather函数,是一个返回父结点的函数。

findFather(int x)

{

    if(x==f[x])

    {

        return x;

    }

    return f[x]=findFather(f[x]);

}

在并查集中,父结点与子结点是没有层次关系的,如果a的父结点的父结点变成了b的父结点,那么在调用findFather(a)查找a的父结点时其父结点也变成了b的父结点,即此时用户a、用户b、用户c的父结点一致了。

要判断所有用户中任意两名用户是否可以通过他们的朋友认识,只需要做如下判断。

bool haveConnection(int a,int b)

{

    bool result;

    if(findFather(a)==findFather(b))

    {

        result=true;

    }

    else

    {

        result=false;

    }

    return result;

}

这样拉取我当前观察用户的关注的人与粉丝的并集unionSet中调用以上函数返回true的用户,就得到了我可能感兴趣的人的列表。

vector<int> getInterestedList(vector<int> unionSet,int myId)

{

    vector<int> ret;

    int len=unionSet.size();

    for(int i=0;i<len;i++)

    {

        if(haveConnection(myId,unionSet[i])

        {

            ret.push_back(unionSet[i]);

        }

    }

    return ret;

}

并查集的本质是连通关系,连通图的概念是一个图的任意两个顶点之间都能从一个顶点出发到达另一个顶点,因此当并查集中所有顶点的父节点都是同一个顶点时,该图就是连通图,只拥有一个连通分量;而如果所有顶点的父节点不只一个,则该图不是连通图,存在多个连通分量,父节点有几个,连通分量就是几。对并查集算法和图论感兴趣的朋友可以自行查资料了解。

并查集的确能够准确得出两个用户之间是否存在关联,但是我们必须面对的现实是一个非常有名的社交网络领域的数学猜想——六度空间理论,也被叫做六度分割理论。其内容是你和任何一个陌生人之间所间隔的人不会超过6个。

举个例子,你只需要6个中间人,就可以与美国总统认识,这听起来有些荒谬,却也有几分道理。例如,我有认识的亲戚朋友在国外做生意,因为做生意认识了外国驻华大使,而外国驻华大使的上司就有可能与总统的下属握过手或通过电话,而总统的下属必然认识总统。因此如果我、我的亲戚朋友、亲戚朋友认识的外国驻华大使、总统的下属与总统注册了同一个社交网络,虽然我没有与总统直接建立关注与被关注的关系,通过并查集算法也可得知我与总统之间存在关系。

 

路径统计法

 

如果配合并查集使用,先调用并查集的findFather函数可知两个用户之间是否存在通路,若存在通路,可以用深度优先搜索+迭代的算法统计出从一个用户出发到另一个用户的路径数,路径数越多则代表两名用户之间越存在着千丝万缕的联系。

如上图所示,我是用户1,我正在查看用户6的好友列表,用户6有好友3和好友5,好友3已经是我的好友了,我联系上好友5共有6条路径,分别是:

1->2->5

1->2->3->5

1->2->3->6->5

1->3->2->5

1->3->5

1->3->6->5

这表明我与用户5一定有着千丝万缕的联系。

然而这种算法的时间复杂度却决于全部顶点的度的大小以及两名用户之间经过的中间用户的个数,如果中间用户过多,则在递归的时候就有可能爆栈导致系统崩溃。

除了时间复杂度和爆栈的问题,如果看到问题的本质,其实该算法也摆脱不了六度空间的影响。比如我在用户5的好友列表中再加入一个用户7,用户7就只有用户5这一个好友,因此到达用户7的路径数与到达用户5的路径数一样,然而用户1到达用户7至少也要通过2个中间人,其关联已经不大了。

 

可行的方案一

 

从目前看来最可行的方案就是进行好友列表比对取交集,按照交集中元素的个数按照从大到小排序。在观察了多种情况后我发现,给我推荐的可能感兴趣的人下面都有一行提示某某某也关注了他/她,而某某某正是我的好友。原来微博的兴趣推荐算法并没有那么高大上,应该是与QQ的共同好友一样。与QQ唯一的区别就是,微博有两个集合,一个是关注的人,一个是粉丝。

求交集同样有两种算法,与求并集类似,一个暴力,时间复杂度是O(m*n);另一个哈希,时间复杂度是O(m+n)。

取交集算法1(暴力)

vector<int> getInterp(vector<int> myFriends,vector<int> yourFriends)

{

    int len1=myFriends.size();

    int len2=yourFriends.size();

    for(int i=0;i<len1;i++)

    {

        bool flag=false;

        for(int j=0;j<len2;j++)

        {

            if(myFriends[i]==yourFriends[j])

            {

                flag=true;

                break;

            }

        }

        if(flag)

        {

            ret.push_back(myFriends[i]);

        }

    }

    return ret;

}

取交集算法2(哈希算法)

vector<int> getInterp(vector<int> myFriends,vector<int> yourFriends)

{

    vector<int> ret;

    int len1=myFriends.size();

    int len2=yourFriends.size();

    unordered_map<int,bool> mp;

    for(int i=0;i<len1;i++)

    {

        mp[myFriends[i]]=true;

    }

    for(int i=0;i<len2;i++)

    {

        if(mp[yourFriends[i]])

        {

            ret.push_back(yourFriends[i]);

        }

    }

    return ret;

}

拉取可能感兴趣的人

vector<int> getInterestedList(vector<int> myFriends,vector<int> yourFriends)

{

    vector<int> ret;

    int len=yourFriends.size();

    for(int i=0;i<len;i++)

    {

        vector<int> temp=getInterp(user[yourFriends[i]].unionSet,myFriends);

        if(temp.size()>0)

        {

            ret.push_back(yourFriends[i]);

        }

    }

    return ret;

}

可行的方案二

 

遍历我所有好友的好友,用哈希表统计他们出现的次数,按出现次数从大到小排序后再剔除已经是我好友的用户,就找到了我“可能认识的人”,并能得到我们之间有多少个共同好友。

//判断某个id是不是我的好友

bool isFriend(int id,vector<int> myFriends)

{

    int len=myFriends.size();

    for(int i=0;i<len;i++)

    {

        if(id==myFriends[i])

        {

            return true;

        }

    }

    return false;

}

//获取可能认识的人

vector<int> getPossibleFriends(vector<int> myFriends)

{

    vector<int> ret;

    int len1=myFriends.size();

    map<int,int> mp;

    //遍历我所有的朋友

    for(int i=0;i<len1;i++)

    {

        //遍历朋友的朋友

        int len2=user[myFriends[i]].unionSet.size();

        for(int j=0;j<len2;j++)

        {

            mp[user[myFriends[i]].unionSet[j]]++;

        }

    }

    //剔除我的好友

    for(map<int,int>::iterator it=mp.end()-1;it>=mp.begin();it--)

    {

        pair<int.int> temp=*it;

        if(!check(temp.second,myFriends))

        {

            ret.push_back(temp.first);

        }

    }

    return ret;

}

实际上,世间万物皆有关系,皆为连接。连接并不是互联网时代的产物,只是社交网络的出现让我们的连接更为密切了。若有错误之处还望大家多多包涵和指出,笔者也很想听听微博程序员到底是如何实现“可能感兴趣的人”的功能的。

最近也是金三银四面试季,面试中还是很喜欢问这种问题的,因为比较能体现候选人分析问题、解决问题、实现具体需求的能力。在日常软件使用上,看到有趣的功能,多去思考其实现是一定没有坏处的,纵使猜想的实现与实际的实现可能存在出入。


更多精彩推荐

每一节网课背后,硬核黑科技大曝光

程序员半年涨薪15%,这些能力你有吗?

微软 CEO 萨提亚·纳德拉:不要重复造轮子,提升技术强密度

黑客用上机器学习你慌不慌?这 7 种窃取数据的新手段快来认识一下!

超详细!一文告诉你 SparkStreaming 如何整合 Kafka !附代码可实践

Libra 的 Move 语言初探,10 行代码实现你第一个智能合约

你点的每个“在看”,我都认真当成了喜欢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值