java 如何高效地搜索多个集合以找到集合的部分并集?

mw3dktmi  于 2023-02-11  发布在  Java
关注(0)|答案(5)|浏览(223)

bounty将在2天后过期。回答此问题可获得+400声望奖励。Samuel Squire正在查找规范答案:我正在寻找一个解决方案,因为它可以用作编程语言的原语

如果我有很多字符串集合,每个字符串都有不同的元素,其中一些元素是重叠的。
我要搜索的集合是搜索集合中所有项目的并集。
我能用散列法做点什么吗?
联合查找算法是否正确?
我想要的是布隆过滤器吗?
在下面的示例中,我可以首先依靠HashSet的equals来检查是否存在精确匹配。
我搜索每个集合,并执行containsAll以查看它是否与搜索集合并。
举个例子,如果我有“水果”集[“橙子”,“猕猴桃”,“苹果”]和“蔬菜”集[“卷心菜”,“胡萝卜”,“花椰菜”].我想搜索[“橘子”,“猕猴桃”]来匹配“水果”
一定有比这更好的办法,但我不确定。

package main;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class SetKeyHash {

    public static void main(String args[]) {
        HashSet<String> vegetables = new HashSet<>();
        vegetables.add("tomato");
        vegetables.add("carrot");
        vegetables.add("broccoli");
        HashSet<String> fruit = new HashSet<>();
        fruit.add("apple");
        fruit.add("kiwi");
        fruit.add("orange");

        Map<HashSet<String>, String> map = new HashMap<>();
        map.put(vegetables, "vegetables");
        map.put(fruit, "fruit");

        HashSet<String> search = new HashSet<>();
        search.add("tomato");
        search.add("carrot");
        search.add("broccoli");
        System.out.println("Exact set search");
        System.out.println(map.get(search));
        System.out.println("Partial set search");
        HashSet<String> partialSearch = new HashSet<>();
        partialSearch.add("tomato");
        partialSearch.add("broccoli");
        for (HashSet<String> set : map.keySet()) {
            if (set.containsAll(partialSearch)) {
                System.out.println(String.format("Found partial match %s", map.get(set)));
            }
        }

    }
}
k2fxgqgv

k2fxgqgv1#

1.您需要改用Map<String, HashSet<String>> map = new HashMap<>();
1.以下解决方案查找包含“search”集合中所有元素的所有集合名称。

HashSet<String> vegetables = new HashSet<>();
vegetables.add("tomato");
vegetables.add("carrot");
vegetables.add("broccoli");
HashSet<String> fruit = new HashSet<>();
fruit.add("apple");
fruit.add("kiwi");
fruit.add("orange");

Map<String, HashSet<String>> map = new HashMap<>();
map.put("vegetables", vegetables);
map.put("fruit", fruit);

HashSet<String> search = new HashSet<>();
search.add("tomato");
search.add("carrot");
//search.add("broccoli");

Set<String> nameOfSets = map.entrySet().stream()
        .filter(entry -> entry.getValue().containsAll(search))
        .map(Map.Entry::getKey).collect(Collectors.toSet());
8mmmxcuj

8mmmxcuj2#

据我所知,你的问题是要把不同的蔬菜和水果分类,并把蔬菜和水果的名称只储存一次,而且你要根据食物的名称来检索食物的种类。
你描述搜索查询的方式,
“如果我将“水果”设置为[“橙子”、“猕猴桃”、“苹果”],将“蔬菜”设置为[“卷心菜”、“胡萝卜”、“花椰菜”]。我希望搜索[“橘子”、“猕猴桃”]以匹配“水果”。“
你必须遍历参数数组([“橙子”,“kiwi”])并寻找匹配项。你可以计算搜索参数中有多少是蔬菜,有多少是水果。
存储数据的工作方式与Moemen描述的方式相同。
Map<String, HashSet<String>> map = new HashMap<>();
变量的类型(“vegeticles”,“fruits”)将是存储为字符串的键,并且您的项将位于哈希集中。
整个Map看起来如下所示:
Map-〉“蔬菜”:[“番茄”、“花椰菜”、“胡萝卜”],

"fruits": ["apple", "kiwi", "orange"]

第二种方法是有一个字符串Map指向字符串。
Map=〉{字符串:字符串}
您可以输入数据并将其设置为:
Map-〉“橙子”:“水果”,

