0Pricing
Coding Interview Prep · درس

اكتشاف الدورات باستخدام خوارزمية Floyd

اكتشف الدورات باستخدام أسلوب المؤشرين البطيء والسريع، واعثر على نقطة دخول الدورة، وأثبت صحة الخوارزمية رياضيًا

اكتشاف الدورات باستخدام خوارزمية Floyd درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ما الدورة في القائمة المترابطة؟

تحدث دورة في القائمة المترابطة عندما يشير مؤشر next لإحدى العقد إلى عقدة تمت زيارتها سابقًا، مما ينشئ حلقة لا نهائية. وسيستمر اجتياز قائمة من هذا النوع باستخدام حلقة while head إلى الأبد. يُعد اكتشاف الدورات مسألة كلاسيكية في المقابلات وأساسًا لخوارزميات المؤشرات الأكثر تقدمًا.

يخزّن النهج الساذج كل عقدة تمت زيارتها في مجموعة ويتحقق من وجودها فيها — في زمن O(n) ومساحة O(n). وتحل خوارزمية Floyd المشكلة نفسها في زمن O(n) ومساحة O(1)، وهو ما يتوقعه المحاورون.

خوارزمية Floyd للمؤشرين البطيء والسريع

تستخدم خوارزمية Floyd لاكتشاف الدورات (المعروفة باسم «السلحفاة والأرنب») مؤشرين: يتقدم slow خطوة واحدة في كل مرة، بينما يتقدم fast خطوتين. إذا لم توجد دورة، يصل fast إلى None أولًا. أما إذا وجدت دورة، فسوف يلحق fast بـ slow في النهاية داخل الدورة، ويلتقيان عند العقدة نفسها. ويثبت هذا الالتقاء وجود دورة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def hasCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False

# Build: 3 -> 2 -> 0 -> -4 -> (back to 2)
nodes = [ListNode(v) for v in [3, 2, 0, -4]]
for i in range(3):
    nodes[i].next = nodes[i+1]
nodes[3].next = nodes[1]   # cycle: -4 -> 2

print(hasCycle(nodes[0]))  # True

لماذا يلتقي المؤشران البطيء والسريع دائمًا؟

بصورة غير رسمية: بعد دخول المؤشرين إلى الدورة، يتغير الفارق بينهما بمقدار 1 في كل خطوة (إذ يتقدم fast بمقدار 2 وslow بمقدار 1، ولذلك ينغلق الفارق بمقدار 1 في كل جولة). وفي صياغة أكثر دقة، إذا كان طول الدورة C، فإن أكبر فارق داخلها هو C-1، وينغلق الفارق بمقدار 1 في كل خطوة؛ لذلك يلتقيان خلال C خطوات بعد دخولهما الدورة.

إجمالي الخطوات قبل الالتقاء: على الأكثر O(n + C) = O(n)، لأن C <= n.

# Visualise convergence: simulate gap in cycle
cycle_length = 5
for start_gap in range(1, cycle_length + 1):
    gap = start_gap
    steps = 0
    while gap != 0:
        gap = (gap - 1) % cycle_length
        steps += 1
    print(f'Start gap {start_gap}: meet after {steps} step(s)')

العثور على نقطة دخول الدورة

بعد اكتشاف دورة، يمكن لخوارزمية Floyd أيضًا العثور على عقدة الدخول، أي العقدة التي تبدأ عندها الدورة. بعد أن يلتقي slow وfast داخل الدورة، أعد أحد المؤشرين إلى الرأس وأبقِ الآخر عند نقطة الالتقاء. ثم حرّك كليهما خطوة واحدة في كل مرة. سيلتقيان بالضبط عند عقدة دخول الدورة. تنجح هذه الطريقة لأن المسافة من الرأس إلى نقطة الدخول تساوي المسافة من نقطة الالتقاء إلى نقطة الدخول (بترديد طول الدورة).

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def detectCycle(head):
    slow = fast = head
    # Phase 1: detect meeting point
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    else:
        return None  # no cycle
    # Phase 2: find entry
    pointer = head
    while pointer is not slow:
        pointer = pointer.next
        slow    = slow.next
    return pointer  # cycle entry node

nodes = [ListNode(v) for v in [3, 2, 0, -4]]
for i in range(3):
    nodes[i].next = nodes[i+1]
nodes[3].next = nodes[1]  # entry is nodes[1] (val=2)

entry = detectCycle(nodes[0])
print(entry.val)  # 2

الإثبات الرياضي لعقدة الدخول

