СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Обходы бинарного дерева в ширину и в глубину (pre-order — CLR, RCL, LCR — in-order) в черновиках Из песочницы

Всем привет! Сегодня мы вспомним азы программирования на C# и повторим, что такое бинарные деревья, чем они отличаются от остальных деревьев, что же такое обход и какой он бывает.



Для начала повторим самое основное. Дерево – связный граф без циклов. Корень – самая верхняя вершина дерева. Лист – вершина, не имеющая потомков. Обход дерева – систематический просмотр всех вершин, при котором каждая вершина встречается один раз.

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

Реализуем же все перечисленные алгоритмы на языке C#!

Тех, кто хочет немного обновить в памяти данный материал, либо еще только учится программировать базовые алгоритмы, без которых не обойдется ни один программист, прошу под кат!) Все фрагменты кода подробно прокомментированы, а демонстрационный проект на языке C# (VS2012) опубликован на GitHub.

Опишем класс Tree на языке C#:

    public class Tree
    {
        // подкласс "элемент дерева"
        public class TreeNode
        {
            public int Value; // численное значение
            public int Count = 0; // сколько раз было добавлено данное численное значение
            public TreeNode Left; // левое поддерево
            public TreeNode Right; // правое поддерево
        }
        public TreeNode Node; // экземпляр класса "элемент дерева"
    }

И реализуем методы:
        // добавление значения в дерево (рекурсивно)
 private void AddRecursion(ref TreeNode node, int val)
        // печать дерева (рекурсивно) 
 private void PrintRecursion(TreeNode node, int spaces, ref string s)
        // обход дерева в ширину (итерационно, используется очередь)
 private void Across(TreeNode node, ref string s, bool detailed)
        // прямой обход (CLR - center, left, right)
 private void CLR(TreeNode node, ref string s, bool detailed)
        // обратный обход (LCR - left, center, right) 
 private void LCR(TreeNode node, ref string s, bool detailed)
        // концевой обход (RCL - left, right, center)
 private void RCL(TreeNode node, ref string s, bool detailed)

Обход в глубину


Сперава разберемся с его видами:
  • Прямой обход (CLR – center, left, right). Сначала берется значение корня, затем обходится левое поддерево, затем правое.
  • Концевой обход (RCL – right, center, left). Сначала обходится правое поддерево, затем берется значение корня, затем левое.
  • Обратный обход (LCR – left, center, right). Сначала обходится левое поддерево, затем берется значение корня, затем правое поддерево.

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

Программная реализация всех трех перечисленных алгоритмов схожа, используется рекурсивный вызов методов. Сложность алгоритма O(N), где N – число вершин.

Прямой обход:
// прямой обход (CLR - center, left, right)
private void CLR(TreeNode node, ref string s, bool detailed)
{
    /*
     Аргументы метода:
     1. TreeNode node - текущий "элемент дерева" (ref  передача по ссылке)       
     2. ref string s - строка, в которой накапливается результат (ref - передача по ссылке)
    */
    if (node != null)
    {
        if (detailed)
            s += "    получили значение " + node.Value.ToString() + Environment.NewLine;
        else
            s += node.Value.ToString() + " "; // запомнить текущее значение
        if (detailed) s += "    обходим левое поддерево" + Environment.NewLine;
            CLR(node.Left, ref s, detailed); // обойти левое поддерево
        if (detailed) s += "    обходим правое поддерево" + Environment.NewLine;
            CLR(node.Right, ref s, detailed); // обойти правое поддерево
    }
    else if (detailed) s += "    значение отсутствует - null" + Environment.NewLine;
}

Обратный обход:
// обратный обход (LCR - left, center, right) 
private void LCR(TreeNode node, ref string s, bool detailed)
{
    /*
     Аргументы метода:
     1. TreeNode node - текущий "элемент дерева" (ref - передача по ссылке)       
     2. ref string s - строка, в которой накапливается результат (ref - передача по ссылке)
    */
    if (node != null)
    {
        if (detailed) s += "    обходим левое поддерево" + Environment.NewLine;
        LCR(node.Left, ref s, detailed); // обойти левое поддерево
        if (detailed)
            s += "    получили значение " + node.Value.ToString() + Environment.NewLine;
        else
            s += node.Value.ToString() + " "; // запомнить текущее значение
        if (detailed) s += "    обходим правое поддерево" + Environment.NewLine;
        LCR(node.Right, ref s, detailed); // обойти правое поддерево
    }
    else if (detailed) s += "    значение отсутствует - null" + Environment.NewLine;
}

