зачем нужны бинарные деревья

Бинарное дерево на Go для новичка

зачем нужны бинарные деревья. 0*Y68pjL23Pr GkvE. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*Y68pjL23Pr GkvE. картинка зачем нужны бинарные деревья. картинка 0*Y68pjL23Pr GkvE.

Mar 6, 2020 · 4 min read

зачем нужны бинарные деревья. 1*fLRe YsCU Er4BkHumIjZQ. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-1*fLRe YsCU Er4BkHumIjZQ. картинка зачем нужны бинарные деревья. картинка 1*fLRe YsCU Er4BkHumIjZQ.

Введение

В этой статье мы сосредоточимся на том, как деревья бинарного поиска могут быть реализованы на Go и почему они предпочтительнее линейных структур данных, таких как массивы и связанные списки. Пока мы думаем о том, почему деревья имеют преимущество над массивами и списками, рассмотрим основные функциональные возможности, которые мы реализуем в этой статье.

Что такое дерево?

Дерево — это структура данных, используемая для представления иерархии. Оно обычно состоит из нескольких небольших деревьев. Деревья представляют собой набор узлов, соединенных ребрами. Каждый узел содержит данные определенного типа. Бинарное дерево — это тип дерева, который по определению может иметь максимум два дочерних узла.

зачем нужны бинарные деревья. 1*fLRe YsCU Er4BkHumIjZQ. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-1*fLRe YsCU Er4BkHumIjZQ. картинка зачем нужны бинарные деревья. картинка 1*fLRe YsCU Er4BkHumIjZQ.

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

Зачем нужны бинарные деревья?

Представьте себе, что вы находите элемент в массиве. Временная сложность с линейной структурой, такой как массив, — O(n), где n увеличивается по мере увеличения числа элементов для поиска.

Таким образом, когда мы ищем значение, если оно меньше корня, мы игнорируем всю правую часть дерева и рекурсивно повторяем поиск в левой части дерева.

Терминология

Дерево бинарного поиска на Go

Основное внимание в этой статье уделяется реализации бинарного дерева на Golang и нескольких функций для добавления или удаления узлов и т.д. Узел в Go может быть представлен как структура:

Вставка узла в дерево

Это довольно просто: мы постоянно сравниваем значение вставляемого узла с узлами в двоичном дереве. Если значение вставляемых данных меньше, чем сравниваемый узел, и если левый потомок равен нулю, мы можем вставить новый узел как левый. Иначе мы сравниваем его с левым поддеревом и повторяем процесс.

Каждая из этих функций принимает параметр.

Извлечение минимума

Эта функция рекурсивно находит минимальный элемент дерева:

Извлечение максимума

Эта функция находит максимальный элемент дерева. Подход тот же, но до достижения nil обходятся все правые потомки:

Обход бинарного дерева

В правильном дереве бинарного поиска обход всегда будет в порядке возрастания. Обход по порядку — это способ прохождения по двоичному дереву, при котором сначала идет левый дочерний элемент, затем родительский, а после — правый дочерний элемент узла. Код ниже показывает рекурсивный обход.

Заключение

Двоичные деревья поражают воображение, когда дело доходит до поиска данных, потому что в них он выполняется намного лучше, чем линейный поиск.

Правильное двоичное дерево будет иметь значение левого потомка меньше корня, а значение правого потомка больше корня.

Источник

Все что нужно знать о древовидных структурах данных

зачем нужны бинарные деревья. 2*OT4cQifa4EUhKQPRfhKi1Q. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-2*OT4cQifa4EUhKQPRfhKi1Q. картинка зачем нужны бинарные деревья. картинка 2*OT4cQifa4EUhKQPRfhKi1Q.

Jul 1, 2018 · 14 min read

зачем нужны бинарные деревья. 0*e75 K0f FOQ EW W. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*e75 K0f FOQ EW W. картинка зачем нужны бинарные деревья. картинка 0*e75 K0f FOQ EW W.

Деревья прекрасны. Вот рисунок, который я сделал ребенком

