讀書筆記 --- 數據結構與算法分析C語言描述 --- 12.22 --- Chapter4 樹 page70 - 76

4.2 二叉樹

二叉樹每個結點至多有兩個兒子,即可以擁有(0,1,2)個兒子。由此二叉樹的平均深度要比N小得多,爲O(sqrt(N)),再對於特殊類型的二叉樹:二叉查找樹(Binary Search Tree)深度平均值爲O(log N)。而特殊情況就是二叉樹是一棵斜樹,爲一個特殊的鏈表,深度可以達到N - 1。

4.2.1 實現

結點的兒子可以用指針指向他們,類似於一個雙向鏈表。

struct DualLinkedList {
	elemType elem;
	DualLinkedList *pre;
	DualLinkedList *next;
}

二叉樹的結構,之前已經附過一次代碼:

struct TreeNode {
	elemType elem;
	TreeNode *right;
	TreeNode *left;
}

記一個結論:具有N個結點的每一棵二叉樹都將需要N + 1個NULL指針。
下面附證明:
在這裏插入圖片描述
下面來探究一下二叉樹的具體應用。

4.2.2 表達式樹

一棵樹的葉結點均爲操作數(operand),而其餘結點均爲操作符(operator)。鑑於這裏本例子中給出的表達式 (a + (b * c)) + (((d * e) + f ) * g)所有的操作均爲二元,所以這棵特定的樹正好是二叉樹。因此結點含有多於兩個兒子或只有一個兒子的情況也是存在的,例如一目運算符(unary minus operator)。
在這裏插入圖片描述
我們日常司空見慣的算術式實質上就是一箇中綴表達式(infix expression),這個表達式即是由對表達式樹進行中序遍歷(inorder traversal)得出的。而我們所得出的前序表達式和後序表達式就不必我多說了吧,對應的就是表達式樹前序遍歷和後序遍歷對應的結果。

前序遍歷:+ + a * b c * + * d e f g
後序遍歷: a b c * + d e * f + g * + (熟悉嘛?棧裏討論過的逆波蘭表達式)

構造一棵表達式樹

構造一棵表達式樹與棧章節所講的中綴轉後綴表達式以及後綴轉中綴表達式非常的相似,我們這裏的構造也需要藉助到棧,下面
是構造過程,首先是a,b兩個操作數(operand)入棧,遇到了操作符(operator),進行第一次整合。這裏我一個比較新穎的理解是:將這棵構造好的樹(constructed tree)放回棧中,對表達式中的operand再進行入棧的操作,遇到operator +進行第二次整合,取的是e,d。同時也將這棵constructed tree放回棧中,此時*入棧,將operand c與剛纔第二次整合的樹進行第三次整合,又得到一棵constructed tree,最後一個operator *入棧,將兩棵constructed tree進行最後一次整合得到最後的結果。不難發現按我這個整合的優先順序是:

operand + operand (1)
operand + constructed tree (2)
constructed tree + constructed tree(3)

按照之前後綴表達式得出結果的方法,即遇到操作數(operand)就入棧,遇到操作符(operator)便對最上方的兩個操作數進行x op y的操作按照這樣的優先級對所有元素進行整合。這裏也是一樣,只是將上方的operand/constructed tree進行了整合,有什麼就整合什麼。
在這裏插入圖片描述
在這裏插入圖片描述

4.3 查找樹ADT — 二叉查找樹(Binary Search Tree)

首先二叉查找樹的前提是要是一棵二叉樹,對於樹中所有結點X,他的左子樹所有關鍵字值需要小於X關鍵字值,右子樹所有關鍵字值需要大於X關鍵字值。這意味着該樹所有的元素可以用某種統一的方式排序。

4.3.1 makeEmpty

首先給出二叉查找樹的定義,和二叉樹是一模一樣的,沒有任何區別,只是我們如何去對內部結構進行調整。最開始的這個makeEmpty可以理解爲一個初始化操作。

struct BinarySearchTreeNode {
	elemType elem;
	TreeNode *right;
	TreeNode *left;
}
BinarySearchTreeNode* makeEmpty(BinarySearchTreeNode *T) {
	if ( T != nullptr ) {
		// 我們在這裏嚴格遵循了遞歸定義建立了一棵空樹
		makeEmpty(T->left);
		makeEmpty(T->right);
		free(T);
	}
	return nullptr;
}

4.3.2 find

找到BST中的目標元素

BinarySearchTreeNode* find(elem X, BinarySearchTreeNode *T) {
	if ( T == nullptr ) 
		return nullptr;
	
	// 目標元素比我當前位置元素值還要小,應該繼續往左找,因爲我需要更小的
	if ( X < T->elem )
		return find(X, T->left);
	// 目標元素比我當前位置元素值還要大,應該繼續往右找,因爲我需要更大的
	else if ( X > T->elem )
		return find(X, T->right);
	// 匹配到目標元素,返回該結點
	else 
		return T;
}

4.3.3 findMin & findMax

找到BST中的最大 & 最小的元素
書上給出的找最小元素使用了遞歸,而在找最大元素時使用了非遞歸。
而在BST中找到最小/最大的元素,找的就是左子樹最左邊的元素/右子樹最右邊的元素。

BinarySearchTreeNode* findMin(BinarySearchTreeNode *T) {
	if ( T == nullptr ) 
		return nullptr;
	
	// 找到最左邊的結點(注意並不是葉子結點)只需要判斷T->left是否爲空
	else if ( T->left == nullptr ) 
		return T;
	else
		return findMin(T->left);
}
BinarySearchTreeNode* findMax(BinarySearchTreeNode *T) {
	// 非遞歸,迭代找最右邊結點,不做過多解釋了
	if ( T != nullptr )
		while ( T->right != nullptr )
			T = T->right;

	return T;
}

4.3.4 insert

思路就是利用find()進行查找,找到BST中有對應的元素則什麼也不用做(或進行一次更新),否則就將元素插入到遍歷路徑的最後一點上。舉個例子來講吧,你要插入一個元素X,首先你要從根這個"分叉路口"開始走,比根小走左邊,比根大走右邊…重複這個過程,直到你不再有"分叉路口"可以選擇。

BinarySearchTreeNode* insert(elem X, BinarySearchTreeNode *T){
	if ( T == nullptr ) {
		T = malloc(sizeof(BinarySearchTreeNode));
		if ( T == nullptr )
			fatalError("Out of Space");
		else {
			T->elem = X;
			T->left = T->right = nullptr;
		}
	}
	
	else if ( X < T->elem )
		T->left = insert(X, T->left);
	else if ( X > T->elem ) 
		T->right = insert(X, T->right);

	return T;
}

書上給的這段代碼還是更側重於遞歸的講解。教材上給的先通過指針定位到search的上一個位置,再通過最後一次與這個位置的元素值的大小進行放在左邊還是右邊。我把教材上的代碼也濃縮一下貼在這裏。

bool insertBST(BiTree *T, int key) {
	BiTree p, s;
	// bool search()定義的就是假設元素並沒有找到,就定位到最後的分叉路口的位置p
	if ( !searchBST(*T, key, nullptr, &p) ) {
		s = malloc(sizeof(BiTNode));
		s->data = key;
		s->lchild = s->rchild = nullptr;
		
        // p爲nullptr的原因是其值比根的值還要大,所以令p成爲新的根
		if ( !p )
			*T = s;
		else if ( key < p->data )
			p->lchild = s;
		else if ( key > p->data )
			p->rchild = s;
	
		return true;
	}
	else 
		return false;
}

這個新手應該也可以理解,理解難度比前一種遞歸小很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章