1.二叉搜索树
1.1概念
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值若它的右子树不为空,则右子树上所有节点的值都大于根节点的值它的左右子树也分别为二叉搜索树

1.2操作
1.2.1二叉搜索树的查找

虽然有极端情况的存在,但是大多数情况,搜索树的查找是要优于链表的
1.2.2二叉搜索树的插入
1.2.3二叉搜索树的删除
一、要删除的结点无孩子结点
二、要删除的结点只有左孩子结点
三、要删除的结点只有右孩子结点
四、要删除的结点有左、右孩子结点这种情况是最难处理的,因为它不能直接托孤了,待删除的结点有两个孩子结点此时最好使用 替换法,找一个可以轻松脱身的结点,与待删除结点交换,然后再删除节点![]()
![]()
情况二:直接删除该结点,使被删除节点的 双亲结点指向被删除结点的左孩子结点情况三:直接删除该结点,使被删除节点的 双亲结点指向被删除结点的右孩子结点情况四:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除结点中,再来处理该结点的删除问题,使用替换法删除结点
1.3实现
1.3.1树的结点
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
这里一般不使用T,而使用K,因为这个数据结构的值要参与数据比较,一般把这个值叫做关键字 key
1.3.2插入
//BinarySearchTree.h
#pragma once
#include<iostream>
using namespace std;
//这里不写命名空间是因为,所写的东西与库中的命名不冲突
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(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* parent = nullptr;
Node* cur = _root; //cur是一个局部变量
//cur走到空的位置就可以完成插入了
while (cur)
{
parent = cur;
if (key > cur->_key)//插入的值比cur指向的_key大
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
//默认情况下,搜索二叉数是不允许有重复值的
else
{
return false;
//所以如果插入的值在树中已经存在了
//就return false
}
}
cur = new Node(key);
//判断cur是连接 在parent的左边还是右边
if (key > parent->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
private:
Node* _root = nullptr;
};
1.3.2.1测试
//Test.cpp
#include"BinarySearchTree.h"
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.Insert(e);
}
return 0;
}
1.3.3中序遍历
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
void InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
InOrder(root->_left);
cout << root->_key << " ";
InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
1.3.3.1测试时的问题
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.Insert(e);
}
bt.InOrder();
//调用中序遍历要传递根结点,但这里不容易拿到
//二叉树的递归需要有参数
return 0;
}
1.3.3.2解决方式一
写一个GetNode函数,把_root返回
1.3.3.3解决方式二
改写中序遍历的实现方式
C++中通常不使用方式一
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
//类外面拿不到根结点,但是类里面可以使用
void InOrder()
{
_InOrder(_root);
}
private:
Node* _root = nullptr;
};
//测试成功
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.Insert(e);
}
bt.InOrder();
return 0;
}
C++中的递归通常都是这样去写的
1.3.4查找
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
private:
Node* _root = nullptr;
};
1.3.5删除
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Erase(const K& key)
{
//首先找到待删除的结点,同时要找到它的父亲
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{//查找的过程
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//找到了待删除结点:cur
else
{
//开始删除
//1.情况三:左为空
if (cur->_left == nullptr)
{//先判断待删除节点是不是根节点
if (cur == _root)
{
_root = cur->_right;
}
else
{//判断父亲的哪个指针指向cur
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
}
//2.情况二:右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{//判断父亲的哪个指针指向cur
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
}
//3.情况四:左、右都不为空
else
{//默认找右树的最小节点来替换
//Node* parent = nullptr;
Node* parent = cur;
Node* subLeft = cur->_right;
while (subLeft->_left)
{
parent = subLeft;
subLeft = subLeft->_left;
}
swap(cur->_key, subLeft->_key);
//替换完之后,就要删除节点了
//注意:这里不能去复用函数来删除最左节点
//因为此时它已经不是二叉搜索树了,很可能根本就找不到最左节点
if (parent->_left == subLeft)
{
parent->_left = subLeft->_right;
}
else
{
parent->_right = subLeft->_right;
}
}
return true;
}//找到了待删除结点:cur
}//while结尾
//没有找到
return false;
}
private:
Node* _root = nullptr;
};
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.Insert(e);
}
bt.InOrder();
bt.Erase(14);
bt.InOrder();
bt.Erase(3);
bt.InOrder();
bt.Erase(8);
bt.InOrder();
for (auto e : a)
{
bt.Erase(e);
bt.InOrder();
}
return 0;
}
1.3.5.1注意事项
一、
二、
我们已经实现了二叉搜索树的增、删、查,那么二叉搜索树的修改,也需要实现吗?
二叉搜索树是不能随意修改的,因为修改之后,就很难保证这棵树依旧是二叉搜索树了。
1.4递归实现二叉搜索树
二叉搜索树本身是一个递归结构,但当前的实现,我们使用的是循环。
实际上还可以写一个递归版本的二叉搜索树
1.4.1查找
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool FindR(const K& key)
{
return _FindR(_root,key);
}
private:
//要实现递归,都要在里面多套一层
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
//比当前的值大,就转换成在当前节点的右子树去搜索
if (key > root->_key)
{
return _FindR(root->_right, key);
}
else if (key < root->_key)
{
return _FindR(root->_left, key);
}
else
{
return true;
}
}
private:
Node* _root = nullptr;
};
1.4.2插入
1.4.2.1方式一
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool InsertR(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
return _InsertR(_root, key,nullptr);
}
private:
bool _InsertR(Node* root,const K& key,Node* parent)
{
if (root == nullptr)
{
root = new Node(key);
if (key > parent->_key)
{
parent->_right = root;
}
else
{
parent->_left = root;
}
return true;
}
if (key > root->_key)
{
return _InsertR(root->_right, key,root);
}
else if (key < root->_key)
{
return _InsertR(root->_left, key,root);
}
else
{
return false;
}
}
private:
Node* _root = nullptr;
};
1.4.2.1方式二:巧妙地使用引用
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
private:
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else
{
return false;
}
}
private:
Node* _root = nullptr;
};
既然递归实现时,可以巧妙地使用引用来达成目的。
那么之前的循环实现的插入函数是不是也可以使用引用呢?
答案是不可以的。C++的引用不能改变指向,
比如起初是A的引用,那么之后就不能再变成B的引用、C的引用了,
int a = 0;
int& b = a;//给a取别名为b
int& c = a;
int& d = b;//给别名取别名
int x = 1;
b = x;//这里是赋值
//b已经是a的引用了,就不能再更改了
递归时可以更改是因为,虽然都叫做root,但是每个栈帧中都是一个新的引用,不同作用域可以定义同名变量,每次都定义了一个新的引用,只是说前几次的引用没有起到作用,最后一次的引用才起到了作用。
1.4.3删除
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
private:
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
//比它大,转换成在右树删除
if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else
{//找到了节点,开始删除
if (root->_left == nullptr)
{
//使用引用传递参数,写起来就会很舒服
//不必再去判断是父亲的左还是右
Node* del = root;
root = root->_right;
delete del;
return true;
}
else if (root->_right == nullptr)
{
Node* del = root;
root = root->_left;
delete del;
return true;
}
else//左右都不为空
{
//这里还要去找替代节点
Node* subLeft = root->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
swap(root->_key, subLeft->_key);
//转换成在子树中,去递归删除
//在子树中,删除的节点一定 不是 左右都不为空的节点
return _EraseR(root->_right,key);
}
}
}
private:
Node* _root = nullptr;
};
画出递归展开图,有助于理解
1.5完善搜索树的实现
1.5.1析构函数
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//不可能说对析构函数递归
//因为递归一定有一个特点:递归一定要控制参数
//由参数的变化来控制这棵树
~BSTree()
{
Destroy(_root);
}
//不写析构释放,是会产生内存泄露的,但是不会报错
private:
void Destroy(Node*& root)//注意auto是不能作为参数的
{
if (root == nullptr)
{
return;
}
//后序遍历
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
//直接置空是无用的,这里不影响实参
//之前的实现,通常会使用二级指针
//学习C++后,更好的办法是使用引用传参
}
private:
Node* _root = nullptr;
};
Destroy函数的参数换成Node& root可以吗?
~BSTree()
{
Destroy(*_root);
}
//如果_root是空呢?所以这样实现纯纯是自找麻烦
void Destroy(Node& root)
{
}
1.5.2拷贝构造
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.InsertR(e);
}
bt.InOrder();
BSTree<int> copy(bt);
//没有写拷贝构造,这里就是浅拷贝,会崩溃
copy.InOrder();
return 0;
}
如何书写拷贝构造呢?
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//同样的,使用递归去拷贝是最好的
BSTree(const BSTree<K>& t)
{
//t._root = Copy(_root); 错误写法
_root = Copy(t._root);
}
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;
}
private:
Node* _root = nullptr;
};
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> bt;
for (auto e : a)
{
bt.InsertR(e);
}
bt.InOrder();
BSTree<int> copy(bt);
copy.InOrder();
return 0;
}
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//此时注意要显式书写构造函数!
BSTree()
{}
//可以这样写,因为有缺省值,哪怕不写初始化,此时内置类型也会使用缺省值去初始化
//第二种写法:使用default
BSTree() = default;
//在计算机术语中,"default" 表示“默认”
//此时会强制去生成默认的构造函数
//C++11增加的关键字default的新用法
BSTree(const BSTree<K>& t)
{
//t._root = Copy(_root);
_root = Copy(t._root);
}
private:
Node* _root = nullptr;
};
1.5.3赋值
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//只要写了拷贝构造,需要使用深拷贝的
//赋值都可以使用现代写法
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
private:
Node* _root = nullptr;
};
1.6应用
搜索树的模型分为两个:Key模型和Key-Value模型
1.6.1Key的搜索模型
1.6.1.1典型场景

