Is this time complexity actually O(n^2)?
我正在研究CTCI的一个问题。
第1章的第三个问题是,你用一个字符串,比如
'Mr John Smith '。
并要求您用%20替换中间空间:
埃多克斯1〔2〕
作者在python中提供了这个解决方案,称之为o(n):
1 2 3 4 5 6 7 8 9 10 11 12 13
| def urlify(string, length):
'''function replaces single spaces with %20 and removes trailing spaces'''
counter = 0
output = ''
for char in string:
counter += 1
if counter > length:
return output
elif char == ' ':
output = output + '%20'
elif char != ' ':
output = output + char
return output |
我的问题:
我理解这是从左到右扫描实际字符串的O(N)。但python中的字符串不是不变的吗?如果我有一个字符串,并且我用+操作符向它添加了另一个字符串,那么它是否分配了必要的空间,复制原始字符串,然后复制附加字符串?
如果我有一组长度为1的n字符串,那么这需要:
埃多克斯1〔5〕
或者O(n^2)时间,是吗?或者我在python如何处理附加的问题上弄错了?
或者,如果你愿意教我如何钓鱼:我该如何为自己找到这个?我在搜索官方信息源的尝试中失败了。我找到了https://wiki.python.org/moin/timecomplexity,但这对字符串没有任何影响。
- 有人应该告诉作者关于urllib.urlencode的事。
- @wim这是一个关于数组和字符串的实践问题
- 这本书的目的是教授面试问题,面试问题通常要求你重新发明轮子来观察被面试者的思维过程。
- @RNAR我想这就是我要问的:当连接到一根绳子时,引擎盖下面到底发生了什么?一份拷贝怎么能持续一段时间?
- 因为它是python,我认为做一个rtrim和replace会更受欢迎,而且在O(n)的棒球场上。通过字符串复制似乎是最不有效的方法。
- @RNAR你能解释一个拷贝是如何持续时间的吗?
- @cricket_007如果这是IRL,我没有URLENCODE,不管什么原因,我会应用修剪然后字符串替换——但这是一个面试问题,他们可能会说"不使用字符串替换或任何外部库(如URLENCODE)就做它"。
- @用户5622964:这不是因为你做了a+b,它意味着python会立即复制并做附加:你可以做懒惰的编程:简单地记住你必须对操作数做这个,当你最终需要值时,看看它是如何组织的,并选择最有效的方法来评估a+b+c+d+...+z。
- @如果不执行实际的复制,而不修改请求的操作,则不可变字符串的复制部分jameswierzba可能需要恒定的时间。只需保存2个指针(头部、长度)。
- 您可以使用ctypes标准库的可变字符串缓冲区。stackoverflow.com/a/26172377/2099608
- 不要手动增加counter,应该使用for counter, char in enumerate(string)(enumerate的文档)。
在python的标准实现cpython中,有一个实现细节使得这个实现通常是O(N),在代码中实现,字节码评估循环使用两个字符串操作数来调用+或+=。如果python检测到左边的参数没有其他引用,它会调用realloc来尝试通过调整字符串的大小来避免复制。这不是您应该依赖的东西,因为它是一个实现细节,而且如果realloc最终需要频繁地移动字符串,那么性能无论如何都会降低到O(n^2)。
如果没有奇怪的实现细节,由于所涉及的二次复制量,该算法是O(n^2)。这样的代码只有在具有可变字符串的语言(如C++)中才有意义,甚至在C++中,您也希望使用EDCOX1(1)。
- 我在看你链接的代码…代码的很大一部分似乎正在清除/删除附加字符串的指针/引用,对吗?最后,它执行_PyString_Resize(&v, new_len)为连接字符串分配内存,然后执行memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);进行复制。如果就地调整失败,则执行PyString_Concat(&v, w);(我假设这意味着原始字符串地址末尾的连续内存不可用时)。如何显示加速?
- 在我之前的评论中,我没有足够的空间,但我的问题是我是否正确理解了代码,以及如何解释这些片段的内存使用/运行时。
- @用户5622964:哎呀,记错了奇怪的实现细节。没有有效的调整策略;它只调用realloc,并希望得到最好的结果。
- memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);是如何工作的?根据cplusplus.com/reference/cstring/memcpy,它有void * memcpy ( void * destination, const void * source, size_t num );的定义和描述:"Copies the values of num bytes from the location pointed to by source directly to the memory block pointed to by destination."在这种情况下,num是附加字符串的大小,source是第二个字符串的地址,我假设?但是为什么目的地(第一个字符串)+len(第一个字符串)?双重记忆?
- 将一个对象传递到memcpy中是否给出了该对象的地址或类似的地址?编辑:实际上,这可能是有意义的,因为如果它返回地址,那么添加长度就意味着它从v结尾开始复制块。字符串是连续存储在内存中的吗?PyString_Concat(&v, w);是字符串v和w的完整副本吗?
- @用户5622964:这是指针算术。如果你想了解cpython源代码的奇怪实现细节,你需要知道c。超级压缩版本是PyString_AS_STRING(v)是第一个字符串数据的地址,添加v_len会在字符串数据结束后立即得到地址。
- 只要每个分配是前一个长度的两倍(或实际上是任何倍数),重复追加的realloc就是o(n)。
- @不过,python并没有这样做,因为在更重要的一般情况下,它需要为所有字符串留出额外的空间,在这种情况下,不会将字符串与+或+=串联在一个循环中。这只是一个机会主义的优化,而不是一个明确的尝试来保证这个字符串连接模式的O(N)性能。
作者依赖于一个恰巧在这里的优化,但并不明确可靠。strA = strB + strC通常是O(n),使函数O(n^2)起作用。但是,很容易确保整个过程是O(n),使用一个数组:
1 2 3 4 5 6 7
| output = []
# ... loop thing
output.append('%20')
# ...
output.append(char)
# ...
return ''.join(output) |
简言之,append操作是在O(1)中进行摊销的(尽管您可以通过将数组预先分配到正确的大小来增强O(1),使循环O(n)。
然后,join也是O(n),但这没关系,因为它在循环之外。
我在python speed>上找到了这段文本,使用了最好的算法和最快的工具:
String concatenation is best done with ''.join(seq) which is an O(n) process. In contrast, using the '+' or '+=' operators can result in an O(n^2) process because new strings may be built for each intermediate step. The CPython 2.4 interpreter mitigates this issue somewhat; however, ''.join(seq) remains the best practice
号
对于未来的访问者:因为这是一个CTCI问题,所以这里不需要任何关于学习URLLIB包的参考,特别是根据OP和书,这个问题是关于数组和字符串的。
以下是一个更完整的解决方案,灵感来自@njzk2的伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| text = 'Mr John Smith'#13
special_str = '%20'
def URLify(text, text_len, special_str):
url = []
for i in range(text_len): # O(n)
if text[i] == ' ': # n-s
url.append(special_str) # append() is O(1)
else:
url.append(text[i]) # O(1)
print(url)
return ''.join(url) #O(n)
print(URLify(text, 13, '%20')) |
号