ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย
ผสานลิสต์เชื่อมโยงที่เรียงแล้วสองลิสต์ในเวลา O(n) แบ่งลิสต์ที่จุดกึ่งกลางด้วยตัวชี้ช้า–เร็ว และค้นหาโหนดลำดับที่ n จากส่วนท้าย
ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
รูปแบบสำคัญสามแบบของรายการเชื่อมโยง
บทเรียนนี้ครอบคลุมการดำเนินการพื้นฐานสามอย่างของรายการเชื่อมโยง ซึ่งปรากฏอยู่เสมอในฐานะส่วนประกอบของโจทย์ที่ยากขึ้น ได้แก่ การผสานรายการที่เรียงลำดับแล้วสองรายการ (ใช้ในการเรียงลำดับแบบผสานและการผสาน k รายการ) การแบ่งรายการที่จุดกึ่งกลาง (ใช้ในการเรียงลำดับแบบผสานและการตรวจจับพาลินโดรม) และ การหาโหนดลำดับที่ n จากท้ายรายการ (ใช้ในการลบโหนดลำดับที่ n จากท้ายรายการ)
ทั้งสามอย่างอาศัยเทคนิคที่คุณได้เห็นมาแล้ว ได้แก่ โหนดหัวรายการจำลอง ตัวชี้ช้าและเร็ว และการติดตามขอบเขตอย่างรอบคอบ
การผสานรายการที่เรียงลำดับแล้วสองรายการ
LeetCode 21 เรื่อง 'ผสานรายการเชื่อมโยงที่เรียงลำดับแล้วสองรายการ': เมื่อกำหนดรายการเชื่อมโยงที่เรียงลำดับแล้วสองรายการ ให้คืนค่ารายการเดียวที่ผสานและเรียงลำดับแล้ว ใช้หัวรายการจำลองและตัวชี้หาง curr ในแต่ละขั้น ให้เปรียบเทียบโหนดหัวของสองรายการ แล้วเชื่อมโหนดที่มีค่าน้อยกว่าเข้ากับ curr เมื่อรายการหนึ่งหมด ให้เชื่อมส่วนที่เหลือของอีกรายการเข้ามา เวลา: O(n+m), พื้นที่: O(1) (เปลี่ยนการเชื่อมตัวชี้ในตำแหน่งเดิม)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 or l2 # attach remaining nodes
return dummy.next
def build(arr):
d = ListNode(); c = d
for v in arr:
c.next = ListNode(v); c = c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val); h=h.next
return r
print(to_list(mergeTwoLists(build([1,2,4]), build([1,3,4]))))ติดตามการผสานทีละขั้น
ติดตามการทำงานของ mergeTwoLists([1,2,4], [1,3,4]): เปรียบเทียบ 1 กับ 1 — เลือก l1(1) แล้วเลื่อน l1 ไปยัง 2 เปรียบเทียบ 2 กับ 1 — เลือก l2(1) แล้วเลื่อน l2 ไปยัง 3 เปรียบเทียบ 2 กับ 3 — เลือก l1(2) แล้วเลื่อน l1 ไปยัง 4 เปรียบเทียบ 4 กับ 3 — เลือก l2(3) แล้วเลื่อน l2 ไปยัง 4 เปรียบเทียบ 4 กับ 4 — เลือก l1(4) แล้วเลื่อน l1 ไปยังค่าว่าง เชื่อม l2(4) ที่เหลือเข้ามา ผลลัพธ์: [1,1,2,3,4,4]
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
step = 0
while l1 and l2:
step += 1
if l1.val <= l2.val:
print(f'Step {step}: pick l1({l1.val})')
curr.next = l1; l1 = l1.next
else:
print(f'Step {step}: pick l2({l2.val})')
curr.next = l2; l2 = l2.next
curr = curr.next
curr.next = l1 or l2
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
mergeTwoLists(build([1,2,4]),build([1,3,4]))การหาจุดกึ่งกลางด้วยตัวชี้ช้า-เร็ว
หากต้องการแบ่งรายการที่จุดกึ่งกลาง ให้ใช้รูปแบบตัวชี้ช้า-เร็ว slow เคลื่อนที่ครั้งละ 1 ขั้น ส่วน fast เคลื่อนที่ครั้งละ 2 ขั้น เมื่อ fast ไปถึง None (หรือโหนดสุดท้าย) slow จะอยู่ที่จุดกึ่งกลาง สำหรับรายการที่มีความยาวเป็นเลขคู่ วิธีนี้จะได้โหนดกึ่งกลางสองโหนดแรก ซึ่งเป็นแนวทางมาตรฐานสำหรับการแบ่งรายการเพื่อเรียงลำดับแบบผสาน
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def split_at_mid(head):
'''Returns (first_half_head, second_half_head).'''
slow, fast = head, head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
mid = slow.next # second half starts here
slow.next = None # sever the list
return head, mid
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val); h=h.next
return r
head=build([1,2,3,4,5])
first, second = split_at_mid(head)
print(to_list(first), to_list(second)) # [1,2,3] [4,5]การเรียงลำดับแบบผสานบนลิงก์ลิสต์
LeetCode 148 'เรียงลำดับลิสต์': เรียงลำดับลิงก์ลิสต์โดยใช้เวลา O(n log n) และหน่วยความจำ O(log n) แนวทางคือแบ่งลิสต์ที่จุดกึ่งกลาง เรียงลำดับแต่ละครึ่งแบบเรียกซ้ำ แล้วผสานเข้าด้วยกัน การเรียงลำดับแบบผสานสำหรับลิงก์ลิสต์เหมาะตามธรรมชาติ เพราะการแบ่งที่จุดกึ่งกลางใช้เวลา O(n) (ไม่ใช่ O(1) เหมือนอาร์เรย์) แต่ความซับซ้อนโดยรวมยังคงเป็น O(n log n) และใช้หน่วยความจำของสแตกเพียง O(log n)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def sortList(head):
if not head or not head.next:
return head
# Split
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
mid = slow.next
slow.next = None
# Recurse
left = sortList(head)
right = sortList(mid)
# Merge
dummy = ListNode(0)
curr = dummy
while left and right:
if left.val <= right.val:
curr.next = left; left = left.next
else:
curr.next = right; right = right.next
curr = curr.next
curr.next = left or right
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(sortList(build([4,2,1,3])))) # [1,2,3,4]การค้นหาโหนดลำดับที่ n จากท้าย
LeetCode 19 'ลบโหนดลำดับที่ n จากท้ายลิสต์': ค้นหาโหนดลำดับที่ n จากท้ายในการเดินผ่านเพียงรอบเดียว ใช้ตัวชี้สองตัวที่ห่างกันพอดี n โหนด เลื่อน fast ล่วงหน้า slow ไป n ขั้น จากนั้นเลื่อนทั้งคู่ไปพร้อมกันจนกว่า fast จะถึงโหนดสุดท้าย ณ จุดนั้น slow จะอยู่ที่โหนดลำดับที่ (n+1) จากท้าย ซึ่งเป็นโหนดก่อนหน้าโหนดที่ต้องลบ
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def removeNthFromEnd(head, n):
dummy = ListNode(0, head)
fast = dummy
for _ in range(n + 1): # advance fast n+1 steps
fast = fast.next
slow = dummy
while fast: # advance both until fast is None
slow = slow.next
fast = fast.next
slow.next = slow.next.next # remove nth node
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(removeNthFromEnd(build([1,2,3,4,5]), 2))) # [1,2,3,5]เหตุผลที่ต้องใช้ n+1 ขั้นในการลบโหนดลำดับที่ n
จุดละเอียดสำคัญคือการเลื่อนตัวชี้เร็วไป n+1 ขั้น (ไม่ใช่ n ขั้น) จากส่วนหัวจำลอง หลังจากเลื่อน n+1 ขั้น ตัวชี้เร็วจะอยู่ล่วงหน้าตัวชี้ช้า n+1 ตำแหน่ง (ทั้งคู่เริ่มจากส่วนหัวจำลอง) เมื่อตัวชี้เร็วไปถึงไม่มีค่า ซึ่งอยู่ถัดจากโหนดท้ายไปหนึ่งตำแหน่ง ตัวชี้ช้าจะอยู่ก่อนหน้าคำว่าไม่มีค่า n+1 ตำแหน่ง กล่าวคืออยู่ที่ตำแหน่ง (ความยาว - n - 1) เมื่อเริ่มนับจากศูนย์ หรืออยู่ที่โหนดก่อนหน้าโหนดเป้าหมาย การทำเช่นนี้ทำให้ slow.next = slow.next.next ลบโหนดลำดับที่ n จากท้ายได้อย่างเรียบร้อย
# Visual: list = [1,2,3,4,5], n=2
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None
# After n+1=3 forward steps from dummy, fast=3
# dummy(slow) 1 2 3(fast) 4 5 None
# Advance both until fast=None:
# Step 1: slow=1, fast=4
# Step 2: slow=2, fast=5
# Step 3: slow=3, fast=None
# slow is at 3, slow.next=4 (the 2nd from end) -> delete
print('slow.next (to delete): 4')
print('Result: [1, 2, 3, 5]')จุดตัดของลิงก์ลิสต์สองรายการ
LeetCode 160 'จุดตัดของลิงก์ลิสต์สองรายการ': ค้นหาโหนดที่ลิงก์ลิสต์สองรายการเริ่มตัดกัน เทคนิคที่ใช้หน่วยความจำ O(1) คือเลื่อนตัวชี้สองตัว โดยให้แต่ละตัวเริ่มจากลิสต์หนึ่งรายการ เมื่อตัวชี้ไปถึงไม่มีค่า ให้เปลี่ยนไปชี้ยังส่วนหัวของอีกลิสต์หนึ่ง หลังจากเดินไม่เกินจำนวนโหนดของลิสต์ A + จำนวนโหนดของลิสต์ B ขั้น ตัวชี้ทั้งคู่จะเดินทางด้วยระยะทางรวมเท่ากัน และต้องมาถึงโหนดจุดตัด (หรือทั้งคู่จะอยู่ที่ไม่มีค่าหากไม่มีจุดตัด)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def getIntersectionNode(headA, headB):
a, b = headA, headB
while a is not b:
a = a.next if a else headB
b = b.next if b else headA
return a # None if no intersection
# Build: A: 4->1->\ B: 5->6->1->\ both -> 8->4->5
shared = [ListNode(v) for v in [8, 4, 5]]
shared[0].next = shared[1]; shared[1].next = shared[2]
A = ListNode(4); A.next = ListNode(1); A.next.next = shared[0]
B = ListNode(5); B.next = ListNode(6); B.next.next = ListNode(1); B.next.next.next = shared[0]
print(getIntersectionNode(A, B).val) # 8การผสานลิสต์ที่เรียงลำดับแล้ว k รายการ (แบ่งแยกแล้วพิชิต)
LeetCode 23 'ผสานลิสต์ที่เรียงลำดับแล้ว k รายการ': เมื่อกำหนดลิสต์ที่เรียงลำดับแล้ว k รายการ ให้ผสานเป็นลิสต์เดียว แนวทางที่เหมาะสมที่สุดคือผสานลิสต์เป็นคู่ ๆ ซ้ำไปโดยใช้วิธีแบ่งแยกแล้วพิชิต ซึ่งจะลดจำนวนลิสต์ลงครึ่งหนึ่งในแต่ละรอบ เมื่อมีลิสต์ k รายการที่มีความยาวเฉลี่ย n วิธีนี้ใช้เวลา O(n k log k) เทียบกับ O(n k²) สำหรับการผสานทีละลิสต์ตามลำดับ วิธีใช้ฮีปค่าต่ำสุดก็มีความซับซ้อน O(n k log k) เช่นกัน
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeKLists(lists):
def merge_two(l1, l2):
dummy = ListNode(0); curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1; l1 = l1.next
else:
curr.next = l2; l2 = l2.next
curr = curr.next
curr.next = l1 or l2
return dummy.next
if not lists: return None
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i+1] if i+1 < len(lists) else None
merged.append(merge_two(l1, l2))
lists = merged
return lists[0]
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
lists=[build([1,4,5]),build([1,3,4]),build([2,6])]
print(to_list(mergeKLists(lists))) # [1,1,2,3,4,4,5,6]ลิงก์ลิสต์คี่-คู่
LeetCode 328 'ลิงก์ลิสต์คี่-คู่': จัดโหนดที่มีดัชนีคี่ทั้งหมดไว้ก่อน แล้วจึงตามด้วยโหนดที่มีดัชนีคู่ (เริ่มดัชนีที่ 1) แนวทางคือรักษาโซ่แยกกันสองโซ่ (โซ่คี่และโซ่คู่) แล้วเชื่อมเข้าด้วยกันเมื่อเสร็จสิ้น การเดินผ่านลิสต์เพียงรอบเดียวก็เพียงพอ จึงใช้เวลา O(n) และหน่วยความจำ O(1) นี่เป็นตัวอย่างที่ชัดเจนของการเลื่อนตัวชี้สองตัวพร้อมกันด้วยระยะก้าวที่ต่างกัน
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def oddEvenList(head):
if not head:
return head
odd = head
even = head.next
even_head = even
while even and even.next:
odd.next = even.next
odd = odd.next
even.next = odd.next
even = even.next
odd.next = even_head
return head
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(oddEvenList(build([1,2,3,4,5])))) # [1,3,5,2,4]สรุปแนวคิดทั้งหมดเข้าด้วยกัน
รูปแบบทั้งสามในบทเรียนนี้ ได้แก่ การผสานลิสต์ที่เรียงลำดับแล้ว การแบ่งที่จุดกึ่งกลาง และการค้นหาโหนดลำดับที่ n จากท้าย ล้วนมีแนวคิดร่วมกันคือ ใช้ตัวแปรตัวชี้เพิ่มเติมเพื่อติดตามตำแหน่งโดยไม่ต้องใช้หน่วยความจำเพิ่ม ส่วนหัวจำลองช่วยให้การผสานและการลบง่ายขึ้น ช่องว่างระหว่างตัวชี้ช้าและตัวชี้เร็วช่วยกำหนดตำแหน่งสัมพัทธ์ที่ต้องการ และการเลื่อนตัวชี้ตัวหนึ่งก่อนจะสร้างระยะห่างที่ต้องการ
ในการสัมภาษณ์ ควรบอกชื่อรูปแบบที่กำลังใช้ก่อนเขียนโค้ด เช่น 'ฉันจะใช้เทคนิคช่องว่างระหว่างตัวชี้สองตัวเพื่อค้นหาโหนดลำดับที่ n จากท้ายในการเดินผ่านรอบเดียว' วิธีนี้แสดงให้เห็นถึงการคิดอย่างเป็นระบบ
ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า การผสานลิสต์ที่เรียงลำดับแล้วสองรายการใช้ส่วนหัวจำลองและการเปรียบเทียบในทุกขั้นตอน โดยใช้เวลา O(n+m) และหน่วยความจำ O(1) การแบ่งที่จุดกึ่งกลางใช้ตัวชี้ช้าและตัวชี้เร็ว โดยให้ตัวชี้เร็วหยุดที่คู่ที่ถูกต้องคู่สุดท้าย และ การค้นหาโหนดลำดับที่ n จากท้ายจะเลื่อนตัวชี้เร็วล่วงหน้า n+1 ขั้น เพื่อให้ตัวชี้ช้าไปอยู่ที่โหนดก่อนหน้า ถัดไปเราจะสร้างสแตกและคิว แล้วนำไปใช้กับโจทย์สัมภาษณ์คลาสสิก
คำถามที่พบบ่อย
บทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย”
ผสานลิสต์เชื่อมโยงที่เรียงแล้วสองลิสต์ในเวลา O(n) แบ่งลิสต์ที่จุดกึ่งกลางด้วยตัวชี้ช้า–เร็ว และค้นหาโหนดลำดับที่ n จากส่วนท้าย คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- คลาสโหนดและการสร้างลิสต์
- การกลับลำดับลิสต์เชื่อมโยง
- การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd
- ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย