Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) Kap. 7, Sid 1 BINÄRA TRÄD Träd används för att representera olika slags hierarkier som ordnats på något sätt. Den mest använda trädstrukturen är binära träd, i vilka varje nod kan ha noll, en eller två söner. Följande postdefinition kan användas för att lagra heltal i ett binärt träd: struct int_bt_node { int i; struct int_bt_node struct int_bt_node }; *left; *right; Pekaren "left" pekar sålunda till det vänstra delträdet om ett sådant finns och "right" pekar till det högra delträdet om ett sådant finns. Träd kan användas för snabb lagring av och snabb sökning i sorterad data. Ett binärt träd av heltalen 0, 12, -3, -8, 6, 6 byggs upp så, att 0 placeras i trädets rotnod och därefter insätts ett mindre tal till vänster om en nod och ett icke mindre tal till höger. Detta illustreras nedan (X = pekarvärdet NULL): struct int_bt_node *pivot, *ny; tomt träd pivot: lägg till 0 pivot: X 0 X X lägg till 12 0 pivot: X 12 X X lägg till –3 0 pivot: -3 12 X X X X Algoritmer och Datastrukturer lägg till –8 Kary FRÄMLING/Göran PULKKIS (v2003) pivot: Kap. 7, Sid 2 0 12 -3 X -8 X X X X Lägg till 6 pivot: 0 12 -3 -8 6 X X X X Lägg till 6 X pivot: 0 12 -3 -8 X X X 6 X X X 6 X X Insättning av ett talvärde (t.ex.) i det binära trädet kan programmeras med pivot = add_tal(-3,pivot); om funktionen ”add_tal” är definierad som Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) Kap. 7, Sid 3 struct int_bt_node *add_tal(int tal, struct int_bt_node *trad) { struct int_bt_node *ny; ny = (struct int_bt_node *)malloc(sizeof(struct int_bt_node)); ny->i = tal; ny->left = ny->right = NULL; return add_nod(ny,trad); } och funktionen ”add_nod” är rekursivt definierad som struct int_bt_node *add_nod(struct int_bt_node *nynod, struct int_bt_node *gamtrad) { if (gamtrad == NULL) return nynod; if (nynod->i < gamtrad->i) gamtrad->left = add_nod(nynod,gamtrad->left); else gamtrad->right = add_nod(nynod,gamtrad->right); return gamtrad; } Heltalen sparas sorterade i det binära trädet. Utskrift av varje heltal på egen rad börjande från det minsta heltalet sker med anrop av de rekursiva funktionen void visa(struct int_bt_nod *rot) { if (rot != NULL) { visa(rot->left); printf(“%d\n”,rot->i); visa(rot->right); } } Exempel 1. Modulärt programmerat binärt träd för sortering av bokstäver (kompilerat och kört). /* BinTree.h */ typedef struct bt_node { char c; struct bt_node *left; struct bt_node *right; } BinTreeNode; typedef struct { BinTreeNode } BinTree; *pivot; BinTree *newBinTree(void); int add(BinTree *bt, char c); int removeNode(BinTree *bt, char c); void show(BinTree *bt); void freeBinTree(BinTree *bt); Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) /* BTmain.c */ #include <stdio.h> #include <string.h> #include "BinTree.h" main() { BinTree *bt; char inp_str[] = "hejsan allihopa!"; int i; /* Vi skapar en ny ko */ bt = newBinTree(); /* Vi satter in hela teckenstrangen */ for ( i = 0 ; i < strlen(inp_str) ; i++ ) add(bt, inp_str[i]); /* Visa tradets innehall */ show(bt); /* Frigor minnet */ freeBinTree(bt); } /* BinTree.c */ #include <stdio.h> #include <stdlib.h> #include "BinTree.h" static void recAdd(BinTreeNode **root, BinTreeNode *newNode); static void recShow(BinTreeNode *root); static void recFree(BinTreeNode *root); BinTree *newBinTree(void) { return (BinTree*) calloc(1, sizeof(BinTree)); } int add(BinTree *bt, char c) { BinTreeNode *new_node; new_node = (BinTreeNode*) calloc(1, sizeof(BinTreeNode)); new_node->c = c; if ( new_node == NULL ) return 0; recAdd(&bt->pivot, new_node); return 1; } void show(BinTree *bt) { recShow(bt->pivot); printf("\n"); } Kap. 7, Sid 4 Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) Kap. 7, Sid 5 void freeBinTree(BinTree *bt) { recFree(bt->pivot); bt->pivot = NULL; } static void recAdd(BinTreeNode **root, BinTreeNode *newNode) { if ( *root == NULL ) *root = newNode; else if ( (*root)->c > newNode->c ) recAdd(&(*root)->left, newNode); else recAdd(&(*root)->right, newNode); } static void recShow(BinTreeNode *root) { if ( root != NULL ) { recShow(root->left); putchar(root->c); recShow(root->right); } } static void recFree(BinTreeNode *root) { if ( root != NULL ) { recFree(root->left); recFree(root->right); free(root); } } Programmet körs: !aaaehhijllnops Programmet sorterar alltså tecknen i teckensträngen "hejsan allihopa!", vilket fungerar fint i och med att vi endast har små tecken som finns i det engelska alfabetet. Alla operationer på trädet görs rekusivt, vilket är typiskt för trädstrukturer. Den algoritm som systematiskt används här kallas för "depth- first" eftersom den alltid går till ändan på varje gren före den fortsätter med följande. En annan algoritm som kallas för "breadth- first" går igenom trädet en nivå i taget. Insättningen av en ny nod är en relativt enkel operation. Däremot är det mera komplicerat att plocka bort en nod, eftersom vi måste behandla tre specialfall skilt, d.v.s. om noden har noll, en eller två söner. Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) Kap. 7, Sid 6 För att implementera borttagandet av en nod börjar tilläggs följande funktionsdeklarationer i filen "BinTree.c": static BinTreeNode *recRemove(BinTreeNode *node, char c); static BinTreeNode *findMin(BinTreeNode *node); Sedan modifieras "main"- funktionen så att två bokstäver plockas bort från trädet, varefter trädets innehåll visas pånytt: /* Plocka bort ett par noder och visa panytt. */ removeNode(bt, 'a'); removeNode(bt, 'i'); show(bt); Funktionsdefinitionerna ser ut enligt följande: int removeNode(BinTree *bt, char c) { if ( bt == NULL || bt->pivot == NULL ) return 0; return ( recRemove(bt->pivot, c) == NULL ) ? 0 : 1; } static BinTreeNode *recRemove(BinTreeNode *node, char c) { BinTreeNode *tmp, *child; if ( node == NULL ) return NULL; /* Icke hittad! */ else if ( c < node->c ) /* Vanster */ node->left = recRemove(node->left, c); else if ( c > node->c ) /* Hoger */ node->right = recRemove(node->right, c); else if ( node->left && node ->right ) { /* Tva soner */ tmp = findMin(node->right); node->c = tmp->c; node->right = recRemove(node->right, node->c); } else { /* En eller ingen son */ tmp = node; if ( node->left == NULL ) child = node->right; if ( node->right == NULL ) child = node->left; free(tmp); return child; } return node; } static BinTreeNode *findMin(BinTreeNode *node) { while ( node->left != NULL ) node = node->left; return node; } Först granskas att trädet existerar och att det inte är tomt. Annars söks den nod som skall förstöras. Algoritmer och Datastrukturer Kary FRÄMLING/Göran PULKKIS (v2003) Kap. 7, Sid 7 I det här exemplet antas att noden hittas. Om noden inte hittas fungerar detta program inte alltid rätt! Principen för borttagning av en nod för de tre möjliga fallen är följande: 1. Inga söner: noden kan tas bort direkt och NULL returneras som nytt pekarvärde. 2. En son: noden kan tas bort direkt och pekaren till nodens son returneras. 3. Två söner: den minsta noden nmin i det högra delträdet uppsökes, dess värde ersätter det gamla värdet för den nod som skall tas bort, varefter vi kan plocka bort noden nmin. Andra mycket använda trädstrukturer är så kallade "B-trees", vars noder kan ha flera än två söner. Detta kan vara fördelaktigt för att göra det skapade trädet mera balanserat t.ex. för sökning. Sorteringen som utförs i föregående exempel ger ett välbalanserat träd om den första bokstaven råkar vara ungefär i mitten av alfabetet. I ett sådant träd blir framförallt sökning mycket effektivt. Om bokstäverna däremot är färdigt sorterade får vi ett helt obalanserat träd. För att undvika detta har algoritmer utvecklats för att balansera träd. Dessa behandlas dock inte i denna kurs.