¿Cuál sería una forma eficiente y pitónica de comprobar la monotonicidad de la lista?
es decir, que tiene valores crecientes o decrecientes monótonamente?
Ejemplos:
[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
python
list
performance
Jonathan
fuente
fuente
Respuestas:
Es mejor evitar términos ambiguos como "aumentar" o "disminuir", ya que no está claro si la igualdad es aceptable o no. Siempre debe usar por ejemplo "no creciente" (claramente se acepta la igualdad) o "estrictamente decreciente" (claramente NO se acepta la igualdad).
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)
fuente
itertools.izip
lugar dezip
usted puede obtener una salida anticipada (en Python 3zip
ya funciona como un iterador)Si tiene listas grandes de números, sería mejor usar numpy, y si es así:
import numpy as np def monotonic(x): dx = np.diff(x) return np.all(dx <= 0) or np.all(dx >= 0)
debería hacer el truco.
fuente
dx[0]
seráNaN
? ¿Cuál es su matriz de entrada?np.diff()
hizo el primer elementoNaN
para que la forma de la salida coincidiera con la entrada, pero en realidad era una pieza de código diferente que hizo eso que me mordió. :)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)
Este enfoque está
O(N)
en la longitud de la lista.fuente
map
aquí se necesita exactamente la abstracción, entonces, ¿por qué recrearla con una expresión generadora?O(N)
. Podrías hacerpairs = itertools.izip(lst, itertools.islice(lst, 1, None))
.@ 6502 tiene el código perfecto para listas, solo quiero agregar una versión general que funcione para todas las secuencias:
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))
fuente
El paquete Pandas lo hace conveniente.
import pandas as pd
Los siguientes comandos funcionan con una lista de números enteros o flotantes.
Aumentando monótonamente (≥):
Estrictamente monótonamente creciente (>):
myseries = pd.Series(mylist) myseries.is_unique and myseries.is_monotonic_increasing
Alternativa usando un método privado indocumentado:
Disminuyendo monótonamente (≤):
Estrictamente monótona decreciente (<):
myseries = pd.Series(mylist) myseries.is_unique and myseries.is_monotonic_decreasing
Alternativa usando un método privado indocumentado:
fuente
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:]))
fuente
Aquí hay una solución funcional que utiliza
reduce
complejidadO(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
Reemplace
9999
con el límite superior de sus valores y-9999
con el límite inferior. Por ejemplo, si está probando una lista de dígitos, puede usar10
y-1
.Probé su rendimiento contra la respuesta de @ 6502 y es más rápido.
Caso verdadero:
[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
Caso falso del segundo elemento
[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
fuente
L = [1,2,3] L == sorted(L) L == sorted(L, reverse=True)
fuente
sorted()
si en realidad no hubiera solucionado nada, solo verifique. Mal nombre: suena como un predicado cuando no lo es.sorted(L)[0]
lugar demin
?Calculé todas las respuestas de esta pregunta en diferentes condiciones y descubrí que:
Aquí está el código para probarlo:
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)
Si la lista ya estaba aumentando monótonamente (
list_method == 1
), el más rápido al más lento era:Si la lista aumentaba en su mayoría de forma monótona (
list_method == 2
), el más rápido al más lento era:(Si el mapa de estrellas o el zip eran más rápidos o no dependía de la ejecución y no pude identificar un patrón. El mapa de estrellas parecía ser más rápido)
Si la lista era completamente aleatoria (
list_method == 3
), la más rápida a la más lenta era:fuente
n
del tamaño de la lista, y que podría variar considerablemente de 100000@ 6502 tiene un elegante código Python para esto. Aquí hay una solución alternativa con iteradores más simples y sin cortes temporales potencialmente costosos:
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)
fuente
>>> l = [0,1,2,3,3,4] >>> l == sorted(l) or l == sorted(l, reverse=True)
fuente
Aquí hay una variante que acepta secuencias materializadas y no materializadas . Determina automáticamente si es o no
monotonic
, y si es así, su dirección (es decir,increasing
odecreasing
) ystrict
ness. Se proporcionan comentarios en línea para ayudar al lector. De manera similar para los casos de prueba proporcionados al final.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)
Supongo que esto podría ser más
Pythonic
, pero es complicado, ya que evita la creación de colecciones intermedias (por ejemplolist
,genexps
, etc); además de emplear un enfoquefall/trickle-through
yshort-circuit
para filtrar a través de los diversos casos: por ejemplo, secuencias de borde (como secuencias vacías o de un elemento; o secuencias con todos los elementos idénticos); Identificar monotonicidad creciente o decreciente, rigor, etc. Espero que ayude.fuente