เมื่อพยายามตอบคำถามเช่นนี้คุณจำเป็นต้องให้ข้อ จำกัด ของรหัสที่คุณเสนอเป็นวิธีแก้ปัญหา ถ้ามันเกี่ยวกับการแสดงฉันจะไม่สนใจมากเกินไป แต่รหัสส่วนใหญ่ที่เสนอเป็นวิธีแก้ปัญหา (รวมถึงคำตอบที่ยอมรับได้) ล้มเหลวในการทำให้รายการที่มีความลึกมากกว่า 1,000 แบนเรียบ
เมื่อฉันพูดถึงรหัสส่วนใหญ่ฉันหมายถึงรหัสทั้งหมดที่ใช้การเรียกซ้ำในรูปแบบใด ๆ (หรือเรียกใช้ฟังก์ชันไลบรารีมาตรฐานที่เรียกซ้ำ) รหัสทั้งหมดเหล่านี้ล้มเหลวเนื่องจากการโทรซ้ำแบบเรียกซ้ำทุกครั้งการเรียก (การโทร) จะเพิ่มขึ้นหนึ่งหน่วยและการเรียกแบบหลาม (เริ่มต้น) ของกองมีขนาด 1,000
หากคุณไม่คุ้นเคยกับ call stack มากบางทีสิ่งต่อไปนี้อาจช่วยได้ (ไม่เช่นนั้นคุณก็สามารถเลื่อนไปที่การใช้งาน )
เรียกขนาดสแต็กและการโปรแกรมแบบเรียกซ้ำ
ค้นหาสมบัติและทางออก
ลองนึกภาพคุณเข้าไปในคุกใต้ดินขนาดใหญ่ที่มีห้องหมายเลขค้นหาสมบัติ คุณไม่รู้สถานที่ แต่มีข้อบ่งชี้ว่าจะหาสมบัติได้อย่างไร ข้อบ่งชี้แต่ละอย่างเป็นปริศนา (ความยากลำบากแตกต่างกันไป แต่คุณไม่สามารถคาดเดาได้ว่าจะยากเพียงใด) คุณตัดสินใจคิดเกี่ยวกับกลยุทธ์เล็กน้อยเพื่อประหยัดเวลาคุณทำการสังเกตสองอย่าง:
- มันยาก (ยาว) ในการค้นหาขุมทรัพย์เพราะคุณจะต้องไขปริศนา (ยาก) เพื่อไปที่นั่น
- เมื่อพบสมบัติการกลับไปที่ทางเข้าอาจจะง่ายคุณเพียงแค่ต้องใช้เส้นทางเดียวกันในทิศทางอื่น (แม้ว่าจะต้องใช้หน่วยความจำสักเล็กน้อยเพื่อจำเส้นทางของคุณ)
เมื่อเข้าไปในคุกใต้ดินคุณจะสังเกตเห็นสมุดบันทึกขนาดเล็กที่นี่ คุณตัดสินใจที่จะใช้มันเพื่อจดบันทึกทุกห้องที่คุณออกหลังจากแก้ปริศนา (เมื่อเข้าห้องใหม่) ด้วยวิธีนี้คุณจะสามารถกลับไปที่ทางเข้าได้ นั่นเป็นความคิดที่อัจฉริยะคุณจะไม่ต้องเสียค่าใช้จ่ายแม้แต่นิดเดียวในการนำกลยุทธ์ของคุณไปใช้
คุณเข้าสู่ดันเจี้ยนแก้ปัญหาด้วยความสำเร็จที่ยิ่งใหญ่ใน 1,1001 ปริศนาแรก แต่ที่นี่มีบางสิ่งที่คุณไม่ได้วางแผนมาคุณไม่มีพื้นที่เหลือในสมุดบันทึกที่คุณยืม คุณตัดสินใจที่จะละทิ้งการแสวงหาของคุณเนื่องจากคุณไม่ต้องการที่จะมีขุมทรัพย์มากกว่าที่จะหลงทางตลอดไปในดันเจี้ยน
การเรียกใช้งานโปรแกรมแบบเรียกซ้ำ
โดยทั่วไปมันเป็นสิ่งเดียวกับการค้นหาสมบัติ ดันเจี้ยนเป็นหน่วยความจำของคอมพิวเตอร์เป้าหมายของคุณคือตอนนี้ไม่ได้หาสมบัติ แต่เพื่อคำนวณฟังก์ชั่นบางอย่าง (ค้นหาf (x)สำหรับx ที่กำหนด) ตัวชี้วัดก็ย่อยการปฏิบัติที่จะช่วยให้คุณแก้f (x) กลยุทธ์ของคุณเหมือนกับกลยุทธ์call stack , notebook คือ stack, ห้องเป็นที่อยู่ส่งคืนของฟังก์ชั่น:
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))
ปัญหาที่คุณพบในดันเจี้ยนจะเหมือนกันที่นี่สแต็คการโทรมีขนาด จำกัด (ที่นี่ 1,000) ดังนั้นหากคุณป้อนฟังก์ชั่นมากเกินไปโดยไม่ย้อนกลับมาคุณจะเติมสแต็กการโทรและมีข้อผิดพลาด เช่น"นักผจญภัยที่รักฉันเสียใจมาก แต่สมุดบันทึกของคุณเต็ม" :RecursionError: maximum recursion depth exceeded
:โปรดทราบว่าคุณไม่จำเป็นต้องเรียกซ้ำเพื่อเติมสแต็คการโทร แต่ก็ไม่น่าเป็นไปได้มากที่โปรแกรม 1000 ที่ไม่เรียกซ้ำจะเรียกใช้ฟังก์ชันโดยไม่ส่งคืน สิ่งสำคัญคือต้องเข้าใจว่าเมื่อคุณกลับมาจากฟังก์ชั่นการเรียกสแต็กจะถูกปลดปล่อยจากที่อยู่ที่ใช้ (เช่นชื่อ "สแต็ค" ที่อยู่ผู้ส่งจะถูกผลักเข้าไปก่อนที่จะเข้าสู่ฟังก์ชั่น ในกรณีพิเศษของการเรียกซ้ำแบบง่าย (ฟังก์ชันf
ที่เรียกตัวเองว่าครั้งแล้วครั้งเล่า -) คุณจะเข้าf
มาซ้ำแล้วซ้ำอีกจนกว่าการคำนวณจะเสร็จสิ้น (จนกว่าจะพบสมบัติ) และกลับจากf
จนกว่าคุณจะกลับไปยังสถานที่ที่คุณโทรหาf
ในตอนแรก สแต็คการโทรจะไม่ถูกทำให้เป็นอิสระจากสิ่งใดจนกว่าจะสิ้นสุดซึ่งจะถูกปลดปล่อยจากที่อยู่ผู้ส่งทั้งหมดหลังจากที่อยู่อีกสายหนึ่ง
จะหลีกเลี่ยงปัญหานี้ได้อย่างไร?
ที่จริงแล้วค่อนข้างเรียบง่าย: "อย่าใช้การเรียกซ้ำถ้าคุณไม่รู้ว่ามันจะไปได้ลึกเพียงใด" ที่ไม่จริงเสมอเช่นในบางกรณีสายหาง recursion สามารถเพิ่มประสิทธิภาพ (TCO) แต่ในไพ ธ อนนี่ไม่ใช่กรณีและแม้แต่ฟังก์ชั่นเรียกซ้ำ "ที่เขียนดี" จะไม่ปรับการใช้สแต็กให้เหมาะสม มีโพสต์ที่น่าสนใจจากกุยโดเกี่ยวกับคำถามนี้: recursion หางกำจัด
มีเทคนิคที่คุณสามารถใช้เพื่อทำการใด ๆ ซ้ำฟังก์ชันเวียนเทคนิคนี้เราอาจจะเรียกเป็นนำโน๊ตบุ๊คของคุณเอง ตัวอย่างเช่นในกรณีของเราโดยเฉพาะเรากำลังสำรวจรายการการเข้าห้องเทียบเท่ากับการป้อนรายการย่อยคำถามที่คุณควรถามตัวเองคือฉันจะกลับจากรายการไปยังรายการหลักได้อย่างไร คำตอบนั้นไม่ซับซ้อนให้ทำซ้ำจนกว่าจะถึงstack
จะว่างเปล่า:
- กดรายการปัจจุบัน
address
และindex
ในstack
เมื่อป้อนรายการย่อยใหม่ (โปรดทราบว่ารายการที่อยู่ + ดัชนียังเป็นที่อยู่ดังนั้นเราจึงใช้เทคนิคแบบเดียวกับที่ใช้โดยสแต็กการโทร)
- ทุกครั้งที่พบรายการ
yield
มัน (หรือเพิ่มในรายการ);
- เมื่อรายการถูกสำรวจอย่างสมบูรณ์แล้วให้กลับไปที่รายการหลักโดยใช้การ
stack
ส่งคืนaddress
(และindex
) )
นอกจากนี้โปรดทราบว่าสิ่งนี้เทียบเท่ากับ DFS ในทรีที่บางโหนดเป็นรายการย่อยA = [1, 2]
และบางรายการเป็นรายการง่าย: 0, 1, 2, 3, 4
(สำหรับL = [0, [1,2], 3, 4]
) ต้นไม้มีลักษณะเช่นนี้:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
การสั่งซื้อล่วงหน้าแบบสำรวจเส้นทาง DFS คือ: L, 0, A, 1, 2, 3, 4 โปรดจำไว้ว่าเพื่อใช้ DFS ซ้ำคุณยังต้อง "สแต็ค" อีกครั้ง การดำเนินการที่ฉันเสนอก่อนผลลัพธ์มีสถานะต่อไปนี้ (สำหรับstack
และและ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]
ในตัวอย่างนี้ขนาดสูงสุดของสแต็กคือ 2 เนื่องจากรายการอินพุต (และต้นไม้) มีความลึก 2
การดำเนินงาน
สำหรับการติดตั้งในไพ ธ อนคุณสามารถลดความซับซ้อนลงเล็กน้อยโดยใช้ตัววนซ้ำแทนรายการอย่างง่าย การอ้างอิงไปยังตัววนซ้ำ (sub) จะถูกใช้เพื่อเก็บรายการที่อยู่ส่งคืนของรายการย่อย (แทนที่จะมีทั้งรายการที่อยู่และดัชนี) นี่ไม่ใช่ความแตกต่างใหญ่ แต่ฉันรู้สึกว่ามันอ่านง่ายกว่า (และเร็วกว่านิดหน่อย):
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)
นอกจากนี้สังเกตว่าในis_list_like
ฉันมีisinstance(item, list)
ซึ่งสามารถเปลี่ยนเป็นประเภทอินพุตได้มากขึ้นที่นี่ฉันแค่อยากจะมีเวอร์ชั่นที่ง่ายที่สุดโดยที่ (iterable) เป็นเพียงรายการ แต่คุณสามารถทำเช่นนั้นได้:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
นี้จะพิจารณาสตริงเป็นรายการ "ง่าย" และดังนั้นจึงflatten_iter([["test", "a"], "b])
จะกลับมาและไม่ได้["test", "a", "b"]
["t", "e", "s", "t", "a", "b"]
โปรดทราบว่าในกรณีiter(item)
นั้นเรียกว่าสองครั้งในแต่ละรายการลองทำเป็นว่าเป็นการออกกำลังกายสำหรับผู้อ่านเพื่อทำความสะอาดนี้
ทดสอบและพูดถึงการใช้งานอื่น ๆ
ในท้ายที่สุดโปรดจำไว้ว่าคุณไม่สามารถพิมพ์รายการซ้อนแบบไม่ จำกัด ได้L
โดยใช้print(L)
เพราะภายในจะใช้การเรียกซ้ำเพื่อ__repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
) ด้วยเหตุผลเดียวกันการแก้ปัญหาที่flatten
เกี่ยวข้องstr
จะล้มเหลวพร้อมกับข้อความแสดงข้อผิดพลาดเดียวกัน
หากคุณต้องการทดสอบโซลูชันของคุณคุณสามารถใช้ฟังก์ชันนี้เพื่อสร้างรายการซ้อนแบบง่าย:
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
ซึ่งจะช่วยให้: >>>build_deep_list(5)
[4, [3, [2, [1, [0]]]]]