【问题标题】:Fastest way to slice a list with upper bound not in the list对列表中没有上限的列表进行切片的最快方法
【发布时间】:2021-05-09 21:59:41
【问题描述】:

假设我们有一个巨大的整数排序列表。使用不在列表中的上限对该列表进行切片的最快方法是什么?

例如,假设我们的列表是:

l=list(range(0,1000000, 2))

(这是一个简单的例子,列表可以是任意长度并且没有特定的区间,所以它不能与某个范围相关)

我们想要得到一个切片,其中的项目小于limit=1001

实现这一目标的最快方法是什么,最好不检查列表中的所有项目? 一种常见的方法是使用列表推导,例如[i for i in l if i<limit],但是这样我们必须检查 l 的所有项目并将它们与限制进行比较。如果限制在列表中,我们可以使用 l[:l.index(limit)] 之类的东西,但如果它不在列表中怎么办? 有什么想法吗?

【问题讨论】:

  • 你要找的是二分搜索。
  • FWIW l.index 也是 O(n),所以 l[:l.index(limit)][i for i in l if i<limit] 具有相同的复杂性
  • @ScottHunter 二进制搜索也不会提高渐近复杂度,因为切片仍然是线性的。但是你为什么要把range 对象变成一个列表呢?如果您知道确切的界限,只需再创建一个range
  • 列表是否总是排序并且元素之间的间隔相同,并且没有丢失元素?如果是这样,您可以使用一些基本的数学公式(很像 range 它自己的实现方式)
  • 顺便说一句,我希望你知道你可以直接切片 range 对象

标签: python list


【解决方案1】:

您可以为此使用bisect

import bisect
print(l[:bisect.bisect_right(l, 1001)])

【讨论】:

  • 打败我!这很快,bisect 未得到充分利用。
【解决方案2】:

我只是想为这两个答案放一些比较时间。

鉴于此基准:

import bisect 
import time 

def f1(l, tgt):
    return bisect.bisect_right(l, tgt)

def f2(l,tgt):
    slice_condition = lambda num: num >= tgt
    try:
        slice_idx = next(idx for idx, num in enumerate(l) if slice_condition(num))
    except StopIteration:
        slice_idx = len(l)
    return slice_idx 

def f3(l,tgt):
    return next((idx for idx, val in enumerate(l) if val>=tgt), len(l))


def cmpthese(funcs, args=(), cnt=10, rate=True, micro=True, deepcopy=True):
    from copy import deepcopy 
    """Generate a Perl style function benchmark"""                   
    def pprint_table(table):
        """Perl style table output"""
        def format_field(field, fmt='{:,.0f}'):
            if type(field) is str: return field
            if type(field) is tuple: return field[1].format(field[0])
            return fmt.format(field)     

        def get_max_col_w(table, index):
            return max([len(format_field(row[index])) for row in table])         

        col_paddings=[get_max_col_w(table, i) for i in range(len(table[0]))]
        for i,row in enumerate(table):
            # left col
            row_tab=[row[0].ljust(col_paddings[0])]
            # rest of the cols
            row_tab+=[format_field(row[j]).rjust(col_paddings[j]) for j in range(1,len(row))]
            print(' '.join(row_tab))                

    results={}
    for i in range(cnt):
        for f in funcs:
            if args:
                local_args=deepcopy(args)
                start=time.perf_counter_ns()
                f(*local_args)
                stop=time.perf_counter_ns()
            results.setdefault(f.__name__, []).append(stop-start)
    results={k:float(sum(v))/len(v) for k,v in results.items()}     
    fastest=sorted(results,key=results.get, reverse=True)
    table=[['']]
    if rate: table[0].append('rate/sec')
    if micro: table[0].append('\u03bcsec/pass')
    table[0].extend(fastest)
    for e in fastest:
        tmp=[e]
        if rate:
            tmp.append('{:,}'.format(int(round(float(cnt)*1000000.0/results[e]))))

        if micro:
            tmp.append('{:,.1f}'.format(results[e]/float(cnt)))

        for x in fastest:
            if x==e: tmp.append('--')
            else: tmp.append('{:.1%}'.format((results[x]-results[e])/results[e]))
        table.append(tmp) 

    pprint_table(table)                    

