Lợi ích của cây nhị phân
Ưu điểm của việc lưu trữ dữ liệu dưới dạng cây tìm kiếm nhị phân là gì? Hãy tưởng tượng rằng chúng ta có một cuốn sách tham khảo 100 trang và chúng ta cần tìm một trang cụ thể chứa dữ liệu chúng ta cần. Chúng tôi cũng biết từ nội dung trang cụ thể nào chúng tôi cần. Nếu đi theo con đường thông thường, chúng ta sẽ phải lật từng trang một cho đến khi đến được trang mình cần. Tức là chúng ta sẽ xem từ 1 đến 100 trang cho đến khi tìm đúng chỗ. Ví dụ ta cần trang 77 thì ta sẽ có 77 lượt tìm kiếm. Nếu chúng ta nói về độ phức tạp về thời gian thì nó sẽ là O(N) . Nhưng điều này có thể được thực hiện hiệu quả hơn, phải không? Hãy thử làm điều tương tự nhưng sử dụng tìm kiếm nhị phân :- Chúng tôi chia thư mục thành hai phần, phần thứ nhất - từ 1 đến 50, phần thứ hai - 51-100. Chúng tôi biết chính xác trang của chúng tôi nằm ở phần nào: nếu chúng tôi xem lại trang 77, nó sẽ nằm ở phần thứ hai của cuốn sách.
- Tiếp theo, chúng tôi xem xét phần thứ hai và chia nó thành hai - từ 51 đến 75, từ 76 đến 100. Trong trường hợp này, trang của chúng tôi sẽ lại ở nửa sau, trong khoảng 76-100.
- Tiếp theo, chúng ta chia khoảng này cho 2 và nhận được 76-88 và 89-100. Trang này vừa với khoảng trống đầu tiên, vì vậy chúng tôi loại bỏ khoảng trống thứ hai.
- Tiếp theo là các khoảng: 76-81 và 82-88, lấy khoảng đầu tiên.
- 76-78 và 79-81, lấy cái đầu tiên.
- 76 và 77-78, lấy cái thứ hai.
- 77 và 78, lấy cái đầu tiên và lấy trang của chúng tôi - 77.
Quy tắc xây dựng cây tìm kiếm nhị phân
Cây tìm kiếm nhị phân được xây dựng theo những quy tắc nhất định:- mỗi nút có nhiều nhất hai nút con;
- mọi giá trị nhỏ hơn giá trị của nút sẽ trở thành con trái hoặc con của con trái;
- mọi giá trị lớn hơn hoặc bằng giá trị của nút sẽ trở thành con bên phải hoặc con của con bên phải.
- tất cả các nút có không quá hai người thừa kế, điều kiện đầu tiên được đáp ứng;
- Ví dụ: nếu chúng ta xem xét một nút có giá trị 7 (hoặc bất kỳ giá trị nào khác), chúng ta sẽ thấy rằng tất cả các giá trị của các phần tử trong cây con bên trái sẽ ít hơn và ở bên phải - nhiều hơn. Điều này có nghĩa là điều kiện 2 và 3 được đáp ứng.
Tìm kiếm một phần tử
Khi tìm kiếm một phần tử có giá trị nhất định, chúng ta bắt đầu từ phần tử gốc:- Nếu nó bằng giá trị mong muốn thì phần tử gốc là phần tử mong muốn, nếu không, chúng ta so sánh giá trị của phần tử gốc và phần tử mong muốn.
- Nếu phần tử chúng ta đang tìm kiếm lớn hơn, chúng ta chuyển sang phần tử con bên phải; nếu không, chúng ta chuyển sang phần tử con bên trái.
- Nếu không tìm thấy phần tử, hãy áp dụng bước 1 và 2 nhưng với phần tử con (phải hoặc trái) cho đến khi tìm thấy phần tử.
- ta so sánh với phần tử gốc, thấy phần tử gốc lớn hơn nên chuyển sang phần tử con bên trái, phần tử này có giá trị là 3;
- chúng tôi so sánh phần tử chúng tôi đang tìm kiếm và phần tử có giá trị 3, chúng tôi thấy rằng phần tử chúng tôi đang tìm kiếm lớn hơn, vì vậy chúng tôi cần phần tử con bên phải của phần tử được đề cập, cụ thể là phần tử có giá trị 5;
- Chúng tôi so sánh hậu duệ này với hậu duệ mà chúng tôi đang tìm kiếm và thấy rằng các giá trị bằng nhau - phần tử được tìm thấy.
Chèn một phần tử
Thuật toán chèn cũng rất, rất đơn giản:-
Chúng ta so sánh phần tử mới với phần tử gốc (nếu nó không tồn tại thì phần tử mới là phần tử gốc).
-
Nếu một phần tử mới:
- nhỏ hơn thì chuyển sang thừa kế bên trái, nếu không có thì phần tử mới thay thế cho thừa kế bên trái và thuật toán hoàn thành;
- lớn hơn hoặc bằng gốc thì ta chuyển sang thừa kế bên phải. Và tương tự, nếu phần tử này không tồn tại thì phần tử mới sẽ thay thế phần tử bên phải và thuật toán hoàn thành.
-
Đối với phần tử mới được đề cập, nằm ở bên phải hoặc bên trái so với bước trước, hãy lặp lại bước 1 và 2 cho đến khi phần tử được chèn vào đúng vị trí.
Ví dụ: chúng ta muốn chèn vào cây được xem xét ở trên một phần tử có giá trị 11:
- ta so sánh với phần tử gốc 7 và thấy phần tử gốc nhỏ hơn nên chuyển sang phần tử thừa kế bên phải;
- phần tử tiếp theo đang được xem xét có giá trị là 9, nhỏ hơn 11 mới, vì vậy chúng ta chuyển sang phần tử thừa kế bên phải;
- người thừa kế bên phải có giá trị là 10, giá trị này lại ít hơn, vì vậy chúng ta chuyển sang phần tử đầu tiên và vì nó không có ở đó nên một phần tử mới có giá trị 11 được đặt ở vị trí này.
↓
Loại bỏ một phần tử
Có lẽ, trong tất cả các thao tác với cây tìm kiếm nhị phân, việc xóa là khó khăn nhất. Trước hết, có một cuộc tìm kiếm phần tử cần xóa, sau khi tìm ra giai đoạn tiếp theo, giai đoạn này có thể có ba biến thể:-
Nút cần xóa là nút lá (không có nút con).
Có lẽ đơn giản nhất. Tất cả bắt nguồn từ thực tế là chúng ta chỉ cần cắt nó ra khỏi cây mà không cần thao tác không cần thiết. Ví dụ: khỏi cây của chúng tôi, chúng tôi xóa nút có giá trị 8:
↓ -
Nút bị xóa có một nút con.
Trong trường hợp này, chúng tôi xóa nút đã chọn và đặt nút con của nó vào vị trí của nó (về bản chất, chúng tôi chỉ cần cắt nút đã chọn khỏi chuỗi). Ví dụ: hãy xóa nút có giá trị 9 khỏi cây:
↓ -
Nút bị xóa có hai nút con.
Phần thú vị nhất. Rốt cuộc, nếu nút bị xóa có hai nút con cùng một lúc, bạn không thể đơn giản thay thế nó bằng một trong những nút con này (đặc biệt nếu nút đó có con riêng của nó).
Ví dụ: Trong cây trên, nút nào sẽ là nút con bên trái của nút 3?
Nếu bạn suy nghĩ một chút, bạn sẽ thấy rõ ràng rằng đây phải là một nút có giá trị là 4. Nhưng nếu cái cây không đơn giản như vậy thì sao? Nếu nó chứa hàng trăm giá trị, liệu ai sẽ là người kế vị dễ dàng đến thế?
Rõ ràng là không. Vì vậy, chúng ta cần thuật toán tìm kiếm máy thu nhỏ của riêng mình:
- Đầu tiên chúng ta phải đi tới nút con bên phải của nút đã chọn, giá trị của nút này phải lớn hơn giá trị của nút.
- Sau đó chúng ta chuyển sang con trái của con phải (nếu có), rồi đến con trái của con trái, v.v., theo chuỗi con trái.
- Theo đó, nút con cuối cùng còn lại trên đường dẫn này sẽ là nút kế thừa của nút ban đầu.
Chúng ta hãy khái quát hóa thuật toán nhỏ này một chút: trong cây con của nút con bên phải của nút nguồn, tất cả các nút đều lớn hơn nút bị xóa, điều này có thể hiểu được từ các quy tắc cơ bản của cây tìm kiếm nhị phân. Trong cây con này, chúng tôi đang tìm kiếm giá trị nhỏ nhất.
Nói cách khác, chúng ta đang tìm nút nhỏ nhất trong tập hợp các nút lớn hơn nút ban đầu. Nút nhỏ nhất này trong số các nút lớn hơn sẽ là nút kế thừa phù hợp nhất.
Cây sẽ trông như thế nào sau khi loại bỏ nút có giá trị 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 +
'}';
}
}
Không có gì quá phức tạp: mỗi phần tử có một liên kết đến phần con bên trái và bên phải của nó. Và bây giờ, có lẽ lớp quan trọng nhất là lớp cây:
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);// подводим черту
}
}
Một lần nữa, không có gì quá phức tạp. Các hoạt động của cây tìm kiếm nhị phân được mô tả trước đó đều có sẵn, cùng với một phương pháp hiển thị cây trong bảng điều khiển. Bây giờ chúng ta hãy quan sát hoạt động của cây:
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();
}
}
Các thao tác tìm kiếm/chèn/xóa trong cây tìm kiếm nhị phân có độ phức tạp về thời gian là O(log(N)) . Nhưng đó là trường hợp tốt nhất. Nói chung, độ phức tạp về thời gian của các hoạt động nằm trong khoảng từ O(log(N)) đến O(N) . Nó phụ thuộc vào mức độ thoái hóa của cây.
GO TO FULL VERSION