【问题标题】:Why is str.replace so fast?为什么 str.replace 这么快?
【发布时间】:2021-12-19 20:18:29
【问题描述】:

我目前正在学习和练习字符串算法。具体来说,我正在尝试用一些修改替换基于KMP 的字符串中的模式,这具有 O(N) 复杂度(我的实现如下)。

def replace_string(s, p, c):
    """
    Replace pattern p in string s with c
    :param s: initial string
    :param p: pattern to replace
    :param c: replacing string
    """
    pref = [0] * len(p)

    s_p = p + '#' + s
    p_prev = 0
    shift = 0

    for i in range(1, len(s_p)):
        k = p_prev

        while k > 0 and s_p[i] != s_p[k]:
            k = pref[k - 1]

        if s_p[i] == s_p[k]:
            k += 1

        if i < len(p):
            pref[i] = k

        p_prev = k

        if k == len(p):
            s = s[:i - 2 * len(p) + shift] + c + s[i - len(p) + shift:]
            shift += len(c) - k

    return s

然后,我用python内置的str.replace函数写了同样的程序:

def replace_string_python(s, p, c):
    return s.replace(p, c)

比较各种字符串的性能,我只附上一个例子,长度为 1e5 的字符串:

import time


if __name__ == '__main__':
    initial_string = "a" * 100000
    pattern = "a"
    replace = "ab"

    start = time.time()
    res = replace_string(initial_string, pattern, replace)

    print(time.time() - start)

输出(我的实现):

total time: 1.1617710590362549

输出(python 内置):

total time: 0.0015637874603271484

如您所见,通过 python str.replace 实现比 KMP 领先光年。所以我的问题是为什么? python C代码使用什么算法?

【问题讨论】:

  • 我希望它仍然是 O(n),但它会更快,因为它是在 C 中实现的,而不必通过 Python 解释器。尝试将每个字符串延长 10 倍,看看这两个时间是否都增加了 ~10 倍。
  • 举个例子,你在你的实现中构造了一堆字符串。您的s = s[...:] + c + s[:...] 将数据分配并复制到多达四个新的字符串对象中。 Python 对象比相应的 C 数据结构占用更多空间。而且,如果您将复制字符串内容所需的微循环计算为非原子的,那么从技术上讲,它不再是 O(N)(尽管您很难看到差异,因为这些循环比那里发生的其他事情)。
  • 这里是C code header 的链接,用于.replace 方法。
  • @S3DEV,不幸的是,我对 C 的了解不够,无法理解该标头中发生了什么。基于之前的 cmets,我假设 str.replace 函数具有相同的 O(N) 复杂度,但它的编写效率更高。
  • ... 因为在您的代码中,每个+、每个:、每个[]=== 都是对专门的、非常快速的函数的调用用 C 语言编写。但是文书工作(对象创建、对象销毁、内存管理......)加起来,突然之间,一堆快速函数最终运行得很慢。一个建筑大师可以做捷径;但如果你有一个完整的组织,捷径最终会害死人(或者让你的代码窒息,可能是这样)。

标签: python python-3.x string algorithm replace


【解决方案1】:

虽然算法可能是 O(N),但您的实现似乎不是线性的,至少对于模式的多次重复而言不是,因为

 s = s[:i - 2 * len(p) + shift] + c + s[i - len(p) + shift:]

本身就是 O(N)。因此,如果您的模式在一个字符串中出现 N 次,那么您的实现实际上是 O(N^2)。

请参阅以下时间以了解您的算法的缩放时间,这可以确认二次形状

LENGTH  TIME
------------
100000    1s
200000    8s
300000   31s
400000   76s
500000  134s

【讨论】:

  • 有什么方法可以更快地进行替换吗?
  • 你不能在 python 的次线性时间内构造一个长度为 N 的字符串,因为字符串是不可变的。您需要对可变数据类型进行操作,例如列表。然后您可以在不重新分配内存的情况下进行编辑(因此复杂性与更改的字符成正比)。对于恒定时间连接,您可能需要一个链表类型(不是在 python 中内置的)。总体而言,python 并不是学习“核心”算法的最佳选择,因为它缺乏非常有效的数据类型,C++ 是 def。效率和低级算法的更好场所。
  • Nitpick:“链表类型(不是在 python 中内置的)”——这不是真的:collections.deque 实现了它。虽然它没有那么有用,因为您没有能力直接引用列表的中间,以便有效地拼接。
  • 你能在 O(1) @Amadan 中连接两个双端队列吗?这是我们在这里需要的链表的关键属性我认为 deque 只是在内部实现为链表(实际上是数组的链表或类似的东西),但没有充分发挥其功能?
  • 同意@Amadan。为了替换链表中的节点(在 O(1) 中完成),必须先找到它,这需要 O(N),还是我错过了什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-14
  • 2018-03-22
  • 2013-09-09
  • 1970-01-01
  • 2014-02-05
  • 2016-05-03
  • 2014-08-12
相关资源
最近更新 更多