Когда вы впервые учитесь кодировать, общепринято изучать массивы в качестве «основной структуры данных».

В конце концов, вы также изучаете хэш-таблицы. Для получения степени по «Компьютерным наукам» (Computer Science) вам придется походить на занятия по структурам данных, на которых вы узнаете о связанных списках, очередях и стеках. Эти структуры данных называются «линейными», поскольку они имеют логические начало и завершение.

Однако в самом начале и зучения деревьев и графов мы можем оказаться слегка сбитыми с толку. Нам привычно хранить данные линейным способом, а эти две структуры хранят данные совершенно иначе.

Данная статья поможет вам лучше понять древовидные структуры данных и устранить все недоразумения на их счет.

Из этой статьи вы узнаете:

Давайте начнем наше учебное путешествие 🙂

Определения

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

Деревья являются широко известными нелинейными структурами. Они хранят данные не линейным способом, а упорядочивают их иерархически.

Давайте вплотную займемся реальными примерами

Что я имею в виду, когда я говорю иерархически?

Представьте себе генеалогическое древо отношений между поколениями: бабушки и дедушки, родители, дети, братья и сестры и т.д. Мы обычно организуем семейные деревья иерархически.

зачем нужны бинарные деревья. 0*Rz ycXAluLvy4AEk. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*Rz ycXAluLvy4AEk. картинка зачем нужны бинарные деревья. картинка 0*Rz ycXAluLvy4AEk.

Мое фамильное дерево

Приведенный рисунок — это мое фамильное древо. Тосико, Акикадзу, Хитоми и Такеми — мои дедушки и бабушки.

Тошиаки и Джулиана — мои родители.

ТК, Юдзи, Бруно и Кайо — дети моих родителей (я и мои братья).

Структура организации — еще один пример иерархии.

зачем нужны бинарные деревья. 0*p5bcPWSK4awbV8vz. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*p5bcPWSK4awbV8vz. картинка зачем нужны бинарные деревья. картинка 0*p5bcPWSK4awbV8vz.

Структура компании является примером иерархии

В HTML, объектная модель документа (DOM) представляется в виде дерева.

зачем нужны бинарные деревья. 0*PEDLDEqhVkUpf8iC. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*PEDLDEqhVkUpf8iC. картинка зачем нужны бинарные деревья. картинка 0*PEDLDEqhVkUpf8iC.

Объектная модель документа (DOM)

Техническое определение

Дерево представляет собой набор объектов, называемых узлами. Узлы соединены ребрами. Каждый узел содержит значение или данные, и он может иметь или не иметь дочерний узел.

зачем нужны бинарные деревья. 0*cvXr6aXya75Kj9PA. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*cvXr6aXya75Kj9PA. картинка зачем нужны бинарные деревья. картинка 0*cvXr6aXya75Kj9PA.

Первый узел дерева называется корнем. Если этот корневой узел соединен с другим узлом, тогда корень является родительским узлом, а связанный с ним узел — дочерним.

зачем нужны бинарные деревья. 0*WtDaK1tFJPehF60m. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*WtDaK1tFJPehF60m. картинка зачем нужны бинарные деревья. картинка 0*WtDaK1tFJPehF60m.

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

зачем нужны бинарные деревья. 0*0h1VhgU Jq4WS5PS. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*0h1VhgU Jq4WS5PS. картинка зачем нужны бинарные деревья. картинка 0*0h1VhgU Jq4WS5PS.

Листья — это последние узлы на дереве. Это узлы без потомков. Как и в реальных деревьях, здесь имеется корень, ветви и, наконец, листья.

зачем нужны бинарные деревья. 0*3C VDYof5Lqe17rA. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*3C VDYof5Lqe17rA. картинка зачем нужны бинарные деревья. картинка 0*3C VDYof5Lqe17rA.

Другими важными понятиями являются высота и глубина.

Высота дерева — это длина самого длинного пути к листу.

Глубина узла — это длина пути к его корню.

Справочник терминов