二、查找一部英文小说中的错别字
把词库读到一棵搜索树中,然后遍历整部小说,查看每个单词是否在词库中,达成纠错的目的,玩法很暴力
1.6.2Key-Value的搜索模型
1.判断Key在不在树中2.通过Key查找对应的Value
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)
{}
};
1.6.2.1代码实现
namespace Key_Value
{
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 BSTree
{
typedef BSTreeNode<K,V> Node;
public:
//_value只是单纯地去存储一个值
//所以KV的实现与之前的K模型没有太大的区别
//比较时,还是要比较key值
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)
{
parent = cur;
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key,value);
if (key > parent->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//Find的参数就不必给value了
//因为KV模型主要是要通过K找V
//如果K和V都有了,也就没有查找的必要了
//bool Find(const K& key, const V& value)
Node* Find(const K& key)
//所以Find的返回值类型也要改变一下,不再返回true或false,
//否则这样只能单纯地去判断Key在不在
{
Node* cur = _root;
while (cur)
{
if (key > _root->_key)
{
cur = cur->_right;
}
else if (key < _root->_key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;//没有找到就返回空指针
}
//Erase同理,参数部分只需要Key就可以
bool Erase(const K& key)
{
}
private:
Node* _root = nullptr;
};
}
1.6.2.2典型场景
int main()
{
//判断字典
Key_Value::BSTree<string,string> dict;
//需要先录入信息
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("insert", "插入");
dict.Insert("key", "关键字");
//最好是把所有的字典库拿出来
//完成一个查找:
string str;
while (cin >> str)
{
Key_Value::BSTreeNode<string, string>* ret=dict.Find(str);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "无此单词" << endl;
}
}
return 0;
}
二、统计单词出现的次数
int main()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
Key_Value::BSTree<string, int> countTree;
for (auto& e : arr)
{
//先去查找
Key_Value::BSTreeNode<string, int>* ret=countTree.Find(e);
if (ret==nullptr)
{
countTree.Insert(e, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
//打印出来是按照key,也就是string所对应的编码大小进行排序的
return 0;
}
三、智能的调度电梯的系统
住户进入小区,在门禁处录入信息,系统根据住户的身份信息,找到住户的楼层信息,然后计算住户到达所住楼层的时间,来调动该楼层的电梯,减少住户等电梯的时间。
四、无纸质票坐车
根据刷取的身份证信息,查找所买的票,达到无纸质票坐车的目的,提高坐车效率
2.二叉树进阶题
2.1
606. 根据二叉树创建字符串 - 力扣(LeetCode)
2.2
利用queue
2.3
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
2.3.1解法一
可以认为是链表相交问题的变形
解题关键在于,对于当前节点,要判断p,q在左树还是右树
2.3.2解法一的时间复杂度
所以这里的效率过低,我们应该换一个解法,转换为找公共祖先的思路
2.3.3解法二
此时,时间复杂度就优化为了O(N)