二叉搜索樹

二叉搜索樹

注意:本文的算法和代碼思路大部分來自《算法導論》算法

什麼是二叉搜索樹

二叉搜索樹首先是一棵二叉樹,此外,它還能用來搜索。由於它知足這樣的性質:每一個結點的左子樹的結點值都比自身小,而它的右子樹的結點值都比自身大。數組

它長得像下面這樣:(依據建立時結點插入順序不一樣,多是滿二叉,也可能不是)數據結構

二叉搜索樹能夠很是方便的用來進行查找指定元素,查找最大值和最小值等。函數

定義數據結構

咱們能夠用鏈表或者數組的方式來實現一棵樹。這裏咱們採用鏈表的方式。post

首先定義節點結構,能夠看到,每一個結點有一個值,而且有兩個孩子,而且有一個父親。3d

此外,咱們還要額外定義一棵樹的結構,它很簡單,只有一個指向樹根的指針。指針

//樹結點結構
struct Node {
	int key;
	Node* left;
	Node* right;
	Node* parent;
};

//樹
struct Tree {
	Node* root;
};

二叉樹的遍歷

由於二叉搜索樹的特殊性質,咱們對其進行中序遍歷就能夠獲得全部元素的一個有序序列。code

遍歷既能夠用遞歸也能夠用迭代的方式去實現,這裏給出遞歸的版本:orm

//中序遍歷
void inorder_tree_walk(Node* x) {
	if (x != NULL) {
		inorder_tree_walk(x->left);
		cout << x->key << " ";
		inorder_tree_walk(x->right);
	}
}
//前序遍歷
void preorder_tree_walk(Node* x) {
	if (x != NULL) {
		cout << x->key << " ";
		preorder_tree_walk(x->left);
		preorder_tree_walk(x->right);
	}
}
//後續遍歷
void postorder_tree_walk(Node* x) {
	if (x != NULL) {
		postorder_tree_walk(x->left);
		postorder_tree_walk(x->right);
		cout << x->key << " ";
	}
}

遍歷二叉樹的時間複雜度是:O(n)。blog

如對上圖左邊的那棵樹進行中序遍歷的話,獲得的序列就是:2 3 4 5 7 9

查詢指定結點值

若是給出一個關鍵字,想要查詢其是否存在與二叉樹中,若是存在則返回指向它的結點的指針。這個過程能夠描述以下:首先把關鍵字和根結點的值作比較,若是相等則返回;不然,若是比根節點值小,那就遞歸的在左子樹中查找,不然就在右子樹中查找。

這個過程既能夠用遞歸去實現,也能夠用迭代去實現,這裏給出兩個版本:

//遞歸版
Node* tree_search(Node* x, int k) {
	//找到或者爲空
	if (x == NULL || k == x->key) {
		return x;
	}
	if (k < x->key) {
		return tree_search(x->left, k);
	}
	else {
		return tree_search(x->right, k);
	}
}

//迭代版
Node* iterative_tree_search(Node* x, int k) {
	while (x != NULL && k != x->key) {
		if (k < x->key) {
			x = x->left;
		}
		else {
			x = x->right;
		}
	}
	return x;
}

最大元素和最小元素

根據二叉搜索樹的性質,左子樹的結點值都比其父節點的小,而右子樹的相反。因此,只要從樹根開始,沿着左孩子進行查找,直到最後一個左孩子,那它確定就是最小值。最大值也是相似。

像這裏:

這裏給出迭代方式的實現:

//找最小結點
Node* tree_minimum(Node* x) {
	while (x->left != NULL) {
		x = x->left;
	}
	return x;
}
//找最大結點
Node* tree_maximum(Node* x) {
	while (x->right != NULL) {
		x = x->right;
	}
	return x;
}

前驅和後繼

一個結點的前驅和後繼是什麼呢?它是按照中序遍歷時,排在該結點前和後的第一個結點。

如:上圖的中序遍歷是:2 3 4 5 7 9 , 那5的前驅就是4,而其後繼就是7。

也就是,前驅是恰好比它小(或者等於)的元素,後繼是恰好比它大(或者等於)的元素。

那要怎麼找呢?

首先看這幅圖:

咱們先討論,有左子樹的結點的前驅,和有右子樹的結點的後繼。

首先,有左子樹的結點的前驅。咱們知道,一個節點的左子樹的值都比他自身小,因此它的前驅確定是在左子樹中,並且是左子樹中最大的那一個。好比說,結點6,它的前驅就是左子樹中最大的那個,也就是4。

而後,是有右子樹的結點的後繼。很顯然,它應該是它的右子樹中最小的那。好比說,結點

6,它的後繼就是右子樹中最小的那個,也就是7。

那麼,爲何有左子樹的前驅必定在左子樹中,而不可能在它的父系結點或其餘地方呢?

咱們能夠這樣考慮,看到結點13,它有一個左子樹,左子樹的結點值都比它小。它有一個父親7,且它是它父親的右孩子,因此父親也比它小。那有沒有可能,父親的某一個取值會使得它是13的前驅呢?答案是不可能的。由於前驅是比它小之中的最大的那個,而若是它有左子樹,那左子樹中的元素由於在它13的父親結點的右子樹中,因此確定比父親結點7要大,可是卻比13小。

