系列文章目录
文章目录
前言
树结构是一种重要的数据结构。树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
一、二叉搜索树
1.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
二叉搜索树一般不出现重复的树,并且不支持修改操作,否则会破坏树的结构。它的变种支持。
1.2 二叉树的结构
//链表节点
template <class K>
struct BSTNode
{
BSTNode(const K& data)
:_data(data)
,_left(nullptr)
,_right(nullptr)
{}
K _data;
BSTNode<K>* _left;
BSTNode<K>* _right;
};
template <class K>
class BST
{
typedef BSTNode<K> Node;
public:
BST()
:_root(nullptr)
{}
protected:
Node* _root;
};
二、二叉搜索树的操作
2.1 二叉搜索树的查找
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
Node* Find(const T& data)
{
//由于搜索二叉树的特性,通过比较大小,可以快速找到
//比根值小的节点在左,比根值大的节点在右
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
//比根大往右找
cur = cur->_right;
else if (cur->_data > data)
//比根小往左找
cur = cur->_left;
else
return cur;
}
//直到空为止,都没找到
return nullptr;
}
二叉搜索树的时间复杂度是:O(N)。
而不是高度次:O(log2N)
因为二叉树可能会退化成类似链表:
2.2 二叉搜索树的插入
插入和查找类似。
由于二叉搜索树的特性,树中般不出现相同的值。
所以当树中有相同的值时,不进行插入,插入失败,返回false
。插入成功,则返回true
如何将16
和0
插入到下面这个树中?
1.树不为空时:先查找位置,如果比根小往左走,如果比根大往右走,如果和根相同返回false
.
当cur
为空时说明该位置为合适位置,但是如何找到父节点?
所以需要俩个指针,在遍历时,保留cur
的前一个位置(即父节点):
2.树为空时,新建节点,直接插入。
bool insert(const K& data)
{
//1.如果根为空,新建节点
if (_root == nullptr)
{
_root = new Node(data);
return true;
}
//2.如果根不为空,插入节点
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
//比根大往右找
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
//比根小往左找
cur = cur->_left;
}
else
return false;
}
//找到位置,新建节点
cur = new Node(data);
if (parent->_data < data)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
//测试代码:
#include <iostream>
using namespace std;
#include "BST.h"
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BST<int> tree;
for (const auto& e : a)
tree.insert(e);
return 0;
}
可以通过调试窗口进行查看。
由于不能右重复的值,所以当一个数组中有很多重复的值。可以使用二叉搜索树的特性插入。进行去重。
2.3 二叉搜索树的中序
由于二叉搜索树的特点。中序遍历是有序的,所以可以进行排序或去重。
//...
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
//...
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BST<int> tree;
for (const auto& e : a)
{
tree.insert(e);
}
tree.InOrder();
return 0;
}
结果:
中序遍历可以进行排序。同时可以去重。
2.4 二叉搜索树的删除
删除操作一定要保证树的结构特性保持不变。所以分情况讨论,删除的节点有何特性,如何保证结构不变。
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
- 要删除的结点无孩子结点。
- 要删除的结点只有左孩子结点。
- 要删除的结点只有右孩子结点。
- 要删除的结点有左、右孩子结点。
看起来有待删除节点有4中情况,实际情况1可以与情况2或者3合并起来,因此真正的删除过程如下:
- 情况2:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
- 情况3:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
- 情况4:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题–替换法删除
替换删除法不能破坏树的结构。
保证链接上的节点左子树都小,右子树都大。
a、左边最大的节点。由于特性保证了左子树<右子树,所以左子树的最大节点一定合适
b、右边最小的节点。 同样的道理,右子树最小的节点一定合适。
要删除该节点首先要找到该节点,如果找不到就返回false
。
同样找到该节点之后,将该节点删除之后要双亲节点链接上,所以需要parent
指针:
bool Erase(const K& data)
{
//首先要找到删除的节点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
cur = cur->_left;
}
else
{
//找到了要删除的节点
}
}
//没找到
return false;
}
假设要删除下图中的13
、14
、3
删除13
和14
可以统计为一种情况。即将双亲节点链接上我的孩纸。如果我的孩纸都为空那就是空,如果我有一个孩纸不为空,就指向我不为空的孩纸。
//找到了要删除的节点
//然后根据要删除的节点的情况进行删除
//删除度为1,或者叶节点
if (cur->_left == nullptr || cur->_right == nullptr)
{
//只有一个孩纸或没有孩纸的操作实际上可以合并
//判断cur的哪一个为空
//如果是左为空
if (cur->_left == nullptr)
{
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else if (cur->_right == nullptr)
{
//如果是右边为空
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
}
上面代码实际上可以简化,因为大的判断实际上在内部又判断了一次:
//找到了要删除的节点
//然后根据要删除的节点的情况进行删除
//只有一个孩纸或没有孩纸的操作实际上可以合并
//判断cur的哪一个为空
//如果是左为空
if (cur->_left == nullptr)
{
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//如果是右边为空
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
return true;
}
else
{
//删除度为2的节点,使用替代删除
//找左子树最大的节点 或 右子树最小的节点
}
接下来删除度为2的节点(左右都不为空):
else
{
//左右都不为空,进行替代删除
//找到替代节点
Node* Rparent = nullptr;
Node* Replacemin = cur->_left;
while (Replacemin->_left)
{
Rparent = Replacemin;
Replacemin = Replacemin->_left;
}
//进行替换然后删除
cur->_data = Replacemin->_data;
if(Rparent->_left == Replacemin)
Rparent->_left = Replacemin->_right;
else
Rparent->_right = Replacemin->_right;
delete Replacemin;
return true;
}
上面的代码是有问题的,如删除下图的3
时:
此时,Rparent
为空,所以在后面会有空指针访问。但Rparent
不为空,所以我们给初值的时候给cur
即可。
else
{
//左右都不为空,进行替代删除
//找到替代节点
Node* Rparent = cur;
//...
}
给出完整的代码:
bool Erase(const K& data)
{
//首先要找到删除的节点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
cur = cur->_left;
}
else
{
//找到了要删除的节点
//然后根据要删除的节点的情况进行删除
//只有一个孩纸或没有孩纸的操作实际上可以合并
//判断cur的哪一个为空
//如果是左为空
if (cur->_left == nullptr)
{
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//如果是右边为空
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
return true;
}
else
{
//左右都不为空,进行替代删除
//找到替代节点
Node* Rparent = cur;
Node* Replacemin = cur->_right;
while (Replacemin->_left)
{
Rparent = Replacemin;
Replacemin = Replacemin->_left;
}
//进行替换然后删除
cur->_data = Replacemin->_data;
if(Rparent->_left == Replacemin)
Rparent->_left = Replacemin->_right;
else
Rparent->_right = Replacemin->_right;
delete Replacemin;
return true;
}
}
}
//没找到
return false;
}
将整颗树都进行删除测试:
#include <iostream>
using namespace std;
#include "BST.h"
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BST<int> tree;
for (const auto& e : a)
{
tree.insert(e);
}
tree.InOrder();
for (const auto& e : a)
{
tree.Erase(e);
tree.InOrder();
}
return 0;
}
测试发现删除到最后俩个节点时,崩溃了。原因:
当这个情形要删除8
时,由于左子树为空,进入第一个判断,但是parent
为nullptr
所以这里非法。
这样的情形,我们秩序要将根动到下一个位置。
bool Erase(const K& data)
{
//首先要找到删除的节点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
cur = cur->_left;
}
else
{
//找到了要删除的节点
//然后根据要删除的节点的情况进行删除
//只有一个孩纸或没有孩纸的操作实际上可以合并
//判断cur的哪一个为空
//如果是左为空
if (cur->_left == nullptr)
{
if (parent == nullptr)
{
_root = cur->_right;
}
else
{
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
if(parent == nullptr)
{
_root = cur->_left;
}
else
{
//如果是右边为空
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//左右都不为空,进行替代删除
//找到替代节点
Node* Rparent = cur;
Node* Replacemin = cur->_right;
while (Replacemin->_left)
{
Rparent = Replacemin;
Replacemin = Replacemin->_left;
}
//进行替换然后删除
cur->_data = Replacemin->_data;
if(Rparent->_left == Replacemin)
Rparent->_left = Replacemin->_right;
else
Rparent->_right = Replacemin->_right;
delete Replacemin;
return true;
}
}
}
//没找到
return false;
}
2.5二叉树的拷贝
拷贝构造不写,析构时会运行奔溃。
拷贝时用前序的方式拷贝,先拷贝根,然后再左右子树:
Node* _Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newnode = new Node(root->_data, root->_value);
newnode->_left = _Copy(root->_left);
newnode->_right = _Copy(root->_right);
return newnode;
}
BST(const BST<K, V>& bst)
{
_root = _Copy(bst._root);
}
2.6二叉搜索树的销毁
使用递归销毁,由于递归需要参数,而析构函数不用参数。所以使用子函数掉用:
//销毁
void _Destroy(Node* root)
{
if (root == nullptr)
return;
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
root = nullptr;
}
//析构函数
~BST()
{
_Destroy(_root);
}
这样完成了析构。
2.7 所有代码:
#include <list>
//链表节点
template <class K>
struct BSTNode
{
BSTNode(const K& data)
:_data(data)
,_left(nullptr)
,_right(nullptr)
{}
K _data;
BSTNode<K>* _left;
BSTNode<K>* _right;
};
template <class K>
class BST
{
typedef BSTNode<K> Node;
public:
BST()
:_root(nullptr)
{}
Node* _Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newnode = new Node(root->_data, root->_value);
newnode->_left = _Copy(root->_left);
newnode->_right = _Copy(root->_right);
return newnode;
}
BST(const BST<K, V>& bst)
{
_root = _Copy(bst._root);
}
bool insert(const K& data)
{
//1.如果根为空,新建节点
if (_root == nullptr)
{
_root = new Node(data);
return true;
}
//2.如果根不为空,插入节点
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
//比根大往右找
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
//比根小往左找
cur = cur->_left;
}
else
return false;
}
//找到位置,新建节点
cur = new Node(data);
if (parent->_data < data)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
Node* Find(const K& data)
{
//由于搜索二叉树的特性,通过比较大小,可以快速找到
//比根值小的节点在左,比根值大的节点在右
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
//比根大往右找
cur = cur->_right;
else if (cur->_data > data)
//比根小往左找
cur = cur->_left;
else
return cur;
}
//直到空为止,都没找到
return nullptr;
}
bool Erase(const K& data)
{
//首先要找到删除的节点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_data < data)
{
parent = cur; //保留父节点
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur; //保留父节点
cur = cur->_left;
}
else
{
//找到了要删除的节点
//然后根据要删除的节点的情况进行删除
//只有一个孩纸或没有孩纸的操作实际上可以合并
//判断cur的哪一个为空
//如果是左为空
if (cur->_left == nullptr)
{
if (parent == nullptr)
_root = cur->_right;
else
{
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
if(parent == nullptr)
_root = cur->_left;
else
{
//如果是右边为空
//让父亲的哪一边指向我的孩纸
if (parent->_left == cur)
//如果是父亲的左边是我,就让父亲的左边指向我的孩纸
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//左右都不为空,进行替代删除
//找到替代节点
Node* Rparent = cur;
Node* Replacemin = cur->_right;
while (Replacemin->_left)
{
Rparent = Replacemin;
Replacemin = Replacemin->_left;
}
//进行替换然后删除
cur->_data = Replacemin->_data;
if(Rparent->_left == Replacemin)
Rparent->_left = Replacemin->_right;
else
Rparent->_right = Replacemin->_right;
delete Replacemin;
return true;
}
}
}
//没找到
return false;
}
//销毁
void _Destroy(Node* root)
{
if (root == nullptr)
return;
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
root = nullptr;
}
//
~BST()
{
_Destroy(_root);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
Node* _root;
};
三、二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
k模型是寻找 “在不在”,例如门禁系统。查人的时候。
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
k模型就是我们上面所实现的搜索二叉树实现。
而kv模型与k模型不同的是 kv模型的节点多了一个值:
//链表节点//kv模型的节点存了俩个值,一个_key一个_value
template <class K,class V>
struct BSTNode
{
BSTNode(const K& data,const V& value)
:_data(data)
, _value(value)
, _left(nullptr)
, _right(nullptr)
{}
K _data;
V _value;
BSTNode<K,V>* _left;
BSTNode<K,V>* _right;
};
template <class K,class V>
class BST
{
typedef BSTNode<K,V> Node;
public:
bool insert(const K& data,const V& value)
{
//1.如果根为空,新建节点
//代码...
_root = new Node(data,value);
//代码...
//2.如果根不为空,插入节点
//代码...
//找到位置,新建节点
cur = new Node(data,value);
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_data << " " << root->_value << endl;
_InOrder(root->_right);
}
//代码...
那么插入值的时候除了插入key
就还要插入value
,比较的时候是按key
比较.
查找的时候也是根据key
来查找。删除的时候也不需要给value
所以其他的代码基本和上面给出的代码一致。我们将其拷贝一份到命名空间kv
。
测试:
#include <iostream>
using namespace std;
#include "BST.h"
#include <string>
using namespace kv;
int main()
{
BST<string, string> dict;
dict.insert("left","左边");
dict.insert( "right","右边");
dict.insert( "up","上边");
dict.insert("down", "下边");
dict.InOrder();
return 0;
}
结果按key 的字典序来排序:
有了这个功能我们就可以写一个简单的字典:
int main()
{
BST<string, string> dict;
dict.insert("left","左边");
dict.insert( "right","右边");
dict.insert( "up","上边");
dict.insert("down", "下边");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret != nullptr)
cout << "->" << ret->_value << endl;
else
cout << "没有该单词" << endl;
}
return 0;
}
补充一个小知识点:
如何结束死循环? 为什么可以使用while(cin >> str)
?cin >> str
是如何进行 条件判断转化的?
1、结束死循环有俩种:ctrl + c
这样是杀进程的方式,异常退出,不太好。
ctrl + z + 换行
,这样是比较合理的方式。
2、因为 cin >> str
调用的是输入流的重载,输入流重载了bool类型,
本来是重载bool类型转化符:(bool)
,但是()
这个符号被仿函数
重载使用了。所以它重载bool()
相当于一个新的小语法。
所以输入流隐式类型转化为bool调用,bool的类型转换函数。有所了解即可。
kv模型在生活中有非常多的场景,如车库收费系统(车牌进入的时间),统计单词出现的次数。
如果要写一本英文小说,要检查是否有错误单词是用k模型,(检查该单词在不在词典中)。
例如下面的例子:
void TestBSTree4()
{
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
BST<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()
{
TestBSTree4();
return 0;
}
三、二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
l
o
g
2
N
log_2 N
log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?那么后续章节学习的AVL树和红黑树就可以上场了。
四、二叉树的进阶题目
4.1 根据二叉树创建字符串
根据二叉树创建字符串:
通过观察示例得出下面几点:
- 1.当树左边为空,要考虑俩种情况:
-
- a、左边为空,右边为空时,
()
需要省略
- a、左边为空,右边为空时,
-
- b、左边为空,右边不为空时,
()
不需要省略
- b、左边为空,右边不为空时,
- 2、当树右边为空时,
()
都要省略
由此可见不省略()
,只要考虑,右子树为空左子树不为空的情况。其他情况都要省略()
。注意保留的时左子树的()
,所以再左子树的条件判断时进行修改。
首先写出,包含所有()
的情况
class Solution {
public:
string tree2str(TreeNode* root) {
string str;
if(root == nullptr)
return str;
str += to_string(root->val);
//把所有空都考虑进去
//递归左子树
str += '(';
str += tree2str(root->left);
str += ')';
//递归右子树
str += '(';
str += tree2str(root->right);
str += ')';
return str;
}
};
接着写出,什么时候需要保留空的情况:
class Solution {
public:
string tree2str(TreeNode* root) {
string str;
if(root == nullptr)
return str;
str += to_string(root->val);
//左子树不为空时,递归包含左子树
//左子树为空时,考虑右子树是否为空,如果不为空则保留左子树的()
if(root->left || root->right)
{
str += '(';
str += tree2str(root->left);
str += ')';
}
//右子树不为空时,递归包含右子树
if(root->right)
{
str += '(';
str += tree2str(root->right);
str += ')';
}
return str;
}
};
4.2 二叉树的层序遍历
思路:队列+levelsize变量统计这一层的个数。
可以发现,levelsize就是队列的元素个数。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vv;
queue<TreeNode*> q;
int levelsize = 0;
if(root) //先插入根节点
{
q.push(root);
levelsize = 1;
}
while(levelsize>0)
{
vector<int> v; //将每一层的数据保存到数组v中
//一层一层出,levelsize是每一层的个数。
while(levelsize--)
{
TreeNode* front = q.front();
q.pop();
//带入孩子
if(front->left) //左为空就不需要带入了
q.push(front->left);
if(front->right) //右为空就不需要带入了
q.push(front->right);
v.push_back(front->val);
}
//这一层出完之后,更新栈中的levelsize == q.size()
vv.push_back(v);
levelsize = q.size(); //更新层数
}
return vv;
}
};
4.3 二叉树的最近公共祖先
二叉树的最近公共祖先:
通过观察示例,我们可以得出:
最近公共祖先的位置,俩个孩纸一定在这个位置的左右俩侧其他的公共祖先都不满足该规律,所以利用该规律进行寻找。
这种情况属于特殊情况。
即只要是孩纸的祖先就是我们要找的俩个孩纸之间的一个。那么就返回这个根。
使用递归遍历解决该问题。
给定的俩个孩纸节点,首先知道,这俩个孩纸一定存在。
最主要的逻辑就是,查找这俩个孩纸是否在一个公共节点的左右俩侧。我们需要写一个寻找函数 IsInTree(TreeNode* root, TreeNode* obj)
接着利用该函数进行查找,找到了返回true
没找到返回false
.
递归的最小子问题:一个空树(返回nullptr
)
class Solution {
public:
bool IsInTree(TreeNode* root, TreeNode* obj)
{
//递归到空,说明没找到
if (root == nullptr)
return false;
//如果找到了就返回true,没找到接着去左右子树中找。左右子树中找到了就返回true
return root == obj || IsInTree(root->left,obj) || IsInTree(root->right,obj);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
//如果是空树返回空
if (root == nullptr)
return nullptr;
//如果找的节点是根,则返回根
//处理特殊情况
if (root == p || root == q)
return root;
//进行找节点,由于p,q一定在树中,不是在左树,就是在右树。
//因此可以进行 取反操作。
bool pInLeft = IsInTree(root->left,p); //是否在左树
bool pInRight = !pInLeft;
bool qInLeft = IsInTree(root->left,q); //是否在右树
bool qInRight = !qInLeft;
//有了情况之后分情况
//1、一个在左子树,一个在右子树说明找到了最近公共祖先
//2、都在左子树,往左子树递归找
//3、都在右子树,往右子树递归找
if((qInLeft && pInRight) || (qInRight && pInLeft))
return root;
else if(qInLeft && pInLeft)
return lowestCommonAncestor(root->left,p,q);
else if(qInRight && pInRight)
return lowestCommonAncestor(root->right,p,q);
//最终就会找到了最近公共祖先
return nullptr;
}
};
但是这样的时间复杂度就比较高了。到达 O(N2)
思路2:求出俩个节点到根节点的路径,使用链表相交的思路进行解决。
使用栈记录
找的过程里的路径。例如下面找6
:
使用前序遍历就可以获取路径:
此处的时间复杂度O(N).
找到路径后,进行找交点:
俩个链表找交点:
长的先走,等到一样长的时候同时走。相等时即交点。
class Solution {
public:
bool GetPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
{
if(root == nullptr)
return false;
//不为空先入栈
path.push(root);
//找到了返回true
if (root == x)
return true;
//没找到
if(GetPath(root->left,x,path)) //往左子树找
return true;
if (GetPath(root->right,x,path)) //往右子树找
return true;
//如果左右都没找到,说明该节点不时路径
path.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
//定义俩个栈,用于存储路径
stack<TreeNode*> Ppath;
stack<TreeNode*> Qpath;
//获取路径
GetPath(root,p,Ppath);
GetPath(root,q,Qpath);
//求交点
while(Ppath.size() != Qpath.size())
{
//长的先走
if(Ppath.size() > Qpath.size())
Ppath.pop();
else
Qpath.pop();
}
//再同时走
while(Ppath.top() != Qpath.top())
{
Ppath.pop();
Qpath.pop();
}
return Ppath.top();
}
};
4.4 二叉搜索树与双向链表
二叉搜索树与双向链表:
思路1:使用vector
容器,利用中序遍历,将有序节点存储。然后遍历容器进行指向修改:
class Solution {
public:
void InOrderCon(TreeNode* root, vector<TreeNode*>& v)
{
if(root == nullptr)
return;
InOrderCon(root->left,v);
v.push_back(root);
InOrderCon(root->right,v);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
if(pRootOfTree == nullptr)
return nullptr;
vector<TreeNode*> v;
InOrderCon(pRootOfTree,v);
for(int i = 0 ;i < v.size() - 1; i++)
{
v[i+1]->left = v[i];
v[i]->right = v[i+1];
}
return v[0];
}
};
上面的思路,空间复杂度:O(N).所以不太提倡。
思路2:在中序遍历的时候进行指向的修改。
依旧中序遍历搜索二叉树,遍历顺序是有序的,遍历过程中修改左指针为前驱和右指针为后继指针。
记录一个cur
和prev
,cur
为当前中序遍历到的结点,prev
为上一个中序遍历(所以要使用&
)的结点,cur->left
指向prev
如下图,
cur->right
无法指向中序下一个,因为不知道中序下一个是谁,但是prev->right
指向cur
;也就是说每个结点的左是在中遍历到当前结点时修改指向前驱的,但是当前结点的右,是在遍历到下一个结点时,修改指向后继的。
注意:
递归前,prev
为nullptr
,并且cur->left
指向nullptr
。
当cur
递归完之后,prev->right
指向 nullptr
.
class Solution {
public:
void InOrderCon(TreeNode* cur, TreeNode*& prev)
{
if(cur == nullptr)
return;
InOrderCon(cur->left,prev);
//当前 cur 中序位置
//left 指向前驱
cur->left = prev;
//前驱的右指向后继
//但是注意第一次为空
if(prev)
prev->right = cur;
//然后保留cur位置,进行接下来的递归,因此要使用引用
prev = cur;
InOrderCon(cur->right,prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
if(pRootOfTree == nullptr)
return nullptr;
TreeNode* prev = nullptr;
InOrderCon(pRootOfTree,prev);
//找到头
TreeNode* head = pRootOfTree;
while(head && head->left)
{
head = head->left;
}
//如果时循环链表需要进行链接
//head->left = prev;
//prev->right = head;
return head;
}
};
4.5 从前序与中序构建二叉树
思路:
前序确定根。
中序找到根,然后分割左右区间。
如果区间不存在说明,子树为空,返回nullptr
由于考虑到区间问题,所以我们使用子函数进行递归。参数使用区间。
最终同样的道理进行左右子树分别构建。
此题较难理解,可以画递归展开图。
从中序与后序遍历序列构造二叉树
理解之后可以不看题解,做一下这一道。
4.6 二叉树的前序遍历(非递归)
二叉树的前序遍历(非递归)
递归的过程本质是在栈内存中进行的。
通过上图,发现只要左子树存在就得先去到左子树进行递归,
直到左子树为空。意思是得先遍历往左边所有节点。
然后回到上一个节点,递归它的右子树。
对于右子树而言,又分为左子树和右子树。
4 -> 2-> N (回退)-> 2 ->7 -> 回退到4(2以及访问)
这一过程就像栈一样,后进的先出。
前序即先处理好了根,再处理左右子树。所以在入栈的时就得处理。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;//记录
vector<int> ret;
while(cur || !st.empty()) //当cur遍历完,以及栈为空时说明没有它的上面一个了。
{
//遍历左路节点,存放到栈中,以便回退可以找到
//根 左子树 右子树
while(cur)
{
st.push(cur);
//先根,然后再访问左
ret.push_back(cur->val);
cur = cur->left;
}
//左路节点都存到了栈,取栈顶元素(即最近的根)
//左子树访问完了//访问右子树,访问完后要pop掉这个节点。
TreeNode* top = st.top();
cur = top->right;
st.pop();
}
return ret;
}
};
4.7 二叉树的中序遍历(非递归)
中序遍历和前序遍历类似。
中序遍历是访问完左子树,再回到访问根,最后是右子树。
那中序只入栈。等访问完左子树和右子树时再处理。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;//记录
vector<int> ret;
while(cur || !st.empty()) //当cur遍历完,以及栈为空时说明没有它的上面一个了。
{
//遍历左路节点,存放到栈中,以便回退可以找到
while(cur)
{
st.push(cur);
cur = cur->left;
}
//左路节点都存到了栈,取栈顶元素(即最近的根)
//左子树访问完了//访问右子树,访问完后要pop掉这个节点。
TreeNode* top = st.top();
st.pop();
cur = top->right;
//先访问左,再退回到根访问
ret.push_back(top->val);
}
return ret;
}
};
4.8 二叉树的后序遍历(非递归)
后序遍历,先左右子树,最后访问根。
在前序遍历的基础上,如何做到知晓左右子树已经访问过了呢?
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;//记录
vector<int> ret;
while(cur || !st.empty()) //当cur遍历完,以及栈为空时说明没有它的上面一个了。
{
//遍历左路节点,存放到栈中,以便回退可以找到
while(cur)
{
st.push(cur);
cur = cur->left;
}
//访问右子树,访问完后要pop掉这个节点。
TreeNode* top = st.top();
st.pop();
cur = top->right;
}
return ret;
}
};
如何区分左右子树是否访问完了。
stack<TreeNode*> st;
TreeNode* cur = root;//记录
vector<int> ret;
while(cur || !st.empty()) //当cur遍历完,以及栈为空时说明没有它的上面一个了。
{
//遍历左路节点,存放到栈中,以便回退可以找到
while(cur)
{
st.push(cur);
cur = cur->left;
}
//这时已经代表左子树访问完了,取左路节点的右节点进行访问
TreeNode* top = st.top();
if(top->right == nullptr)
{
st.pop();
}
else
{
//循环子问题,访问右树
cur = top->right;
}
}
return ret;
但是这又会有新问题。列如:2访问完之后,又会访问。
那么我们只要知道了上一个访问节点就知道是否访问了右。
如果上一个访问的节点是根说明右边已经访问完了。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;//记录
TreeNode* prev = nullptr;
vector<int> ret;
while(cur || !st.empty()) //当cur遍历完,以及栈为空时说明没有它的上面一个了。
{
//遍历左路节点,存放到栈中,以便回退可以找到
while(cur)
{
st.push(cur);
cur = cur->left;
}
//这时已经代表左子树访问完了,取左路节点的右节点进行访问
//如果右子树为空,或右子树已经访问过了,说明可以访问根了
TreeNode* top = st.top();
if(top->right == nullptr || top->right == prev)
{
ret.push_back(top->val);
st.pop();
prev = top;
}
else
{
//循环子问题,访问右树
cur = top->right;
}
}
return ret;
}
};
如果你有所收获可以留下你的点赞和关注,谢谢你的观看!!!