通用算法 - [樹結構] - 二叉搜索樹

1. 二叉搜索樹的概念

二叉搜索樹,又稱二叉排序數或二叉查找樹。
它要麼一棵空樹,要麼具有如下性質:
(1)若它的左子樹不爲空,則左子樹上所有節點的值都小於根節點的值;
(2)若它的右子樹不爲空,則右子樹上所有節點的值都大於根節點的值;
(3)它的左右子樹也分別爲二叉排序樹;
下圖就是一棵二叉搜索樹
在這裏插入圖片描述
二叉排序樹具有如下優點:

  • 排序、查找、插入和刪除方便;
  • 二叉排序樹的中序遍歷序列爲所有節點的排序結果。
  • 查找的時間複雜度爲O(logn)O(logn),僅次於通過哈希表建立數據索引來查找的方式(查找時間複雜度爲O(1)O(1));

2. 二叉搜索樹的存儲形式

二叉搜索樹存儲方式和普通二叉樹一樣,有兩種存儲方式,一種是順序存儲,一種是鏈式存儲。
(1)順序存儲
二叉樹的順序存儲,就是用一組連續的存儲單元存放二叉樹中的結點。因此,必須把二叉樹的所有結點安排成爲一個恰當的序列,結點在這個序列中的相互位置能反映出結點之間的邏輯關係,用編號的方法從樹根起,自上層至下層,每層自左至右地給所有結點編號。
缺點是有可能對存儲空間造成極大的浪費,在最壞的情況下,一個深度爲kk且只有kk個結點的右單支樹需要2k12^{k}-1個結點存儲空間。
依據二叉樹的性質,完全二叉樹和滿二叉樹採用順序存儲比較合適,樹中結點的序號可以唯一地反映出結點之間的邏輯關係,這樣既能夠最大可能地節省存儲空間,又可以利用數組元素的下標值確定結點在二叉樹中的位置,以及結點之間的關係。
如圖所示:

圖1 完全二叉樹使用順序存儲結構存儲。
在這裏插入圖片描述
在這裏插入圖片描述
圖2 非完全二叉樹改造成完全二叉樹後的存儲示意圖。
(2)鏈式存儲結構
二叉樹的鏈式存儲結構是指,用鏈表來表示一棵二叉樹,即用鏈來指示元素的邏輯關係。
通常的方法是鏈表中每個結點由三個域組成,數據域和左右指針域,左右指針分別用來給出該結點左孩子和右孩子所在的鏈結點的存儲地址。其結點結構爲:
在這裏插入圖片描述
其中,data域存放某結點的數據信息;lchild與rchild分別存放指向左孩子和右孩子的指針,當左孩子或右孩子不存在時,相應指針域值爲空(用符號∧或NULL表示)。利用這樣的結點結構表示的二叉樹的鏈式存儲結構被稱爲二叉鏈表,如圖3所示。
在這裏插入圖片描述
圖3 二叉樹的二叉鏈表表示示意圖

3. 二叉搜索樹中的常用操作

  • 查找
    查找操作的思路:
    (1)從根節點開始查找,如果根節點爲空,則返回NULL;
    (2)如果根節點非空,則將查找數據和根節點比較,
    (3)如果查找數據大於根節點,則在右子樹繼續查找;
    (4)如果查找數據小於根節點,則在左子樹繼續查找;
    (5)如果查找數據等於根節點的值,則表示查找完成,返回該節點;

實現代碼:

struct BinaryTreeNode{
	int value;
	BinaryTreeNode* left;
	BinaryTreeNode* right;
	BinaryTreeNode(int val) :value(val), left(nullptr), right(nullptr){}
};
//二叉搜索樹中的遞歸查找
bool search_recursion(BinaryTreeNode* root, int val){
	if (root == nullptr){
		return false;
	}

	if (root->value == val){
		return true;
	}
	else if (root->value > val){
		return search_recursion(root->left, val);
	}
	else{
		return search_recursion(root->right, val);
	}

}

//二叉搜索樹中的非遞歸查找
bool search(BinaryTreeNode* root, int val){
	BinaryTreeNode* p = root;

	while (p!=nullptr){
		if (p->value == val){
			return true;
		}
		else if (p->value > val){
			p = p->left;
		}
		else{
			p = p->right;
		}

	}
	return false;

}

  • 插入
    插入操作基本思路是:
    (1)如果指針指向的當前節點爲空,則找到待插入節點的位置,爲待插入節點分配空間,並令指針指向待插入節點;
    (2)如果指向指向的當前節點不爲空,則將當前節點的值與待插入節點比較;
    (3)如果當前節點的值小於待插入節點,則在右子樹中插入節點;
    (4)如果當前節點大於待插入節點,則在左子樹插入節點;

