二叉搜索樹
- 什么是二叉搜索樹?
- 搜索二叉樹的操作
- 查找
- 插入
- 刪除
- 二叉搜索樹的應用
- 二叉搜索樹的代碼實現
- K模型:
- KV模型
- 二叉搜索樹的性能怎么樣?
什么是二叉搜索樹?
二叉搜索樹又稱二叉排序樹,它或者是一棵空樹,或者是具有以下性質的二叉樹:
- 若它的左子樹不為空,則左子樹上所有節點的值都小于根節點的值
- 若它的右子樹不為空,則右子樹上所有節點的值都大于根節點的值
- 它的左右子樹也分別為二叉搜索樹
由上圖可明顯得知搜索二叉樹的構建方式
搜索二叉樹的操作
這里插入一個搜索二叉樹為樣例:
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
查找
- 從根開始比較,查找,比根大則往右邊走查找,比根小則往左邊走查找。
- 最多查找高度次,走到到空,還沒找到,則這個值不存在
插入
- 當這個樹為空樹時不用多說直接插入成根節點_root
- 當這個樹不為空時,先找到所插入節點的位置,再進行插入
比如要在下面這棵搜索樹中插入9
可以看出,9的位置應該在10的左子樹位置:
在數據結構上這樣就算成功插入
刪除
首先查找元素是否在二叉搜索樹中,如果不存在,則返回false,否則要刪除的結點可能分下面幾種情況:
- 當這個節點是葉子節點或者只有左孩子時,先讓父節點指向該節點的左孩子,再刪除掉該節點–直接刪除
- 當這個節點是葉子節點或者只有右孩子時,先讓父節點指向該節點的右孩子,再刪除掉該節點–直接刪除
- 這個節點既有左子樹又有右子樹,則先將該節點的左子樹鏈接到右子樹的最左節點,再用右子樹來替換該節點–替換法刪除
前兩種情況都好說,例如刪除下列節點中的 7 和 14:
刪7:直接刪除
刪14:刪掉14并將14的左子樹鏈接到10的右子樹
比較復雜的是最后一種情況:當要刪除的節點既有左子樹又有右子樹的時候,則需要進行一些特殊調整,例如,刪除下面這顆樹中的3 和 8 時:
刪除3,使用上述的第三種方法:
先將該節點的左子樹鏈接到右子樹的最左節點,再用右子樹來替換該節點
刪除 8 ,使用上述的第三種方法:
先將該節點的左子樹鏈接到右子樹的最左節點,再用右子樹來替換該節點
以上就是二叉搜索樹的插入和刪除的主要思想,接下來是具體實現
二叉搜索樹的應用
二叉搜索樹分為兩種模型,一種是單鍵值的K模型,另一種是擁有鍵值對的KV模型,而后面需要學習的map,AVL樹等都會涉及到KV模型.
- K模型:K模型即只有key作為關鍵碼,結構中只需要存儲Key即可,關鍵碼即為需要搜索到的值。
比如:給一個單詞word,判斷該單詞是否拼寫正確,具體方式如下:
- 以詞庫中所有單詞集合中的每個單詞作為key,構建一棵二叉搜索樹
- 在二叉搜索樹中檢索該單詞是否存在,存在則拼寫正確,不存在則拼寫錯誤。
- KV模型:每一個關鍵碼key,都有與之對應的值Value,即<Key, Value>的鍵值對。該種方式在現實生活中非常常見:
- 比如英漢詞典就是英文與中文的對應關系,通過英文可以快速找到與其對應的中文,英文單詞與其對應的中文<word, chinese>就構成一種鍵值對;
- 再比如統計單詞次數,統計成功后,給定單詞就可快速找到其出現的次數,單詞與其出現次數就是<word, count>就構成一種鍵值對
二叉搜索樹的代碼實現
K模型:
#pragma once#include<iostream>using namespace std;//單鍵值的搜索二叉樹 K模型
template <class K>
struct BSTNode
{BSTNode<K>* _left;BSTNode<K>* _right;K _key;BSTNode(const K& key):_left(nullptr), _right(nullptr), _key(key){}};template<class K>
class BSTree
{
public:typedef BSTNode<K> Node;bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* cur = _root;Node* parent = cur;while (cur){parent = cur;if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}else{return false;}}cur = new Node(key);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;}void InOrder(){_InOrder(_root);cout << endl;}bool Find(const K& key){if (_root == nullptr)return false;Node* cur = _root;while (cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}else{cout << " 存在" << endl;return true;}}return false;}bool Erase(const K& key){//空樹返回falseif (_root == nullptr)return false;//尋找keyNode* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key > key){//這里注意一下,一定要往下一步走的時候父節點才隨著改變parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{//找到了 分情況//要刪除的節點左孩子或右孩子為空//左孩子為空if (cur->_left == nullptr){//若刪除的是根節點if (cur == _root){_root = cur->_right;}//不是跟節點else{//判斷當前節點是父節點的左孩子還是右孩子if (parent->_left == cur){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}//刪除delete cur;}//右孩子為空else if (cur->_right == nullptr){//若刪除的是根節點if (cur == _root){_root = cur->_left;}//不是跟節點else{//判斷當前節點是父節點的左孩子還是右孩子if (parent->_left == cur){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}//刪除delete cur;}//左右孩子都不為空else{//找當前節點右樹的最左節點來替換Node* parent = cur;Node* subright = cur->_right;while (subright->_left){parent = subright;subright = subright->_left;}swap(cur->_key, subright->_key);//判斷這個節點是parent的左孩子還是右孩子 若刪除的是根節點就是右孩子,因此一定要判斷if (parent->_left == subright){parent->_left = subright->_right;}else{parent->_right = subright->_right;}//刪除delete subright;}//找到返回truereturn true;}}//沒找到return false;}bool FindR(const K& key){return _FindR(_root ,key);}bool InsertR(const K& key){return _InsertR(_root, key);}bool EraseR(const K& key){return _EraseR(_root, key);}//C++11 強制默認構造函數BSTree() = default;~BSTree(){Destroy(_root);}BSTree(const BSTree<K>& t){_root = Copy(t._root);}BSTree<K>& operator=(BSTree<K> t){swap(_root, t->_root);return *this;}private:Node* Copy(Node* root){if (root == nullptr){return nullptr;}Node* NewRoot = new Node(root->_key);NewRoot->_left = Copy(root->_left);NewRoot->_right = Copy(root->_right);return NewRoot;}void Destroy(Node*& root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}bool _EraseR(Node*& root, const K& key){if (root == nullptr){cout << "沒找到" <<key<< endl;return false;}//先找節點if (root->_key > key){return _EraseR(root->_left, key);}else if (root->_key < key){return _EraseR(root->_right, key);}else{//三種情況 左孩子空 右孩子空 左右都不空if (root->_left == nullptr){Node* tmp = root;root = root->_right;delete tmp;return true;}else if (root->_right == nullptr){Node* tmp = root;root = root->_left;delete tmp;return true;}else{//左右孩子都不為空 用左孩子的最右節點 或 右孩子的最左節點Node* subleft = root->_left;while (subleft->_right){subleft = subleft->_right;}swap(root->_key, subleft->_key);return _EraseR(root->_left,key);}}}bool _FindR(Node* root, const K& key){if (root == nullptr)return false;if (root->_key > key){return _FindR(root->_left , key);}else if (root->_key < key){return _FindR(root->_right, key);}else{cout << "找到了" << endl;return true;}}//注意 這里要用 *& 傳值,以保證每次遞歸訪問到每一個節點并能夠對節點進行操作bool _InsertR(Node*& root, const K& key){if (root == nullptr){root = new Node(key);return true;}if (root->_key > key){return _InsertR(root->_left, key);}else if(root->_key < key){return _InsertR(root->_right, key);}else{return false;}}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}Node* _root = nullptr;
};
KV模型
#pragma once#include<iostream>using namespace std;namespace kv
{template <class K, class V>struct BSTreeNode{BSTreeNode<K,V>* _left;BSTreeNode<K,V>* _right;K _key;V _value;BSTreeNode(const K& key , const V& value):_left(nullptr), _right(nullptr), _key(key),_value(value){}};template<class K, class V>class BSTree{typedef BSTreeNode<K, V> Node;public:bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}Node* cur = _root;Node* parent = cur;while (cur){parent = cur;if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){//parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(key, value);if (parent->_key > key){parent->_left = cur;}else {parent->_right = cur;}return true;}Node* Find(const K& key){if (_root == nullptr)return nullptr;Node* cur = _root;while(cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}else{return cur;}}return nullptr;}bool Erase(const K& key){//先找節點if (_root == nullptr)return false;Node* cur = _root;Node* parent = cur;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{//左子樹為空if (cur->_left == nullptr){if (cur == _root){_root = _root->_right;}if (cur == parent->_left){parent->_left = cur->_right;}else if (cur == parent->_right){parent->_right = cur->_right;}delete cur;}//右子樹為空else if (cur->_right == nullptr){if (cur == _root){_root = _root->_left;}if (cur == parent->_left){parent->_left = cur->_left;}else if (cur == parent->_right){parent->_right = cur->_left;}delete cur;}//左右都不為空else{Node* parent = cur;//左子樹的最右節點和cur交換Node* subleft = cur->_left;while (subleft->_right){subleft = subleft->_right;}swap(cur->_key, subleft->_key);delete subleft;}}}//沒找到return false;}void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << root->_value << " ";_InOrder(root->_right);}Node* _root = nullptr;};
}
二叉搜索樹的性能怎么樣?
在二叉搜索樹中,插入和刪除操作都必須先查找,查找效率代表了二叉搜索樹中各個操作的性能.
對有n個結點的二叉搜索樹,若每個元素查找的概率相等,則二叉搜索樹平均查找長度是結點在二叉搜索樹的深度的函數,即結點越深,則比較次數越多。
但對于同一個關鍵碼集合,如果各關鍵碼插入的次序不同,可能得到不同結構的二叉搜索樹:
最優情況下,二叉搜索樹為完全二叉樹(或者接近完全二叉樹),其平均比較次數為: l o g 2 N log_2 N log2?N
最差情況下,二叉搜索樹退化為單支樹(或者類似單支),其平均比較次數為: N 2 \frac{N}{2} 2N?