網站首頁 編程語言 正文
前言
今天我們來學一個新的數據結構:二叉搜索樹。
介紹
二叉搜索樹也稱作二叉排序樹,它具有以下性質:
- 非空左子樹的所有鍵值小于其根節點的鍵值
- 非空右子樹的所有鍵值大于其根節點的鍵值
- 左,右子樹都是二叉搜索樹
那么我先畫一個二叉搜索樹給大家看看,是不是真的滿足上面的性質。
我們就以根節點6為例子來看,我們會發現比6小的都在6的左邊,而比6大的都在6的右邊。對于6的左右子樹來說,所有的節點都遵循這個規則。
同時我們還可以發現如果對搜索二叉樹進行一個中序遍歷,我們得到的序列是個有序序列,這就是為什么二叉搜索樹也可以稱作二叉排序樹。
實現
節點的實現
template <typename K>
struct BTreeNode
{
BTreeNode<K>* _left;
BTreeNode<K>* _right;
K _key;
BTreeNode(const K& key)
:_key(key), _left(nullptr), _right(nullptr)
{}
};
二叉搜索樹的查找
二叉搜索樹的查找思路很簡單:要找的值比當前節點小就去左子樹找,比當前節點大就往右子樹找,找到空節點就說明沒找到返回false即可。
首先我們先看看遞歸的版本。
bool findR(const T &val, Node *root) //T為節點的K, Node為節點
{
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為二叉搜索樹的根節點
while (cur)
{
if (val < cur->_key)
{
cur = cur->_left;
}
else if (val > cur->_key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
二叉搜索樹的插入
二叉搜索樹的插入和查找差別不大,首先尋找插入位置,比當前節點小就往左子樹找,比當前節點大就往右子樹找,直到找到空指針時,就可以進行一個插入。
那么要插入的值和當前節點相同該怎么辦呢?我們此時實現的二叉搜索樹是一個無重復元素的二叉搜索樹,所以對于相同的值我采取的方式是返回一個false,大家如果想實現一個有重復元素的二叉搜索樹,可以選擇將重復的值放在右子樹或左子樹都可。
二叉搜索樹的插入和查找一樣,有遞歸和非遞歸兩個版本,首先我們先來看看非遞歸的版本。
bool insert(const T &val)
{
//空樹直接改變根節點
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;
}
}
//判斷新節點該插入到哪里
cur = new Node(val);
if (val < pre->_key)
{
pre->_left = cur;
}
else
{
pre->_right = cur;
}
return true;
}
接下來用一個動畫來表示一下這個插入過程。
接下來我們來看看遞歸版本是如何實現的
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;
}
}
此時我們可以發現,遞歸版本沒有非遞歸版本中的parent指針了,參數列表中只有一個root指針,這是為什么呢?
我們可以看見我們的root指針不僅是一個指針,同時它還是一個引用。這就意味著我們對root的修改也可以影響上一層傳遞過來的指針的值。所以此時我們不需要parent指針,就可以對上一個節點的指針進行一個修改。
二叉搜索樹的刪除
理論部分:
二叉搜索樹的刪除相比前面兩個要麻煩那么一丟丟,首先當然是找要刪除的節點,找到后通常有以下三種情況:
- 此節點無左右子樹
- 此節點有右子樹或右子樹
- 此節點有左右子樹
我們要做的就是對這三種情況分別進行一個處理。
1.首先是此節點無左右子樹,這種情況我們直接將此節點刪除即可
2.然后是此節點只有一顆子樹,這個也比較簡單,如果此節點是父節點的左節點,那么我們只需要將父節點的左指針改成指向此節點的子樹即可。
3.最后一種就是既有左子樹又有右子樹的情況了,此時為了不破壞結構,我們需要用到替換刪除法。首先我們先找到要刪除的節點,然后找節點的左子樹的最右節點或右子樹的最左節點,將兩個節點進行一下互換,再將原節點刪除。
代碼部分:
首先是非遞歸版本
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 //左右子樹都不為空
{
//找左子樹的最右節點
Node *tmp = cur->_left;
Node *tmp_pre = cur;
while (tmp->_right)
{
tmp_pre = tmp;
tmp = tmp->_right;
}
//節點互換
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;
}
}
總結
大家覺得二叉搜索樹的時間復雜度是多少呢?O(logn)嗎?不,它的時間復雜度還是O(n),當插入數據是有序的,二叉搜索樹會發生退化,變成一個單支樹。比如下面這張圖:
為了解決這個問題,有人對二叉搜索樹進行了一些優化,如:AVL樹和紅黑樹,之后我也會帶著大家來實現一個完整的AVL樹和紅黑樹
原文鏈接:https://blog.csdn.net/m0_60447315/article/details/126464037
相關推薦
- 2023-07-02 Python?中的裝飾器實現函數的緩存(場景分析)_python
- 2022-05-31 詳解Flutter如何繪制曲線,折線圖及波浪動效_Android
- 2023-03-15 Native層消息機制深入探究實例解析_Android
- 2022-08-16 Golang輕量級IoC容器安裝使用示例_Golang
- 2022-06-12 Android開發之保存圖片到相冊的三種方法詳解_Android
- 2022-12-29 一文詳細談談GoLang的panic和error_Golang
- 2022-03-27 centos7安裝mongo數據庫的方法(mongo4.2.8)_MongoDB
- 2022-07-31 ubuntu下常用apt命令介紹_linux shell
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支