一、二叉搜索树
1.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
1.2 二叉搜索树操作
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
1. 二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
- 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题–替换法删除
替换法:左子树的最右节点(最大节点)或者右子树的最左节点(最小节点)。
1.3 二叉搜索树的实现
#pragma once
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:
BSTree()
:_root(nullptr)
{}
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) //当cur等于空指针的时候调出循环,在cur的位置插入,用parent是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);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if(cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else//找到了
{
//左为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = _root->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
}
//右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = _root->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
//左右都不为空
else
{
//找替代节点(左边最右节点或者右边最左节点都可以),这里找最左节点
Node* parent = cur;//这里要用cur,因为如果leftMax->_right一上来就是nullptr,下面的循环没有进入,第二个循环使用parent就会报错
Node* leftMax = cur->_left;
while (leftMax->_right)
{
parent = leftMax;
leftMax = leftMax->_right;
}
//找到可以替代的节点,将数值进行替换
swap(cur->_key, leftMax->_key);
//删除要删除的节点
if (parent->_left == leftMax)
{
parent->_left = leftMax->_left;//将最右节点的左边接起来,是空的话也不影响
}
else
{
parent->_right = leftMax->_left;
}
cur = leftMax;
}
delete cur;
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root;
};
void TestBSTree1()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
t.Erase(4);
t.InOrder();
t.Erase(6);
t.InOrder();
t.Erase(7);
t.InOrder();
t.Erase(3);
t.InOrder();
for (auto e : a)
{
t.Erase(e);
}
t.InOrder();
}
运行结果:
1.4 二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 - KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
// 改造二叉搜索树为KV结构
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:
BSTree(): _pRoot(nullptr){}
PNode Find(const K& key);
bool Insert(const K& key, const V& value)
bool Erase(const K& key)
private:
PNode _pRoot;
};
void TestBSTree3()
{
// 输入单词,查找单词对应的中文翻译
BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
// 插入词库中所有单词
string str;
while (cin>>str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << 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();
}
运行结果:
1.5 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
l
o
g
2
N
log_2 N
log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?AVL树和红黑树就可以。
三、 二叉树进阶面试题
- 二叉树创建字符串。OJ链接
class Solution {
public:
string tree2str(TreeNode* root)
{
if(root == nullptr)
return "";
string 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;
}
};
- 二叉树的分层遍历1。OJ链接
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(!q.empty())
{
vector<int>v;//这一句必须放在 while 循环中,因为每一层都需要一个新的v来存储节点值。
//一层一层存入
//每pop一个数,将它的值存入到v中,再将它的左右子孩子放存入到栈中
for(int i = 0;i < levelsize; i++)
{
TreeNode* front = q.front();
v.push_back(front->val);
q.pop();
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
vv.push_back(v);//将每层的数据放入二维数组
levelsize = q.size();//每层存入的个数就是下一次遍历的个数
}
return vv;
}
};
- 二叉树的分层遍历2。OJ链接
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root)
{
vector<vector<int>> vv;
queue<TreeNode*> q;
int levelsize = 0;
if(root)
{
q.push(root);
levelsize = 1;
}
while(!q.empty())
{
vector<int> v;
for(int i = 0;i<levelsize;i++)
{
TreeNode* front = q.front();
q.pop();
v.push_back(front->val);
if(front->left)
q.push(front->left);
if(front->right)
q.push(front->right);
}
levelsize = q.size();
vv.push_back(v);
}
reverse(vv.begin(),vv.end());
return vv;
}
};
- 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接
在这里插入代码片
//1.p和q在当前根节点的俩侧时,当前节点就是公共祖先
//2.p或q其中一个是根节点时,公共节点就是当前根节点
// class Solution {
// public:
// //查找pq位置
// bool Find(TreeNode* tree,TreeNode* x)
// {
// if(tree==nullptr)
// return false;
// return tree == x || Find(tree->left,x) || Find(tree->right,x);
// }
// TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
// {
// if(root==nullptr)
// return nullptr;
// //p或q其中一个与root相等,当前root就是公共祖先
// if(root==p || root==q)
// return root;
// bool pinleft,pinright,qinleft,qinright;
// pinleft = Find(root->left,p);
// pinright = !pinleft;
// qinleft = Find(root->left,q);
// qinright = !qinleft;
// //p和q在同一侧就继续递归查找,当p和q不在同一侧,当前root就是公共祖先
// if(qinleft && pinleft)
// {
// //都在左边,继续找
// return lowestCommonAncestor(root->left,p,q);
// }
// else if(pinright && qinright)
// {
// return lowestCommonAncestor(root->right,p,q);
// }
// else
// {
// //在根节点的左右侧,返回root
// return root;
// }
// }
// };
//方法二
class Solution {
public:
//查找路径函数,通过前序遍历,将路径放进栈中,注意栈中存放的是节点的地址
bool FindPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
{
if(root == nullptr)
return false;
path.push(root);
//root与x相等,找到节点,返回true
if(root == x)
return true;
//在root节点左侧找到,返回ture
if(FindPath(root->left,x,path))
return true;
//在root节点右侧找到,返回ture
if(FindPath(root->right,x,path))
return true;
//走到这里,说明节点的左右都是空指针或者没有目标节点,将该节点从栈中删除
path.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
stack<TreeNode*> Ppath;
stack<TreeNode*> Qpath;
FindPath(root,p,Ppath);
FindPath(root,q,Qpath);
//让俩条路径的size相等
while(Ppath.size() > Qpath.size())
{
Ppath.pop();
}
while(Qpath.size()>Ppath.size())
{
Qpath.pop();
}
//将不相等的节点pop,当出现第一个相等的节点说明该节点就是最近公共节点
while(Ppath.top()!=Qpath.top())
{
Ppath.pop();
Qpath.pop();
}
return Qpath.top();
}
};
5. 二叉树搜索树转换成排序双向链表。OJ链接
class Solution
{
public:
void InOrder(TreeNode* cur,TreeNode*& prev)//prev要加上引用,因为前后的prev有关联
{
if(cur == nullptr)
return;
InOrder(cur->left,prev);
//当前left指向前一个
cur->left=prev;
//前一个right指向当前节点
if (prev) //第一次prev是空指针就不进入
{
prev->right = cur;
}
prev = cur;
InOrder(cur->right,prev);
}
TreeNode* Convert(TreeNode* pRootOfTree)
{
TreeNode* prev = nullptr;
InOrder(pRootOfTree,prev);
//找头节点
TreeNode* head = pRootOfTree;
while (head&&head->left)
{
head=head->left;
}
return head;
}
};
- 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接
class Solution {
public:
TreeNode* _build(vector<int>& preorder, vector<int>& inorder,
int& prei,int inbegin,int inend)
{
if(inbegin > inend)
return nullptr;
TreeNode* root = new TreeNode(preorder[prei]);
int rooti = inbegin;
while(rooti < inend)
{
if(preorder[prei] == inorder[rooti])
break;
++rooti;
}
//[inbegin,rooti-1] rooti [rooti+1,inend]
prei++;
//递归建立左树
root->left = _build(preorder,inorder,prei,inbegin,rooti-1);
//递归建立右树
root->right = _build(preorder,inorder,prei,rooti+1,inend);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)
{
int prei = 0;
return _build(preorder,inorder,prei,0,preorder.size()-1);
}
};
- 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接
class Solution {
public:
TreeNode* _build(vector<int>& inorder, vector<int>& postorder,
int& posti,int inbegin,int inend)
{
if(inbegin > inend)
return nullptr;
TreeNode* root = new TreeNode(postorder[posti]);
int rooti = inbegin;
while(rooti < inend)
{
if(postorder[posti] == inorder[rooti])
break;
++rooti;
}
//[inbegin,rooti-1] rooti [rooti+1,inend]
posti--;
//递归建立右树
root->right = _build(inorder,postorder,posti,rooti+1,inend);
//递归建立左树
root->left = _build(inorder,postorder,posti,inbegin,rooti-1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder)
{
int posti = postorder.size()-1;
return _build(inorder,postorder,posti,0,postorder.size()-1);
}
};
- 二叉树的前序遍历,非递归迭代实现 。OJ链接
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root)
{
//栈用来存放左路节点,利用栈后进先出的特性依次访问右路的顺序
stack<TreeNode*> st;
TreeNode* cur = root;
vector<int> v;
while(cur || !st.empty())
{
//访问一棵树的开始
//访问左路节点,将左路节点入栈
while(cur)
{
v.push_back(cur->val);
st.push(cur);
cur = cur->left;
}
//依次访问右路
TreeNode* top = st.top();
st.pop();
cur = top->right;
}
return v;
}
};
- 二叉树中序遍历 ,非递归迭代实现。OJ链接
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
//栈用来存放左路节点,利用栈后进先出的特性依次访问右路的顺序
stack<TreeNode*> st;
TreeNode* cur = root;
vector<int> v;
while(cur || !st.empty())
{
//访问一棵树的开始
//访问左路节点,将左路节点入栈,后续依次访问左路节点的右子树
while(cur)
{
st.push(cur);
cur = cur->left;
}
//依次访问左路节点和左路节点的右子树
TreeNode* top = st.top();
st.pop();
v.push_back(top->val);
//子问题的方式访问右子树
cur = top->right;
}
return v;
}
};
- 二叉树的后序遍历 ,非递归迭代实现。OJ链接
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
TreeNode* prev = nullptr;
while(cur || !st.empty())
{
//跳出循环说明左子树为空了,如果右子树为空就进行访问,访问完毕说明该节点的父节点的
//左子树访问了,继续判断父节点的右子树是否为空或者上一个访问节点是否为父节点右子树的根
while(cur)
{
st.push(cur);
cur = cur->left;
}
//一个节点的右子树为空或者上一个访问节点是右子树的根
//那么都说明相当于右树访问过了,可以访问当前根的节点
TreeNode* top = st.top();
if(top->right == nullptr || top->right == prev)
{
prev=top;//将访问完的节点存入到prev
v.push_back(top->val);
st.pop();
}
//继续将右子树放入栈中
else
{
cur = top->right;
}
}
return v;
}
};