1. 什么是二叉树?
二叉树和链表的形式类似,都分为两个部分,即值value和指向下一个节点的next,只不过二叉树区别于链表,有两个指向:left和right。具体区别见下图:
代码上的区别如下:
(1)链表
public class ListNode{
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val = val;
}
ListNode (int val, ListNode next) {
this.val = val;
this.next = next;
}
}
(2)二叉树
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
2. 二叉树的遍历方式
二叉树的遍历分为两种,一种是深度优先,一种是广度优先,其中深度优先就是我们常说的前序遍历、中序遍历以及后序遍历。而广度优先,顾名思义就是一层一层的遍历二叉树。
下面举例分别展示一下各种遍历的差别:
以这棵树为例:
首先,前序遍历的顺序为:中左右,就是先把中间节点记录下来,再去按照这个顺序遍历它的左子树,左子树遍历完再去遍历它的右子树。最终顺序为:1245367
其次,中序遍历的顺序为:左中右,就是遍历左子树在记录中见节点,再遍历右子树。最终顺序为:4251637
然后,后序遍历的顺序为:左右中,就是先遍历两棵子树,再加上中间节点。最终顺序为:4526731
最后,层序遍历,顾名思义就是一层一层的遍历,遍历的顺序为:1234567
3. 练手题目
下面我们就挑几道题来练练手:
3.1 二叉树的前序遍历
方法一:递归
首先是递归的方法,前、中、后序遍历其实比较类似。首先,我们在做题之前要明确一点,就是要把收集结果的那一个步骤作为“中”,向左遍历作为“左”,向右遍历作为“右”。于是有,前序遍历就是:中左右,即收集结果->向左遍历->向右遍历。
看下面递归代码(reverse方法),首先递归的流程,要有退出条件,就是我们遍历到叶子节点的后面了,已经遍历到null,就直接退出这层。如果不为null,说明它有值,那就收集起来,这就是“中”,中完事了再向左遍历,就是收集这个节点的左子树,这是“左”,然后收集这个节点的右子树,就是“右”。
于是,这道题解决了,中序和后序也就解决了。
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
reverse(res, root);
return res;
}
public void reverse(List<Integer> res, TreeNode cur) {
if (cur == null) {
return;
}
res.add(cur.val);
reverse(res, cur.left);
reverse(res, cur.right);
}
方法二:迭代
迭代法也可以遍历树,要用到我们之前学过的栈。给大家画图来解释:
还是以这棵树为例, 先把1压入栈,然后按照上面的逻辑,“中”->收集结果,“左”->遍历左子树,怎么遍历呢?把1弹出来,把1的左子树的头压入栈。
但是,这样的话当我们把左边遍历完了的时候,怎么遍历右边呢?哦,因为之前把头节点弹掉了,所以找不到右子树了。所以为了解决这个问题,在遍历左子树的时候,先把当前节点的右节点压入栈中,等左子树遍历完了,栈中还有一个右子树的头节点,然后再把它弹出来,遍历右子树。
看看这个逻辑,是不是也符合左右中呢?
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
stack.push(cur);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
res.add(pop.val);
if (pop.right != null) {
stack.push(pop.right);
}
if (pop.left != null) {
stack.push(pop.left);
}
}
return res;
}
3.2 二叉树的后序遍历
方法一:递归
与前序类似。
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
reverse(res, root);
return res;
}
public void reverse(List<Integer> res, TreeNode cur) {
if (cur == null) {
return;
}
reverse(res, cur.left);
reverse(res, cur.right);
res.add(cur.val);
}
方法二:迭代
与前序类似,但是这个可以动一个小脑筋。就是我们先按“中右左”的顺序压入栈,然后将收集到的结果翻转,是不是就是“左右中”->后序遍历了。
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
stack.push(cur);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
res.add(pop.val);
if (pop.left != null) {
stack.push(pop.left);
}
if (pop.right != null) {
stack.push(pop.right);
}
}
reverseRes(res);
return res;
}
private void reverseRes(List<Integer> result) {
for (int i = 0; i < result.size() / 2; i++) {
Integer temp = result.get(i);
result.set(i, result.get(result.size() - i - 1));
result.set(result.size() - i - 1, temp);
}
}
3.3 二叉树的中序遍历
方法一:递归
与前序、后序类似。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
reverse(res, root);
return res;
}
public void reverse(List<Integer> res, TreeNode cur) {
if (cur == null) {
return;
}
reverse(res, cur.left);
res.add(cur.val);
reverse(res, cur.right);
}
方法二:迭代
中序的迭代遍历和前序后序还是有区别的。中序的遍历顺序是:左中右,那我们要怎么压栈、弹栈呢?首先,中序遍历的第一个元素是最左边的元素,所以只要左边不为空,就得一直向左遍历,于是就有了,如果每次向左遍历前,都把当前节点先压入栈,直到为null,说明当前节点的左边已经没有节点了,然后按照左中右的逻辑把中收集起来,再遍历右节点,如果右节点不为bull就继续按照这个逻辑遍历下去,如果为null,说明到头了,怎么办?再次进入下一轮循环,把栈里面存的节点弹出来,存起来,看看他的右边是不是null,如此循环。
图解:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Deque<TreeNode> deque = new LinkedList<>();
while (root != null || !deque.isEmpty()) {
if (root != null) {
deque.push(root);
root = root.left;
} else {
TreeNode node = deque.pop();
res.add(node.val);
root = node.right;
}
}
return res;
}