数据结构与算法:二叉树篇(一)
导言
本篇文章主要是为了介绍二叉树的概念和相关的常用解法(DFS,BFS),写这篇文章的主要原因是在面试过程中遇到了很多有关二叉树的问题,借此机会也是对我自身的一个知识的巩固。
什么是二叉树
其实所谓二叉树,是形如以下的数据结构:
即,通过一个节点可以访问到其他的两个节点。其中,一号节点被称之为根节点,二三号节点被称之为一号节点的叶子节点。其表达方式也有很多种,这里我们通过Java来定义二叉树的结构:
class TreeNode {
//当前二叉树节点的值
int val;
//当前二叉树节点的左节点
TreeNode left;
//当前二叉树节点的右节点
TreeNode right;
}
这就是一个最简单的二叉树节点的结构;其实按照我的理解来说,二叉树节点与链表的区别就是其多了一个指针域可以指向别的节点,在很多时候二叉树和链表有许多相通之处。
深度优先(DFS)和广度优先(BFS)
深度优先算法
说到这类结构的数据结构(树和图),我们就不得不提到两种思想了:深度优先,广度优先。我们先来讲深度优先算法,所谓深度优先,英文叫做Deepth First
(DFS),就是先尽可能往二叉树的更深层次去探索,探索不到再进行回溯。有一种不撞南墙不回头
的意思。
前置概念
这里又出现了一个新的概念深度
,我们可以结合下面这张图来理解:
我们以二叉树的根节点为基准,根节点的深度为1,根节点的叶子节点深度就+1,为2,以此类推。在这里可以看出,每一个深度下最多的节点个数为2^(n-1)
,n为深度;
整棵树的深度为二叉树中叶子节点的最大深度,比如说上边图片中的二叉树的深度就为3,一颗深度为n的数最多存贮的节点个数为2^n-1
。
什么是深度优先
说完了前置概念,我们现在正式来介绍什么是深度优先,深度优先的思想就是说相比于访问当前节点的值,我们更愿意先访问当前节点的叶子节点,我们可以先用一个链表的例子来帮助我们理解:
比如说有以下链表,我们以深度优先的思想来说,访问出来的结果就应该是4->3->2->1
,因为深度优先的思想是优先访问深度更大的节点。在后边的二叉树的访问的过程中我们也会演示具体的写法。
广度优先算法
说完了深度优先的思想,我们再来说广度优先的思想,它显然是和深度优先相反的,广度优先更加关注离当前节点更近的节点,还是以上边的单链表来举例,使用广度优先的思想访问下来的结果应该是1->2->3->4
,其实我觉得广度优先的思想在二叉树中还是很好理解的,我们可以把其理解成是水中的涟漪
:
即我们可以把当前访问到的节点理解成是一个波源
,当前访问的是波源节点,接下来要访问的就是以当前节点为波源发出的波要抵达的节点(有点像衍射?):
二叉树的访问
紧接着我们来讲二叉树的访问问题,这里我们主要来讲二叉树的层次遍历以及二叉树的三大遍历(前,中,后序遍历)。
层次遍历
先来讲层次遍历,所谓层次遍历就是一层一层地访问二叉树,这要和我们之前讲到的二叉树的深度相联系起来:
比如说这棵树层次访问的结果就应该是[[1],[2,3],[4,5,6,7]]
,可以看到这个顺序和我们之前讲到的广度优先访问的顺序几乎是一模一样,层序遍历时访问的顺序是从左到右的,所以我们先来用广度优先算法来写这个层次遍历,大家也可以去看力扣上的原题:二叉树的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();
//将根节点入队
if (root != null) {
queue.offer(root);
} else {
return ans;
}
while (!queue.isEmpty()) {
//记录当前深度下节点的个数
int count = queue.size();
//临时变量来存储当前层次的结果
LinkedList<Integer> tmp = new LinkedList<>();
//访问当前的层次
for (int i = 0;i < count;i++) {
//取出当前要访问的节点
TreeNode cur = queue.poll();
//将结果加入到结果中
tmp.add(cur.val);
//如果左节点不为空,将左节点入队
if (cur.left != null) {
queue.offer(cur.left);
}
//如果右节点不为空,将左节点入队
if (cur.right != null) {
queue.offer(cur.right);
}
}
//访问完了一个层次,将结果加入到集合中
ans.add(tmp);
}
return ans;
}
}
这基本上就是一个标准的广度优先搜索,一般来说我们模拟广度优先搜索都是要借助队列这个先进先出的数据结构,具体来说就是我们访问当前节点之后要知道下一次要访问的节点肯定是当前节点的子节点,所以将子节点加入队尾,等待被访问,直到所有节点都被访问完成;
除了广度优先搜索,我们还可以用深度优先来实现层次遍历:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//需要的答案
List<List<Integer>> ans = new LinkedList<>();
//深度优先算法
dfs(root,ans,1);
return ans;
}
//cur为当前要访问的节点,ans为结果,deepth为当前访问的层级
public void dfs(TreeNode cur,List<List<Integer>> ans,int deepth) {
//如果当前节点为null,说明已经到了最终的叶子节点,直接返回
if (cur == null) {
return;
}
//当答案的大小不足以容纳结果,新增一个List来存储新的层级的结果
if (ans.size() < deepth) {
ans.add(new LinkedList<Integer>());
}
//取出当前的结果
int curVal = cur.val;
//将结果添加到对应的层级
ans.get(deepth-1).add(curVal);
//递归,先左后右
dfs(cur.left,ans,deepth+1);
dfs(cur.right,ans,deepth+1);
}
}
深度优先基本上就是用递归来实现的,基本思想就是先当前,然后递归访问当前节点的左右节点。
总结
本篇文章内容较少,主要是介绍了二叉树的最基本概念和BFS,DFS,这两个思想将会贯穿整个二叉树,乃至是图的体系,大家可以理解理解,下篇文章将会介绍三种遍历方式(前序,中序,后序)。