"apple": "fruit",

  "carrot": "vegetable" and so on.

这样,您就可以在搜索食物项中运行一个循环,并检索它们是水果还是蔬菜。
我几乎可以肯定,食物组合不能用来查询Map。这意味着,你必须一个元素一个元素地去。所以[“橙子”,“apple”],你必须有一个循环来查找orange的类型,然后是apple。
希望这能回答您的问题,并让您对规划数据存储结构有一个深入的了解。

ddarikpa

ddarikpa3#

另一种方法是在组件集上准备一个索引,这里的组件集是fruitsvegetables
一个Python的方法是这样做的,我相信类似的可以很容易地在java中完成

>>> lookup = defaultdict(lambda: set())
>>> populate_lookup = lambda x, y: [lookup[elem].add(y) for elem in x]
>>> populate_lookup(["orange", "kiwi", "apple"], "fruits")
[None, None, None]
>>> populate_lookup(["cabbage", "carrot", "broccoli"], "vegetables")
[None, None, None]
>>> search_matching = lambda x: set.intersection(*[lookup[elem] for elem in x])
>>> search_matching(["orange", "kiwi"])
{'fruits'}
>>>

其中,查找只是元素的反向命令

>>> dict(lookup)
{'orange': {'fruits'}, 'kiwi': {'fruits'}, 'apple': {'fruits'}, 'cabbage': {'vegetables'}, 'carrot': {'vegetables'}, 'broccoli': {'vegetables'}}
>>>

如果您想为使用SQL和表的服务解决这个问题,我建议查看Postgres中的GIN索引。
编辑(添加)
intersection的一个替代方案是如下的计数器,不一定是优化(集合交集只会查看最小的集合)

search_matching_counter = lambda x: [xi[0] for xi in Counter([set_name for elem in x for set_name in lookup[elem]]).items() if xi[1] == len(x)]

search_matching_counter(["orange", "kiwi"])
['fruits']
ia2d9nvy

ia2d9nvy4#

实际上你的问题很宽泛,这个主题很大,有很多细节被省略了,但是,我会提供和解释不同的方法,我们也会尝试回顾你提到的布隆过滤器和并集运算。
让我们先考虑一些假设

  • 我们正在处理单个集合中的唯一
  • 从这里开始,我们的目标是找到集合的**交集
  • 也就是说,我们的目标是找到所有与集合的完全交集,这意味着我们要找到所有包含集合中所有值的集合
  • 我们将字符串作为值来处理
  • 比较值时,我们将查找精确匹配
  • 我们省略了细节如何我们实际初始化我们的集合,这超出了讨论范围。我们只考虑如何搜索这些交集。
  • 我们没有自己实现任何底层逻辑,我们只是使用任何现有的库来完成这些任务,只关注原则。
  • 下面的例子并不是“写得最好”的解决方案,它们只是为了说明这些想法

为了简化代码编写,我们将使用Java Google Guava library
要添加到项目中的Maven pom.xml依赖项:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

话虽如此,让我们看看不同的方法:
∮相当直接的方式∮
当我们处理字符串时,如果字符串很长,可能会占用太多的内存空间,但只要我们不关心这些字符串在集合中的实际值,我们可以只使用它们的哈希码,如果字符串很短,那么就不需要哈希,我们可以直接使用它们.
假设我们的字符串可能很长(在真实的世界中,例如URL)。我们应该使用什么哈希算法呢?嗯,这确实取决于,但主要目标是实现唯一性,以尽可能防止冲突。因此,在本例中,我们将使用SHA-256哈希算法,它提供256位输出。
因此,在这种方法中,我们搜索交集的方式如下:

public static void main(String[] args) {
    // initialize the first set in which we'll perform searching
    Set<HashCode> vegetables = Set.of(
            hash256("tomato"),
            hash256("carrot"),
            hash256("broccoli")
    );
    // initialize the second set in which we'll perform searching
    Set<HashCode> fruit = Set.of(
            hash256("apple"),
            hash256("kiwi"),
            hash256("orange")
    );
    // collecting those collections to Map, where key = collection name, value = collection itself
    Map<String, Set<HashCode>> targetMap = Map.of("vegetables", vegetables, "fruit", fruit);

    // initialize set for searching full intersections
    Set<HashCode> search = Set.of(hash256("tomato"), hash256("carrot"));

    // let's search
    Set<String> intersections = targetMap.entrySet().stream()
            // skip sets which size is lower than search set size
            .filter(target -> target.getValue().size() >= search.size())
            // check the rest sets with containsAll()
            .filter(entry -> entry.getValue().containsAll(search))
            .map(Map.Entry::getKey).collect(Collectors.toSet());
    // print names of sets which intersect with our search set
    System.out.println(results);
}

