作者 | 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;}

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

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

【END】

更多精彩推荐

☞那个分分钟处理 10 亿节点图计算的 Plato,现在怎么样了?

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

☞数据库激荡 40 年,深入解析 PostgreSQL、NewSQL 演进历程

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

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

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

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

如何“发现”失联多年好友?代码告诉你!相关推荐

  1. sqlite 0转换为bit_Cisco Talos在SQLite中发现了一个远程代码执行漏洞

    思科Talos的研究人员在SQLite中发现了一个use-after-free() 的漏洞,攻击者可利用该漏洞在受影响设备上远程执行代码. 攻击者可以通过向受影响的SQLite安装发送恶意SQL命令来 ...

  2. grpc双向流究竟是什么情况?2段代码告诉你

    本文分享自华为云社区<grpc双向流究竟是什么情况?2段代码告诉你>,作者:breakDawn. 为什么需要grpc双向流? 有时候请求调用和返回过程,并不是简单的一问一答形式,可能会涉及 ...

  3. itools3.0服务器维护,APP Store失联?iTools3.0告诉你如何解决

    原标题:APP Store失联?iTools3.0告诉你如何解决 1月25日.26日绝对是苹果粉们一个难忘的日子,Apple Store失联了.没有任何原因,苹果方面也没有任何解释,Apple Sto ...

  4. QQ自动强制加好友代码

    是的,你也许见过强行聊天的代码: tencent://Message/?Uin=574201314&websiteName=www.oicqzone.com&Menu=yes 但是你应 ...

  5. QQ自动强制加好友代码html

    鲜为人知的QQ自动强制加好友代码 是的,你也许见过强行聊天的代码: tencent://Message/?Uin=574201314&websiteName=www.oicqzone.com& ...

  6. 转载:QQ自动强制加好友代码html

    鲜为人知的QQ自动强制加好友代码 是的,你也许见过强行聊天的代码: tencent://Message/?Uin=574201314&websiteName=www.oicqzone.com& ...

  7. git更新代码后发现本地comit的代码无法push, 提示The following untracked working tree files would be overwritten by che

    git更新代码后发现本地comit的代码无法push, 还多出很多未提交的类(不是你自己写的类)  git窗口提示The following untracked working tree files ...

  8. 以一名Java程序员穿越到异界发现自己写的代码可以影响到异界的规则为主线写一本50000字的小说...

    好的,我会尽力写出一本符合要求的小说. 这是故事的情节: 在一次意外中,一名Java程序员被传送到了一个神秘的异界.当他在这个陌生的地方游荡时,他发现自己所写的代码竟然可以影响到这个异界的规则. 这名 ...

  9. 用代码告诉你为什么努力工作却不能涨薪水

    昨天下班在QQ上跟同事说我要去参加公司里的培训. 同事:培训完涨工资吗? 我:(突发奇想, 用代码告诉他) finish_training(); if (false) {raise_my_pay(); ...

最新文章

  1. keep-alive的深入理解与使用(配合router-view缓存整个路由页面)
  2. qt定时器是阻塞的吗_吊打面试官 | 面试官:TCP真的可靠吗
  3. 客户关系管理系统-CRM源码
  4. 深度学习入门笔记系列(一)——深度学习框架 tensorflow 的介绍与安装
  5. 计算机Java程序设计标准讲义
  6. 去掉圆角_小米11高清渲染图曝光:蓝色机身 圆角矩形摄像模组
  7. 背景差分法android代码,【学术论文】基于背景差分法的尾气烟度检测系统设计...
  8. Springboot中如何引入本地jar包,并通过maven把项目成功打包成jar包部署
  9. 转科普CPU Cache line
  10. c++中的stl容器——map的介绍与常用用法
  11. 创建和使用视图及异名
  12. Android 打印之将文字转换成 Bitmap 图片,再转换成Bytes 数组 进行打印
  13. alios是安卓吗_鸿蒙OS系统被质疑,谷歌也有新布局!阿里云OS事件会再现吗?
  14. 720P、1080P、1440P、2160P、HD、FHD、UHD、2K屏、4K屏是什么意思
  15. 华为云服务器最新信息,云服务器拉新
  16. Codeforces 605E :Intergalaxy Trips
  17. ExecuteScaler的三种返回值
  18. replaceAll()用法
  19. Android开发人员的代码速查字典
  20. 一款自用的翻译小工具,开源了

热门文章

  1. eclipse中JPA插件的安装与使用
  2. urllib2 request 模拟伪装浏览器
  3. Qt 学习之路 :Qt 线程相关类
  4. RegistryBoostry2010/2011/2012的破解方法
  5. VS.NET的Bug
  6. ubuntu安装docker以及dockerfly
  7. 《SQL高级应用和数据仓库基础(MySQL版)》学习笔记 ·001【数据库基本概念、MySQL安装与介绍】
  8. Pytorch 一种调整学习率的思路
  9. Python+Opencv图像处理新手入门教程(三):阈值与二值化
  10. 剑指Offer字符串加法问题