การจำลองคิวและสแตกซึ่งกันและกัน
สร้างคิวด้วยสแตกสองชุดและสร้างสแตกด้วยคิวสองชุด พร้อมอธิบายต้นทุนตัดจำหน่ายของแต่ละแนวทาง
การจำลองคิวและสแตกซึ่งกันและกัน เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดจึงจำลองโครงสร้างหนึ่งด้วยอีกโครงสร้างหนึ่ง
การสร้าง คิวโดยใช้สแตกสองตัว และ สแตกโดยใช้คิวสองตัว เป็นคำถามคลาสสิกในการสัมภาษณ์ด้านการออกแบบ ทั้งสองคำถามทดสอบความเข้าใจเงื่อนไขคงตัวของโครงสร้างข้อมูลทั้งสองแบบ และความสามารถในการรักษาคุณสมบัติของโครงสร้างหนึ่งขณะใช้การดำเนินการพื้นฐานจากอีกโครงสร้างหนึ่ง ผู้สัมภาษณ์ยังใช้คำถามเหล่านี้เป็นจุดเริ่มต้นเพื่อพูดคุยเรื่องความซับซ้อนแบบเฉลี่ยตัดจำหน่าย
แนวคิดสำคัญคือ สแตกเป็นแบบ LIFO ส่วนคิวเป็นแบบ FIFO หากต้องการแปลงจากแบบหนึ่งเป็นอีกแบบหนึ่ง คุณต้องกลับลำดับ และการกลับสแตกหนึ่งไปเป็นสแตกอีกตัวจะทำให้ได้ลำดับการเพิ่มสมาชิกเดิม ซึ่งก็คือ FIFO
คิวโดยใช้สแตกสองตัว (วิธีเลื่อนการย้าย)
วิธีเลื่อนการย้าย: ใช้สแตก inbox สำหรับการเพิ่มสมาชิก และใช้สแตก outbox สำหรับการนำสมาชิกออก เมื่อเรียกใช้การนำออกจากคิว หาก outbox ว่าง ให้ย้ายสมาชิกทั้งหมดจาก inbox ไปยัง outbox การกลับลำดับนี้จะทำให้กลับมาเป็นลำดับ FIFO หาก outbox ไม่ว่าง ให้นำสมาชิกออกจากสแตกนั้นโดยตรง การย้ายจะเกิดขึ้นเมื่อจำเป็นเท่านั้น จึงกระจายต้นทุนการย้าย O(n) ไปยังการดำเนินการหลายครั้งในลักษณะเฉลี่ยตัดจำหน่าย
class MyQueue:
def __init__(self):
self.inbox = []
self.outbox = []
def push(self, x):
self.inbox.append(x)
def _transfer(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())
def pop(self):
self._transfer()
return self.outbox.pop()
def peek(self):
self._transfer()
return self.outbox[-1]
def empty(self):
return not self.inbox and not self.outbox
q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek()) # 1
print(q.pop()) # 1
print(q.pop()) # 2
q.push(4)
print(q.pop()) # 3การวิเคราะห์ O(1) แบบเฉลี่ยตัดจำหน่ายสำหรับคิวจากสแตก
สมาชิกแต่ละตัวจะถูกย้ายจาก inbox ไปยัง outbox อย่างมากหนึ่งครั้ง การนำสมาชิกออกจาก outbox มีความซับซ้อน O(1) และการย้ายจะเกิดขึ้นเฉพาะเมื่อ outbox ว่าง ดังนั้นงานทั้งหมดสำหรับการเพิ่มสมาชิก n ครั้งและนำสมาชิกออก n ครั้งจึงไม่เกินการดำเนินการกับสแตก 2n ครั้ง หรือ O(n) โดยรวม และมีความซับซ้อน O(1) แบบเฉลี่ยตัดจำหน่ายต่อการดำเนินการ ซึ่งหมายความว่าการดำเนินการแต่ละครั้งอาจมีความซับซ้อน O(n) ในกรณีเลวร้ายที่สุด แต่ค่าเฉลี่ยคือ O(1)
# Trace transfer costs for 10 push/pop interleaved
class TrackedQueue:
def __init__(self):
self.inbox = []; self.outbox = []; self.transfers = 0
def push(self, x): self.inbox.append(x)
def pop(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())
self.transfers += 1
return self.outbox.pop()
q = TrackedQueue()
for i in range(5):
q.push(i)
for _ in range(5):
q.pop()
q.push(10); q.push(20)
q.pop()
print('Total transfer operations:', q.transfers) # at most nสแตกโดยใช้คิวสองตัว (นำออกแบบเลื่อนเวลา)
การสร้างสแตกด้วยคิวสองตัวไม่เป็นธรรมชาตินัก เพราะคิวเป็นแบบ FIFO วิธีนำออกแบบเลื่อนเวลาคือ เก็บคิวหลักหนึ่งตัวและคิวชั่วคราวหนึ่งตัว เมื่อใช้ push ให้เพิ่มสมาชิกเข้าไปในคิวหลักด้วยความซับซ้อน O(1) เมื่อใช้ pop หรือ peek ให้นำสมาชิกทั้งหมด ยกเว้นตัวสุดท้าย ไปใส่ในคิวชั่วคราว เก็บสมาชิกตัวสุดท้ายไว้ แล้วสลับคิว วิธีนี้มีความซับซ้อน O(n) ต่อการนำสมาชิกออกหนึ่งครั้ง แต่มีความซับซ้อน O(1) ต่อการเพิ่มสมาชิกหนึ่งครั้ง
from collections import deque
class MyStack:
def __init__(self):
self.main = deque()
self.temp = deque()
def push(self, x):
self.main.append(x) # O(1)
def pop(self):
# Move all but last element to temp
while len(self.main) > 1:
self.temp.append(self.main.popleft())
val = self.main.popleft() # the 'top'
self.main, self.temp = self.temp, self.main # swap
return val
def top(self):
while len(self.main) > 1:
self.temp.append(self.main.popleft())
val = self.main[0]
self.temp.append(self.main.popleft())
self.main, self.temp = self.temp, self.main
return val
def empty(self):
return len(self.main) == 0
s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.pop()) # 2สแตกโดยใช้คิวตัวเดียว (หมุนเมื่อเพิ่มสมาชิก)
การสร้างสแตกด้วยคิวตัวเดียวที่เรียบง่ายคือ เมื่อใช้ push ให้เพิ่มสมาชิกใหม่เข้าคิว แล้วหมุนคิวเพื่อให้สมาชิกใหม่อยู่ด้านหน้า การหมุนหมายถึงการนำสมาชิกทั้งหมดที่มีอยู่ก่อนการเพิ่มสมาชิกออกจากคิว แล้วเพิ่มกลับเข้าไปใหม่ จากนั้น pop และ peek จะมีความซับซ้อน O(1) (เพียงนำสมาชิกด้านหน้าออกหรือดูสมาชิกด้านหน้า) ส่วน push มีความซับซ้อน O(n) ซึ่งเป็นข้อแลกเปลี่ยนตรงข้ามกับวิธีใช้คิวสองตัว
from collections import deque
class MyStackOneQueue:
def __init__(self):
self.q = deque()
def push(self, x):
self.q.append(x)
# Rotate: move all preceding elements behind x
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self):
return self.q.popleft()
def top(self):
return self.q[0]
def empty(self):
return len(self.q) == 0
s = MyStackOneQueue()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.top()) # 2สรุปข้อแลกเปลี่ยน: ควรเลือกแบบใด
สำหรับ คิวจากสแตกสองตัว: push มีความซับซ้อน O(1) ส่วน pop/peek มีความซับซ้อน O(1) แบบเฉลี่ยตัดจำหน่าย เหมาะเมื่อการนำสมาชิกออกเกิดขึ้นบ่อย สำหรับ สแตกจากคิวสองตัว: push มีความซับซ้อน O(1) ส่วน pop มีความซับซ้อน O(n) เหมาะเมื่อการเพิ่มสมาชิกเกิดขึ้นบ่อยกว่าการนำสมาชิกออกมาก สำหรับ สแตกจากคิวตัวเดียว: push มีความซับซ้อน O(n) ส่วน pop มีความซับซ้อน O(1) เหมาะเมื่อการนำสมาชิกออกเกิดขึ้นเป็นส่วนใหญ่ ในการสัมภาษณ์ ควรอธิบายข้อแลกเปลี่ยนเหล่านี้อย่างชัดเจน เพื่อแสดงให้เห็นว่าคุณคิดมากกว่าเพียงว่า 'ใช้งานได้'
print('Queue from 2 stacks: push O(1), pop O(1) amortised')
print('Stack from 2 queues: push O(1), pop O(n)')
print('Stack from 1 queue: push O(n), pop O(1)')เหตุใดการกลับลำดับจึงทำให้ได้ FIFO
เมื่อเพิ่มสมาชิก 1, 2, 3 ลงในสแตก (inbox) สมาชิกเหล่านี้จะเรียงจากล่างขึ้นบนเป็น 1, 2, 3 เมื่อนำสมาชิกทั้งหมดออกไปใส่ในสแตกตัวที่สอง (outbox) ลำดับจะกลับด้าน โดย outbox มี 3 อยู่ด้านล่างและ 1 อยู่ด้านบน การนำสมาชิกออกจาก outbox จะได้ 1 ตามด้วย 2 แล้วจึง 3 ซึ่งตรงกับลำดับการเพิ่มสมาชิกแบบ FIFO ทุกประการ นี่คือเหตุผลที่การกลับลำดับสองครั้งพอดี (ผ่านสแตกสองตัว) ทำให้กลับมาเป็น FIFO ขณะที่สแตกตัวเดียวจะให้ผลเป็น LIFO
# Demonstrate double-reversal = FIFO
inbox = [1, 2, 3] # pushed in this order
outbox = []
while inbox:
outbox.append(inbox.pop())
print('outbox (one reversal):', outbox) # [3, 2, 1] top-to-bottom
# Pop from outbox gives FIFO
result = []
while outbox:
result.append(outbox.pop())
print('dequeued:', result) # [1, 2, 3] — FIFO!LeetCode 232: สร้างคิวโดยใช้สแตก
LeetCode 232 คือปัญหา 'คิวจากสแตกสองตัว' โดยตรง วิธีที่คาดหวังคือการย้ายไปยัง outbox แบบเลื่อนเวลา ในการสัมภาษณ์ ให้ระบุว่าสมาชิกแต่ละตัวจะย้ายจาก inbox ไปยัง outbox อย่างมากหนึ่งครั้ง ทำให้การดำเนินการทั้งหมดมีความซับซ้อน O(1) แบบเฉลี่ยตัดจำหน่าย ควรกล่าวด้วยว่าการเรียกใช้ pop แต่ละครั้งอาจมีความซับซ้อน O(n) ในกรณีเลวร้ายที่สุด (เมื่อ outbox ว่าง) แต่ค่าเฉลี่ยตลอดการดำเนินการ n ครั้งคือ O(1)
class MyQueue:
def __init__(self):
self.inbox = []
self.outbox = []
def push(self, x):
self.inbox.append(x)
def pop(self):
self.peek() # ensure outbox is populated
return self.outbox.pop()
def peek(self):
if not self.outbox:
while self.inbox: # transfer lazily
self.outbox.append(self.inbox.pop())
return self.outbox[-1]
def empty(self):
return not self.inbox and not self.outbox
# Simulation
q = MyQueue()
q.push(1); q.push(2)
print(q.peek()) # 1
print(q.pop()) # 1
print(q.empty()) # FalseLeetCode 225: สร้างสแตกโดยใช้คิว
LeetCode 225 คือปัญหา 'สแตกจากคิว' วิธีใช้คิวตัวเดียวและหมุนเมื่อเพิ่มสมาชิกเป็นวิธีที่ชัดเจนที่สุด หลังจากเพิ่มสมาชิก x แล้ว ให้หมุนคิวโดยย้ายสมาชิกทั้งหมดที่มีอยู่ก่อนแล้วไปไว้ด้านหลัง x วิธีนี้มีต้นทุน O(n) ต่อการเพิ่มสมาชิกหนึ่งครั้ง แต่ทำให้ top และ pop มีความซับซ้อน O(1) ให้ระบุข้อแลกเปลี่ยนนี้ และยืนยันว่าตรงกับข้อจำกัด (เช่น ปริมาณงานที่มี push น้อยหรือ pop มาก)
from collections import deque
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x): # O(n)
self.q.append(x)
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self): # O(1)
return self.q.popleft()
def top(self): # O(1)
return self.q[0]
def empty(self):
return len(self.q) == 0
s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.top()) # 2
print(s.empty()) # Falseการขยายเป็นสแตกสามชุดในอาร์เรย์เดียว
ความท้าทายด้านการออกแบบที่เกี่ยวข้องกันคือการสร้างสแตกสามชุดโดยใช้อาร์เรย์เดียว วิธีหนึ่งคือแบ่งอาร์เรย์เป็นสามส่วนแบบคงที่ที่มีขนาดเท่ากัน อีกวิธีที่ยืดหยุ่นกว่าคือใช้การจัดเก็บแบบสลับพร้อมตัวชี้ ให้แต่ละสแตกขยายจากพื้นที่ของตน และคัดลอกข้อมูลเมื่อขอบเขตชนกัน โจทย์นี้ทดสอบการจัดการอาร์เรย์แบบไดนามิก และมักใช้ถามในการสัมภาษณ์ระดับอาวุโส วิธีแบ่งส่วนแบบคงที่เรียบง่ายกว่า แต่ทำให้เสียพื้นที่หากสแตกแต่ละชุดเติบโตไม่เท่ากัน
class ThreeStacks:
def __init__(self, size):
self.data = [0] * (3 * size)
self.tops = [-1, -1, -1] # relative top of each stack
self.size = size
def push(self, stack_num, val):
self.tops[stack_num] += 1
if self.tops[stack_num] >= self.size:
raise OverflowError('stack full')
self.data[stack_num * self.size + self.tops[stack_num]] = val
def pop(self, stack_num):
if self.tops[stack_num] < 0:
raise IndexError('stack empty')
val = self.data[stack_num * self.size + self.tops[stack_num]]
self.tops[stack_num] -= 1
return val
ts = ThreeStacks(5)
ts.push(0, 10); ts.push(1, 20); ts.push(2, 30)
print(ts.pop(0), ts.pop(1), ts.pop(2)) # 10 20 30ประเด็นสำคัญ: รูปแบบการจำลอง
ปัญหาการจำลองโครงสร้างข้อมูลซึ่งกันและกันสอนหลักการที่กว้างขึ้นว่า โครงสร้างข้อมูลใด ๆ ก็สามารถสร้างจากโครงสร้างข้อมูลอื่นได้ หากมีพื้นที่พักข้อมูลชั่วคราวระหว่างทางและการกลับลำดับมากเพียงพอ ต้นทุนของการจำลองขึ้นอยู่กับว่าคุณปรับการดำเนินการใดให้เหมาะสม — คุณสามารถทำให้ push หรือ pop มีต้นทุน O(1) ได้เสมอ แต่การทำให้ทั้งสองอย่างเป็น O(1) ต้องอาศัยการคิดต้นทุนแบบเฉลี่ยตลอดการใช้งานหรือโครงสร้างเสริมหลายชุด
ในการสัมภาษณ์ ควรถามเสมอว่า: “การดำเนินการใดเกิดขึ้นบ่อยกว่า” คำถามนี้ช่วยชี้นำการเลือกรูปแบบการนำไปใช้ และแสดงให้เห็นถึงความเข้าใจข้อกำหนดด้านการดำเนินการในระดับอาวุโส
ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า คิวที่สร้างจากสแตกสองชุดทำให้ pop มีต้นทุนเฉลี่ยตลอดการใช้งานเป็น O(1) โดยถ่ายโอนองค์ประกอบจากกล่องรับเข้าไปยังกล่องส่งออกเมื่อจำเป็น สแตกที่สร้างจากคิวหนึ่งชุดทำให้ pop เป็น O(1) โดยหมุนคิวทุกครั้งที่ทำ push (push มีต้นทุน O(n)) และ การเลือกว่าจะทำให้การดำเนินการใดเป็น O(1) ขึ้นอยู่กับรูปแบบการใช้งาน บทถัดไปเราจะสำรวจโครงสร้างภายในของแฮชแมปและการจัดการการชนกัน
คำถามที่พบบ่อย
บทเรียน “การจำลองคิวและสแตกซึ่งกันและกัน” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การจำลองคิวและสแตกซึ่งกันและกัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การจำลองคิวและสแตกซึ่งกันและกัน”
สร้างคิวด้วยสแตกสองชุดและสร้างสแตกด้วยคิวสองชุด พร้อมอธิบายต้นทุนตัดจำหน่ายของแต่ละแนวทาง คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การจำลองคิวและสแตกซึ่งกันและกัน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การสร้างสแตกและการประยุกต์ใช้
- การสร้างคิวและดีคิว
- รูปแบบสแตกโมโนโทน
- การจำลองคิวและสแตกซึ่งกันและกัน