2009-11-03 4 views
7

J'ai une fonction sumranges() qui résume toutes les plages de nombres consécutifs trouvés dans un couple de tuples. Pour illustrer:Sommation de plages consécutives Pythoniquement

def sumranges(nums): 
    return sum([sum([1 for j in range(len(nums[i])) if 
        nums[i][j] == 0 or 
        nums[i][j - 1] + 1 != nums[i][j]]) for 
       i in range(len(nums))]) 

>>> nums = ((1, 2, 3, 4), (1, 5, 6), (19, 20, 24, 29, 400)) 
>>> print sumranges(nums) 
7 

Comme vous pouvez le voir, il renvoie le nombre de gammes de chiffres consécutifs au sein de l'uplet, soit: len ((1, 2, 3, 4), (1), (5, 6), (19, 20), (24), (29), (400)) = 7. Les tuples sont toujours ordonnés.

Mon problème est que mon sumranges() est terrible. Je déteste le regarder. Je suis actuellement en train d'itérer à travers le tuple et chaque sous-groupe, en assignant un 1 si le nombre n'est pas (1 + numéro précédent), et en additionnant le total. J'ai l'impression qu'il me manque un moyen beaucoup plus facile d'atteindre mon objectif déclaré. Est-ce que quelqu'un sait une façon plus pythonique de le faire?

Edit: J'ai comparé toutes les réponses données jusqu'ici. Merci à vous tous pour vos réponses.

Le code d'étalonnage est la suivante, en utilisant une taille d'échantillon de 100K:

from time import time 
from random import randrange 
nums = [sorted(list(set(randrange(1, 10) for i in range(10)))) for 
     j in range(100000)] 

for func in sumranges, alex, matt, redglyph, ephemient, ferdinand: 
    start = time() 
    result = func(nums) 
    end = time() 
    print ', '.join([func.__name__, str(result), str(end - start) + ' s']) 

Les résultats sont les suivants. Réponse réelle montré pour vérifier que toutes les fonctions renvoient la bonne réponse:

sumranges, 250281, 0.54171204567 s 
alex, 250281, 0.531121015549 s 
matt, 250281, 0.843333005905 s 
redglyph, 250281, 0.366822004318 s 
ephemient, 250281, 0.805964946747 s 
ferdinand, 250281, 0.405596971512 s 

RedGlyph n'edge en termes de vitesse, mais la réponse la plus simple est probablement Ferdinand, et gagne probablement pour la plupart pythonique.

Répondre

14

Mes 2 cents:

>>> sum(len(set(x - i for i, x in enumerate(t))) for t in nums) 
7 

Il est fondamentalement la même idée en Alex' post Comme décrits, mais en utilisant un set au lieu de itertools.groupby, entraînant une expression plus courte. Puisque set s sont implémentés en C et len() d'un ensemble s'exécute en temps constant, cela devrait également être assez rapide.

+2

C'est vraiment lisse. –

+0

Ah, j'avais raté le bit "tuples sont toujours ordonnés" dans la question - ce bit _does_ rend cette réponse préférable (la mienne fonctionnerait pour des tuples arbitraires, pas seulement des tuples ordonnés, mais, il y a un petit prix à payer pour ça généralité). –

9

Considérez:

>>> nums = ((1, 2, 3, 4), (1, 5, 6), (19, 20, 24, 29, 400)) 
>>> flat = [[(x - i) for i, x in enumerate(tu)] for tu in nums] 
>>> print flat 
[[1, 1, 1, 1], [1, 4, 4], [19, 19, 22, 26, 396]] 
>>> import itertools 
>>> print sum(1 for tu in flat for _ in itertools.groupby(tu)) 
7 
>>> 

nous « aplatit » la « augmentation des rampes » d'intérêt en soustrayant l'indice de la valeur, en les transformant en « runs » consécutifs de valeurs identiques; alors nous identifions et pourrions le "court" avec le précieux itertools.groupby. Cela semble être une solution assez élégante (et rapide) à votre problème.

+3