Бинарные деревья

Теперь рассмотрим особый тип деревьев, называемых бинарными или двоичными деревьями.

“В информатике бинарным (двоичным) деревом называется иерархическая структура данных, в которой каждый узел имеет не более двух потомков (детей). Как правило, первый называется родительским узлом, а дети называются левым и правым наследниками.” — Wikipedia

Рассмотрим пример бинарного дерева.

зачем нужны бинарные деревья. 0*sNcjPRa0RHXHdwvM. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*sNcjPRa0RHXHdwvM. картинка зачем нужны бинарные деревья. картинка 0*sNcjPRa0RHXHdwvM.

Давайте закодируем бинарное дерево

Как мы реализуем простое двоичное дерево, которое инициализирует эти три свойства?

Вот наш двоичный класс дерева.

Когда мы создаем наш узел, он не имеет потомков. Просто есть данные узла.

Давайте это проверим:

Перейдем к части вставки. Что нам нужно здесь сделать?

Мы реализуем метод вставки нового узла справа и слева.

Давайте это нарисуем 🙂

зачем нужны бинарные деревья. 0*9m3yZ6p j5KiAeBz. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*9m3yZ6p j5KiAeBz. картинка зачем нужны бинарные деревья. картинка 0*9m3yZ6p j5KiAeBz.

Вот программный код:

Еще раз, если текущий узел не имеет левого дочернего элемента, мы просто создаем новый узел и устанавливаем его в качестве left_child текущего узла. Или мы создаем новый узел и помещаем его вместо текущего левого потомка. Назначим этот левый дочерний узел в качестве левого дочернего элемента нового узла.

И мы делаем то же самое, чтобы вставить правый дочерний узел.

Но не полностью. Осталось протестировать.

Давайте построим следующее дерево:

зачем нужны бинарные деревья. 0*FXMXAmo5NYODhZSS. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*FXMXAmo5NYODhZSS. картинка зачем нужны бинарные деревья. картинка 0*FXMXAmo5NYODhZSS.

Подытоживая изображенное дерево, заметим:

Таким образом, вот код для нашего дерева следующий:

Теперь нам нужно подумать об обходе дерева.

У нас есть два варианта: поиск в глубину (DFS) и поиск по ширине (BFS).

• Поиск в глубину (Depth-first search, DFS) — один из методов обхода дерева. Стратегия поиска в глубину, как и следует из названия, состоит в том, чтобы идти «вглубь» дерева, насколько это возможно. Алгоритм поиска описывается рекурсивно: перебираем все исходящие из рассматриваемой вершины рёбра. Если ребро ведёт в вершину, которая не была рассмотрена ранее, то запускаем алгоритм от этой нерассмотренной вершины, а после возвращаемся и продолжаем перебирать рёбра. Возврат происходит в том случае, если в рассматриваемой вершине не осталось рёбер, которые ведут в не рассмотренную вершину. Если после завершения алгоритма не все вершины были рассмотрены, то необходимо запустить алгоритм от одной из не рассмотренных вершин.

• Поиск в ширину (breadth-first search, BFS) — метод обхода дерева и поиска пути. Поиск в ширину является одним из неинформированных алгоритмов поиска. Поиск в ширину работает путём последовательного просмотра отдельных уровней дерева, начиная с узла-источника. Рассмотрим все рёбра, выходящие из узла. Если очередной узел является целевым узлом, то поиск завершается; в противном случае узел добавляется в очередь. После того, как будут проверены все рёбра, выходящие из узла, из очереди извлекается следующий узел, и процесс повторяется.

Давайте подробно рассмотрим каждый из алгоритмов обхода.

Поиск в глубину (DFS)

DFS исследует все возможные пути вплоть до некоторого листа дерева, возвращается и исследует другой путь (осуществляя, таким образом, поиск с возвратом). Давайте посмотрим на пример с этим типом обхода.

зачем нужны бинарные деревья. 0*r1qNeNHhlGTJpu4. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-0*r1qNeNHhlGTJpu4. картинка зачем нужны бинарные деревья. картинка 0*r1qNeNHhlGTJpu4.

