269 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| ---
 | ||
| title: Binary Search Trees
 | ||
| localeTitle: Деревья двоичного поиска
 | ||
| ---
 | ||
| ## Деревья двоичного поиска
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| Дерево представляет собой структуру данных, состоящую из узлов, которые имеют следующие характеристики:
 | ||
| 
 | ||
| 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 
 | ||
| 
 | ||
| ``` |