لنفرض أن F = المسافة من الرأس إلى نقطة دخول الدورة، وأن C = طول الدورة، وأن a = المسافة من نقطة الدخول إلى نقطة الالتقاء داخل الدورة. عند الالتقاء يكون slow قد قطع F + a خطوات، بينما يكون fast قد قطع F + a + n*C خطوات (أي متقدمًا بعدد n من الدورات الكاملة). وبما أن fast = 2 * slow: ‏2(F+a) = F+a+nC → F = nC - a. وهذا يعني أن المسافة من الرأس إلى نقطة الدخول تساوي المسافة من نقطة الالتقاء إلى نقطة الدخول (بترديد C). وعند إعادة أحد المؤشرين إلى الرأس وتحريك كليهما بمقدار 1، سيلتقيان عند عقدة الدخول.

# Verify with our example: F=1 (head to node 2), C=3 (cycle: 2->0->-4->2), a=?
# Meeting inside cycle after F+a slow steps
# Let us measure a by counting from entry to meeting point
# In practice the code handles this automatically
F = 1   # head(3) to entry(2)
C = 3   # cycle length 2->0->-4
# n=1: F = 1*C - a => a = C - F = 3 - 1 = 2
a = C - F
print(f'F={F}, C={C}, a={a}')
print(f'After meeting, {F} more steps reach entry: {F == C - a or F % C == (C - a) % C}')

قياس طول الدورة

