写在前面
树的考察点归根结底是对树的前序、中序、后序和层序的考察,树的前、中、后序遍历的递归写法是图的深度优先搜索的子集,因为带有递归特点,所以往往可以使用栈将其改写成非递归写法,树的层序遍历是图的广度优先搜索的子集。
树的遍历
树的递归遍历难度非常小,一般不会直接考察,而选择考察它的非递归写法,之前专门为二叉树和N叉树的遍历写过总结博客。由于此知识点过于重要,而且在面试手撕代码中是高频代码,因此重新再整理一次。
LeetCode中相关的题如下:
前序遍历
解题思路: 对着题解代码我们很好理解前序遍历的非递归解法,但是为什么这么写?我们有没有想过,虽然网上有很多非递归写法的模板,但是弄懂它为什么这么写会有利于我们做变体题,OK,这里我们愚笨地探讨一下1+1问题。首先我们想一下前序遍历的递归写法是怎么写的,先访问根节点,然后递归访问左子树,再递归访问右子树,既然都说递归的本质是栈,递归进入的过程是入栈,递归退出的过程是出栈,栈是先入后出的,因此若我们想在递归时先访问左子树后访问右子树,那么在入栈时应该先压栈右子树后压栈左子树。【边入栈边出栈】—>前序遍历栈不是很深。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if (!root) return {};
vector<int> res;
stack<TreeNode*> st{{root}};
while (!st.empty()) {
TreeNode *p = st.top(); st.pop();
res.push_back(p->val);
if (p->right) st.push(p->right);
if (p->left) st.push(p->left);
}
return res;
}
};
中序遍历
解题思路: 按照前序遍历中的思维引导,对于中序遍历,我们可以这么思考为什么这么写,中序遍历的递归写法是,先遍历左子树,再访问根节点,后遍历右子树,这里我们做进一步解释,遍历左子树意味着在访问根节点之前需要将整个左子树压入栈中,而在访问完根节点后,我们会将访问的对象切换到右子树根节点,但是并不访问它,而是按照左-根-右访问右子树,因此做指针调转动作后,依然压右子树的左子树入栈。【压左子树至顶,再切右子树】—>中序遍历栈非常深。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> st;
TreeNode *p = root;
while (!st.empty() || p) {
while (p) {
st.push(p);
p = p->left;
}
p = st.top(); st.pop();
res.push_back(p->val);
p = p->right;
}
return res;
}
};
后序遍历
解题思路: 依然按照前序遍历的思维引导,后序遍历是左-右-根,我们可以得出,若根节点访问,则左或者右子树(若有)均已被访问过,那么我们在用栈模拟递归时,是否仍需要像中序将左子树全压栈?或许这个结论我们换一个更明确的表达,因为前序是根-左-右,边访问边出栈,因此严格意义上讲递归的深度并不大,对应的栈的深度也不会太大,确切的说至多为2,而对于中序和后序,因为要向左至底(i.e.,访问左左左…左子树节点不存在)时,才开始递归回退,因此栈的深度很大,OK,讨论完这个问题后,我们依然采取左、右子树依次压栈(而不需要一直压左子树),只有当访问节点为叶子节点或者访问节点的左右子树均被访问过,那么访问该节点并出栈,那么如果表示访问节点(A)的左右子树均被访问过呢?最简单粗暴的访问是,用hash记录所有被访问过的节点,然后当访问到A时,查找A的左右子节点是否被访问过即可,但是显然这并不是最佳的方案,我们分析一下,假设节点A是7,当访问3时,左左子树和左右子树两分支已经被访问过,那么接下来会访问3,而当准备访问7时,6恰好是前一个被访问,OK,我们可以得出一个结论,在考虑到6可能为NULL时,若访问7时,前一个访问的节点是3或者6,则可以认为7的左、右子树已经被访问过,OK,请看下面代码。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if (!root) return {};
vector<int> res;
stack<TreeNode*> st{{root}};
unordered_set<TreeNode*> us;
us.insert(NULL);
TreeNode *head = root;
while (!st.empty()) {
TreeNode *p = st.top();
if ((!p->left && !p->right) || (head == p->left || head == p->right)) {
res.push_back(p->val);
st.pop();
us.insert(p);
head = p;
} else {
if (p->right) st.push(p->right);
if (p->left) st.push(p->left);
}
}
return res;
}
};
——————————————
树的遍历统一解题模板
我们先列出代码,然后观察代码分析特征,然后再从理论分析角度给出合理性解释。观察下面代码,会发现解题形式非常统一,均引入辅助指针节点,在压栈顺序上也是先压左子树后压右子树(当然有时候有说法是,根据栈先入后出特征,需要先压右子树再压左子树,这样能保证出栈时先左后右,具体怎么个压法,我的建议是画图,按照递归转栈的方式,画图即可弄清楚如何左右子树压栈),不同的是,获取遍历序列的位置不同,根据先序特征,遍历到节点时即获取值,根据中序遍历特征,只有再遍历完左节点时才获取值,后序可能麻烦一些,需要记录前置遍历节点,然后代码的另一个特征是,if-else结构中if访问的是节点p左子树,else中访问的节点p右子树。当然后序遍历也可以将其转化为先序遍历解,这篇博客中就讲到这个解法。
1)先序
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if (!root) return {};
stack<TreeNode*> st;
TreeNode *p = root;
vector<int> res;
while (!st.empty() || p) {
if (p) {
st.push(p);
res.push_back(p->val);
p = p->left;
} else {
p = st.top(); st.pop();
p = p->right;
}
}
return res;
}
};
2)中序
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if (!root) return {};
vector<int> res;
stack<TreeNode*> st;
TreeNode *p = root;
while (!st.empty() || p) {
if (p) {
st.push(p);
p = p->left;
} else {
p = st.top(); st.pop();
res.push_back(p->val);
p = p->right;
}
}
return res;
}
};
3)后序
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if (!root) return {};
vector<int> res;
stack<TreeNode*> st;
TreeNode *p = root;
TreeNode *prev = root;
while (!st.empty() || p) {
if (p) {
st.push(p);
p = p->left;
} else {
p = st.top();
if (!p->right || p->right == prev) {
res.push_back(p->val);
st.pop();
prev = p;
p = NULL;
} else {
p = p->right;
}
}
}
return res;
}
};
——————————————
- 树的遍历(先、中、后、层序遍历)时间复杂度和空间复杂度均为O(n)
——————————————
173. 二叉搜索树迭代器
解题思路: 从题意不难得出要求中序遍历序列,偷懒的方法是直接用递归方法将中序求出来存在一个数组里,然后每次从数组中取元素,不过一般面试的时候面试官不会轻易让你写这样的代码,会附加条件,数据不能一次加载到内存中,i.e.,不能将数据存在数组中再遍历,因此本质上此题是在考察中序遍历的非递归写法,因此将中序非递归写法稍加修改即可解此题。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class BSTIterator {
public:
BSTIterator(TreeNode* root) {
p = root;
}
/** @return the next smallest number */
int next() {
int res;
while (p) {
st.push(p);
p = p->left;
}
p = st.top(); st.pop();
res = p->val;
p = p->right;
return res;
}
/** @return whether we have a next smallest number */
bool hasNext() {
return (!st.empty() || p);
}
private:
stack<TreeNode*> st;
TreeNode *p;
};
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator* obj = new BSTIterator(root);
* int param_1 = obj->next();
* bool param_2 = obj->hasNext();
*/