【问题标题】:Python - How to check list monotonicityPython - 如何检查列表单调性
【发布时间】:2011-06-26 09:03:44
【问题描述】:

检查列表单调性的高效且 Pythonic 方法是什么?
即它具有单调递增或递减的值?

例子:

[0, 1, 2, 3, 3, 4]   # This is a monotonically increasing list
[4.3, 4.2, 4.2, -2]  # This is a monotonically decreasing list
[2, 3, 1]            # This is neither

【问题讨论】:

  • 最好使用“严格递增”或“非递减”来避免歧义(类似地,最好避免使用“正数”,而使用“非负数”或“严格肯定”)
  • @6502 术语单调被定义为一组非递增或非递减的有序值,因此问题中没有歧义。
  • 如果您正在寻找提取具有一定单调性的数据部分,请查看:github.com/Weilory/python-regression/blob/master/regression/…

标签: python list performance


【解决方案1】:
def solution1(a):
    up, down = True, True
    for i in range(1, len(a)):
        if a[i] < a[i-1]: up = False
        if a[i] > a[i-1]: down = False
    return up or down
    
def solution2(a):
    return a == sorted(a[::-1])

Solution1 是 O(n),较短的 solution2 是 O(n log n)

【讨论】:

    【解决方案2】:

    这里有两种方法可以仅使用range 或列表推导来确定列表是单调递增还是递减。使用 range 稍微高效一些,因为它可以短路,而列表推导必须遍历整个列表。享受吧。

    a = [1,2,3,4,5]
    b = [0,1,6,1,0]
    c = [9,8,7,6,5]
    
    def monotonic_increase(x):
        if len(x) <= 1: return False
    
        for i in range(1, len(x)):
            if x[i-1] >= x[i]:
                return False
        return True
    
    def monotonic_decrease(x):
        if len(x) <= 1: return False
    
        for i in range(1, len(x)):
            if x[i-1] <= x[i]:
                return False
    
        return True
    
    monotonic_increase = lambda x: len(x) > 1 and all(x[i-1] < x[i] for i in range(1, len(x)))
    monotonic_decrease = lambda x: len(x) > 1 and all(x[i-1] > x[i] for i in range(1, len(x)))
    
    print(monotonic_increase(a))
    print(monotonic_decrease(c))
    print(monotonic_decrease([]))
    print(monotonic_increase(c))
    print(monotonic_decrease(a))
    print(monotonic_increase(b))
    print(monotonic_decrease(b))
    

    【讨论】:

      【解决方案3】:
      def IsMonotonic(data):
          ''' Returns true if data is monotonic.'''
          data = np.array(data)
          # Greater-Equal
          if (data[-1] > data[0]):
              return np.all(data[1:] >= data[:-1])
          # Less-Equal
          else:
              return np.all(data[1:] <= data[:-1])
      

      我的提议(使用 numpy)作为这里几个想法的总结。用途

      • 转换为 np.array 以便为每个列表比较创建布尔值,
      • np.all 检查所有结果是否为True
      • 检查第一个和最后一个元素之间的差异以选择比较运算符,
      • 使用直接比较&gt;=, &lt;=而不是计算np.diff

      【讨论】:

        【解决方案4】:

        pandas 包使这很方便。

        import pandas as pd
        

        以下命令适用于整数或浮点数列表。

        Monotonically increasing(≥):

        pd.Series(mylist).is_monotonic_increasing
        

        严格单调递增(>):

        myseries = pd.Series(mylist)
        myseries.is_unique and myseries.is_monotonic_increasing
        

        使用未记录的私有方法的替代方法:

        pd.Index(mylist)._is_strictly_monotonic_increasing
        

        Monotonically decreasing (≤):

        pd.Series(mylist).is_monotonic_decreasing
        

        严格单调递减(
        myseries = pd.Series(mylist)
        myseries.is_unique and myseries.is_monotonic_decreasing
        

        使用未记录的私有方法的替代方法:

        pd.Index(mylist)._is_strictly_monotonic_decreasing
        

        【讨论】:

          【解决方案5】:

          @6502 为此提供了优雅的 python 代码。这是一个替代解决方案,它具有更简单的迭代器并且没有潜在的昂贵临时切片:

          def strictly_increasing(L):
              return all(L[i] < L[i+1] for i in range(len(L)-1))
          
          def strictly_decreasing(L):
              return all(L[i] > L[i+1] for i in range(len(L)-1))
          
          def non_increasing(L):
              return all(L[i] >= L[i+1] for i in range(len(L)-1))
          
          def non_decreasing(L):
              return all(L[i] <= L[i+1] for i in range(len(L)-1))
          
          def monotonic(L):
              return non_increasing(L) or non_decreasing(L)
          

          【讨论】:

            【解决方案6】:

            最好避免使用诸如“增加”或“减少”之类的模棱两可的术语,因为不清楚平等是否可以接受。例如,您应该始终使用“不增加”(显然接受相等)或“严格减少”(显然不接受相等)。

            def strictly_increasing(L):
                return all(x<y for x, y in zip(L, L[1:]))
            
            def strictly_decreasing(L):
                return all(x>y for x, y in zip(L, L[1:]))
            
            def non_increasing(L):
                return all(x>=y for x, y in zip(L, L[1:]))
            
            def non_decreasing(L):
                return all(x<=y for x, y in zip(L, L[1:]))
            
            def monotonic(L):
                return non_increasing(L) or non_decreasing(L)
            

            【讨论】:

            • 这是清晰、惯用的 Python 代码,其复杂度为 O(n),其中排序答案均为 O(n log n)。一个理想的答案将只在列表上迭代一次,因此它适用于任何迭代器,但这通常已经足够好了,而且它是迄今为止最好的答案。 (我会提供一次通过的解决方案,但 OP 过早地接受答案会抑制我可能不得不这样做的任何冲动......)
            • 只是出于好奇测试了您的实现与使用 sorted。你的显然慢了很多[我用 L = range(10000000) ]。似乎所有的复杂度都是 O(n),我找不到 zip 的实现。
            • 如果列表已经排序,则排序是专门的。您是否尝试过随机打乱列表的速度?另请注意,使用排序您无法区分严格递增和非递减。还要考虑使用itertools.izip 而不是zip 的Python 2.x 可以提前退出(在python 3 中zip 已经像迭代器一样工作)
            • @6502:只需要一个函数:导入运算符; def monotone(L, op): return all(op(x,y) for x, y in zip(L, L[1:])) 然后只输入你想要的:operator.le 或 .ge 或其他
            • zip 和切片操作符都返回整个列表,避免了 all() 的快捷功能;这可以通过使用 itertools.izip 和 itertools.islice 大大改善,因为 strict_increasing 或 strict_decreating 应该很早就快捷方式失败。
            【解决方案7】:

            这是一个同时接受物化非物化序列的变体。它会自动确定它是否是monotonic,如果是,它的方向(即increasingdecreasing)和strictness。提供内联 cmets 以帮助读者。最后提供的测试用例也是如此。

                def isMonotonic(seq):
                """
                seq.............: - A Python sequence, materialized or not.
                Returns.........:
                   (True,0,True):   - Mono Const, Strict: Seq empty or 1-item.
                   (True,0,False):  - Mono Const, Not-Strict: All 2+ Seq items same.
                   (True,+1,True):  - Mono Incr, Strict.
                   (True,+1,False): - Mono Incr, Not-Strict.
                   (True,-1,True):  - Mono Decr, Strict.
                   (True,-1,False): - Mono Decr, Not-Strict.
                   (False,None,None) - Not Monotonic.
                """    
                items = iter(seq) # Ensure iterator (i.e. that next(...) works).
                prev_value = next(items, None) # Fetch 1st item, or None if empty.
                if prev_value == None: return (True,0,True) # seq was empty.
            
                # ============================================================
                # The next for/loop scans until it finds first value-change.
                # ============================================================
                # Ex: [3,3,3,78,...] --or- [-5,-5,-5,-102,...]
                # ============================================================
                # -- If that 'change-value' represents an Increase or Decrease,
                #    then we know to look for Monotonically Increasing or
                #    Decreasing, respectively.
                # -- If no value-change is found end-to-end (e.g. [3,3,3,...3]),
                #    then it's Monotonically Constant, Non-Strict.
                # -- Finally, if the sequence was exhausted above, which means
                #    it had exactly one-element, then it Monotonically Constant,
                #    Strict.
                # ============================================================
                isSequenceExhausted = True
                curr_value = prev_value
                for item in items:
                    isSequenceExhausted = False # Tiny inefficiency.
                    if item == prev_value: continue
                    curr_value = item
                    break
                else:
                    return (True,0,True) if isSequenceExhausted else (True,0,False)
                # ============================================================
            
                # ============================================================
                # If we tricked down to here, then none of the above
                # checked-cases applied (i.e. didn't short-circuit and
                # 'return'); so we continue with the final step of
                # iterating through the remaining sequence items to
                # determine Monotonicity, direction and strictness.
                # ============================================================
                strict = True
                if curr_value > prev_value: # Scan for Increasing Monotonicity.
                    for item in items:
                        if item < curr_value: return (False,None,None)
                        if item == curr_value: strict = False # Tiny inefficiency.
                        curr_value = item
                    return (True,+1,strict)
                else:                       # Scan for Decreasing Monotonicity.
                    for item in items: 
                        if item > curr_value: return (False,None,None)
                        if item == curr_value: strict = False # Tiny inefficiency.
                        curr_value = item
                    return (True,-1,strict)
                # ============================================================
            
            
            # Test cases ...
            assert isMonotonic([1,2,3,4])     == (True,+1,True)
            assert isMonotonic([4,3,2,1])     == (True,-1,True)
            assert isMonotonic([-1,-2,-3,-4]) == (True,-1,True)
            assert isMonotonic([])            == (True,0,True)
            assert isMonotonic([20])          == (True,0,True)
            assert isMonotonic([-20])         == (True,0,True)
            assert isMonotonic([1,1])         == (True,0,False)
            assert isMonotonic([1,-1])        == (True,-1,True)
            assert isMonotonic([1,-1,-1])     == (True,-1,False)
            assert isMonotonic([1,3,3])       == (True,+1,False)
            assert isMonotonic([1,2,1])       == (False,None,None)
            assert isMonotonic([0,0,0,0])     == (True,0,False)
            

            我想这可能更多 Pythonic,但它很棘手,因为它避免创建中间集合(例如 listgenexps 等);以及采用fall/trickle-throughshort-circuit 方法来过滤各种情况:例如边缘序列(如空序列或单项序列;或具有所有相同项的序列);识别增加或减少的单调性、严格性等。希望对你有帮助。

            【讨论】:

              【解决方案8】:
              >>> l = [0,1,2,3,3,4]
              >>> l == sorted(l) or l == sorted(l, reverse=True)
              

              【讨论】:

              • 这是非常非常低效的。
              【解决方案9】:

              我在不同条件下对这个问题的所有答案进行计时,发现:

              • 如果列表已经单调增加,排序是最快的。
              • 如果列表被打乱/随机或乱序元素的数量大于 ~1,则排序是最慢的。当然,列表越乱,结果就越慢。
              • 如果列表大部分是单调递增或完全随机的,则 Michael J. Barbers 方法是最快的。

              这是试用的代码:

              import timeit
              
              setup = '''
              import random
              from itertools import izip, starmap, islice
              import operator
              
              def is_increasing_normal(lst):
                  for i in range(0, len(lst) - 1):
                      if lst[i] >= lst[i + 1]:
                          return False
                  return True
              
              def is_increasing_zip(lst):
                  return all(x < y for x, y in izip(lst, islice(lst, 1, None)))
              
              def is_increasing_sorted(lst):
                  return lst == sorted(lst)
              
              def is_increasing_starmap(lst):
                  pairs = izip(lst, islice(lst, 1, None))
                  return all(starmap(operator.le, pairs))
              
              if {list_method} in (1, 2):
                  lst = list(range({n}))
              if {list_method} == 2:
                  for _ in range(int({n} * 0.0001)):
                      lst.insert(random.randrange(0, len(lst)), -random.randrange(1,100))
              if {list_method} == 3:
                  lst = [int(1000*random.random()) for i in xrange({n})]
              '''
              
              n = 100000
              iterations = 10000
              list_method = 1
              
              timeit.timeit('is_increasing_normal(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)
              
              timeit.timeit('is_increasing_zip(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)
              
              timeit.timeit('is_increasing_sorted(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)
              
              timeit.timeit('is_increasing_starmap(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)
              

              如果列表已经单调增加 (list_method == 1),从最快到最慢的是:

              1. 已排序
              2. 星图
              3. 正常
              4. 压缩包

              如果列表大部分是单调递增的 (list_method == 2),从最快到最慢的是:

              1. 星图
              2. 压缩包
              3. 正常
              4. 已排序

              (starmap 或 zip 是否最快取决于执行,我无法识别模式。Starmap 似乎通常更快)

              如果列表是完全随机的 (list_method == 3),最快到最慢的是:

              1. 星图
              2. 压缩包
              3. 正常
              4. 排序(非常糟糕)

              【讨论】:

              • 我没有尝试@Assem Chelli 的方法,因为它需要了解列表中的最大项目
              • 时间比较也很大程度上取决于列表的大小n,并且可能与 100000 相差很大
              【解决方案10】:

              这是一个使用复杂的reduce O(n) 的功能解决方案:

              is_increasing = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999
              
              is_decreasing = lambda L: reduce(lambda a,b: b if a > b else -9999 , L)!=-9999
              

              9999 替换为您的值的上限,将-9999 替换为下限。例如,如果您正在测试一个数字列表,您可以使用10-1


              我针对@6502's answer 测试了它的性能,它的速度更快。

              案例真实:[1,2,3,4,5,6,7,8,9]

              # my solution .. 
              $ python -m timeit "inc = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999; inc([1,2,3,4,5,6,7,8,9])"
              1000000 loops, best of 3: 1.9 usec per loop
              
              # while the other solution:
              $ python -m timeit "inc = lambda L: all(x<y for x, y in zip(L, L[1:]));inc([1,2,3,4,5,6,7,8,9])"
              100000 loops, best of 3: 2.77 usec per loop
              

              第二个元素的大小写为 False:[4,2,3,4,5,6,7,8,7]

              # my solution .. 
              $ python -m timeit "inc = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999; inc([4,2,3,4,5,6,7,8,7])"
              1000000 loops, best of 3: 1.87 usec per loop
              
              # while the other solution:
              $ python -m timeit "inc = lambda L: all(x<y for x, y in zip(L, L[1:]));inc([4,2,3,4,5,6,7,8,7])"
              100000 loops, best of 3: 2.15 usec per loop
              

              【讨论】:

                【解决方案11】:
                import operator, itertools
                
                def is_monotone(lst):
                    op = operator.le            # pick 'op' based upon trend between
                    if not op(lst[0], lst[-1]): # first and last element in the 'lst'
                        op = operator.ge
                    return all(op(x,y) for x, y in itertools.izip(lst, lst[1:]))
                

                【讨论】:

                • 我正在考虑这样的解决方案 - 但如果列表单调增加并且前两个元素相等,它会失败。
                • @Hugh Bothwell:我现在检查第一个和最后一个以了解趋势:如果它们相等,那么所有其他元素也应该相等,这适用于 operator.le 和 operator.ge
                【解决方案12】:

                @6502 有完美的列表代码,我只想添加一个适用于所有序列的通用版本:

                def pairwise(seq):
                    items = iter(seq)
                    last = next(items)
                    for item in items:
                        yield last, item
                        last = item
                
                def strictly_increasing(L):
                    return all(x<y for x, y in pairwise(L))
                
                def strictly_decreasing(L):
                    return all(x>y for x, y in pairwise(L))
                
                def non_increasing(L):
                    return all(x>=y for x, y in pairwise(L))
                
                def non_decreasing(L):
                    return all(x<=y for x, y in pairwise(L))
                

                【讨论】:

                  【解决方案13】:

                  如果你有大量的数字列表,最好使用 numpy,如果你是:

                  import numpy as np
                  
                  def monotonic(x):
                      dx = np.diff(x)
                      return np.all(dx <= 0) or np.all(dx >= 0)
                  

                  应该可以解决问题。

                  【讨论】:

                  • 请注意 dx[0] 是 np.nan。您可能想使用: dx = np.diff(x)[1:] 跳过它。否则,至少对我来说, np.all() 调用总是返回 False。
                  • @Ryan,为什么dx[0] 会是NaN?你的输入数组是什么?
                  • N/m,我在想 np.diff() 是第一个元素 NaN 所以输出的形状与输入相匹配,但这实际上是另一段代码我。 :)
                  【解决方案14】:
                  L = [1,2,3]
                  L == sorted(L)
                  
                  L == sorted(L, reverse=True)
                  

                  【讨论】:

                  • 如果 sorted() 实际上没有对任何内容进行排序,我会选择它,只需检查一下。名字不好——听起来像谓词,其实不是。
                  • 下一步是什么?使用sorted(L)[0] 代替min
                  • 这在算法上很糟糕;这个解决方案是 O(n log n),而这个问题可以在 O(n) 中轻松完成。
                  • @all 同意你们所有人的意见,感谢建设性的批评。
                  • 我测试了这个线程here中的所有解决方案,发现排序方法实际上是最好的......如果列表实际上是单调递增的。如果列表中有任何项目乱序,它会变得最慢。
                  【解决方案15】:
                  import itertools
                  import operator
                  
                  def monotone_increasing(lst):
                      pairs = zip(lst, lst[1:])
                      return all(itertools.starmap(operator.le, pairs))
                  
                  def monotone_decreasing(lst):
                      pairs = zip(lst, lst[1:])
                      return all(itertools.starmap(operator.ge, pairs))
                  
                  def monotone(lst):
                      return monotone_increasing(lst) or monotone_decreasing(lst)
                  

                  这种方法在列表长度上是O(N)

                  【讨论】:

                  • 正确 (TM) 解决方案 IMO。胜利的功能范式!
                  • 为什么使用 itertools 而不是普通的生成器?
                  • 函数范式在 Python 中通常不是“胜利”。
                  • @6502 习惯,主要是。另一方面,map 正是这里需要的抽象,那么为什么要使用生成器表达式重新创建它呢?
                  • 计算对也是O(N)。你可以让pairs = itertools.izip(lst, itertools.islice(lst, 1, None)).
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-09-23
                  • 2012-11-09
                  • 2020-01-04
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-09-03
                  相关资源
                  最近更新 更多