如何确定一个列表是不是是另一个列表的子集?

Posted

技术标签:

【中文标题】如何确定一个列表是不是是另一个列表的子集?【英文标题】:How to determine if a list is subset of another list?如何确定一个列表是否是另一个列表的子集? 【发布时间】:2009-08-26 15:53:06 【问题描述】:

确定一个列表是否是另一个列表的子集的有效方法是什么?

例子:

is_subset(List(1,2,3,4),List(2,3))    //Returns true
is_subset(List(1,2,3,4),List(3,4,5))  //Returns false

我主要在寻找有效的算法,不太关心列表的存储方式。它可以存储在数组、链表或其他数据结构中。

谢谢

编辑:列表已排序

【问题讨论】:

一堆不同的语言会给你一堆不同的答案。 另外,“子集”是什么意思?例如,对于 (1,2,3),(2,1),您的子集运算会返回什么? @Anna,是的,列表已排序 @Neil 列表已排序 @cory 列表没有子集。根据您的定义,(1,3,5)是(1,2,3,4,5)的“子集”吗?如果是这样,那么您正在谈论集合。如果不是,那么您在谈论子列表或子序列。 【参考方案1】:

以下是您可以做出的一些权衡。假设您有两组元素 S 和 T,它们来自宇宙 U。我们要确定 S≥T。在给定的示例之一中,我们有

S=1,2,3,4 T=3,4,5 U=1,2,3,4,5

1.排序列表(或平衡搜索树) 大多数海报建议的方法。如果您已经有排序列表,或者不关心创建它们所需的时间长度(例如,您不经常这样做),那么该算法基本上是线性时间和空间。这通常是最好的选择。

(为了公平对待这里的其他选择,时间和空间界限实际上应该在适当的地方包含“Log |U|”的因素,但这通常不相关)

数据结构:每个 S 和 T 的排序列表。或者可以在恒定空间中迭代的平衡搜索树(例如 AVL 树、红黑树、B+树)。

算法:对于T中的每个元素,依次线性搜索S中的那个元素。记住每次搜索的中断位置,然后从那里开始下一次搜索。如果每次搜索都成功,则 S≥T。

时间复杂度:大约O( |S| Log|S| + |T| Log|T| )列表,O( max(|S|, |T|) ) 进行比较。

空间复杂度:大约O( |S| + |T| )

示例(C++)

#include <set>
#include <algorithm>

std::set<int> create_S()

    std::set<int> S;
    // note: std::set will put these in order internally
    S.insert(3);
    S.insert(2);
    S.insert(4);
    S.insert(1);
    return S;


std::set<int> create_T()

    std::set<int> T;
    // note std::set will put these in order internally
    T.insert(4);
    T.insert(3);
    T.insert(5);
    return T;


int main()

    std::set<int> S=create_S();
    std::set<int> T=create_T();
    return std::includes(S.begin(),S.end(), T.begin(), T.end());

2.哈希表 使用哈希表可以获得比排序列表更好的平均时间复杂度。大型集合的改进行为是以小型集合的性能普遍较差为代价的。

与排序列表一样,我忽略了宇宙大小造成的复杂性。

数据结构:S的哈希表,T的任何可快速迭代的东西。

算法:将 S 的每个元素插入到其哈希表中。然后,对于 T 中的每个元素,检查它是否在哈希表中。

时间复杂度O( |S| + |T| )设置,O( |T| ) 进行比较。

空间复杂度O( |S| + |T| )

示例(C++)

#include <tr1/unordered_set>

std::tr1::unordered_set<int> create_S()

    std::tr1::unordered_set<int> S;
    S.insert(3);
    S.insert(2);
    S.insert(4);
    S.insert(1);
    return S;


std::tr1::unordered_set<int> create_T()

    std::tr1::unordered_set<int> T;
    T.insert(4);
    T.insert(3);
    T.insert(5);
    return T;


bool includes(const std::tr1::unordered_set<int>& S, 
              const std::tr1::unordered_set<int>& T)

    for (std::tr1::unordered_set<int>::const_iterator iter=T.begin();
         iter!=T.end();
         ++iter)
    
        if (S.find(*iter)==S.end())
        
            return false;
        
    
    return true;


int main()

    std::tr1::unordered_set<int> S=create_S();
    std::tr1::unordered_set<int> T=create_T();
    return includes(S,T);

3.位组 如果你的宇宙特别小(假设你只能有 0-32 个元素),那么 bitset 是一个合理的解决方案。运行时间(同样,假设您不关心设置时间)基本上是恒定的。如果您确实关心设置,它仍然比创建排序列表更快。

