【经典结构】二叉树

二叉树

1.基本概念

二叉树是每个节点最多有两个子树的树结构,度可能是0,1,2;

完成二叉树:从左到右依次填满;
满二叉树:除了叶子节点,所有节点都有两个孩子,并且所有叶子节点在同一层;

2.性质

1.完全二叉树除了最后一层外,下一层节点个数是上一层两倍,
如果一颗完全二叉树的节点总数是n,那么叶子节点个数为n/2(n为偶数)或(n+1)/2(n为奇数);
2. if一颗二叉树的层数是n,那么满二叉树的节点数为2^n-1; 叶子节点的总数为2^(n-1)

3.递归在二叉树中的应用

写递归算法的关键就是明确函数的定义是什么,然后相信这个定义,利用这个定义推道出最终结果,绝不要跳入递归的细节 (人的小脑袋才能堆几层栈)

写树相关的算法,简单的就是说,去想象一个最小单元,搞清楚当前最小单元中t节点“该做什么,什么时候做”这两点,然后根据函数定义递归调用子节点。

  • 该做什么:就是我们的最小单元中的root节点(可能不止一个)想要实现功能,需要得到什么信息,然后能做什么;
    • 做点什么能够提供信息给下面的子树利用(先序)
    • 能从下面的子树上获得什么信息然后利用(后序)
  • 什么时候做:刚才写的代码应该放在前序、中序还是后序的代码位置上。

把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了,难点在于如何通过题目的要求思考出每一个节点需要做什么。
写完之后自己代入一个最基本的看能不能实现功能,会不会出不来,检验一下。

4.遍历

4.1 概念

  • 前序遍历:根节点 -> 左子树 -> 右子树;
  • 中序遍历:左子树 -> 根节点 -> 右子树;
  • 后序遍历:左子树 -> 右子树 -> 根节点;

4.2 递归实现

递归的时候不要太在意实现的细节,其本质上是通过栈来实现的,每次在方法里自己调自己,就把新调用的自己压栈,是一个有去有回的过程。
对于递归,关键就是要清楚函数的功能,什么时候停下来。
对于前序遍历,想先打印根节点,再左再右;那就先输出,再去递归调用,传入当前节点的左子树,左子树同样打印它的根,传入根的左子树,知道整个左子树处理完了,再去处理右子树;