if __name__=='__main__':
    import sys
    print(sys.version)
    
    small=range(1_000)
    mid=range(100_000)
    large=range(1_000_000)
    cases=(
        ('small, found', small, len(small)//2),
        ('small, not found', small, len(small)),
        ('mid, found', mid, len(mid)//2),
        ('mid, not found', mid, len(mid)),
        ('large, found', large, len(large)//2),
        ('large, not found', large, len(large))
    )
    for txt, x, tgt in cases:
        print(f'\n{txt}:')
        l=list(x)
        args=(l,tgt)
            cmpthese([f1,f2,f3],args)

如果您使用小型、中型和大型列表运行它,每个列表都有 1) 在中间找到的情况或 2) 一直扫描到最后,您可以看到 bisect 明显更快。按数量级。

基准打印在我的电脑上:

3.9.1 (default, Jan 30 2021, 15:51:59) 
[Clang 12.0.0 (clang-1200.0.32.29)]

small, found:
   rate/sec μsec/pass      f2      f3     f1
f2      182   5,501.4      --  -59.2% -98.8%
f3      445   2,246.9  144.9%      -- -96.9%
f1   14,562      68.7 7911.4% 3172.0%     --

small, not found:
   rate/sec μsec/pass       f2      f3     f1
f2       90  11,053.2       --  -58.8% -99.5%
f3      220   4,555.5   142.6%      -- -98.7%
f1   17,349      57.6 19076.4% 7803.3%     --

mid, found:
   rate/sec μsec/pass        f2       f3     f1
f2        2 561,882.8        --   -57.2% -99.9%
f3        4 240,253.2    133.9%       -- -99.9%
f1    2,942     339.9 165184.0% 70573.1%     --

mid, not found:
   rate/sec   μsec/pass        f2        f3      f1
f2        1 1,119,041.1        --    -58.0% -100.0%
f3        2   469,960.8    138.1%        --  -99.9%
f1    3,804       262.9 425552.8% 178660.3%      --

large, found:
   rate/sec   μsec/pass        f2       f3     f1
f2        0 5,833,734.0        --   -55.3% -99.9%
f3        0 2,605,010.2    123.9%       -- -99.9%
f1      335     2,988.1 195135.5% 87080.9%     --

large, not found:
   rate/sec    μsec/pass        f2        f3      f1
f2        0 11,553,311.3        --    -54.4% -100.0%
f3        0  5,264,216.7    119.5%        -- -100.0%
f1      710      1,408.9 819923.5% 373540.2%      --

【讨论】:

  • 不错的比较。我想知道循环的常见解决方案在哪里,例如以下一个:m=[l[0]] n=1 while m[-1]<limit: m.append(l[n]) n+=1 m.pop()
  • 不确定我可以直接比较。 Bisect 是基于 C 的,并且比纯 Python 方法更快。列表中的.pop() 也不是最佳的。如果.pop() 对您的应用程序至关重要,请考虑使用deque 来更快地在每一端插入/删除...
【解决方案3】:

O(n)(特别是2k,其中k 是列表需要切片的索引)中,一种快速简便的方法是搜索不满足条件的第一个元素,并使用迭代器切片到该点:

slice_condition = lambda num: num >= limit
slice_idx = next((idx for idx, num in enumerate(l) if slice_condition(num)), len(l))
slice = l[:slice_idx]

当然,二分查找会在O(log(n))时间找到slice_idx,但是切片无论如何都是线性操作,所以整个单元的复杂度仍然是O(n)

【讨论】:

  • 如果您使用 next(thing, DEFAULT) 而不是 try 块恕我直言,这会更快
  • @dawg 老实说,我忘记了 next(thing, default) 的存在。感谢您的提醒,我已经相应地编辑了答案
  • 您的新版本抛出错误:SyntaxError: Generator expression must be parenthesized if not sole argument 不过您原来的解决方案足够快,谢谢!
猜你喜欢
  • 1970-01-01
  • 2012-09-27
  • 1970-01-01
  • 2012-03-29
  • 2020-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多