0Pricing
DSA Interview Prep · درس

قطر الأشجار وارتفاعها وتوازنها

احسب قطر الشجرة وارتفاعها في مرور DFS واحد باستخدام مساعد يعيد القيمتين، ثم تحقق مما إذا كانت الشجرة متوازنة الارتفاع

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

ارتفاع الشجرة الثنائية

يمثل الارتفاع، أو أقصى عمق، للشجرة الثنائية طول أطول مسار من الجذر إلى أي عقدة ورقية. ويُحسب تعاوديًا؛ إذ يساوي ارتفاع أي عقدة 1 + max(height(left), height(right))، مع حالة أساسية قيمتها 0 للعقد الفارغة. ويُعد هذا الحساب وفق ترتيب من الأسفل إلى الأعلى أساسيًا، فالارتفاع هو اللبنة التي يُبنى عليها حساب القطر، والتحقق من التوازن، ودورانات شجرة AVL.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def height(root):
    if not root:
        return 0
    return 1 + max(height(root.left), height(root.right))

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.left.left.left = TreeNode(6)
print(height(root))  # 4

القطر: أطول مسار

يمثل قطر الشجرة الثنائية طول أطول مسار بين أي عقدتين، وقد يمر المسار عبر الجذر أو لا يمر به. ويُقاس طول المسار بعدد الحواف. بالنسبة إلى أي عقدة، يساوي القطر المار بها height(left) + height(right). أما القطر الكلي فهو أكبر قيمة من هذه القيم عبر جميع عقد الشجرة.

