【问题标题】:Check if list is valid sequence of chunks检查列表是否是有效的块序列
【发布时间】:2021-11-04 15:01:05
【问题描述】:

我想检查一个列表是否是一个有效的块序列,其中每个块都以某个值开始,并以相同值的下一次出现结束。例如,这是三个块的有效序列:

lst = [2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5, 2]
       \___________/  \_____/  \_______________________/

这是一个无效的:

lst = [2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4]
       \___________/  \_____/  \_____ ... missing the 2 to end the chunk

我有一个解决方案,但它很糟糕。你看到更好的东西了吗?

def is_valid(lst):
    while lst:
        start = lst.pop(0)
        if start not in lst:
            return False
        while lst[0] != start:
            lst.pop(0)
        lst.remove(start)
    return True

# Tests, should print: True, False, True, False, True
print(is_valid([2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5, 2]))
print(is_valid([2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4]))
print(is_valid(['I', 'N', 'O', 'A', 'I', 'L', 'L', 'T', 'R', 'X', 'I', 'I', 'N', 'X', 'F', 'T']))
print(is_valid(['T', 'I', 'N', 'I', 'X', 'R', 'O', 'F', 'T', 'I', 'N', 'I', 'X', 'L', 'L', 'A']))
print(is_valid([]))

【问题讨论】:

    标签: python list validation sequence chunks


    【解决方案1】:

    为此创建解决方案的简短尝试:

    def isValid(input):
       if len(input) == 0:
           return True
       firstChar = input.pop(0)
       if firstChar not in input:
           return False
       input = input[input.index(firstChar)+1:]
       isValid(input)
    

    虽然我不认为这是最快的方法,但我认为这是一个足够有趣的方法,可以在这里包含。此外,这可以通过删除这些行来进一步优化:

    if firstChar not in input:
        return False
    

    然后将代码放在 try/except 块中,如下所示:

    def isValid(input):
        if len(input) == 0:
            return True
        firstChar = input.pop(0)
        try:
            input = input[input.index(firstChar)+1:]
            isValid(input)
        except:
            return False
    

    因为如果索引不存在,此代码将给出 ValueError

    目前我还没有测试过确切的速度差异,但我确定这不是最快的方法,但它应该是相对不错的速度。

    【讨论】:

    • 请注意,使用pop(0) 和列表切片在最坏的情况下会有 O(n²),例如为[1,1,2,3,4,5, ... ,1000,1000]。在递归中使用start 参数会更快。
    • 我明白你关于 O(n^2) 复杂性的观点,但你能解释一下start 参数的含义吗? @tobias_k
    • 如果要保持递归,可以def is_valid(lst, start=0),然后递归为is_valid(lst, start=lst.index(lst[start], start+1) + 1),类似trinkot's answer。 (哦,我第一条评论中的例子当然应该是[1,1,2,2,3,3,...]
    • 哦,这很有意义。谢谢你的提示。我一回家就编辑代码并尝试进一步优化它。干杯伙伴。
    • “我还没有测试过确切的速度差异” - 似乎你根本没有测试过:-)。鉴于此问题的五个测试用例中有四个失败(实际上是所有测试用例,因为您更改了函数名称)。
    【解决方案2】:

    怎么样,从列表中创建一个iter 并在该迭代上向前搜索,直到找到next 匹配元素。请注意,这可能会失败,因为 None 可以是列表的元素;那么您应该定义并与哨兵obj = object()进行比较。

    def is_valid(lst):
        it = iter(lst)
        for x in it:
            if next((y for y in it if y == x), None) is None:
                return False
        return True
    

    由于我们实际上并不需要next返回的值,所以我们也可以直接使用any,同时解决default元素的问题。与next 一样,any 将使用迭代器,直到匹配元素(如果有):

    def is_valid(lst):
        it = iter(lst)
        for x in it:
            if not any(y == x for y in it):
                return False
        return True
    

    这可以使用all 而不是外部for 循环进一步缩短:

    def is_valid(lst):
        it = iter(lst)
        return all(any(y == x for y in it) for x in it)
    

    这个终于可以归结为同样神秘而耐人寻味的:

    def is_valid(lst):
        it = iter(lst)
        return all(x in it for x in it)
    

    每一种方式,每个元素只被访问一次,原始列表没有改变,几乎没有额外的空间,恕我直言,它甚至有点容易阅读和理解。


    这与速度无关,但无论如何:以下是不同解决方案(以及更多变体)的一些基准测试,运行问题中的测试用例以及 1,000 个整数的两个随机列表,一个有效,一个无效, 10,000 次,在 Python 3.8.10 上:

    # with long lists             # only short test lists
    1.52 is_valid_index           0.22 is_valid_index
    3.28 is_valid_next            0.30 is_valid_next
    2.78 is_valid_for_for_else    0.13 is_valid_for_for_else
    5.26 is_valid_for_any         0.32 is_valid_for_any
    5.29 is_valid_all_any         0.38 is_valid_all_any
    3.42 is_valid_all_any_if      0.36 is_valid_all_any_if
    2.02 is_valid_all_in          0.18 is_valid_all_in
    1.97 is_valid_all_in_if       0.17 is_valid_all_in_if
    1.87 is_valid_for_in          0.11 is_valid_for_in
    

    当然,都是 O(n)。对于长 1000 个元素列表,使用 index 的解决方案最快,但使用 x in it 的解决方案也不错。 any 解决方案有些落后,但在使用 generator with condition 时与 next 一样快(或慢),但仍比使用普通 for 循环时慢。 只有简短的测试列表,情况有点不同:这里,使用一个迭代器和for-for-elsefor-in 的解决方案是最快的。

    【讨论】:

    • 嗯,“神秘”?也许乍一看:-)。但它只是找到块起始值并测试它们是否再次出现。或者也许我只是非常熟悉在迭代器上使用成员资格测试,我之前解决了一些其他问题(example)。顺便说一句,我很欣赏您甚至使用了相同的变量名(请参阅第四个测试用例向后阅读。也许 that 是神秘的 :-D)。
    • @don'ttalkjustcode 当然x in itany(y == x for y in it) 完全相同,以至于我想知道为什么这个heureka-moment 花了我这么长时间,但不知何故我仍然找到带有@ 的版本987654351@ 和any 更清晰一些。也许for y in it 只是更明确地表明它继续 迭代下一个元素。不过,非常漂亮和简短!
    • 嗯,几乎,就像also checks identity 一样(但我认为它与这个问题无关)。是的,我也一直在想,想着“拜托,你怎么没看到?” :-)。哦,刚刚查找旧的东西,我偶然发现this...看看wim链接到什么/谁:-D
    • did it again :-D
    • 您能否分享基准代码,以便我们查看您测量的内容并自己运行它?例如,在 trincot 的基准测试中,我认为您的最终解决方案将是最快的。
    【解决方案3】:

    这个问题没有完全解释我们是否需要贪婪的解决方案。

    考虑一个例子 - [1, 2, 1, 1]

    如果我们考虑贪心方法,则解决方案将找到第一个序列为 [1, 2, 1],并留下 [1]。因此,将返回 False。

    但如果没有贪心方法,解决方案会将 [1, 2, 1, 1] 视为完整序列并返回 True。

    我运行了你提供的解决方案,它返回 False,所以我假设我们需要一个贪婪的方法。

    所以,这是一种可能的解决方案:

    def is_valid(lst):
        to_find = None
        
        for value in lst:
            if to_find is None:
                to_find = value
                continue
            
            if to_find is value:
                to_find = None
    
        return to_find is None
        
    # Tests, should print: True, False, True, False, True
    print(is_valid([2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5, 2]))
    print(is_valid([2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4]))
    print(is_valid(['I', 'N', 'O', 'A', 'I', 'L', 'L', 'T', 'R', 'X', 'I', 'I', 'N', 'X', 'F', 'T']))
    print(is_valid(['T', 'I', 'N', 'I', 'X', 'R', 'O', 'F', 'T', 'I', 'N', 'I', 'X', 'L', 'L', 'A']))
    print(is_valid([]))
    

    【讨论】:

    • 实际上这个问题确实很清楚,说“以下一次发生结束”。正是出于这个原因,问题中已经强调了“下一次发生”。我故意写了一个“无效”的例子,以便可以进行“非贪婪”分块,以证明那是无效的。
    • isNone 的正确比较,但不是一般情况。例如,对于is_valid([x**x for x in [9, 9]]),您返回False
    • 在设置之后,我有点希望在这里看到一个非贪婪的解决方案......
    • @tobias_k 哈,是的,这可能很有趣。
    • None-safe dict version of this(可能效率较低,但对称性有点可爱)。
    【解决方案4】:

    您似乎想确保最后一个“块”在列表末尾关闭。这应该这样做:

    def is_valid(lst):
      search = None
      paired = True
      for item in lst:
        if paired:
          search = item
          paired = False
        elif search == item:
          paired = True
      return paired
    

    这是O(n),每个元素只检查一次,因此您无需为长输入列表的start not in lst 检查支付费用。

    【讨论】:

    • 我认为我的 start not in lst 检查总体上也是 O(n),因为这些检查不重叠。
    【解决方案5】:

    这是我对这个问题的看法。我已经针对可读性进行了优化,而不是速度(当然要保持在 O(n) 中):

    def is_valid(sequence):
        iterator = iter(sequence)
        for element in iterator:
            for other in iterator:
                if element == other:
                    break
            else:
                return False
        return True
    

    外循环的每次迭代都对应一个块。当我们这里的元素用完时,我们在块边界处结束序列,我们可以return True。否则,我们循环遍历迭代器,直到找到匹配的元素。如果我们用完了元素(一个“自然”终止的 for 循环,没有break,进入它的else)我们return False


    还有另一个使用itertools。我不喜欢上面的解决方案,主要是因为 next 与哨兵的神秘使用:

    from itertools import dropwhile
    
    def is_valid(iterable):
        iterator = iter(iterable)
        sentinel = object()
        for element in iterator:
            if next(dropwhile(lambda x: x != element, iterator), sentinel) is sentinel:
                return False
        return True
    

    【讨论】:

    • 我也认为这是我第一个解决方案的变体。不过,您可以使用另一个外部 for 循环而不是 try/while/next/except
    • @tobias_k 你说得对,看起来更好;编辑。这只是第二个不起作用的解决方案。
    • @tobias_k 如果我使用 next... 编辑的替代形式,我什至可以避免那里的 try
    • 嗯,现在已经非常接近我的第一个变体了,你可以用all 来缩短它(这就是我的第三个变体)。我其实最喜欢你的第一个;这就像我的第 2 步和第 3 步“之前”的一步,但同时确实非常可读。
    【解决方案6】:

    以下是该问题的替代递归解决方案。基本上只是检查下一个目标是否在列表中,然后跳到该索引再次检查。我不是这里的专家,但想尝试并贡献一种不同的方式来解决这个问题。

    def is_valid(
        input_list: list, 
        target_index: int = 0):
    
        # If we have only one element remaining, or if an empty list is passed initially, there cannot be a pair.
        if len(input_list) <= 1:
            return False
        
        target = input_list[target_index]
        search_range = input_list[target_index + 1 :]
    
        # print(f"target index: {target_index}")
        # print(f"target: {target}")
        # print(f"search range: {search_range}")
        # print("-------------------------------")
    
        if target in search_range:
    
            found_target_sublist_index = search_range.index(target)
    
            # Plus 2: two indexes start at 0 -> off by two
            next_target_index = target_index + found_target_sublist_index + 2
    
            if next_target_index == len(input_list):
                return True
    
            return is_valid(input_list, next_target_index)
        else:
            return False
    
    
    test_one = [2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5, 2]
    test_two = [2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4]
    test_three = ['I', 'N', 'O', 'A', 'I', 'L', 'L', 'T', 'R', 'X', 'I', 'I', 'N', 'X', 'F', 'T']
    test_four = ['T', 'I', 'N', 'I', 'X', 'R', 'O', 'F', 'T', 'I', 'N', 'I', 'X', 'L', 'L', 'A']
    
    print(is_valid(test_one)) 
    print(is_valid(test_two))
    print(is_valid(test_three))
    print(is_valid(test_four))
    

    【讨论】:

    • 正如在另一个答案中提到并由我的代码指出的那样,一个空列表是一个有效的序列(零块)。
    【解决方案7】:

    使用pop(0) 更改列表的成本很高且不需要。

    你可以使用index...当块很大时,这可能会特别快:

    def is_valid(lst):
        i = 0
        n = len(list)
        while i < n:
            try:
                i = lst.index(lst[i], i + 1) + 1
            except:
                return False
        return True
    

    【讨论】:

    • 索引调用对于大型输入列表同样昂贵。您最终会反复扫描输入列表的内容。
    • 是的,但它们使用编译后的代码进行扫描,这与在 Python 代码中迭代的循环形成对比。时间复杂度是一样的,但是当chunk比较大的时候执行时间会偏向index
    • @g.d.d.c 这不是从列表开始索引,而是从i + 1
    • 我“多次”错过:不,不是多次,@g.d.d.c
    • 这是一个比较这个解决方案与 gddc 的基准,使用一个包含 100000 个个位数的随机列表:repl.it
    猜你喜欢
    • 2014-01-05
    • 2021-02-15
    • 2022-11-25
    • 2017-06-08
    • 1970-01-01
    • 2013-05-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多