class BinaryTreeTraverse<T>{ /** * 前序遍历(递归实现); */ public static void preOrderByRecursion(TreeNode node){ if (node == null) return; //递归终止条件; System.out.println(node.value); //获取根节点的值; preOrderByRecursion(node.left); //左子树的根节点; preOrderByRecursion(node.right);//右子树的根节点; } /** * 中序遍历(递归实现) */ public static void inOrderByRecursion(TreeNode node){ if (node == null) return; //递归终止; inOrderByRecursion(node.left); //先左子树; System.out.println(node.value); //再根节点; inOrderByRecursion(node.right); //再右节点; } /** * 后序遍历(递归实现) */ public static void postOrderByRecursion(TreeNode node){ if (node == null) return; postOrderByRecursion(node.left); postOrderByRecursion(node.right); System.out.println(node.value); }

时间复杂度:0(N),每个节点遍历N次;
空间复杂度:O(N),递归过程中栈的开销;

4.3 迭代实现

前序遍历
前序遍历就是我们来手动实现在递归过程中的栈。
想要实现先左再右,那压栈的时候右先入栈,左再入栈。
如下图所示;
image
规则:
压入根节点;
1.弹出就打印;
2.如有右孩子,压入右;
3.如有左孩子,压入左;
重复1.2.3
思考一下这个过程;其实就是相当于两个孩子来替换栈里的根节点,这就很符合前序的定义,根先走,左子树干到了最顶部,要是我左子树还有左孩子,那我也走,两个孩子来替我,每次都是我先走,我左孩子这边整个都完事了,再来我右孩子,因为我右孩子被压在最下面。

public static void preOrder(TreeNode node){ Stack<TreeNode> stack = new Stack<>(); if (node != null){ stack.push(node); //根节点入栈; } while (!stack.isEmpty()){ TreeNode top = stack.pop(); //弹出就打印; System.out.println(top.value); if (top.right != null) stack.push(top.right); //依次入栈右节点和左节点; if (top.left != null) stack.push(top.left); } }

中序遍历
规则:
1.整条左边界依次入栈;
2.条件1执行不了了,弹出就打印;
3.来到弹出节点的右子树上,继续执行条件1;(右树为空,执行2.弹出就打印;右树有节点,执行1.压栈;
说明:将整个树全用左边界去看,都是先处理了左边界,将左边界分解成了左头,头先入,左再入,然后弄不动了,弹出,然后看其右节点,再把右节点里的左依次进去;就这样往返;
如下如所示;

image

public static void inOrder(TreeNode node){ Stack<TreeNode> stack = new Stack<>(); while (!stack.isEmpty() || node != null){ if (node != null){ //条件1;能往左走就往左走; stack.push(node); node = node.left; }else { TreeNode top = stack.pop(); //条件2;弹出打印; System.out.println(top.value); node = top.right; //条件3;来弹出节点右树上,继续1; } } }

后序遍历
后序遍历可以用前序遍历来解决,想一下前序遍历:根左右,我们先压右树再压左树。怎么实现根右左呢,可以先压左树再压右树嘛,然后反过来不就是左右根了吗?(反过来用栈来实现,栈一个很大的作用就是实现逆序)

public static void postOrder(TreeNode node){ Stack<TreeNode> stackA = new Stack<>(); Stack<TreeNode> stackB = new Stack<>(); if (node != null){ stackA.push(node); } while (!stackA.isEmpty()){ TreeNode top = stackA.pop(); stackB.push(top); //栈A弹出的进入栈B;先实现根右左,B倒序实现左右根; if (top.left != null) stackA.push(top.left); if (top.right != null) stackA.push(top.right); } while (!stackB.isEmpty()){ System.out.println(stackB.pop().value); } }

4.4 Morris实现

二叉树的结构中只有父节点指向孩子节点,孩子节点不能向上指,所以需要栈。
而Morris遍历的实质就是让下层节点也能指向上层,怎么办呢,一个节点有两个指针,左和右,如果这两个指针都有指向具体节点了那指定用不上了。
但是二叉树可是有很多空闲指针啊,比如说所有的叶子节点,它们的指针就都指向null,所以可以利用其right指针指向上层。
这样把下层往上层建立连接以后,cur指针就可以完整的顺着一个链条遍历完整个树。
因为不用堆栈,所以其空间复杂度变为O(1);
如下图所示:

image

cur指针走的顺序:1 2 4 2 5 1 3 6 3 7;
核心: 以某个根节点开始,找到其左子树的最右节点(必然是个叶子节点),然后利用其right指针指向根节点(建立从下到上的连接)

  • 到达两次的是有左子树的节点;
  • 到达一次的是没有左子树的节点;

原则:

  • 1.如果cur无左孩子,cur向右移动(cur=cur.right)【图中有4到2】
  • 2.如果cur有左孩子,找到cur左子树上最右的节点,记为mostRightNode;
    • 1.如果mostRightNode的right指针指向空,让其指向cur,cur向左移动(cur=cur.left)
    • 2.如果mostRightNode的right指针指向cur,让其指向空,cur向右移动(cur=cur.right)【图中由2到5】

(迭代法的中序遍历我们可以将整个树全部分成左边界去看,如上面的图,其实在morris遍历里我们可以将整个树全部分成右边界来看)

public static void morris(TreeNode node){ if(node == null){ return; } TreeNode cur = node; TreeNode mostRightNode = null; //记录cur左子树的最右节点; while(cur != null){ mostRightNode = cur.left; if(mostRightNode != null){ //cur有左子树,就证明有下一层; //找到cur左子树的最右节点(找到cur下一层右边界的最后一个) while(mostRight.right != null && mostRightNode != cur){ //1.最右节点为空说明到头了,找到了; //2.最右节点指向上层说明已经处理过了,来过了; mostRightNode = mostRightNode.right; } //走到这里证明跳出上面循环,无非两个原因: //1.右边没了;2.右边指向上层了(之前就处理过了); if(mostRightNode.right == null){ mostRightNode.right = cur; //建立从最右节点到cur的连接; cur = cur.left; //处理下一个节点; continue; }else{ //能到这里说明已经建立了最右节点到cur的连接; //也说明cur指的这个节点是第二次到了,断开连接; mostRightNode.right = null; } } //cur右移的情况: //1.cur没有左子树了(自然要开始处理右子树) //2.cur有左子树,但是cur左子树最右节点已经指向cur了(执行完上面else断开后,cur左边已经完全处理好了,开始右移。) cur = cur.right; } }

前序遍历

1.对于cur只达到一次的节点(没有左子树),cur达到就打印;
2.对于cur到达两次的节点(有左子树),到达第一次时打印;

public static void preOrderMorris(TreeNode node){ if (node == null){ return; } TreeNode cur = node; TreeNode mostRightNode = null; while (cur != null){ mostRightNode = cur.left; if (mostRightNode != null){ //到达两次的节点; //找到cur左子树的最右节点; while (mostRightNode.right != null && mostRightNode.right != cur){ mostRightNode = mostRightNode.right; } if (mostRightNode.right == null){ mostRightNode.right = cur; //指向上层cur; System.out.println(cur.value); //第一次到的时候打印; cur = cur.left; continue; }else{ mostRightNode.right = null; //第二次到时不打印; } }else { System.out.println(cur.value); //只到达一次的节点; } cur = cur.right; } }

中序遍历

1.对于cur只达到一次的节点(没有左子树),cur达到就打印;
2.对于cur到达两次的节点(有左子树),到达第二次时打印;

public static void inOrderMorris(TreeNode node){ if (node == null){ return; } TreeNode cur = node; TreeNode mostRightNode = null; while (cur != null){ mostRightNode = cur.left; if (mostRightNode != null){ //有左子树,到达两次的节点; while (mostRightNode.right != null && mostRightNode.right != cur){ mostRightNode = mostRightNode.right; } if (mostRightNode == null){ mostRightNode.right = cur; //第一次不打印; cur = cur.left; continue; }else { System.out.println(cur.value); //第二次遇到时打印; mostRightNode.right = null; } }else { System.out.println(cur.value); //只到达一次的节点,遇到就打印; } cur = cur.right; } }

后序遍历

后序遍历比前面两个要复杂一点;
将一个节点的连续右节点当成是一个单链表看,如下图所示:

image

当我们到达最左侧,也就是左边连线已经创建完毕了。
打印 4
打印 5 2
打印 6
打印 7 3 1
我们将一个节点的连续右节点当成一个单链表来看待。
当我们返回上层之后,也就是将连线断开的时候,打印下层的单链表
比如返回到 2,此时打印 4
比如返回到 1,此时打印 5 2
比如返回到 3,此时打印 6
最后别忘记头节点那一串,即1 3 7
那么我们只需要将这个单链表逆序打印就行了。
这里不应该打印当前层,而是下一层,否则根结点会先与右边打印。

public static void postOrderMorris(TreeNode node){ if (node == null){ return; } TreeNode cur = node; TreeNode mostRightNode = null; while (cur != null){ mostRightNode = cur.left; if (mostRightNode != null){ while (mostRightNode.right != null && mostRightNode.right != cur){ mostRightNode = mostRightNode.right; } if (mostRightNode.right == null){ mostRightNode.right = cur; cur = cur.left; continue; }else { //能到这里的都是达到过两次的,也就是是有左孩子的。 mostRightNode.right = null; //这时候是已经返回上层之后,断开了连接,所以打印下层的单链表; postMorrisPrint(cur.left); } } cur = cur.right; } postMorrisPrint(node); //最后把头节点那一串右打印一遍; } public static void postMorrisPrint(TreeNode node){ TreeNode reverseList = postMorrisReverseList(node); //反转链表; TreeNode cur = reverseList; while (cur != null){ System.out.println(cur.value); cur = cur.right; } postMorrisReverseList(reverseList); //最后再还原; } public static TreeNode postMorrisReverseList(TreeNode node){ TreeNode cur = node; TreeNode pre = null; while (cur != null){ TreeNode next = cur.right; cur.right = pre; pre = cur; cur = next; } return pre; }

4.5 层次遍历

层次遍历顾名思义就是一层一层的遍历。从上到下,从左到右,那需要借助什么结构呢?
可以采用队列的结构,利用其先进先出的特性,每一层依次入队,再依次出队。
对该层节点进行出队时,将这个节点的左右节点入队,这样当一层所有节点出队完成后,下一层也入队完成了。

/** * 层次遍历 * 借助队列的结构,每一层依次入队,再依次出队; * 对该层节点进行出队操作时,需要将该节点的左孩子和右孩子入队; */ public static int layerOrder(TreeNode node){ if (node == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); queue.add(node); while (!queue.isEmpty()){ int size = queue.size(); //当前层的节点数量; for (int i = 0; i < size; i++){ TreeNode front = queue.poll(); System.out.println(front.value); if (front.left != null) queue.add(front.left); if (front.right != null) queue.add(front.right); } } }

5.深度

二叉树的最大深度是根节点到最远叶子结点的距离;

5.1 最大深度

递归实现

1.终止条件:在二叉树为空的时候,深度为1;
2.缩小范围,等价关系:给定一个二叉树,其深度为左子树的深度和右子树的深度的最大值+1;

/** * 求最大深度(递归) * 最大深度是左子树和右子树的最大深度的大的那个+1; */ public static int maxDepthByRecursion(TreeNode node){ if(node == null) return 0; int leftDepth = maxDepthByRecursion(node.left); int rightDepth = maxDepthByRecursion(node.right); return Math.max(leftDepth,rightDepth)+1; }

非递归实现(层次遍历)
关键点:每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。
采用二叉树的层次遍历,来计数总共有多少层,采用队列的结构,当前层节点出队,计数器加1,然后把下一层的节点全部入队,直到队为空。

/** * 求最大深度(非递归) * 层次遍历(BFS) * 每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。 */ public static int maxDepth(TreeNode node){ if (node == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); int level = 0; //层数; queue.add(node); while (!queue.isEmpty()){ level++; int levelNum = queue.size(); //每层的节点数; for (int i = 0; i < levelNum; i++){ TreeNode front = queue.poll(); //当前层出队,下一层入队; if (front.left != null) queue.add(front.left); if (front.right != null) queue.add(front.right); } } return level; }

5.2 最小深度

二叉树的深度是根节点到最近叶子节点的距离;
递归实现

此题不能像最大深度那样直接求两颗子树的最大然后+1,最大深度可以是因为取大值不会影响一棵树为空的时候。但是取最小就不一样了,如果一棵树为空,那最小的应该是不为空的那边的值,但是还按原来方式就变成了0+1;比如下面这个例子:最小深度应该我2.但是按原来方式写的话最小深度就会变为1.所以,在处理每一个节点的时候,如果有两个孩子,那就可以继续取小+1,如果只有一个孩子,那就只能去递归它的孩子。
image

/** * 求最小深度(递归) * 注意和求最大深度的区别; */ public static int minDepthByRecursion(TreeNode node){ if (node == null) return 0; if (node.right == null && node.left == null) return 1; if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1; if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1; return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1; }

非递归实现(层次遍历)
关键点:每遍历一层,则计数器加+1;在遍历的过程中,如果出现了没有叶子节点的节点,那就可以结束了,就是最小深度
采用二叉树的层次遍历,来计数总共有多少层,采用队列的结构,当前层节点出队,计数器加1,然后把下一层的节点全部入队,直到遇到叶子节点或队为空。

/** * 求最小深度(非递归) */ public static int minDepth(TreeNode node){ if (node == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); int level = 0; queue.add(node); while (!queue.isEmpty()){ level++; int levelnum = queue.size(); for(int i = 0; i < levelnum; i++){ TreeNode front = queue.poll(); if (front.left == null && front.right == null){ return level; //遇到第一个无叶子节点的时候,该节点的深度为最小深度; } if (front.right != null){ queue.add(front.right); } if (front.left != null){ queue.add(front.left); } } } return level; }

6.重构二叉树

根据二叉树的前序或后序中的一个再加上中序来还原出整个二叉树。
注意: 中序是必须有的,因为其可以明确的把左右子树分开。
先看下3种遍历的特点(如下图):

image

特点

  • 1.前序的第一个节点是root,后序的最后一个节点是root。
  • 2.每种排序的左右子树分布都是有规律的。
  • 3.每一个子树又可以看成是一颗全新的树,仍然遵循上述规律。

6.1 前序+中序

前序的遍历顺序是根左右,中序的遍历顺序是左中右,

递归实现
1.前序的第一个节点是root节点,对应能够找到在中序中的位置。
2.根据中序遍历的特点,在找到的根前边序列是左子树的中序遍历序,后边序列是右子树的中序遍历。
3.求出左边序列的个数,比如设为leftSize,那在前序序列中紧跟着根的leftSize个元素是左子树的前序序列,后边的为右子树的前序序列。
4.这样就又获得了两个子树的前序遍历和中序遍历,开始递归。

/** * 根据前序遍历和中序遍历构造二叉树; */ public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){ if (preorder == null){ return null; } //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树 //用一个map来存储元素在中序遍历中的位置, Map<Integer,Integer> map = new HashMap<>(); for(int i = 0; i < inorder.length; i++){ map.put(inorder[i], i); } return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map); } //传入前序和中序,传入前序的左右边界,中序的左右边界; private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){ if (preleft > preright) return null; //获得整颗树的根节点:先序中的第一个元素; TreeNode root = new TreeNode(preorder[preleft]); //得到此元素在中序中的位置,以此进行划分出左右子树; int rootIndex = map.get(root); //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。 int leftTreeSize = rootIndex - inleft; //左子树的中序:inleft不变,inright为rootIndex-1; //左子树的前序:preleft为根后一位,即preleft+1,preright为根后leftTreeSize位,即preleft+leftTreeSize; root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map); //右子树的中序:inleft为rootIndex+1,inright不变; //右子树的前序:preleft为左子树的右边界+1,即preleft+leftTreeSize+1,preright不变; root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map); return root; }

6.2 后序+中序

后序的遍历顺序是左右根,中序的遍历顺序是左根右,

递归实现
1.后序的第一个节点是root节点,对应能够找到在中序中的位置。
2.根据中序遍历的特点,在找到的根前边序列是左子树的中序遍历,后边序列是右子树的中序遍历。
3.求出左边序列的个数,比如设为leftSize,那在后序序列中的leftSize个元素是左子树的后序序列,后边的为右子树的后序序列。
4.这样就又获得了两个子树的后序遍历和中序遍历,开始递归。

/** * 根据后序遍历和中序遍历构造二叉树 */ public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){ if (postorder == null){ return null; } //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树 //用一个map来存储元素在中序遍历中的位置, Map<Integer,Integer> map = new HashMap<>(); for(int i = 0; i < inorder.length; i++){ map.put(inorder[i], i); } return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map); } //传入后序和中序,传入后序的左右边界,中序的左右边界; private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){ if (postleft > postright) return null; //获得整颗树的根节点:后序中的最后个元素; TreeNode root = new TreeNode(postorder[postleft]); //得到此元素在中序中的位置,以此进行划分出左右子树; int rootIndex = map.get(root.value); //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。 int leftTreeSize = rootIndex - inleft; root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map); root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map); return root; }

6.3 层序遍历

除了前中后序外,其实有些题目里都会给出层序遍历的数组,有时候需要能够根据层序遍历来重构出来树;

# 和对二叉树进行层序遍历一样,同样需要借助队列的结构 # 对于每个元素,需要检验其后面两个元素; class TreeNode: def __init__(self, data): self.val = data self.leftchild = None self.rightchild = None def create_tree(self, nodelist): if nodelist[0] = -1: return -1 queue = deque() head = TreeNode(nodelist[0]) queue.append(head) index = 1 #指示列表的位置 while queue: node = queue.popleft() cur_node = nodelist[index] if cur_node == -1 node.leftchild = None else: node.leftchild = TreeNode(cur_node) queue.append(node.leftchild) index += 1 if index == len(nodelist): return head #已经到头了 cur_node = nodelist[index] if cur_node == -1 node.rightchild = None else: node.rightchild = TreeNode(cur_node) queue.append(node.rightchild) index += 1 if index == len(nodelist): return head #已经到头了 return head

附录(全程序)

package xin.utils; import jdk.internal.dynalink.beans.StaticClass; import sun.reflect.generics.tree.VoidDescriptor; import javax.sound.midi.Soundbank; import java.awt.font.TransformAttribute; import java.util.*; public class Tree { } /** * 定义一个二叉树节点; */ class TreeNode<T>{ public T value; //数据; public TreeNode<T> left; //左子树; public TreeNode<T> right;//右子树; public TreeNode(){} //空参的; public TreeNode(T value){ //有参的; this.value = value; } public TreeNode(T value, TreeNode left, TreeNode right){ this.value = value; this.left = left; this.right = right; } } class BinaryTreeTraverse<T> { /** * 前序遍历(递归实现); */ public static void preOrderByRecursion(TreeNode node) { if (node == null) return; //递归终止条件; System.out.println(node.value); //获取根节点的值; preOrderByRecursion(node.left); //左子树的根节点; preOrderByRecursion(node.right);//右子树的根节点; } /** * 前序遍历(非递归实现); * 本质上就是维持递归实现的栈; * 1.先入栈根节点,输出根节点的值,再入栈其右节点,左节点;(为了出栈的时候先出左节点,再出右节点); * 2.出栈左节点,输出值,再入栈左节点的右节点、左节点;直到遍历完左子树; * 3.出栈右节点,输出值,再入栈右节点的右节点、左节点;直到遍历完右子树; * 每次都是出栈一个根节点,如果有孩子,就依次入栈其右节点和左节点。 * 规则: * 压入根节点; * 1.弹出就打印; * 2.如有右孩子,压入右; * 3.如有左孩子,压入左;重复; * (相当于两个孩子替换掉了栈里的根节点,这就很符号:根先走了,左子树干到了最顶部,要是我左子树还有孩子,ok,我也走,两个孩子来替我, * 要是左子树没孩子了,我自己出去,我这里就完事了;再去看右子树就可以了) */ public static void preOrder(TreeNode node) { Stack<TreeNode> stack = new Stack<>(); if (node != null) { stack.push(node); //根节点入栈; } while (!stack.isEmpty()) { TreeNode top = stack.pop(); //弹出就打印; System.out.println(top.value); if (top.right != null) stack.push(top.right); //依次入栈右节点和左节点; if (top.left != null) stack.push(top.left); } } /** * 中序遍历(递归实现) */ public static void inOrderByRecursion(TreeNode node) { if (node == null) return; //递归终止; inOrderByRecursion(node.left); //先左子树; System.out.println(node.value); //再根节点; inOrderByRecursion(node.right); //再右节点; } /** * 中序遍历(非递归实现) * 规则: * 1.整条左边界依次压栈; * 2.条件1执行不了,弹出就打印; * 3.来到弹出节点右树上,继续执行条件1;(右树为空,执行2.弹出打印;右树不为空,执行1;压栈) * 说明:将整个树全用左边界去看,都是先处理了左边界,将左边界分解成了左头,头先入,左再入,然后弄不动了,弹出,然后看其右节点,再把右节点里的左依次进去;就这样往返; */ public static void inOrder(TreeNode node) { Stack<TreeNode> stack = new Stack<>(); while (!stack.isEmpty() || node != null) { if (node != null) { //条件1;能往左走就往左走; stack.push(node); node = node.left; } else { TreeNode top = stack.pop(); //条件2;弹出打印; System.out.println(top.value); node = top.right; //条件3;来弹出节点右树上,继续1; } } } /** * 后序遍历(递归实现) */ public static void postOrderByRecursion(TreeNode node) { if (node == null) return; postOrderByRecursion(node.left); postOrderByRecursion(node.right); System.out.println(node.value); } /** * 后序遍历(非递归实现) * 想一下前序遍历:根左右;过程是先压右孩子,再压左孩子; * 如果我们想实现根右左:那就把前序里的换成先压左,再压右;就处理成了 根右左; * 然后再从后往前看,就变成了右左根;所以可以再准备一个栈,用来把第一个栈弹出的压到第二个,那第二个弹出的时候就倒过来了; * 要记住:栈有实现倒序的功能; */ public static void postOrder(TreeNode node) { Stack<TreeNode> stackA = new Stack<>(); Stack<TreeNode> stackB = new Stack<>(); if (node != null) { stackA.push(node); } while (!stackA.isEmpty()) { TreeNode top = stackA.pop(); stackB.push(top); //栈A弹出的进入栈B;先实现根右左,B倒序实现左右根; if (top.left != null) stackA.push(top.left); if (top.right != null) stackA.push(top.right); } while (!stackB.isEmpty()) { System.out.println(stackB.pop().value); } } /** * Morris遍历; * 二叉树的结构中只有父节点指向孩子节点,孩子节点不能向上指,所以需要栈。 * 而morris遍历的实质就是让下层节点能够指向上层。怎么办呢,一个节点有两个指针,左和右,如果这两个指针上都有指向具体的节点肯定就不行了。 * 但是二叉树上有很多空闲指针,比如所有的叶子节点,它们的指针就指向null,所以可以利用其right指针指向上层的节点。 * 这样连接后,cur这个指针就可以完整的顺着一个链条遍历完整个树。 * 核心:以某个根节点开始,找到它左子树的最右侧节点(必然是个叶子节点),然后利用其right指向根节点(完成向上层的返回)。 * 到达两次的是有左子树的节点; * 到达一次的是没有左子树的节点; */ public static void morris(TreeNode node) { if (node == null) { return; } TreeNode cur = node; TreeNode mostRight = null; //cur左子树的最右节点; while (cur != null) { mostRight = cur.left; if (mostRight != null) { //cur有左子树; //找到左子树的最右节点; while (mostRight.right != null && mostRight.right != cur) { mostRight = mostRight.right; //1.右边为空了证明到头了(找到了);2.右边指向上层了证明之前就处理过了,结束; } //走到这里证明跳出上面循环,无非两个原因: //1.右边没了;2.右边指向上层了(之前就处理过了); if (mostRight.right == null) { //右边走到头了; mostRight.right = cur; //左子树的最右节点指向上层(cur); cur = cur.left; //cur左移,处理下一个节点; continue; //此次循环结束,开始下一个cur; } else { //证明这个mostRight的right指针已经处理过了,即mostRight已经指向了cur; // 能到这里说明我们已经回到了根节点,并且重复了之前的操作;也说明我们已经完全处理完了此根节点左边的的树了,把路断开; mostRight.right = null; } } //cur右移的情况: //1.cur没有左子树了(自然要开始处理右子树) //2.cur有左子树,但是cur左子树最右节点已经指向cur了(执行完上面else断开后,cur左边已经完全处理好了,开始右移。) cur = cur.right; } } /** * 前序遍历(Morris实现); * 1.对于cur只到达一次的节点(没有左子树),cur到达就打印; * 2.对于cur到达两次的节点,到达第一次时打印; */ public static void preOrderMorris(TreeNode node) { if (node == null) { return; } TreeNode cur = node; TreeNode mostRightNode = null; while (cur != null) { mostRightNode = cur.left; if (mostRightNode != null) { //到达两次的节点; //找到cur左子树的最右节点; while (mostRightNode.right != null && mostRightNode.right != cur) { mostRightNode = mostRightNode.right; } if (mostRightNode.right == null) { mostRightNode.right = cur; //指向上层cur; System.out.println(cur.value); //第一次到的时候打印; cur = cur.left; continue; } else { mostRightNode.right = null; //第二次到时不打印; } } else { System.out.println(cur.value); //只到达一次的节点; } cur = cur.right; } } /** * 中序遍历(Morris实现); * 1.对于cur只到达一次的节点(没有左子树),cur到达就打印; * 2.对于cur到达两次的节点,到达第二次时打印; */ public static void inOrderMorris(TreeNode node) { if (node == null) { return; } TreeNode cur = node; TreeNode mostRightNode = null; while (cur != null) { mostRightNode = cur.left; if (mostRightNode != null) { //有左子树,到达两次的节点; while (mostRightNode.right != null && mostRightNode.right != cur) { mostRightNode = mostRightNode.right; } if (mostRightNode == null) { mostRightNode.right = cur; //第一次不打印; cur = cur.left; continue; } else { System.out.println(cur.value); //第二次遇到时打印; mostRightNode.right = null; } } else { System.out.println(cur.value); //只到达一次的节点,遇到就打印; } cur = cur.right; } } /** * 后序遍历(morris实现) * 后序遍历比前面两个要复杂一点; * 将一个节点的连续右节点当做是一个单链表来看待, * 当返回上层后,也就是将建立的连线断开后,打印下层的单链表; * 单链表逆序打印,就和我们做的把单链表逆序一样。 */ public static void postOrderMorris(TreeNode node) { if (cur == null) { return; } TreeNode cur = node; TreeNode mostRightNode = null; while (node != null) { mostRightNode = cur.left; if (mostRightNode != null) { while (mostRightNode.right != null && mostRightNode.right != cur) { mostRightNode = mostRightNode.right; } if (mostRightNode.right == null) { mostRightNode.right = cur; cur = cur.left; continue; } else { //能到这里的都是达到过两次的,也就是是有左孩子的。 mostRightNode.right = null; //这时候是已经返回上层之后,断开了连接,所以打印下层的单链表; postMorrisPrint(cur.left); } } cur = cur.right; } postMorrisPrint(node); //最后把头节点那一串右打印一遍; } public static void postMorrisPrint(TreeNode node) { TreeNode reverseList = postMorrisReverseList(node); //反转链表; TreeNode cur = reverseList; while (cur != null) { System.out.println(cur.value); cur = cur.right; } postMorrisReverseList(reverseList); //最后再还原; } public static TreeNode postMorrisReverseList(TreeNode node) { TreeNode cur = node; TreeNode pre = null; while (cur != null) { TreeNode next = cur.right; cur.right = pre; pre = cur; cur = next; } return pre; } /** * 层次遍历 * 借助队列的结构,每一层依次入队,再依次出队; * 对该层节点进行出队操作时,需要将该节点的左孩子和右孩子入队; */ public static void layerOrder(TreeNode node) { if (node == null) return; Queue<TreeNode> queue = new LinkedList<>(); queue.add(node); while (!queue.isEmpty()) { int size = queue.size(); //当前层的节点数量; for (int i = 0; i < size; i++) { TreeNode front = queue.poll(); System.out.println(front.value); if (front.left != null) queue.add(front.left); if (front.right != null) queue.add(front.right); } } } } class Depth{ /** * 求最大深度(递归) * 最大深度是左子树和右子树的最大深度的大的那个+1; */ public static int maxDepthByRecursion(TreeNode node){ if(node == null) return 0; int leftDepth = maxDepthByRecursion(node.left); int rightDepth = maxDepthByRecursion(node.right); return Math.max(leftDepth,rightDepth)+1; } /** * 求最大深度(非递归) * 层次遍历(BFS) * 每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。 */ public static int maxDepth(TreeNode node){ if (node == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); int level = 0; //层数; queue.add(node); while (!queue.isEmpty()){ level++; int levelNum = queue.size(); //每层的节点数; for (int i = 0; i < levelNum; i++){ TreeNode front = queue.poll(); //当前层出队,下一层入队; if (front.left != null) queue.add(front.left); if (front.right != null) queue.add(front.right); } } return level; } /** * 求最小深度(递归) * 注意和求最大深度的区别; */ public static int minDepthByRecursion(TreeNode node){ if (node == null) return 0; if (node.right == null && node.left == null) return 1; if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1; if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1; return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1; } /** * 求最小深度(非递归) */ public static int minDepth(TreeNode node){ if (node == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); int level = 0; queue.add(node); while (!queue.isEmpty()){ level++; int levelnum = queue.size(); for(int i = 0; i < levelnum; i++){ TreeNode front = queue.poll(); if (front.left == null && front.right == null){ return level; //遇到第一个无叶子节点的时候,该节点的深度为最小深度; } if (front.right != null){ queue.add(front.right); } if (front.left != null){ queue.add(front.left); } } } return level; } } class BuildBinaryTree{ /** * 根据前序遍历和中序遍历构造二叉树; */ public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){ if (preorder == null){ return null; } //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树 //用一个map来存储元素在中序遍历中的位置, Map<Integer,Integer> map = new HashMap<>(); for(int i = 0; i < inorder.length; i++){ map.put(inorder[i], i); } return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map); } //传入前序和中序,传入前序的左右边界,中序的左右边界; private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){ if (preleft > preright) return null; //获得整颗树的根节点:先序中的第一个元素; TreeNode root = new TreeNode(preorder[preleft]); //得到此元素在中序中的位置,以此进行划分出左右子树; int rootIndex = map.get(root.value); //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。 int leftTreeSize = rootIndex - inleft; //左子树的中序:inleft不变,inright为rootIndex-1; //左子树的前序:preleft为根后一位,即preleft+1,preright为根后leftTreeSize位,即preleft+leftTreeSize; root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map); //右子树的中序:inleft为rootIndex+1,inright不变; //右子树的前序:preleft为左子树的右边界+1,即preleft+leftTreeSize+1,preright不变; root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map); return root; } /** * 根据后序遍历和中序遍历构造二叉树 */ public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){ if (postorder == null){ return null; } //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树 //用一个map来存储元素在中序遍历中的位置, Map<Integer,Integer> map = new HashMap<>(); for(int i = 0; i < inorder.length; i++){ map.put(inorder[i], i); } return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map); } //传入后序和中序,传入后序的左右边界,中序的左右边界; private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){ if (postleft > postright) return null; //获得整颗树的根节点:后序中的最后个元素; TreeNode root = new TreeNode(postorder[postleft]); //得到此元素在中序中的位置,以此进行划分出左右子树; int rootIndex = map.get(root.value); //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。 int leftTreeSize = rootIndex - inleft; root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map); root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map); return root; } }

参考链接

史上最全遍历二叉树详解
二叉树总结
算法基地-二叉树


__EOF__

本文作者Curryxin
本文链接https://www.cnblogs.com/Curryxin/p/15063475.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Curryxin  阅读(1791)  评论(4编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 我与微信审核的“相爱相杀”看个人小程序副业
· DeepSeek “源神”启动!「GitHub 热点速览」
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
Live2D
欢迎阅读『【经典结构】二叉树』
点击右上角即可分享
微信分享提示