Tree Co prawda kiedyś dawno temu (w 2011 roku) zrobiłem wpis z implementacją drzewa binarnego. Znając jednak siebie z 2011 roku nie zaszkodzi zrobić takie szybkie przypomnieje i użyć wzorca iterator.
Zaczynamy od tworzenia gałęzi i liści drzewa binarnego. Każda gałąź ma wartość i może, ale nie musi mieć następne gałęzi.
W konstruktorze przekazuje dalsze instancje tej samej klasy. Co oznacza, że możemy stworzyć prawdziwy łańcuch łączący te klasy, a raczej drzewo.
public class Node<T>
{
public T Value;
public Node<T> Left, Right;
public Node<T> Parent;
public Node(T value)
{
Value = value;
}
public Node(T value, Node<T> left, Node<T> right)
{
Value = value;
Left = left;
Right = right;
left.Parent = right.Parent = this;
}
}
Mam więc drzewo binarne. Teraz jak po tym drzewie chodzić.
// 3
// / \
// 2 1
var root = new Node<int>(3,
new Node<int>(2),
new Node<int>(1));
Można przejść po drzewie na trzy sposoby. Umieszczę fragment wikipedi by nie popełnić błędu nazewnictwa metody.
- pre-order – prefiksowym, gdyż wynik odwiedzania poszczególnych węzłów jest trawestacją wyrażenia zawartego w strukturze AST do postaci przedrostkowej (notacji Łukasiewicza),
- in-order – infiksowym, gdyż trawestuje wyrażenie do postaci wrostkowej,
- post-order – postfiksowym, gdyż trawestuje wyrażenie do postaci przyrostkowej (odwrotnej notacji polskiej).
My zrobimy przejście in-order.
public class IteratorInOrder<T>
{
public Node<T> Current { get; set; }
private readonly Node<T> root;
private bool yieldedStart;
public IteratorInOrder(Node<T> root)
{
this.root = Current = root;
while (Current.Left != null)
Current = Current.Left;
}
public bool MoveNext()
{
throw new NotImplementedException();
}
}
Wygląda to całkiem nieźle i co najważniejsze korzystamy ze wzorca Iterator w jakiś sensowny sposób. Poziom skomplikowania tego kodu wzrośnie wraz z stworzeniem metody MoveNexr().
public bool MoveNext()
{
if (!yieldedStart)
{
yieldedStart = true;
return true;
}
if (Current.Right != null)
{
Current = Current.Right;
while (Current.Left != null)
Current = Current.Left;
return true;
}
else
{
var p = Current.Parent;
while (p != null && Current == p.Right)
{
Current = p;
p = p.Parent;
}
Current = p;
return Current != null;
}
}
Logika szukania następnego element w drzewie na pewno nie może być napisana krócej. Eee tylko Ciebie podpuszczam można tą logikę sprawdzania gałęzi drzewa napisać lepiej, ale o tym później.
Najpierw ustawiamy flagę, że zaczynamy spacer po kolekcji. Później będziemy sprawdzać gałęzi drzewa zaczynając od prawej stony.
Wygląda to dziwnie wiem, ale z drugiej strony całkiem nie dawno pisałem listy jednokierunkowe w JavaScript i wiesz mi pisanie algorytmów od zera jakby to był C++ zawsze wygląda dziwnie.
Mamy więc iteratora. Jak on działa?
var root = new Node<int>(-1, new Node<int>(-2),
new Node<int>(4,
new Node<int>(2, new Node<int>(1), new Node<int>(3)),
new Node<int>(5))
);
// 0
// / \
// -1 4
// / \
// 2 5
// / \
// 1 3
var it = new IteratorInOrder<int>(root);
while (it.MoveNext())
{
Console.WriteLine(it.Current.Value);
}
Aplikacja wyświetli wartości w następującej kolejności : -1,0,1,2,3,4,5
Zbudujmy też klasę, która zadeklaruję ten domyślny iterator w sobie.
public class BinaryTree<T>
{
private Node<T> root;
public BinaryTree(Node<T> root)
{
this.root = root;
}
public IteratorInOrder<T> GetEnumerator()
{
return new IteratorInOrder<T>(root);
}
}
Teraz możemy skorzystać z pętli foreach. Co ciekawe nie musimy implementować interfejsu IEnumerable. Kompilator sam znajduje metodę GetEnumerator(). Magia dynamicznej części C# zapewne na to pozwala.
var root2 = new Node<int>(0,
new Node<int>(-2, new Node<int>(-3), new Node<int>(-1)),
new Node<int>(4,
new Node<int>(2, new Node<int>(1), new Node<int>(3)),
new Node<int>(5))
);
var tree = new BinaryTree<int>(root2);
foreach (var node in tree)
Console.WriteLine(node.Value);
A jak to można ulepszyć? Po pierwsze ten przykład prosi się o rekurencję, ale metoda MoveNext() nie jest w stanie zapamiętać kontekstu za każdym razem, gdy ją wywołujemy.
Pamiętamy tylko poprzedni element. Teraz sobie myślisz ten styl programowania jest beznadziejny. Nie możliwe, że kolekcję w C# są tak napisane. No cóż te kolekcję korzystają jeszcze z czegoś.
Na pomoc przychodzi wyrażenie yield return.
Dzięki temu mamy prymitywna maszynę stanów. Teraz jak widzisz wszystkie zapytania są rekurencyjne. Tworzymy kolekcję z poleceń yield, a one korzystają z pętli foreach, która wywoła kolekcję z poleceń yield, a one skorzysta z pętli foreach...
..i tak dalej...i tak dalej, aż znajdziemy węzeł, który nie ma ani gałęzi po prawej, ani po lewej.
A potem wracamy do poprzedniego węzła i sprawdzamy wartość prawej gałęzi. Jak już to zrobimy to wracamy do tego wężła jeszcze raz i cofamy się do poprzedniej gałęzi.
public class BinaryTree<T>
{
public IEnumerable<Node<T>> NaturalInOrder
{
get
{
IEnumerable<Node<T>> TraverseInOrder(Node<T> current)
{
if (current.Left != null)
{
foreach (var left in TraverseInOrder(current.Left))
yield return left;
}
yield return current;
if (current.Right != null)
{
foreach (var right in TraverseInOrder(current.Right))
yield return right;
}
}
foreach (var node in TraverseInOrder(root))
yield return node;
}
}
private Node<T> root;
public BinaryTree(Node<T> root)
{
this.root = root;
}
}
Na koniec użyjmy LINQ aby scalić wartości zwrócone przez drzewo.
var root3 = new Node<int>(0,
new Node<int>(-2, new Node<int>(-3), new Node<int>(-1)),
new Node<int>(4,
new Node<int>(2, new Node<int>(1), new Node<int>(3)),
new Node<int>(6, new Node<int>(5), new Node<int>(7)))
);
var tree = new BinaryTree<int>(root3);
Console.WriteLine(string.Join(",",
tree.NaturalInOrder.Select(x =>
x.Value)));
Wynik to : -3,-2,-1,0,1,2,3,4,5,6,7
Podsumowanie:
Wzorzec projektowy iterator w C# jest bardzo ukryty . Zauważ, że nie wspieramy iteracji, w której moglibyśmy się cofać . Nie mamy metody MoveBack().
Słowo kluczowe yield na pewno bardzo uproszcza kod. Dla kogoś to wygląda jak kolekcja, a dla Ciebie może jest to tylko maszyna stanów, która wypluwa wartości według określonego algorytmu.
Pamiętaj zawsze może napisać taki kod w C#.
public IEnumerable<int> RandomNumbers
{
get
{
Random random = new Random();
for (int i = 0; i < 1000; i++)
{
yield return
random.Next(int.MinValue, int.MaxValue);
}
}
}
A dla kodu z tego poziomu dalej wygląda to, jak kolekcja z 1000 elementami.
Console.WriteLine(string.Join(",", tree2.RandomNumbers.Take(10)));