题目1:. - 力扣(LeetCode)
题目2:. - 力扣(LeetCode)
题目3:. - 力扣(LeetCode)
注意1:每种遍历方式我都提供了两种方法,带图解的方法为个人尝试编写,并非力扣题解,因此时间和空间复杂度可能并不是最优的,只是记录一下自己当时写这个题的时候的思路;
注意2:还有一种统一迭代法,非常的方便,跟递归法一样,三种遍历方式只需改变一下代码的顺序就行了,逻辑都是一样的;
一、个人方法
前序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
//个人尝试:迭代法 + 栈 -- 实现前序遍历
//前序遍历:中左右
//思路:将右边元素全部放入栈中,直接处理左边和中间的元素
vector<int> result;
stack<TreeNode*> my_stack;
TreeNode* ptr = root;
//剪枝1:
if(root == nullptr) return result;
//先将根节点装进栈中,否则无法进入下面的 while 循环
my_stack.push(ptr);
//思路:把每个取出来的节点都当作一个子树的根节点,然后对这个子树进行前序遍历;
// 对于这个子树而言,首先沿着左侧一路遍历到最底部,所有遇到的右侧节点全部装进栈中,稍后进行遍历;
// 当遍历完一个子树的左半部分之后,开始从栈中取出右侧元素,把他们当作新的子树的根节点,从新进行只遍历左侧的操作;
while(my_stack.size() != 0) {
//获得指向 “新子树的根节点” 的指针
ptr = my_stack.top();
my_stack.pop();
//沿新子树的左侧一直遍历到最底部,则停止遍历
while(ptr->left != nullptr || ptr->right != nullptr) {
result.push_back(ptr->val);
if(ptr->left != nullptr && ptr->right != nullptr) {
my_stack.push(ptr->right);
ptr = ptr->left;
}
else if(ptr->left != nullptr && ptr->right == nullptr) {
ptr = ptr->left;
}
else if(ptr->left == nullptr && ptr->right != nullptr) {
my_stack.push(ptr->right);
break;
}
else break;
}
//剪枝2:针对测试用例3,因为在测试用例3的情况下无法进入 while 循环
//剪枝3:针对测试用例1,因为当 ptr 在新的一轮循环中指向叶节点的时候,是无法进入 while 循环的,所以只能在外面加;
if(ptr->left == nullptr && ptr->right == nullptr) result.push_back(ptr->val);
}
return result;
}
};
图解:
中序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
//个人尝试:迭代法 + 栈 -- 实现中序遍历
//中序遍历:左中右
//思路:和前序遍历不同,虽然我们在前序遍历的时候也是沿着树的最左边往下遍历,但是我们是遍历一个元素就把这个元素
// 加到 result 中,因为添加顺序是中左右,而在沿左侧往下遍历的时候“中”元素都已经被遍历到了,所以直接把他添加
// 到 result 中即可,只需要把右边元素的指针添加到栈中即可,栈中只放同一种东西,就是右侧分支的指针;
//思路:但是对于中序遍历,我们的添加顺序是左中右,所以在我们沿着树的最左侧一直往下走的时候,我们经过的节点都是属于“中”,
// 而他对应的值应该放在“左”元素的后面,所以我们不仅要把“右”元素的指针压入栈中,还要把“中”元素的指针也压入栈中;
// 这样就造成一个问题,如果栈中“右”和“中”元素交替存在,那么对于一棵只有左分支的二叉树(只有根节点有右分支),这样栈中
// 的元素就是“右中中中中中”,而不是类似满二叉树那样的“右中右中右中”,这样我们就无法辨认栈中的元素是“右”还是“中”;
//思路:其实如果针对“中”元素和“右”元素的处理是一样的就还好说,但是对于“中”元素需要将其直接放入 result ,对于“右元素”则
// 需要将他作为新的子树的根节点,然后沿左侧遍历,所以两个元素不能装在一起;
//思路:那么我想到了第一个解决思路就是,使用两个栈分别存储“中”和“右”,但是这又造成一个问题,我们无法知道哪两个元素是
// 连在一起的;
//思路:因此我使用一个栈,但是栈中每个元素存两个指针,一个指向“中”一个指向“右”;
TreeNode* ptr = root;
stack<pair<TreeNode*, TreeNode*>> my_stack;
vector<int> result;
//剪枝:
if(root == nullptr) return result;
//初始化栈和 ptr 指针,使 ptr 指针最开始的位置指向整个二叉树的最左侧叶节点
while(ptr != nullptr) {
my_stack.push(pair<TreeNode*, TreeNode*>(ptr, ptr->right));
ptr = ptr->left;
}
//开始取出栈中的元素,给 result 添加元素
while(my_stack.size() != 0) {
pair<TreeNode*, TreeNode*> temp_pair = my_stack.top();
my_stack.pop();
ptr = temp_pair.first;
result.push_back(ptr->val);
ptr = temp_pair.second;
if(ptr == nullptr) continue;
//先沿着新子树的根节点一直遍历到最左侧叶节点
while(ptr != nullptr) {
my_stack.push(pair<TreeNode*, TreeNode*>(ptr, ptr->right));
ptr = ptr->left;
}
}
return result;
}
};
图解:
后序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
//个人尝试:迭代法 + 栈 -- 实现后序遍历
//后序遍历:左右中
vector<int> result;
stack<pair<TreeNode*, TreeNode*>> my_stack;
TreeNode* ptr = root;
//剪枝:
if(root == nullptr) return result;
//初始化栈,将 ptr 指针指向子树的最左侧叶节点
while(ptr != nullptr) {
my_stack.push(pair<TreeNode*, TreeNode*>(ptr, ptr->right));
ptr = ptr->left;
};
//思路:后序遍历和中序遍历差不太多,细节中小修改一下就行,也是把“中”和“右”元素封装到一层保存;
// 因为后序遍历的顺序是左右中,所以我们在将左元素加入后,我们就要取出栈中的元素,去遍历“右”元素
// 的子树,但是这就存在一个问题,因为栈中的元素弹出后就被删除了,而对这个 pair 元素,里面包含
// 的“中”元素却没有被加入 result 中,被错过了;
// 所以这里我想了一个小技巧,就是在取出 pair 元素后,先将“中”元素和空指针重新压入栈中,这样就不
// 会丢掉这个“中”元素,然后去遍历“右”元素作为根节点的子树,沿着左边一直找到该新子树的最左叶节点;
while(my_stack.size() != 0) {
pair<TreeNode*, TreeNode*> temp = my_stack.top();
my_stack.pop();
ptr = temp.second;
if(ptr == nullptr) {
result.push_back(temp.first->val);
continue;
}
else {
my_stack.push(pair<TreeNode*, TreeNode*>(temp.first, nullptr));
while(ptr != nullptr) {
my_stack.push(pair<TreeNode*, TreeNode*>(ptr, ptr->right));
ptr = ptr->left;
}
}
}
return result;
}
};
图解:
二、统一迭代法
前序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
//自己尝试实现【统一迭代法】
vector<int> result;
stack<TreeNode*> my_stack;
if(root == nullptr) return result;
my_stack.push(root);
while(my_stack.size() != 0) {
TreeNode* ptr = my_stack.top();
my_stack.pop();
if(ptr != nullptr) {
if(ptr->right != nullptr) my_stack.push(ptr->right);
if(ptr->left != nullptr) my_stack.push(ptr->left);
my_stack.push(ptr);
my_stack.push(nullptr);
}
else {
TreeNode* temp = my_stack.top();
my_stack.pop();
result.push_back(temp->val);
}
}
return result;
}
};
//思路解析:将“中”元素入栈之后,在后面接上空指针,这样当元素出栈的时候读取到空,就知道空指针后面那一个元素
// 需要加入 result 中;
// 正因为空指针在栈中起到一个标识符的作用,即标志着他后面的那个元素是“中”,因此我们就不能随便往
// 栈里面丢入空指针了,因此当我们在读到叶节点的时候,因为他的两个子节点都是空节点,如果要入栈的话
// 就是入栈两个空指针,这样会导致空指针的标识符作用发生改变,即空指针后面可能不再是“中”元素,因此
// 我们需要【if(ptr->right != nullptr)】和【if(ptr->left != nullptr)】语句来限制叶节点的空子
// 节点随便入栈;
//总结一点:【统一迭代法】给 result 增加元素的时候,必须保证是“中”节点,才会将节点值赋值给 result 数组;
// 不管是前序遍历/中序遍历/后序遍历都是如此,只将“中”节点的值塞进 result 中;
中序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
//自己尝试实现【统一迭代法】
vector<int> result;
stack<TreeNode*> my_stack;
if(root == nullptr) return result;
my_stack.push(root);
while(my_stack.size() != 0) {
TreeNode* ptr = my_stack.top();
my_stack.pop();
if(ptr != nullptr) {
if(ptr->right != nullptr) my_stack.push(ptr->right);
my_stack.push(ptr);
my_stack.push(nullptr);
if(ptr->left != nullptr) my_stack.push(ptr->left);
}
else {
TreeNode* temp = my_stack.top();
my_stack.pop();
result.push_back(temp->val);
}
}
return result;
}
};
//思路讲解:非常巧妙的方法,因为对“中”和“右”元素的处理是不同的,对“中”元素要直接加入 result 中,对“右”元素
// 要将其视为一棵新的子树进行遍历,因此如果将“中”和“右”元素都放入同一个栈中,就无法区分这两种元素,
// 因此无法对症下药,对不同属性的元素采取不同的处理方式;
//解决方案:因此当我们遇到“中”元素的时候,先把“中”元素压入栈中,然后再压入一个空指针,这样我们读取到空指针的
// 的时候,就知道了这个空指针之后的那个元素要被放入 result 中;对于叶子节点,叶子节点相当于有两个
// 空节点,所以在读取到叶子节点后面挂的俩空节点时,这个叶子节点也就是“中”元素了,然后再在后面入栈
// 一个空节点,那么叶子节点在栈中也变成了前面有一个空指针的“中”元素了,也可以正常的加入到 result 中了;
后序遍历:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
//自己尝试实现【统一迭代法】
vector<int> result;
stack<TreeNode*> my_stack;
if(root == nullptr) return result;
my_stack.push(root);
while(my_stack.size() != 0) {
TreeNode* ptr = my_stack.top();
my_stack.pop();
if(ptr != nullptr) {
my_stack.push(ptr);
my_stack.push(nullptr);
if(ptr->right != nullptr) my_stack.push(ptr->right);
if(ptr->left != nullptr) my_stack.push(ptr->left);
}
else {
TreeNode* temp = my_stack.top();
my_stack.pop();
result.push_back(temp->val);
}
}
return result;
}
};
总结:关于统一迭代法的思路
- 只有“中节点”才能被加入到数组 result 中;
- 叶节点也属于一种特殊的“中节点”,相当于有两个空的子节点,因此叶子节点也可以当作“中节点”压入栈中,实现写入 result 的目的;
- 在将“左节点”,“中节点”,“右节点”压入栈中的时候,要在压入“中节点”之后再压入一个空指针 nullptr 作为标识符,当栈弹出空指针的时候,我们就知道栈当前的头部元素是一个“中节点”而不是“左节点”或者“右节点”,就可以对这个节点元素进行插入数组 result 的操作;
- 因为我们需要空指针作为“中节点”的标识符,所以当我们读取到叶节点的时候,不能直接将其两个子节点(空节点)压入栈中,否则空指针的标识符作用会受影响;