项目背景:
伙伴匹配系统目的就是就是找到志同道合的伙伴,用户可以给自己打上标签,比如java,后端,篮球什么的,系统根据你给的标签会给你随机匹配和你标签重复度最高的伙伴
那问题来了,如何找到和自己标签匹配度最高的伙伴呢?
比如我的标签:["java","大一","篮球","男"]
下面有四个用户:
["java","大一","乒乓球","女"]
["python","大二","乒乓球","男"]
["python","大二","乒乓球","女"]
["java","大一","篮球","女"]
从这四个用户中如何决定和我匹配度最高的呢
我们的想法其实很简单就是看你这个标签有多少个和我是一样的,越多我就觉得你和我越匹配
当然这样做其实还有优化的空间,我们这样去做的话,就把每个标签都当成是一样的权值了,但其实现实生活中或者单论这个伙伴匹配系统而言,学习的语言(或者叫学习的方向)应该是比爱好啊,性别更重要的,所以这里其实还有优化的空间
说回这个匹配算法,我们应该怎么计算你这个标签有多少个和我是一样的,
可能最直观的想法就是指数级别的直接遍历,我的第一个标签开始,遍历你的标签数组,如果有这个标签,我就记下来,count++,然后第二个标签,这样一想直接就是指数级别的复杂度,
而且每一个人都是指数级别,我可能从数据库中查出几万条,这样性能就很低了
下面就介绍一下中O(n2)的匹配算法
编辑距离算法:
先贴一个leetcode的网址:72. 编辑距离 - 力扣(LeetCode)
如果做过这个题,那就也能解决上面的问题了,
这个编辑距离就是计算比如 horse 变成 ros 需要多少步,
再回到我们的项目,我们是否只需要去计算,把我的标签变成他的标签需要多少步,然后我们根据这个最后返回的值进行排序。
直接把这个题目的代码贴一下:
public static int minDistance(List<String> tagList1, List<String> tagList2) {
int n = tagList1.size();
int m = tagList2.size();
if (n * m == 0) {
return n + m;
}
int[][] d = new int[n + 1][m + 1];
for (int i = 0; i < n + 1; i++) {
d[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
d[0][j] = j;
}
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = d[i - 1][j] + 1;
int down = d[i][j - 1] + 1;
int left_down = d[i - 1][j - 1];
if (!Objects.equals(tagList1.get(i - 1), tagList2.get(j - 1))) {
left_down += 1;
}
d[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return d[n][m];
}
这道题也算是dp的入门题目
简单说说逻辑
dp[i][j]表示将str1的前i个字符变成str2需要几步 ------------------确定dp数组的含义
将dp这个二维数组第一行和第一列初始化为i和j ------------------初始化dp数组
增,dp[i][j] = dp[i][j - 1] + 1
删,dp[i][j] = dp[i - 1][j] + 1
改,dp[i][j] = dp[i - 1][j - 1] + 1
如果当前这个地方的字母和要改的字母相同,则dp[i][j] = dp[i - 1][j - 1] 即可
就是取步骤的最小值:
d[i][j] = Math.min(left, Math.min(down, left_down)); -------------------状态转移方程
至此我们就能算出把一个字符串变成另一个字符串需要多少步
那同理我们也能算我们把我们的标签数组变成另一个人的标签数组需要多少步,我们根据这个步数进行一个排列,输出即可
业务逻辑:
Controll层代码:
@GetMapping("/match")
public BaseResponse<List<User>> matchUser(int num,HttpServletRequest request){
log.info("寻找匹配的用户");
if(num < 0||num > 10){
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
List<User> userList = userService.matchUser(num,request);
return ResultUtils.success(userList);
}
Service层:
@Override
public List<User> matchUser(int num, HttpServletRequest request) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("tags");
queryWrapper.select("id","tags");
List<User> userlist = this.list(queryWrapper);
User loginuser = getLoginUser(request);
String tags = loginuser.getTags();
Gson gson = new Gson();
List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
}.getType());
PriorityQueue<Pair<User,Integer>> pq = new PriorityQueue<>(num,(o1,o2)->{return o1.getValue()-o2.getValue();});
for (int i = 0; i < userlist.size(); i++) {
User user = userlist.get(i);
String userTags = user.getTags();
//跳过自己和标签为空
if(userTags.equals("[]") ||StringUtils.isBlank(userTags)||user.getId().equals(loginuser.getId())){
continue;
}
List<String> usertagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
}.getType());
int minDistance = AlgorithmUtils.minDistance(usertagList, tagList);
pq.add(new Pair<>(user,minDistance));
}
List<Pair<User,Integer>> ToppairList = new ArrayList<>();
for(int i=0;i<num;++i){
if(pq.peek()!=null){
ToppairList.add(pq.poll());
}
}
ArrayList<User> finalUserList = new ArrayList<>();
User safetyuser = new User();
for(int i=ToppairList.size()-1;i>=0;i--){
final User user = this.getById(ToppairList.get(i).getKey().getId());
safetyuser = this.getSaftyUser(user);
finalUserList.add(safetyuser);
}
return finalUserList;
}
整体的代码逻辑:
1:先从数据库中选出所有的用户
2:选择优先队列这种数据结构来存储我们需要的人数(num是前端传过来的参数,表示要匹配几个人)
为什么要用优先队列,我们分析之后,发现,我们需要前num个人即可,这就是一个典型的TOPK问题。
3:将优先队列中的数据取出对用户进行一个脱敏封装到返回列表中
优化点:
优化点主要就是体现在第一步从数据库中选出所有的用户:
1:切忌不要在数据量大的时候循环输出日志
这样跑出来的控制台东西很多不便于我们调试
把这个Mybatis-plus的配置注释掉即可
2:尽量只查需要的数据
- 过滤掉标签为空的用户
-
只查需要的数据(比如 id 和 tags)
-
剔除自己
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("tags");
queryWrapper.select("id","tags");
List<User> userlist = this.list(queryWrapper);
首先封装查询条件,我们需要的字段id和tags,并且tags还得是非空的
if(userTags.equals("[]") ||StringUtils.isBlank(userTags)||user.getId().equals(loginuser.getId())){
continue;
}
遇到不符合条件的直接continue跳过即可
小坑:
queryWrapper.select("id","tags");
这段代码返回的字段只有两个就是id和tags
User [Hash = -1959240667, id=2, username=null, userAccount=null, avatarUrl=null, gender=null, userPassword=null, phone=null, email=null, userStatus=null, createTime=null, updateTime=null, isDelete=null, role=null, planetCode=null, tags=["python","大二","乒乓球","女"], profile=null, serialVersionUID=1]
User [Hash = -727357341, id=3, username=null, userAccount=null, avatarUrl=null, gender=null, userPassword=null, phone=null, email=null, userStatus=null, createTime=null, updateTime=null, isDelete=null, role=null, planetCode=null, tags=["java","大一","篮球","女"], profile=null, serialVersionUID=1]
User [Hash = -104257931, id=4, username=null, userAccount=null, avatarUrl=null, gender=null, userPassword=null, phone=null, email=null, userStatus=null, createTime=null, updateTime=null, isDelete=null, role=null, planetCode=null, tags=["java","大一","乒乓球","女"], profile=null, serialVersionUID=1]
User [Hash = 505344349, id=6, username=null, userAccount=null, avatarUrl=null, gender=null, userPassword=null, phone=null, email=null, userStatus=null, createTime=null, updateTime=null, isDelete=null, role=null, planetCode=null, tags=["python","大二","乒乓球","男"], profile=null, serialVersionUID=1]
但是我们往前端传的数据肯定不能是这样的
这也是我调试快20分钟才发现的点
所以,我们要再后面再查一次数据库,根据用户id进行查询
最后结果展示应该是: