269 lines
16 KiB
Markdown
269 lines
16 KiB
Markdown
---
|
||
title: Binary Search Trees
|
||
localeTitle: Деревья двоичного поиска
|
||
---
|
||
## Деревья двоичного поиска
|
||
|
||
![Двоичное дерево поиска](https://cdn-images-1.medium.com/max/1320/0*x5o1G1UpM1RfLpyx.png)
|
||
|
||
Дерево представляет собой структуру данных, состоящую из узлов, которые имеют следующие характеристики:
|
||
|
||
1. Каждое дерево имеет корневой узел (вверху), имеющий некоторое значение.
|
||
2. Корневой узел имеет ноль или более дочерних узлов.
|
||
3. Каждый дочерний узел имеет ноль или более дочерних узлов и т. Д. Это создает поддерево в дереве. Каждый узел имеет свое собственное поддерево, состоящее из его детей и их детей и т. Д. Это означает, что каждый узел сам по себе может быть деревом.
|
||
|
||
Двоичное дерево поиска (BST) добавляет эти две характеристики:
|
||
|
||
1. Каждый узел имеет максимум до двух детей.
|
||
2. Для каждого узла значения его левых узлов-потомков меньше, чем у текущего узла, который, в свою очередь, меньше, чем правые узлы-потомки (если они есть).
|
||
|
||
BST построен на идее алгоритма [бинарного поиска](https://guide.freecodecamp.org/algorithms/search-algorithms/binary-search) , который позволяет быстро находить, вставлять и удалять узлы. Способ их настройки означает, что в среднем каждое сравнение позволяет операциям пропускать около половины дерева, так что каждый поиск, вставка или удаление занимает время, пропорциональное логарифму количества элементов, хранящихся в дереве, `O(log n)` . Однако иногда может произойти худший случай, когда дерево не сбалансировано, а временная сложность `O(n)` для всех трех этих функций. Вот почему самобалансирующиеся деревья (AVL, red-black и т. Д.) Намного эффективнее базового BST.
|
||
|
||
**Пример худшего сценария:** это может произойти, когда вы добавляете узлы, которые _всегда_ больше, чем узел (родительский), то же самое может произойти, когда вы всегда добавляете узлы со значениями ниже своих родителей.
|
||
|
||
### Основные операции на BST
|
||
|
||
* Create: создает пустое дерево.
|
||
* Вставить: вставить узел в дерево.
|
||
* Поиск: поиск узла в дереве.
|
||
* Удалить: удаляет узел из дерева.
|
||
|
||
#### Создайте
|
||
|
||
Сначала создается пустое дерево без каких-либо узлов. Переменная / идентификатор, который должен указывать на корневой узел, инициализируется значением `NULL` .
|
||
|
||
#### Поиск
|
||
|
||
Вы всегда начинаете искать дерево в корневом узле и спускаетесь оттуда. Вы сравниваете данные в каждом узле с тем, который вы ищете. Если сравниваемый узел не совпадает, вы либо переходите к правильному ребенку, либо к левому ребенку, что зависит от результата следующего сравнения: если узел, который вы ищете, меньше, чем тот, с которым вы сравнивали его, вы переходите к левому ребенку, иначе (если он больше) вы переходите к правильному ребенку. Зачем? Поскольку BST структурирован (согласно его определению), что правильный ребенок всегда больше родителя, а левый ребенок всегда меньше.
|
||
|
||
#### Вставить
|
||
|
||
Он очень похож на функцию поиска. Вы снова начинаете с корня дерева и возвращаетесь рекурсивно, ища подходящее место для вставки нашего нового узла так же, как описано в функции поиска. Если узел с тем же значением уже находится в дереве, вы можете выбрать либо вставить дубликат, либо нет. Некоторые деревья допускают дубликаты, некоторые - нет. Это зависит от определенной реализации.
|
||
|
||
#### делеция
|
||
|
||
Есть три случая, которые могут произойти, когда вы пытаетесь удалить узел. Если это так,
|
||
|
||
1. Нет поддерева (без детей): этот самый простой. Вы можете просто удалить узел без каких-либо дополнительных действий.
|
||
2. Одно поддерево (один ребенок): вы должны убедиться, что после удаления узла его дочерний элемент затем подключается к родительскому элементу удаленного узла.
|
||
3. Два поддерева (двое детей): вы должны найти и заменить узел, который хотите удалить, с его преемником (letfmost node в правом поддереве).
|
||
|
||
Сложность времени для создания дерева - `O(1)` . Сложность времени для поиска, вставки или удаления узла зависит от высоты дерева `h` , поэтому худшим случаем является `O(h)` .
|
||
|
||
#### Предшественник узла
|
||
|
||
Предшественники можно охарактеризовать как узел, который появится прямо перед узлом, в котором вы сейчас находитесь. Чтобы найти предшественника текущего узла, посмотрите на правый / самый большой листовой узел в левом поддереве.
|
||
|
||
#### Преемник узла
|
||
|
||
Преемники можно охарактеризовать как узел, который появится сразу после узла, в котором вы сейчас находитесь. Чтобы найти преемника текущего узла, посмотрите на самый левый или самый маленький листовой узел в правом поддереве.
|
||
|
||
### Специальные типы BT
|
||
|
||
* отвал
|
||
* Красно-черное дерево
|
||
* В-дерево
|
||
* Splay tree
|
||
* N-арное дерево
|
||
* Trie (дерево Radix)
|
||
|
||
### время выполнения
|
||
|
||
**Структура данных: массив**
|
||
|
||
* Наихудшая производительность: `O(log n)`
|
||
* Лучшая производительность: `O(1)`
|
||
* Средняя производительность: `O(log n)`
|
||
* Сложная сложность пространства: `O(1)`
|
||
|
||
Где `n` - количество узлов в BST.
|
||
|
||
### Внедрение BST
|
||
|
||
Вот определение для узла BST, имеющего некоторые данные, ссылающиеся на его левый и правый дочерние узлы.
|
||
|
||
```c
|
||
struct node {
|
||
int data;
|
||
struct node *leftChild;
|
||
struct node *rightChild;
|
||
};
|
||
```
|
||
|
||
#### Операция поиска
|
||
|
||
Всякий раз, когда элемент нужно искать, начните поиск с корневого узла. Затем, если данные меньше значения ключа, выполните поиск элемента в левом поддереве. В противном случае выполните поиск элемента в правом поддереве. Следуйте одному и тому же алгоритму для каждого узла.
|
||
|
||
```c
|
||
struct node* search(int data){
|
||
struct node *current = root;
|
||
printf("Visiting elements: ");
|
||
|
||
while(current->data != data){
|
||
|
||
if(current != NULL) {
|
||
printf("%d ",current->data);
|
||
|
||
//go to left tree
|
||
if(current->data > data){
|
||
current = current->leftChild;
|
||
}//else go to right tree
|
||
else {
|
||
current = current->rightChild;
|
||
}
|
||
|
||
//not found
|
||
if(current == NULL){
|
||
return NULL;
|
||
}
|
||
}
|
||
}
|
||
return current;
|
||
}
|
||
```
|
||
|
||
#### Вставить операцию
|
||
|
||
Всякий раз, когда элемент должен быть вставлен, сначала найдите его правильное местоположение. Начните поиск с корневого узла, затем, если данные меньше значения ключа, найдите пустое место в левом поддереве и вставьте данные. В противном случае найдите пустое место в правом поддереве и вставьте данные.
|
||
|
||
```c
|
||
void insert(int data) {
|
||
struct node *tempNode = (struct node*) malloc(sizeof(struct node));
|
||
struct node *current;
|
||
struct node *parent;
|
||
|
||
tempNode->data = data;
|
||
tempNode->leftChild = NULL;
|
||
tempNode->rightChild = NULL;
|
||
|
||
//if tree is empty
|
||
if(root == NULL) {
|
||
root = tempNode;
|
||
} else {
|
||
current = root;
|
||
parent = NULL;
|
||
|
||
while(1) {
|
||
parent = current;
|
||
|
||
//go to left of the tree
|
||
if(data < parent->data) {
|
||
current = current->leftChild;
|
||
//insert to the left
|
||
|
||
if(current == NULL) {
|
||
parent->leftChild = tempNode;
|
||
return;
|
||
}
|
||
}//go to right of the tree
|
||
else {
|
||
current = current->rightChild;
|
||
|
||
//insert to the right
|
||
if(current == NULL) {
|
||
parent->rightChild = tempNode;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Двоичные деревья поиска (BST) также дают нам быстрый доступ к предшественникам и преемникам. Предшественники можно охарактеризовать как узел, который появится прямо перед узлом, в котором вы сейчас находитесь.
|
||
|
||
* Чтобы найти предшественника текущего узла, посмотрите на самый правый / самый большой листовой узел в левом поддереве. Преемники можно охарактеризовать как узел, который появится сразу после узла, в котором вы сейчас находитесь.
|
||
* Чтобы найти преемника текущего узла, посмотрите на самый левый / самый маленький листовой узел в правом поддереве.
|
||
|
||
### Давайте посмотрим на пару процедур, работающих на деревьях.
|
||
|
||
Поскольку деревья рекурсивно определены, очень часто приходится писать процедуры, которые работают на деревьях, которые сами являются рекурсивными.
|
||
|
||
Например, если мы хотим рассчитать высоту дерева, то есть высоту корневого узла, мы можем идти вперед и рекурсивно делать это, проходя через дерево. Поэтому мы можем сказать:
|
||
|
||
* Например, если у нас есть дерево nil, то его высота равна 0.
|
||
* В противном случае мы достигнем 1 плюс максимум левого дочернего дерева и правого дочернего дерева.
|
||
* Поэтому, если мы посмотрим на лист, например, эта высота будет равна 1, так как высота левого дочернего элемента равна нулю, равно 0, а высота нулевого правильного ребенка равна 0. Таким образом, максимальная величина равна 0, затем 1 плюс 0.
|
||
|
||
#### Алгоритм высоты (дерева)
|
||
```
|
||
if tree = nil:
|
||
return 0
|
||
return 1 + Max(Height(tree.left),Height(tree.right))
|
||
```
|
||
|
||
#### Вот код в C ++
|
||
```
|
||
int maxDepth(struct node* node)
|
||
{
|
||
if (node==NULL)
|
||
return 0;
|
||
else
|
||
{
|
||
int rDepth = maxDepth(node->right);
|
||
int lDepth = maxDepth(node->left);
|
||
|
||
if (lDepth > rDepth)
|
||
{
|
||
return(lDepth+1);
|
||
}
|
||
else
|
||
{
|
||
return(rDepth+1);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Мы могли бы также посмотреть на вычисление размера дерева, которое является числом узлов.
|
||
|
||
* Опять же, если у нас есть дерево nil, у нас есть нулевые узлы.
|
||
* В противном случае мы имеем число узлов в левом дочернем элементе плюс 1 для себя плюс число узлов в правом дочернем элементе. Итак, 1 плюс размер левого дерева плюс размер правильного дерева.
|
||
|
||
#### Алгоритм размера (дерева)
|
||
```
|
||
if tree = nil
|
||
return 0
|
||
return 1 + Size(tree.left) + Size(tree.right)
|
||
```
|
||
|
||
#### Вот код в C ++
|
||
```
|
||
int treeSize(struct node* node)
|
||
{
|
||
if (node==NULL)
|
||
return 0;
|
||
else
|
||
return 1+(treeSize(node->left) + treeSize(node->right));
|
||
}
|
||
```
|
||
|
||
### Соответствующие видео на канале freeCodeCamp YouTube
|
||
|
||
* [Двоичное дерево поиска](https://youtu.be/5cU1ILGy6dM)
|
||
* [Двоичное дерево поиска: обход и высота](https://youtu.be/Aagf3RyK3Lw)
|
||
|
||
### Ниже приведены общие типы двоичных деревьев:
|
||
|
||
Полное двоичное дерево / строковое двоичное дерево: двоичное дерево является полным или строгим, если каждый узел имеет ровно 0 или 2 детей.
|
||
```
|
||
18
|
||
/ \
|
||
15 30
|
||
/ \ / \
|
||
40 50 100 40
|
||
```
|
||
|
||
В полном двоичном дереве количество листовых узлов равно числу внутренних узлов плюс один.
|
||
|
||
Полное двоичное дерево: двоичное дерево является полным двоичным деревом, если все уровни полностью заполнены, за исключением, возможно, последнего уровня, а последний уровень имеет все ключи как можно дальше
|
||
```
|
||
18
|
||
/ \
|
||
15 30
|
||
/ \ / \
|
||
40 50 100 40
|
||
/ \ /
|
||
8 7 9
|
||
|
||
``` |