def diameter_of_binary_tree(root):
    max_diameter = [0]  # use list to allow closure mutation

    def dfs(node):
        if not node:
            return 0
        left_h = dfs(node.left)
        right_h = dfs(node.right)
        # Diameter through this node
        max_diameter[0] = max(max_diameter[0], left_h + right_h)
        return 1 + max(left_h, right_h)  # height for parent

    dfs(root)
    return max_diameter[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(diameter_of_binary_tree(root))  # 3

مرور DFS واحد لحساب القطر

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

# O(n^2) NAIVE: recomputes height for every node
def diameter_naive(root):
    if not root:
        return 0
    through_root = height(root.left) + height(root.right)
    in_left = diameter_naive(root.left)
    in_right = diameter_naive(root.right)
    return max(through_root, in_left, in_right)

# O(n) OPTIMAL: single DFS pass (shown in previous scene)
# The naive version is O(n^2) because height() is O(n)
# and it is called for every node.
print('Naive: O(n^2) | Optimal single-pass: O(n)')

التحقق من توازن الشجرة الثنائية

تكون الشجرة الثنائية متوازنة من حيث الارتفاع إذا اختلف ارتفاعا الشجرتين الفرعيتين اليسرى واليمنى لكل عقدة بمقدار لا يتجاوز واحدًا. يستدعي الأسلوب بالقوة الغاشمة height() عند كل عقدة، مما يؤدي إلى O(n²). أما الأسلوب الأمثل فيستخدم الحيلة نفسها ذات المرور الواحد: أعيدوا -1 كقيمة حارسة للدلالة على «غير متوازنة»، ومرروها إلى الأعلى، مع إيقاف المعالجة مبكرًا فور اكتشاف عدم توازن أي عقدة.

def is_balanced(root):
    def check(node):
        if not node:
            return 0
        left = check(node.left)
        if left == -1:
            return -1  # propagate early exit
        right = check(node.right)
        if right == -1:
            return -1
        if abs(left - right) > 1:
            return -1  # unbalanced here
        return 1 + max(left, right)  # height if balanced

    return check(root) != -1

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.left.left = TreeNode(5)  # too deep on left
print(is_balanced(root))  # False

نمط القيمة الحارسة المُعادة

تُعد إعادة قيمة حارسة، مثل -1 لعدم التوازن أو tuple خاصة، نمطًا شائعًا عندما تحتاج دالة مساعدة في DFS إلى الإشارة إلى نوعين من المعلومات: النتيجة المحسوبة وما إذا كان أحد القيود قد انتُهك. وبدلًا من رفع الاستثناءات أو استخدام علامات عامة، يمكنكم ترميز الخطأ في نوع القيمة المُعادة. يتميز هذا الأسلوب بالوضوح، ويتجنب الحالة العامة، ويتكامل طبيعيًا مع الدوال المساعدة التعاودية الأخرى.

# General pattern: return (is_valid, computed_value)
def balanced_height(node):
    if not node:
        return True, 0
    left_ok, left_h = balanced_height(node.left)
    if not left_ok:
        return False, 0  # short-circuit
    right_ok, right_h = balanced_height(node.right)
    if not right_ok:
        return False, 0
    balanced = abs(left_h - right_h) <= 1
    return balanced, 1 + max(left_h, right_h)

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
ok, h = balanced_height(root)
print(ok, h)  # True 2

القطر بحسب العقد والحواف

انتبهوا إلى نص المسألة: يقيس LeetCode #543 القطر بعدد الحواف، بينما تقيس بعض المسائل القطر بعدد العقد. إذا كنتم بحاجة إلى عدد العقد، فإن القطر المار بعقدة يساوي height(left) + height(right) + 1، إذ أضيفوا 1 للعقدة نفسها. أما إذا كنتم بحاجة إلى عدد الحواف، فاحذفوا ‎+1. احرصوا دائمًا على توضيح ذلك مع المحاور قبل كتابة الشيفرة.

def diameter_in_nodes(root):
    max_path = [0]

    def dfs(node):
        if not node:
            return 0
        left_h = dfs(node.left)
        right_h = dfs(node.right)
        # Path through this node in NODE count
        nodes_through = left_h + right_h + 1
        max_path[0] = max(max_path[0], nodes_through)
        return 1 + max(left_h, right_h)

    dfs(root)
    return max_path[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(diameter_in_nodes(root))  # 4 nodes: 4-2-1-3 or 5-2-1-3

مجموع المسار: أي مسار من الجذر إلى ورقة

تطرح مسألة مجموع المسار السؤال الآتي: هل يساوي مجموع أي مسار من الجذر إلى ورقة قيمة مستهدفة؟ استخدموا DFS واطرحوا قيمة العقدة الحالية من القيمة المستهدفة أثناء النزول. وعند الوصول إلى ورقة، تحققوا مما إذا كانت القيمة المستهدفة المتبقية تساوي قيمة الورقة. هذا اجتياز DFS تمهيدي تمررون فيه المجموع المتبقي بوصفه وسيطًا، وهو مثال كلاسيكي على التعاود من الأعلى إلى الأسفل.

def has_path_sum(root, target):
    if not root:
        return False
    # Leaf node: check if we've exactly hit the target
    if not root.left and not root.right:
        return root.val == target
    remaining = target - root.val
    return (has_path_sum(root.left, remaining) or
            has_path_sum(root.right, remaining))

root = TreeNode(5)
root.left = TreeNode(4)
root.right = TreeNode(8)
root.left.left = TreeNode(11)
root.left.left.left = TreeNode(7)
root.left.left.right = TreeNode(2)
print(has_path_sum(root, 22))  # True: 5+4+11+2=22

أقصى مجموع لمسار (الحالة الصعبة)

تُعد مسألة أقصى مجموع لمسار (LeetCode #124) أصعب بكثير؛ إذ يمكن أن يبدأ المسار وينتهي عند أي عقدة، وليس بالضرورة من الجذر إلى ورقة، كما يمكن أن تكون القيم سالبة. عند كل عقدة، ضعوا أربعة خيارات في الحسبان: العقدة وحدها، أو العقدة + الفرع الأيسر، أو العقدة + الفرع الأيمن، أو العقدة + كلا الفرعين. يمكن للخيارات الثلاثة الأولى فقط أن تمتد إلى الأب، أما الخيار الرابع فهو مرشح نهائي لأقصى قيمة عامة.

def max_path_sum(root):
    max_sum = [float('-inf')]

    def gain(node):
        if not node:
            return 0
        # Only take positive contributions
        left = max(gain(node.left), 0)
        right = max(gain(node.right), 0)
        # Best path through this node (can't go both ways upward)
        max_sum[0] = max(max_sum[0], node.val + left + right)
        # Return the best single-branch gain for parent
        return node.val + max(left, right)

    gain(root)
    return max_sum[0]

root = TreeNode(-10)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
print(max_path_sum(root))  # 42: 15+20+7

أشجار AVL والتوازن الذاتي

شجرة AVL هي شجرة BST تحافظ على خاصية التوازن من حيث الارتفاع عبر إجراء دورانات بعد عمليتي الإدراج والحذف. تخزن كل عقدة عامل توازن، وهو ‎height(right) - height(left)، ويجب أن يبقى ضمن {-1, 0, 1}. عند حدوث انتهاك، تعيد دورة مفردة أو مزدوجة التوازن في زمن O(1)، فتحافظ على الارتفاع الكلي عند O(log n)، وتضمن أن تكون جميع العمليات عند O(log n).

# Balance factor = height(right) - height(left)
# AVL invariant: balance factor in {-1, 0, 1} for every node

# Four violation types and their fixes:
# LL (left-heavy left child): single right rotation
# RR (right-heavy right child): single left rotation
# LR (right-heavy left child): left rotate child, then right rotate root
# RL (left-heavy right child): right rotate child, then left rotate root

# Knowing this is enough for interviews; you rarely implement
# full AVL in an interview but must discuss the concept.
print('AVL maintains O(log n) height via rotations')

التحقق من تماثل الشجرة

تكون الشجرة الثنائية متماثلة إذا كانت صورة مرآة لنفسها. تحققوا من ذلك تعاوُديًا: تكون الشجرة متماثلة إذا كانت لكل زوج من العقد المتناظرة على جانبي المحور قيم متساوية، وكانت شجرتاهما الفرعيتان صورتين مرآتين إحداهما للأخرى. عرّفوا دالة مساعدة هي is_mirror(left, right) تتحقق من الحالات الآتية: كلتاهما فارغتان، وهذا صحيح؛ أو إحداهما فارغة، وهذا غير صحيح؛ أو تساوي القيم وتماثل الشجرتين الفرعيتين الداخلية والخارجية.

def is_symmetric(root):
    def is_mirror(left, right):
        if not left and not right:
            return True
        if not left or not right:
            return False
        return (left.val == right.val and
                is_mirror(left.left, right.right) and
                is_mirror(left.right, right.left))

    return is_mirror(root.left, root.right)

sym = TreeNode(1)
sym.left = TreeNode(2)
sym.right = TreeNode(2)
sym.left.left = TreeNode(3)
sym.right.right = TreeNode(3)
print(is_symmetric(sym))  # True

nosym = TreeNode(1)
nosym.left = TreeNode(2)
nosym.right = TreeNode(2)
nosym.left.right = TreeNode(3)
print(is_symmetric(nosym))  # False

دمج مفهومي الارتفاع والقطر

إن نمط ما بعد الترتيب بمرور واحد، حيث تعيد دالة مساعدة الارتفاع وتحدّث نتيجة عامة في الوقت نفسه، قابل لإعادة الاستخدام في مسائل عديدة: القطر، وأقصى مجموع لمسار، والتحقق من التوازن، وعدّ العقد الجيدة، وغير ذلك. اسألوا دائمًا: «ما المعلومات التي تحتاج إليها العقدة الأب من كل عقدة ابنة؟» تلك هي القيمة المُعادة. و«ما العملية الحسابية المحلية لهذه العقدة؟» تلك هي التي تحدّث الإجابة العامة. هذا التفكيك هو المهارة الأساسية لحل مسائل الأشجار الصعبة.

# Reusable template for post-order dual-purpose DFS:
def tree_problem(root):
    result = [float('-inf')]  # or 0 depending on problem

    def dfs(node):
        if not node:
            return 0  # base return (height, count, etc.)
        left_val = dfs(node.left)
        right_val = dfs(node.right)
        # --- Update global result using both children ---
        candidate = left_val + right_val  # example: diameter
        result[0] = max(result[0], candidate)
        # --- Return info needed by PARENT ---
        return 1 + max(left_val, right_val)  # example: height

    dfs(root)
    return result[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
print(tree_problem(root))  # diameter = 2

تحقق سريع

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

مراجعة الدرس

تعلّمتم في هذا الدرس: حساب الارتفاع باستخدام DFS عودي بترتيب ما بعد الترتيب، وحساب القطر في مرور واحد بزمن O(n) باستخدام دالة مساعدة لـ DFS ذات غرضين، والتحقق من التوازن باستخدام قيمة خاصة للخروج المبكر. بعد ذلك سنتناول مسائل مجموع المسارات وأدنى سلف مشترك.

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

هل درس «قطر الأشجار وارتفاعها وتوازنها» مجاني؟

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

ماذا ستتعلم في «قطر الأشجار وارتفاعها وتوازنها»؟

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

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

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

كم من الوقت يستغرق درس «قطر الأشجار وارتفاعها وتوازنها»؟

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

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

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

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

  1. فئة TreeNode واجتياز BFS حسب المستويات
  2. DFS بالترتيب الوسطي والقبلي والبعدي
  3. قطر الأشجار وارتفاعها وتوازنها
  4. مجموع المسار والسلف المشترك الأدنى
← العودة إلى DSA Interview Prep