目录
1. 二叉搜索树(BinarySearchTree)
1.1 二叉搜索树的优势和劣势
二叉搜索树的时间复杂度:O(N)
1.2 二叉搜索树的改良
2. 二叉搜索树的实现
2.1 二叉搜索树的定义
2.2 二叉搜索树的插入
2.3 二叉搜索树的查找
2.4 二叉搜索树的删除
2.5 二叉搜索树的查找(递归)
2.6 二叉搜索树的插入(递归)
2.7 二叉搜索树的删除(递归)
2.8 析构和拷贝构造和赋值
3. 搜索二叉树的应用
3.1 K 模型
3.2 KV 模型
4. 笔试选择题
答案:
5. 完整代码:
本章完。
此篇算是用C++讲高阶数据结构第一篇,在C++完结之前高阶数据结构内容都放在④⑤两个专栏,
等后面C++完结还会学图和算法的内容。
先讲二叉搜索树是因为讲解 map 和 set 的特性需要二叉搜索树做铺垫,理解搜索二叉树有助于更好地理解 map 和 set 的特性。第二个原因是为了后期讲解查找效率极高的平衡搜索二叉树,随后再讲完红黑树,我们就可以模拟实现 map 和 set 了。
1. 二叉搜索树(BinarySearchTree)
概念:搜索二叉树(二叉搜索树)又称为二叉排序树,它或者是一颗空树,
或者是具有以下性质的二叉树:
- 若其左子树不是空,则左子树上所有节点的值都小于根结点的值
- 若其右子树不是空,则右子树上所有结点的值都大于根结点的值
- 其左右子树必须都是二叉搜索树
至于叫它 "搜索二叉树",还是 "二叉搜索树",都是可以的,就是叫搜索二叉树得英文缩写不太好。
结论:任意一个子树都需要满足,左子树的值 < 根 < 右子树的值,才能构成二叉搜索树。
1.1 二叉搜索树的优势和劣势
既然叫搜索二叉树,它肯定是用来搜索的,当满足搜索二叉树时你将可以快速地查找任意的值。
举个例子: 查找7
放到以前我们如果不用二分查找,可能会选择用暴力的方式去从头到尾遍历一遍。
但现在学了搜索二叉树,我们就可以轻松找到这个7了,7 比 8(根节点) 小,
根据搜索二叉树的性质,它必然不会出现在右子树 (右边大) ...
搜索二叉树查找一个值的最坏情况,也只是查找高度次。
二叉搜索树的时间复杂度:O(N)
上面的例子会让人误以为搜索二叉树的增删查改的时间复杂度是O(logN),
但实际上是O(N),因为这棵树是有可能会 蜕化 的,极端情况下会蜕化成一个 "单边树"
比如按有序插入:
最差情况:二叉搜索树退化为单边树(或类似单边),其平均比较次数为:O(N)
最优情况:二叉搜索树为完全二叉树(或接近完全二叉树),其平均比较次数为:O(logN)
对于时间复杂度的分析我们要做一个悲观主义者,根据最差情况去定时间复杂度:O(N)
1.2 二叉搜索树的改良
果搜索二叉树蜕化成了单边树,其性能也就失去了,能否进行改进让它保持性能?
如何做到不论按照上面次序插入关键码,二叉搜索树的性能均能达到最优?
搜索二叉树由于控制不了极端情况,与 O(logN)失之交臂,但平衡二叉搜索树做到了。
严格意义上来说满二叉树才是O(logN)完全二叉树是接近O(logN)。
而平衡搜索二叉树维持左右两边均匀程度,让它接近完全二叉树,从而让效率趋近O(logN)。
后面我们学的各种树(AV树,红黑树)可以说都是二叉搜索树的改良。
2. 二叉搜索树的实现
2.1 二叉搜索树的定义
此时我们的BinarySearchTree就缩写成BSTree了。
这里我们用模板,模板参数我们给了一个K,表示 key 的意思(模板参数并非一定要用 T)。
template<class K>
class BSTreeNode
{
public:
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
下面我们来定义整个树,BSTreeNode<K> 有些长了,我们不如将其 typedef 成 Node 。
这里我们构造函数都没必要写,它自己生成的就够用了:
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
protected:
Node* _root = nullptr;
};
2.2 二叉搜索树的插入
二叉搜索树的插入是会“去重”的。
我们先来实现最简单的插入操作:
- 如果树为空,则直接新增结点,赋值给 root 指针。
- 如果树不为空,按二叉搜索树性质查找插入位置,插入新节点。
Insert 的实现我们可以用递归,也可以用非递归,这一块递归比非递归更难理解。
秉着先难后易的态度,我们先讲比较难理解的非递归版本
Step1:首先检查是否有根结点 _root,如果没有我们就 new 一个结点出来作为根结点。
此时插入成功,返回 true。
Step2:插入就需要找到插入位置,我们定义一个 cur 变量,从根节点开始,
根据搜索二叉树 性质,将 cur 结点的值与插入的值 进行大小比较。
如果插入的值大于当前结点值,则将 cur 结点向右移动 cur=cur->_right ;
如果插入的值小于当前节点值,就将 cur 结点向左移动 cur=cur->_left。
值得注意的是,我们还需要额外记录一下 cur 的父结点,
因为你不知道什么时候会碰到nullptr结束。
并且当我们找到插入位置后,仅仅 new 上一个新结点给 cur 是完成不了插入操作的!
因为直接这么做 cur 也只是一个局部变量而已,你需要 cur 跟上一层(cur 的父亲)相链接才行!
为了能找到上一层,所以我们还需要额外定义一个 prev 变量来记录 cur 的父结点,
在我们更换 cur 结点时记录父结点的位置 prev=cur 即可。
当然了,还有一种插入失败的情况,就是判断大小时出现等于的情况,返回 false 即可。
(重复的值是不允许插入的,默认情况是不允许冗余的!但是也有针对这个的变形,后续再说)
Step3:插入!new 一个新结点给 cur,此时 cur 只是一个局部变量,必须要和父亲链接,
此时应该链接父亲的左边,还是链接父亲的右边?我们不知道,所以我们需要再做一个比较:
如果父节点的值大于插入的值,则将 cur 链接到父亲左边 prev->_left=cur;
反之将 cur 链接到父亲右边 prev->_right=cur。
最后,插入成功返回 true。
insert 代码:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* prev = nullptr;
Node* cur = _root;
while (cur != nullptr) // 找到要插入的位置
{
if (key < cur->_key) // 要插入的值比当前值小
{
prev = cur; // 记录cur,等下cur更新就是cur的父亲
cur = cur->_left; // 到左边插入
}
else if (key > cur->_key)
{
prev = cur;
cur = cur->_right;
}
else
{
return false; // 相等,插入失败
}
}
cur = new Node(key); // 走到这,cur就是要插入的位置
if (key < prev->_key) // 如果key比cur的父亲小
{
prev->_left = cur; // 插入到父亲的左孩子
}
else
{
prev->_right = cur;
}
return true;
}
再写一个中序遍历来测试一下插入的效果:
void InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
InOrder(root->_left);
cout << root->_key << " ";
InOrder(root->_right);
}
模拟出一个测试用例:
void TestBSTree1()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (const auto& e : a)
{
t.Insert(e);
}
t.InOrder(); // 没法传根
}
此时会出现一个问题,因为根是私有的,我们没办法把根传过去。
此时我们可以选择在类内部写一个成员函数 GetRoot 去取根,但是这里我们可以选择这么做:
干脆将刚才我们实现的中序设为 protected 保护,然后再写一个 InOrder 放在公有的区域。
这就是在类内访问 _root 了,没有什么问题。
如此一来我们在类外就可以直接调用 InOrder,并且也不需要传递参数了。
BinarySearchTree.h:
#pragma once
#include <iostream>
using namespace std;
template<class K>
class BSTreeNode
{
public:
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* prev = nullptr;
Node* cur = _root;
while (cur != nullptr) // 找到要插入的位置
{
if (key < cur->_key) // 要插入的值比当前值小
{
prev = cur; // 记录cur,等下cur更新就是cur的父亲
cur = cur->_left; // 到左边插入
}
else if (key > cur->_key)
{
prev = cur;
cur = cur->_right;
}
else
{
return false; // 相等,插入失败
}
}
cur = new Node(key); // 走到这,cur就是要插入的位置
if (key < prev->_key) // 如果key比cur的父亲小
{
prev->_left = cur; // 插入到父亲的左孩子
}
else
{
prev->_right = cur;
}
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
Test.c:
#include "BinarySearchTree.h"
void TestBSTree1()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.Insert(e);
}
t.InOrder();
}
int main()
{
TestBSTree1();
return 0;
}
2.3 二叉搜索树的查找
二叉搜索树的查找 Find 实现很容易,用和刚才一样的思路,从根结点开始查找。
从根开始,如果要查找的值大于 cur 目前的值,则让 cur 往右走,反之往左走。
当查找得值与 cur 的值相等时则说明找到了,返回 true。
当 cur 触及到空(while 循环结束)则说明找不到,返回 false。
代码:
bool Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
cur = cur->_left;
}
else if (key > cur->_key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
2.4 二叉搜索树的删除
搜索二叉树删除的实现是有很有难度的。
没有孩子或者只有一个孩子,可以直接删除,孩子托管给父亲。
两个还是没办法给父亲,父亲养不了这么多孩子,但是可以找个人替代父亲养孩子。
当然,也不能随便找,找的人必须仍然维持搜索二叉树的性质,这是原则。
必须比左边的大,比右边的小。所以在家族中找是最合适的。
找左子树的最大值结点,或者右子树的最小值结点。
首先要查找元素是否在二叉搜索树中,如果不存在,则返回。
如果存在,那么删除的结点可能分为下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左孩子结点也有右孩子结点
看起来有待删除节点有 4 种情况,但实际上 a 和 b,或 a 和 c 可以合并。
因此,真正的删除过程如下:
我们还是定义一个 cur 变量,定义一个prev为空,后面是cur的父亲。
先找到要删除的结点,然后分下面三种情况
① 该结点无左孩子
如果要删除下面这颗二叉树的 10 节点和 4 节点:
当 cur 找到 10 结点后,如果左侧为空情况如下:
若该结点为 _root,直接让 _root 等于它的右孩子结点。
对于删除 10 结点:若 cur是右孩子,则令其父亲的右孩子指向其右孩子 (如图所示)
对于删除 4 结点:若 cur是左孩子,则令其父亲的左孩子指向其右孩子(如图所示)
最后删除 cur 结点
② 该结点无右孩子
如果要删除 14 结点,删除逻辑和删除左孩子是类似的:
③ 该结点有左右两个孩子
如果删除的结点有左右两个孩子,我们就在它的右子树中寻找中序的第一个结点。
即与右子树的最小值进行替换,当然也可以选择左子树的最大值进行替换。
例子:比如下面这颗子树,我们要删除 3 结点:
如果该结点有两个孩子,则采用如下替换法:
该结点和右子树的最小值或左子树的最大值进行值的替换,然后删除替换后的结点。
这里我们采用与右子树的最小值进行替换。
Erase代码:
bool Erase(const K& key)
{
Node* cur = _root;
Node* father = nullptr;
while (cur) // 找到要删除的结点
{
if (key < cur->_key)
{
father = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
father = cur;
cur = cur->_right;
}
else // 找到后开始删除,分三种情况
{
if (cur->_left == nullptr) // ①该结点无左孩子
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == father->_left)
{
father->_left = cur->_right;
}
else //(cur == father->_right)
{
father->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr) // ②该结点无右孩子
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == father->_left)
{
father->_left = cur->_left;
}
else //(cur == father->_left)
{
father->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else // ③有两个结点,替换法删除
{
Node* MinNode = cur->_right;
Node* MinParNode = cur;
while (MinNode->_left) // 找右子树的最小
{
MinParNode = MinNode;
MinNode = MinNode->_left;
}
swap(cur->_key, MinNode->_key); // 找到后交换
if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
{
MinParNode->_right = MinNode->_right;
}
else
{
MinParNode->_left = MinNode->_right;
}
delete MinNode;
MinNode = nullptr;
}
return true;
}
}
return false;
}
Test.c:
#include "BinarySearchTree.h"
void TestBSTree1()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.Insert(e);
}
t.InOrder();
t.Erase(8);
t.Erase(3);
t.InOrder();
for (const auto& e : arr)
{
t.Erase(e);
}
t.InOrder();
}
int main()
{
TestBSTree1();
return 0;
}
2.5 二叉搜索树的查找(递归)
二叉搜索树的递归查找很简单,因为外面不能传根,这里像InOrder一样封装起来
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (key < root->_key)
{
_FindR(root->_left, key);
}
else if (key > root->_key)
{
_FindR(root->_right, key);
}
else
{
return true;
}
}
2.6 二叉搜索树的插入(递归)
二叉搜索树的递归插入如果这样写是插入不了的:
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool _InsertR(Node* root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else
{
return false;
}
}
因为递归到最后一步的时候,root只是一个局部变量,根本插入不了数据。
可以一步一步的把父亲传下来,但是这里有一个神之一手:加引用:
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else
{
return false;
}
}
这里的引用最后一步才起作用,它是空,但它也是上一层传下来的别名。
给给root,就把父亲链接起来了,可以用二级指针,但是用引用很方便。
2.7 二叉搜索树的删除(递归)
这里的递归删除和上面的递归插入一样,也用到了非常巧妙的引用:
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else // 找到要删除的结点,开始删除
{
Node* del = root;
if (root->_left == nullptr) // 这里就体现了引用的神之一手,根本不用判断父亲
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else // 找右树的最左节点替换删除
{
Node* MinNode = root->_right;
while (MinNode->_left)
{
MinNode = MinNode->_left;
}
swap(root->_key, MinNode->_key);
//return EraseR(key); 错的
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
Test.c:
#include "BinarySearchTree.h"
void TestBSTree1()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.Insert(e);
}
t.InOrder();
t.Erase(8);
t.Erase(3);
t.InOrder();
for (const auto& e : arr)
{
t.Erase(e);
}
t.InOrder();
}
void TestBSTree2()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
t.EraseR(8);
t.EraseR(3);
t.EraseR(2);
t.InOrder();
cout << t.Find(10) << endl;
cout << t.Find(100) << endl;
cout << t.FindR(10) << endl;
cout << t.FindR(100) << endl;
for (const auto& e : arr)
{
t.EraseR(e);
}
t.InOrder();
}
int main()
{
TestBSTree2();
return 0;
}
2.8 析构和拷贝构造和赋值
Test.c: 默认生成的:
#include "BinarySearchTree.h"
void TestBSTree3()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
BSTree<int> copy = t;
copy.InOrder();
}
int main()
{
TestBSTree3();
return 0;
}
这里也是浅拷贝的问题,指向的是同一颗树,没崩只是没写析构,写下析构:
~BSTree()
{
_Destory(_root);
}
protected:
void _Destory(Node*& root) // 加引用下面的置空就起作用了
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
再运行刚才测试,程序崩溃:
这时我们就应该自己写拷贝构造了:
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
protected:
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
CopyRoot->_left = _Copy(root->_left);
CopyRoot->_right = _Copy(root->_right);
return CopyRoot;
}
这时运行就会报错:
错误(活动) E0291 类 "BSTree<int>" 不存在默认构造函数
这时写个默认的拷贝构造:
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
//BSTree() // 这样写也行,但是下面是C++11的用法
//{}
BSTree() = default; // C++11的关键字,强制编译器生成默认的构造
protected:
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
CopyRoot->_left = _Copy(root->_left);
CopyRoot->_right = _Copy(root->_right);
return CopyRoot;
}
运行程序:
写了拷贝构造我们就可以直接用现代写法写一个赋值:
BSTree<K> operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
测试:
void TestBSTree3()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
BSTree<int> copy = t;
copy.InOrder();
BSTree<int> t2;
t2.Insert(3);
t2.Insert(5);
t2.Insert(4);
copy = t2;
t2.InOrder();
copy.InOrder();
}
3. 搜索二叉树的应用
3.1 K 模型
K模型,即只有 key 作为关键码,我们上面写的就是K模型,
结构中只需存储 key 即可,关键码就是需要搜索到的值。
举个例子:对于单词 word,我们需要判断该单词是否拼写正确
以单词集合中的每个单词作为 key,构建一个搜索二叉树。
在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
3.2 KV 模型
KV模型,每一个关键码 key,都有与之对应的值 Value,即 <Key, Value> 的键值对。
这就像 Python 中的 dict 字典类型一样,key 和 value 对应。
这在生活中也是非常常见的,比如英汉词典就是英文与中文的对应关系,通过英文可以快读检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:
<string, string> 即 <word, chinese>
再比如统计水果次数,就构建出了一种键值对:
<string, int> 即 <水果, count>
直接放用于测试的代码:
//二叉搜索树的KV结构
namespace KeyValue
{
//定义两个类模板参数K、V
template<class K, class V>
class BSTreeNode
{
public:
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key; //存放了两个类型的数据,相比较与K模型
V _value;
};
//同样的,定义两个类模板参数K、V
//搜索二叉树依旧是按照K的数据进行排序,和V无关
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* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//查找只和数据_key有关,与数据_value无关
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
//删除只和数据_key有关,与数据_value无关
bool Erase(const K& key)
{
//... 和K模型一样
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
Node* _root = nullptr;
};
void TestBSTree1()
{
BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("string", "字符串");
dict.Insert("insert", "插入");
string str;
while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << "对应的中文:" << ret->_value << endl;
}
else
{
cout << "对应的中文->无此单词" << endl;
}
}
}
void TestBSTree2() // 统计水果出现的次数
{
string arr[] = { "香蕉", "苹果", "香蕉", "草莓", "香蕉", "苹果", "苹果", "苹果" };
BSTree<string, int> countTree;
for (auto& str : arr)
{
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret)
{
ret->_value++;
}
else
{
countTree.Insert(str, 1);
}
}
countTree.InOrder();
}
}
4. 笔试选择题
1. 关于二叉搜索树特性说法错误的是( )
A.二叉搜索树最左侧的节点一定是最小的
B.二叉搜索树最右侧的节点一定是最大的
C.对二叉搜索树进行中序遍历,一定能够得到一个有序序列
D.二叉搜索树的查找效率为O(log_2N)
2. 下面的哪个序列可能是二叉搜索树中序遍历的结果? ( )
A.73 8 2 9 4 11
B. 2 3 4 7 8 9 11
C.11 2 9 3 8 4 7
D.以上均可
3. 将整数序列(7-2-4-6-3-1-5)按所示顺序构建一棵二叉排序树a(亦称二叉搜索树),之后将整数8按照二叉排序树规则插入树a中,请问插入之后的树a中序遍历结果是( )
A.1-2-3-4-5-6-7-8
B.7-2-1-4-3-6-5-8
C.1-3-5-2-4-6-7-8
D.1-3-5-6-4-2-8-7
E.7-2-8-1-4-3-6-5
F.5-6-3-4-1-2-7-8
4. 下面关于二叉搜索树正确的说法是( )
A.待删除节点有左子树和右子树时,只能使用左子树的最大值节点替换待删除节点
B.给定一棵二叉搜索树的前序和中序遍率历结果,无法确定这棵二叉搜索树
C.给定一棵二叉搜索树,根据节点值大小排序所需时间复杂度是线性的
D.给定一棵二叉搜索树,可以在线性时间复杂度内转化为平衡二叉搜索树
答案:
1. D
二叉搜索树的概念:
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3. 它的左右子树也分别为二叉搜索树
从概念中可以得出以下性质:
1. 二叉搜索树中最左侧节点一定是最小的,最右侧节点一定是最大的
2. 对二叉搜索树进行中序遍历,可以得到一个有序的序列
A ,B,C:正确
D:错误,二叉搜索树最差情况下会退化为单支树,因此:其查找的效率为O(N)
2. B
二叉搜索树的特性:如果对二叉搜索树进行中序遍历,可以得到有序的序列
3. A
插入之后的树仍旧是二叉搜索树,因此只要是有序的结果则正确,而有序的结果只有A
4. C
A:错误,当待删除节点的左右子树均存在时,既可以在左子树中找一个最大的节点作为替代节 点,也可以在右子树中找一个最小的节点作为替代节点,左右子树中都可以找替代节点
B:错误,根据前序遍历和中序遍历,是可以确定一棵树的结构,使用两个遍历结果确定树的结构, 其中有一个遍历结果必须要是中序遍历结果。
C:正确,二叉搜索树遍历一遍,就可以得到一个有序序列,因此,时间复杂度为O(N)
D:错误,这里面还需要牵扯到旋转等其他操作,时间复杂度不是线性的
5. 完整代码:
#pragma once
#include <iostream>
#include <algorithm>
using namespace std;
template<class K>
class BSTreeNode
{
public:
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* prev = nullptr;
Node* cur = _root;
while (cur != nullptr) // 找到要插入的位置
{
if (key < cur->_key) // 要插入的值比当前值小
{
prev = cur; // 记录cur,等下cur更新就是cur的父亲
cur = cur->_left; // 到左边插入
}
else if (key > cur->_key)
{
prev = cur;
cur = cur->_right;
}
else
{
return false; // 相等,插入失败
}
}
cur = new Node(key); // 走到这,cur就是要插入的位置
if (key < prev->_key) // 如果key比cur的父亲小
{
prev->_left = cur; // 插入到父亲的左孩子
}
else
{
prev->_right = cur;
}
return true;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
cur = cur->_left;
}
else if (key > cur->_key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
bool Erase(const K& key)
{
Node* cur = _root;
Node* father = nullptr;
while (cur) // 找到要删除的结点
{
if (key < cur->_key)
{
father = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
father = cur;
cur = cur->_right;
}
else // 找到后开始删除,分三种情况
{
if (cur->_left == nullptr) // ①该结点无左孩子
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == father->_left)
{
father->_left = cur->_right;
}
else //(cur == father->_right)
{
father->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr) // ②该结点无右孩子
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == father->_left)
{
father->_left = cur->_left;
}
else //(cur == father->_left)
{
father->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else // ③有两个结点,替换法删除
{
Node* MinNode = cur->_right;
Node* MinParNode = cur;
while (MinNode->_left) // 找右子树的最小
{
MinParNode = MinNode;
MinNode = MinNode->_left;
}
swap(cur->_key, MinNode->_key); // 找到后交换
if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
{
MinParNode->_right = MinNode->_right;
}
else
{
MinParNode->_left = MinNode->_right;
}
delete MinNode;
MinNode = nullptr;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
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);
}
~BSTree()
{
_Destory(_root);
}
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
//BSTree() // 这样写也行,但是下面是C++11的用法
//{}
BSTree() = default; // C++11的关键字,强制编译器生成默认的构造
BSTree<K> operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
protected:
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
CopyRoot->_left = _Copy(root->_left);
CopyRoot->_right = _Copy(root->_right);
return CopyRoot;
}
void _Destory(Node*& root) // 加引用下面的置空就起作用了
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (key < root->_key)
{
_FindR(root->_left, key);
}
else if (key > root->_key)
{
_FindR(root->_right, key);
}
else
{
return true;
}
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else
{
return false;
}
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else // 找到要删除的结点,开始删除
{
Node* del = root;
if (root->_left == nullptr) // 这里就体现了引用的神之一手,根本不用判断父亲
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else // 找右树的最左节点替换删除
{
Node* MinNode = root->_right;
while (MinNode->_left)
{
MinNode = MinNode->_left;
}
swap(root->_key, MinNode->_key);
//return EraseR(key); 错的
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
//二叉搜索树的KV结构
namespace KeyValue
{
//定义两个类模板参数K、V
template<class K, class V>
class BSTreeNode
{
public:
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key; //存放了两个类型的数据,相比较与K模型
V _value;
};
//同样的,定义两个类模板参数K、V
//搜索二叉树依旧是按照K的数据进行排序,和V无关
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* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//查找只和数据_key有关,与数据_value无关
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
//删除只和数据_key有关,与数据_value无关
bool Erase(const K& key)
{
//... 和K模型一样
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
Node* _root = nullptr;
};
void TestBSTree1()
{
BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("string", "字符串");
dict.Insert("insert", "插入");
string str;
while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << "对应的中文:" << ret->_value << endl;
}
else
{
cout << "对应的中文->无此单词" << endl;
}
}
}
void TestBSTree2() // 统计水果出现的次数
{
string arr[] = { "香蕉", "苹果", "香蕉", "草莓", "香蕉", "苹果", "苹果", "苹果" };
BSTree<string, int> countTree;
for (auto& str : arr)
{
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret)
{
ret->_value++;
}
else
{
countTree.Insert(str, 1);
}
}
countTree.InOrder();
}
}
#include "BinarySearchTree.h"
void TestBSTree1()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.Insert(e);
}
t.InOrder();
t.Erase(8);
t.Erase(3);
t.InOrder();
for (const auto& e : arr)
{
t.Erase(e);
}
t.InOrder();
}
void TestBSTree2()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
t.EraseR(8);
t.EraseR(3);
t.EraseR(2);
t.InOrder();
cout << t.Find(10) << endl;
cout << t.Find(100) << endl;
cout << t.FindR(10) << endl;
cout << t.FindR(100) << endl;
for (const auto& e : arr)
{
t.EraseR(e);
}
t.InOrder();
}
void TestBSTree3()
{
BSTree<int> t;
int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
BSTree<int> copy = t;
copy.InOrder();
BSTree<int> t2;
t2.Insert(3);
t2.Insert(5);
t2.Insert(4);
copy = t2;
t2.InOrder();
copy.InOrder();
}
int main()
{
//TestBSTree3();
KeyValue::TestBSTree2();
return 0;
}
本章完。
下一部分:树的OJ题,然后是map和set,再然后是AVL树和红黑树。