最近刷了几道题,把思路在此记录一下。刷 leetcode 上的题目,最近也有一点小小的心得:
- 要看懂题目,一定要清楚题目表达的是什么?面试的时候也是,一定要搞懂题目的意思,没看懂就跟面试官沟通一下;
- 不会的题目很正常,平常心,不必灰心
- 学会解题的思路很重要
- 记录一下你的解题心路历程,你在哪个步骤卡住了,怎么突破的等等
- 搜索一下大神们是怎么解决类似问题的(B 站,油管,谷歌等)。
什么前序遍历?
关于什么是二叉树,二叉树有什么性质,规律等,在此不过多赘述了,直接进入主题 ,leetcode 144 是关于前序遍历的一个题目,下面解释一下什么是前序遍历:
二叉树的前序遍历,中序遍历,后序遍历都是以根结点来做参考点的,前序遍历就是从根结点出发遍历,依次遍历以下节点,如上图所以,我把这个二叉树做了一个拆分:前序遍历的顺序是 A -> B -> C
A 是根结点, A = {1}
B 和 C 是一个子二叉树;
B 前序遍历的顺序是: B = {2,4,5};
C 前序遍历的顺序是: C = {3,6};
那么综上前序遍历的顺序是 A -> B -> C 也就是就是 {1,2,4,5,3,6};
这道题虽然是 leetcode 的简单题,如果你第一次遍历二叉树,应该会耗费一点时间的,因为相关的解题思维没有建立起来,leetcode 简单题很有必要耐心做一下的,有时候它的简单对于你我未必简单,简单题可以构建我们的算法思维,简单题会了,难题才有可能突破。
栈来遍历
题目的要求是使用前序遍历依次打印各个节点的值,这时候我们可以使用一个数据结构-栈, 栈的性质是后进先出或者先进后出,我们控制好入栈的顺序,依次弹出就好,这里的难点就是怎么控制这个入栈的顺序,如果你有兴趣,可以试着想一下 leetcode 144 ,先不要看答案:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> values = new ArrayList();
if(root == null){
return values;
}
Stack<TreeNode> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
if(node != null){
values.add(node.val);
}
if(node.right !=null){
stack.push(node.right);
}
if(node.left != null){
stack.push(node.left);
}
}
return values;
}
}
这里的难点就是控制好入栈的一个顺序:起初根结点入栈,弹出来后获取到左右节点,那么是先入栈左结点还是先入右边的结点呢?那么这里就要思考,接下来应该遍历的是哪一个结点,这里很明显接下来遍历的应该是左结点,因此,左结点放在后面入栈,充分利用栈的性质,后入先出。
你可以通过上面的方法处理中序遍历和后序遍历吗?
递归遍历(隐藏的栈)
递归其实也是一个另类的栈-方法栈,java 的 JVM 的结构中就有一个栈结构,其实也是利用后进先出的一个特性,代码如下:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> values = new ArrayList();
if(root == null){
return new values;
}
values.add(root.val);
values.addAll(preorderTraversal(root.left));
values.addAll(preorderTraversal(root.right));
return values;
}
}
这里题目给的返回值是 List 集合,在构建返回值的时候可能会纠结一下,我们可以使用 List 的 addAll 方法把前一个方法栈的返回值添加到一个 List 集合当中。
你可以使用递归处理中序遍历和后序遍历吗?
不一样的思考,还是栈
下面我们来分析一下前序遍历树的时候我们对每个结点做了什么?
- 遍历该结点,添加该结点到集合中
- 找到左结点
- 找到右结点
在进一步总结一下:每个结点大致可以总结如下:
- 打印该结点,题目中也就是添加到集合中
- 走到下一个节点(左结点或者右结点)
因此我们可以定义一个类用来保存当下结点的一个状态:
private class Operate{
int status; // 0:打印,1,访问
TreeNode node;
public Operate(int status,TreeNode node){
this.status = status;
this.node = node;
}
}
定义一个操作类,status 用来保存当前结点的一个状态, 比如 0 就是打印该结点也就是该结点需要添加到集合中,1 就是访问下一个节点,node 用来保存下一个节点;
分析到此,实现代码如下:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> values = new ArrayList();
if(root == null){
return values;
}
Stack<Operate> stack = new Stack();
stack.push(new Operate(1,root));
while(!stack.isEmpty()){
Operate operate = stack.pop();
if(operate.node == null) continue;
if(operate.status == 0){
values.add(operate.node.val);
}
if(operate.status == 1){
stack.push(new Operate(1,operate.node.right));
stack.push(new Operate(1,operate.node.left));
stack.push(new Operate(0,operate.node));
}
}
return values;
}
private class Operate{
int status; // 0:打印,1,访问
TreeNode node;
public Operate(int status,TreeNode node){
this.status = status;
this.node = node;
}
}
}
这里需要考虑清楚的就是如何入栈? 入栈的顺序应该怎么去处理,想清楚了,题目也就解决了。
你可以使用该方法处理中序遍历和后序遍历吗?
morris 遍历
这个方法也不知道是咋想出来的,真是厉害了,佩服,一开始理解可能需要点时间去消化。为啥叫 morris 遍历,我也不太清楚,但是可以猜测是一个叫 morris 的人,想出了这个解法,morris 遍历的来历,知道的可以评论回复一下,感谢。。。
morris 遍历是怎么操作的呢?充分利用二叉树的特性,二叉树中任何一个结点,都是有两个子结点的,不过有的结点的左子结点是 null ;有的右子结点为 null ;有的左右子结点都是 null ,也就是叶子结点。
把这个草稿图在拿出来研究,研究,morris 遍历是怎么回事 ?
我们把上面二叉树分成 A ,B , C 三个部分:
A 的左子结点为 B , A 的右子结点为 C ;
B 是叶子结点, C 也是叶子结点;
遍历的顺序是 A -> B -> A ,morris 遍历是找到 A 的左子结点,此时左子结点 B 没有孩子, 先让 B 的右子结点指向 A , 遍历的时候就可以 A -> B -> A ;
回到 A 后,切断 B -> A 的指针, 这样不影响二叉树的原始结构紧接着 A -> C 遍历结束。
同理我们分析 B 的内部子二叉树也是上述的一个处理逻辑 ;C 这个子二叉树没有左结点直接向右结点遍历就好;
综上我们可以分析 morri 遍历,如果一个结点有左子结点,那么该结点会到达两次,最后一次会切断到达该结点的路径,比如 A -> B -> A 的过程,然后切断 B ->A 不影响二叉树的结构。
上面我们是整体分析,那么抛弃整体,上图可以看到,什么时候 1 这个结点在遍历左分支的时候能再次回到 1 这个结点呢? 应该是 5 -> 1 。所以 morrs 遍历的核心是每走到一个结点先找到该结点左子树上面的最右面的一个结点,找到最右边的结点后使其右指针指向当前遍历的结点。
下面做一个 morri 遍历的总结:记作当前节点为curr.
-
如果curr无左孩子,cur向右移动(cur = cur.right)
-
如果curr有左孩子,找到curr左子树上最右的节点,记为mostRightNode
-
如果mostRightNode的right指针指向空,让其指向cur,curr向左移动(curr = curr.left)
-
如果mostRightNode的right指针指向curr,让其指向空,curr向右移动(curr = curr.right)
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> values = new ArrayList();
if(root == null){
return values;
}
TreeNode curr = root;
TreeNode mostRightNode = null;
while(curr != null){
mostRightNode = curr.left;
if(mostRightNode != null){
while(mostRightNode.right !=null && mostRightNode.right != curr){
mostRightNode = mostRightNode.right;
}
if(mostRightNode.right == null){
mostRightNode.right = curr;
values.add(curr.val);
curr = curr.left;
continue;
}else{
mostRightNode = null;
}
}else{
values.add(curr.val);
}
curr = curr.right;
}
return values;
}
}
因为是前序遍历,这里打印结点的位置需要思考清楚,也就是 values.add(curr.val)
的地方。上面的代码可能需要花点时间思考一下:第一个 while 循环控制树遍历什么时候结束;第二个 while 循环用来找每一个结点的左子上的最右结点;第一个 while 循环中的 if 语句用来判断当前结点是否有左结点,没有左结点,当前结点就向右结点遍历 curr = curr.right;
;If 语句中的嵌套 if ,是用来处理当找到当前结点左子树最右边的结点的时候,最右边的结点右指针指向该结点,然后当前结点继续向左遍历,跳到第一个 while 循环,如此往复。
morri 遍历难度较大,但是仔细想一想,对用笔画一画,还是可以想得通的,加油。
你可以使用 morris 遍历来中序遍历和后序遍历二叉树吗?