不幸的是,即使对于中等大小的宇宙,位集也会很快变得笨拙。

数据结构:每个 S 和 T 的位向量(通常是机器整数)。在给定的示例中,我们可以编码 S=11110 和 T=00111。

算法:计算交集,通过计算S中每个位与T中相应位的按位“与”。如果结果等于T,则S≥T。

时间复杂度O( |U| + |S| + |T| ) 设置,O( |U| ) 进行比较。

空间复杂度O( |U| )

示例:(C++)

#include <bitset>

// bitset universe always starts at 0, so create size 6 bitsets for demonstration.
// U=0,1,2,3,4,5

std::bitset<6> create_S()

    std::bitset<6> S;
    // Note: bitsets don't care about order
    S.set(3);
    S.set(2);
    S.set(4);
    S.set(1);
    return S;


std::bitset<6> create_T()

    std::bitset<6> T;
    // Note: bitsets don't care about order
    T.set(4);
    T.set(3);
    T.set(5);
    return T;


int main()

    std::bitset<6> S=create_S();
    std::bitset<6> T=create_T();

    return S & T == T;

4. Bloom filters 位集的所有速度优势,没有位集对宇宙大小的讨厌限制。只有一个缺点:他们有时(通常,如果你不小心的话)会给出错误的答案:如果算法说“不”,那么你肯定没有包含。如果算法说“是”,你可能会也可能不会。选择较大的过滤器尺寸和良好的散列函数可以获得更好的准确性。

鉴于他们可以而且会给出错误的答案,布隆过滤器听起来可能是个可怕的想法。但是,它们有明确的用途。通常,人们会使用布隆过滤器快速进行许多包含检查,然后在需要时使用较慢的确定性方法来保证正确性。链接的 Wikipedia 文章提到了一些使用 Bloom 过滤器的应用程序。

数据结构:Bloom filter 是一个奇特的位集。必须事先选择过滤器大小和哈希函数。

算法(草图):将bitset初始化为0。要将元素添加到bloom过滤器,请使用每个散列函数对其进行散列,并设置bitset中的相应位。确定包含与位集一样。

时间复杂度O( 过滤器大小 )

空间复杂度O( 过滤器大小 )

正确概率:如果它回答“S 不包括 T”,则始终正确。如果它回答“S 包括 T”,则类似于 0.6185^(|S|x|T|/(filter size)))。特别是,必须选择与 |S| 的乘积成比例的过滤器尺寸。和 |T|给出合理的准确概率。

【讨论】:

【参考方案2】:

对于C++,最好的方法是使用std::includes算法:

#include <algorithm>

std::list<int> l1, l2;
...
// Test whether l2 is a subset of l1
bool is_subset = std::includes(l1.begin(), l1.end(), l2.begin(), l2.end());

这需要按照您的问题中的说明对两个列表进行排序。复杂性是线性的。

【讨论】:

【参考方案3】:

只想提一下 Python 有一个方法:

return set(list2).issubset(list1)

或者:

return set(list2) <= set(list1)

【讨论】:

我们在java中有没有类似的东西? docs.oracle.com/javase/7/docs/api/java/util/…【参考方案4】:

如果两个列表都是有序的,一个简单的解决方案是同时遍历两个列表(两个列表中都有两个凸点指针),并验证第二个列表中的所有元素都出现在第一个列表中(直到所有元素被找到,或者直到你在第一个列表中找到更大的数字)。

C++ 中的伪代码如下所示:

List l1, l2;
iterator i1 = l1.start();
iterator i2 = l2.start();
while(i1 != l1.end() && i2 != l2.end()) 
  if (*i1 == *i2) 
    i1++;
    i2++;
   else if (*i1 > *i2) 
    return false;
   else 
    i1++;
  

return true;

(显然不能按原样工作,但思路应该很清楚)。

如果列表没有排序,您可以使用哈希表 - 在第一个列表中插入所有元素,然后检查第二个列表中的所有元素是否都出现在哈希表中。

这些是算法答案。在不同的语言中,有默认的内置方法来检查这一点。

【讨论】:

【参考方案5】:

如果您担心订单或连续性,您可能需要使用Boyer-Moore 或 Horspool algorithm。

问题是,您是否要将 [2, 1] 视为 [1, 2, 3] 的子集?您是否希望将 [1, 3] 视为 [1, 2, 3] 的子集?如果这两个答案都不是,您可以考虑上面链接的算法之一。否则,您可能需要考虑使用哈希集。

【讨论】:

或者你可以做一些预处理(如果值得的话,即你将一遍又一遍地使用更大的列表)并制作一个后缀树或后缀数组。【参考方案6】:

