คลาสโหนดและการสร้างลิสต์
กำหนดคลาสข้อมูล Node สร้างลิสต์ด้วยการเชื่อมโหนดด้วยตนเอง และเขียนตัวช่วยแทรก ลบ และพิมพ์เพื่อมองเห็นการเปลี่ยนแปลงของตัวชี้
คลาสโหนดและการสร้างลิสต์ เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
รายการเชื่อมโยงคืออะไร
รายการเชื่อมโยงคือลำดับของโหนด โดยแต่ละโหนดเก็บค่าและตัวชี้ไปยังโหนดถัดไป ต่างจากอาร์เรย์ โหนดจะกระจายอยู่ในหน่วยความจำ จึงไม่มีการเข้าถึงด้วยดัชนีในเวลา O(1) แต่แลกมาด้วยการแทรกและการลบที่ตำแหน่งใด ๆ ที่ทราบได้ในเวลา O(1) โดยไม่ต้องเลื่อนสมาชิก
ใน Python เราแทนแต่ละโหนดด้วยคลาสขนาดเล็กที่เก็บ val และ next การเชื่อมโหนดเข้าด้วยกันจะสร้างรายการขึ้นมา ส่วน next ของโหนดสุดท้ายคือ None เพื่อบอกจุดสิ้นสุด
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Build: 1 -> 2 -> 3 -> None
head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)
# Traverse and print
curr = head
while curr:
print(curr.val, end=' -> ')
curr = curr.next
print('None')การสร้างรายการจากอาร์เรย์
ในการสัมภาษณ์ คุณมักได้รับรายการและถูกขอให้สร้างรายการเชื่อมโยงที่เทียบเท่ากัน หรือทำย้อนกลับกัน ฟังก์ชันช่วยเหลือ build และ to_list เป็นสิ่งที่ควรจดจำ: build เชื่อมโหนดจากอาร์เรย์เข้าด้วยกัน ส่วน to_list เดินไปตามรายการเพื่อรวบรวมค่าไว้สำหรับตรวจสอบได้ง่าย
การสร้างรายการเชื่อมโยงจากสมาชิก n ตัวใช้เวลา O(n) และพื้นที่ O(n) การใช้โหนดหัวจำลองช่วยลดกรณีขอบที่โหนดแรกอาจเปลี่ยนแปลง
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build(arr):
dummy = ListNode(0)
curr = dummy
for val in arr:
curr.next = ListNode(val)
curr = curr.next
return dummy.next
def to_list(head):
result = []
while head:
result.append(head.val)
head = head.next
return result
head = build([1, 2, 3, 4, 5])
print(to_list(head)) # [1, 2, 3, 4, 5]แทรกที่หัวและ tail
การแทรกโหนดใหม่ที่หัวรายการใช้เวลา O(1): สร้างโหนด ชี้ next ของโหนดนั้นไปยังหัวรายการเดิม แล้วคืนโหนดใหม่เป็นหัวรายการ การแทรกที่tailต้องเดินไปยังโหนดสุดท้าย (O(n)) แล้วจึงเชื่อมโหนดใหม่เข้าด้วยกัน
การใช้โหนดหัวจำลองช่วยตัดกรณีพิเศษของรายการว่างสำหรับการแทรกทั้งสองแบบ เพราะ dummy.next จะเป็นหัวรายการจริงเสมอ
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def insert_head(head, val):
return ListNode(val, head) # O(1)
def insert_tail(head, val):
new_node = ListNode(val)
if not head:
return new_node
curr = head
while curr.next:
curr = curr.next
curr.next = new_node
return head
head = None
for v in [1, 2, 3]:
head = insert_tail(head, v)
head = insert_head(head, 0)
curr = head
while curr:
print(curr.val, end=' -> ')
curr = curr.next
print('None') # 0 -> 1 -> 2 -> 3 -> Noneลบโหนดตามค่า
หากต้องการลบโหนดแรกที่มีค่าตามกำหนด ให้รักษาตัวชี้ prev ไว้หลัง curr หนึ่งตำแหน่ง เมื่อ curr.val == target ให้กำหนด prev.next = curr.next เพื่อข้ามโหนดนั้นไป โหนดหัวจำลองมีประโยชน์อย่างยิ่งในกรณีนี้ เพราะช่วยตัดกรณีพิเศษของการลบโหนดหัวรายการจริงออกไปได้ และ prev สามารถเริ่มต้นที่โหนดจำลองเสมอ
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def delete_val(head, target):
dummy = ListNode(0)
dummy.next = head
prev, curr = dummy, head
while curr:
if curr.val == target:
prev.next = curr.next
break
prev, curr = curr, curr.next
return dummy.next
def to_list(h):
r = []
while h:
r.append(h.val)
h = h.next
return r
head = None
for v in [1, 2, 3, 2, 4]:
dummy2 = ListNode(v)
dummy2.next = head
head = dummy2 # build in reverse for speed
head = delete_val(head, 2)
print(to_list(head))ทำความเข้าใจการเปลี่ยนแปลงของตัวชี้
ข้อผิดพลาดที่พบบ่อยคือการติดตามโหนดไม่ทันเมื่ออัปเดตตัวชี้ ให้บันทึก next ไว้ก่อนเขียนทับเสมอ: saved = curr.next จากนั้นจึงกำหนดค่าใหม่ วาดรายการเป็นกล่องที่เชื่อมด้วยลูกศร แล้วจำลองการอัปเดตตัวชี้แต่ละครั้งบนกระดาษก่อนเขียนโค้ด วิธีแสดงภาพนี้ช่วยป้องกันข้อผิดพลาดจากตัวชี้ค่าว่างโดยไม่ตั้งใจระหว่างการสัมภาษณ์
โปรดจำไว้ว่า ใน Python การกำหนดค่าใหม่ให้ curr.next ไม่ส่งผลต่อตัว curr เอง แต่หากสูญเสียการอ้างอิงไปยัง curr.next ก่อนบันทึกไว้ คุณจะไม่สามารถเดินต่อไปข้างหน้าได้อีก
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Demonstrate safe pointer update
def swap_first_two(head):
if not head or not head.next:
return head
first = head
second = head.next
# Save third before losing the reference
third = second.next
# Rewire
second.next = first
first.next = third
return second
from functools import reduce
nodes = [ListNode(i) for i in range(1, 5)]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = swap_first_two(nodes[0])
curr = head
while curr:
print(curr.val, end=' ')
curr = curr.next
# 2 1 3 4รายการเชื่อมโยงทางเดียวเทียบกับสองทาง
รายการเชื่อมโยงทางเดียวเก็บเฉพาะตัวชี้ next จึงเดินรายการได้เพียงทิศทางเดียว ส่วนรายการเชื่อมโยงสองทางเก็บทั้ง prev และ next ทำให้เดินย้อนกลับได้ในเวลา O(1) และลบได้ในเวลา O(1) เมื่อมีการอ้างอิงโดยตรงไปยังโหนด โดยไม่ต้องใช้ลูปติดตาม prev
collections.deque ของ Python สร้างขึ้นโดยใช้รายการเชื่อมโยงสองทาง จึงรองรับ appendleft และ popleft ในเวลา O(1) ในการสัมภาษณ์ คุณจะได้ลงมือสร้างรายการเชื่อมโยงทางเดียว ส่วนรายการเชื่อมโยงสองทางมักปรากฏในการออกแบบแคช LRU
class DLNode:
def __init__(self, val=0):
self.val = val
self.prev = None
self.next = None
# Build doubly linked: 1 <-> 2 <-> 3
a, b, c = DLNode(1), DLNode(2), DLNode(3)
a.next = b; b.prev = a
b.next = c; c.prev = b
# Traverse forward
curr = a
while curr:
print(curr.val, end=' <-> ')
curr = curr.next
print('None')
# Traverse backward from c
curr = c
while curr:
print(curr.val, end=' <-> ')
curr = curr.prev
print('None')ฟังก์ชันช่วยเหลือสำหรับความยาว tail และการพิมพ์
ฟังก์ชันอรรถประโยชน์สามอย่างที่ควรเตรียมไว้สำหรับการสัมภาษณ์เรื่องรายการเชื่อมโยง: length(head) นับจำนวนโหนดในเวลา O(n), tail(head) คืนโหนดสุดท้ายในเวลา O(n) และ print_list(head) จัดรูปแบบรายการเพื่อช่วยแก้ไขข้อผิดพลาด การเตรียมฟังก์ชันเหล่านี้ไว้ช่วยให้คุณมุ่งความสนใจกับอัลกอริทึมหลัก แทนที่จะต้องเขียนตรรกะของฟังก์ชันช่วยเหลือใหม่
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def length(head):
count = 0
while head:
count += 1
head = head.next
return count
def tail(head):
while head and head.next:
head = head.next
return head
def print_list(head):
parts = []
while head:
parts.append(str(head.val))
head = head.next
print(' -> '.join(parts) + ' -> None')
# Build and test
nodes = [ListNode(i) for i in [10, 20, 30, 40]]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = nodes[0]
print('Length:', length(head))
print('Tail:', tail(head).val)
print_list(head)การตั้งค่าตัวชี้สองตัวบนรายการเชื่อมโยง
เทคนิคตัวชี้สองตัวมีความสำคัญต่อรายการเชื่อมโยงไม่แพ้อาร์เรย์ แต่ตัวชี้ในที่นี้คือโหนดของรายการเชื่อมโยง ไม่ใช่ดัชนี รูปแบบที่พบบ่อย ได้แก่ ตัวชี้ช้าและเร็ว (ตัวชี้เร็วเคลื่อนที่เร็วกว่า 2 เท่า) สำหรับค้นหาจุดกึ่งกลางและตรวจจับวงจร และคู่ตัวชี้โหนดก่อนหน้าและโหนดปัจจุบันสำหรับการลบและการกลับลำดับ
กำหนดค่าเริ่มต้นให้ตัวชี้ทั้งสองอย่างชัดเจนเสมอ และตรวจสอบการสิ้นสุดด้วยค่าว่างอย่างระมัดระวัง — fast and fast.next ช่วยป้องกันข้อผิดพลาดจากตัวชี้ค่าว่างเมื่อ fast อยู่ใกล้จุดสิ้นสุด
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Find middle node using slow-fast pointers
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # for even length, returns second of two middle nodes
nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
print(find_middle(nodes[0]).val) # 3 (middle of 1->2->3->4->5)รูปแบบโหนดหัวจำลอง
รูปแบบโหนดหัวจำลอง (โหนดเฝ้าระวัง) เป็นเทคนิคที่มีประโยชน์ที่สุดอย่างหนึ่งในปัญหารายการเชื่อมโยง การเพิ่มโหนดจำลองที่มีค่า 0 ไว้ด้านหน้าทำให้ไม่ต้องจัดการกรณีพิเศษของรายการว่างหรือการเปลี่ยนแปลงที่หัวรายการจริง ผลลัพธ์ของคุณจะอยู่ที่ dummy.next เสมอ รูปแบบนี้ปรากฏในงานผสานรายการที่เรียงลำดับ การลบสมาชิกตำแหน่งที่ n จากท้าย การแบ่งรายการ และงานอื่น ๆ อีกมากมาย
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Remove all nodes with val == target (may include head)
def remove_all(head, target):
dummy = ListNode(0)
dummy.next = head
curr = dummy
while curr.next:
if curr.next.val == target:
curr.next = curr.next.next # skip the node
else:
curr = curr.next
return dummy.next
def to_list(h):
r = []
while h:
r.append(h.val)
h = h.next
return r
nodes = [ListNode(v) for v in [1, 2, 6, 3, 4, 5, 6]]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = remove_all(nodes[0], 6)
print(to_list(head)) # [1, 2, 3, 4, 5]ความซับซ้อนด้านเวลาและพื้นที่
การดำเนินการส่วนใหญ่กับรายการเชื่อมโยงมีความซับซ้อนดังนี้ เข้าถึงด้วยดัชนี: O(n) — ต้องเดินจากหัวรายการ แทรกหรือลบที่โหนดที่ทราบ: O(1) — เพียงเชื่อมตัวชี้ใหม่ แทรกหรือลบที่ตำแหน่ง k: O(k) — ต้องเดินไปก่อน ค้นหา: O(n) — ในกรณีเลวร้ายที่สุดต้องเดินทั้งรายการ พื้นที่คือ O(1) สำหรับการดำเนินการทั้งหมดที่ทำในพื้นที่เดิม (ไม่รวมโครงสร้างข้อมูลเพิ่มเติม)
เมื่อเปรียบเทียบกับอาร์เรย์ อาร์เรย์เข้าถึงได้ในเวลา O(1) แต่การแทรกหรือลบใช้เวลา O(n) เนื่องจากต้องเลื่อนสมาชิก รายการเชื่อมโยงเหมาะกว่าเมื่อมีการแทรกและลบที่ตำแหน่งต่าง ๆ บ่อยครั้ง
เคล็ดลับการสัมภาษณ์เรื่องรายการเชื่อมโยง
ก่อนเขียนโค้ดรายการเชื่อมโยง ให้วาดรายการเป็นภาพด้วยกล่องและลูกศร ยืนยันกรณีขอบออกเสียงให้ชัดเจน ได้แก่ รายการว่าง โหนดเดียว และความยาวคู่เทียบกับคี่ ใช้โหนดหัวจำลองเพื่อทำให้เงื่อนไขขอบเขตง่ายขึ้น ตรวจสอบ if not head ตั้งแต่ต้นเสมอ หลังเขียนโค้ดแล้ว ให้ไล่การทำงานกับรายการสามโหนดเพื่อจับข้อผิดพลาดของตัวชี้ก่อนที่ผู้สัมภาษณ์จะพบ
ข้อผิดพลาดส่วนใหญ่ของรายการเชื่อมโยงมาจากหนึ่งในสามสาเหตุ ได้แก่ ลืมบันทึก next ก่อนเขียนทับ ความคลาดเคลื่อนทีละหนึ่งในเงื่อนไขสิ้นสุด หรือไม่จัดการกรณีขอบที่หัวรายการเปลี่ยนแปลง — โหนดจำลองช่วยกำจัดสาเหตุที่สามได้ทั้งหมด
ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า รายการเชื่อมโยงสร้างจากออบเจ็กต์โหนดที่มีฟิลด์ค่าและตัวชี้ไปยังโหนดถัดไป รูปแบบโหนดหัวจำลองช่วยกำจัดกรณีขอบที่หัวรายการเปลี่ยนแปลง และ การตั้งค่าตัวชี้สองตัวแบบช้าและเร็วเป็นพื้นฐานของการค้นหาจุดกึ่งกลางและการตรวจจับวงจร บทถัดไปเราจะจัดการกับการกลับลำดับรายการเชื่อมโยง ซึ่งเป็นหนึ่งในปัญหาตัวชี้ที่มักถูกถามมากที่สุด
คำถามที่พบบ่อย
บทเรียน “คลาสโหนดและการสร้างลิสต์” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “คลาสโหนดและการสร้างลิสต์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “คลาสโหนดและการสร้างลิสต์”
กำหนดคลาสข้อมูล Node สร้างลิสต์ด้วยการเชื่อมโหนดด้วยตนเอง และเขียนตัวช่วยแทรก ลบ และพิมพ์เพื่อมองเห็นการเปลี่ยนแปลงของตัวชี้ คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “คลาสโหนดและการสร้างลิสต์” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- คลาสโหนดและการสร้างลิสต์
- การกลับลำดับลิสต์เชื่อมโยง
- การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd
- ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย