【问题标题】:Efficiently finding matching pairs of objects有效地找到匹配的对象对
【发布时间】:2014-03-11 14:35:24
【问题描述】:

我需要一种算法来在列表中找到匹配的对象对。这是一个例子:

class Human 
{
   int ID;
   string monthOfBirth;
   string country;
   string [] hobbies = {};
}

人类列表很大,问题是要找到匹配的人类配对,这需要高效地完成,因为列表很大。

匹配条件:

  1. 出生月份和国家必须完全匹配
  2. 两者都应该有超过 x% 的爱好匹配。

由于 (2) 个标准,我们无法进行精确的等式比较。

我能想到的方法有:

  1. 蛮力 - 将每个对象与其他对象进行比较。复杂度 O(n^2)
  2. 哈希表

对于哈希表方法,我正在考虑以下方式:

  1. 创建<String, List<Human>>(或MultiMap)的哈希集
  2. 将每个人类的出生月份和国家/地区连接到一个字符串
  3. 使用这个连接的字符串散列到 HashSet(两个出生月份和国家相同的人必须给出相同的散列码)
  4. 如果已经有一个元素,则比较 x% 匹配的爱好
  5. 如果匹配,则这是重复的
  6. 如果爱好不匹配超过 x%,则添加此人(链表方法)

有没有更好的方法来做到这一点?

连接月份和国家是否有意义?该列表会很大,所以我假设“更好”是指存储量,而不是执行速度。

【问题讨论】:

  • 爱好列表一般有多大?它是可变长度吗?如果是,它有最大长度吗?
  • 国家和爱好列表是不是提前知道的?您可以使用它来减少内存使用量,并可能使用更好的数据结构(命中,HashMap 不是Map 接口的唯一实现)。
  • 例如,您可以使用树形结构来存储您的Humans。第一层可以是一个长度为 12 的数组,每个槽对应一个出生月份,0 代表一月,1 代表二月,依此类推。这样,您就不会将出生月份的字符串表示形式存储在数据结构中。然后树的下一级可能是国家,依此类推。
  • @pjs 我不确定“monthOfBirth”是否是“最近一个月的出生日期”的简写。否则,它应该是一个枚举。
  • @slim 然后使用哈希值。

标签: java c++ algorithm hashmap


【解决方案1】:

首先,您需要通过monthOfBirth + country 将人类分类到桶中。这样做应该很便宜 - 只需遍历它们,将每一个弹出到适当的存储桶中。

请注意,附加字符串是解决此问题的“hacky”方式。 “正确”的方法是使用正确的 hashCode 方法创建一个关键对象:

 public class MonthCountryKey {
     String monthOfBirth;
     String country;
     // <snip> constructor, setters 
     @Override public int hashCode() {
         return Arrays.hashCode(new Object[] {
            monthOfBirth, 
            country,
         });
     }
     @Override public boolean equals(Object o) {
         ...
     }
 }

见:What is a best practice of writing hash function in java?

Map<MonthCountryKey,List<Human>> buckets = new HashMap<List<Human>>;

while(Human human = humanSource.get()) {
    MonthCountryKey key = new MonthCountryKey(human.getMonthOfBirth(), human.getCountry());
    List list = buckets.get(key);
    if(list == null) {
       list = new ArrayList<Human>();
       buckets.put(key,list);
    }
    list.add(human);
}

请注意,还有其他种类的 Set。例如,new TreeSet(monthCountryHumanComparator) -- 使用 Apache BeanUtils new TreeSet(new BeanComparator("monthOfBirth.country"))!

如果真的有 很多 人,则可能值得将存储桶存储在数据库中 - SQL 或其他,如您所见。您只需要能够通过存储桶和列表索引号合理快速地获取它们。

然后你可以依次对每个bucket应用一个爱好匹配算法,大大减少了蛮力搜索的规模。

我无法避免将存储桶中的每个人与同一存储桶中的每个其他人进行比较,但您可以做一些工作来降低比较成本。

考虑将爱好编码为整数;每个爱好一点点。一个长的给你多达64个爱好。如果您需要更多,您将需要更多整数或 BigInteger(对两种方法进行基准测试)。当您通过人类工作并遇到新的爱好时,您可以建立兴趣爱好的位位置字典。比较两组爱好然后是廉价的二进制 '&' 后跟 Long.bitCount()。

为了说明,第一个人类有爱好[ "cooking", "cinema" ]

所以右边的位是“烹饪”,左边的下一位是“电影院”,这个人类的编码爱好是二进制 {60 个零}00011 == 3

下一个人喜欢[ "cooking", "fishing" ]

所以fishing 被添加到字典中,这个人的编码爱好是{60 个零}0101 = 5

 public long encodeHobbies(List<String> hobbies, BitPositionDictionary dict) {
      long encoded = 0;
      for(String hobby : hobbies) {
          int pos = dict.getPosition(hobby); // if not found, allocates new
          encoded &= (1 << pos)
      }
      return encoded;
 }

...与...

 public class BitPositionDictionary {
     private Map<String,Integer> positions = new HashMap<String,Integer>();
     private int nextPosition;
     public int getPosition(String s) {
         Integer i = positions.get(s);
         if(i == null) {
             i = nextPosition;
             positions.put(i,s);
             nextPosition++;
         }
         return i;
     }
 }

二进制 & 他们得到 {60 zeroes}0001; Long.bitCount(1) == 1。这两个人有一个共同爱好。

