第一章:内容安排说明
- map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
- 二叉搜索树的特性了解,有助于更好的理解map和set的特性
- 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
- 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。
第二章:二叉搜索树
2.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
2.2 二叉搜索树操作
1. 二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
3. 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
- 情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
- 情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
- 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除
2.3 二叉搜索树的实现
template <class K> //因为键值参与比较,所以改名为K(Key)
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* cur = _root;
// while (cur) {
// if (key > cur->_key) //比当前根节点大,走右边。
// cur = cur->_right;
// else if (key < cur->_key) //比当前根节点小,走左边
// cur = cur->_left;
// else
// return false; //默认二叉搜索树不允许重复值
// }
// cur = new Node(key);
// //这里虽然new了节点,但并未链接。
// //二叉树的链接是要父节点指向子节点。
// return true;
//}
//为了解决上述问题还需要一个父节点指针,
//cur往后遍历之前,父节点指向cur节点
bool Insert(const K& key) {
if (_root == nullptr) {
_root = new Node(key);
return true;
}
//不为空找插入位置
//比当前根节点小,走左边;比当前根节点大,走右边。直到节点为空
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
parent = cur; //cur往后遍历之前,父节点指向cur节点
if (key > cur->_key) //比当前根节点大,走右边。
cur = cur->_right;
else if (key < cur->_key) //比当前根节点小,走左边
cur = cur->_left;
else
return false; //默认二叉搜索树不允许重复值
}
cur = new Node(key);
//这里虽然new了节点,但并未链接。
//二叉树的链接是要父节点指向子节点。
//但链接在左或右不知道,所以需要比较
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
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;
}
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;
}
else { //到这,cur指向被删除的节点
if (cur->_left == nullptr) { //1.被删除节点的左子节点为空,即只有右子节点
if (cur == _root) //还需要考虑被删除节点是根节点的情况
_root = cur->_right;
else {
if (cur == parent->_left) //且被删除节点是其父节点的左子节点
parent->_left = cur->_right;
else //被删除节点是其父节点的右子节点
parent->_right = cur->_right;
}
}
else if (cur->_right == nullptr) { //2.被删除节点的右子节点为空,即只有左子节点
if (cur == _root)
_root = cur->_left;
else {
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
}
else { //3.左右都不为空
//右树的最小节点(最左节点)
//不能用递归或上方Find函数找到要删除的节点,
//因为下方交换了两个节点后,现在已经不是二叉搜索树,
//所以还需要最左节点的父节点。
//如果右树的最左节点就是cur->_right,且parent初始化为空,
//那么循环就不会进,导致parent为空(无法链接),所以要将parent赋值为cur
//parent = nullptr;
parent = cur;
Node* subLeft = cur->_right;//将被删除节点的右子节点作为起点
while (subLeft->_left) { //找到最左下角的节点
parent = subLeft;
subLeft = subLeft->_left;
}
swap(cur->_key, subLeft->_key);//交换最左节点和被删除节点
//虽然上方循环一直在找最左节点,但最左节点并不一定是其父节点的左子节点。
//假设一种情况,(只描述树的一部分),一棵树只有右子树,
//且根节点的右子节点也只有右子树,要删除的是根节点,这时的最左节点是根的右子节点。
//所以需要判断最左节点是其父节点的哪边
if (subLeft == parent->_left)
parent->_left = subLeft->_right;
else
parent->_right = subLeft->_right;
}
return true;
}
}
return false;
}
//这种方式会导致在类外调用时找不到根节点
//bt.InOrder();//调用中序遍历需要根节点,但不知道根节点是谁
//void InOrder(Node* root) {
// if (root == nullptr)
// return;
// InOrder(root->_left);
// cour << root->_key << " ";
// InOrder(root->_right);
//}
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() = default;//强制生成默认
BSTree() {} //有缺省值初始化
~BSTree() { //通过后序递归释放数,当不能递归析构函数
Destroy(_root);
}
//拷贝构造同样是递归实现。以根、左子树、右子树的顺序拷贝。
//对于其左右子树同样可以以相同方法实现。
BSTree(const BSTree<K>& t) {
_root = Copy(t._root);
}
BSTree<K>& operator=(BSTree<K> t) {
swap(_root, t._root);
return *this;
}
private:
Node* _root = nullptr;
//类里面可以获取根节点,所以在套一层,方便外面调用InOrder()
void _InOrder(Node* root) {
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
bool _FindR(Node* root, const K& key) {
if (root == nullptr)
return false;
if (key > root->_key)
_FindR(root->_right, key);
else if (key < root->_key)
_FindR(root->_left, key);
else
return true;
}
//bool _InsertR(Node* root, const K& key) { //这种方式虽然插入了节点,但没有链接
//在递归中,每次递归调用都会创建新的栈帧,
//每个栈帧中的引用参数都是独立的,可以绑定到不同的变量。
bool _InsertR(Node*& root, const K& key) {
if (root == nullptr) {
root = new Node(key);
return true;
}
//当递归进行到上面这个条件时,当前这一层的递归是
//_InsertR(root->_right, key);或_InsertR(root->_left, key);
//也就是说,上一层的root->_left或root->_right引用了这层的root
//在root这里创建节点自然就被链接上了。
if (key > root->_key)
return _InsertR(root->_right, key);
else if (key < root->_key)
return _InsertR(root->_left, key);
else
return false;
}
//这里的引用与插入类似,删除需要被删除节点的父节点,引用恰好能实现
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 {
//这里root指向的是被删除的节点,
//同时,当前这一层的递归是_EraseR(root->_right, key);或_EraseR(root->_left, key);
//也就是说,root上一层的root->_left或root->_right引用了这层的root。
if (root->_left == nullptr) {
Node* del = root;//记录被删除节点的指针
//将root上一层的root->_left或root->_right指向了root下一层的_right
root = root->_right;
delete del;
//如果删除的是根节点也没有问题,(走到这里时说明没有左子树)
//因为root是根节点指针的引用,删除根节点
//也可以理解为将根节点指针移动到根的右子节点
}
else if (root->_right == nullptr) {
Node* del = root;
root = root->_left;
delete del;
}
else {
Node* subLeft = root->_right;
while (subLeft->_left)
subLeft = subLeft->_left;
swap(root->_key, subLeft->_key);
return _EraseR(root->_right, key);
//不能再整棵树中用递归解决,
//因为交换了替换节点和被删除节点就不在满足二叉搜索树的性质了
//但可以在小范围内使用递归(被删除节点右子节点作为根的树),
//被删除节点的右子树都比被删除节点大,替换节点是该右子树的最左节点(最小节点),但依然比删除节点大
//被删除节点与最左节点交换相当于换过来一个更小的值,依然能满足二叉搜索树的性质,即左子节点<根
//当删除的是根节点时,根节点被换到了其右子数的最左节点,再将此时根的右子节点当做树的根来删除,
//此时逻辑就回到上面的if (root->_left == nullptr)或else if (root->_right == nullptr)
//也就是说第一层的root->_right是第二层root的别名,
//第二层的root又指向了第三层的root->_left或root->_right
//即第一层的root->_right指向了第三层的root->_left或root->_right
//既删除了根节点又完成链接
}
return true;
}
}
//void Destroy(Node* root) { //该方式导致root置空无效,形参的改变不影响实参
void Destroy(Node*& root) { //改为引用
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
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;
}
};
2.4 二叉搜索树的应用
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
改造二叉搜索树为KV结构
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 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) {
parent = cur; //cur往后遍历之前,父节点指向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;
}
Node* Find(const K& key) { //通过key找value
Node* cur = _root;
while (cur) {
if (key > cur->_key)
cur = cur->_right;
else if (key < cur->_key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
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;
}
else { //到这,cur指向被删除的节点
if (cur->_left == nullptr) { //1.被删除节点的左子节点为空,即只有右子节点
if (cur == _root) //还需要考虑被删除节点是根节点的情况
_root = cur->_right;
else {
if (cur == parent->_left) //且被删除节点是其父节点的左子节点
parent->_left = cur->_right;
else //被删除节点是其父节点的右子节点
parent->_right = cur->_right;
}
}
else if (cur->_right == nullptr) { //2.被删除节点的右子节点为空,即只有左子节点
if (cur == _root)
_root = cur->_left;
else {
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
}
else { //3.左右都不为空
parent = cur;
Node* subLeft = cur->_right;
while (subLeft->_left) {
parent = subLeft;
subLeft = subLeft->_left;
}
swap(cur->_key, subLeft->_key);
if (subLeft == parent->_left)
parent->_left = subLeft->_right;
else
parent->_right = subLeft->_right;
}
return true;
}
}
return false;
}
void InOrder() {
_InOrder(_root);
cout << endl;
}
private:
Node* _root = nullptr;
//类里面可以获取根节点,所以在套一层,方便外面调用InOrder()
void _InOrder(Node* root) {
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
};
2.5 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(log₂N)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(N)
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。
第三章:二叉树进阶面试题
1. 二叉树创建字符串。606. 根据二叉树创建字符串 - 力扣(LeetCode)
class Solution {
public:
//右为空、左右为空可以省略括号
//只要有任意子树,左子树的括号都要加。右子树只有存在时才加。
string tree2str(TreeNode* root) {
string str;
if (root == nullptr)
return str;
str += to_string(root->val);
//如果左子树或右子树至少有一个存在,就必须处理左子树(即使左子树为空,也要加 ())。
//if ((root->left == nullptr && root->right) || root->left)
if (root->left || root->right) {
str += '(';
str += tree2str(root->left);
str += ')';
}
//只有右子树存在时才处理,否则完全省略。
if (root->right) {
str += '(';
str += tree2str(root->right);
str += ')';
}
return str;
}
};
2. 二叉树的分层遍历1。102. 二叉树的层序遍历 - 力扣(LeetCode)
class Solution {
public:
// 数据结构选择
//队列:用于按层级顺序存储待访问的节点。
//二维数组:存储最终的层序遍历结果,每一层的节点值用一个子数组保存。
vector<vector<int>> levelOrder(TreeNode* root) {
//初始化队列:如果根节点 root 非空,将其加入队列,
//并初始化 levelSize = 1(表示第一层有 1 个节点)。
queue<TreeNode*> q;
int levelSize = 0;
if (root) {
q.push(root);
levelSize = 1;
}
vector <vector<int>> vv;
while (!q.empty()) { //检查队列是否为空,如果不空则继续处理。
vector<int> v;
while (levelSize--) { //处理当前层的所有节点(levelSize 控制循环次数)
//从队列头部取出节点,将其值存入当前层的子数组 v。
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);
}
levelSize = q.size();//更新 levelSize 为当前队列大小(即下一层的节点数)。
vv.push_back(v);
}
return vv;
}
};
3. 二叉树的分层遍历2 107. 二叉树的层序遍历 II - 力扣(LeetCode)
class Solution {
public:
//从vector<vector<int>> vv结果来看,从最后一层进行层序遍历
//其实就是正向层序遍历后再反转
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;
while (levelSize--) {
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);
}
vv.push_back(v);
levelSize = q.size();
}
reverse(vv.begin(), vv.end());
return vv;
}
};
4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。236. 二叉树的最近公共祖先 - 力扣(LeetCode)
class Solution {
public:
//方法一:效率低
//判断节点 x 是否在以 root 为根的子树中。
//为下面 判断p、q是否在当前节点的左右子树 准备
bool isTree(TreeNode* root, TreeNode* x) {
if (root == nullptr) return false;
if (root == x) return true;
return isTree(root->left, x) || isTree(root->right, x);
}
//1.p和q分别在左右,当前节点就是公共祖先
//2.其中一个就是当前树的根
// 1.首先检查当前节点是否是p或q中的一个,如果是,直接返回当前节点
// 2.然后检查p和q分别位于当前节点的左子树还是右子树
// 3.根据p和q的位置关系:
// 如果一个在左,一个在右,当前节点就是LCA
// 如果都在左,递归处理左子树
// 如果都在右,递归处理右子树
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == p || root == q) //当前root是p或q,说明它是自身的祖先
return root;
bool pInLeft = isTree(root->left, p);//p是否在当前root的左子树
bool pInRight = !pInLeft;//p不在左子树则一定在右子树
bool qInLeft = isTree(root->left, q);
bool qInRight = !qInLeft;
//一个在左,一个在右
if ((pInLeft && qInRight) || (pInRight && qInLeft))
return root;
//都在左
if (pInLeft && qInLeft)
return lowestCommonAncestor(root->left, p, q);
//都在右
if (pInRight && qInRight)
return lowestCommonAncestor(root->right, p, q);
return nullptr;//其他情况(理论上不会走到这里,因为题目保证p和q在树中)
}
//方法二
//分别找出p和q的路径,在找出相交节点
//从根节点开始找,每遇到一个节点入栈,期间如果遇到叶子节点还不是,
//那么将该节点出栈(因为这条路径找不到)。直到找到目标节点。
//分别找到从根节点到 p 和 q 的路径(用栈存储)。
bool FindPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path) {
if (root == nullptr) return false;//空树或当前节点为空
path.push(root);//当前节点入栈
if (root == x) return true;//如果当前节点是目标节点返回真
//递归在左、右子树中查找
if (FindPath(root->left, x, path)) return true;
if (FindPath(root->right, x, path)) return true;
path.pop(); //左右子树都没找到,当前节点不在路径中,弹出
return false;
}
//比较两条路径,找到最近公共祖先。
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode*> pPath, qPath;
//找到根到p、q的路径。
//其实这里只需要栈里的路径,所以不需要接收返回值
FindPath(root, p, pPath);
FindPath(root, q, qPath);
//让两条路径长度一致(从底部对齐)
//因为最近公共祖先的深度一定 ≤ min(p深度, q深度),
//且栈顶是最深节点
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();
}
};
5. 二叉树搜索树转换成排序双向链表。二叉搜索树与双向链表_牛客题霸_牛客网
class Solution {
public:
//中序遍历每个节点。创建prev和cur两个指针
//初始时prev指向空,cur指向中序遍历第一个节点
//每次都是prev的右指向cur,cur的左指向prev
//假设中序遍历结果是:4 6 8 10 12 14 16
//如果 prev 不使用引用,而是值传递,那么每次递归调用时,
//prev 的修改只会影响当前函数的副本,不会传递回上一层递归。
//prev的第一次修改是在最深层的递归
//当处理节点4时,prev初始为nullptr。
//处理完4后,prev更新为4(但因为是值传递,外层的prev仍然是nullptr)。
//接下来处理6时,prev仍然是nullptr,导致6的左指针无法正确指向4。
//二叉搜索树的中序遍历结果是一个有序的序列。
//利用中序遍历的顺序,将每个节点的left指向前驱节点,right指向后继节点,
void InorderConvert(TreeNode* cur, TreeNode*& prev) { //prev需要引用才能链接
if (cur == nullptr) return;
InorderConvert(cur->left, prev);
cur->left = prev;
if (prev) prev->right = cur;//cur指向第一个节点时,prev为空,解引用报错,要注意。
prev = cur;
InorderConvert(cur->right, prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
TreeNode* cur = pRootOfTree, *prev = nullptr;
InorderConvert(cur, prev);
//转换后pRootOfTree的最左依然是头
TreeNode* head = pRootOfTree;
while (head && head->left)//找最左
head = head->left;
return head;
}
};
6. 根据一棵树的前序遍历与中序遍历构造二叉树。105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
class Solution {
public:
//前序确定根,中序确定左右子树
TreeNode* _build(vector<int>& preorder, vector<int>& inorder, int& prei,
int inbegin, int inend) {
//中序数组是用来确定左右子树
//当前子树在中序数组中对应的区间为空,因此没有节点属于该子树。
if (inbegin > inend) return nullptr;
//在中序数组中查找当前根节点的位置。
//前序遍历的第一个节点 preorder[prei] 是当前子树的根节点。
//在中序数组内查找该根节点的位置rooti。从而划分其左右子树区间
int rooti = inbegin;
while (rooti <= inend) {
if (inorder[rooti] == preorder[prei])
break;
rooti++;
}
TreeNode* root = new TreeNode(preorder[prei++]);
//[inbegin, rooti - 1] rooti [rooti + 1, inend]
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 i = 0;//prei 前序数组下标
//prei:前序遍历的当前索引(引用传递,确保递归中能正确更新)。
//inbegin:当前子树在中序遍历中的起始位置。
//inend:当前子树在中序遍历中的结束位置。
TreeNode* root = _build(preorder, inorder, i, 0, inorder.size() - 1);
return root;
}
};
7. 根据一棵树的中序遍历与后序遍历构造二叉树 106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
class Solution {
public:
TreeNode* _build(vector<int>& inorder, vector<int>& postorder, int& posi,
int inbegin, int inend) {
if (inbegin > inend) return nullptr;
int rooti = inbegin;
while (rooti <= inend) {
if (postorder[posi] == inorder[rooti])
break;
++rooti;
}
TreeNode* root = new TreeNode(postorder[posi--]);
//后序遍历的顺序是 左子树 → 右子树 → 根,所以在处理后序数组时:
//应该先构建右子树,再构建左子树(因为后序数组的最后一个元素是根,倒数第二个是右子树的根,然后是左子树的根)。
//先递归右子树,再递归左子树(因为后序数组的右子树根在左子树根之前)。
root->right = _build(inorder, postorder, posi, rooti + 1, inend);
root->left = _build(inorder, postorder, posi, inbegin, rooti - 1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int i = postorder.size() - 1;
TreeNode* root = _build(inorder, postorder, i, 0, inorder.size() - 1);
return root;
}
};
8. 二叉树的前序遍历,非递归迭代实现 。144. 二叉树的前序遍历 - 力扣(LeetCode)
class Solution {
public:
//1.先访问包括当前根的左路节点
//2.在访问每个左路节点的右子树
//重复上述两个步骤
vector<int> preorderTraversal(TreeNode* root) {
//用栈存储左路节点
//根据前序遍历的顺序,要先访问最后的左路节点的右子树
stack<TreeNode*> s;
vector<int> v;//存储前序遍历每个节点的值
TreeNode* cur = root;
while (cur || !s.empty()) {
//将cur指向节点当做根,访问该条左路节点
while (cur) {
v.push_back(cur->val);
s.push(cur);
cur = cur->left;
}
//访问左路节点的右子树
TreeNode* top = s.top();
s.pop();
cur = top->right;
}
return v;
}
};
9. 二叉树中序遍历 ,非递归迭代实现。94. 二叉树的中序遍历 - 力扣(LeetCode)
class Solution {
public:
//思路和前序遍历类似,只是最先访问的是左路节点的最后节点
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> v;
TreeNode* cur = root;
while (cur || !s.empty()) {
//一路向左深入,入栈
while (cur) {
s.push(cur);
cur = cur->left;
}
// "访问完最左节点后访问根节点"这一关键步骤在代码中看起来不太直观。
// 中序遍历的顺序:左子树 -> 根节点 -> 右子树
// 沿着左子树一直向下,把经过的每个节点压入栈中,
// 这些被压入的节点实际上都是"未来的根节点"。
// 如果没有左了,那么栈里面的就是根,所以去栈顶访问根
TreeNode* top = s.top();
s.pop();
v.push_back(top->val);
cur = top->right;
}
return v;
}
};
10. 二叉树的后序遍历 ,非递归迭代实现 145. 二叉树的后序遍历 - 力扣(LeetCode)
class Solution {
public:
//思路和前序中序有相似之处
//沿着左子树一直向下,将经过的节点全部压栈,查看栈顶节点,判断是否可访问当前节点
//因为后序遍历是左、右、根,如果它的右为空可以访问,右不为空,继续走右子树。
//此时出现一个问题,不知道该节点应该何时访问,因为在访问它的右子树之前已经访问过它。
//根据后序遍历顺序,当该节点的右子树已访问完时,前一个节点一定是它的右子节点。
//所以需要一个prev指针记录。
vector<int> postorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> s;
TreeNode* cur = root;
TreeNode* prev = nullptr;//记录前一个访问的节点
while (cur || !s.empty()) {
while (cur) {
s.push(cur);
cur = cur->left;
}
TreeNode* top = s.top();//查看栈顶节点
//当前节点没有右子树 或 当前节点的右子树已经访问过
if (top->right == nullptr || top->right == prev) { //判断是否可访问当前节点
s.pop();
v.push_back(top->val);
prev = top;//记录最后一个被弹出栈并访问的节点
}
else
cur = top->right;//处理右子树
}
return v;
}
};
作业
1. 将整数序列(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
答案:A
插入之后的树仍旧是二叉搜索树,因此只要是有序的结果则正确,而有序的结果只有A
因此:选择A
2. 下面关于二叉搜索树正确的说法是( )
A.待删除节点有左子树和右子树时,只能使用左子树的最大值节点替换待删除节点
B.给定一棵二叉搜索树的前序和中序遍率历结果,无法确定这棵二叉搜索树
C.给定一棵二叉搜索树,根据节点值大小排序所需时间复杂度是线性的
D.给定一棵二叉搜索树,可以在线性时间复杂度内转化为平衡二叉搜索树
答案:C
A:错误,当待删除节点的左右子树均存在时,既可以在左子树中找一个最大的节点作为替代节 点,也可以在右子树中找一个最小的节点作为替代节点,左右子树中都可以找替代节点
B:错误,根据前序遍历和中序遍历,是可以确定一棵树的结构,使用两个遍历结果确定树的结构, 其中有一个遍历结果必须要是中序遍历结果。
C:正确,二叉搜索树遍历一遍,就可以得到一个有序序列,因此,时间复杂度为O(N)
D:错误,这里面还需要牵扯到旋转等其他操作,时间复杂度不是线性的
因此:选择C
3. 下面的哪个序列可能是二叉搜索树中序遍历的结果? ( )
A.73 8 2 9 4 11
B. 2 3 4 7 8 9 11
C.11 2 9 3 8 4 7
D.以上均可
答案:B
二叉搜索树的特性:如果对二叉搜索树进行中序遍历,可以得到有序的序列
因此:选择B
4. 关于二叉搜索树特性说法错误的是( )
A.二叉搜索树最左侧的节点一定是最小的
B.二叉搜索树最右侧的节点一定是最大的
C.对二叉搜索树进行中序遍历,一定能够得到一个有序序列
D.二叉搜索树的查找效率为O(log_2N)
答案:D
二叉搜索树的概念:
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3. 它的左右子树也分别为二叉搜索树
从概念中可以得出以下性质:
1. 二叉搜索树中最左侧节点一定是最小的,最右侧节点一定是最大的
2. 对二叉搜索树进行中序遍历,可以得到一个有序的序列
A:正确
B:正确
C:正确
D:错误,二叉搜索树最差情况下会退化为单支树,因此:其查找的效率为O(N)
因此:选择D