Leetcode - 分类经典题目汇总
回溯算法
动态规划
通解通法
动态规划问题的一般形式就是求最值
判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义
dp
数组/函数的含义「状态」就是指,f(n) 把参数
n
想做一个状态,这个状态n
是由状态n - 1
和状态n - 2
转移(相加)而来,这就叫状态转移
最长递增子序列
原文:https://labuladong.gitee.io/algo/di-er-zhan-a01c6/zi-xu-lie--6bc09/dong-tai-g-6ea57/
一维最长递增子序列
二维最长递增子序列
二叉树
0.通解通法
遇到一道二叉树的题目时的通用思考过程是:
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse
函数配合外部变量来实现。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。
1.以树的视角看动归/回溯/DFS算法
动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同:
- 动态规划算法属于分解问题的思路,它的关注点在整棵「子树」。
- 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」。
- DFS 算法属于遍历的思路,它的关注点在单个「节点」。
有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了。回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点。
怎么理解?我分别举三个例子你就懂了:
第一个例子
给你一棵二叉树,请你用分解问题的思路写一个 count
函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了。
你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」。
// 定义:输入一棵二叉树,返回这棵二叉树的节点总数 int count(TreeNode root) { if (root == null) { return 0; } // 我这个节点关心的是我的两个子树的节点总数分别是多少 int leftCount = count(root.left); int rightCount = count(root.right); // 后序位置,左右子树节点数加上自己就是整棵树的节点数 return leftCount + rightCount + 1; }
第二个例子
给你一棵二叉树,请你用遍历的思路写一个 traverse
函数,打印出遍历这棵二叉树的过程,你看下代码:
你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」。
void traverse(TreeNode root) { if (root == null) return; printf("从节点 %s 进入节点 %s", root, root.left); traverse(root.left); printf("从节点 %s 回到节点 %s", root.left, root); printf("从节点 %s 进入节点 %s", root, root.right); traverse(root.right); printf("从节点 %s 回到节点 %s", root.right, root); }
第三个例子
我给你一棵二叉树,请你写一个 traverse
函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下:
你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」。
void traverse(TreeNode root) { if (root == null) return; // 遍历过的每个节点的值加一 root.val++; traverse(root.left); traverse(root.right); }
好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。
有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:
// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 void dfs(Node root) { if (root == null) return; // 做选择 print("我已经进入节点 %s 啦", root) for (Node child : root.children) { dfs(child); } // 撤销选择 print("我将要离开节点 %s 啦", root) } // 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 void backtrack(Node root) { if (root == null) return; for (Node child : root.children) { // 做选择 print("我站在节点 %s 到节点 %s 的树枝上", root, child) backtrack(child); // 撤销选择 print("我站在节点 %s 到节点 %s 的树枝上", child, root) } }
2.后序遍历的特殊之处
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值,传递回来的数据。
那么换句话说:
- 只有后序位置才能通过返回值获取子树的信息。
- 一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
字符串
1.辗转相除法
数组
0.只要数组有序,就应该想到双指针技巧
1.双指针法:左右指针
2.双指针法:快慢指针
这里要注意:
1)不必两两交换,时间复杂度高
2)直接快慢指针,慢指针找到第一个0的位置,快指针找到第一个非0的位置,不交换,直接把快指针位置的值,覆盖慢指针位置的值。
然后重复该步骤,直到快指针到头,把慢指针之后的元素再都变成0即可
链表
1. 虚拟头结点
Tips: 利用虚拟头结点,简化边界问题
当你需要创造一条新链表的时候,可以使用虚拟头结点简化边界情况的处理。
- 比如说,让你把两条有序链表合并成一条新的有序链表,是不是要创造一条新链表?
- 再比你想把一条链表分解成两条链表,是不是也在创造新链表?
这些情况都可以使用虚拟头结点简化边界情况的处理。
2 双指针法:快慢指针
找出环形列表开始的节点:
1.先通过快慢指针,速度为2和1,找到相遇的节点
2.然后一个指针回到head,一个指针从相遇节点触发,速度均为1,再次相遇的节点就是环开始的节点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-06-14 Docker - Swarm 集群