كيفية الحصول على عنصر من شجرة ثنائية بواسطة الفهرس في فترة زمنية معقولة؟

مرحبا يا هبر!

منذ ستة أشهر ، فكرت في كيفية الحصول على عنصر من شجرة ثنائية في O (log (N)). وجاء الجواب بسرعة كبيرة - كسول نشر. لكنني كنت كسول جدا لتنفيذ هذا في التعليمات البرمجية. الآن نحن بحاجة إلى اتخاذ مشروع التخرج في الجامعة ، لذلك أنا أفعل أي شيء عدا ذلك. هكذا جلست لتنفيذه.

بعض الملاحظات:

  • الشجرة غير متوازنة (حتى الآن ، لأن مشروع الدبلوم لا يزال بحاجة إلى كتابته) ، ونتيجة لذلك يتم إطفاء التقدير O (log (N)) في كل مكان للحصول على بيانات الإدخال العشوائي.
  • لم أجد بنية بيانات مماثلة ، كما أن الزملاء والأصدقاء الذين سألتهم ، لم يقدموا أي شيء مماثل. إذا كنت تعرف تنفيذ مثل هذه الفكرة ، فيرجى إخبارنا بذلك.
  • في فئة قمة الرأس ، يمكن للمرء أن يستغني عن الحقل الأصل عن طريق تمرير الأصل إلى الأساليب.

وصف الفكرة

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

  • زيادة countLefter على طول المسار بأكمله الذي نذهب إلى نقطة الإدراج من قمة جديدة
  • لكل القمم التي تكون على يمين ما سبق ، قم بزيادة الإضافة بمقدار 1

من المنطقي الآن إدخال طريقة الدفع () ، والتي ستضيف الإضافة إلى countLefter من الرأس نفسه وأسلافه.

هكذا جاءت فئة قمة الرأس:
public class UNode { UNode parent; int key; int countLefter; int add; UNode left; UNode right; public UNode() { } public UNode(UNode parent, int key, int countLefter, int add, UNode left, UNode right) { this.parent = parent; this.key = key; this.countLefter = countLefter; this.add = add; this.left = left; this.right = right; } public void push() { countLefter += add; if (left != null) left.add += add; if (right != null) right.add += add; add = 0; } @Override public String toString() { return "Node{" + "key=" + key + ", countLefter=" + countLefter + '}'; } } 


عظيم ، الآن يمكنك البدء في بناء شجرة!

أول شيء نفعله ، ننتقل إلى الأعلى - نسميها طريقة push ().

سنحذف العنصر عن طريق الحذف الأيسر (نأخذ أقصى اليمين من القمم الموجودة على يسار القاعة المحذوفة).

للحصول على عنصر حسب الفهرس ، نتصرف بوضوح تام: إذا كان الفهرس <countLefter من القمة الحالية ، فاذهب إلى اليسار. إذا كانت القيم متساوية ، وجدنا قمة ذات فهرس معين. خلاف ذلك ، نذهب إلى اليمين.

حذف وإضافة عنصر ، من حيث المبدأ ، لا يختلف كثيرًا عن شجرة ثنائية عادية ، باستثناء تغيير countLefter وإضافة حقول. إذا عدنا إلى الأعلى على اليسار بعد الإضافة / الإزالة الناجحة ، فستحتاج إلى تغيير هذه الحقول. إذا على اليمين ، لا.

