day14(均有视频讲解)
理论基础
二叉树的种类,存储方式,遍历方式 以及二叉树的定义
二叉搜索树结构无所谓,关键是节点上元素的大小不能错。
平衡二叉搜索树需要看的就是左右子树高度差(整体的左右子树或是一小部分的左右子树)
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!
顺序存储:
若想找到一个下标为i的左右节点如何找–>i2+1是左;i2+2是右
深度优先搜索:前中后序指的都是中间节点的遍历顺序(前:中左右;中:左中右;后左右中;都是指的子树而不是单单只是节点);可以用递归实现也可以用迭代法
广度优先搜索:层序遍历,就是迭代法,用队列实现对二叉树一层一层的搜索
数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼。(手写二叉树等基本数据结构的能力一定要有)
递归遍历(No.144 145 94)
题目链接:
144 94 145
讲解:
递归算法的三个元素:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
对应这三个元素写一个前序遍历:
-
确定递归函数的参数和返回值
为了遍历二叉树,我们的递归函数需要知道当前正在遍历的节点。因此,函数的参数就是当前节点。对于前序遍历,我们通常不需要一个具体的返回值,因为遍历的目的是访问或输出每个节点的值。但是,为了收集遍历结果,我们可以使用一个数组来存储节点值。 -
确定终止条件
递归的终止条件是当前节点为空。当遍历到叶子节点的子节点时,递归应该停止。 -
确定单层递归的逻辑
在前序遍历的单层递归逻辑中,我们首先处理(访问或输出)当前节点,然后递归地遍历左子树,最后递归地遍历右子树。
function TreeNode(val, left, right) {
//用于创建表示二叉树节点的对象。每个TreeNode对象包含三个属性:val、left和right。
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
//如果调用TreeNode时提供了相应的参数值,则使用该值。
//如果没有提供(即参数为undefined),则使用默认值。
//对于val,默认值是0;对于left和right,默认值是null。
}
/**
* 前序遍历二叉树
* @param {TreeNode} root 二叉树的根节点
* @returns {number[]} 遍历结果
*/
function preorderTraversal(root) {
let result = []; // 用于存储遍历的结果
// 定义递归函数
function traverse(node) {
if (node === null) {
// 终止条件:当前节点为空
return;
}
// 处理当前节点:将当前节点的值添加到结果数组中
result.push(node.val);
// 递归遍历左子树
traverse(node.left);
// 递归遍历右子树
traverse(node.right);
}
traverse(root); // 从根节点开始遍历
return result; // 返回遍历结果
}
// 示例
const tree = new TreeNode(1, null, new TreeNode(2, new TreeNode(3), null));
console.log(preorderTraversal(tree)); // 输出: [1, 2, 3]
//虽然变量名为tree,它实际上只是一个指向二叉树根节点的引用
//因为在二叉树的数据结构中,通过根节点就可以访问到树的所有其他节点
写递归时注意按顺序写:前中后序遍历代码的差异就在传入数组时的顺序,以上代码是中左右的顺序,中序就是把处理当前节点(中)的代码放到左节点代码和右节点代码的中间即可
迭代遍历
题目链接:
144 94 145
放入栈,再按一定数据弹出栈后,放入数组。因为先进后出,所以前序的中左右,入栈顺序是中右左(根、右、左->右、左);
后序实现就是把代码中右和左的代码的顺序交换(变为中左右);然后再用reverse反转数组(变为右左中)
有点没想明白。
中序遍历不能这么写,因为前后序都是先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点
中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
所以中序遍历需要借助指针。
往左一直向下遍历,并且存入栈中(541);遍历到叶子节点(1),再遍历他的左孩子时是null(看作是左孩子,只是没有左孩子罢了);然后需要遍历右孩子,也没有;于是把栈里遍历过的元素再弹出来一个(4);然后去遍历弹出元素的右孩子(2)加入栈里(栈里是52);然后去遍历右孩子(2)的左孩子,为空;于是弹出栈里的元素(2);2的右孩子也为空,继续弹出5;因为5的左孩子找完了,开始找5的右孩子(6),入栈;6的左孩子为空,把6弹出;6的右孩子也为空,此时栈里没有元素了,结束。
视频的代码实现没看,直接看js代码,看不懂再回去看视频理解
统一迭代
针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记–>就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法
将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。
代码