理论基础需要了解 二叉树的种类,存储方式,遍历方式 以及二叉树的定义
文章讲解:代码随想录
二叉树的种类
满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置(连续)。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。(优先级队列其实是一个堆,堆就是完全二叉树,同时保证父节点和子节点的顺序关系)
二叉搜索树(BST)
上述的树,都没有数值,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。(它对节点结构无要求,但节点上的元素要有一定顺序)
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树(AVL(Adelson-Velsky and Landis))
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。(插入节点、查询某一个元素都是O(logn))
使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set(里面的元素都是有序的,因为底层实现是AVL树)等等,否则自己写的代码,自己对其性能分析(里面的元素是否是有序的,时间复杂度是多少,为什么)都分析不清楚!
二叉树的存储方式
二叉树可以链式存储(一般用),也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组(如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。)。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
二叉树的遍历方式
二叉树主要有两种遍历方式:
1、深度优先遍历:先往深走,遇到叶子节点再往回走。(一般通过递归实现)一个方向一直搜索,到终点后回退,换下一个方向...大多数可以递归法的,都可以迭代法,只是可能较复杂。
- 前序遍历(递归法,迭代法(非递归方式))中左右
- 中序遍历(递归法,迭代法)左中右
- 后序遍历(递归法,迭代法)左右中
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;
}
}
递归遍历 (必须掌握)
二叉树的三种递归遍历掌握其规律后,其实很简单
题目链接/文章讲解/视频讲解:代码随想录
递归三步:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
// 前序遍历·递归·LC144_二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
preorder(root, result);
return result;
}
public void preorder(TreeNode root, List<Integer> result){// 确定递归函数的参数和返回值
if(root == null) return;// 确定终止条件
// 确定单层递归的逻辑
result.add(root.val);// 中
preorder(root.left, result);// 左
preorder(root.right, result);// 右
}
}
// 中序遍历·递归·LC94_二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}
public void inorder(TreeNode root, List<Integer> result){// 确定递归函数的参数和返回值
if(root == null) return;// 确定终止条件
// 确定单层递归的逻辑
inorder(root.left, result);// 左
result.add(root.val);// 中
inorder(root.right, result);// 右
}
}
// 后序遍历·递归·LC145_二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
postorder(root, result);
return result;
}
public void postorder(TreeNode root, List<Integer> result){// 确定递归函数的参数和返回值
if(root == null) return;// 确定终止条件
// 确定单层递归的逻辑
postorder(root.left, result);// 左
postorder(root.right, result);// 右
result.add(root.val);// 中
}
}
迭代遍历 (基础不好的录友,迭代法可以放过)题目链接/文章讲解/视频讲解:代码随想录
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
因此可以用栈实现二叉树的前中后序遍历。
前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
前序遍历(迭代法)(遍历和处理节点是一个逻辑,所以代码较简洁)
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子(这样出栈的时候才是中左右的顺序)。
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
//非递归方式,用栈模拟了前序遍历
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.pop();
// 中
if(temp != null){
result.add(temp.val);
}else{
continue;
}
stack.push(temp.right); // 右
stack.push(temp.left); // 左
}
return result;
}
}
后序遍历(迭代法)
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了
将左右入栈顺序颠倒,得到result数组为中右左, 翻转result数组,得到左右中
所以后序遍历只需要前序遍历的代码稍作修改就可以了
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
// 将左右入栈顺序颠倒,得到result数组为中右左, 翻转result数组,得到左右中
// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.pop();
if(temp != null){
result.add(temp.val);
}else{
continue;
}
stack.push(temp.left);
stack.push(temp.right);
}
Collections.reverse(result);
return result;
}
}
中序遍历(迭代法)
用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。
在迭代的过程中,有两个操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;//使用指针来遍历二叉树
while(cur != null || !stack.isEmpty()){
// 1、访问:指针来访问节点,访问到最底层
if(cur != null){
stack.push(cur); // 将访问的节点放进栈
cur = cur.left; // 左
}else{// 2、处理:将元素放进result数组中
cur = stack.pop();// 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
result.add(cur.val); // 中
cur = cur.right; // 右
}
}
return result;
}
}
统一迭代 (基础不好的录友,迭代法可以放过)这是统一迭代法的写法, 如果学有余力,可以掌握一下
题目链接/文章讲解:代码随想录
以中序遍历为例,使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
// 前序遍历,,右左中入栈顺序
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if(root != null) stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.peek();
// 中
if(temp != null){
stack.pop();// 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
// 添加右节点(空节点不入栈)
if(temp.right != null){
stack.push(temp.right);
}
// 添加左节点(空节点不入栈)
if(temp.left != null){
stack.push(temp.left);
}
stack.push(temp);// 添加中节点
stack.push(null);// 中节点访问过,但是还没有处理,加入空节点做为标记。
}else{// 只有遇到空节点的时候,才将下一个节点放进结果集
stack.pop();// 将空节点弹出
temp = stack.pop();// 重新取出栈中元素
result.add(temp.val);// 加入到结果集
}
}
return result;
}
}
// 中序遍历,右中左入栈顺序
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if(root != null) stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.peek();
// 中
if(temp != null){
stack.pop();// 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
// 添加右节点(空节点不入栈)
if(temp.right != null){
stack.push(temp.right);
}
stack.push(temp);// 添加中节点
stack.push(null);// 中节点访问过,但是还没有处理,加入空节点做为标记。
// 添加左节点(空节点不入栈)
if(temp.left != null){
stack.push(temp.left);
}
}else{// 只有遇到空节点的时候,才将下一个节点放进结果集
stack.pop();// 将空节点弹出
temp = stack.pop();// 重新取出栈中元素
result.add(temp.val);// 加入到结果集
}
}
return result;
}
}
// 后序遍历,中右左入栈顺序
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if(root != null) stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.peek();
// 中
if(temp != null){
stack.pop();// 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
stack.push(temp);// 添加中节点
stack.push(null);// 中节点访问过,但是还没有处理,加入空节点做为标记。
// 添加右节点(空节点不入栈)
if(temp.right != null){
stack.push(temp.right);
}
// 添加左节点(空节点不入栈)
if(temp.left != null){
stack.push(temp.left);
}
}else{// 只有遇到空节点的时候,才将下一个节点放进结果集
stack.pop();// 将空节点弹出
temp = stack.pop();// 重新取出栈中元素
result.add(temp.val);// 加入到结果集
}
}
return result;
}
}