1、什么是二叉树(Binary Tree)
关于树,有几个比较常用的概念你需要掌握,那就是:根节点、叶子节点、父节点、子节点、兄弟节点,还有节点的高度、深度、层数,以及树的高度。
二叉树,顾名思义,每个节点最多有两个子节点,分别是左子节点和右子节点。不过,一个二叉树中并不要求每个节点都必须有两个节点,有的节点只有左子节点,有的节点只有右子节点,还有的节点左右子节点都为空。如下图所示
上图中有两个比较特殊的二叉树,分别是编号 2 和编号 3 这两个。
其中编号 2 的二叉树中的叶子节点全都在最低层,除了叶子节点之外,每个节点都有左右两个子结点,这种二叉树叫做满二叉树
编号 3 的二叉树中,叶子节点都在最低下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其它层的节点个数都达到了最大,这种二叉树叫做完全二叉树
2、如何存储一棵二叉树
想要存储一棵二叉树,通常两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
2.1、链式存储法
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子结点的指针。我们只要通过根节点,就可以通过左右子结点的指针,把整棵树都串起来,如图所示。大部分二叉树都是通过这种结构来实现的。
2.2、基于数组的顺序存储法
用数组的顺序存储法实现二叉树,当前节点的下标为 i,左子节点的下标为i * 2,右子节点的下标为2 * i + 1,这里i从 1 开始,也就是根节点的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置)。如图所示,通过这种方式,我们只要知道根节点存储的位置,这样就可以通过下标计算,把整棵二叉树穿起来了。
上图的二叉树是一颗完全二叉树,所以仅仅浪费了一个下标为 0 的存储位置。如果是非完全二叉树,会浪费比较多的数组存储空间。如图所示
当二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式,因为数组的存储方式并不需要像链式存储法那样,需要存储额外的左右子节点的指针。这里我们通常会想到堆,堆的底层实现就是一棵完全二叉树,最常用的存储方式就是数组。
3、二叉树的遍历
如何将所有节点都遍历打印出来呢?经典的方法有三种,前序遍历、中序遍历和后序遍历。其中前、中、后序,表示的是当前节点与它左右子树节点遍历打印的先后顺序。
- 前序遍历,对于树中的任意节点,先打印当前节点,然后打印它的左子树,最后打印它的右子树
- 中序遍历,对于树中的任意节点,先打印它的左子树,然后打印当前节点,最后打印它的右子树
- 后序遍历,对于树中的任意节点,先打印它的左子树,然后打印它的右子树,最后打印当前节点
3.1、递归实现
/**
* @description: 二叉树遍历 递归
* @author: erlang
* @since: 2020-11-25 22:54
*/
public class RecursiveTraversalBT {
/**
* 前序遍历
*
* @param root 待遍历二叉树的根节点
*/
public static void pre(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.value + " ");
pre(root.left);
pre(root.right);
}
/**
* 中序遍历
*
* @param root 待遍历二叉树的根节点
*/
public static void in(TreeNode root) {
if (root == null) {
return;
}
in(root.left);
System.out.print(root.value + " ");
in(root.right);
}
/**
* 后序遍历
*
* @param root 待遍历二叉树的根节点
*/
public static void post(TreeNode root) {
if (root == null) {
return;
}
post(root.left);
post(root.right);
System.out.print(root.value + " ");
}
public static void main(String[] args) {
TreeNode head = new TreeNode(1);
head.left = new TreeNode(2);
head.right = new TreeNode(3);
head.left.left = new TreeNode(4);
head.left.right = new TreeNode(5);
head.right.left = new TreeNode(6);
head.right.right = new TreeNode(7);
System.out.print("pre-order: ");
pre(head);
System.out.println("\n========");
System.out.print("in-order: ");
in(head);
System.out.println("\n========");
System.out.print("post-order: ");
post(head);
System.out.println("\n========");
}
}
3.2、栈实现
import java.util.Stack;
/**
* @description: 二叉树遍历 非递归
* @author: erlang
* @since: 2020-11-25 22:54
*/
public class UnRecursiveTraversalBT {
/**
* 前序遍历
* 1、弹栈,打印
* 2、如有右,压入右
* 3、如有左,压入左
*
* @param root 待遍历二叉树的根节点
*/
public static void pre(TreeNode root) {
System.out.print("pre-order: ");
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
// 先压入根节点
stack.add(root);
while (!stack.isEmpty()) {
root = stack.pop();
// 1、弹栈,打印
System.out.print(root.value + " ");
// 2、如有右,压入右
if (root.right != null) {
stack.push(root.right);
}
// 3、如有左,压入左
if (root.left != null) {
stack.push(root.left);
}
}
}
/**
* 中序遍历
* 1、整条左边界依次入栈
* 2、不满足 1 时,弹出节点并打印
* 3、到右子树上继续执行 1
* @param root 待遍历二叉树的根节点
*/
public static void in(TreeNode root) {
System.out.print("in-order: ");
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || root != null) {
if (root != null) {
// 1、整条左子树依次入栈
stack.push(root);
root = root.left;
} else {
// 2、不满足 1 时,弹出节点并打印
root = stack.pop();
System.out.print(root.value + " ");
// 3、到右子树上继续执行 1
root = root.right;
}
}
}
/**
* 后序遍历
* 1、弹栈,收集到 res 栈中,
* 2、如有左,压入左
* 3、如有右,压入右
* 4、最后打印 res
*
* @param root 待遍历二叉树的根节点
*/
public static void post1(TreeNode root) {
System.out.print("post-order1: ");
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> res = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
root = stack.pop();
res.push(root);
if (root.left != null) {
stack.push(root.left);
}
if (root.right != null) {
stack.push(root.right);
}
}
while (!res.isEmpty()) {
System.out.print(res.pop().value + " ");
}
}
/**
* 后序遍历
*
* @param root 待遍历二叉树的根节点
*/
public static void post2(TreeNode root) {
System.out.print("post-order2: ");
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.peek();
if (cur.left != null && root != cur.left && root != cur.right) {
// 左树未处理的时候,处理左树
// 当前节点的左子树不为空,且上次打印的节点不是右子树也不是左子树
// 则说明当前节点是第一次访问
// 即当前节点未打印过
stack.push(cur.left);
} else if (cur.right != null && root != cur.right) {
// 右树未处理的时候,处理右树
// 当前节点的右子树不为空,且上次打印的节点不是右子树
// 则说明当前节点是第一次访问
// 即当前节点未打印过
stack.push(cur.right);
} else {
// 当前节点的左右子树都被访问过后,打印当前节点
System.out.print(stack.pop().value + " ");
// 记录上次打印的节点
root = cur;
}
}
}
public static void main(String[] args) {
TreeNode head = new TreeNode(1);
head.left = new TreeNode(2);
head.right = new TreeNode(3);
head.left.left = new TreeNode(4);
head.left.right = new TreeNode(5);
head.right.left = new TreeNode(6);
head.right.right = new TreeNode(7);
pre(head);
System.out.println("\n========");
in(head);
System.out.println("\n========");
post1(head);
System.out.println("\n========");
post2(head);
System.out.println("\n========");
}
}
3.3、二叉树实现
/**
* @description: 二叉树
* @author: erlang
* @since: 2020-11-25 22:56
*/
public class TreeNode {
public int value;
public TreeNode left;
public TreeNode right;
public TreeNode(int v) {
value = v;
}
}
4、小结
二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。
二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。除此之外,二叉树里非常重要的操作就是前、中、后序遍历操作,遍历的时间复杂度是 O(n)。