Python - Як перевірити монотонність списку


82

Що може бути ефективним і пітонічним способом перевірити монотонність списку?
тобто що вона має монотонно зростаючі або зменшувані значення?

Приклади:

[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

14
@ 6502 термін монотонний визначається як набір, що не збільшується або не зменшується, впорядкованих значень, тому в питанні не було двозначності.
Autoplectic

якщо ви хочете отримати частину даних з певною монотонністю , будь ласка, подивіться: github.com/Weilory/python-regression/blob/master/regression/…
Вейлорі

Відповіді:


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)

15
Це зрозумілий ідіоматичний код Python, і його складність - O (n), де всі відповіді на сортування - O (n log n). Ідеальна відповідь буде переглядати список лише один раз, тому він працює на будь-якому ітераторі, але це, як правило, досить добре, і це, безумовно, найкраща відповідь серед тих, що є на сьогодні. (Я б запропонував однопрохідне рішення, але ОП передчасно приймаючи відповідь, стримує будь-яке спонукання, яке мені, можливо, доведеться зробити ...)
Гленн Мейнард,

2
просто з цікавості перевірив вашу реалізацію проти використання відсортованих. Очевидно, набагато повільніше [я використовував L = діапазон (10000000)]. Здається, складність всього - O (n), і я не зміг знайти реалізацію zip.
Зірочка

4
Сортування є спеціалізованим, якщо список уже відсортований. Ви пробували швидкість за допомогою випадково перемішаного списку? Також зауважте, що за допомогою сортування ви не можете розрізнити строго зростаюче та не зменшуване. Також враховуйте, що за допомогою Python 2.x, використовуючи itertools.izipзамість zipвас, можна отримати достроковий вихід (у python 3 zipвже працює як ітератор)
6502

3
@ 6502: потрібна лише одна функція: оператор імпорту; def monotone (L, op): повернути все (op (x, y) для x, y в zip (L, L [1:])), а потім просто подати те, що ви хочете: operator.le або .ge або що завгодно
akira

5
zip і оператор зрізу повертають цілі списки, усуваючи швидкі можливості всіх (); це можна значно покращити, використовуючи itertools.izip та itertools.islice, оскільки або строго_збільшення, або строго_зменшення має призвести до швидкого виходу з ладу дуже рано.
Х'ю Ботуелл

37

Якщо у вас великі списки чисел, можливо, найкраще використовувати 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
Н / м, я думав, що np.diff()перший елемент був зроблений NaNтаким чином, щоб форма виводу відповідала вхідним, але насправді це був інший шматок коду, який робив це, що мене вкусило. :)
Райан

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
Правильне (TM) рішення IMO. Функціональна парадигма перемоги!
mike3996

2
навіщо використовувати itertools замість простих генераторів?
6502

3
Функціональні парадигми, як правило, не є "перемогою" в Python.
Гленн Мейнард,

@ 6502 Звичка, переважно. З іншого боку, mapчи потрібна саме абстракція тут, то навіщо її відтворювати за допомогою виразу генератора?
Michael J. Barber

3
Обчислення пар - O(N)це теж. Ви могли б зробити pairs = itertools.izip(lst, itertools.islice(lst, 1, None)).
Томаш Елендт

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

Пакет Pandas робить це зручним.

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

Я думав про таке рішення, але воно не вдається, якщо список монотонно збільшується і перші два елементи рівні.
Х'ю Ботуелл

@Hugh Bowwell: я перевіряю зараз перший і останній, щоб отримати тенденцію: якщо вони рівні, то всі інші елементи також повинні бути рівними, що потім працює як для 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 і швидше.

Case True: [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 з 2-го елемента[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

2

Я приурочив усі відповіді на це запитання за різних умов і виявив, що:

  • Сортування було найшвидшим на довгий удар, ЯКЩО список уже монотонно збільшувався
  • Сортування було найповільнішим за далеким знімком, ЯКЩО список було перемішано / випадковим або якщо кількість елементів, що не працювали, перевищувала ~ 1. Тим більше, що неправильний список, звичайно, відповідає більш повільному результату.
  • Метод Майкла Дж. Барберса був найшвидшим, ЩОБ список переважно монотонно збільшувався або був абсолютно випадковим.

Ось код, щоб спробувати:

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
nealmcb

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

L == sorted(L, reverse=True)

Я б погодився, sorted()якби це насправді нічого не сортувало, просто перевірте. Погано названий - звучить як предикат, коли цього немає.
mike3996

13
Що далі? Використовуючи sorted(L)[0]замість min?
6502

4
Це алгоритмічно погано; це рішення - O (n log n), коли цю проблему можна зробити тривіально в O (n).
Гленн Мейнард

@ всі погоджуємося з усіма вами, дякую за конструктивну критику.
Зірочка

1
Я протестував усі рішення в цій темі тут і виявив, що метод сортування насправді є найкращим ... якщо список насправді монотонно збільшується. Якщо в списку є якісь елементи, які не працюють, він стає найповільнішим.
Метью Мойзен

1

@ 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, і якщо так, його напрямок (тобто increasingабо decreasing) та 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, але це складно , так як це дозволяє уникнути створення проміжних колекцій (наприклад list, genexpsі т.д.); а також використовує a fall/trickle-throughта short-circuitпідхід для фільтрації різних випадків: Наприклад, Edge-послідовності (наприклад, порожні послідовності або послідовності з одним елементом; або послідовності з усіма однаковими елементами); Виявлення зростаючої або зменшуваної монотонності, суворості тощо. Сподіваюся, це допоможе.


Чому голос проти? Це добре пояснено та пропонує читачеві альтернативний підхід до інших.
NYCeyes,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.