ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU
แก้โจทย์ลำดับต่อเนื่องที่ยาวที่สุดในเวลา O(n) ด้วยเซต แล้วออกแบบแคช LRU โดยใช้ OrderedDict
ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
โจทย์ลำดับจำนวนต่อเนื่องที่ยาวที่สุด
LeetCode 128 «ลำดับจำนวนต่อเนื่องที่ยาวที่สุด»: เมื่อกำหนดอาร์เรย์ที่ไม่ได้เรียงลำดับมา ให้หาความยาวของลำดับจำนวนเต็มต่อเนื่องที่ยาวที่สุด ตัวอย่างเช่น [100,4,200,1,3,2] มีลำดับจำนวนต่อเนื่อง [1,2,3,4] ซึ่งมีความยาว 4 ความท้าทายคือการแก้โจทย์ให้ได้ในเวลา O(n) แทน O(n log n) ซึ่งเป็นเวลาที่ได้จากการเรียงลำดับแล้วตรวจสอบ
แนวคิดสำคัญคือ ใช้ เซต เพื่อตรวจสอบการมีอยู่ในเวลา O(1) และเริ่มนับลำดับจาก องค์ประกอบที่น้อยที่สุด เท่านั้น โดยระบุได้จากการตรวจสอบว่าองค์ประกอบก่อนหน้าไม่มีอยู่ในเซต
def longestConsecutive(nums):
num_set = set(nums)
best = 0
for n in num_set:
if n - 1 not in num_set: # n is the start of a sequence
curr_n = n
length = 1
while curr_n + 1 in num_set:
curr_n += 1
length += 1
best = max(best, length)
return best
print(longestConsecutive([100,4,200,1,3,2])) # 4
print(longestConsecutive([0,3,7,2,5,8,4,6,0,1])) # 9เหตุใดการพิสูจน์ O(n) จึงเป็นจริง
แต่ละจำนวนจะถูกเยี่ยมชมในลูป while ไม่เกินหนึ่งครั้งตลอดการทำงานของลูป for ภายนอกทั้งหมด แม้ว่าจะมีลูป while อยู่ภายในลูป for แต่จำนวนรอบของลูป while ทั้งหมด เมื่อรวมทุกการทำงานของลูปภายนอกจะไม่เกิน n เนื่องจากแต่ละจำนวนจะเป็นค่า «curr_n + 1» ของลำดับได้ไม่เกินหนึ่งลำดับ เหตุผลแบบเฉลี่ยตามการใช้งานนี้ทำให้เวลารวมเป็น O(n) เช่นเดียวกับการวิเคราะห์สแตกแบบโมโนโทนิก
# Demonstrate O(n) total inner iterations
nums = list(range(1000)) # worst case: one long sequence
num_set = set(nums)
inner_iters = 0
for n in num_set:
if n - 1 not in num_set:
curr = n
while curr + 1 in num_set:
curr += 1
inner_iters += 1
print('n =', len(nums), ' total inner iterations =', inner_iters)
# inner_iters = n-1 <= n => O(n)ทางเลือก: แนวทางที่ใช้การเรียงลำดับ
เมื่อเปรียบเทียบกัน แนวทางเรียงลำดับแล้วตรวจสอบใช้เวลา O(n log n): เรียงลำดับอาร์เรย์ ลบค่าซ้ำที่อยู่ติดกัน แล้วนับช่วงของจำนวนต่อเนื่อง แม้จะช้ากว่า แต่ใช้พื้นที่เพิ่มเติม O(1) หากเรียงลำดับภายในอาร์เรย์เดิม ส่วนแนวทางที่ใช้เซตใช้พื้นที่เพิ่มเติม O(n) ในการสัมภาษณ์ ควรกล่าวถึงทั้งสองแนวทางและชี้แจงว่าโซลูชัน O(n log n) ยอมรับได้หรือไม่เมื่อพิจารณาจากข้อจำกัดด้านพื้นที่
def longestConsecutive_sort(nums):
if not nums:
return 0
nums.sort()
best = length = 1
for i in range(1, len(nums)):
if nums[i] == nums[i-1]:
continue # skip duplicates
if nums[i] == nums[i-1] + 1:
length += 1
best = max(best, length)
else:
length = 1
return best
print(longestConsecutive_sort([100,4,200,1,3,2])) # 4แคช LRU คืออะไร
แคช LRU (ใช้งานล่าสุดน้อยที่สุด) คือโครงสร้างข้อมูลที่มีความจุคงที่ เมื่อแคชเต็มและจำเป็นต้องเพิ่มรายการใหม่ แคชจะนำรายการที่ไม่ได้ใช้งานล่าสุดออก การดำเนินการมีดังนี้: get(key) คืนค่าหากมี key อยู่ (และทำเครื่องหมายว่าเป็นรายการที่ใช้งานล่าสุด) หรือคืนค่า -1 หากไม่มี และ put(key, value) เพิ่มคู่ข้อมูล (โดยนำรายการ LRU ออกเมื่อมีข้อมูลเต็มความจุ)
แคช LRU ถูกใช้ในระบบปฏิบัติการ (การแทนที่หน้า) แคชของเบราว์เซอร์ และแคชคำค้นหาฐานข้อมูล LeetCode 146 ให้คุณสร้างการทำงานนี้โดยที่ get และ put ใช้เวลา O(1)
แคช LRU โดยใช้ OrderedDict
collections.OrderedDict ของ Python จะรักษาลำดับการเพิ่มข้อมูลและรองรับ move_to_end(key) (O(1)) เพื่อทำเครื่องหมายว่ารายการเป็นรายการที่ใช้งานล่าสุด เมื่อใช้ put ให้ย้าย key ไปไว้ท้ายสุด และเมื่อมีข้อมูลเกินความจุ ให้นำรายการแรกออก (LRU) วิธีนี้ทำให้ get และ put ใช้เวลา O(1) โดยใช้เครื่องมือในตัวซึ่งภายในทำงานด้วยลิงก์ลิสต์เชื่อมสองทิศทางร่วมกับแฮชแมป
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict LRU (first item)
cache = LRUCache(2)
cache.put(1, 1); cache.put(2, 2)
print(cache.get(1)) # 1 (and 1 becomes most recently used)
cache.put(3, 3) # evict key 2 (LRU)
print(cache.get(2)) # -1
cache.put(4, 4) # evict key 1 (LRU)
print(cache.get(1)) # -1
print(cache.get(3)) # 3
print(cache.get(4)) # 4สร้างแคช LRU ตั้งแต่ต้น: ลิงก์ลิสต์เชื่อมสองทิศทางและ HashMap
การสร้างจากต้นใช้ ลิงก์ลิสต์เชื่อมสองทิศทาง (เพื่อรองรับการนำโหนดออกในเวลา O(1)) และ แฮชแมป (เพื่อค้นหาโหนดด้วย key ในเวลา O(1)) ลิสต์จะรักษาลำดับจาก LRU (head.next) ไปยัง MRU (tail.prev) โหนดเฝ้ายาม head และ tail ที่ไม่มีข้อมูลจริงช่วยตัดกรณีพิเศษสำหรับการเพิ่มและการนำข้อมูลออกที่ปลายทั้งสองด้าน
class DNode:
def __init__(self, key=0, val=0):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCacheDLL:
def __init__(self, capacity):
self.cap = capacity
self.map = {} # key -> DNode
self.head = DNode() # dummy LRU end
self.tail = DNode() # dummy MRU end
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_tail(self, node):
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node
def get(self, key):
if key not in self.map:
return -1
node = self.map[key]
self._remove(node)
self._add_to_tail(node)
return node.val
def put(self, key, val):
if key in self.map:
self._remove(self.map[key])
node = DNode(key, val)
self._add_to_tail(node)
self.map[key] = node
if len(self.map) > self.cap:
lru = self.head.next
self._remove(lru)
del self.map[lru.key]
cache = LRUCacheDLL(2)
cache.put(1,1); cache.put(2,2)
print(cache.get(1)) # 1
cache.put(3,3)
print(cache.get(2)) # -1 (evicted)เหตุใดจึงใช้ลิงก์ลิสต์เชื่อมสองทิศทางสำหรับ LRU
ลิงก์ลิสต์เชื่อมทิศทางเดียวไม่สามารถนำโหนดที่อยู่ตำแหน่งใด ๆ ออกในเวลา O(1) ได้ หากไม่ทราบโหนดก่อนหน้า ลิงก์ลิสต์เชื่อมสองทิศทางจะเก็บตัวชี้ทั้ง prev และ next จึงทำให้สามารถนำข้อมูลออกในเวลา O(1) เมื่อมีการอ้างอิงถึงโหนดนั้น แฮชแมปช่วยให้เข้าถึงโหนดด้วย key ได้ในเวลา O(1) เมื่อใช้ร่วมกัน get(key) จะใช้เวลา O(1) ในการค้นหาโหนดและ O(1) ในการย้ายไปยังท้ายลิสต์ ส่วน put(key) จะใช้เวลา O(1) ในการเพิ่มข้อมูลและ O(1) ในการนำโหนด LRU ออกจากหัวลิสต์
# Why not a singly linked list?
# To remove a node you need its predecessor
# With SLL: must traverse from head to find predecessor => O(n)
# With DLL: node.prev IS the predecessor => O(1) removal
print('SLL removal: O(n) — must find predecessor by traversal')
print('DLL removal: O(1) — node.prev is immediately available')
print('Hash map lookup: O(1) — get DNode reference by key')
print('Combined LRU get/put: O(1) average')แคช LFU (ใช้งานน้อยที่สุด)
รูปแบบที่ยากขึ้นคือแคช LFU (LeetCode 460) ซึ่งจะนำรายการที่มีจำนวนการเข้าถึงน้อยที่สุดออก หากจำนวนเท่ากัน จะใช้ความใหม่ในการเข้าถึงเป็นตัวตัดสิน โดยนำรายการที่ใช้งานล่าสุดน้อยที่สุดในกลุ่มที่มีความถี่ต่ำสุดออก การสร้างต้องใช้โครงสร้างข้อมูลสามอย่าง ได้แก่ แผนที่จาก key ไปยัง value แผนที่จาก key ไปยังความถี่ และแผนที่จากความถี่ไปยัง OrderedDict (เพื่อรักษาลำดับการเพิ่มข้อมูลภายในแต่ละกลุ่มความถี่) การทำงาน get และ put ของ LFU มีเวลาเฉลี่ย O(1)
from collections import defaultdict, OrderedDict
class LFUCache:
def __init__(self, capacity):
self.cap = capacity
self.min_f = 0
self.kv = {} # key -> val
self.kf = {} # key -> freq
self.fk = defaultdict(OrderedDict) # freq -> {key: None}
def _touch(self, key):
f = self.kf[key]
self.kf[key] = f + 1
del self.fk[f][key]
if not self.fk[f] and f == self.min_f:
self.min_f += 1
self.fk[f+1][key] = None
def get(self, key):
if key not in self.kv:
return -1
self._touch(key)
return self.kv[key]
def put(self, key, val):
if self.cap == 0: return
if key in self.kv:
self.kv[key] = val
self._touch(key)
else:
if len(self.kv) == self.cap:
lfu_key, _ = self.fk[self.min_f].popitem(last=False)
del self.kv[lfu_key]; del self.kf[lfu_key]
self.kv[key] = val; self.kf[key] = 1
self.fk[1][key] = None; self.min_f = 1รูปแบบการออกแบบ: แฮชแมปและลิงก์ลิสต์
แคช LRU แสดงให้เห็นรูปแบบการออกแบบที่มีประสิทธิภาพ: ใช้แฮชแมปเพื่อค้นหา key ในเวลา O(1) ร่วมกับลิงก์ลิสต์เพื่อดำเนินการตามลำดับในเวลา O(1) รูปแบบนี้ปรากฏในโจทย์การออกแบบสำหรับการสัมภาษณ์หลายประเภท ได้แก่ แคช LRU แคช LFU ลิสต์แบบข้าม และรูปแบบคิวบางประเภท เมื่อใดก็ตามที่โจทย์ต้องการทั้งการค้นหาในเวลา O(1) และการดำเนินการตามลำดับในเวลา O(1) ให้พิจารณาการใช้โครงสร้างข้อมูลคู่นี้
ในการสัมภาษณ์ การกล่าวถึงรูปแบบนี้อย่างชัดเจนจะแสดงให้เห็นถึงการคิดในระดับระบบและความคุ้นเคยกับการผสานโครงสร้างข้อมูลแบบคลาสสิก
ลำดับจำนวนต่อเนื่องในเมทริกซ์
การต่อยอดแนวคิดลำดับจำนวนต่อเนื่องไปยังข้อมูลสองมิติ: เมื่อกำหนดเมทริกซ์จำนวนเต็มมา ให้หาความยาวของลำดับจำนวนต่อเนื่องที่ยาวที่สุดซึ่งสามารถติดตามได้ โดยแต่ละก้าวจะย้ายไปยังเซลล์ข้างเคียง โจทย์นี้ผสาน BFS/DFS เข้ากับแนวทางใช้เซตสำหรับลำดับจำนวนต่อเนื่อง ให้เก็บตำแหน่งของแต่ละค่าไว้ จากนั้นสำหรับค่าเริ่มต้นแต่ละค่า ให้ตรวจสอบว่าค่า value+1 มีอยู่ในเซลล์ข้างเคียงหรือไม่
# Simpler: find longest consecutive values in a 2D matrix (no adjacency)
def longestConsecutiveMatrix(matrix):
all_vals = set()
for row in matrix:
for v in row:
all_vals.add(v)
best = 0
for v in all_vals:
if v - 1 not in all_vals: # start of sequence
length = 0
while v in all_vals:
v += 1
length += 1
best = max(best, length)
return best
m = [[1, 5, 3], [4, 6, 2], [8, 7, 9]]
print(longestConsecutiveMatrix(m)) # 9 (1..9 all present)สรุปการสัมภาษณ์: พลังของเซตและ HashMap
โจทย์สองข้อนี้มีแนวคิดร่วมกันคือ การเปลี่ยนโจทย์ที่ใช้เวลา O(n log n) หรือ O(n²) ให้เป็น O(n) ด้วยโครงสร้างแฮชที่เหมาะสม ลำดับจำนวนต่อเนื่องที่ยาวที่สุดใช้เซตเพื่อตอบว่า «องค์ประกอบก่อนหน้ามีอยู่หรือไม่» ในเวลา O(1) ส่วนแคช LRU ใช้แฮชแมปเพื่อค้นหาโหนดได้ทันที และใช้ลิงก์ลิสต์เชื่อมสองทิศทางเพื่อปรับลำดับในเวลา O(1) ทั้งสองแนวทางแทนที่การไล่ตรวจสอบที่ช้าด้วยการตรวจสอบการมีอยู่หรือการค้นหาในเวลา O(1)
เมื่อผู้สัมภาษณ์ถามว่า «ทำให้ดีกว่า O(n log n) ได้หรือไม่» คำตอบมักจะเป็น «ใช้แฮชแมปหรือแฮชเซตเพื่อหลีกเลี่ยงการเรียงลำดับ»
แบบทดสอบสั้น ๆ
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า ลำดับจำนวนต่อเนื่องที่ยาวที่สุดสามารถทำงานในเวลา O(n) โดยใช้เซตเพื่อตรวจสอบการมีอยู่ในเวลา O(1) และเริ่มนับเฉพาะจากจุดเริ่มต้นของลำดับ แคช LRU ทำงาน get และ put ในเวลา O(1) โดยใช้ OrderedDict (หรือใช้แฮชแมปร่วมกับลิงก์ลิสต์เชื่อมสองทิศทางที่สร้างขึ้นเอง) และ รูปแบบแฮชแมปร่วมกับลิงก์ลิสต์เป็นองค์ประกอบพื้นฐานที่นำกลับมาใช้ซ้ำได้สำหรับโครงสร้างข้อมูลที่ไวต่อการเรียงลำดับและทำงานในเวลา O(1) ต่อไปเราจะกลับมาทบทวนการเรียกซ้ำด้วยกรอบการทำงานกรณีฐาน ความไว้วางใจ และการสร้างคำตอบ
คำถามที่พบบ่อย
บทเรียน “ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU”
แก้โจทย์ลำดับต่อเนื่องที่ยาวที่สุดในเวลา O(n) ด้วยเซต แล้วออกแบบแคช LRU โดยใช้ OrderedDict คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ภายในฟังก์ชันแฮชและการจัดการการชนกัน
- ผลรวมสองค่าและรูปแบบหลากหลาย
- การนับความถี่และการจัดกลุ่ม
- ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU