Matura z informatyki - nauka i materiały.

Struktury danych: drzewo binarne

drzewo_miniatura

Dawno się nie widzieliśmy! Właśnie z tego powodu przygotowałem dla Was kolejny wpis. Tym razem, nie podejmiemy się jednak tematu algorytmów sortowania, a kontynuować będziemy serię dotyczącą struktur danych. Zanim rozpoczniecie czytanie poniższego wpisu, zachęcam was do zapoznania się z poprzednimi częściami tej serii. Znajdziecie w nich opis stosu, listy jednokierunkowej oraz dwukierunkowej. W tych wpisach poznacie zagadnienia powiązane z podstawami struktur danych, które warto znać jako przyszły programista. Przejdźmy jednak do właściwej części tekstu, w której odpowiemy sobie na najważniejsze pytanie.

Czym jest drzewo?

Drzewo jako struktura danych, to grupa węzłów połączonych krawędziami. Węzeł początkowy, nazywany jest powszechnie korzeniem (root), a wierzchołki z których nie wychodzą żadne krawędzie, liśćmi (leafs). Każdy węzeł połączony z węzłem wyżej jest jego dzieckiem.

Jeżeli w drzewie każdy z węzłów ma maksymalnie dwoje dzieci, jest to drzewo binarne. W tym tekście omówimy sobie dokładniej BST (binary search tree) – podstawowy rodzaj drzewa binarnego.

BST, czyli binarne drzewo poszukiwań posiada jedną cechę charakterystyczną – po lewej stronie każdego poddrzewa znajdują się elementy mniejsze od wartości korzenia tego poddrzewa, a po prawej większe bądź równe. Może wydawać się to dość skomplikowane, lecz łatwo zrozumieć zasadę jego działania dzięki przykładowi.

BST – zasada działania

Weźmy drzewo, które posiada na początku tylko jeden węzeł (korzeń) o wartości 12. Chcąc dodać do tego drzewa nowy węzeł o wartości 5, przyrównujemy wartość nowego węzła do węzła początkowego. Jeśli będzie większa bądź równa idziemy na prawo, w przeciwnym wypadku idziemy na lewo. 5 jest mniejsze od 12, a co za tym idzie poruszamy się na lewo. Ze względu na to, że korzeń nie ma żadnych dzieci, lewa strona jest pusta. Oznacza to, że możemy wstawić w tym miejscu węzeł. Następnie wstawmy węzeł o wartości 33. Analogicznie poruszamy się w prawo, gdzie znajdziemy wolne miejsce na węzeł. Co należy jednak zrobić, gdy zechcemy w tym momencie wstawić do drzewa węzeł o wartości 7?

W pierwszym kroku przyrównujemy wartość nowego węzła do wartości korzenia. Ze względu na to, że wartość 7 jest mniejsza od 12, przechodzimy w lewo. Następnie przyrównujemy wartość 7 do wartości lewego dziecka korzenia, czyli 5. Wartość tego węzła jest mniejsza od wartości nowego, tak więc przechodzimy w prawo. Tam nie znajdziemy żadnego węzła, więc możemy wstawić w tym miejscu węzeł o wartości 7. Poniższa animacja pomoże wam zobrazować sobie ten przykład.

Podsumowując – gdy chcemy dodać jakikolwiek nowy węzeł staramy się znaleźć wolne miejsce, przechodząc w prawo, jeśli wartość nowego węzła jest większa bądź równa od aktualnie wskazywanego, lub w lewo, gdy jest mniejsza.



O wielu innych, równie przydatnych strukturach i algorytmach, przeczytasz w tej książce

Drzewo BST – implementacja w C++

Nim zaprezentuję wam kod, należy poruszyć ważną kwestię. Według zasad drzewa BST, każdy z węzłów musi przechowywać przynajmniej: zmienną wartości węzła, wskaźnik na rodzica, wskaźnik na lewe dziecko i wskaźnik na prawe dziecko. Jeśli dany węzeł nie ma dziecka, po danej stronie, wskaźnik należy ustawić na wartość NULL. Dzięki temu, jesteśmy w stanie zweryfikować w prosty sposób czy dany wskaźnik wskazuje na dziecko, czy na pustą przestrzeń.

Poniższa implementacja posiada funkcje pozwalające na dodanie węzła, usunięcie węzła oraz znalezienie maksymalnej i minimalnej wartości w drzewie.

//program do obslugi BST
//Jan Żwirek

#include <iostream>
#include <fstream>
#include <Windows.h>
#include <string>

using namespace std;

struct node {
	int key;
	struct node *left;
	struct node *right;
	struct node *parent;
};

//Wskaznik na korzen
struct node *root = NULL;

/*
Funkcja dodawania wezla - funkcja ta przechodzi przez drzewo
w poszukiwaniu wolnego miejsca na wezel. Jesli wartosc nowego wezla
jest mniejsza od aktualnego, przechodzi w lewo, w przeciwnym przypadku
przechodzimy w prawo. Gdy natkniemy sie na wartosc null wstawiamy tam
nowy wezel pamietajac o przypisaniu wskaznikow.
*/
void add_node(int val) {
	//tworzymy wskazniki wskazujace na aktualnie rozpatrywany wezel i nowy wezel
	struct node *now = root;
	struct node *addedNode = new node;

	//Przypisujemy wartosci do wezla
	addedNode->key = val;
	addedNode->left = NULL;
	addedNode->right = NULL;

	//Jesli korzen ma wartosc null, nowy wezel staje sie korzeniem
	if (root == NULL) {
		addedNode->parent = NULL;
		root = addedNode;
		return;
	}
	else {

		while (now!= NULL) {
			//wartosc nowego wezla jest wieksza badz rowna od wskazywanego
			if (now->key <= addedNode->key) {
				//jesli po prawej jest wolne miejsce dodajemy w nie wezel
				if (now->right == NULL) {
					addedNode->parent = now;
					now->right = addedNode;
					return;
				}
				//w innym przypadku przechodzimy w glab drzewa
				else {
					now = now->right;
				}
			}
			//wartosc nowego wezla jest mniejsza od wskazywanego
			if (now->key > addedNode->key) {
				//jesli po lewej jest wolne miejsce dodajemy w nie wezel
				if (now->left == NULL) {
					addedNode->parent = now;
					now->left = addedNode;
					return;
				} 
				//w innym przypadku przechodzimy w glab drzewa
				else {
					now = now->left;
				}
			}
		}
	}
}

/*
Funkcja usuwania wezla - funkcja przechodzi po drzewie w poszukiwaniu
wezla o zadanej wartosci. Jesli szukana wartosc jest mniejsza od wartosci 
aktualnie wskazywanego wezla, przechodzi w lewo, w przeciwnym przypadku
przechodzimy w prawo. Jesli znajdziemy wezel o zadanej wartosci, usuwamy go.
W przypadku gdy posiadal jakiekolwiek dzieci, wstawiamy je w wolne miejsce.
Jesli zadana wartosc nie znajduje sie w drzewie, zwracamy NULL.
*/
struct node* del_node_key(struct node* node, int find) {

	if (node == NULL) 
		return node;

	if (find < node->key) {
		return node->left = del_node_key(node->left, find);		//Rekurencyjne przejście do szukanej wartości
	}

	else if (find > node->key) {
		return node->right = del_node_key(node->right, find);
	}

	if (find == node->key) {
		//Jeśli drzewo składa sie z samego korzenia
		if (root->left == NULL && root->right == NULL) {
			delete(root);
			root = NULL;
			return NULL;
		}
		//Wsunięcie prawego syna na miejsce aktualnie wskazywanego
		if (node->left == NULL) {								
			struct node *tmp = node->right;
			delete(node);
			return tmp;
		}
		//Wsunięcie lewego syna na miejsce aktualnie wskazywanego
		else if (node->right == NULL) {
			struct node *tmp = node->left;
			delete(node);
			return tmp;
		}

		//Przypadek gdy wskazywany węzeł ma dwóch synów	
		struct node* tmp = node->right;							

		node->key = tmp->key;
		while (tmp->left != NULL) {
			tmp = tmp->left;
		}

		//Znalezienie następcy
		node->right = del_node_key(node->right, tmp->key);		
	}
	return node;
}

//Szukanie najwiekszej wartosci w drzewie - skrajny prawy wezel
int find_max() {
	struct node *now = root;
	//przejście do ostatniego elementu po prawej
	while (now->right != NULL) {
		now = now->right;
	}
	return now->key;
}

//Szukanie najmniejszej wartosci w drzewie - skrajny lewy wezel
int find_min() {
	struct node *now = root;
	//przejście do ostatniego elementu po lewej
	while (now->left != NULL) {
		now = now->left;
	}
	return now->key;
}

int main()
{
	bool isRunning = true;
	int answ, inpu;
	while (isRunning) {
		system("cls");
		cout << endl;

		cout << "###########MENU###########" << endl;
		cout << "[1] Dodaj wezel" << endl;
		cout << "[2] Usun wezel" << endl;
		cout << "[3] Znajdz najmniejsza wartosc" << endl;
		cout << "[4] Znajdz najwieksza wartosc" << endl;
		cout << "[5] Wyjdz" << endl;
		cout << "##########################" << endl;

		cin >> answ;
		cout << endl;

		switch (answ) {
		case 1: {
			cout << "Wporwadz wartosc wezla: ";
			cin >> inpu;
			add_node(inpu);
			break;
		}
		case 2: {
			cout << "Wprowadz wartosc do usuniecia: ";
			cin >> inpu;
			if (root == NULL) {
				cout << "Brak korzenia - drzewo jest puste" << endl;
			}
			system("PAUSE");
			break;
		}
		case 3: {
			cout << "Najmniejsza wartosc = " << find_min() << endl;
			system("PAUSE");
			break;
		}
		case 4: {
			cout << "Najwieksza wartosc = " << find_max() << endl;
			system("PAUSE");
			break;
		}
		case 5: {
			isRunning = false;
			break;
		}
		default: {
			cout << "Zly wybor!" << endl;
			system("PAUSE");
			break;
		}
		}
	}

	return 0;
}

Podsumowanie

Drzewa (a zwłaszcza te binarne) to niezwykle popularny rodzaj struktury danych w informatyce. Drzewa ułatwiają i przyspieszają wyszukiwanie, a także pozwalają w łatwy sposób operować na posortowanych danych. Warto więc znać zasadę ich działania.