接下來是,沒有左子樹的結點的前驅,和沒有右子樹的結點的後繼。

顯然,沒有左子樹的結點的前驅不可能在左子樹裏找,只能在其餘地方找。注意到,前驅和後繼實際上是對稱的關係,若是b的前驅是a,那麼a的後繼確定就是b。因此咱們要找到a的後繼,至關於要找到b的前驅。好比說咱們要找到結點7的前驅,那若是能找到某個結點,它的後繼是7,那就完事了。由於7沒有左子樹,因此7的前驅確定在父親結點上面。而由於6的後繼就是7,因此6就是7的前驅(能夠這樣驗證,由於6有右子樹,且7是右子樹中最小的那個,因此7是6的後繼)。因此這個前驅節點a知足這樣一個性質:它確定在結點b的父系結點上,而且,它是第一個使得b在它的右子樹中的結點。

相應的,沒有右子樹的結點的後繼也是相似求法。

這裏給出實現方法:

//前驅
Node* tree_predecessor(Node* x) {
	//若是左子樹非空,則前驅是左子樹中最大的結點
	if (x->left != NULL) {
		return tree_maximum(x->left);
	}
	//不然,找到父系結點中第一個使得它是其右子孫的結點
	Node* y = x->parent;
	while (y != NULL && y->left == x) {
		x = y;
		y = x->parent;
	}
	return y;
}

//後繼
Node* tree_successor(Node* x) {
	//若是右子樹非空,則後繼是右子樹中的最左結點
	if (x->right != NULL) {
		return tree_minimum(x->right);
	}
	//不然,找到父系結點中第一個使得它是其左子孫的結點
	Node* y = x->parent;
	while (y != NULL && y->right == x) {
		x = y;
		y = x->parent;
	}
	return y;
}

插入

首先,要明確一點,新結點確定是以葉節點的形式插入的。而咱們要找的就是那個能收養它的父結點。

好比說,咱們要在這棵樹裏插入結點8:

首先,8和5比較,比5大,在右子樹中查找。而後和9比較,比9小;最後和7比較,比7大,但由於7已經沒有右子樹了,因此就把8掛在7的右子樹上。

實現以下:

void tree_insert(Tree* T, Node* z) {
	Node* y = NULL;  //用來記住父節點
	Node* x = T->root;   //從根開始查找
	while (x != NULL) {
		y = x;   //記住要掛留的父節點
		if (z->key < x->key) {   //在左子樹中找
			x = x->left;
		}
		else {
			x = x->right;    //在右子樹中找
		}
	}
	z->parent = y;  //掛上去
	if (y == NULL) {   //若是樹是空的
		T->root = z;
	}
	//父親收養它
	else if (z->key < y->key) {
		y->left = z;
	}
	else {
		y->right = z;
	}
}

刪除

刪除是一件比較麻煩的事。咱們分3種大的狀況來討論。

  • 若是z沒有孩子結點,那麼只是簡單的把它刪除掉,而且修改它的父節點指向空。

  • 若是z只有一個孩子,那麼將這個孩子提高到樹中z的位置上,並修改z的父節點的孩子指針。

  • 若是z有兩個孩子,那麼找到z的後繼y(在右子樹中),並讓y佔據z的位置。

    這裏有細分爲兩種狀況:

    • 若是y是z的右孩子,則直接把以y爲根的子樹放到z上,在讓z的左子樹成爲y的左子樹。

    • 若是y不是z的右孩子,則先用y的右孩子來替換y,在用y替換z。

    爲了完成以上工做,額外定義一個函數transplant,它專門用來移植結點。它用一棵以v爲根的子樹來替換一棵以u爲根的子樹,結點u的雙親變爲結點v的雙親,而且最後v成爲u的雙親的相應孩子。

    代碼以下:

    void transplant(Tree* T, Node* u, Node* v) {
    	if (u->parent == NULL) {   //若是被替換的是樹根,則要讓其成爲樹根
    		T->root = v;
    	}
    	else if (u == u->parent->left) {   //若是被替換的那個結點是其父節點的左孩子
    		u->parent->left = v;
    	}
    	else {                         //不然是右孩子
    		u->parent->right = v;
    	}
    	if (v != NULL) {         //指向父節點
    		v->parent = u->parent;
    	}
    }
    
    void tree_delete(Tree* T, Node* z) {
    	//對應第一種和第二種狀況
    	if (z->left == NULL) {          
    		transplant(T, z, z->right);
    	}
    	else if (z->right == NULL) {     
    		transplant(T, z, z->left);
    	}
    	//對應第三種狀況
    	else {
    		Node* y = tree_minimum(z->right);
    		if (y->parent != z) { //若是y不是z的直接右孩子
    			transplant(T, y, y->right);
    			y->right = z->right;
    			y->right->parent = y;
    		}
    		transplant(T, z, y);
    		y->left = z->left;
    		y->left->parent = y;
    	}
    }

參考資料: 《算法導論》Thomas H. Cormen Charles E.Leiserson && Ronald L.Rivest Clifford Stein 著

相關文章
相關標籤/搜索