数组是存放在连续内存空间上的相同类型数据的集合。
需要两点注意的是
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
链表的定义(代码)
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用.
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
常见的三种哈希结构:
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
匹配问题都是栈的强项
二叉树的种类(无值)
二叉树的种类(有值)
存储方式:二叉树可以链式存储,也可以顺序存储。
链式存储
顺序存储
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 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;
}
}
二叉树的遍历方式
深度优先遍历
广度优先遍历
求二叉树的属性
二叉树:是否对称(opens new window)
二叉树:求最大深度(opens new window)
二叉树:求最小深度(opens new window)
二叉树:求有多少个节点(opens new window)
二叉树:是否平衡(opens new window)
二叉树:找所有路径(opens new window)
二叉树:递归中如何隐藏着回溯(opens new window)
二叉树:求左叶子之和(opens new window)
二叉树:求左下角的值(opens new window)
二叉树:求路径总和(opens new window)
二叉树的修改与构造
翻转二叉树(opens new window)
构造二叉树(opens new window)
构造最大的二叉树(opens new window)
合并两个二叉树(opens new window)
求二叉搜索树的属性
二叉搜索树中的搜索(opens new window)
是不是二叉搜索树(opens new window)
求二叉搜索树的最小绝对差(opens new window)
求二叉搜索树的众数(opens new window)
二叉树公共祖先问题
二叉树的公共祖先问题(opens new window)
二叉搜索树的公共祖先问题(opens new window)
二叉搜索树的修改与构造
二叉搜索树中的插入操作(opens new window)
二叉搜索树中的删除操作(opens new window)
修剪二叉搜索树(opens new window)
构造二叉搜索树(opens new window)
在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。
回溯算法中函数返回值一般为void。
再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
void backtracking(参数)
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
回溯函数终止条件伪代码
if (终止条件) {
存放结果;
return;
}
回溯法的问题可以转为树结构进行分析
在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeBmPmOP-1691754403535)(https://code-thinking-1253855093.file.myqcloud.com/pics/20210130173631174.png “回溯算法理论基础”)]
注意图中,我特意举例集合大小和孩子的数量是相等的!
回溯函数遍历过程伪代码如下:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
backtracking这里自己调用自己,实现递归。
大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
组合总和问题
多个集合求组合问题
切割问题
难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
子集问题
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
排列问题
大家此时可以感受出排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
棋盘问题
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
贪心的套路(什么时候用贪心):最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
一般解题步骤
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
贪心简单题
贪心中等题
贪心解决股票问题
两个维度权衡问题
在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。
贪心难题
这里的题目如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练!
贪心解决区间问题
关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。
动态规划应该如何debug?
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了。
自己先思考这三个问题:
如果这灵魂三问自己都做到了,基本上这道题目也就解决了
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
动划基础
背包问题系列
打家劫舍系列
股票系列
子序列系列
一刷跟着顺序让我对算法有了自己的解题思路
接下来准备二刷,这次要更认真一些才行,把方法论和题真正的掌握了