如何对列表列表进行排序,并按间隔仅保留每个第一元素的最大第二元素? 初始版本尚无答案最终版本进一步的优化基准化

问题描述

这是this question的较硬版本,但我无法有效解决最好,而无需导入库)。

假设我有一些清单:

lst = [[1,2],[1,4],6],[2,3],[3,5],[7,8]]

假设我有一个间隔列表:

intervals = [0,3,5,8]

我想在每个间隔中保留一个一个元素的子列表和一个具有最高第二个元素的子列表。在此示例中,这意味着将只有一个一个元素在0和3之间的子列表,一个只有第一个元素在3和5之间的子列表,依此类推,因此结果将是:

result:
>>> [[1,8]]

请注意:

  • 以{0 =
  • 最好,例如,如果我们有[1,6]和[2,6]的间隔与 将保留的是第一个元素最低的元素([1,6])

解决方法

以下是三种解决方案,按性能排序:

  1. 为每个元素中的第一个/第二个数字创建两个列表。它增加了内存使用量,但却是最快的选择。

  2. 使用key中的max参数获得第二个数字最高的元素。避免重复使用内存,但速度要慢30%。这可能是一个很好的中间立场。

  3. 使用itertools.groupbykey function来获取每个元素中第一个数字的间隔。它可以用于更健壮的应用程序,但是效率不高,它迭代Intervals直到找到匹配间隔为止的每个元素。它比第一种选择慢了近三倍。


选项1:创建两个列表

将列表分为两个列表,以列出每个元素的第一个/第二个数字。

# sort and separate lst
lst = sorted(lst)
first = [e[0] for e in lst]
second = [e[1] for e in lst]

# iterate upper limits of intervals and get max of each sublist
i = k = 0
keep = []
while lst[i][0] < Intervals[0]:
    i += 1
for upper in Intervals[1:]:
    k = sum(f < upper for f in first[i:])
    keep.append(i + second[i:i+k].index(max(second[i:i+k])))
    i += k

result = [lst[i] for i in keep]
print(result)

输出

[[1,6],[3,5],[7,8]]

选项2:使用max(lst,key)

您可以使用max(lst,key=lambda x: x[1])获得第二个数字最大的元素。这是间隔的实现。

lst = sorted(lst)

i = k = 0
result = []
for upper in Intervals:
    i += k
    # old solution summed a generator
    # k = sum(e[0] < upper for e in lst[i:])
    # this one uses a while-loop to avoid checking the rest of the list on each iteration
    # a good idea if `lst` is long and `Intervals` are many
    k = 0
    while i + k < len(lst) and lst[i+k][0] < upper: 
        k += 1
    if upper == Intervals[0]:
        continue
    result.append(max(lst[i:i+k],key=lambda x:x[1]))

输出

[[1,8]]

选项3:itertools.groubpy(lst,key)

from itertools import groupby

def get_bin(element,bins):
    x = element[0]
    if x < bins[0]:
        return -1
    elif x in bins:
        return bins.index(x)
    else:
        for i,b in enumerate(bins[1:]):
            if x < b:
                break
        return i
        

result = sorted([
    max(items,key=lambda x: x[1])
    for _,items in groupby(lst,lambda x: get_bin(x,Intervals))
])

输出

[[1,8]]
,

为简单起见:

lst = [[1,2],[1,4],[2,3],8]]
intervals = [0,3,5,8] #usually,variables starts lowercase

初始版本(尚无答案)

我将演示如何根据intervals 中的索引将列表分为几组,然后在此处返回每个组的最大项。您可以使用一个技巧,我想将其称为数组的“ shift”:

def get_groups(lst,intervals):
    return [lst[i:j] for i,j in zip(intervals[:-1],intervals[1:])]

这是一种构造切片元组的好方法,这些切片是:(0,3)(3,5)(5,8)。现在您有了:

>>> groups = get_groups(lst,interval)
>>> groups
[[[1,6]],[[2,3]],[[3,8]]]

然后在按第二列排序时提取最大元素:

>>> [max(n,key = lambda x: x[1]) for n in groups]
[[1,8]]

重要的是要区分第二列具有相同值的两个项目:

[max(n,key = lambda x: (x[1],x[0])) for n in groups]

最终版本

相反,

OP需要根据属于intervals 的值将列表分为几组。如果列表已预先排序,则有可能在第一个结果的顶部构建算法,并且我们正在对数组进行一次搜索,以查找应该插入元素以保持顺序的索引。在这种情况下,get_groups应该重新定义如下:

def get_groups(lst,intervals):
    lst = sorted(lst)
    firstcolumn = [n[0] for n in lst]
    intervals = searchsorted(first_column,intervals)
    return [lst[i:j] for i,intervals[1:])]

目前,您还可以使用RichieV答案的改编版本:

def searchsorted(array,intervals):
    idx,i,n = [],len(array)
    for upper in intervals:
        while array[i] < upper:
            i += 1
            if i == n:
                idx.append(n)
                return idx
        else:
            idx.append(i)
    return idx

>>> searchsorted([1,1,2,7],[0,8])
[0,6,7]

请注意,get_groups并不是最佳选择,因为first_columnlst都被迭代了两次。

用法:

def simple_sol(lst,intervals):
    return [max(n,key=lambda x: x[1]) for n in get_groups(lst,intervals)]
#Output: [[1,8]]

进一步的优化

我写了一个由替代方法np.searchsorted启发的searchsorted的定义,该方法基于二进制搜索。效率也更高(O(m log(n))O(mn))。对于Python版本,另请参阅bisect.bisect_leftdocs中的source coderelated answer关于二进制搜索。这是双赢的C级+二进制搜索(与我的previous answer差不多):

def binsorted(lst,intervals):
    lst = np.array(lst)
    lst = lst[np.argsort(lst[:,0])] #sorting lst by first row
    idx = np.searchsorted(lst[:,0],intervals)
    if idx[-1] == len(lst):
        return np.maximum.reduceat(lst,idx[:-1],axis=0)
    else:
        return np.maximum.reduceat(lst,idx,axis=0)

#Output: [[2,8]]

基准化

我比较了样本的option1option2option3simple_solbinsorting

lst = np.random.randint(1000,size = (1000000,2)).tolist()
intervals = np.unique(np.random.randint(1000,size = 100)).tolist() + [1000]

timeit是:

18.4 s ± 472 ms per loop (mean ± std. dev. of 7 runs,1 loop each)
4.21 s ± 386 ms per loop (mean ± std. dev. of 7 runs,1 loop each)
10.3 s ± 410 ms per loop (mean ± std. dev. of 7 runs,1 loop each)
4.12 s ± 202 ms per loop (mean ± std. dev. of 7 runs,1 loop each)
1.38 s ± 97.2 ms per loop (mean ± std. dev. of 7 runs,1 loop each)