Python

时间:2016-11-01 19:41:05

标签: python algorithm

我的一位朋友在最近得到的一个面试问题上通过了我,我对解决方案的态度不太满意。问题如下:

  • 你有两个清单。
  • 每个列表将包含长度为2的列表,其表示范围(即,[3,5]表示范围从3到5,包括3和5)。
  • 您需要返回集合之间所有范围的交集。如果我给你[1,5]和[0,2],结果将是[1,2]。
  • 在每个列表中,范围将始终增加并且永远不会重叠(即,[[0,2],[5,10] ...]永远不会[[0,2],[2,5]。 ..])

一般来说,在列表的排序或重叠方面没有“陷阱”。

示例:

a = [[0, 2], [5, 10], [13, 23], [24, 25]]
b = [[1, 5], [8, 12], [15, 18], [20, 24]]

预期产量:     [[1, 2], [5, 5], [8, 10], [15, 18], [20, 24]]

我的懒惰解决方案涉及将范围列表扩展为整数列表,然后执行集合交集,如下所示:

def get_intersection(x, y):
    x_spread = [item for sublist in [list(range(l[0],l[1]+1)) for l in x] for item in sublist]
    y_spread = [item for sublist in [list(range(l[0],l[1]+1)) for l in y] for item in sublist]
    flat_intersect_list = list(set(x_spread).intersection(y_spread))
...

但我想有一个既可读也有效的解决方案。

如果你不介意,请解释你如何在心理上解决这个问题。时间/空间复杂性分析也会有所帮助。

由于

6 个答案:

答案 0 :(得分:9)

[[max(first[0], second[0]), min(first[1], second[1])] 
  for first in a for second in b 
  if max(first[0], second[0]) <= min(first[1], second[1])]

列表理解给出了答案: [[1, 2], [5, 5], [8, 10], [15, 18], [20, 23], [24, 24]]

打破它:

[[max(first[0], second[0]), min(first[1], second[1])] 

第一学期的最大值,第二学期的最小学期

for first in a for second in b 

对于第一和第二学期的所有组合:

if max(first[0], second[0]) <= min(first[1], second[1])]

仅当第一个的最大值不超过第二个的最小值时。