實現代碼:


//二叉搜索樹的遞歸插入操作
 BinaryTreeNode* insert_recursion(BinaryTreeNode* root, int node_value){
	//當走到空指針,就找到了node_value的插入位置。
	if (root == nullptr){
		root = new BinaryTreeNode(node_value);
	}
	/*if (root->value == node_value){
		return;
	}*/
    if (root->value > node_value){
		root->left = insert_recursion(root->left, node_value);

	}
	else if(root->value < node_value){
		root->right = insert_recursion(root->right, node_value);
	}
	return root;

}

BinaryTreeNode* insert(BinaryTreeNode* root, int node_value){
	BinaryTreeNode* iter = root;

	//記錄待插入節點的父節點;
	BinaryTreeNode* iter_parent = nullptr;

	//記錄待插入節點是左孩子還是右孩子。
	int enter = -1;
	while (iter != nullptr){

		iter_parent = iter;
		/*if (iter->value == node_value){
			return;
		}*/
		if (iter->value > node_value){
			iter = iter->left;
			enter = 0;
			
		}
		else if(iter->value < node_value){
			iter = iter->right;
			enter = 1;
		}	
	}
	BinaryTreeNode* node = new BinaryTreeNode(node_value);
	if (enter == -1){
		root = node;
	}
	else if (enter == 0){
		iter_parent->left = node;
	}
	else{
		iter_parent->right = node;
	}
	return root;
}

  • 刪除
    刪除操作根據待刪除的節點分爲三種情況:
    第一種情況,如果待刪除的節點爲葉子節點,則直令刪除該節點即可;
    第二種情況,如果待刪除節點只有一個子樹(左子樹或右子樹),則直接用子樹覆蓋該節點;
    第三種情況,如果待刪除節點既有左子樹,又有右子樹,則用左子樹的最大節點或右子樹的最小節點覆蓋待刪除節點,並刪除左子樹中的最大節點或刪除右子樹中的最小節點。
    以下面這個例子來說明:
    在這裏插入圖片描述
    假如我們要刪除35這個結點,它有左右兩個子樹,這時我們可以選擇第一,從右子樹中找最小值,35的右子樹只有41,可以把41的值拷貝去替換掉35,然後刪除41這個節點。,這棵樹仍是二叉搜索樹,滿足相應的關係。第二,從左子樹中找最大值,35的左子樹中最大值是28,就把28拷貝替換掉35的位置,然後刪除28這個結點,此時25順應就接到了22的右子樹的位置上,可以發現替換後樹仍然是一棵二叉搜索樹。這樣做的好處是,把第三種情況要刪除的結點有兩個子樹變成要刪除的結點只有一個子樹。爲什麼是這樣?因爲我們可以想一下,左子樹中的最大值,肯定是在左子樹的最右邊,而這個結點肯定不會有兩個結點,要麼沒有要麼就只有一個結點。右子樹中的最小值同樣是這樣。

實現代碼:

//在二叉搜索樹中找到最大節點
BinaryTreeNode* findMax(BinaryTreeNode* root){
	BinaryTreeNode* iter = root;
	while (iter->right != nullptr){
		iter = iter->right;
	}
	return iter;
}

//在二叉搜索樹中找到最小節點
BinaryTreeNode* findMin(BinaryTreeNode* root){
	BinaryTreeNode* iter = root;
	while (iter->left != nullptr){
		iter = iter->left;
	}
	return iter;

}


BinaryTreeNode*  remove(BinaryTreeNode* root,int remove_value){
	
	if (root == nullptr){
		cout << "找不到要刪除的節點" << endl;
		return nullptr;
	}
	//找到要刪除的節點
	if (root->value == remove_value){
		//若待刪除節點的左右子樹都不爲空
		if (root->left != nullptr && root->right != nullptr){
			//在待刪除節點的左子樹中找到最大節點或在右子樹中找到最小節點
			BinaryTreeNode* leftmaxnode = findMax(root->left);
			//用左子樹最大節點的值覆蓋待刪除節點的值
			root->value = leftmaxnode->value;
			//刪除待左子樹中的最大節點
			root->left = remove(root->left, leftmaxnode->value);

		}
		else{
			BinaryTreeNode* temp = root;
			//如果待刪除節點的左孩子爲空,則用右子樹覆蓋待刪除節點,否則用左子樹覆蓋待刪除節點
			if (root->left == nullptr){
				root = root->right;
			}
			else if (root->right == nullptr){
				root = root->left;
			}
			//釋放待刪除節點所佔的空間
			delete temp;
		}
		
	}
	else if (root->value < remove_value){
		root->right = remove(root->right, remove_value);
	}
	else{
		root->left = remove(root->left, remove_value);
	}
	return root;
}

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