Результатом этого алгоритма будет: 1–2–3–4–5–6–7.

Давайте разъясним это подробно.

Проход в глубь дерева, а затем возврат к исходной точке называется алгоритмом DFS.

После знакомства с этим алгоритмом обхода, рассмотрим различные типы DFS-алгоритма: предварительный обход (pre-order), симметричный обход (in-order) и обход в обратном порядке (post-order).

Предварительный обход

Именно это мы и делали в вышеприведенном примере.

1. Записать значение узла.

2. Перейти к левому потомку и записать его. Это выполняется тогда и только тогда, когда имеется левый потомок.

3. Перейти к правому потомку и записать его. Это выполняется тогда и только тогда, когда имеется правый потомок.

Источник

Структуры данных: бинарные деревья. Часть 1

Интро

Этой статьей я начинаю цикл статей об известных и не очень структурах данных а так же их применении на практике.

В своих статьях я буду приводить примеры кода сразу на двух языках: на Java и на Haskell. Благодаря этому можно будет сравнить императивный и функциональный стили программирования и увидить плюсы и минусы того и другого.

Начать я решил с бинарных деревьев поиска, так как это достаточно базовая, но в то же время интересная штука, у которой к тому же существует большое количество модификаций и вариаций, а так же применений на практике.

Зачем это нужно?

Бинарные деревья поиска обычно применяются для реализации множеств и ассоциативных массивов (например, set и map в с++ или TreeSet и TreeMap в java). Более сложные применения включают в себя ropes (про них я расскажу в одной из следующих статей), различные алгоритмы вычислительной геометрии, в основном в алгоритмах на основе «сканирующей прямой».

В этой статье деревья будут рассмотрены на примере реализации ассоциативного массива. Ассоциативный массив — обобщенный массив, в котором индексы (их обычно называют ключами) могут быть произвольными.

Ну-с, приступим

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

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

У этого дерева корнем будет вершина A. Видно, что у вершины D отсутствует левый сын, у вершины B — правый, а у вершин G, H, F и I — оба. Вершины без сыновей принято называть листьями.

Каждой вершине X можно сопоставить свое дерево, состоящее из вершины, ее сыновей, сыновей ее сыновей, и т.д. Такое дерево называют поддеревом с корнем X. Левым и правым поддеревьями X называют поддеревья с корнями соответственно в левом и правом сыновьях X. Заметим, что такие поддеревья могут оказаться пустыми, если у X нет соответствующего сына.

Данные в дереве хранятся в его вершинах. В программах вершины дерева обычно представляют структурой, хранящей данные и две ссылки на левого и правого сына. Отсутствующие вершины обозначают null или специальным конструктором Leaf:

Как видно из примеров, мы требуем от ключей, чтобы их можно было сравнивать между собой ( Ord a в haskell и T1 implements Comparable в java). Все это не спроста — для того, чтобы дерево было полезным данные должны храниться в нем по каким-то правилам.

Какие же это правила? Все просто: если в вершине X хранится ключ x, то в левом (правом) поддереве должны храниться только ключи меньшие (соответственно большие) чем x. Проиллюстрируем:

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Что же нам дает такое упорядочевание? То, что мы легко можем отыскать требуемый ключ x в дереве! Просто сравним x со значением в корне. Если они равны, то мы нашли требуемое. Если же x меньше (больше), то он может оказаться только в левом (соответственно правом) поддереве. Пусть например мы ищем в дереве число 17:

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Функция, получающая значение по ключу:

Добавление в дерево

Теперь попробуем сделать операцию добавления новой пары ключ/значение (a,b). Для этого будем спускаться по дереву как в функции get, пока не найдем вершину с таким же ключем, либо не дойдем до отсутсвующего сына. Если мы нашли вершину с таким же ключем, то просто меняем соответствующее значение. В противно случае легко понять что именно в это место следует вставить новую вершину, чтобы не нарушить порядок. Рассмотрим вставку ключа 42 в дерево на прошлом рисунке:

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Лирическое отступление о плюсах и минусах функционального подхода

