Ketika mencoba menjawab pertanyaan seperti itu, Anda benar-benar perlu memberikan batasan kode yang Anda usulkan sebagai solusi. Jika itu hanya tentang kinerja saya tidak akan terlalu keberatan, tetapi sebagian besar kode yang diusulkan sebagai solusi (termasuk jawaban yang diterima) gagal untuk meratakan daftar yang memiliki kedalaman lebih dari 1000.
Ketika saya mengatakan sebagian besar kode saya maksud adalah semua kode yang menggunakan segala bentuk rekursi (atau memanggil fungsi pustaka standar yang rekursif). Semua kode ini gagal karena untuk setiap panggilan rekursif yang dibuat, tumpukan (panggilan) bertambah satu unit, dan tumpukan panggilan python (default) memiliki ukuran 1000.
Jika Anda tidak terlalu terbiasa dengan tumpukan panggilan, maka mungkin yang berikut ini akan membantu (jika tidak, Anda bisa menggulir ke Implementasi ).
Panggil ukuran tumpukan dan pemrograman rekursif (analogi penjara)
Menemukan harta dan keluar
Bayangkan Anda memasuki ruang bawah tanah besar dengan kamar-kamar bernomor , mencari harta karun. Anda tidak tahu tempat itu tetapi Anda memiliki beberapa indikasi tentang bagaimana menemukan harta karun itu. Setiap indikasi adalah teka-teki (kesulitan bervariasi, tetapi Anda tidak dapat memprediksi seberapa sulit mereka akan). Anda memutuskan untuk berpikir sedikit tentang strategi menghemat waktu, Anda membuat dua pengamatan:
- Sulit (lama) untuk menemukan harta karun itu karena Anda harus menyelesaikan (berpotensi sulit) teka-teki untuk sampai ke sana.
- Setelah harta ditemukan, kembali ke pintu masuk mungkin mudah, Anda hanya perlu menggunakan jalur yang sama di arah lain (meskipun ini membutuhkan sedikit memori untuk mengingat jalan Anda).
Saat memasuki ruang bawah tanah, Anda melihat notebook kecil di sini. Anda memutuskan untuk menggunakannya untuk menuliskan setiap kamar yang Anda keluar setelah menyelesaikan teka-teki (saat memasuki ruangan baru), dengan cara ini Anda akan dapat kembali ke pintu masuk. Itu ide jenius, Anda bahkan tidak akan menghabiskan satu sen menerapkan strategi Anda.
Anda memasuki ruang bawah tanah, menyelesaikan dengan sukses besar teka-teki 1001 pertama, tetapi inilah sesuatu yang belum Anda rencanakan, Anda tidak memiliki ruang tersisa di notebook yang Anda pinjam. Anda memutuskan untuk meninggalkan pencarian karena Anda lebih suka tidak memiliki harta daripada hilang selamanya di dalam penjara bawah tanah (yang memang terlihat pintar).
Menjalankan program rekursif
Pada dasarnya, itu sama persis dengan menemukan harta karun itu. Penjara bawah tanah adalah memori komputer , tujuan Anda sekarang bukan untuk menemukan harta tetapi untuk menghitung beberapa fungsi (temukan f (x) untuk x yang diberikan ). Indikasinya sederhana adalah sub-rutin yang akan membantu Anda memecahkan f (x) . Strategi Anda sama dengan strategi tumpukan panggilan , notebook adalah tumpukan, kamar-kamar adalah alamat pengirim fungsi:
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))
Masalah yang Anda temui di ruang bawah tanah akan sama di sini, tumpukan panggilan memiliki ukuran yang terbatas (di sini 1000) dan oleh karena itu, jika Anda memasukkan terlalu banyak fungsi tanpa kembali maka Anda akan mengisi tumpukan panggilan dan memiliki kesalahan yang terlihat seperti "Sayang petualang, aku sangat menyesal tapi notebook Anda penuh" : RecursionError: maximum recursion depth exceeded
. Perhatikan bahwa Anda tidak perlu rekursi untuk mengisi tumpukan panggilan, tetapi sangat tidak mungkin bahwa program non-rekursif memanggil 1000 fungsi tanpa pernah kembali. Penting juga untuk memahami bahwa begitu Anda kembali dari suatu fungsi, tumpukan panggilan dibebaskan dari alamat yang digunakan (karenanya nama "tumpukan", alamat pengirim didorong masuk sebelum memasukkan suatu fungsi dan ditarik keluar ketika kembali). Dalam kasus khusus rekursi sederhana (fungsif
panggilan itu sendiri sekali - lagi dan lagi -) Anda akan masuk f
berulang sampai perhitungan selesai (sampai harta ditemukan) dan kembali dari f
sampai Anda kembali ke tempat di mana Anda memanggil f
tempat pertama. Tumpukan panggilan tidak akan pernah dibebaskan dari apa pun sampai akhir di mana ia akan dibebaskan dari semua alamat pengirim satu demi satu.
Bagaimana cara menghindari masalah ini?
Itu sebenarnya cukup sederhana: "jangan menggunakan rekursi jika Anda tidak tahu seberapa dalam itu bisa terjadi". Itu tidak selalu benar seperti dalam beberapa kasus, rekursi Tail Call dapat Dioptimalkan (TCO) . Tetapi dalam python, ini tidak terjadi, dan bahkan fungsi rekursif "ditulis dengan baik" tidak akan mengoptimalkan penggunaan stack. Ada pos menarik dari Guido tentang pertanyaan ini: Penghapusan Rekursi Ekor .
Ada teknik yang dapat Anda gunakan untuk membuat fungsi berulang berulang, teknik ini bisa kita sebut membawa notebook Anda sendiri . Misalnya, dalam kasus khusus kami, kami hanya menjelajahi daftar, memasuki ruangan sama dengan memasukkan sublist, pertanyaan yang harus Anda tanyakan pada diri sendiri adalah bagaimana saya bisa kembali dari daftar ke daftar induknya? Jawabannya tidak rumit, ulangi yang berikut sampaistack
kosong:
- dorong daftar saat ini
address
danindex
di stack
saat memasuki sublist baru (catatan bahwa alamat daftar + indeks juga alamat, oleh karena itu kita hanya menggunakan teknik yang sama persis digunakan oleh panggilan stack);
- setiap kali item ditemukan,
yield
itu (atau menambahkannya dalam daftar);
- setelah daftar dieksplorasi sepenuhnya, kembali ke daftar induk menggunakan
stack
pengembalian address
(dan index
) .
Perhatikan juga bahwa ini setara dengan DFS di pohon di mana beberapa node adalah daftar A = [1, 2]
dan beberapa item sederhana: 0, 1, 2, 3, 4
(untuk L = [0, [1,2], 3, 4]
). Pohon itu terlihat seperti ini:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Pre-order traversal DFS adalah: L, 0, A, 1, 2, 3, 4. Ingat, untuk menerapkan iteratif DFS Anda juga "perlu" tumpukan. Implementasi yang saya usulkan sebelum menghasilkan negara-negara berikut (untuk stack
dan 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]
Dalam contoh ini, ukuran maksimum tumpukan adalah 2, karena daftar input (dan karenanya pohon) memiliki kedalaman 2.
Penerapan
Untuk implementasinya, dalam python Anda dapat menyederhanakan sedikit dengan menggunakan iterator dan bukan daftar sederhana. Referensi ke (sub) iterator akan digunakan untuk menyimpan sublists mengembalikan alamat (bukan memiliki kedua daftar alamat dan indeks). Ini bukan perbedaan besar tapi saya merasa ini lebih mudah dibaca (dan juga sedikit lebih cepat):
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)
Juga, perhatikan bahwa di is_list_like
I have isinstance(item, list)
, yang bisa diubah untuk menangani lebih banyak tipe input, di sini saya hanya ingin memiliki versi paling sederhana di mana (iterable) hanya daftar. Tetapi Anda juga bisa melakukannya:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Ini menganggap string sebagai "item sederhana" dan karenanya flatten_iter([["test", "a"], "b])
akan kembali ["test", "a", "b"]
dan tidak ["t", "e", "s", "t", "a", "b"]
. Komentar bahwa dalam kasus itu,iter(item)
disebut dua kali pada setiap item, mari kita berpura-pura sebagai latihan bagi pembaca untuk membuat ini lebih bersih.
Menguji dan memberi komentar tentang implementasi lain
Pada akhirnya, ingat bahwa Anda tidak dapat mencetak daftar jauh bersarang L
menggunakan print(L)
karena internal akan menggunakan panggilan rekursif untuk __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Untuk alasan yang sama, solusi untuk flatten
melibatkan str
akan gagal dengan pesan kesalahan yang sama.
Jika Anda perlu menguji solusi Anda, Anda dapat menggunakan fungsi ini untuk menghasilkan daftar bersarang sederhana:
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
Yang memberi: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.