List Rotate Algorithms
文章目录
最近在《编程珠玑》上看到一道很有趣的题目:
假设我们在开发一个编辑器,要实现一个功能,将下面代码的2个函数调换位置,该怎么做呢?如果空间复杂度要求$O(1)$,又应该怎么做呢?
texts = [
'def add(a, b):',
' return a + b',
' ',
'def sub(a, b):',
' retturn a - b',
' '
]
其实这就是数组左旋转的问题,将数组的前3个元素移到数组的结尾。我最直接的想法,就是用另外一个数组保存前3个元素,然后将后3个元素往前面移动, 最后把前面保存的3个元素放到结尾,代码也比较简洁:
def left_rotate_normal(texts, rotate_len):
copy = texts[:rotate_len]
texts[:len(texts) - rotate_len] = texts[rotate_len:]
texts[len(texts) - rotate_len:] = copy
test_texts = texts[:]
print(test_texts)
left_rotate_normal(test_texts, 3)
print(test_texts)
['def add(a, b):', ' return a + b', ' ', 'def sub(a, b):', ' retturn a - b', ' ']
['def sub(a, b):', ' retturn a - b', ' ', 'def add(a, b):', ' return a + b', ' ']
测试结果,是没有问题的,这个算法的最大问题是,需要额外开辟空间,如果数组很大或要移动的部分很大,就会额外占用内存。
书里面给出了两种不需要额外开辟空间的方法:
第一种我叫它跳跃法吧,其实是将整块元素的移动分成一个一个元素的移动,每次移动一个元素,就把后面的补充上来, 以第一个元素为例, 先用一个临时变量temp保存第一个元素$texts[0]$, 然后将$texts[i]$移动到$texts[0]$, $texts[2 * i]$移到到$texts[i]$,直到结束,最后将 temp赋值回末尾空出来的位置。代码实现如下:
def left_rotate_jump(texts, rotate_len):
i = 0
while i < rotate_len:
temp = texts[i]
j = rotate_len
k = i
while j < len(texts):
texts[k] = texts[j]
k = j
j *= 2
texts[j // 2] = temp
i += 1
test_texts = texts[:]
print(test_texts)
left_rotate_jump(test_texts, 3)
print(test_texts)
['def add(a, b):', ' return a + b', ' ', 'def sub(a, b):', ' retturn a - b', ' ']
['def sub(a, b):', 'def add(a, b):', ' return a + b', ' ', ' retturn a - b', ' ']
还有一种方法,也是我觉得很神奇的算法,就是通过几次反转,就实现了数组的左旋转!原理是, 我们把数组分成2部分a和b,然后有这个恒等式: $$ ba = (a^rb^r)^r $$ 其中r是反转的意思。根据这个恒等式,就可以这么实现:
def left_rotate_reverse(texts, rotate_len):
reverse(texts, 0, rotate_len - 1)
reverse(texts, rotate_len, len(texts) - 1)
reverse(texts, 0, len(texts) - 1)
def reverse(texts, start, end):
while start < end:
temp = texts[start]
texts[start] = texts[end]
texts[end] = temp
start += 1
end -= 1
test_texts = texts[:]
print(test_texts)
left_rotate_reverse(test_texts, 3)
print(test_texts)
['def add(a, b):', ' return a + b', ' ', 'def sub(a, b):', ' retturn a - b', ' ']
['def sub(a, b):', ' retturn a - b', ' ', 'def add(a, b):', ' return a + b', ' ']
非常神奇,而且很简洁!三种算法的时间复杂度都是$O(n)$,后面2种方法空间复杂度为$O(1)$,不过最后一种算法最简洁,也容易理解。
现在来比较一下三种算法的性能:
import timeit
import random
nums = [random.randint(0, 10000) for i in range(10000)]
def test_left_rotate_reverse():
test_nums = nums[:]
left_rotate_reverse(nums, random.randint(0, len(test_nums)))
def test_left_rotate_jump():
test_nums = nums[:]
left_rotate_jump(nums, random.randint(0, len(test_nums)))
def test_left_rotate_normal():
test_nums = nums[:]
left_rotate_normal(nums, random.randint(0, len(test_nums)))
print(timeit.timeit('test_left_rotate_reverse()', number=1000, setup="from __main__ import test_left_rotate_reverse"))
print(timeit.timeit('test_left_rotate_jump()', number=1000, setup="from __main__ import test_left_rotate_jump"))
print(timeit.timeit('test_left_rotate_normal()', number=1000, setup="from __main__ import test_left_rotate_normal"))
2.267212925973581
2.8630741160013713
0.15271255999687128
多利用$O(N)$空间的正常方法居然是最快的!这也许是python的slice的功劳,而求逆的方法比跳转法要快一些。