C++數(shù)據(jù)結(jié)構(gòu)之二叉搜索樹的實現(xiàn)詳解
前言
今天我們來學一個新的數(shù)據(jù)結(jié)構(gòu):二叉搜索樹。
介紹
二叉搜索樹也稱作二叉排序樹,它具有以下性質(zhì):
- 非空左子樹的所有鍵值小于其根節(jié)點的鍵值
- 非空右子樹的所有鍵值大于其根節(jié)點的鍵值
- 左,右子樹都是二叉搜索樹
那么我先畫一個二叉搜索樹給大家看看,是不是真的滿足上面的性質(zhì)。
我們就以根節(jié)點6為例子來看,我們會發(fā)現(xiàn)比6小的都在6的左邊,而比6大的都在6的右邊。對于6的左右子樹來說,所有的節(jié)點都遵循這個規(guī)則。
同時我們還可以發(fā)現(xiàn)如果對搜索二叉樹進行一個中序遍歷,我們得到的序列是個有序序列,這就是為什么二叉搜索樹也可以稱作二叉排序樹。
實現(xiàn)
節(jié)點的實現(xiàn)
template <typename K> struct BTreeNode { BTreeNode<K>* _left; BTreeNode<K>* _right; K _key; BTreeNode(const K& key) :_key(key), _left(nullptr), _right(nullptr) {} };
二叉搜索樹的查找
二叉搜索樹的查找思路很簡單:要找的值比當前節(jié)點小就去左子樹找,比當前節(jié)點大就往右子樹找,找到空節(jié)點就說明沒找到返回false即可。
首先我們先看看遞歸的版本。
bool findR(const T &val, Node *root) //T為節(jié)點的K, Node為節(jié)點 { if (root == nullptr) { return false; } if (root->_key < val) { return findR(root->_right, val); } else if (root->_key > val) { return findR(root->_left, val); } else { return true; } }
接著看看非遞歸的版本
bool find(const T &val) { Node *cur = _root; //_root為二叉搜索樹的根節(jié)點 while (cur) { if (val < cur->_key) { cur = cur->_left; } else if (val > cur->_key) { cur = cur->_right; } else { return true; } } return false; }
二叉搜索樹的插入
二叉搜索樹的插入和查找差別不大,首先尋找插入位置,比當前節(jié)點小就往左子樹找,比當前節(jié)點大就往右子樹找,直到找到空指針時,就可以進行一個插入。
那么要插入的值和當前節(jié)點相同該怎么辦呢?我們此時實現(xiàn)的二叉搜索樹是一個無重復元素的二叉搜索樹,所以對于相同的值我采取的方式是返回一個false,大家如果想實現(xiàn)一個有重復元素的二叉搜索樹,可以選擇將重復的值放在右子樹或左子樹都可。
二叉搜索樹的插入和查找一樣,有遞歸和非遞歸兩個版本,首先我們先來看看非遞歸的版本。
bool insert(const T &val) { //空樹直接改變根節(jié)點 if (_root == nullptr) { _root = new Node(val); return true; } //非空樹先尋找插入位置 Node *pre = nullptr; Node *cur = _root; while (cur) { if (val > cur->_key) { pre = cur; cur = cur->_right; } else if (val < cur->_key) { pre = cur; cur = cur->_left; } else { return false; } } //判斷新節(jié)點該插入到哪里 cur = new Node(val); if (val < pre->_key) { pre->_left = cur; } else { pre->_right = cur; } return true; }
接下來用一個動畫來表示一下這個插入過程。
接下來我們來看看遞歸版本是如何實現(xiàn)的
bool insertR(const T &val, Node *&root) { if (root == nullptr) { Node *newNode = new Node(val); root = newNode; } if (root->_key < val) { return insertR(val, root->_right); } else if (root->_key > val) { return insertR(val, root->_left); } else { return false; } }
此時我們可以發(fā)現(xiàn),遞歸版本沒有非遞歸版本中的parent指針了,參數(shù)列表中只有一個root指針,這是為什么呢?
我們可以看見我們的root指針不僅是一個指針,同時它還是一個引用。這就意味著我們對root的修改也可以影響上一層傳遞過來的指針的值。所以此時我們不需要parent指針,就可以對上一個節(jié)點的指針進行一個修改。
二叉搜索樹的刪除
理論部分:
二叉搜索樹的刪除相比前面兩個要麻煩那么一丟丟,首先當然是找要刪除的節(jié)點,找到后通常有以下三種情況:
- 此節(jié)點無左右子樹
- 此節(jié)點有右子樹或右子樹
- 此節(jié)點有左右子樹
我們要做的就是對這三種情況分別進行一個處理。
1.首先是此節(jié)點無左右子樹,這種情況我們直接將此節(jié)點刪除即可
2.然后是此節(jié)點只有一顆子樹,這個也比較簡單,如果此節(jié)點是父節(jié)點的左節(jié)點,那么我們只需要將父節(jié)點的左指針改成指向此節(jié)點的子樹即可。
3.最后一種就是既有左子樹又有右子樹的情況了,此時為了不破壞結(jié)構(gòu),我們需要用到替換刪除法。首先我們先找到要刪除的節(jié)點,然后找節(jié)點的左子樹的最右節(jié)點或右子樹的最左節(jié)點,將兩個節(jié)點進行一下互換,再將原節(jié)點刪除。
代碼部分:
首先是非遞歸版本
bool erase(const T &val) { Node *cur = _root; Node *pre = nullptr; //尋找刪除位置 while (cur) { if (cur->_key < val) { pre = cur; cur = cur->_right; } else if (cur->_key > val) { pre = cur; cur = cur->_left; } else //找到了進行刪除 { if (cur->_left == nullptr) //左子樹為空 { if (cur == _root) { _root = cur->_right; } else { if (cur == pre->_left) { pre->_left = cur->_right; } else { pre->_right = cur->_right; } } delete cur; } else if (cur->_right == nullptr) //右子樹為空 { if (cur == _root) { _root = cur->_left; } else { if (cur == pre->_left) { pre->_left = cur->_left; } else { pre->_right = cur->_left; } } delete cur; } else //左右子樹都不為空 { //找左子樹的最右節(jié)點 Node *tmp = cur->_left; Node *tmp_pre = cur; while (tmp->_right) { tmp_pre = tmp; tmp = tmp->_right; } //節(jié)點互換 cur->_key = tmp->_key; if (tmp == tmp_pre->_left) { tmp_pre->_left = tmp->_left; } else { tmp_pre->_right = tmp->_left; } delete tmp; } return true; } } return false; }
接下來是遞歸版本
bool eraseR(const T &val, Node *&root) { //找不到返回false if (root == nullptr) { return false; } //尋找插入位置 if (root->_key < val) { return eraseR(val, root->_right); } else if (root->_key > val) { return eraseR(val, root->_left); } else { if (root->_left == nullptr)//左子樹為空 { root = root->_right; } else if (root->_right == nullptr)//右子樹為空 { root = root->_left; } else //左右子樹都不為空 { Node *cur = root->_left; while (cur->_right) { cur = cur->_right; } swap(cur->_key, root->_key); return eraseR(val, root->_left); } return true; } }
總結(jié)
大家覺得二叉搜索樹的時間復雜度是多少呢?O(logn)嗎?不,它的時間復雜度還是O(n),當插入數(shù)據(jù)是有序的,二叉搜索樹會發(fā)生退化,變成一個單支樹。比如下面這張圖:
為了解決這個問題,有人對二叉搜索樹進行了一些優(yōu)化,如:AVL樹和紅黑樹,之后我也會帶著大家來實現(xiàn)一個完整的AVL樹和紅黑樹
到此這篇關(guān)于C++數(shù)據(jù)結(jié)構(gòu)之二叉搜索樹的實現(xiàn)詳解的文章就介紹到這了,更多相關(guān)C++二叉搜索樹內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C/C++中const關(guān)鍵字的用法及其與宏常量的比較
簡單的說const關(guān)鍵字修飾的變量具有常屬性,也就是說它所修飾的變量不能被修改,下文給大家介紹C/C++中const關(guān)鍵字的用法及其與宏常量的比較,需要的朋友可以參考下2017-07-07