让我们把它分成两个步骤。首先我们会找到循环。然后我们将根据您的需要格式化输出。
查找循环很容易,因为您有一个字典:值变成键,查找速度很快。我们可以将循环按排序顺序存储在一个集合中以避免重复:
cycles = set()
for k, v in m.items():
if m.get(v) == k:
cycles.add(tuple(sorted((k, v))))
不是我推荐它,但上面可以写成难以辨认的单行:
cycles = set(tuple(sorted(item)) for item in m.items() if m.get(item[1]) == item[0])
现在来格式化数据。您想要一个列表输出,以及格式为列表的重复条目:
output = [[k] if k == v else (k, v) for k, v in cycles]
如果你不喜欢干净的代码,你可以想象如何将整个操作变成一个单行代码:)
更新
值得考虑循环长于一两个条目的情况。您似乎希望每个周期只存储一个元素,所以让我们这样做。我们可以跟踪字典中每个元素的链。如果链的任何部分形成一个循环,我们可以找到要报告的循环的最小元素,并删除所有访问过的元素,因为它们都不再被考虑:
def find_cycles(m):
n = m.copy() # don't mutilate the original
cycles = []
while n:
visited = {}
count = 0
k, v = n.popitem()
while v is not None:
visited[k] = (count, v)
count += 1
k = v
v = n.pop(k, None)
if k in visited:
cycle_start = visited[k][0]
item = min((k, v) for k, (c, v) in visited.items() if c >= cycle_start)
cycles.append(item)
return [[k] if k == v else (k, v) for k, v in cycles]
例如:
>>> find_cycles({1:2, 2:3, 3:4, 4:5, 5:1, 6:1, 7:1, 8:8})
[(1, 2), [8]]
更好的概括可能是返回一个包含整个循环的元组,从最小的键开始。大多数情况下,if k in visited: 下的语句需要更改才能做到这一点:
visited[k] = count
...
if k in visited:
if len(visited) == 1:
cycle = list(visited.keys())
else:
cycle_start = visited[k]
cycle = sorted((c, k) for k, c in visited.items() if c >= cycle_start)
cycle = tuple(k for c, k in cycle)
k = min(range(len(cycle)), key=lambda x: cycle[x])
cycle = cycle[k:] + cycle[:k]
cycles.append(cycle)
return cycles
这个版本信息量更大:
>>> find_cycles({1:2, 2:3, 3:4, 4:5, 5:1, 6:1, 7:1, 8:8})
[(1, 2, 3, 4, 5), [8]]
如果您有兴趣,这里是我的 IDEOne 暂存空间:https://ideone.com/6kpRrW