字符串切片的时间复杂度

时间:2016-02-03 14:57:17

标签: python time-complexity

切片Python字符串的时间复杂度是多少?鉴于Python字符串是不可变的,我可以想象将它们切换为O(1)O(n),具体取决于切片的实现方式。

我需要编写一个迭代一个(可能很大)字符串的所有后缀的函数。我可以通过将后缀表示为整个字符串的元组以及开始读取字符的索引来避免切割字符串,但这很难看。如果相反,我天真地写下我的函数:

def do_something_on_all_suffixes(big_string):
    for i in range(len(big_string)):
        suffix = big_string[i:]
        some_constant_time_operation(suffix)

...时间复杂度是O(n)还是O(n2),其中nlen(big_string)

3 个答案:

答案 0 :(得分:30)

简短回答:str切片,一般来说,复制。这意味着,为每个字符串的n后缀执行切片的函数正在执行O(n2)工作。也就是说,如果您可以使用bytes处理str对象,则可以避免使用副本。请参阅下面的如何进行零拷贝切片,了解如何使其正常工作。

长答案:(C)Python str不会通过引用数据子集的视图进行切片。 mystr[:]切片有三种操作模式:

  1. 完整切片,例如str:返回对完全相同的mystr is mystr[:]的引用(不仅仅是共享数据,相同的实际对象str,因为mystr[1:1] is mystr[2:2] is ''是不可变的,所以没有风险这样做)
  2. 零长度切片和(依赖于实现)缓存长度为1个切片;空字符串是单例(range(256)),长度为1的低序数字符串也是缓存单例(在CPython 3.5.0上,它看起来像所有字符在latin-1中都可表示,即{}中的Unicode序号{1}},已缓存)
  3. 所有其他切片:切片str在创建时复制,之后与原始str
  4. 无关

    #3是一般规则的原因是为了避免大str的大问题通过视图中的一小部分保存在内存中。如果您有一个1GB的文件,请将其读入并将其切成片状(是的,当您可以寻找时,这是浪费的,这只是为了说明):

    with open(myfile) as f:
        data = f.read()[-1024:]
    

    然后您将1 GB的数据保存在内存中以支持显示最终1 KB的视图,这是一种严重的浪费。由于切片通常很小,因此在切片上复制而不是创建视图几乎总是更快。这也意味着str可以更简单;它需要知道它的大小,但它也不需要跟踪数据的偏移量。

    如何进行零拷贝切片

    方法在Python中执行基于视图的切片,在Python 2中,它将在str上工作(因为str在Python 2中类似于字节,支持memoryviews to get zero-copy views of the original bytes data)。使用Py2 str和Py3 bytes(以及许多其他数据类型,如bytearrayarray.arraynumpy数组,mmap.mmap s等。),您可以创建buffer protocol,并且可以在不复制数据的情况下进行切片。因此,如果您可以使用(或编码)Py2 str / Py3 bytes,并且您的函数可以使用任意bytes类似的对象,那么您可以这样做:

    def do_something_on_all_suffixes(big_string):
        # In Py3, may need to encode as latin-1 or the like
        remaining_suffix = memoryview(big_string)
        # Rather than explicit loop, just replace view with one shorter view
        # on each loop
        while remaining_suffix:  # Stop when we've sliced to empty view
            some_constant_time_operation(remaining_suffix)
            remaining_suffix = remaining_suffix[1:]
    

    memoryview的切片会生成新的视图对象(它们只是超轻量级,固定大小与它们查看的数据量无关),而不是任何数据,所以{{1}如果需要可以存储副本,当我们稍后将其分割时,它不会被更改。如果您需要Py2 some_constant_time_operation / Py3 str的正确副本,您可以调用bytes来获取原始.tobytes() obj,或者(仅在Py3中显示),解码它直接从缓冲区复制的bytes,例如str

答案 1 :(得分:4)

这完全取决于你的切片有多大。我将以下两个基准汇总在一起。第一个切片整个字符串,第二个切片只是一点点。使用OpenCV Documentation进行曲线拟合

# s[1:-1]
y = 0.09 x^2 + 10.66 x - 3.25

# s[1:1000]
y = -0.15 x + 17.13706461

对于最大4MB的字符串切片,第一个看起来非常线性。我想这确实可以测量构建第二个字符串所需的时间。第二个是相当稳定的,虽然速度很快,但可能并不那么稳定。

this tool enter image description here

import time

def go(n):
    start = time.time()
    s = "abcd" * n
    for j in xrange(50000):

        #benchmark one
        a = s[1:-1]

        #benchmark two
        a = s[1:1000]

    end = time.time()
    return (end - start) * 1000

for n in range(1000, 100000, 5000):
    print n/1000.0, go(n)

答案 2 :(得分:0)

如果将索引打包在一起放在一个对象中,那么遍历索引也不错

from dataclasses import dataclass

@dataclass
class StrSlice:
    str: str
    start: int
    end: int

def middle(slice):
    return slice.str[(slice.start + slice.end) // 2]

def reverse(slice):
    return slice.str[slice.start : slice.end][::-1]

def length(slice):
    return slice.end - slice.start

def chars(slice):
    yield from (slice.str[i] for i in range(slice.start, slice.end)

def equals(slice1, slice2):
    if length(slice1) != length(slice2):
        return False
    return all(c1 == c2 for c1, c2 in zip(chars(slice1), chars(slice2))