递归之谜
(17条消息) 关于递归中return的理解(最浅显易懂)_都return了为什么还在递归_Pledgee的博客-CSDN博客
(17条消息) 二叉树三种遍历(动态图+代码深入理解)_二叉树的三种遍历例题带图_杨 戬的博客-CSDN博客
二叉树
代码随想录 (programmercarl.com)
#定义:由父节点和子节点组成,且父节点只能直接分叉两个节点;结构单独抽离出来像是一颗倒立着的树。
#分类:
1.满二叉树:每个节点有两个直接子节点,且叶子节点是满的。
2.完全二叉树:父节点左子树和右子树之间的高度差小于等于1,且其子节点均居左分布,满二叉树是一种特殊的完全二叉树(左右子树高度差为0,且最底层节点满的状态符合完全二叉树的定义)。
3.二叉搜索树:满足二叉树定义的基础之上,且每个节点存储数值,且该数值有序(父节点的数值大于左子节点及其字节点的数值,小于右子节点及其所有节点上的数值,且其子节点也具有这种规律)。
4.平衡二叉搜索树(AVL 树):在二叉搜索树的基础之上,其左右子树高度差不大于1。
TreeMap:TreeMap 是一个基于红黑树实现的有序映射(键值对)容器。它根据键的自然顺序进行排序,或者根据自定义的 Comparator 进行排序。TreeMap 的插入、删除和查找操作的时间复杂度均为 O(log n)。
TreeSet:TreeSet 是一个基于红黑树实现的有序集合容器。它根据元素的自然顺序进行排序,或者根据自定义的 Comparator 进行排序。TreeSet 的插入、删除和查找操作的时间复杂度均为 O(log n)。
这两个容器都是有序的,且支持高效的插入、删除和查找操作。它们的底层实现都是基于平衡二叉搜索树,因此能够保持元素的有序性,并且具有较快的操作性能。
其他集合补充:
HashMap:HashMap 使用了数组和链表(或红黑树)的组合实现。具体来说,它使用了数组作为底层的数据结构,每个数组元素是一个链表(或红黑树)的头节点。当发生哈希冲突时,即多个键值对被映射到同一个数组索引上时,它们会以链表(或红黑树)的形式存储在同一个数组索引位置上。
HashSet:HashSet 是基于 HashMap 实现的,它使用 HashMap 的键来存储元素,而值则为一个固定的常量对象。HashSet 实际上是一个不允许重复元素的集合,它通过 HashMap 来实现元素的存储和查找。
ArrayList:ArrayList 使用了动态数组作为底层的数据结构。它可以根据需要自动扩容,并且支持随机访问和快速的插入、删除操作。ArrayList 的实现是基于数组的,当需要插入或删除元素时,可能需要进行数组的拷贝和移动操作。
LinkedList:LinkedList 使用了双向链表作为底层的数据结构。双向链表可以支持快速的插入和删除操作,但随机访问的性能较差。LinkedList 的每个节点都包含了前驱节点和后继节点的引用。
#存储方式:链式存储和顺序存储
链式存储:每个节点存储左右指针和data,每个节点通过指针串联在一起。
顺序存储:底层数据存储在内存是有序连续分布的,也即使用数组。
#遍历方式:广度优先遍历、深度优先遍历
广度优先遍历:
层序遍历(迭代法)
深度优先遍历:
前序遍历(递归法、迭代法)
中序遍历(递归法、迭代法)
后续遍历(递归法、迭代法)
前中后序的命名是以中间节点的位置判断的。前序遍历:中左右;中序遍历:左中右;后续遍历:左右中
#二叉树定义:
1// 二叉树定义: 2public class TreeNode { 3 int val; // 节点的值 4 TreeNode left; // 左子节点 5 TreeNode right; // 右子节点 6 7 // 默认构造方法 8 TreeNode() {} 9 10 // 带有一个参数的构造方法 11 TreeNode(int val) { 12 this.val = val; 13 } 14 15 // 带有三个参数的构造方法 16 TreeNode(int val, TreeNode left, TreeNode right) { 17 this.val = val; 18 this.left = left; 19 this.right = right; 20 } 21}
递归三部曲:
1.确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
1preorder(root, result);
2.确定终止条件:写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
1if (root == null) { 2 return; 3 }
3.确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
1// 前序为例 2result.add(root.val); // 中间节点 3preorder(root.left, result); // 左节点 4preorder(root.right, result); // 右节点
LeetCode:144前序遍历
方法一:递归法
1.思路
递归三部曲
确定方法形参及返回值;确定单层递归逻辑;确定终止条件。
1.思路
递归三部曲:(终止条件应该在递归逻辑中体现)
1.确定递归函数的参数和返回值;
1preorder(root, result);
2.确定终止条件
1if (root == null) { 2 return; 3 }
3.确定单层递归逻辑;
1// 前序为例 2result.add(root.val); // 中间节点 3preorder(root.left, result); // 左节点 4preorder(root.right, result); // 右节点
2.代码实现
方法一:
1// 前序遍历 2class Solution { 3 public List<Integer> preorderTraversal(TreeNode root) { 4 List<Integer> result = new ArrayList<>(); 5 6 // 确定递归函数的参数和返回值 7 preorder(root, result); 8 return result; 9 } 10 11 // 确定单层递归的逻辑 12 public void preorder(TreeNode root, List<Integer> result) { 13 // 确定终止条件 14 if (root == null) { 15 return; 16 } 17 result.add(root.val); 18 preorder(root.left, result); 19 preorder(root.right, result); 20 } 21}
3.复杂度分析
时间复杂度:O(logn).
空间复杂度:O(n).
方法二:迭代法
1.思路
为什么用栈?
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
2.代码实现
1// 中序遍历 2class Solution { 3 public List<Integer> inorderTraversal(TreeNode root) { 4 List<Integer> result = new ArrayList<>(); 5 // 确定递归函数的参数和返回值 6 inorder(root, result); 7 return result; 8 // 确定终止条件 9 } 10 // 确定单层递归的逻辑 11 public void inorder(TreeNode root, List<Integer> result) { 12 if (root == null) { 13 return; 14 } 15 inorder(root.left, result); 16 result.add(root.val); 17 inorder(root.right, result); 18 } 19}
3.复杂度分析:
时间复杂度:O(logn)
空间复杂度:O(n),创建了result动态数组
方法二:迭代法
1.思路:
定义节点指针cur指向root,用于控制遍历方向,定义stack栈暂存节点,用于辅助遍历,当指针和栈均为空时,则退出循环,否则继续循环。很绕==
2.代码实现:
1// 中序遍历(出栈顺序):左——中——右 入栈顺序:中——左——右 2 3class Solution { 4 public List<Integer> inorderTraversal(TreeNode root) { 5 List<Integer> result = new ArrayList<>(); // 用于存储遍历结果的列表 6 if (root == null) { 7 return result; // 如果根节点为空,直接返回空列表 8 } 9 10 Stack<TreeNode> stack = new Stack<>(); // 用于辅助遍历二叉树的栈 11 TreeNode cur = root; // 引入指针指向根节点 12 while (cur != null || !stack.isEmpty()) { // 当当前节点不为空 或 栈不为空时,进行循环 13 if (cur != null) { // 当前节点不为空 14 stack.push(cur); // 该节点压入栈中 15 cur = cur.left; // 将挡墙节点更新为左子节点 16 } else { // 当前节点为空时 17 cur = stack.pop(); // 弹出栈顶节点 18 result.add(cur.val); // 将节点的值加入到结果集中 19 cur = cur.right; // 将当前节点更新为右子节点 20 } 21 } 22 return result; // 将当前节点更新为右子节点 23 } 24}
3.复杂度分析:
时间复杂度:O(logn)
空间复杂度:O(n),创建了result数组
LeetCode:145.后序遍历
方法一:递归法
1.思路:
递归三部曲,如上。
2.代码实现:
1// 后续遍历 2class Solution { 3 public List<Integer> postorderTraversal(TreeNode root) { 4 List<Integer> result = new ArrayList<>(); 5 // 确定递归函数的参数与返回值 6 postorder(root, result); 7 return result; 8 9 } 10 // 确定单层递归的逻辑 11 public void postorder(TreeNode root, List<Integer> list) { 12 if (root == null) { 13 return; 14 } 15 postorder(root.left, list); 16 postorder(root.right, list); 17 list.add(root.val); 18 } 19}
方法二:迭代法
1.思路:
后续遍历(出栈顺序):左右中
前序遍历(出栈顺序):中左右
将前序遍历调整为:中右左
再将中右左进行反转,得到左右中,也即实现了后续迭代遍历法
2.代码实现:
1. 1class Solution { 2 public List<Integer> postorderTraversal(TreeNode root) { 3 List<Integer> result = new ArrayList<>(); // 用于存储遍历结果的列表 4 if (root == null){ 5 return result; // 如果根节点为空,直接返回空列表 6 } 7 Stack<TreeNode> stack = new Stack<>(); // 用于辅助遍历的栈 8 stack.push(root); // 将根节点压入栈中 9 while (!stack.isEmpty()){ // 当栈不为空时,进行循环 10 TreeNode node = stack.pop(); // 弹出栈顶节点 11 result.add(node.val); // 将节点的值加入到结果列表中 12 if (node.left != null){ // 如果节点的左子节点不为空,将左子节点压入栈中 13 stack.push(node.left); 14 } 15 if (node.right != null){ // 如果节点的右子节点不为空,将右子节点压入栈中 16 stack.push(node.right); 17 } 18 } 19 Collections.reverse(result); // 翻转结果列表 20 return result; // 返回遍历结果列表 21 } 22}
3.复杂度分析:
时间复杂度:O(logn)
空间复杂度:2O(n)