如何从中间开始完全遍历列表?

How to iterate completely through a list after starting in the middle?

python 的新手。从中间(或任何一点)开始后如何完全遍历列表?例如,如果我有:

[a,b,c,d,e,f,g]

我想从 d 开始,然后是 e、f、g、a、b、c。但我不知道该怎么做。我希望某些东西也能够执行 b、c、d、e、f、g、a 或从任何起点完全迭代的任何顺序。

我发现这个 discussing something similar, and an 通过以下方式解决了这个问题:

>>> a = [1, 2, 3, 4, 5, 6, 7]
>>> [a[(len(a) + (~i, i)[i%2]) // 2] for i in range(len(a))]
[4, 5, 3, 6, 2, 7, 1]

但是,老实说,我不知道如何解释这段代码是如何完成这项任务的。我确实认为我正在寻找的任何东西都与此类似。

您可以试试这个方法:

>>> a = [1, 2, 3, 4, 5, 6, 7]
>>> start = 3
>>> [a[(start + j) % len(a)] for j in range(len(a))]
[4, 5, 6, 7, 1, 2, 3]

另一种方法(这在后台是不必要的复杂):

>>> a[start:] + a[:start]
[4, 5, 6, 7, 1, 2, 3]

在此解决方案中,我使用 range(len(items)) 生成一个可迭代对象,其值从 0 到列表长度减去 1

这意味着在这个例子中,在 for 循环中 i 的值将为 0,1,2,3... 所以 i + startIndex 将为 3, 4,5,...

这几乎就是我们想要的,但我们需要确保不会遗漏列表开头的元素。当 (i + startIndex) 除以列表的长度时,% 运算符得到余数。这确保了一旦 i 大于或等于列表的长度就打印其余项目,因为余数将为 0、1、2 ...

items = ["a", "b", "c", "d", "e", "f", "g"]
startIndex = 3

for i in range(len(items)):
    print(items[(i + startIndex) % len(items)])

如前所述,您只需将两个切片相加即可:

>>> a[3:] + a[:3]
[4, 5, 6, 7, 1, 2, 3]

但是 + 运算符会创建一个新列表(即使您只打算遍历它一次),如果列表非常长,这可能是不可取的。

如果您想要切片语法的简单性 您希望能够对列表元素进行一次迭代 而无需 将所有内容复制到一个新列表中,有 itertools.chain,它允许您将多个可迭代对象链接在一起,而无需将它们连接到 in-memory 列表中:

>>> import itertools
>>> for n in itertools.chain(a[3:], a[:3]):
...     print(n)
...
4
5
6
7
1
2
3

这是一个非常高效的 迭代器(因为您使用了 iterator 标签并多次说“迭代”)。它仅使用 O(1) space 并且非常快:

from itertools import islice, chain

def rotated(lst, start):
    it = iter(lst)
    next(islice(it, start, start), None)
    return chain(it, islice(lst, start))

lst = list('abcdefgh')
start = 3
print(*rotated(lst, start))

输出(Try it online!):

d e f g h a b c

具有一百万个数字列表的基准测试,显示 tracemalloc 峰值和运行时间。使用到目前为止建议的迭代器:

list_comprehension    8,697,776 bytes  1331.9 milliseconds
add_list_slices      16,000,000 bytes    28.4 milliseconds
iadd_list_slices     13,000,048 bytes    23.1 milliseconds
rotated_deque_1       8,243,328 bytes    26.9 milliseconds
rotated_deque_2             296 bytes     8.1 milliseconds
chained_list_slices   8,000,240 bytes    18.1 milliseconds
chained_iterators           360 bytes    11.6 milliseconds  <-- mine

第一个 deque 解决方案就是问题所要求的(从列表开始)。第二个是 ShadowRanger 的建议(使用 get-go 中的 deque,而不是列表)。

代码(Try it online!):

import tracemalloc as tm
from timeit import default_timer as timer
from collections import deque
from itertools import islice, chain

def list_comprehension(a, start):
    return [a[(start + j) % len(a)] for j in range(len(a))]

def add_list_slices(a, start):
    return a[start:] + a[:start]

def iadd_list_slices(a, start):
    b = a[start:]
    b += a[:start]
    return b

def chained_list_slices(lst, start):
    return chain(lst[start:], lst[:start])

def rotated_deque_1(lst, start):
    d = deque(lst)
    d.rotate(-start)
    return d

def rotated_deque_2(lst, start):
    deck.rotate(-start)
    return deck
    # deck.rotate(start) is done in the benchmark code, after the iteration

def chained_iterators(lst, start):
    it = iter(lst)
    next(islice(it, start, start), None)
    return chain(it, islice(lst, start))

funcs = [
    list_comprehension,
    add_list_slices,
    iadd_list_slices,
    rotated_deque_1,
    rotated_deque_2,
    chained_list_slices,
    chained_iterators,
]

a = list(range(10))
start = 3
deck = deque(a)
for func in funcs:
    print(*func(a, start))

n = 10 ** 6
a = list(range(n))
start = n // 2
deck = deque(a)
for _ in range(3):
    print()
    for func in funcs:
        tm.start()
        t0 = timer()
        deque(func(a, start), 0)
        if func is rotated_deque_2:
            deck.rotate(start)
        t1 = timer()
        print(f'{func.__name__:{max(len(func.__name__) for func in funcs)}}  '
              f'{tm.get_traced_memory()[1]:10,} bytes  '
              f'{(t1 - t0) * 1e3:6.1f} milliseconds')
        tm.stop()

这不是给定所写代码和所问问题的答案,但您可以考虑在迭代之前使用 a collections.deque instead of a list from the get-go. With deque, you can do a rotate operation,正常迭代(没有复杂的 skipping/slicing 逻辑),然后(如果你want/need到)rotate它回到原来的位置。

例如,与其构建 list,不如从头构建 deque

from collections import deque

a = deque(range(1, 8))

a.rotate(-3)  # Shifts three elements from beginning to end
for x in a:   # deque is actually reordered, so just iterate normally
    print(x)
a.rotate(3)   # Shifts three elements from end back to beginning

输出:

4
5
6
7
1
2
3