Концевой обход:
// концевой обход (RCL -right, center, left)
private void RCL(TreeNode node, ref string s, bool detailed)
{
    /*
     Аргументы метода:
     1. TreeNode node - текущий "элемент дерева" (ref  передача по ссылке)       
     2. ref string s - строка, в которой накапливается результат (ref - передача по ссылке)
    */
    if (node != null)
    {
        if (detailed) s += "    обходим правое поддерево" + Environment.NewLine;
        RCL(node.Right, ref s, detailed); // обойти правое поддерево
        if (detailed)
            s += "    получили значение " + node.Value.ToString() + Environment.NewLine;
        else
            s += node.Value.ToString() + " "; // запомнить текущее значение
        if (detailed) s += "    обходим левое поддерево" + Environment.NewLine;
        RCL(node.Left, ref s, detailed); // обойти левое поддерево
    }
    else if (detailed) s += "    значение отсутствует - null" + Environment.NewLine;
}

Обход в ширину


Поддеревья посещаются уровень за уровнем, каждый уровень обходится слева направо. Сложность алгоритма O(V+E), где V – множество вершин, а E-множество дуг.
// обход дерева в ширину (итерационно, используется очередь)
private void Across(TreeNode node, ref string s, bool detailed)
{
    /*
     Аргументы метода:
     1. TreeNode node - текущий "элемент дерева" (ref  передача по ссылке)       
     2. ref string s - строка, в которой накапливается результат (ref - передача по ссылке)
    */
    var queue = new Queue<TreeNode>(); // создать новую очередь
    if (detailed) s += "    заносим в очередь значение " + node.Value.ToString() + Environment.NewLine; queue.Enqueue(node); // поместить в очередь первый уровень
    while (queue.Count!=0) // пока очередь не пуста
    {    
        //если у текущей ветви есть листья, их тоже добавить в очередь
        if (queue.Peek().Left != null)
        {
            if (detailed) s += "    заносим в очередь значение " + queue.Peek().Left.Value.ToString() + " из левого поддерева" + Environment.NewLine;
            queue.Enqueue(queue.Peek().Left);
        }
        if (queue.Peek().Right != null)
        {
            if (detailed) s += "    заносим в очередь значение " + queue.Peek().Right.Value.ToString() + " из правого поддерева" + Environment.NewLine;
            queue.Enqueue(queue.Peek().Right);
        }
        //извлечь из очереди информационное поле последнего элемента
        if (detailed) s += "    извлекаем значение из очереди: " + queue.Peek().Value.ToString() + Environment.NewLine;
        else s += queue.Peek().Value.ToString() + " "; // убрать последний элемент очереди
        queue.Dequeue();
    }
} 

С использованием данных методов я по-быстрому написал такую вот небольшую программку:





А теперь самое интересное — примеры!


Пример 1


Пример: дерево с большим числом поддеревьев, «правых» и «левых».
В глубину CLR: 5 4 3 1 9 8 6 7 15 10
В глубину LCR: 1 3 4 5 6 7 8 9 10 15
В глубину RCL: 15 10 9 8 7 6 5 4 3 1
В ширину: 5 4 9 3 8 15 1 6 10 7

Пример2


Пример: только отрицательные значения в дереве.
В глубину CLR: -2 -4 -15 -5 -1
В глубину LCR: -15 -5 -4 -2 -1
В глубину RCL: -1 -2 -4 -5 -15
В ширину: -2 -4 -1 -15 -5

Пример 3


Пример: отрицательные и положительные значения в дереве
В глубину CLR: 5 4 3 -30 0 1 2 15 8 30
В глубину LCR: -30 0 1 2 3 4 5 8 15 30
В глубину RCL: 30 15 8 5 4 3 2 1 0 -30
В ширину: 5 4 15 3 8 30 -30 0 1 2