Если внимательно рассмотреть примеры на обоих языках, можно увидеть некоторое различие в поведении функциональной и императивной реализаций: если на java мы просто модифицируем данные и ссылки в имеющихся вершинах, то версия на haskell создает новые вершины вдоль всего пути, пройденного рекурсией. Это связано с тем, что в чисто функциональных языках нельзя делать разрушающие присваивания. Ясно, что это ухудшает производительность и увеличивает потребляемую память. С другой стороны, у такого подхода есть и положительные стороны: отсутствие побочных эффектов сильно облегчает понимание того, как функционирует программа. Более подробно об этом можно прочитать в практически любом учебнике или вводной статье про функциональное программирование.

В этой же статье я хочу обратить внимание на другое следствие функционального подхода: даже после добавления в дерево нового элемента старая версия останется доступной! За счет этого эффекта работают ropes, в том числе и в реализации на императивных языках, позволяя реализовывать строки с асимптотически более быстрыми операциями, чем при традиционном подходе. Про ropes я расскажу в одной из следующих статей.

Вернемся к нашим баранам

Теперь мы подобрались к самой сложной операции в этой статье — удалению ключа x из дерева. Для начала мы, как и раньше, найдем нашу вершину в дереве. Теперь возникает два случая. Случай 1 (удаляем число 5):

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Видно, что у удаляемой вершины нет правого сына. Тогда мы можем убрать ее и вместо нее вставить левое поддерево, не нарушая упорядоченность:

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Если же правый сын есть, налицо случай 2 (удаляем снова вершину 5, но из немного другого дерева):

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

Тут так просто не получится — у левого сына может уже быть правый сын. Поступим по-другому: найдем в правом поддереве минимум. Ясно, что его можно найти если начать в правом сыне и идти до упора влево. Т.к у найденного минимума нет левого сына, можно вырезать его по аналогии со случаем 1 и вставить его вместо удалеемой вершины. Из-за того что он был минимальным в правом поддереве, свойство упорядоченности не нарушится:

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

На десерт, пара функций, которые я использовал для тестирования:

Чем же все это полезно?

У читателя возможно возникает вопрос, зачем нужны такие сложности, если можно просто хранить список пар [(ключ, значение)]. Ответ прост — операции с деревом работают быстрее. При реализации списком все функции требуют O(n) действий, где n — размер структуры. (Запись O(f(n)) грубо говоря означает «пропорционально f(n)», более корректное описание и подробности можно почитать тут). Операции с деревом же работают за O(h), где h — максимальная глубина дерева (глубина — расстояние от корня до вершины). В оптимальном случае, когда глубина всех листьев одинакова, в дереве будет n=2^h вершин. Значит, сложность операций в деревьях, близких к оптимуму будет O(log(n)). К сожалению, в худшем случае дерево может выродится и сложность операций будет как у списка, например в таком дереве (получится, если вставлять числа 1..n по порядку):

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.

К счастью, существуют способы реализовать дерево так, чтобы оптимальная глубина дерева сохранялась при любой последовательности операций. Такие деревья называют сбалансированными. К ним например относятся красно-черные деревья, AVL-деревья, splay-деревья, и т.д.

Анонс следующих серий

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

Оставайтесь на связи!

Полезные ссылки

Исходники примеров целиком:

Также очень советую почитать книгу Кормен Т., Лейзерсон Ч., Ривест Р.: «Алгоритмы: построение и анализ», которая является прекрасным учебником по алгоритмам и структурам данных

Источник

Бинарные деревья поиска и рекурсия – это просто

Существует множество книг и статей по данной теме. В этой статье я попробую понятно рассказать самое основное.

Бинарное дерево — это иерархическая структура данных, в которой каждый узел имеет значение (оно же является в данном случае и ключом) и ссылки на левого и правого потомка. Узел, находящийся на самом верхнем уровне (не являющийся чьим либо потомком) называется корнем. Узлы, не имеющие потомков (оба потомка которых равны NULL) называются листьями.

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.
Рис. 1 Бинарное дерево