بمجرد حصولكم على نقطة الالتقاء داخل الدورة (المرحلة الأولى من خوارزمية Floyd)، يمكنكم قياس طول الدورة: أبقوا أحد المؤشرين ثابتًا وحرّكوا الآخر حتى يلتقيا مجددًا. يساوي عدد الخطوات المقطوعة طول الدورة. ويفيد ذلك في المسائل التي تطلب طول الدورة صراحةً.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def cycle_length(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:  # found meeting point
            length = 1
            fast = fast.next
            while fast is not slow:
                fast = fast.next
                length += 1
            return length
    return 0  # no cycle

nodes = [ListNode(v) for v in [1, 2, 3, 4, 5]]
for i in range(4):
    nodes[i].next = nodes[i+1]
nodes[4].next = nodes[2]  # cycle: 3->4->5->3, length=3
print(cycle_length(nodes[0]))  # 3

العدد السعيد (اكتشاف الدورة من دون قائمة)

لا تقتصر خوارزمية Floyd على القوائم المترابطة. تسأل مسألة LeetCode 202 «العدد السعيد» عما إذا كان استبدال n مرارًا بمجموع مربعات أرقامه سيؤدي في النهاية إلى الوصول إلى 1. فإذا دخل n في دورة لا تتضمن 1، فسوف يستمر في الدوران إلى الأبد. يمكنكم نمذجة ذلك على أنه اجتياز قائمة مترابطة افتراضية، حيث تكون قيمة 'next' لكل عقدة هي القيمة المحسوبة التالية، ثم تطبيق خوارزمية Floyd لاكتشاف الدورة.

def isHappy(n):
    def next_val(x):
        total = 0
        while x:
            x, d = divmod(x, 10)
            total += d * d
        return total

    slow, fast = n, next_val(n)
    while fast != 1 and slow != fast:
        slow = next_val(slow)
        fast = next_val(next_val(fast))
    return fast == 1

print(isHappy(19))  # True  (1->81+1=82->68->100->1)
print(isHappy(2))   # False (enters cycle)

مقارنة الاكتشاف الساذج باستخدام مجموعة بخوارزمية Floyd

يخزّن النهج القائم على المجموعة كل عقدة تمت زيارتها، ويتحقق من وجودها قبل زيارتها. ويتميز بتعقيد زمني O(n) ومساحة O(n). أما خوارزمية Floyd فتتميز أيضًا بزمن O(n)، لكنها تستخدم مساحة O(1) فقط، من دون أي بنية بيانات إضافية. وفي البيئات المقيدة من حيث الذاكرة، مثل الأنظمة المضمّنة ونوى أنظمة التشغيل، يكون ضمان استخدام مساحة O(1) مهمًا. وقد يطلب المحاورون صراحةً استخدام مساحة O(1) كسؤال متابعة بعد تقديم حل المجموعة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Naive O(n) space approach
def hasCycle_set(head):
    seen = set()
    while head:
        if id(head) in seen:
            return True
        seen.add(id(head))
        head = head.next
    return False

# Floyd's O(1) space approach
def hasCycle_floyd(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False

print('Both implementations give the same result')

الحالات الطرفية لاكتشاف الدورات

هناك ثلاث حالات طرفية ينبغي التعامل معها. أولًا، القائمة الفارغة: head is None — يخرج شرط حلقة Floyd، fast and fast.next، فورًا ويعيد False. ثانيًا، عقدة واحدة من دون دورة: تكون قيمة fast.next هي None، فتخرج الحلقة وتعيد False. ثالثًا، عقدة واحدة مع دورة: يشير next للعقدة إلى نفسها؛ يبدأ slow وfast عند head، وبعد خطوة واحدة ينتقل fast إلى head.next.next = head، بينما يكون slow عند head.next = head. وعندها يصبح fast == slow في أول تكرار مباشرة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def hasCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False

# Edge cases
print(hasCycle(None))               # False: empty
node = ListNode(1)
print(hasCycle(node))               # False: single, no cycle
node.next = node
print(hasCycle(node))               # True: single node cycle

دورة القائمة المترابطة II: LeetCode 142

تطلب مسألة LeetCode 142 «دورة القائمة المترابطة II» العثور على العقدة التي تبدأ عندها الدورة (أو None إذا لم توجد دورة). وهذا تطبيق مباشر لخوارزمية Floyd ذات المرحلتين. يطرح المحاورون هذه المسألة كسؤال متابعة لاكتشاف الدورة الأساسي. يتكوّن الحل الكامل من الآتي: تحدد المرحلة الأولى نقطة الالتقاء داخل الدورة؛ ثم تعيد المرحلة الثانية أحد المؤشرين إلى الرأس وتحرك كليهما إلى الأمام حتى يلتقيا، وتكون نقطة الالتقاء هذه هي نقطة دخول الدورة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def detectCycle(head):
    slow = fast = head
    # Phase 1
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    else:
        return None
    # Phase 2
    ptr = head
    while ptr is not slow:
        ptr  = ptr.next
        slow = slow.next
    return ptr

nodes = [ListNode(v) for v in [1, 2, 3, 4, 5]]
for i in range(4):
    nodes[i].next = nodes[i+1]
nodes[4].next = nodes[2]  # cycle entry: node with val=3
entry = detectCycle(nodes[0])
print(entry.val)  # 3

لماذا تتفوق خوارزمية Floyd على نهج المجموعة

مع أن كلا النهجين له تعقيد زمني O(n)، فإن الثابت العملي يختلف بينهما. إذ يتعين على نهج المجموعة إجراء تجزئة لمؤشر كل عقدة (حساب قيمة التجزئة، وفحص جدول التجزئة، وتخزين المؤشر)، بينما لا تنفذ خوارزمية Floyd سوى عمليات الوصول إلى المؤشرات، وهي أقل تكلفة بكثير في كل خطوة. والأهم من ذلك أن ضمان استخدام مساحة O(1) يعني أن خوارزمية Floyd يمكنها العمل على قوائم طويلة بأي طول من دون خطر نفاد الذاكرة.

إن ذكر ميزة المساحة هذه استباقيًا في المقابلة يدل على فهم عميق للمفاضلات الخوارزمية التي تتجاوز تدوين Big-O الأساسي.

اختبار سريع

اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتم أن: خوارزمية Floyd للمؤشرين البطيء والسريع تكتشف الدورات في زمن O(n) ومساحة O(1)، والمرحلة الثانية (إعادة أحد المؤشرين إلى الرأس وتحريك كليهما بمقدار 1) تعثر على عقدة دخول الدورة بدقة، وتنطبق التقنية نفسها خارج القوائم المترابطة على أي تسلسل ضمني تكون فيه 'next' دالةً. في الدرس التالي سنغطي دمج القوائم المرتبة، وتقسيم القوائم عند نقاط المنتصف، والعثور على العقدة رقم n من النهاية.

الأسئلة الشائعة

هل درس «اكتشاف الدورات باستخدام خوارزمية Floyd» مجاني؟

نعم — نص درس «اكتشاف الدورات باستخدام خوارزمية Floyd» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «اكتشاف الدورات باستخدام خوارزمية Floyd»؟

اكتشف الدورات باستخدام أسلوب المؤشرين البطيء والسريع، واعثر على نقطة دخول الدورة، وأثبت صحة الخوارزمية رياضيًا تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «اكتشاف الدورات باستخدام خوارزمية Floyd»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. فئة Node وإنشاء القوائم
  2. عكس قائمة مرتبطة
  3. اكتشاف الدورات باستخدام خوارزمية Floyd
  4. الدمج والتقسيم والعثور على العنصر N من النهاية
← العودة إلى Coding Interview Prep