如果您需要压缩输出,则以下函数执行此操作(在O(n^2)时间内,因为从列表中删除O(n),我们执行O(n)次的步骤:

def reverse_compact(lst):
    for index in range(len(lst) - 2,-1,-1):
        if lst[index][1] + 1 >= lst[index + 1][0]:
            lst[index][1] = lst[index + 1][1]
            del lst[index + 1]  # remove compacted entry O(n)*
    return lst

它加入触摸的范围,因为它们按顺序。它是反过来的,因为我们可以在就地执行此操作并在我们去的时候删除压缩的条目。如果我们没有反过来做,那么删除其他条目就会破坏我们的索引。

>>> reverse_compact(comp)
[[1, 2], [5, 5], [8, 10], [15, 18], [20, 24]]
  • 通过前进就地压缩和复制元素,压缩函数可以进一步减少到O(n),因为每个内部步骤都是O(1)(get / set而不是del),但是这不太可读:

这在O(n)时间和空间复杂度中运行:

def compact(lst):
    next_index = 0  # Keeps track of the last used index in our result
    for index in range(len(lst) - 1):
        if lst[next_index][1] + 1 >= lst[index + 1][0]:
            lst[next_index][1] = lst[index + 1][1]
        else:    
            next_index += 1
            lst[next_index] = lst[index + 1]
    return lst[:next_index + 1]

使用压缩器,列表理解是这里的主导术语,时间= O(n*m),空格= O(m+n),因为它比较了两个列表的所有可能组合而没有提前出局。这样做利用提示中给出的列表的有序结构:您可以利用该结构将时间复杂度降低到O(n + m),因为它们总是增加并且永远不会重叠,这意味着您可以一次完成所有比较。

请注意,有多个解决方案,希望您可以解决问题,然后反复改进。

满足所有可能输入的100%正确答案不是面试问题的目标。这是为了了解一个人如何思考和处理挑战,以及他们是否可以推理解决方案。

事实上,如果你给我一个100%正确的教科书答案,可能是因为你之前已经看过这个问题并且你已经知道了解决方案......因此这个问题不是&#39对我作为面试官有帮助。 &#39;检查,可以反复在StackOverflow上找到解决方案。&#39; 我们的想法是看你解决问题,而不是反复解决问题。

太多的候选人错过了森林的树木:承认缺点并提出解决方案是回答面试问题的正确方法。你不必有解决方案,你必须展示如何解决问题。

如果您能够解释并详细说明使用它的潜在问题,那么您的解决方案就可以了。

我没有回答面试问题而得到了我现在的工作:在我大部分时间都在尝试之后,我解释了为什么我的方法不起作用,而第二种方法我会尝试给予更多时间,以及潜力我在这种方法中看到的陷阱(以及为什么我最初选择了我的第一个策略)。

答案 1 :(得分:6)

OP,我相信这个解决方案有效,它运行在O(m + n)时间,其中m和n是列表的长度。 (确保,使ranges成为一个链表,以便更改其长度在恒定时间内运行。)

def intersections(a,b):
    ranges = []
    i = j = 0
    while i < len(a) and j < len(b):
        a_left, a_right = a[i]
        b_left, b_right = b[j]

        if a_right < b_right:
            i += 1
        else:
            j += 1

        if a_right >= b_left and b_right >= a_left:
            end_pts = sorted([a_left, a_right, b_left, b_right])
            middle = [end_pts[1], end_pts[2]]
            ranges.append(middle)

    ri = 0
    while ri < len(ranges)-1:
        if ranges[ri][1] == ranges[ri+1][0]:
            ranges[ri:ri+2] = [[ranges[ri][0], ranges[ri+1][1]]]

        ri += 1

    return ranges

a = [[0,2], [5,10], [13,23], [24,25]]
b = [[1,5], [8,12], [15,18], [20,24]]
print(intersects(a,b))
# [[1, 2], [5, 5], [8, 10], [15, 18], [20, 24]]

答案 2 :(得分:4)

算法

给出两个间隔,如果它们重叠,则交点的起始点是两个间隔的起始点的最大值,而其终止点是终止点的最小值:

diagram of intersecting intervals diagram of intersecting intervals

要查找所有可能相交的间隔对,请从第一对开始,并以较低的停止点不断增加间隔:

animation of algorithm

最多考虑m + n个间隔对,其中m是第一个列表的长度,n是第二个列表的长度。计算一对间隔的交点是在恒定时间内完成的,因此该算法的时间复杂度为O(m+n)

实施

为使代码简单,我在间隔中使用了Python的内置range对象。这与问题描述略有不同,因为该范围是半开区间而不是闭合区间。就是

(x in range(a, b)) == (a <= x < b)

给定两个range对象xy,它们的交点为range(start, stop),其中start = max(x.start, y.start)stop = min(x.stop, y.stop)。如果两个范围不重叠,则start >= stop会得到一个空范围:

>>> len(range(1, 0))
0

因此,给定两个xsys范围列表,每个列表的起始值都增加,则交集可以如下计算:

def intersect_ranges(xs, ys):

    # Merge any abutting ranges (implementation below):
    xs, ys = merge_ranges(xs), merge_ranges(ys)

    # Try to get the first range in each iterator:
    try:
        x, y = next(xs), next(ys)
    except StopIteration:
        return

    while True:
        # Yield the intersection of the two ranges, if it's not empty:
        intersection = range(
            max(x.start, y.start),
            min(x.stop, y.stop)
        )
        if intersection:
            yield intersection

        # Try to increment the range with the earlier stopping value:
        try:
            if x.stop <= y.stop:
                x = next(xs)
            else:
                y = next(ys)
        except StopIteration:
            return

从您的示例看来,范围可以邻接。因此,任何邻接范围必须先合并:

def merge_ranges(xs):
    start, stop = None, None
    for x in xs:
        if stop is None:
            start, stop = x.start, x.stop
        elif stop < x.start:
            yield range(start, stop)
            start, stop = x.start, x.stop
        else:
            stop = x.stop
    yield range(start, stop)

将此应用于您的示例:

>>> a = [[0, 2], [5, 10], [13, 23], [24, 25]]
>>> b = [[1, 5], [8, 12], [15, 18], [20, 24]]
>>> list(intersect_ranges(
...     (range(i, j+1) for (i, j) in a),
...     (range(i, j+1) for (i, j) in b)
... ))
[range(1, 3), range(5, 6), range(8, 11), range(15, 19), range(20, 25)]

答案 3 :(得分:1)

我知道这个问题已经得到了正确的答案。为了完整起见,我想提一下我前一段时间开发的Python库,即portionhttps://github.com/AlexandreDecan/portion),它支持这种操作(原子间隔列表之间的交集)。

您可以看一下实现,它非常接近此处提供的一些答案:https://github.com/AlexandreDecan/portion/blob/master/portion/interval.py#L406

为说明其用法,让我们考虑您的示例:

a = [[0, 2], [5, 10], [13, 23], [24, 25]]
b = [[1, 5], [8, 12], [15, 18], [20, 24]]

我们首先需要将这些“项目”转换为封闭的(原子的)间隔:

import portion as P

a = [P.closed(x, y) for x, y in a]
b = [P.closed(x, y) for x, y in b]

print(a)

...显示[[0,2], [5,10], [13,23], [24,25]](每个[x,y]Interval对象)。

然后我们可以创建一个表示这些原子间隔的并集的间隔:

a = P.Interval(*a)
b = P.Interval(*b)

print(b)

...显示[0,2] | [5,10] | [13,23] | [24,25](单个Interval对象,表示所有原子对象的并集)。

现在我们可以轻松地计算出交点:

c = a & b
print(c)

...显示[1,2] | [5] | [8,10] | [15,18] | [20,23] | [24]

请注意,我们的答案与您的答案([20,23] | [24]而不是[20,24]有所不同,因为该库要求值的连续域。我们可以按照https://github.com/AlexandreDecan/portion/issues/24#issuecomment-604456362中提出的方法,很容易地将结果转换为离散间隔,如下所示:

def discretize(i, incr=1):
  first_step = lambda s: (P.OPEN, (s.lower - incr if s.left is P.CLOSED else s.lower), (s.upper + incr if s.right is P.CLOSED else s.upper), P.OPEN)
  second_step = lambda s: (P.CLOSED, (s.lower + incr if s.left is P.OPEN and s.lower != -P.inf else s.lower), (s.upper - incr if s.right is P.OPEN and s.upper != P.inf else s.upper), P.CLOSED)
  return i.apply(first_step).apply(second_step)

print(discretize(c))

...显示[1,2] | [5] | [8,10] | [15,18] | [20,24]

答案 4 :(得分:0)

我不是一个python程序员,但是不要认为这个问题适合于灵活的Python式短解决方案,这些解决方案也很有效。

我将间隔边界视为&#34;事件&#34;标记为1和2,按顺序处理它们。每个事件切换奇偶校验字中的相应位。当我们切换到3或从3切换时,是时候发出交叉区间的开始或结束。

棘手的部分是,例如[13, 23], [24, 25]被视为[13, 25];相邻的间隔必须连接在一起。下面的嵌套if通过继续当前间隔而不是开始新间隔来处理这种情况。此外,对于相等的事件值,必须在结束之前处理间隔开始,以便例如[1, 5][5, 10]将作为[5, 5]发出,而不是一无所有。这是用事件元组的中间字段处理的。

由于排序,此实现为O(n log n),其中n是两个输入的总长度。通过成对合并两个事件列表,它可能是O(n),但是this article表明在库合并将击败库排序之前列表必须是巨大的。

def get_isect(a, b):
  events = (map(lambda x: (x[0], 0, 1), a) + map(lambda x: (x[1], 1, 1), a)
          + map(lambda x: (x[0], 0, 2), b) + map(lambda x: (x[1], 1, 2), b))
  events.sort()
  prevParity = 0
  isect = []
  for event in events:
    parity = prevParity ^ event[2]
    if parity == 3:
      # Maybe start a new intersection interval.
      if len(isect) == 0 or isect[-1][1] < event[0] - 1:
        isect.append([event[0], 0])
    elif prevParity == 3:
      # End the current intersection interval.
      isect[-1][1] = event[0]
    prevParity = parity
  return isect

这是一个O(n)版本,它有点复杂,因为它通过合并输入列表动态地找到下一个事件。除了输入和输出之外,它还只需要持续存储:

def get_isect2(a, b):
  ia = ib = prevParity = 0
  isect = []
  while True:
    aVal = a[ia / 2][ia % 2] if ia < 2 * len(a) else None
    bVal = b[ib / 2][ib % 2] if ib < 2 * len(b) else None
    if not aVal and not bVal: break
    if not bVal or aVal < bVal or (aVal == bVal and ia % 2 == 0):
      parity = prevParity ^ 1
      val = aVal
      ia += 1
    else:
      parity = prevParity ^ 2
      val = bVal
      ib += 1
    if parity == 3:
      if len(isect) == 0 or isect[-1][1] < val - 1:
        isect.append([val, 0])
    elif prevParity == 3:
      isect[-1][1] = val
    prevParity = parity
  return isect

答案 5 :(得分:0)

像我本人一样回答您的问题可能会回答一个面试问题,也可能最感谢一个答案。受访者的目标可能是证明一系列技能,而不仅限于python。因此,这个答案肯定会比这里的其他答案更抽象。

询问有关我正在操作的任何约束的信息可能会有所帮助。操作时间和空间复杂度是共同的约束,开发时间也是常见的约束,所有这些都在前面的答案中提到过。但也可能会出现其他限制。维护和与现有代码的集成与任何其他通用。

  

在每个列表中,范围将始终增加且永不重叠

当我看到此消息时,它可能意味着存在一些预先存在的代码来规范范围列表,从而对范围进行排序并合并重叠。这是一个非常普通的联合操作。加入现有团队或进行中的项目时,成功的最重要因素之一就是与现有模式集成。

交叉操作也可以通过联合操作执行。反转排序范围,将它们合并,然后反转结果。

对我来说,该答案展示了一般算法的经验,特别是“范围”问题的理解,最易读和可维护的代码方法通常是重用现有代码,并渴望帮助团队成功克服简单的困惑

另一种方法是将两个列表一起排序到一个可迭代的列表中。迭代列表,参考将每个开始/结束作为递增/递减步骤进行计数。如果在1和2的引用计数之间进行转换,则会发出范围。如果排序操作满足了我们的需求(通常也是如此),则这种方法可以固有地扩展为支持两个以上的列表。

除非另有说明,否则我将提供通用方法并讨论在编写代码之前可能会使用每种方法的原因。

因此,这里没有代码。但是您确实要求一般的方法和思考:D