Boundary Traversal of Binary Tree
تحتوي المصفوفة root على شجرة ثنائية بترتيب المستويات: الجذر عند الفهرس 0، وابنا الفهرس i عند 2i+1 (الأيسر) و2i+2 (الأيمن)، وتشير القيمة -1 إلى موضع فارغ، وقد توجد قيم -1 إضافية في النهاية. الورقة هي عقدة ليس لها أبناء.
أعِد حدود الشجرة عكس اتجاه عقارب الساعة بدءًا من الجذر:
- الجذر.
- الحد الأيسر، من الأعلى إلى الأسفل: بدءًا من الابن الأيسر للجذر، انتقل إلى الابن الأيسر إن وُجد، وإلا فإلى الابن الأيمن. توقّف قبل الوصول إلى ورقة.
- كل الأوراق، من اليسار إلى اليمين.
- الحد الأيمن، من الأسفل إلى الأعلى: وهو صورة معكوسة للحد الأيسر، بدءًا من الابن الأيمن للجذر.
لا تُدرج أي عقدة مرتين، لذا فإن الشجرة المكوّنة من عقدة واحدة تعطي جذرها فقط.
الدالة
- rootinteger-array
- الشجرة الثنائية بترتيب المستويات، مع استخدام -1 للدلالة على موضع فارغ
- تُرجعinteger-array
- القيم الحدّية، عكس اتجاه عقارب الساعة انطلاقًا من الجذر
القيود
1 ≤ root.length ≤ 32767- كل
root[i]إما-1أو قيمة تحقق0 ≤ root[i] ≤ 105. root[0]لا تكون أبدًا-1، لذا تحتوي الشجرة على عقدة واحدة على الأقل.- قد تنتهي المصفوفة بعناصر إضافية
-1بعد العقدة الأخيرة. - كلا الابنين في الموضع الفارغ فارغان أيضًا، والعمق لا يتجاوز
14. - قيم العقد متميزة.
أمثلة
- المدخلات
- root = [7, 3, 9, 1, 5, 8, -1, -1, -1, 4, 6]
- المخرجات
- [7, 3, 1, 4, 6, 8, 9]
- الشرح
- بعد الجذر
7، الحد الأيسر هو3: ابنه1ورقة. الأوراق هي1،4،6،8. الحد الأيمن هو9، فوق الورقة8.
- المدخلات
- root = [10, 4, -1, -1, 6, -1, -1, -1, -1, 5, 8]
- المخرجات
- [10, 4, 6, 5, 8]
- الشرح
- ليس لـ
4ابن أيسر، لذا يتجه الحد الأيسر إلى6، ثم يتوقف عند الورقة5. وتأتي الورقتان5و8بعد ذلك. وليس للجذر ابن أيمن.
- المدخلات
- root = [3]
- المخرجات
- [3]
- الشرح
- الجذر هو الشجرة بأكملها. يُدرج مرة واحدة، وليس مرة أخرى كورقة.
سؤال إضافي
كيف ستسرد حدود الشجرة باتجاه عقارب الساعة: الجذر، والحافة اليمنى من الأعلى إلى الأسفل، والأوراق من اليمين إلى اليسار، ثم الحافة اليسرى من الأسفل إلى الأعلى؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
قسّم الإجابة إلى أربعة أجزاء: الجذر، والحافة اليسرى، والأوراق، والحافة اليمنى. أيٌّ منها يمثّل مسارًا واحدًا نزولًا عبر الشجرة، وأيٌّ منها يتطلب اجتيازًا كاملًا؟
كل حافة تمثل مسارًا واحدًا: ابدأ من ابن الجذر، واستمر في الانتقال إلى الابن الموجود على ذلك الجانب، أو إلى الابن الآخر عندما يكون ذلك الجانب فارغًا. تأتي الأوراق بترتيبها من اليسار إلى اليمين من اجتيازٍ بالبحث أولًا في العمق، يزور الشجرة الفرعية اليسرى أولًا. انتبه إلى نقاط الالتقاء: فالورقة التي تنتهي عندها حافة تُعدّ من الأوراق.
تحرّك نزولًا بدءًا من الفهرس
1وألحِق كل عقدة غير ورقية. ثم ألحِق كل عقدة ورقية باستخدام اجتياز سابق الترتيب. ثم تحرّك نزولًا بدءًا من الفهرس2، واجمع كل عقدة غير ورقية، وألحِق تلك القائمة بترتيب عكسي.
الحل
تتكوّن الإجابة من أربعة أجزاء: الجذر، ومسار على طول الحافة اليسرى، والأوراق من اليسار إلى اليمين، ومسار على طول الحافة اليمنى يُقرأ بالعكس. عند نقاط الالتقاء، يجب إدراج الورقة التي تنهي حافةً والجذر إذا كان ورقةً مرةً واحدةً لكلٍّ منهما.
انتقل عبر الأجزاء الثلاثة بشكل منفصل
الفكرة
يمثّل كل طرف مسارًا واحدًا: بدءًا من ابن الجذر، أدرج العقد حتى تصبح العقدة التالية ورقة، إذ يسردها جزء الأوراق بدلًا من ذلك. يزور اجتياز سابق الترتيب الأوراق من اليسار إلى اليمين عندما ينتقل إلى اليسار قبل اليمين، لأن كل عقدة في الشجرة الفرعية اليسرى تقع إلى يسار الشجرة الفرعية اليمنى، مهما كان عمقها. اجمع الطرف الأيمن من الأعلى إلى الأسفل، ثم أضفه معكوسًا.
يتبع كل طرف مسارًا واحدًا، ويزور اجتياز الأوراق كل عقدة مرة واحدة: الزمن O(n)، مع تعمّق الاستدعاء التعاودي h مستوى.
الخوارزمية
- ابدأ بالجذر؛ إذا كان ورقة، فأعِده.
- من الفهرس
1، ما دام العقدة موجودة وليست ورقة، أضِفها وانتقل إلى ابنها الأيسر، أو إلى ابنها الأيمن إذا لم يكن لها ابن أيسر. - اجتز بترتيب سابق، بدءًا باليسار، من الفهرسين
1و2، مع إضافة كل ورقة. - من الفهرس
2، كرّر الخطوة 2 مع تفضيل الأبناء الأيمنين، واجمع العقد. - أضِف العقد المجمّعة بترتيب عكسي.
def boundaryTraversal(root):
n = len(root)
def real(i):
return i < n and root[i] != -1
def is_leaf(i):
return not real(2 * i + 1) and not real(2 * i + 2)
result = [root[0]]
if is_leaf(0):
return result
# 1. The left boundary, top-down, stopping before its leaf.
i = 1
while real(i) and not is_leaf(i):
result.append(root[i])
i = 2 * i + 1 if real(2 * i + 1) else 2 * i + 2
# 2. Every leaf: a pre-order walk meets them from left to right.
def add_leaves(i):
if not real(i):
return
if is_leaf(i):
result.append(root[i])
return
add_leaves(2 * i + 1)
add_leaves(2 * i + 2)
add_leaves(1)
add_leaves(2)
# 3. The right boundary, collected top-down and added bottom-up.
right = []
i = 2
while real(i) and not is_leaf(i):
right.append(root[i])
i = 2 * i + 2 if real(2 * i + 2) else 2 * i + 1
result.extend(reversed(right))
return resultاجتياز واحد بترتيب سابق مع علامات الحواف
الفكرة
ادمج عمليات الاجتياز في مرور واحد سابق الترتيب باستخدام مكدس، وذلك بإعطاء كل عقدة علامة: 1 على الحافة اليسرى، و2 على الحافة اليمنى، و0 في الداخل. تبدأ أبناء الجذر بالعلامتين 1 و2. تمرر العقدة الواقعة على الحافة علامتها إلى الابن الواقع على جهتها، أو إلى ابنها الوحيد؛ ويحصل كل ابن آخر على 0. في المثال الثاني، تنتقل العلامة 1 من 4 إلى 6 ثم إلى 5.
عندما تُسحب العقد من المكدس، أضف كل ورقة وكل عقدة غير ورقية تحمل العلامة 1؛ واجمع العقد غير الورقية التي تحمل العلامة 2 وأضفها معكوسة الترتيب في النهاية. يصل الاجتياز سابق الترتيب إلى كل عقدة على الحافة اليسرى قبل أي ورقة، لأن الحافة تنعطف إلى اليمين فقط عندما لا يوجد شيء على يسارها.
الخوارزمية
- ابدأ بالجذر. ادفع ابنه الأيمن مع العلم
2، ثم ابنه الأيسر مع العلم1. - أخرج عقدة من المكدس. أضفها إذا كانت ورقة أو كان علمها
1؛ وأضف العقدة غير الورقية ذات العلم2إلى القائمة اليمنى. - ادفع ابنها الأيمن، ثم ابنها الأيسر، مع العلم الذي تحدده لهما القاعدة في كل حالة.
- عندما يصبح المكدس فارغًا، أضف القائمة اليمنى بترتيب معكوس.
def boundaryTraversal(root):
n = len(root)
def real(i):
return i < n and root[i] != -1
result = [root[0]]
right = [] # the right boundary, top-down; it goes on the end reversed
# Each entry is (index, edge): 1 = left boundary, 2 = right boundary, 0 = inside.
stack = []
if real(2):
stack.append((2, 2))
if real(1):
stack.append((1, 1))
while stack:
i, edge = stack.pop()
has_left, has_right = real(2 * i + 1), real(2 * i + 2)
if not has_left and not has_right:
result.append(root[i]) # a leaf; pre-order meets them left to right
elif edge == 1:
result.append(root[i]) # the left boundary, top-down
elif edge == 2:
right.append(root[i])
# An edge passes to the child on its own side, or to an only child.
# The right child goes on the stack first, so the left one comes out first.
if has_right:
stack.append((2 * i + 2, edge if edge == 2 or not has_left else 0))
if has_left:
stack.append((2 * i + 1, edge if edge == 1 or not has_right else 0))
result.extend(reversed(right))
return result
أخطاء شائعة وحالات حدّية
- إدراج العقدة مرتين. فاجتياز الحواف الذي لا يتوقف قبل ورقته يُدرج
1مرتين في المثال 1، ويجب ألّا تُدرج الشجرة ذات العقدة الواحدة جذرها مرة أخرى بوصفه ورقة. - اعتبار العقدة الأولى في كل مستوى هي الحد الأيسر. إذا كان الابن الأيسر للجذر ورقة وكان العمق أكبر في الشجرة الفرعية اليمنى، فستبدأ المستويات الأعمق بعقد من الشجرة الفرعية اليمنى، مع أن الحد الأيسر فارغ.
- اتباع الأبناء اليساريين فقط. في المثال 2، يجب أن ينتقل الحد من
4إلى ابنه الأيمن6. - جمع الأوراق مستوىً تلو الآخر. عندها يعطي المثال 1 القيم
1،8،4،6. - في Lua وR، أبقِ فهارس العقد تبدأ من 0 لحساب
2i+1، واقرأroot[i + 1].
أسئلة شائعة3
ما المقصود باجتياز حدود الشجرة الثنائية؟
يسرد محيط الشجرة عكس اتجاه عقارب الساعة: الجذر، والحافة اليسرى من الأعلى إلى الأسفل، والأوراق من اليسار إلى اليمين، والحافة اليمنى من الأسفل إلى الأعلى. لا تشمل الحافتان أوراق الشجرة، لذا لا تظهر أي عقدة مرتين.
ما هو التعقيد الزمني لاجتياز الحدود؟
O(n). يتبع كلٌّ من المسارين على الحافتين مسارًا واحدًا، ويزور مسار الأوراق كل عقدة مرة واحدة. المساحة الإضافية هي O(h) للاستدعاء التكراري أو للمكدس.
هل يتبع الحد الأيسر دائمًا الأبناء الموجودين على اليسار؟
لا. ينتقل إلى الابن الأيسر إن وُجد، وإلى الابن الأيمن خلاف ذلك: المسار الخارجي الذي تراه من اليسار. إذا لم يكن للجذر ابن أيسر، فالحد الأيسر فارغ.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def boundaryTraversal(root):
# اكتب الكود هنااختبار #1
اختبار #2
اختبار #3
وسيط
root = [7, 3, 9, 1, 5, 8, -1, -1, -1, 4, 6]
الإخراج المتوقع
[7, 3, 1, 4, 6, 8, 9]