Бинарное дерево поиска — это бинарное дерево, обладающее дополнительными свойствами: значение левого потомка меньше значения родителя, а значение правого потомка больше значения родителя для каждого узла дерева. То есть, данные в бинарном дереве поиска хранятся в отсортированном виде. При каждой операции вставки нового или удаления существующего узла отсортированный порядок дерева сохраняется. При поиске элемента сравнивается искомое значение с корнем. Если искомое больше корня, то поиск продолжается в правом потомке корня, если меньше, то в левом, если равно, то значение найдено и поиск прекращается.

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.
Рис. 2 Бинарное дерево поиска
Сбалансированное бинарное дерево поиска — это бинарное дерево поиска с логарифмической высотой. Данное определение скорее идейное, чем строгое. Строгое определение оперирует разницей глубины самого глубокого и самого неглубокого листа (в AVL-деревьях) или отношением глубины самого глубокого и самого неглубокого листа (в красно-черных деревьях). В сбалансированном бинарном дереве поиска операции поиска, вставки и удаления выполняются за логарифмическое время (так как путь к любому листу от корня не более логарифма). В вырожденном случае несбалансированного бинарного дерева поиска, например, когда в пустое дерево вставлялась отсортированная последовательность, дерево превратится в линейный список, и операции поиска, вставки и удаления будут выполняться за линейное время. Поэтому балансировка дерева крайне важна. Технически балансировка осуществляется поворотами частей дерева при вставке нового элемента, если вставка данного элемента нарушила условие сбалансированности.

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.
Рис. 3 Сбалансированное бинарное дерево поиска

Сбалансированное бинарное дерево поиска применяется, когда необходимо осуществлять быстрый поиск элементов, чередующийся со вставками новых элементов и удалениями существующих. В случае, если набор элементов, хранящийся в структуре данных фиксирован и нет новых вставок и удалений, то массив предпочтительнее. Потому что поиск можно осуществлять алгоритмом бинарного поиска за то же логарифмическое время, но отсутствуют дополнительные издержки по хранению и использованию указателей. Например, в С++ ассоциативные контейнеры set и map представляют собой сбалансированное бинарное дерево поиска.

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.
Рис. 4 Экстремально несбалансированное бинарное дерево поиска

Теперь кратко обсудим рекурсию. Рекурсия в программировании – это вызов функцией самой себя с другими аргументами. В принципе, рекурсивная функция может вызывать сама себя и с теми же самыми аргументами, но в этом случае будет бесконечный цикл рекурсии, который закончится переполнением стека. Внутри любой рекурсивной функции должен быть базовый случай, при котором происходит выход из функции, а также вызов или вызовы самой себя с другими аргументами. Аргументы не просто должны быть другими, а должны приближать вызов функции к базовому случаю. Например, вызов внутри рекурсивной функции расчета факториала должен идти с меньшим по значению аргументом, а вызовы внутри рекурсивной функции обхода дерева должны идти с узлами, находящимися дальше от корня, ближе к листьям. Рекурсия может быть не только прямой (вызов непосредственно себя), но и косвенной. Например А вызывает Б, а Б вызывает А. С помощью рекурсии можно эмулировать итеративный цикл, а также работу структуры данных стек (LIFO).

Кратко обсудим деревья с точки зрения теории графов. Граф – это множество вершин и ребер. Ребро – это связь между двумя вершинами. Количество возможных ребер в графе квадратично зависит от количества вершин (для понимания можно представить турнирную таблицу сыгранных матчей). Дерево – это связный граф без циклов. Связность означает, что из любой вершины в любую другую существует путь по ребрам. Отсутствие циклов означает, что данный путь – единственный. Обход графа – это систематическое посещение всех его вершин по одному разу каждой. Существует два вида обхода графа: 1) поиск в глубину; 2) поиск в ширину.

