为啥 str.replace 这么快?
Posted
技术标签:
【中文标题】为啥 str.replace 这么快?【英文标题】: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 语言编写。但是文书工作(对象创建、对象销毁、内存管理......)加起来,突然之间,一堆快速函数最终运行得很慢。一个建筑大师可以做捷径;但如果你有一个完整的组织,捷径最终会害死人(或者让你的代码窒息,可能是这样)。
【参考方案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),还是我错过了什么?以上是关于为啥 str.replace 这么快?的主要内容,如果未能解决你的问题,请参考以下文章