跟左神学算法6 进阶数据结构(二叉树)
内容:
1、二叉树基础结构及常用函数
2、三种二叉树
3、二叉树的后继节点与前继节点
4、二叉树的序列化和反序列化
5、折纸问题
6、求树的节点
1、二叉树基础结构及常用函数
二叉树的基本结构:
1 public static class Node { 2 public int value; 3 public Node left; 4 public Node right; 5 6 public Node(int data) { 7 this.value = data; 8 } 9 }
二叉树常用函数:
遍历二叉树:
- 递归先序遍历、递归中序遍历、递归后序遍历
- 非递归先序遍历、非递归中序遍历、非递归后序遍历
- 非递归按层遍历
代码:
1 // 递归先序遍历 2 public static void preOrderRecur(Node head){ 3 if(head == null){ 4 return; 5 } 6 System.out.print(head.value + " "); 7 preOrderRecur(head.left); 8 preOrderRecur(head.right); 9 } 10 11 // 递归中序遍历 12 public static void inOrderRecur(Node head){ 13 if(head == null){ 14 return; 15 } 16 inOrderRecur(head.left); 17 System.out.print(head.value + " "); 18 inOrderRecur(head.right); 19 } 20 21 // 递归后序遍历 22 public static void posOrderRecur(Node head){ 23 if(head == null){ 24 return; 25 } 26 posOrderRecur(head.left); 27 posOrderRecur(head.right); 28 System.out.print(head.value + " "); 29 } 30 31 // 非递归先序遍历 32 public static void preOrderUnRecur(Node head){ 33 System.out.print("pre-order: "); 34 if(head != null){ 35 Stack<Node> stack = new Stack<Node>(); 36 stack.add(head); 37 while(!stack.isEmpty()){ 38 head = stack.pop(); 39 System.out.print(head.value + " "); 40 if(head.right != null){ 41 stack.push(head.right); 42 } 43 if(head.left != null){ 44 stack.push(head.left); 45 } 46 } 47 } 48 } 49 50 // 非递归中序遍历 51 public static void inOrderUnRecur(Node head){ 52 System.out.print("in-order: "); 53 if(head != null){ 54 Stack<Node> stack = new Stack<Node>(); 55 while(!stack.isEmpty() || head != null){ 56 if(head != null){ 57 stack.push(head); 58 head = head.left; 59 } else{ 60 head = stack.pop(); 61 System.out.print(head.value + " "); 62 head = head.right; 63 } 64 } 65 } 66 } 67 68 // 非递归后序遍历 69 public static void posOrderUnRecur(Node head){ 70 System.out.print("pos-order: "); 71 if(head != null){ 72 Stack<Node> s1 = new Stack<Node>(); 73 Stack<Node> s2 = new Stack<Node>(); // 辅助栈 74 s1.push(head); 75 while(!s1.isEmpty()){ 76 head = s1.pop(); 77 s2.push(head); 78 if(head.left != null){ 79 s1.push(head.left); 80 } 81 if(head.right != null){ 82 s1.push(head.right); 83 } 84 } 85 86 while(!s2.isEmpty()){ 87 head = s2.pop(); 88 System.out.print(head.value + " "); 89 } 90 } 91 } 92 93 // 非递归按层遍历 -> 队列实现 94 public static void levelOrderUnRecur(Node head) { 95 System.out.print("level-order: "); 96 if (head != null) { 97 Queue<Node> que = new LinkedList<Node>(); 98 que.offer(head); 99 while (!que.isEmpty()) { 100 head = que.poll(); 101 System.out.print(head.value + " "); 102 if (head.left != null) { 103 que.offer(head.left); 104 } 105 if (head.right != null) { 106 que.offer(head.right); 107 } 108 } 109 } 110 System.out.println(); 111 }
打印二叉树:
下面是一个打印二叉树的福利函数,使用方法: 打印节点标志为H表示头节点 打印节点标志为v表示为右子树 打印节点标志为^表示为左子树
1 // 打印二叉树的入口函数 2 public static void printTree(Node head) { 3 System.out.println("Binary Tree:"); 4 printInOrder(head, 0, "H", 17); 5 System.out.println(); 6 } 7 8 // 打印二叉树的主逻辑函数 9 public static void printInOrder(Node head, int height, String to, int len) { 10 /* 11 * head 头节点 12 * height 当前高度 13 * to 代表是左子树还是右子树 => 为H时代表根节点 14 * len 代表每个节点最多占的宽度 15 * */ 16 if (head == null) { 17 return; 18 } 19 20 // 继续打印右子树 21 printInOrder(head.right, height + 1, "v", len); 22 23 String val = to + head.value + to; 24 int lenM = val.length(); // 节点(加上节点标志)的长度 25 int lenL = (len - lenM) / 2; // 节点左边的长度 26 int lenR = len - lenM - lenL; // 节点右边的长度 27 val = getSpace(lenL) + val + getSpace(lenR); // 为节点加上左右两边的空格 28 System.out.println(getSpace(height * len) + val); // 打印当前节点 29 30 // 继续打印左子树 31 printInOrder(head.left, height + 1, "^", len); 32 } 33 34 // 为节点加上左右两边的空格 35 public static String getSpace(int num) { 36 String space = " "; 37 StringBuffer buf = new StringBuffer(""); 38 for (int i = 0; i < num; i++) { 39 buf.append(space); 40 } 41 return buf.toString(); 42 }
2、三种二叉树
- 平衡二叉树:空树 或 二叉树的每个左右子树的高度差的绝对值不超过1
- 搜索二叉树:空树 或 左子树上所有结点的值均小于它的根结点的值以及右子树上所有结点的值均大于它的根结点的值
- 完全二叉树:空树 或 对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树
判断是否是平衡二叉树:
1 // 返回数据 2 public static class ReturnData{ 3 public boolean isB; 4 public int h; 5 6 public ReturnData(boolean isB, int h){ 7 this.isB = isB; 8 this.h = h; 9 } 10 } 11 12 // 判断一颗二叉树是否是平衡二叉树 13 public static boolean isB(Node head){ 14 return process(head).isB; 15 } 16 17 // 递归求解返回数据过程 => 列举多种可能性 18 // 不是平衡二叉树的情况: 1、左子树不是平衡二叉树 2、右子树不是平衡二叉树 3、左子树和右子树的高度差大于1 19 // 其他情况均是平衡二叉树: 空二叉树(高度为0)、不是以上三种情况的非空二叉树(高度为左右子树最大高度+1) 20 public static ReturnData process(Node head){ 21 if(head==null){ 22 return new ReturnData(true, 0); 23 } 24 ReturnData leftData = process(head.left); 25 if(!leftData.isB){ 26 return new ReturnData(false, 0); 27 } 28 ReturnData rightData = process(head.right); 29 if(!rightData.isB){ 30 return new ReturnData(false, 0); 31 } 32 if(Math.abs(leftData.h - rightData.h) > 1){ 33 return new ReturnData(false, 0); 34 } 35 return new ReturnData(true, Math.max(leftData.h, rightData.h) + 1); 36 }
判断是否是搜索二叉树:
1 // 借助非递归中序遍历实现(最简单的方法) -> 后一个节点必须大于前一个节点 2 public static boolean isBST(Node head) { 3 int pre = Integer.MIN_VALUE; 4 if (head != null) { 5 Stack<Node> stack = new Stack<Node>(); 6 while(!stack.isEmpty() || head!=null){ 7 if(head != null){ 8 stack.push(head); 9 head = head.left; 10 } else{ 11 head = stack.pop(); 12 if(head.value > pre){ 13 pre = head.value; 14 } else{ 15 return false; 16 } 17 head = head.right; 18 } 19 } 20 } 21 22 return true; 23 } 24 25 // 借助递归中序遍历实现 判断一棵二叉树是否是搜索二叉树 26 private static int pre = Integer.MIN_VALUE; 27 private static boolean flag; 28 public static void isBST2(Node head) { 29 if (head == null) { 30 return; 31 } 32 isBST2(head.left); 33 // 将当前节点和之前的数比较 如果大于说明满足搜索二叉树条件 否则不满足 34 if (head.value > pre) { 35 pre = head.value; 36 } else { 37 flag = false; 38 return; 39 } 40 isBST2(head.right); 41 }
判断是否是完全二叉树:
1 // 按层遍历 两种情况: 2 // 1、有右节点无左节点 直接返回false 3 // 2、有左节点无右节点或者左右节点都没有 那么如果接下来的节点都是叶节点就直接返回true 否则返回false 4 5 public static boolean isCBT(Node head) { 6 if (head == null) { 7 return true; 8 } 9 Queue<Node> queue = new LinkedList<Node>(); 10 boolean leaf = false; // 标志情况2是否发生 11 Node l = null; 12 Node r = null; 13 queue.offer(head); 14 while (!queue.isEmpty()) { 15 head = queue.poll(); 16 l = head.left; 17 r = head.right; 18 if (l == null && r != null) { 19 // 情况1 左空右不空 20 return false; 21 } 22 if (leaf && (l != null || r != null)) { 23 // 情况2 24 return false; 25 } 26 if (l != null) { 27 queue.offer(l); 28 } 29 if (r != null) { 30 queue.offer(r); 31 } else { 32 // 开启情况2 33 // 实质上就是左右均为空或左不空右空的情况 34 leaf = true; 35 } 36 } 37 return true; 38 }
3、二叉树的后继节点与前继节点
题目描述:
现在有一种新的二叉树节点类型如下:
public class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data)
{
this.value = data;
}
}
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一棵Node类型的节点组成的
二叉树,树中每个节点的parent指针都正确地指向自己的父节点,头节点的parent指向null
只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中,node的下一个节点叫作node的后继节点
另外前继节点就是中序遍历中当前节点的前一个节点,方法和找后继节点类似
思路:
根据这个节点有无右子树来讨论:
- 如果节点x有右子树 那么x的后继节点一定是它右子树中最左的节点
- 如果节点x没有右子树 通过parent指针一直向上找父节点 如果当前节点是父节点的左孩子那么节点x的后继节点是当前节点的父节点
- 另外也要注意最后一个节点的问题(通过判断父节点为空解决)
代码(后继节点):
1 // 找到节点的后继节点 2 public static Node getSuccessorNode(Node node) { 3 if (node == null) { 4 return node; 5 } 6 if (node.right != null) { 7 // 节点有右子树 8 // 找到右子树上最左的节点 9 return getLeftMost(node.right); 10 } else { 11 // 节点没有右子树 12 Node parent = node.parent; 13 while (parent != null && parent.left != node) { 14 node = parent; 15 parent = node.parent; 16 } 17 return parent; 18 } 19 } 20 21 // 找到右子树上最左的节点 22 public static Node getLeftMost(Node node) { 23 while (node.left != null) { 24 node = node.left; 25 } 26 27 return node; 28 }
代码(前继节点):
1 // 找到节点的前继节点 2 public static Node getPredecessorNode(Node head) { 3 if (head == null) { 4 return null; 5 } 6 7 if (head.left != null) { 8 return getRightMost(head.left); 9 } else { 10 Node parent = head.parent; 11 while(parent!=null&&parent.right!=head){ 12 head = parent; 13 parent = head.parent; 14 } 15 16 return parent; 17 } 18 } 19 20 // 获得左子树中最右的节点 21 public static Node getRightMost(Node head) { 22 while (head.right != null) { 23 head = head.right; 24 } 25 26 return head; 27 }
4、二叉树的序列化和反序列化
二叉树的序列化: 二叉树的序列化是指把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,
从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、按层的遍历方式进行
序列化的结果是一个字符串,通过#表示空节点, 以!表示一个结点值的结束, 节点之间用下划线分割
二叉树的反序列化: 根据某种遍历顺序得到的序列化字符串结果str,重构二叉树
代码:
1 // 下面是以前序遍历和按层遍历实现的序列化和反序列化 2 // 以先序遍历的方式序列化二叉树 3 public static String serialByPre(Node head) { 4 if (head == null) { 5 return "#_"; 6 } 7 String res = head.value + "_"; 8 res += serialByPre(head.left); 9 res += serialByPre(head.right); 10 return res; 11 } 12 13 // 以按层遍历的方式序列化 14 public static String serialByLevel(Node head) { 15 if (head == null) { 16 return "#_"; 17 } 18 String res = head.value + "_"; 19 Queue<Node> queue = new LinkedList<Node>(); 20 queue.offer(head); 21 while (!queue.isEmpty()) { 22 head = queue.poll(); 23 // 处理左边 24 if (head.left != null) { 25 res += head.left.value + "_"; 26 queue.offer(head.left); 27 } else { 28 res += "#_"; 29 } 30 // 处理右边 31 if (head.right != null) { 32 res += head.right.value + "_"; 33 queue.offer(head.right); 34 } else { 35 res += "#_"; 36 } 37 } 38 return res; 39 } 40 41 // 实现先序遍历反序列化二叉树 42 public static Node reconPreOrder(Queue<String> queue) { 43 String value = queue.poll(); 44 if (value.equals("#")) { 45 return null; 46 } 47 Node head = new Node(Integer.valueOf(value)); 48 head.left = reconPreOrder(queue); 49 head.right = reconPreOrder(queue); 50 return head; 51 } 52 53 // 将字符串分割转换成数组然后存入队列中然后调用反序列化函数 54 public static Node reconByString(String str) { 55 String[] values = str.split("_"); 56 // 保存分割出来的节点值 57 Queue<String> queue = new LinkedList<String>(); 58 for (int i = 0; i != values.length; i++) { 59 queue.offer(values[i]); 60 } 61 62 return reconPreOrder(queue); 63 } 64 65 // 根据节点值生成节点 66 public static Node generateNodeByString(String val) { 67 if (val.equals("#")) { 68 return null; 69 } 70 return new Node(Integer.valueOf(val)); 71 } 72 73 // 实现按层遍历反序列化二叉树 74 public static Node reconByLevelString(String levelStr) { 75 String[] values = levelStr.split("_"); 76 int index = 0; 77 Node head = generateNodeByString(values[index++]); 78 Queue<Node> queue = new LinkedList<Node>(); 79 if (head != null) { 80 queue.offer(head); 81 } 82 Node node = null; 83 while (!queue.isEmpty()) { 84 node = queue.poll(); 85 node.left = generateNodeByString(values[index++]); 86 node.right = generateNodeByString(values[index++]); 87 if (node.left != null) { 88 queue.offer(node.left); 89 } 90 if (node.right != null) { 91 queue.offer(node.right); 92 } 93 } 94 return head; 95 }
5、折纸问题
题目描述:
请把一段纸条竖着放在桌子上,然后从纸条的下边向 上方对折1次,压出折痕后展开。
此时 折痕是凹下去的,即折痕 突起的方向指向纸条的背面。如果从纸条的下边向上方连续对2
次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。 给定一个
输入参数N,代表纸条都从下边向上方连续对折N次, 请从上到下打印所有折痕的方向。
例如:N=1时,打印: down N=2时,打印: down down up
思路:
其实对于纸折的次数与树有关系,也就是说可以使用树中的便利方法来求解:
对折次数:
- 1 down
- 2 down down up
- 3 down down up down down up up
- 4 down down up down down up up down down down up up down up up
题目中要求是从上到下的折痕的数组,那么就可以使用二叉树的中序遍历序列来列出所有的从上到下的数组
代码:
1 public static void printProcess(int i, int N, boolean down) { 2 if (i > N) { 3 return; 4 } 5 // 实质上就是中序遍历二叉树 6 printProcess(i + 1, N, true); 7 System.out.print(down ? "down " : "up "); 8 printProcess(i + 1, N, false); 9 } 10 11 // 打印所有折痕(由上到下) 12 public static void printAllFolds(int N) { 13 printProcess(1, N, true); 14 } 15 16 public static void main(String[] args) { 17 int N = 4; 18 printAllFolds(N); 19 System.out.println(); 20 }
6、求树的节点
题目描述:
已知一棵完全二叉树,求其节点的个数
要求: 时间复杂度低于O(N),N为这棵树的节点个数
思路:
采用二分的思想。看完全二叉树的最后的最右一个节点的位置
1、找到完全二叉树的最左节点,也就是求左子树的深度
2、找到完全二叉树头节点右子树中的最左节点,记录右子树深度
3、如果两个深度相等,说明头节点左子树是一棵满二叉树,使用公式求得左子树长度再加上头节点,然后对于右子树使用递归求解
4、如果左子树深度大于右子树深度,说明右子树是一棵完全二叉树,使用公式求得右子树长度再加上头节点,然后对于左子树使用递归求解
代码:
1 // 主逻辑函数 2 public static int nodeNum(Node head) { 3 if (head == null) { 4 return 0; 5 } 6 return bs(head, 1, mostLeftLevel(head, 1)); 7 } 8 9 // 递归过程 10 public static int bs(Node node, int level, int h) { 11 // node: 当前节点 level: 当前节点位于第几层 h: 树的层数 12 if (level == h) { 13 return 1; 14 } 15 if (mostLeftLevel(node.right, level + 1) == h) { 16 // 当前节点的右子树可以到最后一层 17 // 此时左子树满 18 return (1 << (h - level)) + bs(node.right, level + 1, h); 19 } else { 20 // 当前节点的右子树不可以到最后一层 21 // 此时右子树满 22 return (1 << (h - level - 1)) + bs(node.left, level + 1, h); 23 } 24 } 25 26 // 返回当前节点的最左子节点位于的层数 27 public static int mostLeftLevel(Node node, int level) { 28 while (node != null) { 29 level++; 30 node = node.left; 31 } 32 return level - 1; 33 }