ประโยชน์ของไบนารีทรี
ข้อดีของการจัดเก็บข้อมูลเป็นแผนผังการค้นหาแบบไบนารีคืออะไร? ลองจินตนาการว่าเรามีหนังสืออ้างอิง 100 หน้าและเราจำเป็นต้องค้นหาหน้าเฉพาะที่จะมีข้อมูลที่เราต้องการ เรายังรู้จากเนื้อหาว่าเราต้องการหน้าใดโดยเฉพาะ หากเราปฏิบัติตามเส้นทางปกติเราจะต้องเปิดทีละหน้าจนกว่าจะถึงหน้าที่ต้องการ นั่นคือเราจะอ่านตั้งแต่ 1 ถึง 100 หน้าจนกว่าเราจะพบว่าตัวเองถูกที่แล้ว ตัวอย่างเช่น หากเราต้องการหน้า 77 เราก็จะมีการค้นหา 77 ครั้ง ถ้าเราพูดถึงความซับซ้อนของเวลาก็จะเป็นO(N ) แต่สามารถทำได้อย่างมีประสิทธิภาพมากขึ้นใช่ไหม? ลองทำสิ่งเดียวกัน แต่ใช้การค้นหาแบบไบนารี่ :- เราแบ่งไดเร็กทอรีออกเป็นสองส่วนส่วนแรก - ตั้งแต่ 1 ถึง 50 ส่วนส่วนที่สอง - 51-100 เรารู้ว่าหน้าของเราอยู่ในส่วนไหน: ถ้าเราอ่านหน้า 77 อีกครั้ง ก็จะอยู่ในส่วนที่สองของหนังสือ
- ต่อไปเราจะพิจารณาส่วนที่สองและแบ่งออกเป็นสอง - จาก 51 ถึง 75 จาก 76 ถึง 100 ในกรณีนี้ หน้าของเราจะอีกครั้งในช่วงครึ่งหลังในช่วง 76-100
- ต่อไป เราหารช่วงเวลานี้ด้วย 2 แล้วได้ 76-88 และ 89-100 หน้าพอดีกับช่องว่างแรก ดังนั้นเราจึงละทิ้งส่วนที่สอง
- ถัดไปคือช่วง: 76-81 และ 82-88 ใช้ช่วงแรก
- 76-78 และ 79-81 เอาอันแรกครับ
- 76 และ 77-78 เอาอันที่สอง.
- 77 และ 78 เอาอันแรกแล้วได้หน้าของเรา - 77
กฎสำหรับการสร้างแผนผังการค้นหาแบบไบนารี
แผนผังการค้นหาแบบไบนารีถูกสร้างขึ้นตามกฎบางประการ:- แต่ละโหนดมีลูกไม่เกินสองคน
- ทุกค่าที่น้อยกว่าค่าของโหนดจะกลายเป็นลูกด้านซ้ายหรือลูกของลูกด้านซ้าย
- ทุกค่าที่มากกว่าหรือเท่ากับค่าของโหนดจะกลายเป็นลูกที่ถูกต้องหรือลูกของลูกที่ถูกต้อง
- โหนดทั้งหมดมีทายาทไม่เกินสองคน ตรงตามเงื่อนไขแรก
- ตัวอย่างเช่นหากเราพิจารณาโหนดที่มีค่า 7 (หรืออื่น ๆ ) เราจะเห็นว่าค่าทั้งหมดขององค์ประกอบในทรีย่อยด้านซ้ายจะน้อยลงและทางด้านขวา - มากกว่า ซึ่งหมายความว่าเป็นไปตามเงื่อนไข 2 และ 3
ค้นหาองค์ประกอบ
เมื่อค้นหาองค์ประกอบด้วยค่าที่กำหนด เราจะเริ่มต้นที่องค์ประกอบรูท:- ถ้ามันเท่ากับค่าที่ต้องการ องค์ประกอบรูทจะเป็นค่าที่ต้องการ ถ้าไม่ เราจะเปรียบเทียบค่าของรูทกับค่าที่ต้องการ
- หากองค์ประกอบที่เรากำลังมองหามีขนาดใหญ่ขึ้น เราจะย้ายไปที่รายการย่อยด้านขวา ถ้าไม่ เราจะย้ายไปรายการย่อยด้านซ้าย
- หากไม่พบองค์ประกอบ ให้ใช้ขั้นตอนที่ 1 และ 2 แต่กับองค์ประกอบย่อย (ขวาหรือซ้าย) จนกว่าจะพบองค์ประกอบ
- เราเปรียบเทียบกับองค์ประกอบรูท เราจะเห็นว่าองค์ประกอบรูทมีขนาดใหญ่กว่า ดังนั้นเราจึงย้ายไปที่ลูกด้านซ้ายซึ่งมีค่าเป็น 3
- เราเปรียบเทียบองค์ประกอบที่เรากำลังค้นหากับองค์ประกอบที่มีค่า 3 เราจะเห็นว่าองค์ประกอบที่เรากำลังมองหานั้นใหญ่กว่า ดังนั้นเราจึงต้องการผู้สืบทอดที่ถูกต้องขององค์ประกอบที่เป็นปัญหา กล่าวคือ องค์ประกอบที่มีค่า 5
- เราเปรียบเทียบลูกหลานนี้กับสิ่งที่เรากำลังมองหาและเห็นว่าค่าเท่ากัน - พบองค์ประกอบแล้ว
การแทรกองค์ประกอบ
อัลกอริธึมการแทรกนั้นง่ายมาก:-
เราเปรียบเทียบอันใหม่กับรูท (หากไม่มี องค์ประกอบใหม่จะเป็นรูท)
-
หากมีองค์ประกอบใหม่:
- น้อยกว่าเราก็ไปที่ทายาทด้านซ้าย หากไม่มี องค์ประกอบใหม่จะเข้ามาแทนที่ทายาทด้านซ้ายและอัลกอริทึมจะเสร็จสมบูรณ์
- มากกว่าหรือเท่ากับรากแล้วเราก็ย้ายไปยังทายาทที่ถูกต้อง และในทำนองเดียวกัน หากไม่มีองค์ประกอบนี้ องค์ประกอบใหม่จะเข้ามาแทนที่องค์ประกอบที่ถูกต้องและอัลกอริทึมจะเสร็จสิ้น
-
สำหรับองค์ประกอบใหม่ที่เป็นปัญหา ซึ่งอยู่ทางขวาหรือซ้ายจากขั้นตอนก่อนหน้า ให้ทำซ้ำขั้นตอนที่ 1 และ 2 จนกว่าองค์ประกอบที่แทรกจะเข้าที่
ตามตัวอย่าง เราจะต้องการแทรกองค์ประกอบที่มีค่า 11 เข้าไปในแผนผังที่พิจารณาข้างต้น:
- เราเปรียบเทียบกับองค์ประกอบรากที่ 7 และเห็นว่าองค์ประกอบรากนั้นเล็กกว่า ดังนั้นเราจึงย้ายไปยังทายาทที่ถูกต้อง
- องค์ประกอบถัดไปที่พิจารณามีค่าเท่ากับ 9 ซึ่งน้อยกว่า 11 ใหม่ ดังนั้นเราจึงย้ายไปยังทายาทที่ถูกต้อง
- ทายาทที่ถูกต้องมีค่า 10 ซึ่งน้อยกว่าอีกครั้ง ดังนั้นเราจึงไปที่องค์ประกอบแรก และเนื่องจากไม่มีอยู่ จึงวางองค์ประกอบใหม่ที่มีค่า 11 ไว้ในที่นี้
↓
การลบองค์ประกอบ
บางที การดำเนินการทั้งหมดที่มีแผนผังการค้นหาแบบไบนารี การลบอาจทำได้ยากที่สุด ประการแรก มีการค้นหาองค์ประกอบที่จะลบ หลังจากนั้นขั้นตอนจะตามมา ซึ่งสามารถมีได้สามรูปแบบ:-
โหนดที่จะลบคือโหนดปลายสุด (ไม่มีลูก)
บางทีสิ่งที่ง่ายที่สุด ทั้งหมดนี้เกิดจากการที่เราตัดมันออกจากต้นไม้โดยไม่มีการยักย้ายที่ไม่จำเป็น ตัวอย่างเช่น จากแผนผังของเรา เราลบโหนดที่มีค่า 8:
↓ -
โหนดที่กำลังลบมีลูกหนึ่งคน
ในกรณีนี้ เราจะลบโหนดที่เลือกและแทนที่โหนดนั้นในตำแหน่งนั้น (โดยพื้นฐานแล้ว เราเพียงแค่ตัดโหนดที่เลือกออกจากห่วงโซ่) ตามตัวอย่าง ลองลบโหนดที่มีค่า 9 ออกจากแผนผัง:
↓ -
โหนดที่กำลังลบมีลูกสองคน
ส่วนที่น่าสนใจที่สุด ท้ายที่สุดแล้ว หากโหนดที่ถูกลบมีลูกสองคนพร้อมกัน คุณไม่สามารถแทนที่มันด้วยลูกคนใดคนหนึ่งเหล่านี้ได้ (โดยเฉพาะถ้าลูกมีลูกของตัวเอง)
ตัวอย่าง:ในแผนผังด้านบน โหนดใดควรเป็นลูกด้านซ้ายของโหนด 3
หากคุณลองคิดดูสักนิดก็จะเห็นได้ชัดว่านี่ควรเป็นโหนดที่มีค่า 4 แต่จะเกิดอะไรขึ้นถ้าต้นไม้ไม่ง่ายขนาดนั้น? หากมีคุณค่านับร้อย จะง่ายขนาดนั้นไหมที่จะตัดสินว่าใครจะเป็นผู้สืบทอด?
เป็นที่ชัดเจนว่าไม่มี ดังนั้นเราจึงจำเป็นต้องมีอัลกอริธึมการค้นหาตัวรับสัญญาณขนาดเล็กของเราเอง:
- ขั้นแรกเราต้องไปที่ลูกด้านขวาของโหนดที่เลือก ซึ่งค่าจะต้องมากกว่าค่าของโหนด
- จากนั้นเราก็ย้ายไปลูกซ้ายของเด็กขวา (ถ้ามี) จากนั้นไปที่ลูกซ้ายของเด็กซ้าย ฯลฯ ตามสายโซ่ของลูกซ้าย
- ดังนั้นลูกคนสุดท้ายบนเส้นทางนี้จะเป็นผู้สืบทอดของโหนดดั้งเดิม
มาสรุปอัลกอริทึมเล็กๆ น้อยๆ นี้กันสักหน่อย: ในแผนผังย่อยของโหนดย่อยด้านขวาของโหนดต้นทาง โหนดทั้งหมดจะมีขนาดใหญ่กว่าโหนดที่ถูกลบ ซึ่งสามารถเข้าใจได้จากกฎพื้นฐานของแผนผังการค้นหาแบบไบนารี ในแผนผังย่อยนี้ เรากำลังมองหาค่าที่น้อยที่สุด
กล่าวอีกนัยหนึ่ง เรากำลังมองหาโหนดที่เล็กที่สุดในชุดของโหนดที่มีขนาดใหญ่กว่าโหนดดั้งเดิม โหนดที่เล็กที่สุดในบรรดาโหนดที่ใหญ่กว่านี้จะเป็นผู้สืบทอดที่เหมาะสมที่สุด
ต้นไม้จะมีลักษณะอย่างไรหลังจากลบโหนดที่มีค่า 3:
↓
class Node {
private int value; // ключ узла
private Node leftChild; // Левый узел потомок
private Node rightChild; // Правый узел потомок
public void printNode() { // Вывод значения узла в консоль
System.out.println(" Выбранный узел имеет meaning :" + value);
}
public int getValue() {
return this.value;
}
public void setValue(final int value) {
this.value = value;
}
public Node getLeftChild() {
return this.leftChild;
}
public void setLeftChild(final Node leftChild) {
this.leftChild = leftChild;
}
public Node getRightChild() {
return this.rightChild;
}
public void setRightChild(final Node rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", leftChild=" + leftChild +
", rightChild=" + rightChild +
'}';
}
}
ไม่มีอะไรซับซ้อนเกินไป แต่ละองค์ประกอบมีลิงก์ไปยังลูกด้านซ้ายและขวา และตอนนี้บางทีคลาสที่สำคัญที่สุดคือคลาส tree:
class Tree {
private Node rootNode; // корневой узел
public Tree() { // Пустое дерево
rootNode = null;
}
public Node findNodeByValue(int value) { // поиск узла по значению
Node currentNode = rootNode; // начинаем поиск с корневого узла
while (currentNode.getValue() != value) { // поиск покуда не будет найден элемент or не будут перебраны все
if (value < currentNode.getValue()) { // движение влево?
currentNode = currentNode.getLeftChild();
} else { //движение вправо
currentNode = currentNode.getRightChild();
}
if (currentNode == null) { // если потомка нет,
return null; // возвращаем null
}
}
return currentNode; // возвращаем найденный элемент
}
public void insertNode(int value) { // метод вставки нового element
Node newNode = new Node(); // создание нового узла
newNode.setValue(value); // вставка данных
if (rootNode == null) { // если корневой узел не существует
rootNode = newNode;// то новый элемент и есть корневой узел
}
else { // корневой узел занят
Node currentNode = rootNode; // начинаем с корневого узла
Node parentNode;
while (true) // мы имеем внутренний выход из цикла
{
parentNode = currentNode;
if(value == currentNode.getValue()) { // если такой элемент в дереве уже есть, не сохраняем его
return; // просто выходим из метода
}
else if (value < currentNode.getValue()) { // движение влево?
currentNode = currentNode.getLeftChild();
if (currentNode == null){ // если был достигнут конец цепочки,
parentNode.setLeftChild(newNode); // то вставить слева и выйти из методы
return;
}
}
else { // Или направо?
currentNode = currentNode.getRightChild();
if (currentNode == null) { // если был достигнут конец цепочки,
parentNode.setRightChild(newNode); //то вставить справа
return; // и выйти
}
}
}
}
}
public boolean deleteNode(int value) // Удаление узла с заданным ключом
{
Node currentNode = rootNode;
Node parentNode = rootNode;
boolean isLeftChild = true;
while (currentNode.getValue() != value) { // начинаем поиск узла
parentNode = currentNode;
if (value < currentNode.getValue()) { // Определяем, нужно ли движение влево?
isLeftChild = true;
currentNode = currentNode.getLeftChild();
}
else { // or движение вправо?
isLeftChild = false;
currentNode = currentNode.getRightChild();
}
if (currentNode == null)
return false; // yзел не найден
}
if (currentNode.getLeftChild() == null && currentNode.getRightChild() == null) { // узел просто удаляется, если не имеет потомков
if (currentNode == rootNode) // если узел - корень, то дерево очищается
rootNode = null;
else if (isLeftChild)
parentNode.setLeftChild(null); // если нет - узел отсоединяется, от родителя
else
parentNode.setRightChild(null);
}
else if (currentNode.getRightChild() == null) { // узел заменяется левым поддеревом, если правого потомка нет
if (currentNode == rootNode)
rootNode = currentNode.getLeftChild();
else if (isLeftChild)
parentNode.setLeftChild(currentNode.getLeftChild());
else
parentNode.setRightChild(currentNode.getLeftChild());
}
else if (currentNode.getLeftChild() == null) { // узел заменяется правым поддеревом, если левого потомка нет
if (currentNode == rootNode)
rootNode = currentNode.getRightChild();
else if (isLeftChild)
parentNode.setLeftChild(currentNode.getRightChild());
else
parentNode.setRightChild(currentNode.getRightChild());
}
else { // если есть два потомка, узел заменяется преемником
Node heir = receiveHeir(currentNode);// поиск преемника для удаляемого узла
if (currentNode == rootNode)
rootNode = heir;
else if (isLeftChild)
parentNode.setLeftChild(heir);
else
parentNode.setRightChild(heir);
}
return true; // элемент успешно удалён
}
// метод возвращает узел со следующим meaningм после передаваемого аргументом.
// для этого он сначала переходим к правому потомку, а затем
// отслеживаем цепочку левых потомков этого узла.
private Node receiveHeir(Node node) {
Node parentNode = node;
Node heirNode = node;
Node currentNode = node.getRightChild(); // Переход к правому потомку
while (currentNode != null) // Пока остаются левые потомки
{
parentNode = heirNode;// потомка задаём How текущий узел
heirNode = currentNode;
currentNode = currentNode.getLeftChild(); // переход к левому потомку
}
// Если преемник не является
if (heirNode != node.getRightChild()) // правым потомком,
{ // создать связи между узлами
parentNode.setLeftChild(heirNode.getRightChild());
heirNode.setRightChild(node.getRightChild());
}
return heirNode;// возвращаем приемника
}
public void printTree() { // метод для вывода дерева в консоль
Stack globalStack = new Stack(); // общий стек для значений дерева
globalStack.push(rootNode);
int gaps = 32; // начальное meaning расстояния между elementми
boolean isRowEmpty = false;
String separator = "-----------------------------------------------------------------";
System.out.println(separator);// черта для указания начала нового дерева
while (isRowEmpty == false) {
Stack localStack = new Stack(); // локальный стек для задания потомков element
isRowEmpty = true;
for (int j = 0; j < gaps; j++)
System.out.print(' ');
while (globalStack.isEmpty() == false) { // покуда в общем стеке есть элементы
Node temp = (Node) globalStack.pop(); // берем следующий, при этом удаляя его из стека
if (temp != null) {
System.out.print(temp.getValue()); // выводим его meaning в консоли
localStack.push(temp.getLeftChild()); // соохраняем в локальный стек, наследники текущего element
localStack.push(temp.getRightChild());
if (temp.getLeftChild() != null ||
temp.getRightChild() != null)
isRowEmpty = false;
}
else {
System.out.print("__");// - если элемент пустой
localStack.push(null);
localStack.push(null);
}
for (int j = 0; j < gaps * 2 - 2; j++)
System.out.print(' ');
}
System.out.println();
gaps /= 2;// при переходе на следующий уровень расстояние между elementми каждый раз уменьшается
while (localStack.isEmpty() == false)
globalStack.push(localStack.pop()); // перемещаем все элементы из локального стека в глобальный
}
System.out.println(separator);// подводим черту
}
}
อีกครั้งไม่มีอะไรซับซ้อนเกินไป มีการดำเนินการค้นหาแผนผังแบบไบนารีที่อธิบายไว้ก่อนหน้านี้ พร้อมด้วยวิธีการแสดงแผนผังในคอนโซล ทีนี้เรามาดูการทำงานของต้นไม้กันดีกว่า:
public class Application {
public static void main(String[] args) {
Tree tree = new Tree();
// вставляем узлы в дерево:
tree.insertNode(6);
tree.insertNode(8);
tree.insertNode(5);
tree.insertNode(8);
tree.insertNode(2);
tree.insertNode(9);
tree.insertNode(7);
tree.insertNode(4);
tree.insertNode(10);
tree.insertNode(3);
tree.insertNode(1);
// отображение дерева:
tree.printTree();
// удаляем один узел и выводим оставшееся дерево в консоли
tree.deleteNode(5);
tree.printTree();
// находим узел по значению и выводим его в консоли
Node foundNode = tree.findNodeByValue(7);
foundNode.printNode();
}
}
การดำเนินการค้นหา/แทรก/ลบในแผนผังการค้นหาแบบไบนารีมีเวลาซับซ้อนเท่ากับO(log(N) ) แต่นั่นคือสถานการณ์กรณีที่ดีที่สุด โดยทั่วไป ความซับซ้อนของเวลาในการดำเนินการมีตั้งแต่O(log(N))ถึงO(N ) ขึ้นอยู่กับระดับความเสื่อมของต้นไม้
GO TO FULL VERSION