如何做一个反“范围”,即根据一组数字创建一个紧凑的范围?

时间:2012-02-27 18:59:04

标签: python numbers range pcre

Python有一个range方法,允许使用以下内容:

>>> range(1, 6)
[1, 2, 3, 4, 5]

我正在寻找的是相反的:拿一个数字列表,然后返回开始和结束。

>>> magic([1, 2, 3, 4, 5])
[1, 5] # note: 5, not 6; this differs from `range()`

这对于上面的示例来说很容易做到,但是否可以允许间隙或多个范围,以类似PCRE的字符串格式返回范围?如下所示:

>>> magic([1, 2, 4, 5])
['1-2', '4-5']
>>> magic([1, 2, 3, 4, 5])
['1-5']

编辑:我正在寻找Python解决方案,但我也欢迎其他语言的工作示例。它更多的是要找出一个优雅,高效的算法。额外问题:是否有任何编程语言具有内置方法?

6 个答案:

答案 0 :(得分:11)

简化代码的一个很好的技巧是查看排序列表的每个元素及其索引的区别:

a = [4, 2, 1, 5]
a.sort()
print [x - i for i, x in enumerate(a)]

打印

[1, 1, 2, 2]

相同数字的每次运行对应a中的一系列连续数字。我们现在可以使用itertools.groupby()来提取这些运行。这是完整的代码:

from itertools import groupby

def sub(x):
    return x[1] - x[0]

a = [5, 3, 7, 4, 1, 2, 9, 10]
ranges = []
for k, iterable in groupby(enumerate(sorted(a)), sub):
     rng = list(iterable)
     if len(rng) == 1:
         s = str(rng[0][1])
     else:
         s = "%s-%s" % (rng[0][1], rng[-1][1])
     ranges.append(s)
print ranges

印刷

['1-5', '7', '9-10']

答案 1 :(得分:5)

对数字进行排序,找到连续范围(还记得RLE压缩吗?)。

这样的事情:

input = [5,7,9,8,6, 21,20, 3,2,1, 22,23, 50]

output = []
first = last = None # first and last number of current consecutive range
for item in sorted(input):
  if first is None:
    first = last = item # bootstrap
  elif item == last + 1: # consecutive
    last = item # extend the range
  else: # not consecutive
    output.append((first, last)) # pack up the range
    first = last = item
# the last range ended by iteration end
output.append((first, last))

print output

结果:[(1, 3), (5, 9), (20, 23), (50, 50)]。你弄明白了其余部分:)

答案 2 :(得分:4)

我以为你可能会喜欢我的广义clojure解决方案。

(def r [1 2 3 9 10])

(defn successive? [a b]
  (= a (dec b)))

(defn break-on [pred s]
  (reduce (fn [memo n]
            (if (empty? memo)
              [[n]]
              (if (pred (last (last memo)) n)
                (conj (vec (butlast memo))
                      (conj (last memo) n))
                (conj memo [n]))))
          []
          s))

(break-on successive? r)

答案 3 :(得分:2)

这有点优雅,但也有点恶心,取决于你的观点。 :)

import itertools

def rangestr(iterable):
    end = start = iterable.next()
    for end in iterable:
        pass
    return "%s" % start if start == end else "%s-%s" % (start, end)

class Rememberer(object):
    last = None

class RangeFinder(object):
    def __init__(self):
        self.o = Rememberer()

    def __call__(self, x):
        if self.o.last is not None and x != self.o.last + 1:
            self.o = Rememberer()
        self.o.last = x
        return self.o

def magic(iterable):
    return [rangestr(vals) for k, vals in
            itertools.groupby(sorted(iterable), RangeFinder())]


>>> magic([5,7,9,8,6, 21,20, 3,2,1, 22,23, 50])
['1-3', '5-9', '20-23', '50']

说明:它使用itertools.groupby通过键将排序后的元素组合在一起,其中键是Rememberer对象。只要连续的一组项属于同一范围块,RangeFinder类就会保留Rememberer对象。一旦您退出给定的块,它将替换Rememberer,以便密钥不会比较相等,groupby将创建一个新组。当groupby遍历排序列表时,它将元素逐个传递到rangestr,它通过记住第一个和最后一个元素并忽略其间的所有内容来构造字符串。

是否有任何实际理由使用此代替9000's answer?可能不是;它基本上是相同的算法。

答案 4 :(得分:2)

由于9000击败我,我将发布代码的第二部分,它打印出先前计算的output加上类型检查的pcre-like范围:

for i in output:
    if not isinstance(i, int) or i < 0:
        raise Exception("Only positive ints accepted in pcre_ranges")
result = [ str(x[0]) if x[0] == x[1] else '%s-%s' % (x[0], x[1]) for x in output ]
print result

输出:['1-3', '5-9', '20-23', '50']

答案 5 :(得分:2)

让我们试试发电机!

# ignore duplicate values
l = sorted( set( [5,7,9,8,6, 21,20, 3,2,1, 22,23, 50] ) )

# get the value differences 
d = (i2-i1 for i1,i2 in zip(l,l[1:]))

# get the gap indices
gaps = (i for i,e in enumerate(d) if e != 1)

# get the range boundaries
def get_ranges(gaps, l):
  last_idx = -1
  for i in gaps:
    yield (last_idx+1, i)
    last_idx = i
  yield (last_idx+1,len(l)-1)

# make a list of strings in the requested format (thanks Frg!)
ranges = [ "%s-%s" % (l[i1],l[i2]) if i1!=i2 else str(l[i1]) \
  for i1,i2 in get_ranges(gaps, l) ]

我觉得这已经变得相当可怕了。)