Пример 4


Пример: только правые поддеревья
В глубину CLR: 4 5 6 7 8
В глубину LCR: 4 5 6 7 8
В глубину RCL: 8 7 6 5 4
В ширину: 4 5 6 7 8

Пример 5


Пример: только левые поддеревья
В глубину CLR: 10 9 8 7 6
В глубину LCR: 6 7 8 9 10
В глубину RCL: 10 9 8 7 6
В ширину: 10 9 8 7 6

Пример 6


Пример: наиболее емкая наглядность обходов
В глубину CLR: 10 9 11
В глубину LCR: 9 10 11
В глубину RCL: 11 10 9
В ширину: 10 9 11

Исходники проекта


Полные исходники программы для работы с бинарными деревьями выкладываю на GitHub. Можете свободно пользоваться ими для своих проектов или для более локальных задач, к примеру, выполнения лабораторных работ в университете. Присутствуют возможности загрузки исходных данных из текстового файла, сохранения результатов в файл, а также распечатки подробных действий реализации алгоритма (то есть логирование).

Спасибо за внимание! Если будут вопросы или дополнения — рад выслушать.

комментарии (10)

0
+1 –1
nsinreal ,   * (был изменён)
Вообще, если пишете на C# то методы обхода должны возвращать ленивый IEnumerable<T>, но тогда логгирование нельзя будет делать.
+3
+4 –1
spiff ,  
Первый раз вижу аббревиатуры CLR, LCR, RCL. В универе нас учили — ЛКП (левое-корень-правое) и т.д. Вроде в CS принято in-order (по порядку — LCR по вашему). pre-order (СLR) и post-order (LRC).
+1
+2 –1
DreamWalker ,  
Поддерживаю. Если говорить об английских именованиях, то наиболее распространённые варианты — In-order, Pre-order и Post-order. В вики они именно так и называются.
+6
chersanya ,   * (был изменён)
Сложность алгоритма O(N*log2(N))

Нет же, сложность O(N) — откуда вы вообще логарифм взяли? Если даже имелась в виду глубина дерева, то она может быть любой от O(log2(N)) до N.

Сложность алгоритма O(V+E), где V – множество вершин, а E-множество дуг.

Здесь всё правильно, но у нас же не общий случай, а деревья — можно и написать, что сложность тоже O(n).

Корень – самая верхняя вершина дерева.

Не то, чтобы остальные определения отличаются особой строгостью — но это вообще убило) Как это самая верхняя? Корень — просто выбранная любым образом вершина дерева, и всё.

Вообще, надеюсь этот материал не для преподавания — запутаны и перепутаны и другие определения.
0
Dageron ,  
Спасибо, поправил! Конечно же O(N).
+2
+3 –1
PsyHaSTe ,   * (был изменён)
Void-методы с ref-параметрами, итеративная конкатегация строк, возвращение строки вместо IEnumerable(T)… Ужас, однако.
0
+1 –1
Dageron ,  
Про IEnumerable все понятно, но в чем беда с ref-ами?
0
Lertmind ,  
Лучше было бы ещё показать вариант со стеком, всё-таки несложно реализовывается для бинарный деревьев, в том же Кнуте есть описание.
В своё время на Python делал так:

class Node:
    def __init__(self, key, left=None, right=None):
        self.key = key
        self.left = left
        self.right = right

class Tree:
    def __init__(self):
        self.root = None

    def print_tree(self):
        stack = []
        p = self.root
        while True:
            if p == None:
                if not stack:
                    return
                p = stack.pop()
                print p.key # Обратный обход
                p = p.right
                continue
            
            #print p.key # Прямой обход
            stack.append(p)
            p = p.left

    # Другие методы
    # ...
0
nikitadanilov ,  
Замечательный, но мало упоминаемый факт: если в обходе в ширину заменить «Queue» на «Stack», то получится обход в глубину.
0
klirichek ,  
Посмотрите в примере 1:
В глубину LCR: 1 3 4 5 6 7 8 9 10 15
В глубину RCL: 15 10 9 8 7 6 5 4 3 1

явно либо на картинке, либо в выводе результата 6 и 7 перепутаны!