Khi cố gắng trả lời một câu hỏi như vậy, bạn thực sự cần đưa ra những hạn chế của mã mà bạn đề xuất như một giải pháp. Nếu đó chỉ là về màn trình diễn thì tôi sẽ không bận tâm lắm, nhưng hầu hết các mã được đề xuất là giải pháp (bao gồm câu trả lời được chấp nhận) không thể làm phẳng bất kỳ danh sách nào có độ sâu lớn hơn 1000.
Khi tôi nói hầu hết các mã tôi có nghĩa là tất cả các mã sử dụng bất kỳ hình thức đệ quy nào (hoặc gọi một hàm thư viện chuẩn là đệ quy). Tất cả các mã này đều thất bại vì với mỗi cuộc gọi đệ quy được thực hiện, ngăn xếp (cuộc gọi) tăng thêm một đơn vị và ngăn xếp cuộc gọi python (mặc định) có kích thước 1000.
Nếu bạn không quá quen thuộc với ngăn xếp cuộc gọi, thì có thể những điều sau đây sẽ có ích (nếu không bạn chỉ có thể cuộn đến Thực hiện ).
Kích thước ngăn xếp cuộc gọi và lập trình đệ quy (tương tự dungeon)
Tìm kho báu và thoát
Hãy tưởng tượng bạn bước vào một hầm ngục khổng lồ với các phòng được đánh số , tìm kiếm một kho báu. Bạn không biết địa điểm nhưng bạn có một số chỉ dẫn về cách tìm kho báu. Mỗi dấu hiệu là một câu đố (độ khó khác nhau, nhưng bạn không thể dự đoán mức độ khó của chúng). Bạn quyết định suy nghĩ một chút về một chiến lược để tiết kiệm thời gian, bạn thực hiện hai quan sát:
- Thật khó (lâu) để tìm kho báu vì bạn sẽ phải giải những câu đố (có khả năng khó) để đến đó.
- Khi kho báu được tìm thấy, quay trở lại lối vào có thể dễ dàng, bạn chỉ cần sử dụng cùng một đường dẫn theo hướng khác (mặc dù điều này cần một chút bộ nhớ để nhớ lại đường dẫn của bạn).
Khi vào ngục tối, bạn nhận thấy một cuốn sổ nhỏ ở đây. Bạn quyết định sử dụng nó để ghi lại mọi phòng bạn thoát sau khi giải câu đố (khi vào phòng mới), bằng cách này bạn sẽ có thể quay lại lối vào. Đó là một ý tưởng thiên tài, bạn thậm chí sẽ không chi một xu thực hiện chiến lược của mình.
Bạn vào ngục tối, giải quyết thành công 1001 câu đố đầu tiên, nhưng ở đây có một điều bạn chưa lên kế hoạch, bạn không còn chỗ trống trong cuốn sổ bạn đã mượn. Bạn quyết định từ bỏ nhiệm vụ của mình vì bạn không muốn có kho báu hơn là bị mất mãi mãi bên trong ngục tối (điều đó có vẻ thông minh thực sự).
Thực hiện một chương trình đệ quy
Về cơ bản, đó chính xác là điều tương tự như tìm kho báu. Hầm ngục là bộ nhớ của máy tính , mục tiêu của bạn bây giờ không phải là tìm kho báu mà là tính toán một số chức năng (tìm f (x) cho một x cho trước ). Các chỉ dẫn đơn giản là các thường trình con sẽ giúp bạn giải quyết f (x) . Chiến lược của bạn giống như chiến lược ngăn xếp cuộc gọi , sổ ghi chép là ngăn xếp, các phòng là địa chỉ trả về của các chức năng:
x = ["over here", "am", "I"]
y = sorted(x) # You're about to enter a room named `sorted`, note down the current room address here so you can return back: 0x4004f4 (that room address looks weird)
# Seems like you went back from your quest using the return address 0x4004f4
# Let's see what you've collected
print(' '.join(y))
Vấn đề bạn gặp phải trong ngục tối sẽ giống nhau ở đây, ngăn xếp cuộc gọi có kích thước hữu hạn (ở đây 1000) và do đó, nếu bạn nhập quá nhiều chức năng mà không quay lại thì bạn sẽ điền vào ngăn xếp cuộc gọi và gặp lỗi như "Thưa nhà thám hiểm, tôi rất xin lỗi nhưng sổ ghi chép của bạn đã đầy" : RecursionError: maximum recursion depth exceeded
. Lưu ý rằng bạn không cần đệ quy để điền vào ngăn xếp cuộc gọi, nhưng rất có thể chương trình không đệ quy gọi 1000 hàm mà không bao giờ quay lại. Điều quan trọng là phải hiểu rằng một khi bạn quay trở lại từ một hàm, ngăn xếp cuộc gọi sẽ được giải phóng khỏi địa chỉ được sử dụng (do đó tên "stack", địa chỉ trả về được đẩy vào trước khi nhập hàm và rút ra khi quay lại). Trong trường hợp đặc biệt của đệ quy đơn giản (một hàmf
tự gọi nó một lần - lặp đi lặp lại -) bạn sẽ nhập đi f
lặp lại cho đến khi tính toán kết thúc (cho đến khi tìm thấy kho báu) và trở về f
cho đến khi bạn quay lại nơi bạn đã gọi f
ở nơi đầu tiên. Ngăn xếp cuộc gọi sẽ không bao giờ được giải phóng khỏi bất cứ điều gì cho đến khi kết thúc, nơi nó sẽ được giải phóng khỏi tất cả các địa chỉ trả lại lần lượt.
Làm thế nào để tránh vấn đề này?
Điều đó thực sự khá đơn giản: "không sử dụng đệ quy nếu bạn không biết nó có thể đi sâu đến đâu". Điều đó không phải lúc nào cũng đúng như trong một số trường hợp, đệ quy Đuôi cuộc gọi có thể được tối ưu hóa (TCO) . Nhưng trong python, đây không phải là trường hợp và thậm chí chức năng đệ quy "được viết tốt" sẽ không tối ưu hóa việc sử dụng ngăn xếp. Có một bài viết thú vị từ Guido về câu hỏi này: Loại bỏ đệ quy đuôi .
Có một kỹ thuật mà bạn có thể sử dụng để thực hiện bất kỳ chức năng đệ quy nào, đó là kỹ thuật mà chúng tôi có thể gọi là mang sổ ghi chép của riêng bạn . Ví dụ, trong trường hợp cụ thể của chúng tôi, chúng tôi chỉ đơn giản là đang khám phá một danh sách, vào một phòng tương đương với việc vào danh sách phụ, câu hỏi bạn nên tự hỏi là làm thế nào tôi có thể quay lại từ danh sách vào danh sách phụ huynh? Câu trả lời không phức tạp, lặp lại như sau cho đến khi stack
trống:
- đẩy danh sách hiện tại
address
và index
trong stack
khi nhập danh sách con mới (lưu ý rằng địa chỉ danh sách + chỉ mục cũng là một địa chỉ, do đó chúng tôi chỉ sử dụng chính xác kỹ thuật tương tự được sử dụng bởi ngăn xếp cuộc gọi);
- mỗi khi một mục được tìm thấy,
yield
nó (hoặc thêm chúng vào danh sách);
- khi danh sách được khám phá đầy đủ, hãy quay lại danh sách cha bằng cách sử dụng
stack
trả về address
(và index
) .
Cũng lưu ý rằng điều này tương đương với một DFS trong cây trong đó một số nút là danh sách con A = [1, 2]
và một số là các mục đơn giản: 0, 1, 2, 3, 4
(for L = [0, [1,2], 3, 4]
). Cây trông như thế này:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Thứ tự đặt trước truyền tải DFS là: L, 0, A, 1, 2, 3, 4. Hãy nhớ rằng, để thực hiện một DFS lặp, bạn cũng "cần" một ngăn xếp. Việc triển khai tôi đã đề xuất trước khi có các trạng thái sau (đối với stack
và flat_list
):
init.: stack=[(L, 0)]
**0**: stack=[(L, 0)], flat_list=[0]
**A**: stack=[(L, 1), (A, 0)], flat_list=[0]
**1**: stack=[(L, 1), (A, 0)], flat_list=[0, 1]
**2**: stack=[(L, 1), (A, 1)], flat_list=[0, 1, 2]
**3**: stack=[(L, 2)], flat_list=[0, 1, 2, 3]
**3**: stack=[(L, 3)], flat_list=[0, 1, 2, 3, 4]
return: stack=[], flat_list=[0, 1, 2, 3, 4]
Trong ví dụ này, kích thước tối đa của ngăn xếp là 2, vì danh sách đầu vào (và do đó là cây) có độ sâu 2.
Thực hiện
Để thực hiện, trong python bạn có thể đơn giản hóa một chút bằng cách sử dụng các trình vòng lặp thay vì các danh sách đơn giản. Các tham chiếu đến các trình vòng lặp (phụ) sẽ được sử dụng để lưu trữ các địa chỉ trả về danh sách phụ (thay vì có cả địa chỉ danh sách và chỉ mục). Đây không phải là một sự khác biệt lớn nhưng tôi cảm thấy điều này dễ đọc hơn (và cũng nhanh hơn một chút):
def flatten(iterable):
return list(items_from(iterable))
def items_from(iterable):
cursor_stack = [iter(iterable)]
while cursor_stack:
sub_iterable = cursor_stack[-1]
try:
item = next(sub_iterable)
except StopIteration: # post-order
cursor_stack.pop()
continue
if is_list_like(item): # pre-order
cursor_stack.append(iter(item))
elif item is not None:
yield item # in-order
def is_list_like(item):
return isinstance(item, list)
Ngoài ra, lưu ý rằng trong is_list_like
tôi có isinstance(item, list)
, có thể thay đổi để xử lý nhiều loại đầu vào hơn, ở đây tôi chỉ muốn có phiên bản đơn giản nhất trong đó (lặp lại) chỉ là một danh sách. Nhưng bạn cũng có thể làm điều đó:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Điều này coi các chuỗi là "các mục đơn giản" và do đó flatten_iter([["test", "a"], "b])
sẽ trả về ["test", "a", "b"]
và không ["t", "e", "s", "t", "a", "b"]
. Lưu ý rằng trong trường hợp đó, iter(item)
được gọi hai lần cho mỗi mục, hãy giả vờ đó là một bài tập cho người đọc để làm cho điều này sạch hơn.
Kiểm tra và nhận xét về các triển khai khác
Cuối cùng, hãy nhớ rằng bạn không thể in một danh sách lồng nhau vô hạn L
bằng cách sử dụng print(L)
vì bên trong nó sẽ sử dụng các lệnh gọi đệ quy đến __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Vì lý do tương tự, các giải pháp flatten
liên quan str
sẽ thất bại với cùng một thông báo lỗi.
Nếu bạn cần kiểm tra giải pháp của mình, bạn có thể sử dụng chức năng này để tạo danh sách lồng đơn giản:
def build_deep_list(depth):
"""Returns a list of the form $l_{depth} = [depth-1, l_{depth-1}]$
with $depth > 1$ and $l_0 = [0]$.
"""
sub_list = [0]
for d in range(1, depth):
sub_list = [d, sub_list]
return sub_list
Mà cho: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.