理论基础
二叉树种类
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。当一棵满二叉树的深度为k,则有2k-1个节点。
完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2h-1 个节点。
二叉搜索树
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
平衡二叉树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
红黑树
红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)。它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logn)。它同时满足以下特性:
- 节点是红色或黑色;
- 根是黑色;
- 叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),null节点的父节点在红黑树里不将其看作叶子节点;
- 从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点;
- 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点。
在Java中,HashMap
和LinkedHashMap
的底层都是由数组+链表/红黑树组成的,而HashSet
底层采用HashMap
,LinkedHashSet
底层采用LinkedHashMap
实现,TreeMap
和TreeSet
通过红黑树实现。
二叉树的遍历方式
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
栈是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
递归遍历
题目链接:144.二叉树的前序遍历、145.二叉树的后序遍历、94.二叉树的中序遍历
文档讲解:https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
视频讲解:https://www.bilibili.com/video/BV1Wh411S7xt
思路
递归三要素:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
代码
144.二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
traversal(root, res);
return res;
}
public void traversal(TreeNode root, List<Integer> res) {
if (root == null) return;
res.add(root.val);
traversal(root.left, res);
traversal(root.right, res);
}
}
145.二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
traversal(root, res);
return res;
}
public void traversal(TreeNode root, List<Integer> res) {
if (root == null) return;
traversal(root.left, res);
traversal(root.right, res);
res.add(root.val);
}
}
94.二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
trversal(root, res);
return res;
}
public void trversal(TreeNode root, List<Integer> res) {
if (root == null) return;
trversal(root.left, res);
res.add(root.val);
trversal(root.right, res);
}
}
迭代遍历
题目链接:144.二叉树的前序遍历、145.二叉树的后序遍历、94.二叉树的中序遍历
文档讲解:https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%BF%AD%E4%BB%A3%E9%81%8D%E5%8E%86.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
视频讲解:前序和后序、中序
思路
- 前序遍历:用栈作为辅助。先放入根节点,然后弹出节点,将节点的值放入数组,然后先放入右孩子,再放入左孩子。(因为栈是先进后出,先放右孩子,后面弹出处理的时候就是后处理右孩子)
- 后序遍历:只有存入顺序和前序遍历不一样。前序遍历是中左右,后序遍历是左右中。我们先处理根节点,然后放入左孩子、右孩子,这样遍历出来的顺序是中右左,再把结果数组翻转,结果就是左右中。
- 中序遍历:先遍历到最左的节点,将沿路的节点都放入栈中。当遍历到的cur.left=null时,就弹出栈顶元素放入数组,然后遍历右节点。
代码
144.二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> st = new LinkedList<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.add(node.val);
if (node.right != null) st.push(node.right);
if (node.left != null) st.push(node.left);
}
return res;
}
}
145.二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> st = new LinkedList<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.add(node.val);
if (node.left != null) st.push(node.left);
if (node.right != null) st.push(node.right);
}
Collections.reverse(res);
return res;
}
}
94.二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> st = new LinkedList<>();
TreeNode cur = root;
while (!st.isEmpty() || cur != null) {
if (cur != null) {
st.push(cur);
cur = cur.left;
} else {
cur = st.pop();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
}
统一迭代
题目链接:144.二叉树的前序遍历、145.二叉树的后序遍历、94.二叉树的中序遍历
文档讲解:https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E7%BB%9F%E4%B8%80%E8%BF%AD%E4%BB%A3%E6%B3%95.html#%E6%80%9D%E8%B7%AF
视频讲解:
思路
我们将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点(中节点)放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
代码
144.二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
145.二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
94.二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}