Vous venez de me faire perdre la tête. –

+2

Je me demande si 'sum (len (liste (itertools.groupby (tu))) pour tu dans flat)' serait plus rapide ou plus lent. que 'sum (1 pour ...) '. – ephemient

+2

@ ephemient, pourquoi se demander? Mesure! 'python -mtimeit' est votre ami. –

0

Cela pourrait probablement être mis ensemble sous une forme plus compacte, mais je pense que la clarté souffrirait:

def pairs(seq): 
    for i in range(1,len(seq)): 
     yield (seq[i-1], seq[i]) 

def isadjacent(pair): 
    return pair[0]+1 == pair[1] 

def sumrange(seq): 
    return 1 + sum([1 for pair in pairs(seq) if not isadjacent(pair)]) 

def sumranges(nums): 
    return sum([sumrange(seq) for seq in nums]) 


nums = ((1, 2, 3, 4), (1, 5, 6), (19, 20, 24, 29, 400)) 
print sumranges(nums) # prints 7 
+2

Ce serait bien si vous comparez les différentes solutions proposées et laissez-nous savoir comment ils fonctionnent. C'est ainsi que les gens ont décidé d'utiliser '' .join (séquence) comme un idiome Python, en comparant toutes les différentes façons de le coder. –

+0

Je vais résumer les repères dans ma question. Aussi, merci pour la réponse Matt. –

0

Vous pourriez probablement faire mieux si vous aviez un IntervalSet class parce qu'alors vous parcourir vos gammes de construisez votre IntervalSet, puis utilisez simplement le nombre de membres de l'ensemble.

Certaines tâches ne se prêtent pas toujours au code propre, en particulier si vous devez écrire le code pour les performances.

7

Juste pour montrer quelque chose plus proche de votre code d'origine:

def sumranges(nums): 
    return sum((1 for i in nums 
        for j, v in enumerate(i) 
        if j == 0 or v != i[j-1] + 1)) 

L'idée ici était de:

  • éviter de construire des listes intermédiaires, mais utiliser un lieu générateur, il permettra d'économiser des ressources
  • évitez d'utiliser des indices lorsque vous avez déjà sélectionné un sous-élément (i et v ci-dessus).

Le reste sum() est toujours nécessaire avec mon exemple.

0

Il existe une formule pour cela, la somme des n premiers nombres, 1+ 2+ ... + n = n (n + 1)/2. Alors si vous voulez avoir la somme de i-j alors c'est (j (j + 1)/2) - (i (i + 1)/2) ceci est sûr que je simplifie mais vous pouvez le résoudre. Ce n'est peut-être pas pythonique mais c'est ce que j'utiliserais.

+0

Pouvez-vous expliquer cela un peu? Gardez à l'esprit que je ne cherche pas la somme des nombres mais un compte des séquences consécutives dans mes tuples. –

+0

Est à voir, je réponds à la mauvaise question, je comprends mal.Pourquoi pas trouver la différence de tous alors ne gardez que 1s et ajouter ensuite ou Len() la liste. Je montrerais mais en tapant sur mon téléphone il suçait – Vincent

1

Voilà ma tentative:

def ranges(ls): 
    for l in ls: 
     consec = False 
     for (a,b) in zip(l, l[1:]+(None,)): 
      if b == a+1: 
       consec = True 
      if b is not None and b != a+1: 
       consec = False 
      if consec: 
       yield 1 

''' 
>>> nums = ((1, 2, 3, 4), (1, 5, 6), (19, 20, 24, 29, 400)) 
>>> print sum(ranges(nums)) 
7 
''' 

Il regarde les chiffres par paires, de vérifier si elles sont une paire consécutive (à moins que ce soit au dernier élément de la liste). Chaque fois qu'il y a une paire de chiffres consécutifs, cela donne 1.

+0

J'ai dû modifier cela un peu pour travailler avec mon benchmark (notamment en faisant une liste sur l'expression du générateur). Il arrive à 0.64s. Je pense que l'utilisation d'une expression de générateur est une solution intéressante, cependant. –