一. 学习文章及资料
二. 学习内容
1. 理论基础
(1) 种类:
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1 ~ 2^(h-1) 个节点。
二叉搜索树:前面介绍的树都没数值,而二叉搜索树是有数值的。二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
(2) 存储方式:
二叉树可以链式存储,也可以顺序存储.
链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起
链式存储如图:
顺序存储如图:
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
(3) 遍历方式;
主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历:一层一层的去遍历
- 层次遍历(迭代法)
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;
}
}
2. 递归遍历
(1) 递归算法的三个要素:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
traversal(root,result);
return result;
}
//确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入list来放节点的数值,
//除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void
public void traversal(TreeNode root,List<Integer> result){
if(root==null) return;
result.add(root.val); //中
traversal(root.left, result); //左
traversal(root.right, result); //右
}
}
中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
traversal(root,result);
return result;
}
public void traversal(TreeNode root,List<Integer> result){
if(root==null) return;
traversal(root.left,result); //左
result.add(root.val); //中
traversal(root.right,result); //右
}
}
后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
traversal(root,result);
return result;
}
public void traversal(TreeNode root,List<Integer> result){
if(root==null) return;
traversal(root.left,result); //左
traversal(root.right,result); //右
result.add(root.val); //中
}
}
3. 迭代遍历
(1) 两个顺序:
访问顺序(入栈顺序) ——>处理顺序(加入结果List顺序)
(2) 解题思路:
前序遍历
- 根放入栈后弹出,因为根是要处理的元素,加入list
- 再放右孩子、左孩子 ,为什么先放右孩子?因为栈是先进后出,只有先放右孩子再放左孩子,弹出时才能先处理左孩子再右孩子,结果list中元素达到中左右这个顺序
- 弹出左孩子,加入list,进而处理左孩子的左右孩子。左孩子的左孩子弹出时直接加入list,因为它已经是叶子结点了
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root==null) return result;
stack.push(root);
while(!stack.isEmpty()){
TreeNode node=stack.pop();
result.add(node.val); // 中
if(node.right!=null){
stack.push(node.right); // 右(空节点不入栈)
}
if(node.left!=null){
stack.push(node.left); // 左(空节点不入栈)
}
}
return result;
}
}
后序遍历
只需在前序遍历的基础上修改三行代码
- 入栈顺序改为 左->右
- 将结果list reverse一下 中左右->左右中
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root==null) return result;
stack.push(root);
while(!stack.isEmpty()){
TreeNode node=stack.pop();
result.add(node.val);
if(node.left!=null){
stack.add(node.left);
}
if(node.right!=null){
stack.add(node.right);
}
}
Collections.reverse(result);
return result;
}
}
中序遍历
特殊就特殊在,只有中序遍历访问顺序和处理顺序不同
- 先从根开始一直访问左孩子直到为空
- 弹出处理栈顶元素,再看这个元素有无右孩子,也为空的话就继续弹出元素;如这个元素有右孩子就将其右孩子加入栈
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root==null) return result;
TreeNode cur=root;
while(cur!=null||!stack.isEmpty()){
if(cur!=null){ // 指针来访问节点,访问到最底层
stack.push(cur); // 将访问的节点放进栈
cur=cur.left; //左
}else{
cur=stack.pop();// 从栈里弹出的数据,就是要处理的数据(放进result里的数据)
result.add(cur.val); //中
cur=cur.right; //右
}
}
return result;
}
}
4. 统一迭代
(1) 算法思路:
前序遍历
-
核心目标:
无需递归,用栈显式跟踪待处理节点,通过空节点标记NULL
区分 未处理的节点 和 待输出值的节点。 -
操作步骤:
-
遇到 非空节点:弹出后按 右 → 中 → NULL → 左 顺序重新入栈(栈是后进先出,实际处理顺序为左 → 中 → 右)。
-
遇到 空节点:表示下一个栈顶节点是待输出的根节点,将其值加入结果。
-
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null) stack.push(root);
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if(node.right!=null) stack.push(node.right); // 添加右节点(空节点不入栈)
if(node.left!=null) stack.push(node.left); // 添加左节点(空节点不入栈)
stack.push(node); // 添加中节点
stack.push(null);// 中节点访问过,但是还没有处理,加入空节点做为标记。
}else{
stack.pop();// 只有遇到空节点的时候,才将下一个节点放进结果集
node=stack.peek(); // 将空节点弹出
stack.pop(); // 重新取出栈中元素
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null) stack.push(root);
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if(node.right!=null) stack.push(node.right); // 添加右节点(空节点不入栈)
stack.push(node); // 添加中节点
stack.push(null);// 中节点访问过,但是还没有处理,加入空节点做为标记。
if(node.left!=null) stack.push(node.left); // 添加左节点(空节点不入栈)
}else{
stack.pop();// 只有遇到空节点的时候,才将下一个节点放进结果集
node=stack.peek(); // 将空节点弹出
stack.pop(); // 重新取出栈中元素
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result=new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null) stack.push(root);
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop();
stack.push(node);
stack.push(null);
if(node.right!=null) stack.push(node.right);
if(node.left!=null) stack.push(node.left);
}else{
stack.pop();
node=stack.peek();
stack.pop();
result.add(node.val);
}
}
return result;
}
}