Python-如何检查列表的单调性


81

什么是检查列表单调性的有效且Python方式?
即它具有单调增加或减少的值?

例子:

[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

5
最好使用术语“严格增加”或“不减少”来消除任何歧义(以类似的方式,最好避免使用“正面”,而改用“非负面”或“严格正面”)
6502年

13
@ 6502术语“单调性”被定义为有序值的非递增或非递减集合,因此问题没有任何歧义。
Autoplectic 2011年

如果您正在寻找提取某些单调的数据部分,请看看:github.com/Weilory/python-regression/blob/master/regression/...
Weilory

Answers:


160

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

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)

14
这是清晰易懂的Python代码,其复杂度为O(n),其中排序答案均为O(n log n)。一个理想的答案只能在列表上进行一次迭代,因此它可以在任何迭代器上使用,但这通常足够好,并且是迄今为止最好的答案。(我会提供单程解决方案,但是OP过早接受答案会遏制我可能必须这样做的任何冲动...)
Glenn Maynard

2
出于好奇,只是测试了您对使用排序的实现。您的速度显然要慢得多[我用过L = range(10000000)]。似乎所有的复杂度都是O(n),而且我找不到zip的实现。
星号

4
如果列表已经排序,则排序是专用的。您是否使用随机混排的列表尝试提高速度?还要注意,使用排序您无法区分严格递增和非递减。还要考虑使用Python 2.xitertools.izip代替zip可以提前退出(在python 3中zip已经像迭代器一样工作了)
6502

3
@ 6502:只需要一个功能:导入运算符;def monotone(L,op):在zip(L,L [1:])中返回all(x,y的op(x,y)),然后只输入您想要的内容:operator.le或.ge或其他
akira

5
zip和slice运算符都返回整个列表,从而消除了all()的快捷功能;这可以通过使用itertools.izip和itertools.islice进行极大地改进,因为strict_increasing或strict_decreasing应该在很短的时间内出现快捷方式失败。
休·博斯韦尔

36

如果您有大量数字,最好使用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?您的输入数组是什么?
DilithiumMatrix

1
N / m,我以为是np.diff()第一个元素,NaN所以输出的形状与输入匹配,但是实际上那是另一段代码,这对我造成了影响。:)
Ryan 2015年

24
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)在列表的长度中。


3
正确的解决方案IMO。功能范例取胜!
mike3996 2011年

2
为什么使用itertools代替普通生成器?
6502

3
在Python中,功能范式通常不是“胜利”。
Glenn Maynard

@ 6502习惯,主要是。另一方面,map正是这里需要抽象,那么为什么要使用生成器表达式重新创建抽象呢?
Michael J. Barber

3
计算对O(N)也是。你可以做pairs = itertools.izip(lst, itertools.islice(lst, 1, None))
Tomasz Elendt'2

18

@ 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))

6

大熊猫封装使这个方便。

import pandas as pd

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

单调递增(≥):

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

单调递减(≤):

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

4
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:]))

我当时正在考虑这样的解决方案-但如果列表单调增加并且前两个元素相等,则失败。
休·博斯韦尔

@休·博斯韦尔:我现在检查第一个和最后一个趋势:如果它们相等,则所有其他元素也应相等,然后对operator.le和operator.ge都起作用
akira

3

这是使用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的答案及其更快的速度测试了它的性能。

真实情况: [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

第二个元素的情况为假[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

1
L = [1,2,3]
L == sorted(L)

L == sorted(L, reverse=True)

sorted()如果它实际上没有进行任何排序,我会去检查一下。名称不正确-听起来好像不是谓词。
mike3996

13
下一步是什么?使用sorted(L)[0]代替min
6502

4
这在算法上很差;当此问题可以在O(n)中轻松解决时,此解决方案为O(n log n)。
Glenn Maynard

@all同意所有人,感谢您的建设性批评。
星号

1
我在这里测试了该线程中的所有解决方案,发现排序的方法实际上是最好的...如果列表实际上是单调增加的。如果列表中的任何项目混乱,它将成为最慢的项目。
马修·摩森

1

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

  • 如果列表已经单调增加,排序是远景最快的
  • 如果列表随机/随机排列,或者如果元素的数量大于或等于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. 已排序

(无论星图或zip是否最快取决于执行情况,我都无法确定模式。星图通常看起来更快)

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

  1. 星图
  2. 压缩
  3. 正常
  4. 排序(极差)

我没有尝试@Assem Chelli的方法,因为它要求了解列表中的最大项目
Matthew Moisen

时间比较也将在很大程度上取决于n列表的大小,并且可能与100000的列表有很大
出入

0

@ 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)


-1

这是一个接受物化非物化序列的变体。它会自动确定它是否为monotonic,如果是,则确定其方向(即increasingdecreasing)和strict强度。提供内联注释以帮助读者。对于最后提供的测试用例也是如此。

    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方法来过滤各种情况:例如边缘序列(如空序列或单项序列;或具有所有相同项目的序列);确定增加或减少的单调性,严格性等。希望对您有所帮助。


为什么要投票?它得到了很好的解释,并为读者提供了另一种替代方法。
NYCeyes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.