// using Guava library to get 256-bit hash of string
public static HashCode hash256(String input) {
    HashFunction hashFunction = Hashing.sha256();
    return hashFunction.hashString(input, StandardCharsets.UTF_8);
}

注意:在遍历集合时,我们首先跳过所有小于搜索集的集合,这只是为了优化。显然,检查这些集合是没有意义的,因为它保证了它们没有足够的元素与我们的搜索集完全相交。此外,如果你想检查搜索集是否是目标集的实际示例,你可以添加equals()检查。

使用布隆过滤器

Bloom filter是一种概率数据结构,它很适合检查元素是否肯定不在集合中,但不能肯定地告诉你某个东西是否在集合中。查询返回“可能在集合中”或“肯定不在集合中”。布隆过滤器不存储元素本身,这是关键的一点,你不用布隆过滤器来测试一个元素是否存在,你用它来测试它是否确实不存在,因为它保证没有假否定。并且与散列表/散列集等相比,它占用显著更少的空间,并且加速检查。
布隆过滤器的工作原理是:获取元素,将它们通过N个不同的哈希函数,每个函数都有一个伴随的位向量,然后根据观察到的哈希值将伴随向量中的位置设置为True。一旦位置被翻转为True(1),它就永远不会被设置回False(0)。
不同散列函数的数量N以及位向量的长度通常在实现中基于配置的容量和最大允许错误率进行逆向工程。容量是我们期望布隆过滤器看到的不同元素的数量。

当然,它也有缺点:

  • 本质上是概率性的。它有假阳性概率
  • 布隆过滤器增加了复杂性。复杂性增加了出错的机会
  • 您应该注意 * 预期插入 * 的上限,因为溢出布隆过滤器的元素明显多于指定的元素,将导致其饱和,并急剧恶化其误报概率。
  • 无法删除插入的元素

因此,使用Guava库实现了布隆过滤器,我们可以写我们的代码如下:

public static void main(String[] args) throws IOException {
    // initialize the first set in which we'll perform searching
    Set<String> vegetables = Set.of("tomato", "carrot", "broccoli");
    // initialize the second set in which we'll perform searching
    Set<String> fruit = Set.of("apple", "kiwi", "orange");

    // preparing our blooms.
    // assign expectedAssertions for test = 100.
    // assign fpp (the desired false positive probability) to 0.01 = 1%
    int expectedAssertions = 100;
    BloomFilter<String> vegetablesBloom = BloomFilter.create(
            Funnels.stringFunnel(StandardCharsets.UTF_8), expectedAssertions, 0.01);
    BloomFilter<String> fruitBloom = BloomFilter.create(
            Funnels.stringFunnel(StandardCharsets.UTF_8), expectedAssertions, 0.01);

    // put all the vegetables and fruits to blooms
    vegetables.forEach(vegetablesBloom::put);
    fruit.forEach(fruitBloom::put);
    // collecting those blooms to Map, where key = bloom name, value = bloom itself
    Map<String, BloomFilter<String>> targetMap =
            Map.of("vegetables", vegetablesBloom, "fruit", fruitBloom);

    // initialize set for searching full intersections
    Set<String> search = Set.of("tomato", "carrot");

    Set<String> results = targetMap.entrySet().stream()
            // skip blooms which size is lower than search set size
            .filter(entry -> entry.getValue().approximateElementCount() >= search.size())
            .filter(entry -> intersect(search, entry.getValue()))
            .map(Map.Entry::getKey).collect(Collectors.toSet());

    System.out.println(results);
}

// check intersection of our search set with bloom
public static boolean intersect(Set<String> search, BloomFilter<String> bloomFilter) {
    for (String value : search) {
        // return false immediately if this is definitely not the case.
        if (!bloomFilter.mightContain(value)) {
            return false;
        }
    }
    return true;
}

