1. 内容安排说明
二叉树在前面
C
数据结构阶段已经讲过,本节取名二叉树进阶是因为:
1.
map
和
set
特性需要
先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
2.
二叉搜索树的特性了解,有助于更好的理解
map
和
set
的特性
3.
二叉树中部分面试题稍微有点难度
,在前面讲解
大家不容易接受
,且时间长容易忘
4.
有些
OJ
题使用
C
语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻
烦。
因此本节借二叉树搜索树,对二叉树部分进行收尾总结。
2. 二叉搜索树
2.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树
,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
-
它的左右子树也分别为二叉搜索树
2.2 二叉搜索树操作
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
1.
二叉搜索树的查找
a
、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b
、最多查找高度次,走到到空,还没找到,这个值不存在。
2.
二叉搜索树的插入
插入的具体过程如下:
a.
树为空,则直接新增节点,赋值给
root
指针
b.
树不空,按二叉搜索树性质查找插入位置,插入新节点
1. **
二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回
,
否则要删除的结点可能分下面四种情
况:
a.
要删除的结点无孩子结点
b.
要删除的结点只有左孩子结点
c.
要删除的结点只有右孩子结点
d.
要删除的结点有左、右孩子结点
看起来有待删除节点有
4
中情况,实际情况
a
可以与情况
b
或者
c
合并起来,因此真正的删除过程
如下:
情况
b
:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
--
直接删除
情况
c
:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
--
直接删除
情况
d
:在它的右子树中寻找中序下的第一个结点
(
关键码最小
)
,用它的值填补到被删除节点
中,再来处理该结点的删除问题
--
替换法删除
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <class T>
struct BSTNode
{
BSTNode(const T &data = T()) : _pLeft(nullptr), _pRight(nullptr), _data(data) {}
~BSTNode() { _data = 0; }
BSTNode<T> *_pLeft;
BSTNode<T> *_pRight;
T _data;
};
template <class T>
class BSTTree
{
typedef BSTNode<T> Node;
typedef Node *PNode;
public:
BSTTree() : _pRoot(nullptr) {}
~BSTTree()
{
if (_pRoot)
{
delete _pRoot;
_pRoot = nullptr;
}
}
//根据二叉树的性质查找,找到值为data的结点在二叉搜索树的位置
PNode Find(const T &data)
{
PNode pCur = _pRoot;
// PNode pParent=nullptr;
while (pCur)
{
if (data == pCur->data)
return pCur;
else if (data < pCur->_data)
pCur = pCur->_pLeft;
else
pCur = pCur->_pRight;
}
if (nullptr == pCur)
return nullptr;
}
bool Insert(const T &data)
{
if (nullptr == _pRoot)
{
_pRoot = new Node(data);
return true;
}
//按照二叉搜索树的性质查找data 树中的位置
PNode pCur = _pRoot;
//记录pCur 的双亲,最终新元素插在PCur双亲左右孩子的位置
PNode pParent = nullptr;
while (pCur)
{
pParent = pCur;
if (data < pCur->_data)
{
pCur = pCur->_pLeft;
}
else if (data > pCur->_data)
pCur = pCur->_pRight;
else
return false;
}
//插入元素
pCur = new Node(data);
if (data < pParent->_data)
pParent->_pLeft = pCur;
else
pParent->_pRight = pCur;
return true;
}
bool Erase(const T &data)
{
//如果树是空,那么就删除失败
if (nullptr == _pRoot)
return false;
//查找data 在树中的位置
Node *pCur = _pRoot; // pNode 即使是Node *
PNode pParent = nullptr;
while (pCur)
{
if (data == pCur->_data)
break;
else if (data < pCur->_data)
{
pParent = pCur;
pCur = pCur->_pLeft;
}
else
{
pParent = pCur;
pCur = pCur->_pRight;
}
}
// data 不再二叉搜索树中,无法删除。
if (nullptr == pCur)
return false;
//删除又分几种情况删除
// 1.左右都为空
if (nullptr == pCur->_pRight && nullptr == pCur->_pLeft)
{
delete pCur;
pCur = nullptr;
}
//只有右节点,用右结点替换要删除的结点
else if (nullptr == pCur->_pLeft)
{
pCur->_data = pCur->_pLeft->_data;
Node *temp = pCur->_pRight;
pCur->_pLeft = temp->_pLeft;
pCur->_pRight = temp->_pRight;
// Node * temp=pCur;
// pCur=pCur->_pRight;
// delete temp;
}
//只有左节点
else if (nullptr == pCur->_pRight)
{
pCur->_data = pCur->_pLeft->_data;
Node *temp = pCur->_pLeft;
pCur->_pLeft = temp->_pLeft;
pCur->_pRight = temp->_pRight;
// pCur=pCur->_pLeft;
// delete temp;
}
else
{
// 如果要删除的结点有两个字结点,找到右子树最小结点,用该结点替换要删除的结点
Node *minNode = findMinNode(pCur->_pRight);
PNode temp = pCur->_pRight;
pCur->_data = minNode->_data;
pCur->_pRight = temp->_pRight;
// Node->right=pCur->_pRight;
delete minNode;
minNode = nullptr;
}
return true;
}
PNode findMinNode(Node *node)
{
while (node->_pLeft)
{
node = node->_pLeft;
}
return node;
}
private:
PNode _pRoot;
};
int main()
{
BSTTree<int> bs;
bs.Insert(8);
bs.Insert(3);
bs.Insert(10);
bs.Insert(1);
bs.Insert(6);
bs.Insert(14);
bs.Insert(4);
bs.Insert(13);
bs.Erase(8);
// bs.Erase(7);
// bs.Erase(14);
// bs.Erase(3);
// bs.Erase(8);
cout << endl;
system("pause");
return 0;
}
2.4 二叉搜索树的应用
1.
K
模型:
K
模型即只有
key
作为关键码,结构中只需要存储
Key
即可,关键码即为需要搜索到
的值
。
比如:
给一个单词
word
,判断该单词是否拼写正确
,具体方式如下:以词库中所有单词集合中的每个单词作为key
,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2.
KV
模型:每一个关键码
key
,都有与之对应的值
Value
,即
<Key, Value>
的键值对
。该种方
式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系
,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>
就构成一种键值对;再比如统计单词次数
,统计成功后,给定单词就可快速找到其出现的次数,
单词与其出
现次数就是
<word, count>
就构成一种键值对。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <class k, class v>
struct BSTNode
{
BSTNode(const K &key = k(), const v &value = v()) : _pLeft(nullptr), _pRight(nullptr), _key(key), _value(value)
{
}
BSTNode<T> *_pLeft;
BSTNode<T> *_pRight;
k _key;
v _value;
};
template <class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
typedef Node *PNode;
public:
BSTNode() : _pRoot(nullptr) {}
PNode Find(const K &key);
bool Insert(const K &key, const V &val);
bool Erase(const K &key);
private:
PNode _pRoot;
};
void TestBSTree3()
{
BSTree<string, string> dict;
dict.Insert("string", "zifuchuan");
dict.Insert("tree", "shu");
dict.Insert("left", "zuobian,shengyu");
dict.Insert("right", "youbian");
dict.Insert("sort", "paixu");
string str;
while (cin >> str)
{
BSTNode<string, string> *ret = dict.Find(str);
if (ret == nullptr)
{
cout << "error" << str << endl;
}
else
{
cout << str << "translate:" << str << endl;
}
}
}
void TestBSTree4()
{
// 统计水果出现的次数
string arr[] = {"苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉"};
BSTree<string, int> countTree;
for (const auto &str : arr)
{
// 先查找水果在不在搜索树中
// 1、不在,说明水果第一次出现,则插入<水果, 1>
// 2、在,则查找到的节点中水果对应的次数++
// BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
int main()
{
system("pause");
return 0;
}
2.5 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n
个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树
(
或者接近完全二叉树
)
,其平均比较次数为:
$log_2 N$
最差情况下,二叉搜索树退化为单支树
(
或者类似单支
)
,其平均比较次数为:
$\frac{N}{2}$
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的
AVL
树和红黑树就可以上
场了。
3.
二叉树进阶面试题
这些题目更适合使用
C++
完成,难度也更大一些
1.
二叉树创建字符串。
力扣
2.
二叉树的分层遍历
1
。
力扣
3.
二叉树的分层遍历
2
。
力扣
4.
给定一个二叉树
,
找到该树中两个指定节点的最近公共祖先 。
力扣
5.
二叉树搜索树转换成排序双向链表。
二叉搜索树与双向链表_牛客题霸_牛客网
6.
根据一棵树的前序遍历与中序遍历构造二叉树。
力扣
7.
根据一棵树的中序遍历与后序遍历构造二叉树。
力扣
8.
二叉树的前序遍历,非递归迭代实现 。
力扣
9.
二叉树中序遍历 ,非递归迭代实现。
力扣
10.
二叉树的后序遍历 ,非递归迭代实现。
力扣