使用map降低算法的时间复杂度

在前两周在开发新接口时,遇到了今天要介绍的两种场景,我都通过map降低了整个算法的时间复杂度。我认为有必要记录一下思考过程,也顺便分享给各位读者。虽然简单,但确实很好用。

1. 群组中的好友

背景

各位读者应该都知道在微信聊天框中输入@之后,会出现各群员的列表。新接口功能上差不多,只是在这基础上还要加上一个限制条件:“列表中的群成员必须是好友”。我可以从其他微服务已经有的“好友接口”获取某一个用户的好友列表(list),那么我只需要找出和群成员列表的交集即可。

实现

当时想到有两种方法可以实现上面说的效果(设群有N个群成员,M个好友):

  1. 采用两层遍历查询方式,遍历群成员list + 二分查找好友list(查询的时间复杂度:O(N * logM))
  2. 采用map,将群成员id作为map的key,role作为value(查询的时间复杂度:O(1))

哪种方式更好?通过时间复杂度来看,答案就不言而喻了。部分代码如下:

// 获取群内成员(我的源码有用到role做别的业务逻辑处理)
func GetMemberInGroup(groupID int64) (map[int64]int8, error) 
   dataSet := make(map[int64]int8)
   strSql := "SELECT `player_id`, `role` FROM `friend_group` WHERE `group_id` = ? AND `type` = 1 AND `role` = 0"
   rows, err := db.MysqlCon.Raw(strSql, groupID).Rows()
   if err != nil {
      return nil, err
   }
   var (
      playerID int64
      role     int8
   )
   defer rows.Close()
   for rows.Next() {
      if err = rows.Scan(&playerID, &role); err == nil {
         dataSet[playerID] = role
      }
   }
   return dataSet, err
}
...
// 用其他微服务的接口获取`playerID`这个用户的好友列表(设长度为N1)
friends, err := model.ListFriend(playerID, int32(pb.FriendType_FT_FRIEND))
if err != nil {
   ...
}
// 用上面的持久层方法获取群内成员
memberMap, err := model.GetMemberInGroup(groupID)
if err != nil {
   ...
}
// 结果(保存即是群员也是好友的用户id)
var result []int64
for _, friend := range friends {
   _, exists := memberMap[friend.FriendId]
   if exists {
      result = append(result, friend.FriendId)
   }
}
...

若各位读者要使用以上代码,最好在使用前能大致预测一下各组数据的大小,因为将数据多的list转为map,用数据小的list来做遍历会更好。

若其他微服务提供了获取好友map的接口,可以忽略上面的代码了。

2. 勋章排序

背景

需要将用户获得的勋章列表返回给客户端,且勋章列表需要管理员按照配置的权重排序。存在的问题:用户获得的勋章储存在数据库里(数据库里没有勋章的权重),勋章配置的结构体列表储存在Redis里(权重值存在Redis)。

实现

部分代码如下:

// Redis的勋章配置结构体(权重值存在Redis)
// 这个是redis.Medal
type Medal struct {
   MedalId   string    `json:"column:Id"`
   MedalName string    `json:"Name"`
   CreateAt  time.Time `json:"CreateAt"`
   MedalType int32     `json:"Type"`
   MedalIcon string    `json:"Icon"`
   WeightInAchievePage int32  `json:"WeightInAchievePage"`
   ...
}

// 储存在数据库的用户获取的勋章(没有权重值)
// 这个是db.Medal
type Medal struct {
   MedalId   string    `gorm:"primary_key;column:Id"`
   CreateAt  time.Time `gorm:"column:CreateAt"`
   ...
}
...
// 从数据库查询用户获取的勋章
medalList, err := dao.MemberDB.GetMedalList()
var result []string
if err == nil {
   // 将用户获取的勋章list转map,减少查询的时间复杂度
   medalMap := make(map[string]*db.Medal, 0)
   for _, v := range medalList {
      medalMap[v.MedalId] = v
   }
   // 查询管理员后台页面的勋章配置列表
   // 这里medalConfList的类型是上面redis.Medal结构体数组
   // 值得一提的是这里redis.Medal结构体数组数据是在数据库按照权重排好序存进Redis的,代码在下面
   // 为了加快客户端获取配置的效率
   medalConfList := common.GetMedalList()
   for _, v := range medalConfList {
      if v.MedalType == int32(pb.MedalType_Glory) {
         // 按照后台页面配置的勋章的权重,对已获取的勋章排序
         if _, ok := medalMap[v.MedalId]; ok {
            result = append(result, v.MedalName)
         }
      }
   }
}
...

func GetMedalList() []*redis.Medal {
   list := make([]*redis.Medal, 0)
   bin, err := db.RedisCon.Get(db.CacheMedalListKey).Bytes()
   if err == nil && len(bin) > 0 {
      if err = ujson.Unmarshal(bin, &list); err != nil {
         ...
      }
   }

   err = dao.MainDB().Order("WeightInAchievePage DESC").Find(&list).Error
   if err == nil || err == gorm.ErrRecordNotFound {
      if bin, err = ujson.Marshal(list); err == nil {
         db.RedisCon.Set(db.CacheMedalListKey, bin, 0)
      }
   }
   return list
}
...

如果Redis存的勋章结构体里有储存勋章的权重,上面的代码就不适用了。

结语

两个算法都是采用了类似的方法,用map这个数据结构,通过空间换时间来将查询时间复杂度降到了常数阶。如果读者有想到更好的算法,欢迎留言。