أطول تسلسل متتالٍ وذاكرة LRU المؤقتة
حل longest-consecutive-sequence في O(n) باستخدام set، ثم صمّم ذاكرة LRU مؤقتة باستخدام OrderedDict
أطول تسلسل متتالٍ وذاكرة LRU المؤقتة درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
مسألة أطول متتالية متصلة
LeetCode 128 «أطول متتالية متصلة»: عند إعطائكم مصفوفة غير مرتبة، أوجدوا طول أطول متتالية من الأعداد الصحيحة المتصلة. على سبيل المثال، تحتوي [100,4,200,1,3,2] على المتتالية المتصلة [1,2,3,4] التي يبلغ طولها 4. يكمن التحدي في حلها بتعقيد O(n) بدلًا من O(n log n)، وهو التعقيد الناتج عن الفرز ثم الفحص.
الفكرة الأساسية هي استخدام set لاختبارات العضوية بزمن 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) إذا تم الفرز داخل المصفوفة نفسها. أما أسلوب set فيستخدم مساحة إضافية قدرها 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) القيمة إذا كان المفتاح موجودًا، وتضعه بوصفه مستخدمًا مؤخرًا، أو تعيد -1 إذا لم يكن موجودًا؛ أما put(key, value) فتُدرج الزوج، وتُخرج عنصر LRU إذا كانت الذاكرة ممتلئة.
تُستخدم ذاكرات LRU المؤقتة في أنظمة التشغيل (استبدال الصفحات)، وذاكرات المتصفح المؤقتة، وذاكرات التخزين المؤقت لاستعلامات قواعد البيانات. تطلب مسألة LeetCode 146 تنفيذ واحدة منها مع جعل عمليتي get وput بزمن O(1).
ذاكرة LRU المؤقتة باستخدام OrderedDict
تحافظ Python's collections.OrderedDict على ترتيب الإدراج، وتدعم move_to_end(key) بزمن O(1) لوضع عنصر بوصفه الأكثر استخدامًا مؤخرًا. عند تنفيذ put، انقلوا المفتاح إلى النهاية؛ وعند تجاوز السعة، أزيلوا العنصر الأول (وهو 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)) وجدول تجزئة (للعثور على العقدة حسب المفتاح بزمن O(1)). وتحافظ القائمة على الترتيب من LRU (head.next) إلى MRU (tail.prev). وتزيل عقدتا الرأس والذيل الوهميتان بوصفهما حارسين الحالات الخاصة عند الإدراج والإزالة من الطرفين.
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) عند توفر مرجع العقدة. وتوفّر خريطة التجزئة وصولًا بزمن 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)، حيث يُخرج العنصر ذو أصغر عدد من مرات الوصول. وتُحسم حالات التعادل حسب حداثة الاستخدام، أي يُخرج الأقل استخدامًا مؤخرًا من بين العناصر الأقل تكرارًا. يتطلب التنفيذ ثلاث بنى بيانات: خريطة من المفتاح إلى القيمة، وخريطة من المفتاح إلى التكرار، وخريطة من التكرار إلى 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 المؤقتة نمط تصميم قويًا: دمج جدول تجزئة للعثور على المفتاح بزمن O(1) مع قائمة مترابطة لتنفيذ العمليات المرتبة بزمن O(1). يظهر هذا النمط في عدة مسائل لتصميم المقابلات، منها ذاكرة LRU المؤقتة، وذاكرة LFU المؤقتة، والقوائم المتخطية، وبعض أنواع الطوابير. كلما تطلبت المسألة بحثًا بزمن O(1) وعمليات تعتمد على الترتيب بزمن O(1)، ففكّروا في هذا الدمج.
إن التصريح بهذا النمط صراحةً في المقابلات يبرهن على التفكير على مستوى الأنظمة والإلمام بتركيبات هياكل البيانات الكلاسيكية.
المتتالية المتصلة في مصفوفة
تمديد لفكرة المتتالية المتصلة إلى بُعدين: عند إعطائكم مصفوفة من الأعداد الصحيحة، أوجدوا طول أطول متتالية متصلة يمكن تتبعها، بحيث تنتقل كل خطوة إلى خلية مجاورة. يجمع ذلك بين BFS/DFS ونهج set الخاص بالمتتاليات المتصلة. خزّنوا موضع كل قيمة، ثم تحقّقوا لكل قيمة بداية مما إذا كانت 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)ملخص المقابلة: قوة Set + HashMap
تشترك هاتان المسألتان في فكرة واحدة: تحويل مسائل O(n log n) أو O(n²) إلى O(n) باستخدام بنية التجزئة المناسبة. تستخدم مسألة أطول متتالية متصلة مجموعة set للإجابة عن سؤال «هل العنصر السابق موجود؟» بزمن O(1). أما ذاكرة LRU المؤقتة فتستخدم جدول تجزئة للعثور على العقدة فورًا، وقائمة مترابطة مزدوجة لتحديث الترتيب بزمن O(1). ويستبدل كلاهما الاجتياز البطيء بالتحقق من العضوية أو البحث بزمن O(1).
عندما يسألكم المحاور «هل يمكنكم تقديم حل أفضل من O(n log n)؟»، فالإجابة غالبًا هي «استخدموا جدول تجزئة أو مجموعة تجزئة لتجنب الفرز».
تحقق سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.
ملخص الدرس
تعلمتم في هذا الدرس أن: أطول متتالية متصلة تعمل بتعقيد O(n) باستخدام set للتحقق من العضوية بزمن O(1)، والبدء في العد من بدايات المتتاليات فقط، وأن ذاكرة LRU المؤقتة تحقق عمليتي get وput بزمن O(1) باستخدام OrderedDict، أو باستخدام جدول تجزئة + قائمة مترابطة مزدوجة عند بنائها من الصفر، وأن نمط جدول التجزئة + القائمة المترابطة يمثل لبنة قابلة لإعادة الاستخدام لبنى البيانات الحساسة للترتيب والتي تعمل بزمن O(1). بعد ذلك، سنعود إلى الاستدعاء التكراري مع إطار الحالة الأساسية والثقة والبناء.
الأسئلة الشائعة
هل درس «أطول تسلسل متتالٍ وذاكرة LRU المؤقتة» مجاني؟
نعم — نص درس «أطول تسلسل متتالٍ وذاكرة LRU المؤقتة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «أطول تسلسل متتالٍ وذاكرة LRU المؤقتة»؟
حل longest-consecutive-sequence في O(n) باستخدام set، ثم صمّم ذاكرة LRU مؤقتة باستخدام OrderedDict تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «أطول تسلسل متتالٍ وذاكرة LRU المؤقتة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- آليات دوال التجزئة ومعالجة التصادمات
- Two-Sum ومتغيراته العديدة
- عدّ التكرارات والتجميع
- أطول تسلسل متتالٍ وذاكرة LRU المؤقتة