有效地找到匹配的对象对

Efficiently finding matching pairs of objects

本文关键字:对象 有效地      更新时间:2023-10-16

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

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. 使用此连接字符串散列到哈希集(具有相同出生月份和国家的两个人必须给出相同的哈希码)
  4. 如果已经有一个元素,比较x%匹配的爱好
  5. 如果匹配,则为重复
  6. 如果爱好匹配不超过x%,那么添加这个人(链表方法)

有更好的方法吗?

将月份和国家连接起来有意义吗?这个列表会很大,所以我假设,"更好"指的是存储量,而不是执行速度。

首先,您需要按monthOfBirth + country对人类进行分类。这应该是相当便宜的-只需遍历它们,将每个都放入适当的bucket中。

注意,附加字符串是实现此目的的"hack"方法。"正确"的方法是使用正确的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) {
         ...
     }
 }

参见:在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或其他方式,根据您的需要。您只需要能够通过桶和列表索引号合理快速地获得它们。

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

我想不出有什么方法可以避免将同一桶中的每个人都与同一桶中的其他每个人进行比较,但是你可以做一些工作来降低比较的成本。

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

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

右边的位是"烹饪",左边的位是"电影院"这个人的编码爱好是二进制{60 0}00011 == 3

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

因此fishing被添加到字典中并且这个人的编码爱好是{60 0}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 0}0001;Long.bitCount(1) == 1。这两个人有一个共同的爱好。

处理你的第三个人:["fishing", "clubbing", "chess"],你的成本是:

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

您将希望将二进制编码的爱好存储在非常便宜的地方。我很想使用一个long数组,带有相应的human索引:

  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中有一个已回答的问题。所以关于搜索与汉明距离接近的配对:https://cstheory.stackexchange.com/questions/18516/find-all-pairs-of-values-that-are-close-under-hamming-distance——从我对公认答案的理解来看,这是一种可以找到"非常高比例"匹配的方法,而不是全部,我想这确实需要一个蛮力搜索。

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

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

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

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