هنا هو رمز شجرة:
 import java.util.LinkedList; public class UTree { private UNode root; private int size; public UTree() { } public int size() { return size; } public int get(int index) { return get(index, root); } public boolean add(int key) { if (root == null) { root = new UNode(null, key, 0, 0, null, null); size++; return true; } boolean res = add(key, root); if (res) size++; return res; } public boolean remove(int key) { if (root == null) return false; if (key == this.root.key) { root.push(); removeRoot(); size--; return true; } boolean res = remove(key, root); if (res) size--; return res; } private int get(int index, UNode root) { root.push(); if (index == root.countLefter) return root.key; if (index < root.countLefter) return get(index, root.left); return get(index, root.right); } private boolean add(int key, UNode root) { if (key == root.key) return false; root.push(); if (key < root.key) if (root.left != null) { boolean res = add(key, root.left); if (res) { root.countLefter++; if (root.right != null) root.right.add++; } return res; } else { root.left = new UNode(root, key, root.countLefter, 0, null, null); root.countLefter++; if (root.right != null) root.right.add++; return true; } if (root.right != null) return add(key, root.right); else { root.right = new UNode(root, key, root.countLefter + 1, 0, null, null); return true; } } public boolean removeByIndex(int index) { if(this.root == null) return false; root.push(); if (index == this.root.countLefter) { removeRoot(); return true; } boolean res = removeByIndex(index, root); if (res) size--; return res; } private boolean removeByIndex(int index, UNode root) { if (root == null) return false; root.push(); if (index == root.countLefter) return removeNode(root); if (index < root.countLefter) { boolean res = removeByIndex(index, root.left); if (res) { root.countLefter--; if (root.right != null) root.right.add--; } return res; } else return removeByIndex(index, root.right); } private boolean removeNode(UNode root) { if (root.left == root.right) { if (root.parent.left == root) root.parent.left = null; else root.parent.right = null; return true; } if (root.left == null) { if (root.parent.left == root) { root.parent.left = root.right; root.right.add--; } else { root.parent.right = root.right; root.right.add--; } root.right.parent = root.parent; return true; } if (root.right == null) { if (root.parent.left == root) root.parent.left = root.left; else root.parent.right = root.left; root.left.parent = root.parent; return true; } UNode right = getRight(root.left); cut(right); root.key = right.key; root.countLefter--; root.right.add--; return true; } private boolean remove(int key, UNode root) { if (root == null) return false; root.push(); if (key == root.key) return removeNode(root); if (key < root.key) { boolean res = remove(key, root.left); if (res) { root.countLefter--; if (root.right != null) root.right.add--; } return res; } else return remove(key, root.right); } private void removeRoot() { if (root.left == root.right) { root = null; return; } if (root.left == null) { root = root.right; root.add--; return; } if (root.right == null) { root = root.left; return; } UNode right = getRight(root.left); cut(right); root.key = right.key; root.countLefter--; root.right.add--; } private static void cut(UNode node) { if (node.parent.left == node) node.parent.left = node.left; else node.parent.right = node.left; if (node.left != null) node.left.parent = node.parent; } private UNode getRight(UNode root) { root.push(); if (root.right == null) return root; return getRight(root.right); } public void printTree() { printTree(root); } private void printTree(UNode root) { if (root == null) return; root.push(); printTree(root.left); System.out.println(root); printTree(root.right); } public LinkedList<UNode> getAll(){ LinkedList<UNode> res = new LinkedList<>(); getAll(root, res); return res; } private void getAll(UNode root, LinkedList<UNode> res){ if (root == null) return; root.push(); getAll(root.left, res); res.add(root); getAll(root.right, res); } } 


هنا يمكن العثور على الكود على جيثب.

سأقدم بعض نتائج سرعة العمل. تم إجراء الاختبار على:



إضافة إلى الشجرة:

لذلك ، إضافة مليون عنصر عشوائي في النطاق [0 ؛ 1_000)
حوالي 100 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 130 مللي ثانية.

إضافة مليون عنصر عشوائي في النطاق [0 ؛ 10_000):
حوالي 150 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 190 مللي ثانية.

إضافة مليون عنصر عشوائي في النطاق [0 ؛ 100_000):
حوالي 320 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 415 مللي ثانية.

إضافة مليون عنصر عشوائي في النطاق [0 ؛ 1_000_000):
حوالي 510 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 700 مللي ثانية.

إضافة مليون عنصر عشوائي في النطاق [0 ؛ 10_000_000):
حوالي 590 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 750 مللي ثانية.

إزالة الآن

أضف عشوائيا مليون رقم إلى الشجرة. ثم نحاول حذف رقم عشوائي مليون مرة. في الاختبارات ، يتم أخذ الوقت المستغرق في الإزالة فقط في الاعتبار.

نطاق إضافة وإزالة [0؛ 10_000_000):
حوالي 740 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 750 مللي ثانية.

نطاق إضافة وإزالة [0؛ 1_000_000):
حوالي 600 مللي ثانية. أكملت TreeSet هذه المهمة في حوالي 800 مللي ثانية (أكثر مما كانت عليه في الاختبار السابق).

نطاق إضافة وإزالة [0؛ 100_000):
حوالي 130 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 160 مللي ثانية.

نطاق إضافة وإزالة [0؛ 10_000):
حوالي 45 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 50 مللي ثانية.

نطاق إضافة وإزالة [0؛ 1_000)
حوالي 30 مللي ثانية. أنجزت TreeSet هذه المهمة في حوالي 37 مللي ثانية.

حسنًا ، وأخيراً ، من أجل كل شيء بدأ ، الوصول إلى الفهرس

ليس لدى TreeSet هذه الوظيفة. لذلك سوف أعطي النتائج فقط ل UTree. مرة أخرى نضيف مليون عنصر ، ثم نحصل على العنصر في فهرس عشوائي من 0 إلى عدد العناصر في الشجرة. يؤخذ الوقت في الاعتبار فقط للوصول عن طريق الفهرس.

نطاق الإضافة [0؛ 1000): 85 مللي ثانية

نطاق الإضافة [0؛ 10_000): 140 مللي ثانية

نطاق الإضافة [0؛ 100_000): 300 مللي ثانية

نطاق الإضافة [0؛ 1_000_000): 655 مللي ثانية

آمل أن يجد شخص ما فكرتي مفيدة ، ولكن ربما بالنسبة لشخص ما ، ستكون هذه مناسبة للتعامل مع الأشجار الثنائية ، إذا لم تكن قد فعلت ذلك :)

PS

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

Source: https://habr.com/ru/post/ar479142/


All Articles