1、二叉树基础
二叉树是一种非线性数据结构,通过父类和子类的派生关系,体现了一分为二的逻辑,与链表类似,二叉树的基本单元是节点,每个节点包含值,左子节点引用和右子节点引用。
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点引用
TreeNode right; // 右子节点引用
TreeNode(int x) { val = x; }
}
二叉树的常见术语:
1.1 二叉树的基本操作
1、初始化二叉树
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建节点之间的引用(指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
2、插入和删除节点
与链表类似,在二叉树中插入和删除节点可以通过修改指针来实现
1.2 二叉树种类
满二叉树
这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树
前面介绍的树,都没有数值,而二叉搜索树是有数值的,是一个有序树。中序排序是递增的
平衡二叉搜索树
又称为 AVL 树,且具有以下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树。
1.3 二叉树的存储方式
二叉树可以是链式存储,也可以顺序存储。
但是数组存储是不直观的,不采用这种编码。
1.4 二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
-
深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
-
广度优先遍历
- 层次遍历(迭代法)
在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。
这里前中后,其实指的就是中间节点的遍历顺序。
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。
之前我们讲栈与队列的时候,就说过栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
2、递归遍历
前中后遍历都属于深度优先遍历,它体现了一种先走到尽头,再回溯继续的遍历方式。
本篇介绍前后中序的递归写法,每次写递归,都按照按照三要素来写,可以写出正确的递归。
1、确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么,从而确定返回类型。
2、确定终止条件:写完了递归算法,运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
3、确定单层递归的逻辑:确定每一层递归需要处理的信息,在这里也就会重复调用自己来实现递归的过程。
1、确定参数和返回值
举例,确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入 vector 来放节点的数值,返回值就是 void。
2、确定终止条件:在递归的过程中,如何算是递归结束了呢?当然是当前遍历的节点是空了
if(cur == null) return;
3、确定单层递归的逻辑:前序遍历是中左右的顺序,所以,递归逻辑
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
2.1 前序遍历
题目144:leetcode.cn/problems/bi…
public class Number144_1 {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traver(root,list);
return list;
}
private void traver(TreeNode root, List<Integer> list) {
if(root == null){
return;
}
list.add(root.val); // 中
traver(root.left,list); // 左
traver(root.right,list); // 右
}
}
2.2 中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traver(root,list);
return list;
}
private void traver(TreeNode root, List<Integer> list) {
if(root == null){
return;
}
traver(root.left,list);// 左
list.add(root.val);// 中
traver(root.right,list);// 右
}
}
2.3 后序遍历
题目145:leetcode.cn/problems/bi…
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traver(root,list);
return list;
}
private void traver(TreeNode root, List<Integer> list) {
if(root == null){
return;
}
traver(root.left,list);// 左
traver(root.right,list);// 右
list.add(root.val);// 中
}
}
3、迭代遍历
迭代遍历,即采用非递归的方式来实现二叉树的前中后序遍历。
递归的实现就是,每一次递归调用都会把函数的局部变量、参数值、返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
3.1 前序遍历(迭代法)
前序遍历就是中左右
每次想处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入右孩子?因为出栈就是中左右的顺序了。
我们使用栈来实现。
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root == null){
return list;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode pop = stack.pop();
list.add(pop.val);
if(pop.right != null){
stack.push(pop.right);
}
if(pop.left !=null){
stack.push(pop.left);
}
}
return list;
}
3.2 中序遍历(迭代法)
前序遍历访问的是中左右、先访问的是中间节点,处理的元素也是中间节点
但是中序遍历,是左中右,先访问中间节点,然后要先处理树的最左面底部节点,顺序不一致。
public List<Integer> preorderTraversal2(TreeNode root){
ArrayList<Integer> list = new ArrayList<>();
if(root == null){
return list;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur !=null || !stack.isEmpty()){
if(cur !=null){
stack.push(cur);
cur = cur.left;
}else{
// 到底部了
cur = stack.pop();
list.add(cur.val);
cur = cur.right;
}
}
return list;
}
3.3 后序遍历(迭代法)
再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中。
public List<Integer> preorderTraversal3(TreeNode root){
ArrayList<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if(root == null){
return list;
}
stack.push(root);
while(!stack.isEmpty()){
TreeNode pop = stack.pop();
list.add(pop.val);
if(pop.left != null){
stack.push(pop.left);
}
if(pop.right!=null){
stack.push(pop.right);
}
}
Collections.reverse(list);
return list;
}
因为迭代遍历还有一种统一迭代法,但是一刷就不考虑实现了。
4、二叉树的层序遍历
题目102:leetcode.cn/problems/bi…
给你一个二叉树,返回其按层遍历得到的节点指。
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
因此,使用队列实现二叉树广度优先遍历。思想如下:
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> resList = new ArrayList<List<Integer>>();
if(root == null){
return resList;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()){
ArrayList<Integer> itemList = new ArrayList<>();
int len = queue.size();
while (len>0){
TreeNode tmpNode = queue.poll();
itemList.add(tmpNode.val);
if(tmpNode.left!=null){
queue.offer(tmpNode.left);
}
if(tmpNode.right !=null){
queue.offer(tmpNode.right);
}
len--;
}
resList.add(itemList);
}
return resList;
}
总结
今天开始二叉树的基础部分学习,首先搞清楚二叉树的定义,然后三种遍历方式,以及递归排序和迭代排序,还有层次排序,后续有时间再看统一迭代排序方式。而层序遍历是本次的重点,有它就可以斩获很多题目。