Бинарное дерево - это структура данных, в которой каждый узел имеет не более двух дочерних узлов. Ниже приведен пример двоичного дерева. Самый верхний узел - это корневой узел.

Все узлы внизу, к которым не прикреплены дочерние узлы, называются листовыми узлами.

Если двоичное дерево имеет высоту h, то может быть максимум 2⁽ ʰ⁺ ¹⁾-1 узлов. Пусть n будет количеством узлов. Затем:
= ›n = 2⁽ ʰ⁺ ¹⁾-1
=› n + 1 = 2⁽ ʰ⁺ ¹⁾
= ›log₂ (n + 1) = log₂ (2⁽ ʰ⁺ ¹⁾)
=› log₂ (n + 1) = (h + 1) log₂2
= ›log₂ (n + 1) -1 = h или h = log₂ (n + 1) -1
Итак, учитывая n узлов, оптимальная высота h дерева составляет log₂ (n + 1) -1

Корневой узел находится на уровне 0. На каждом уровне может быть максимум узлов, где l - это уровень.

Довольно математики! Теперь позвольте мне объяснить дерево двоичного поиска. Бинарное дерево поиска - это специальная структура данных, в которой данные в левом дочернем узле меньше, чем его родительский узел, а правый дочерний элемент больше, чем его родительский узел. Ниже приведен пример двоичного дерева поиска:

А в Go мы можем определить узел таким образом:

type Node struct{
 Data int
 Left *Node
 Right *Node
}

Как мы знаем, struct - это агрегированный тип данных, который содержит значения любого типа данных под одним зонтиком. Эти значения известны как поля.
В нашем примере есть три поля, которые принадлежат структуре Node, а именно Data для хранения целочисленных данных, Left для указания на левый дочерний элемент и Right для указания на правого дочернего элемента.

Моя структура бинарного дерева поиска выглядит более простой. Что-то вроде этого:

type BinSearchTree struct{
 root *Node
}

Узел root указывает на первый или корневой узел двоичного дерева поиска. Давайте посмотрим на логику вставки.

func (t *BinSearchTree) Insert(data int){
  newNode := Node{data, nil, nil}
  if t.root == nil {
    t.root = &newNode
    return
  }
  iter, prev := t.root, t.root
  for iter != nil {
    prev = iter
    if data < iter.Data {
      iter = iter.Left
    } else {
      iter = iter.Right
    }
  }
  if data < prev.Data {
    prev.Left = &newNode
  } else {
    prev.Right = &newNode
  }
}

Первая строка func (t *BinSearchTree) Insert(data int) сообщает нам, что функция Insert связана с struct BinSearchTree. В Go мы называем такие функции методами. (t *BinSearchTree) - это получатель, который сообщает нам, что Insert может вызываться только литералами типа BinSearchTree.

Как вы могли заметить, я не использовал рекурсию для вставки данных в подходящий узел, вместо этого использовал цикл for для поиска позиции, подходящей для вставки узла.

newNode := Node{data, nil, nil} создает новый узел, где newNode.Data - это data, newNode.Left - это nil, а newNode.Right - это nil.

Узел root в начале будет ноль. Таким образом, ссылка на первый узел присваивается root, как показано ниже:

if t.root == nil {
  t.root = &newNode
  return
}

Последующие вызовы Insert будут проверять это if условие (и будет оценено как ложное) перед тем, как продолжить. Чтобы вставить еще один узел, нам нужно проверить, больше или меньше данных корневого узла. Если данные меньше корневого узла, тогда нам нужно двигаться влево, иначе двигаться вправо, и это следует проверять итеративно, пока мы не найдем подходящее место, как показано ниже.

iter, prev := t.root, t.root
for iter != nil {
    prev = iter
    if data < iter.Data {
      iter = iter.Left
    } else {
      iter = iter.Right
    }
}

Найдя подходящее место, мы вставляем элемент:

if data < prev.Data {
    prev.Left = &newNode
} else {
    prev.Right = &newNode
}

Какой смысл создавать дерево, если мы не можем его пройти? Дерево двоичного поиска можно перемещать по порядку, по предварительному заказу и по почте.

В обходе по порядку мы переходим к левому дочернему элементу, затем к родительскому и затем к правому дочернему.
В обходе pre-order мы переходим к родительскому узлу, затем к левому дочернему элементу а затем правого дочернего.
В обходе post-order мы переходим к левому дочернему элементу, затем к правому дочернему элементу, а затем к родительскому узлу.

Метод обхода по порядку выглядит примерно так:

func (t *BinSearchTree) InOrder(node *Node){
 if node == nil {
  return
 }
 t.InOrder(node.Left)
 fmt.Println(node.Data)
 t.InOrder(node.Right)
}

Как вы заметили, рекурсивные методы, используемые для обхода двоичного дерева поиска. Полный исходный код можно найти здесь https://gist.github.com/sandeep-sarkar/07e47cdae0c63f14b695370536b434ab

Надеюсь, вам понравилась эта статья. Пожалуйста, поделитесь своим мнением в комментариях.