文章目录
一、什么是二叉树?
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。
遍历方式可以分为两大类:
-
深度遍历:前序、中序、后序
-
广度遍历:层次
二、二叉树的前序遍历
前序遍历的遍历顺序是:根结点 —> 左子树 —> 右子树
所以上图的前序遍历结果为:1 2 4 5 7 8 3 6
递归写法:
public void preOrderTraverse1(TreeNode root) {
if (root != null) {
System.out.print(root.val+" ");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
迭代写法:
注意:要认真体会此处栈的用法和中序遍历迭代写法中的栈用法是不一样的。这里的栈是用来找右节点和父节点的
根据前序遍历的顺序,优先访问根结点,然后在访问左子树和右子树。所以对于任意结点,首先遍历自己,然后再判断左子树是否为空,不为空时即重复上面的步骤,直到遍历到的结点的左子树为空。此时开始访问右子树。
由此我们可以知道在访问过左孩子之后需要反过来访问其右孩子,所以我们以栈这种数据结构的支持。对于任意一个结点node,具体步骤如下:
-
遍历到当前节点时,将该节点入栈并接入到结果中,将当前节点置为左孩子
-
判断此时的节点是否为空
若为空,则令当前节点为栈顶元素并出栈,然后再将当前节点置为右孩子
若不为空,则重复第一步骤直到当前节点为空(即右孩子为null且无父节点后退)或者栈为空
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
TreeNode cur=root;
while(cur!=null||!stack.isEmpty()){
while(cur!=null){
result.add(cur.val);
stack.push(cur);
cur=cur.left;
}
cur=stack.pop();
cur=cur.right;
}
return result;
}
注意:Java堆栈Stack类已经过时,Java官方推荐使用Deque替代Stack使用。Deque堆栈操作方法:push()、pop()、peek()。
学习Deque链接:(128条消息) 【Java】Java双端队列Deque使用详解_java deque_devnn的博客-CSDN博客
三、二叉树的中序遍历
中序遍历的遍历顺序是:左子树 —> 根结点 —> 右子树
所以上图的中序遍历结果为:4 2 7 5 8 1 3 6
递归写法:
public void inOrderTraverse1(TreeNode root) {
if (root != null) {
inOrderTraverse1(root.left);
System.out.print(root.val+" ");
inOrderTraverse1(root.right);
}
}
迭代写法:
我们利用栈后进先出的性质,从根节点不断向左节点移动并将当前节点入栈,直到遇到空节点,此时不再移动将当前节点设置为栈顶元素并将栈顶元素弹出加入到结果中,继续向当前节点的右孩子移动
具体步骤如下:
- 定义一个List作为结果列表
- 定义当前节点指向根节点
- 定义一个栈用来存储节点
- 定义外循环,循环条件为当前节点非空或栈非空
- 从当前节点向左孩子移动,非空节点入栈。若遍历到空节点,让当前节点等于栈顶元素并弹出栈顶元素,还要将该栈顶元素加入到结果列表中
- 然后当前节点移向右节点,右节点进入下一次循环检查左右节点
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
Stack<TreeNode> stack=new Stack<TreeNode>();
TreeNode cur=root;
while((cur!=null)||(!stack.empty())){
while(cur!=null){
stack.push(cur);
cur=cur.left;
}
cur=stack.pop();
// stack.pop;
result.add(cur.val);
cur=cur.right;
}
return result;
}
四、二叉树的后序遍历
后序遍历的遍历顺序是:左子树 —> 右子树 —> 根结点
所以上图的后序遍历结果为:4 7 8 5 2 6 3 1
递归写法:
public void postOrderTraverse1(TreeNode root) {
if (root != null) {
postOrderTraverse1(root.left);
postOrderTraverse1(root.right);
System.out.print(root.val+" ");
}
}
迭代写法:
双栈解法:
后序遍历的遍历顺序是左右根,而前序遍历是根左右。那我们是否可以从我们熟悉且简单的前序遍历转化过去后序遍历呢?
答案是可以的。
通过观察,如果求出遍历顺序是根右左的节点序列,那么进行倒序就刚好得到后序遍历的顺序左右根。
那我们如何得到根右左的遍历顺序呢?
其实也就是前序遍历我们从根节点下来后先查找右节点,即说将代码中左右节点的位置换一下就行了。
因此利用两个栈,一个用于根右左遍历,一个用于保存序列(因为后进先出,所以到时候就能够的到根右左的倒序了)。
public List<Integer> postorderTraversal(TreeNode root) {
Deque<TreeNode> stack=new LinkedList<>();
Deque<Integer> postStack=new LinkedList<>();
TreeNode cur=root;
List<Integer> result=new ArrayList<>();
while(cur!=null||!stack.isEmpty()){
while(cur!=null){
postStack.push(cur.val);
stack.push(cur);
cur=cur.right;
}
cur=stack.pop();
cur=cur.left;
}
while(!postStack.isEmpty()){
result.add(postStack.pop());
}
return result;
}
单栈解法:
只用一个栈去解决后序遍历是有点麻烦的,因为前序遍历和中序遍历的思路也是有点像的,所以我们考虑能不能借鉴前序遍历和中序遍历的思路?
分析如下:
1.后序遍历的开头和中序遍历是可以一样的,都是先经过二叉树的最左分支,直到当前节点为空。
2.前序遍历和中序遍历经过的路径都是左根右,但是在后序遍历中,在经过根节点的时候,会选择跳过先去访问它的右子节点。
因此,对于这个情况我们就将根节点留在栈中不弹出,等到需要访问到它的时候在弹出。
3.当访问完右子节点后就会访问根节点。那我们怎么判断已经访问完右子节点现在需要访问根节点?(即判断当前节点是否需要访问)
对于这个情况我们可以记录上一次访问的节点,然后判断当前经过的节点和上一次访问的节点是什么关系,如果当前节点的右子节点是上一次访问过的节点,那说明这个就是根节点,也就是我们需要访问的当前节点?
也就是说我们要遍历当前节点,需要符合其中之一:
- 当前经过节点是叶子节点。
- 当前经过节点的右子节点是上一次访问的节点
public List<Integer> postorderTraversal(TreeNode root) {
/*
//双栈解法
Deque<TreeNode> stack=new LinkedList<>();
Deque<Integer> postStack=new LinkedList<>();
TreeNode cur=root;
List<Integer> result=new ArrayList<>();
while(cur!=null||!stack.isEmpty()){
while(cur!=null){
postStack.push(cur.val);
stack.push(cur);
cur=cur.right;
}
cur=stack.pop();
cur=cur.left;
}
while(!postStack.isEmpty()){
result.add(postStack.pop());
}
return result;
*/
Deque<TreeNode> stack=new LinkedList<>();
TreeNode cur=root;//记录当前节点
TreeNode pre=null;//记录上一个节点
List<Integer> result=new ArrayList<>();
while(cur!=null||!stack.isEmpty()){
while(cur!=null){
//用来遍历左节点
stack.push(cur);
//result.add(cur.val);
cur=cur.left;
}
cur=stack.pop();//弹出的是最左子节点,也就是说该节点没有左孩子了(但是不知道有没有右孩子)
//然后现在有两种情况
//若当前节点的右孩子为空(前面能弹出栈顶元素说明没有左孩子,说明不用访问它的右孩子,那直接将当前节点加入到结果列表中
//若当前节点的右孩子不为空,那就判断要不要先去访问右孩子(即判断上一个访问节点是否为当前节点的右孩子)若是上一个访问过的节点那就将当前节点加入到结果列表中
//如果这两种情况都不是,那就将当前节点入栈然后去访问当前节点的右孩子,继续循环
if(cur.right==null||pre==cur.right){
result.add(cur.val);
pre=cur;//记录最近一次访问的节点
cur=null; // 此处为了跳过下一次循环的访问左子节点的过程,直接进入栈的弹出阶段,因为但凡在栈中的节点,它们的左子节点都肯定被经过且已放入栈中。然后继续判断右孩子的情况,通过这个情况进行操作
}else{
stack.push(cur);// 将已弹出的根节点放回栈中
cur=cur.right;
}
}
return result;
}
五、二叉树的层次遍历
所谓层次遍历就是一层层按顺序读取
所以上图的后序遍历结果为:1 2 3 4 5 6 7 8
迭代写法:
层次遍历很简单,一层一层添加就好了
首先向队列中添加根节点,然后开始遍历。
从根节点开始,令当前节点为队列弹出的节点(弹多少次由这一层有多少个节点决定,而且这样弹出当一层循环结束后,队列就只剩下一层的节点,当队列为空说明遍历完了)将当前节点不为空的左右孩子加入队列
public List<List<Integer>> levelOrder(TreeNode root) {
TreeNode cur=root;
List<List<Integer>> result=new ArrayList<>();
Deque<TreeNode> queue=new LinkedList<>();
if(root==null) return result;
queue.add(root);
while(!queue.isEmpty()){
List<Integer> eachLevel=new ArrayList<>();
for(int i=0,len=queue.size();i<len;i++){
//遍历这层的所有节点
cur=queue.poll();
eachLevel.add(cur.val);
if(cur!=null){
//继续推进节点,作为下次遍历的父节点
if(cur.left!=null){
queue.add(cur.left);
// eachLevel.add(cur.left.val);
}
if(cur.right!=null){
queue.add(cur.right);
// eachLevel.add(cur.right.val);
}
}
}
result.add(eachLevel);
}
return result;
}
六、深度优先遍历DFS和广度优先遍历BFS
DFS和 BFS就像孪生兄弟,提到一个总是想起另一个。然而在实际使用中,我们用 DFS(更方便写、空间复杂度更低) 的时候远远多于 BFS。
应用:
广度优先搜索应用:层序遍历、最短路径、求二叉树的最大高度、由点到面遍历图、拓扑排序
深度优先搜索应用:先序遍历,中序遍历,后序遍历
代码框架:
DFS
非递归:
1.利用栈实现
2.从源节点开始把节点按照深度放入栈,然后弹出
3.每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4.直到栈为空
BFS:利用队列实现(FIFO)
1.利用队列实现
2.从源节点开始依次按照宽度进队列,然后弹出
3.每弹出一个节点,就把该节点所有没有进过队列的邻接点放入队列
4.直到队列为空
还没学完55555