文章目录
数组
数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
需要两点注意的是
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
数组的经典题目
- 二分法
- 双指针法
- 滑动窗口
- 模拟行为(循环不变量原则)
链表
链表的理论基础
- 链表的种类主要为:单链表,双链表,循环链表
- 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
- 链表是如何进行增删改查的。
- 数组和链表在不同场景下的性能分析。
链表的定义(代码)
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;
}
}
链表经典题目
- 虚拟头结点(链表使用技巧:虚拟头节点)
- 设计链表(链表的基本操作,都涵盖了)
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点的数值
- 反转链表
- 删除倒数第N个节点
- 链表相交
- 环形链表
哈希表
哈希表理论基础
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用.
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
常见的三种哈希结构:
- 数组
- set(集合)
- map(映射)
哈希表经典题目
- 有效的字母异位词(数组作为哈希表)
- 两个数组的交集(set作为哈希表)
- 快乐数(map作为哈希表)
- 两数之和(map作为哈希表)
- 四数相加II(map作为哈希表)
- 赎金信(数组作为哈希表)
- 三数之和(map作为哈希表)
- 四数之和(map作为哈希表)
字符串
经典题目
双指针法
- 反转字符串(使用双指针法进行反转操作)
- 替换空格(使用双指针法进行替换操作)
- 移除元素(使用双指针法进行移除操作)
- 翻转字符串里的单词(使用双指针法删除冗余空格)
反转系列
- 反转字符串II(当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。)
- 翻转字符串里的单词(先整体反转再局部反转)
- 左旋转字符串(先局部反转再整体反转达到了左旋的效果)
KMP
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
栈与队列
栈与队列的理论基础
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
栈经典题目
匹配问题都是栈的强项
- 用栈实现队列(练习栈操作)
- 用队列实现栈(练习队列操作)
- 有效的括号(栈解决的经典问题(匹配问题))
- 删除字符串中的所有相邻重复项
- 逆波兰表达式求值
- 滑动窗口最大值
- 前 K 个高频元素
二叉树
二叉树的理论基础
二叉树的种类(无值)
- 满二叉树
- 完全二叉树
二叉树的种类(有值)
- 二叉搜索树
- 二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
- 二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 平衡二叉搜索树
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
存储方式:二叉树可以链式存储,也可以顺序存储。
-
链式存储
- 链式存储则是通过指针把分布在各个地址的节点串联一起。
-
顺序存储
- 顺序存储的元素在内存是连续分布的
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 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)中递归如何隐藏着回溯
-
- 递归:后序,必须三层约束条件,才能判断是否是左叶子。
- 迭代:直接模拟后序遍历
-
- 递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点。
- 迭代:层序遍历找最后一行最左边
-
- 递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。
- 迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和
二叉树的修改与构造
-
- 递归:前序,交换左右孩子
- 迭代:直接模拟前序遍历
-
- 递归:前序,重点在于找分割点,分左右区间构造
- 迭代:比较复杂,意义不大
-
- 递归:前序,分割点为数组最大值,分左右区间构造
- 迭代:比较复杂,意义不大
-
- 递归:前序,同时操作两个树的节点,注意合并的规则
- 迭代:使用队列,类似层序遍历
求二叉搜索树的属性
-
- 递归:二叉搜索树的递归是有方向的
- 迭代:因为有方向,所以迭代法很简单
-
- 递归:中序,相当于变成了判断一个序列是不是递增的
- 迭代:模拟中序,逻辑相同
-
求二叉搜索树的最小绝对差(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(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
回溯算法能解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
回溯经典题目
组合问题
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
- 组合
组合总和问题
- **组合总和 ** (元素只能使用一次)
- **组合总和II ** (元素可以使用无数次)
- 组合总和III (难在去重问题,“树枝去重”和“树层去重”)
多个集合求组合问题
- 电话号码的字母组合
切割问题
难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
- 分割回文串
- 复原IP地址
子集问题
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
- 子集
- 子集II (对子集问题进行去重,子集问题一定要排序(方便去重))
- **递增子序列 **
排列问题
大家此时可以感受出排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
- 全排列
- 全排列 II (强调了“树层去重”和“树枝去重”)
- 重新安排行程 (如果单纯的回溯搜索(深搜)并不难,难还难在容器的选择和使用上!)
棋盘问题
- N皇后
- 解数独
贪心算法
贪心算法理论基础
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
贪心的套路(什么时候用贪心):最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
一般解题步骤
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
贪心算法经典题目
贪心简单题
贪心中等题
贪心解决股票问题
两个维度权衡问题
在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。
贪心难题
这里的题目如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练!
贪心解决区间问题
关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。
- 贪心算法:跳跃游戏(opens new window)
- 贪心算法:跳跃游戏II(opens new window)
- 贪心算法:用最少数量的箭引爆气球(opens new window)
- 贪心算法:无重叠区间(opens new window)
- 贪心算法:划分字母区间(opens new window)
- 贪心算法:合并区间
动态规划法
动态规划法理论基础
动态规划应该如何debug?
- 把dp数组打印出来,看看究竟是不是按照自己思路推导的
- 做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了。
自己先思考这三个问题:
- 这道题目我举例推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
如果这灵魂三问自己都做到了,基本上这道题目也就解决了
动态规划五步曲
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
动态规划经典题目
动划基础
- 关于动态规划,你该了解这些!(opens new window)
- 动态规划:斐波那契数(opens new window)
- 动态规划:爬楼梯(opens new window)
- 动态规划:使用最小花费爬楼梯(opens new window)
- 动态规划:不同路径(opens new window)
- 动态规划:不同路径还不够,要有障碍!(opens new window)
- 动态规划:整数拆分,你要怎么拆?(opens new window)
- 动态规划:不同的二叉搜索树
背包问题系列
- 动态规划:关于01背包问题,你该了解这些!(opens new window)
- 动态规划:关于01背包问题,你该了解这些!(滚动数组)(opens new window)
- 动态规划:分割等和子集可以用01背包!(opens new window)
- 动态规划:最后一块石头的重量 II(opens new window)
- 动态规划:目标和!(opens new window)
- 动态规划:一和零!(opens new window)
- 动态规划:关于完全背包,你该了解这些!(opens new window)
- 动态规划:给你一些零钱,你要怎么凑?(opens new window)
- 动态规划:Carl称它为排列总和!(opens new window)
- 动态规划:以前我没得选,现在我选择再爬一次!(opens new window)
- 动态规划: 给我个机会,我再兑换一次零钱(opens new window)
- 动态规划:一样的套路,再求一次完全平方数(opens new window)
- 动态规划:单词拆分(opens new window)
- 动态规划:关于多重背包,你该了解这些!(opens new window)
- 听说背包问题很难? 这篇总结篇来拯救你了
打家劫舍系列
股票系列
- 动态规划:买卖股票的最佳时机(opens new window)
- 动态规划:本周我们都讲了这些(系列六)(opens new window)
- 动态规划:买卖股票的最佳时机II(opens new window)
- 动态规划:买卖股票的最佳时机III(opens new window)
- 动态规划:买卖股票的最佳时机IV(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)
- 动态规划:最长回文子序列
总结
一刷跟着顺序让我对算法有了自己的解题思路
接下来准备二刷,这次要更认真一些才行,把方法论和题真正的掌握了