二叉树遍历和延伸
之前为了求快,认为递归和动态规划是一类的,导致做了不少递归的题只能想到思路,但就是没法代码实现出来!!!
现在学习二叉树,我觉得对学习递归真的很有帮助!
二叉树的基础:遍历二叉树#
二叉树遍历是基础,如果我们不能遍历二叉树,又如何对二叉树的节点进行操作呢?
二叉树的节点:
/**
* @author keboom
* @date 2021/4/30
*/
public class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
/**
* 10
* 12 15
* 4 7 5 18
* @return
*/
public static Node getTestTree() {
Node node10 = new Node(10);
Node node5 = new Node(5);
Node node15 = new Node(15);
Node node4 = new Node(4);
Node node7 = new Node(7);
Node node12 = new Node(12);
Node node18 = new Node(18);
node10.left = node12;
node10.right = node15;
node12.left = node4;
node12.right = node7;
node15.left = node5;
node15.right = node18;
return node10;
}
}
getTestTree
此方法为了方便测试,不用理会。
递归遍历#
public void preOrderRecur(Node head) {
if (head == null) {
return;
}
System.out.println(head.value + " ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}
public void inOrderRecur(Node head) {
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.println(head.value + " ");
inOrderRecur(head.right);
}
public void posOrderRecur(Node head) {
if (head == null) {
return;
}
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.println(head.value + " ");
}
以上分别为先序,中序和后序遍历。
我认为这三种遍历的区别:
-
先序遍历:先打印,再向下递归
-
中序和后序:先递归到最下面,再打印
只看简单的遍历太简单不直观,请看下面的几道题
接下里的几道题目在求解过程中我会跟二叉树的遍历做一些联系,我认为做这些联系是有助于解题的😄
非递归遍历#
public void preOrderUnRecur(Node head) {
System.out.println("pre-order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
public void inOrderUnRecur(Node head) {
System.out.println("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
/**
* 先将头放入s2,然后将右子树放入s2,最后将左子树放入s2
*/
public void posOrderUnRecur1(Node head) {
System.out.println("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<>();
Stack<Node> s2 = new Stack<>();
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();
s2.push(head);
if (head.left != null) {
s1.push(head.left);
}
if (head.right != null) {
s1.push(head.right);
}
}
while (!s2.isEmpty()) {
System.out.print(s2.pop().value + " ");
}
}
System.out.println();
}
递归的前中后序遍历看起来挺有规律,但是非递归的就不太一样了,我是找不到啥规律。
非递归的前序遍历和中序遍历都模拟了递归调用栈。
打印边缘节点#
题目:#
给定一棵二叉树的头节点 head,按照标准实现二叉树的边界节点的逆时针打印。
1.头节点为边界节点。
2.叶节点为边界节点。
3.如果节点在其所在的层中是最左的或最右的,那么该节点也是边界节点。
打印结果为:1,2,4,7,11,13,14,15,16,12,10,6,3
思路:#
- 获得树高
- 依据树高构建
Node[][] edgeMap = new Node[height][2]
,edgeMap
是存储二叉树每一层的最左和最右的边缘节点 - 有了
edgeMap
后便可以打印左边缘节点 - 打印如14,15这样的非边缘节点,但为叶结点
- 从下向上打印右边缘节点
代码:#
二叉树遍历是基础,以下代码我会跟二叉树的前中后序遍历做一些联系,如果读者觉着这样的联系并不好,那就忽略吧。
获得树高
/**
* 获得二叉树的高度
* 通过不断向下递归查找,找到最大高度
* @param head
* @param height
* @return
*/
private int getHeight(Node head, int height) {
if (head == null) {
return height;
}
return Math.max(getHeight(head.left, height + 1), getHeight(head.right, height + 1));
}
此方法我认为类似后序遍历。向左右递归,得到左右的树高,然后在做比较取大一点的。
符合先左右递归,然后操作。
获得edgeMap
#
此方法将每一层的左右边缘节点存储到edgeMap
。
/**
* edgeMap是一个二维数组,记录树的每一层最左和最右边缘节点。
* @param h
* @param l
* @param edgeMap
*/
private void setEdgeMap(Node h, int l, Node[][] edgeMap) {
if (h == null) {
return;
}
edgeMap[l][0] = edgeMap[l][0] == null ? h : edgeMap[l][0];
edgeMap[l][1] = h;
setEdgeMap(h.left, l + 1, edgeMap);
setEdgeMap(h.right, l + 1, edgeMap);
}
这个一看就符合先序遍历。
先操作edgeMap
,然后递归。
edgeMap[l][0] = edgeMap[l][0] == null ? h : edgeMap[l][0];
edgeMap[l][1] = h;
上面两行代码挺巧妙的,仔细思考先序遍历的过程和规律,发现这两行代码正好就能求的左右边缘。
打印非边缘节点的叶结点#
/**
* 打印不是边缘节点的叶节点
* @param h
* @param l
* @param m
*/
private void printLeafNotInMap(Node h, int l, Node[][] m) {
if (h == null) {
return;
}
// 首先要是叶子结点,并且不是左右边缘
if (h.left == null && h.right == null && h != m[l][0] && h != m[l][1]) {
System.out.print(h.value + " ");
}
printLeafNotInMap(h.left, l + 1, m);
printLeafNotInMap(h.right, l + 1, m);
}
这个也是先序遍历了。
符合先操作,后递归。
先序遍历每个节点,判断其是叶结点但不为边缘节点。
主方法#
/**
* @param head
*/
public void printEdge1(Node head) {
if (head == null) {
return;
}
int height = getHeight(head, 0);
Node[][] edgeMap = new Node[height][2];
setEdgeMap(head, 0, edgeMap);
for (int i = 0; i != edgeMap.length; i++) {
System.out.print(edgeMap[i][0].value + " ");
}
printLeafNotInMap(head, 0, edgeMap);
for (int i = edgeMap.length - 1; i != -1; i--) {
if (edgeMap[i][0] != edgeMap[i][1]) {
System.out.print(edgeMap[i][1].value + " ");
}
}
System.out.println();
}
主方法与思路一致,不多说了。
完整代码:Gitee
序列化二叉树#
题目:#
将一棵二叉树通过先序遍历序列化成字符串。再通过字符串反序列化出二叉树。
序列化结果为字符串:“10!12!4!#!#!7!#!#!15!5!#!#!18!#!#!”
思路:#
以!为分隔符,空指针用#,在先序遍历的过程中构造一个字符串,然后返回。
代码:#
/**
* 通过先序遍历来序列化
* @param head
* @return
*/
public String serialByPre(Node head) {
if (head == null) {
return "#!";
}
String res = head.value + "!";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}
看看那!!!多么像先序遍历打印二叉树的代码啊!!!
以下为反序列化:
/**
* 先序遍历反序列化
* @param preStr
* @return
*/
public Node reconByPreString(String preStr) {
String[] values = preStr.split("!");
LinkedList<String> queue = new LinkedList<>();
for (int i = 0; i < values.length; i++) {
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
/**
* 正好模拟了先序遍历的过程
* @param queue
* @return
*/
private Node reconPreOrder(Queue<String> queue) {
String value = queue.poll();
if (value.equals("#")) {
return null;
}
Node head = new Node(Integer.valueOf(value));
head.left = reconPreOrder(queue);
head.right = reconPreOrder(queue);
return head;
}
地址:gitee
打印二叉树#
题目:#
打印结果:
"H"表示头结点,"v"表示此节点为父节点的右节点,"^"表示为父节点的左节点。
思路:#
先打印右子树,再打印左子树。这样的打印结果顺时针旋转90度就是二叉树直观形状了。
上图的打印结果,第一行为最右节点,第二行为最右节点的父节点,第三行为左兄弟节点,如此规律那就是一个中序遍历啊!!
代码:#
printTree
方法为主函数,printInOrder
会以中序遍历来打印节点,只不过先打印的是右子树,len
为一个节点所占的字符串长度。getSpace
用来凑空字符串。
public void printTree(Node head) {
System.out.println("Binary Tree: ");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
private String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
这就是中序遍历,只不过中间我们需要记录当前遍历到树的当前高度等信息。
地址:gitee
判断 t1 树是否包含 t2 树全部的拓扑结构#
题目:#
第一棵树包含第二棵树全部的拓扑结构,返回true
思路:#
- 我们需要先找到节点2
- 然后从节点2开始比较所有节点是否相等
代码:#
contains
方法让我们先遍历t1的节点,先找到我们上面所说的“节点2”。check
方法则判断t2的每个节点值是否与t1对应。
public boolean contains(Node t1, Node t2) {
if (t2 == null) {
return true;
}
if (t1 == null) {
return false;
}
// 因为t1不是一开头就跟t2匹配,所以也需要contains递归去找
// 只要找到t1的子节点与t2相等的,则进行check递归,由于或运算只要有一个true,则结果为true
return check(t1, t2) || contains(t1.left, t2) || contains(t1.right, t2);
}
private boolean check(Node h, Node t2) {
if (t2 == null) {
return true;
}
// 此还可判断t1子节点是否与t2头结点相等呢!
if (h == null || h.value != t2.value) {
return false;
}
return check(h.left, t2.left) && check(h.right, t2.right);
}
上面的就跟正常的递归遍历形式上不太一样,但是核心思想还是在通过左右递归遍历二叉树。
地址:gitee
以后还有很多二叉树题目都是在二叉树递归的基础上加大难度,比如在递归过程中需要记录高度或者其他信息,这是对于递归的参数而言。对于返回值来说,打印类的题目自然就是void,判断类的题目自然就是boolean,构造二叉树自然就是Node,返回二叉树高度自然就是int,这是对于递归的返回值来说。对于终止条件最常见的就是head == null
,当然了要视情况而定。然后就是左递归,右递归,一坨操作。如果我们需要立即进行操作,就是说从传进来一个头结点我们就操作他,然后在递归左节点和右节点,那么就类似先序遍历。如果我们需要先递归到二叉树的最左下,然后在操作之,那么就需要用中序或者后序遍历了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库