Scala,假设您的意思是子序列的子序列:

def is_subset[A,B](l1: List[A], l2: List[B]): Boolean =
  (l1 indexOfSeq l2) > 0

无论如何,子序列只是一个子字符串问题。最优算法包括 Knuth-Morris-Pratt 和 Boyer-Moore,以及一些更复杂的算法。

如果你真正的意思是子集,那么你说的是集合而不是列表,你可以在 Scala 中使用subsetOf 方法。算法将取决于集合的存储方式。以下算法适用于列表存储,这是一个非常次优的算法。

def is_subset[A,B](l1: List[A], l2: List[B]): Boolean = (l1, l2) match 
  case (_, Nil) => true
  case (Nil, _) => false
  case (h1 :: t1, h2 :: t2) if h1 == h2 => is_subset(t1, t2)
  case (_ :: tail, list) => is_subset(tail, list)

【讨论】:

【参考方案7】:

对于 scala trunk 中的 indexOfSeq,我实现了 KMP,您可以查看:SequenceTemplate

【讨论】:

【参考方案8】:

如果您可以将数据存储在哈希集中,您可以简单地检查 list1 是否包含 list2 中每个 x 的 x。 list2 的大小将接近 O(n)。 (当然你也可以对其他数据结构做同样的事情,但这会导致不同的运行时)。

【讨论】:

应该注意,如果您担心顺序或连续性,这可能不起作用。【参考方案9】:

这在很大程度上取决于语言/工具包,以及列表的大小和存储。

如果列表已排序,则单个循环可以确定这一点。您可以开始遍历较大的列表,试图找到较小列表的第一个元素(如果您将值传递给它,则中断),然后继续下一个,并从当前位置继续。这很快,因为它是单循环/单遍算法。

对于未排序的列表,从第一个列表的元素构建某种形式的哈希表通常是最快的,然后从哈希中搜索第二个列表中的每个元素。这是许多 .NET LINQ 扩展在内部用于在列表中进行项目搜索的方法,并且可以很好地扩展(尽管它们具有相当大的临时内存需求)。

【讨论】:

【参考方案10】:
func isSubset ( @list, @possibleSubsetList ) 
    if ( size ( @possibleSubsetList ) > size ( @list ) ) 
        return false;
    
    for ( @list : $a ) 
        if ( $a != @possibleSubsetList[0] ) 
            next;
         else 
            pop ( @possibleSubsetList );
        
    
    if ( size ( @possibleSubsetList ) == 0 ) 
        return true;
     else 
        return false;
    

O(n) 中提琴。当然,isSubset( (1,2,3,4,5), (2,4) ) 会返回 true

【讨论】:

【参考方案11】:

你应该看看 STL 方法搜索的实现。这就是我认为可以完成的 C++ 方式。

http://www.sgi.com/tech/stl/search.html

说明:

当逐个元素比较时,搜索会在 [first1, last1) 范围内找到与 [first2, last2) 相同的子序列。

【讨论】:

这在以下示例中不起作用:(1, 2, 3, 4, 5), (1, 2, 5)。【参考方案12】:

您可以将检查一个列表是否是另一个列表的子集的问题与验证子字符串是否属于字符串的问题相同。最著名的算法是 KMP (Knuth-Morris-Pratt)。查看 wikipedia 以获取伪代码,或者仅使用您喜欢的语言中可用的一些 String.contains 方法。 =)

【讨论】:

【参考方案13】:

高效算法使用某种状态机,您可以将接受状态保存在内存中(在 python 中):

def is_subset(l1, l2):
    matches = []
    for e in l1:
        # increment
        to_check = [0] + [i+1 for i in matches]
        matches = [] # nothing matches
        for i in to_check:
            if l2[i] = e:
                if i == len(l2)-1:
                    return True
                matches.append(i)
    return False

编辑:当然,如果列表已排序,则不需要该算法,只需:

def is_subset(l1, l2):
    index = 0
    for e in l1:
        if e > l2[index]:
            return False
        elif e == l2[index]:
            index += 1
        else:
            index == 0
        if index == len(l2):
            return True
    return False

【讨论】:

以上是关于如何确定一个列表是不是是另一个列表的子集?的主要内容,如果未能解决你的问题,请参考以下文章

如何确定至少一个 2 项集是不是在 3 项集列表中?

检查一个列表是不是是另一个与重复项一起使用的列表的轮换

如何证明一个集合是另一个集合的子集

元组是另一个元组的子集 - Apriori 算法

检查元组列表中的所有第一个元素是不是满足条件

带有 2675 个数字列表的子集总和