要处理您的第三个人:[“钓鱼”、“俱乐部”、“国际象棋”],您的成本是:

  • 添加到爱好->位位置字典并编码为整数
  • 与迄今为止创建的所有二进制编码的爱好字符串进行比较

您会希望将二进制编码的爱好存储在访问成本非常低廉的地方。我很想只使用一个长数组,并带有相应的人类索引:

  long[] hobbies = new long[numHumans];
  int size = 0;
  for(int i = 0; i<numHumans; i++) {
      hobby = encodeHobbies(humans.get(i).getHobbies(),
                             bitPositionDictionary);
      for(int j = 0; j<size; j++) {
          if(enoughBitsInCommon(hobbies[j], hobby)) {
              // just record somewhere cheap for later processing
              handleMatch(i,j); 
          }
      }
      hobbies[size++] = hobby;
  }

随着...

  // Clearly this could be extended to encodings of more than one long
  static boolean enoughBitsInCommon(long x, long y) {
      int numHobbiesX = Long.bitCount(x);
      int hobbiesInCommon = Long.bitCount(x & y);
      // used 128 in the hope that compiler will optimise!
      return ((hobbiesInCommon * 128) / numHobbiesX ) > MATCH_THRESHOLD;
  }

这样,如果爱好类型足够少,可以长期保存,1GB数组可以保存1.68亿组爱好:)

它应该非常快;我认为 RAM 访问时间是这里的瓶颈。但这是一个蛮力搜索,并且继续是 O(n2)

如果您谈论的是真正庞大的数据集,我怀疑这种方法适用于使用 MapReduce 或其他方式进行分布式处理。


附加说明:您可以使用 BitSet 代替 long(s),并获得更多表现力;也许以牺牲一些性能为代价。再次,基准测试。

  long x,y;
  ...
  int numMatches = Long.bitCount(x & y);

  ... becomes

  BitSet x,y;
  ...
  int numMatches = x.and(y).cardinality();

两个字符串不同的位置数称为汉明距离,在 cstheory.so 上有一个回答的问题关于搜索具有接近汉明距离的对:https://cstheory.stackexchange.com/questions/18516/find-all-pairs-of-values-that-are-close-under-hamming-distance --根据我对已接受答案的理解,这是一种可以找到“非常高比例”的匹配项的方法,而不是全部,我猜这确实需要蛮力搜索。

【讨论】:

  • +1 表示概念。实际上,我对使用 BitSet 进行 encodeHobies 的想法是一样的。然后我被认为建议为国家和月份保留枚举/静态常量数组。而不是将 String 作为 Human 的一部分,他可以持有 integer 来表示月份和国家索引的乘积。
  • 我在 2.5GHz Intel CPU 上尝试了 BitCount(x &amp; y) &gt; n 蛮力搜索 long 的随机数组。它花费了(数组大小,时间):(2^16,2215ms) (2^17,9002ms), (2^18, 37349ms)。所以是的,它呈指数增长,但对于相当大的系列来说它很快。对于随机输入和n 的低值,存储匹配项变得昂贵。我猜现实世界的爱好数据的匹配项会比随机多头数据少。
  • 谢谢苗条。这非常有用。
  • 只是一个注释*,BitSet内部使用long来存储位信息。
【解决方案2】:

哈希通常是要走的路。与其连接月份和国家,您可以作弊,只需将这两个值的哈希码加在一起形成一个组合哈希码;这将为您节省一些处理工作和内存使用。您还可以为记录定义 .equals() 以实现您描述的匹配逻辑,这将让哈希集直接检查是否存在匹配条目。

【讨论】:

  • 但是如果我将月份和国家的两个哈希码加在一起,是不是有可能月份和国家的两个不同组合给出相同的哈希?所以两个不同的人有相同的哈希码?
  • 代码不是唯一的,只要它们很少发生冲突,就不必是唯一的——在这种情况下是正确的。只要 .equals() 也正确实现,哈希表就知道如何处理。
  • 建议您使用31 * hashcode(string1) + hashcode(string2) -- 但是如果发生哈希冲突,这并不是世界末日。使用哈希码的算法总是使用 equals() 来仔细检查。
【解决方案3】:

此结果假定您可以编写蛮力方法。有优化的空间,但总的来说这是正确的算法。

FindMatches (std::vector <Human> const & input, back_insert_iterator<vector> result)
{
  typedef std::pair <std::string, std::string> key_type;
  typedef std::vector <Human> Human_collection;

  typedef std::map <key_type, Human_collection> map_type;

  map_type my_map;

  for (ci = input.begin(); ci != input.end(); ++ci)
  {
    key_type my_key(ci->monthOfBirth, ci->country);

    my_map[my_key].push_back(*ci);
  }

  // Each value of my_map is now a collection of humans sharing the same birth statistics, which is the key.
  for (ci = my_map.begin(); ci != my_map.end(); ++ci)
  {
    FindMatches_BruteForce (ci->second, result);
  }

  return;
}

这里有很多可能的效率空间,例如您可以复制完整对象的指针,或者使用地图以外的其他数据结构,或者只是对输入容器进行就地排序。但从算法上讲,我相信这是最好的。

【讨论】:

    猜你喜欢
    • 2019-11-28
    • 1970-01-01
    • 2019-06-21
    • 2020-06-21
    • 2014-06-22
    • 1970-01-01
    • 2022-01-16
    • 1970-01-01
    • 2016-10-18
    相关资源
    最近更新 更多