樹是一種相對比較複雜的資料結構,它要麼為空白,要麼具有一個值並具有零個或多個孩子,每個孩子本身就是樹,這是一個遞迴的定義,二叉樹是樹的一種特殊形式,也是資料結構中最常用到的形式,二叉樹又可以根據左右孩子節點與跟節點的大小關係分為很多種,這裡以二叉搜尋樹為例學習,二叉搜尋樹有一個額外的特點:每一個節點的值比它的左子樹的所有節點的值都要大,但比它的右子樹的所有節點的值都要小,這個定義排除了樹中存在值相同的節點的可能性。二叉搜尋樹是一種關索引值快速尋找資料的優秀工具。
和分析堆棧、隊列一樣我們首先還是分析二叉搜尋樹提供的介面,首先就是資料的插入InsertBST()、資料的刪除DeleteBST()。既然它是一個尋找資料的優秀工具那麼必不可少的就是尋找介面了SerchBST(),和堆棧和隊列不同,二叉樹並沒有限制只能訪問某一個值,因此二叉樹具有一個基本操作遍曆。除此之外,那就是二叉搜尋樹的建立與銷毀。
介面如下:
/*二叉搜尋樹模組的介面定義*//***此處定義數的值的類型為整型 **在實際使用中可以根據自己的需要進行修改 */#define TREE_TYPE int #define TRUE 1#define FALSE 0 /***Insert()向樹添加一個新值,參數是需要被添加的值**它必須原先並不存在於樹中,**因為二叉搜尋樹不存在相同的值函數返回真或假 */int insertBST( TREE_TYPE value ); /***find()尋找一個特定的值,這個值作為第一個參數傳遞給函數**函數返回一個指向這個值的指標,若未找到函數返回空 */TREE_TYPE *findBST( TREE_TYPE value )/***pre_order_traverse**執行屬的前序走訪,它的參數是一個回呼函數指標,**它所指向的函數將在樹中處理每一個節點被調用、**節點的值作為參數傳遞給這個函數。 */ void pre_order_traverse( void (*callback)(TREE_TYPE value));
建立一個二叉樹實際上就是一個不斷插入新值的過程,因為這個新值必須插入到一個合適的位置才能繼續保持二叉搜尋樹的特性。插入的基本思路如下:
如果樹為空白:
把新值作為跟節點插入
否則:
如果新值小於當前節點的值:
把新值插入到當前節點的左子樹
否則:
把新值插入當前節點的右子樹
這個演算法是按照搜尋二叉樹的定義來完成的,可以看書這個演算法也是一個遞迴,並且是一個尾部遞迴,因此可以很容易使用迭代來代替遞迴提高程式的效率。
從二叉搜尋樹刪除一個值是比較困難的,從一顆數的中部刪除一個節點將導致它的的子樹和屬的其餘部分斷開,我們必須重新連結他們,並且面臨三種處理情況:刪除沒有孩子的節點;刪除有一個孩子的節點;刪除有兩個孩子的節點。第一種情況比較簡單,直接刪除該節點就可以了不會導致子樹斷開,因為這個節點已經沒有子樹了,刪除有一個孩子的節點時把這個節點的雙親節點和它的孩子串連起來就可以了,最後一種情況是比較困難的,因為一個節點有兩個孩子,它的雙親不能連結到它的兩個孩子,解決這個問題的一個策略是刪除它的左子樹最大的那個值,並用這個值代替原先被刪除的那個節點的值。
由於二叉搜尋樹的有序性,所以在樹中尋找一個也定的值是比較容易的,同意是一個遞迴的演算法:
如果樹為空白:
這個值不存在於樹中
否則:
如果這個值和根節點的值相等:
成功找到這個值
否則:
如果這個值小於根幾點的值:
尋找左子樹
否則:
尋找右子樹
這也是一個尾部遞迴可以使用迭代代替提高演算法的效率。
遍曆樹的節點有幾種不同的次序,最常用的是前序、中序、後序和層次遍曆。所有類型的遍曆都是從樹的跟節點或你希望開始遍曆的子樹的根節點開始。
前序走訪:簡單點記就是“根左右”,先查看根節點然後遍曆左子樹,遍曆左子樹也是“根左右”的方式,當做子樹遍曆完成以後開始遍曆右子樹也是以“根左右”的方式,也就是說遍曆也是一個遞迴的方式。
中序遍曆:可記作“左根右”;後序遍曆可以記作“左右根”。遍曆的方式可以參考前序走訪的方式。
層次遍曆:層次遍曆先從跟節點開始,同一層從左向右開始遍曆。一種比較普遍的做法是使用隊列來實現層次遍曆,當訪問某給節點時首先將這個節點的左右孩子進入隊列,然後依次從隊列中出列遍曆。
接下來就是實現二叉搜尋樹的各種是介面了,還是堆棧和隊列一樣,使用靜態數組、動態數組、鏈式結構來分別實現。
靜態數組的實現比較簡單,某個節點的左右孩子很容易計算處理,可以使用數組的下標來代替指標,因此不需要額外的空間來儲存指向左右節點的指標,計算公式如下:
節點N的雙親節點為: N/2 (注意這裡沒有問題,因為整數的除法會截去小數部分)
節點N的左孩子節點為: 2*N
節點N的右孩子節點為: 2*N+1
注意這個規則是建立在根節點是數組下標為1的元素,但是實際上數組都是以下標0開始的,如果不想忽略數組的第0個空間,可以將公式稍微修改一下:
節點N的雙親節點為: (N+1)/2 -1
節點N的左孩子節點為: 2*N + 1
節點N的右孩子節點為: 2*N + 2
由於數組元素的個數是預先確定的,不管二叉樹有沒有只有這個空間這個空間已經分配,因此必須使用一個特殊的值用於標識這個值是否被使用,這裡我們使用0值作為此節點未被使用的標識。
具體的實現方式如下:
/*使用靜態數組實現的二叉搜尋樹*/#include <stdio.h>#include "tree.h"/*定義用於儲存二叉樹的數組最大長度*/#define TREE_SIZE 100 /*定義一個特殊值用於標識節點未使用*/#define BLANK 0 static TREE_TYPE tree[TREE_SIZE];/***計算一個節點的左孩子的下標*/static int left_child( int current ){return current*2 + 1;}/***計算一個節點右孩子的下標 */ static int right_child( int current ){return current*2 + 2;}/***插入新值 */int insert( TREE_TYPE value ){int current;/*確保值為非 0 ,因為 0值用於標識一個未使用的節點*/if( value == BLANK ){printf( "插入的值不合法!/n" );return FALSE; }/*從根節點開始比較,使用迭代來代替遞迴*/current = 0;while( tree[current] != BLANK ){if( value < tree[current] ){current = left_child( current );}else{if( value == tree[current] ){printf( "需要插入的值已存在!/n" );return FALSE;}else{current = right_child( current );}}}/*需要插入的值是否超過了數組的範圍*/if( current >= TREE_SIZE ){printf( "插入的值已超出數組的範圍!/n" );return FALSE;} tree[current] = value;return TRUE;}/***尋找節點 */TREE_TYPE *find( TREE_TYPE value ){int current;/***首先還是判斷需要尋找的值是不是特殊的用於標識未使用的節點*/if( value != BLANK ){current = 0;while( (current < TREE_SIZE) && (tree[current] != value) ){if( value < tree[current] ){current = left_child( current );}else{current = right_child( current );}}if( current < TREE_SIZE ){return tree+current;}}return NULL; } /***二叉樹的遍曆,這裡將節點值列印出來反映遍曆的結果*/static void print( TREE_TYPE value ){printf( "%d/n", value );} /***do_pre_order_traverse**執行一層前序走訪即根左右,利用遞迴實現*/static void do_pre_order_traverse( int current, void (*print)( TREE_TYPE value ) ){if( (current < TREE_SIZE) && (tree[current] != BLANK) ){print( tree[current] );do_pre_order_traverse( left_child( current), print );do_pre_order_traverse( right_child( current ), print);}}/***二叉樹的遍曆 */ void pre_order_traverse( void ){do_pre_order_traverse( 0, print );}/***二叉樹節點的刪除**需分三種情況討論 */int delet( TREE_TYPE value ){TREE_TYPE *del;del = find( value );if( del != NULL ){/*若無孩子節點*/if( ((left_child(del-tree) > TREE_SIZE) || (tree[left_child(del-tree)] == BLANK))&& ( (right_child(del-tree) > TREE_SIZE) ||(tree[right_child(del-tree)] == BLANK)) ){tree[del-tree] = BLANK;}/*若有兩個孩子節點*/else{if( (left_child(del-tree) < TREE_SIZE) && (tree[left_child(del-tree)] != BLANK) && (right_child(del-tree) < TREE_SIZE) && (tree[right_child(del-tree)] != BLANK) ){ /* **尋找該節點左孩子中值最大的節點用於替換需要刪除的節點 **這裡也可以使用右孩子最小的節點替換需要刪除的節點 **在這裡尋找左孩子最大的節點實際上就是左孩子中最右邊的葉子節點 */ TREE_TYPE *right_max; right_max = del-tree; while( (right_max < TREE_SIZE)&&(tree[(int)right_max] != BLANK) ) { right_max = right_child( right_max ); } /*因為判斷是否是最右端的節點的緣故,因此最大值應該是其父節點*/ right_max = (TREE_TYPE *)(((int)right_max - 2)/2); /*使用左孩子的最大節點替換需要刪除的節點*/ tree[del-tree] = tree[(int)right_max]; tree[(int)right_max] = BLANK; }/*若只有一個孩子節點*/else{if( (right_child( del-tree ) < TREE_SIZE) && (tree[right_child(del-tree)] != BLANK) ){tree[del-tree] = tree[right_child(del-tree)];tree[right_child(del-tree)] = BLANK;}else{tree[del-tree] = tree[left_child(del-tree)];tree[left_child(del-tree)] = BLANK;}} }return TRUE;}printf( "您需要刪除的節點未找到!/n" );return FALSE;}
動態數組的實現只是多了一個建立數組的過程,其他實現方式和靜態數組的實現完全一樣。