【经典结构】二叉树
二叉树
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 迭代实现
前序遍历
前序遍历就是我们来手动实现在递归过程中的栈。
想要实现先左再右,那压栈的时候右先入栈,左再入栈。
如下图所示;
规则:
压入根节点;
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.压栈;
说明:将整个树全用左边界去看,都是先处理了左边界,将左边界分解成了左头,头先入,左再入,然后弄不动了,弹出,然后看其右节点,再把右节点里的左依次进去;就这样往返;
如下如所示;
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);
如下图所示:
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;
}
}
后序遍历
后序遍历比前面两个要复杂一点;
将一个节点的连续右节点当成是一个单链表看,如下图所示:
当我们到达最左侧,也就是左边连线已经创建完毕了。
打印 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,如果只有一个孩子,那就只能去递归它的孩子。
/**
* 求最小深度(递归)
* 注意和求最大深度的区别;
*/
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种遍历的特点(如下图):
特点
- 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;
}
}