注意expectedAssertions参数,这是fpp--期望的误报概率(必须为正且小于1.0)。我们提供的值越小,我们的bloom就越准确,然而,它越准确,我们的bloom占用的内存就越多。
这里我们在遍历我们的blooms,用mightContain(value)检查我们的搜索集合中的每个元素,如果它是false,那么我们确定不是这样的,这个bloom和我们的集合不相交。

布隆过滤器上的交集和并集运算

在前面的方法中,我们使用布隆过滤器检查搜索集合中的每个元素。您可以对两个布隆过滤器执行的一个操作是合并它们,这意味着如果您有布隆过滤器A和布隆过滤器B,最后得到包含来自A和B的所有唯一项的第三布隆过滤器C。联合两个布隆过滤器的方法是,对A和B中的每一位进行位或运算,得到C的位数。计算简单快速。
但是这个操作本身在我们的例子中并没有什么用处。但是,我们可以使用它来执行交集检查。我们如何实现这一点呢?
好吧,这里面包含了一些数学:
计数(交集(A,B))=(计数(A)+计数(B))-计数(并集(A,B))
话虽如此,让我们尝试执行此操作。
将之前方法的代码段更改为以下代码:

// prepare our search bloom
BloomFilter<String> searchBloom = BloomFilter.create(
        Funnels.stringFunnel(StandardCharsets.UTF_8), expectedAssertions, 0.01);
search.forEach(searchBloom::put);

Set<String> results = targetMap.entrySet().stream()
        // skip blooms which size is lower than search set size
        .filter(entry -> entry.getValue().approximateElementCount() >= search.size())
        .filter(entry -> intersectWithUnion(searchBloom, entry.getValue()))
        .map(Map.Entry::getKey).collect(Collectors.toSet());

System.out.println(results);

intersectWithUnion()方法:

// check intersection of our search bloom with bloom
public static boolean intersectWithUnion(BloomFilter<String> searchBloom, BloomFilter<String> bloomFilter) {
    long searchCount = searchBloom.approximateElementCount();
    long targetCount = bloomFilter.approximateElementCount();
    // making a copy of search bloom
    BloomFilter<String> union = searchBloom.copy();
    // perform union operation
    union.putAll(bloomFilter);

    // apply our formula
    long intersectionFactor = (searchCount + targetCount) - union.approximateElementCount();
    // if intersection factor is equal to initial search count, then we have full intersection
    return intersectionFactor == searchCount;
}

在这里,我们首先从搜索集中创建一个bloom。然后用我们的新方法检查每个bloom的交集。联合操作是在Guava库中用putAll()方法实现的。我们为什么要在搜索bloom上调用copy()呢?正如putAll()的文档所述:
通过对基础数据执行按位OR,将此Bloom筛选器与另一个Bloom筛选器组合在一起。变异发生在示例上。调用方必须确保Bloom筛选器大小合适,以避免饱和。
所以,putAll()改变了我们的bloom,copy()是我们的开销。

重要提示:要执行联合操作,bloom必须兼容。基本上,它们必须具有相同的 expectedAssertionsfpp。要兼容,有以下要求:

  1. BloomFilters必须具有相同数量的哈希函数
  2. BloomFilters必须具有相同大小的基础位数组
  3. BloomFilters必须具有相同的策略
  4. BloomFilters必须具有相等的漏斗
    否则将引发IllegalArgumentException

我们应该使用什么方法?

嗯,这个问题没有直接的答案,因为它确实取决于你实际情况中的许多因素。没有“总是更好”或“总是性能好”的情况。例如,当处理巨大数据量时,布隆过滤器非常有用,而且非常节省内存。数以百万计的字符串。但它有它自己的缺点。在简单的情况下,你没有这么多的数据,肯定是更好地使用一些直接的方法。希望它能帮助:)
此外,interesting article about bloom filters

4sup72z8

4sup72z85#

你可以使用倒排索引,lucene使用它。

vegetables(Document)    fruit(Document)
tomato               +
carrot               +
broccoli             +
apple                                         +
kiwi                                          +
orange                                        +

其他答案可能也是这样说的,把Map倒过来,一个一个地搜索项目。
如果你有很多这样的设置,最好插入到数据库中,比如ElasticSearch也使用倒排索引,或者使用mysql上的全文搜索引擎(仍然使用倒排索引)。

相关问题