Поиск в ширину (BFS) идет из начальной вершины, посещает сначала все вершины находящиеся на расстоянии одного ребра от начальной, потом посещает все вершины на расстоянии два ребра от начальной и так далее. Алгоритм поиска в ширину является по своей природе нерекурсивным (итеративным). Для его реализации применяется структура данных очередь (FIFO).

Поиск в глубину (DFS) идет из начальной вершины, посещая еще не посещенные вершины без оглядки на удаленность от начальной вершины. Алгоритм поиска в глубину по своей природе является рекурсивным. Для эмуляции рекурсии в итеративном варианте алгоритма применяется структура данных стек.

С формальной точки зрения можно сделать как рекурсивную, так и итеративную версию как поиска в ширину, так и поиска в глубину. Для обхода в ширину в обоих случаях необходима очередь. Рекурсия в рекурсивной реализации обхода в ширину всего лишь эмулирует цикл. Для обхода в глубину существует рекурсивная реализация без стека, рекурсивная реализация со стеком и итеративная реализация со стеком. Итеративная реализация обхода в глубину без стека невозможна.

Асимптотическая сложность обхода и в ширину и в глубину O(V + E), где V – количество вершин, E – количество ребер. То есть является линейной по количеству вершин и ребер. Записи O(V + E) с содержательной точки зрения эквивалентна запись O(max(V,E)), но последняя не принята. То есть, сложность будет определятся максимальным из двух значений. Несмотря на то, что количество ребер квадратично зависит от количества вершин, мы не можем записать сложность как O(E), так как если граф сильно разреженный, то это будет ошибкой.

DFS применяется в алгоритме нахождения компонентов сильной связности в ориентированном графе. BFS применяется для нахождения кратчайшего пути в графе, в алгоритмах рассылки сообщений по сети, в сборщиках мусора, в программе индексирования – пауке поискового движка. И DFS и BFS применяются в алгоритме поиска минимального покрывающего дерева, при проверке циклов в графе, для проверки двудольности.
Обходу в ширину в графе соответствует обход по уровням бинарного дерева. При данном обходе идет посещение узлов по принципу сверху вниз и слева направо. Обходу в глубину в графе соответствуют три вида обходов бинарного дерева: прямой (pre-order), симметричный (in-order) и обратный (post-order).

Прямой обход идет в следующем порядке: корень, левый потомок, правый потомок. Симметричный — левый потомок, корень, правый потомок. Обратный – левый потомок, правый потомок, корень. В коде рекурсивной функции соответствующего обхода сохраняется соответствующий порядок вызовов (порядок строк кода), где вместо корня идет вызов данной рекурсивной функции.

Если нам дано изображение дерева, и нужно найти его обходы, то может помочь следующая техника (см. рис. 5). Обводим дерево огибающей замкнутой кривой (начинаем идти слева вниз и замыкаем справа вверх). Прямому обходу будет соответствовать порядок, в котором огибающая, двигаясь от корня впервые проходит рядом с узлами слева. Для симметричного обхода порядок, в котором огибающая, двигаясь от корня впервые проходит рядом с узлами снизу. Для обратного обхода порядок, в котором огибающая, двигаясь от корня впервые проходит рядом с узлами справа. В коде рекурсивного вызова прямого обхода идет: вызов, левый, правый. Симметричного – левый, вызов, правый. Обратного – левый правый, вызов.

зачем нужны бинарные деревья. image loader. зачем нужны бинарные деревья фото. зачем нужны бинарные деревья-image loader. картинка зачем нужны бинарные деревья. картинка image loader.
Рис. 5 Вспомогательный рисунок для обходов

Для бинарных деревьев поиска симметричный обход проходит все узлы в отсортированном порядке. Если мы хотим посетить узлы в обратно отсортированном порядке, то в коде рекурсивной функции симметричного обхода следует поменять местами правого и левого потомка.

Надеюсь Вы не уснули, и статья была полезна. Скоро надеюсь последует продолжение банкета статьи.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *