文章目录
- 前言
- 1、两数之和(哈希表,双指针,数组)
- 2、有效的括号(栈,哈希表)
- 3、合并两个有序链表(递归,迭代)
- 4、最大子数组和(动态规划,分治,贪心)
- 5、爬楼梯(迭代,递归,动态规划,数学)
- 6、买卖股票的最佳时间(贪心,双指针,动态规划)
- 7、二叉树的中序遍历(Morris中序遍历,递归,迭代)
- 8、对称二叉树(递归,迭代)
- 9、二叉树的最大深度(BFS,DFS)
- 10、只出现一次的数字(哈希表,异或)
- 11、移动零(双指针)
- 12、多数元素(哈希表,排序,随机化,分治)
- 13、最小栈(栈)
- 14、比特位计数(Brian Kernighan算法,动态规划)
- 15、汉明距离(Brian Kernighan 算法,异或)
- 16、找到所有数组中消失的数(哈希表,Set集合)
- 17、环形链表(快慢指针,哈希表)
- 18、相交链表(哈希表,双指针)
- 19、反转链表(栈,递归,迭代,双指针,头插法)
- 20、回文链表(双指针)
- 21、翻转二叉树(BFS,DFS,递归)
- 22、二叉树的直径(BFS)
- 23、合并二叉树(BFS,DFS)
- 总结
前言
涵盖热门100题中的所有简单部分的题目,把第一遍做的笔记重新整理了一下放到一起了,修改了一些错误的部分,把没能实现的都实现了一下,去掉了重复的代码,只留下了最精炼的各种方法的代码
我也是一个力扣新手,很多算法并不是特别掌握,也是摸着石头过河,但我的经验和总结是很不错的学习内容,大家可以好好看看我的分析,思考,错误过程,如何解决,总结
好的题目是值得反复回看的,大家也是为了找工作或者提升自己,这些经典题目涵盖了很多的思路和方法,我后面做中等题或者是困难题的时候,很多简单题的思路都启发了我,大家不要着急,想一口吃成个胖子,把我的总结好好的跟自己的思路对比看看,重点掌握每个题目的一两个最重要的思路,然后再学习其他的扩展思路,吃透,会对后面的刷题有很大的帮助,如果是着急,囫囵吞枣,大致看完就过去,效果不会太好
我总结的时候,吸取了很多网友的评论和解答,结合自己的理解形成了这些题目的笔记,我又整理了一遍,确保没有不是重要的东西存在,每个题都总结了多种方法和思路,希望可以给大家帮助
希望大家关注,点赞,收藏,支持一下我,后面还会为大家其他重点的简单题,以及更新力扣热门的中等和困难的题目,有需要我的这个文档的,可以关注私信我,可以把我的这个吸收作为你自己的笔记,也可以节省一些时间。
1、两数之和(哈希表,双指针,数组)
1、问题描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
2、示例
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
3、提示
- 2 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- 只会存在一个有效答案
4、进阶
你可以想出一个时间复杂度小于 O(n2) 的算法吗?
5、具体解法(暴力遍历,哈希表,二分法)
//2022年3月21日10:58:28
//解法一,用暴力枚举的方式,注意边界
/*
public class Solution {
//public static void main(String[] args) {
//为什么不用写这个,因为我们是负责完成一个函数,系统会自动调用我们的函数进行测试。
//首先方法是数组类型的,因为要返回的是数组类型。两个参数也是对应的写好,符合写程序的思路
public int[] twoSum (int[] nums, int target){
int n = nums.length;
//i是外层循环,而且只需要判断到倒数第二个数即可,因为最后一个数由内层循环提供
for (int i = 0; i < nums.length - 1; i++) {
//j是内层循环,从i+1开始,且需要判断到最后一个数
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
//当符合条件的时候就输出一个数组,里面的内容是此时的i和j,也就是下标
return new int[]{i, j};//这个数组连名字都没有也可以?
}
}
}
return new int[0];//当for循环都跳出了,还是没有,就返回一个空数组,这个写法就是标准的返回空数组的写法
}
}
*/
//解法二:用HashMap的方式,哈希表
//并不是先将所有数据都放进HashMap,而是从数组第一个数先开始判断target-x在哈希表中是不是存在
//然后再将这个数放进Map中,可以避免自己跟自己匹配
//注意:为什么是containsKey而不是value,因为存的时候是数组值在key里,下标再value里
/*
public class Solution {
public int[] twoSum(int[] nums, int target) {
//创建一个HashMap集合,Map是键值对的形式,而HashMap是键唯一
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
//判断每一个值x对应的target-x是不是存在
if (hashtable.containsKey(target - nums[i])) {//因为这个map的键值对种是下标是值,键反而是数组中是数值
//存在就返回两个下标,其中的target-x下标可以用HashMap的get方法获得
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);//而且要注意键值对,谁是键,谁是值
}
return new int[0];
}
}
*/
//解法三、双指针,二分大法
//这其实是一道搜索题,固定一个数后,就是如何找到另外一个数(target - [i])。
//因为是数组,要利用随机访问的能力,因此内部loop可以用双指针从两头往中间找,这样可以节省一半的时间,整体时间复杂度会到O(nlogn),仍要注意边界。
/*
public class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = {0, 1};
if (nums.length <= 2) {
//回顾来看,因为题目强调了数组至少会有两个数,所以才是这么写的,写成==2也可以
return result;
}
for (int i = 0; i < nums.length - 1; i++) {
result[0] = i;//这个reault[0]和reault[1]的写法也值得去学习一下
int x = target - nums[i];
//对内部循环做双指针寻找,但我感觉本质还是遍历呢,但执行时间减了很多
//对自己最初疑问的解答:因为他是从两端同时开始的,相当于每次只遍历n/2次,所以这一层的复杂度会降到O(logn)
for (int j = i + 1, k = nums.length - 1; j <= k; j++, k--) {
if (nums[j] == x) {
result[1] = j;
return result;
}
if (nums[k] == x) {
result[1] = k;
return result;
}
}
//你看其本质就是循环的时候不是一个个去看,而是两个两个去看,但还是全看了
//所以说循环是for遍历的内容,而for里面要操作的多少并不是算进循环的,虽然所有数都判断,但是循环只跑了n/2次
}
return result;
}
}
*/
//解法四:内外都用双指针,四分大法
//其实主loop也可以采用二分法,用双指针从两头往中间遍历,这样又可以节省一半的时间。
//还可以做剪支,如果主loop的双指针之和恰好是target,那就直接返回,剪支在分析效率的时候可能帮助不大,但在真实的运行时,能大大提高效率。
//在不借助额外的数据结构的前提下,这是最优解,从运行结果来看,时间是0,严格来说是O(n/2*logn)。
/*
public class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = {0, 1};
if (nums.length <= 2) {
return result;
}
for (int i = 0, j = nums.length - 1; i < j; i++, j--) {
if (nums[i] + nums[j] == target) {
//主loop的双指针之和恰好是target
result[0] = i;
result[1] = j;
return result;
}
//这次是一次进来两个数做比较,i和j,然后内层会从i+1到j-1之间去找遍历找数,也是通过二分法找两个数分别和i,j去比较
int x = target - nums[i];
int y = target - nums[j];//这两个的作用就是后面写起来可以清晰一点,把一个长的用一个符号来代替
for (int k = i + 1, m = j - 1; k <= m; k++, m--) {
result[0] = i;
if (nums[k] == x) {
result[1] = k;
return result;
} else if (nums[m] == x) {
result[1] = m;
return result;
}
result[1] = j;
if (nums[k] == y) {
result[0] = k;
return result;
} else if (nums[m] == y) {
result[0] = m;
return result;
}
}
}
return result;
}
}
*/
//解法五:二分加Map
//其实一开始没想到用Map,因为以往做ACM的时候,一般来说不让直接使用标准库里的东西,这本是一个搜索,前面的四分法能满足要求,额外再实现一个Map,不太划算。
//注意,题目中说返回结果不要求有一定的顺序,这就暗示了,可以使用Map来做内层的搜索,外层仍要遍历,内层用Map来搜索,整体效率会达到O(n)。
//同时,受四分大法启发,外层主loop,其实仍可以用二分大法,这样时间复杂度又提高到O(n/2)。
/*
public class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = {0, 1};
if (nums.length <= 2) {
return result;
}
Map<Integer, Integer> valueToIndex = new HashMap<>();
for (int i = 0, j = nums.length - 1; i <= j; i++, j--) {
if (nums[i] + nums[j] == target) {
//这种情况是第一个和最后一个恰好就是要的两个数
result[0] = i;
result[1] = j;
break;
}
int x = target - nums[i];
if (valueToIndex.containsKey(x)) {
result[0] = i;
result[1] = valueToIndex.get(x);
break;
}
x = target - nums[j];//这个比解法四还要省了一个变量的空间
if (valueToIndex.containsKey(x)) {
result[0] = j;
result[1] = valueToIndex.get(x);
break;
}
//如果没找到,就把这个数存进哈希表,避免重复的数字使用
valueToIndex.put(nums[i], i);
valueToIndex.put(nums[j], j);
}
return result;
}
}
}
6、收获
- 这种求和的题目,有数组的,可以想到暴力解决,用循环进行遍历
- 这种题目的目的,其实是找一个x并判断对应的target-x是否存在,这种理解题目的思路是我从没有过的,要学习
- 正是通过上面的那种理解,可以发现暴力法的问题在于寻找的过程是遍历全部,时间复杂度高,所以采用创建哈希表的方式,这种方式可以将两层for循环的O(n^2)降低到O(n)。原理是将所有数组元素先放进哈希表,这样是n个数据,遍历放进去是O(n),而查的过程,不用遍历去找,哈希表是可以直接根据键值对进行定位的,所以整体的查找过程是O(n)的时间复杂度,但是多以哈希表的存储空间,所以是用空间换时间,空间复杂度由O(1)变为了O(n)。这种用空间换时间的思路要学会。
- 对于HashMap和containsKey()和get()有了更深入的理解,注意谁是键谁是值,这个是在存储的时候确定了的
- 对于return也有了新认识,需要返回多个数值的时候,可以使用数组来进行,而且return new int[]{里面是数组的内容},直接就可以返回{内容},不需要给数组起一个名字的。如本题就是 return new int[]{i, j};
- 写程序的思路:输入输出类型,循环边界,特殊的情况,然后才是具体的方法使用
- 有序:对撞指针或者二分法,无序:哈希表
- 发现一道题的解法太多了,还有没收录进来的 排序加数据封装加折半查找 这样的方法,可以以后再看
- return new int[0]; 标准的返回空数组的写法
- return new int[]{i, j}; 直接返回一个数组以及相应内容的写法
- Map<Integer, Integer> valueToIndex = new HashMap<>(); 标准的哈希表创建代码
2、有效的括号(栈,哈希表)
1.问题描述
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
2.示例
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
3.提示
1 <= s.length <= 10^4
s 仅由括号 ‘()[]{}’ 组成
4.具体解法(栈加哈希表判断,栈加直接判断,栈加ASCII值判断,repalce方法解决)
//方法一:使用栈加哈希表的思路
/*
public class Solution {
public boolean isValid(String s){
int n=s.length();
if(n%2==1){//如果字符串长度是奇数,那肯定是不能匹配的
return false;
}
Map<Character,Character> pairs=new HashMap<>();//创建一个HashMap来存储每一种括号,键是右括号。值是左括号
pairs.put(')','(');
pairs.put(']','[');
pairs.put('}','{');//为什么键是右括号呢,后面分析
//将括号存储好后,就可以将字符串进栈进行判断了
//字符串每个字符依次进栈,只要遇到左括号就入栈
//遇到右括号就从栈中取出一个进行匹配(用哈希表),依次类推,直到字符串遍历完毕,栈中无括号,则是有效的
Deque<Character> stack=new LinkedList<>();
for (int i=0;i<n;i++){
//charAt(i),返回字符串指定索引处的char值,序列的第一个char值是索引0 ,下一个索引为1 ,一次类推,像数组索引一样
char ch=s.charAt(i);
//containsKey的作用是判断哈希表中是不是有这个键(也就是右括号),如果有,说明此时判断的字符是右括号
if(pairs.containsKey(ch)){
//peek()方法是只查看,不从栈中取出
//pop()是查看并从栈中取出
if(stack.isEmpty()||stack.peek()!=pairs.get(ch)){
return false;//已知是右括号了(因为有这个键),如果此时栈是空的,或者对应的左括号不匹配就说明不匹配
}
else{
stack.pop();//如果不属于前面的情况,那说明右括号和左括号匹配上了,那么将栈中的左括号拿出来即可
}
}
else{
stack.push(ch);//如果通过containsKey发现不存在这个键,那么说明是左括号,那么就应该入栈
}
}
return stack.isEmpty();//字符串全遍历完毕了,如果栈是空的那么就是匹配好了,如非空,就没匹配好。所以用isEmpty就正好对应了这个返回值的需求
}
}
*/
//方法二:是使用了栈而没用哈希表的一种方法,效率优于使用哈希表
//直接就是用获得的字符进行判断,省空间,时间复杂度是一样的
/*
class Solution {
public boolean isValid(String s) {
Stack<Character> stack=new Stack<>();//这个就是用Stack类创建的栈,而不是用Deque
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(c=='('||c=='['||c=='{'){
stack.push(c);
}//如果是左括号,那么push进栈
else
if(stack.isEmpty()||c==')'&&stack.pop()!='('||c==']'&&stack.pop()!='['||c=='}'&&stack.pop()!='{'){
//如果不是左括号,那么说明是右括号
//此时如果栈是空的,或者此时判断的右括号是),而栈顶的元素不是(,或者}{][不能匹配到,那么就输出false
return false;
}
}
return stack.isEmpty();
}
}
*/
//方法三:使用replace方法的巧妙思路,效率低,但是代码很短小精悍
//replace(s1,s2),用s2串去替换所有的s1串
/*
class Solution {
public boolean isValid(String s) {
while(true){
int len=s.length();
s=s.replace("()","");//用空字符替换匹配的括号
s=s.replace("{}","");
s=s.replace("[]","");
//执行完一轮之后,如果长度变了,说明有匹配的,所以返回去再做赋值和匹配。
//如果没变,那说明没有一组可以匹配的了,此时判断字符串的长度是不是0,如果是0,那么会返回true
if(s.length()==len){
return len==0;
}
}
}
}
*/
//方法四:使用ASCII加字节数组进行判断
//其实就是将字符类型通过数字来进行判断了,而转换的途径就是ASCII
//而且,匹配的括号应该相差的ASCII值是1或者2,由此可以进行括号是否匹配的判断
/*
class Solution {
public boolean isValid(String s) {
byte[] s_byte = s.getBytes();//字符串转化为byte数组,即对应字符的ASCII码表。
Stack<Byte> stack = new Stack<>();//创建栈。
byte symbol = 0;//这个目标一直是栈顶的那个元素,等待被消除
if(s.length() % 2 != 0){
return false; //奇数个字符直接返回false。
}
for(int i = 0; i < s.length(); i++){
if(s_byte[i] == 40 || s_byte[i] == 91 || s_byte[i] == 123){
//下一个字符为(、{、[之一时,压栈。
stack.push(s_byte[i]);//被消除者为(、{、[ ,将这些字符压入栈中。
symbol = s_byte[i]; //修改消除目标为当前最新压入的被消除者。
}
else if(s_byte[i] == (symbol + 1) || s_byte[i] == (symbol + 2)){
//下一个字符为 )、}、]之一时,弹栈。
stack.pop();
symbol = stack.empty() ? 0 : stack.peek(); //消除成功,更换新的消除目标。
}
else{
return false;
}
}
while(stack.empty()){ //结束且栈中无剩余。
return true;
}
return false; //栈中有剩余。
}
}
*/
//解法五:ASCII值加字符数组
//类似于方法四,只不过更简洁一点,采用的s.toCharArray()将s变成字符数组进行判断,而方法四是变成字节数组
class Solution {
public boolean isValid(String s) {
if (s.isEmpty()) return true;//if的代码块只有一句,可以不加大括号
//一种全新的判断是不是偶数的方式,跟1按位与,结果是1则为奇数,底层逻辑应该是二进制
if ((s.length() & 1) == 1) return false;
//发现这几种方法中,创建栈的方式各种各样,都可以,新手不要拘束,各自优点可以以后慢慢了解
Deque<Character> stack = new ArrayDeque<>();
//s.toCharArray()是将串s变成字符数组
for (char ch : s.toCharArray()) {
// '(',')','{','}','[',']' 的 ASCII 码分别是 40、41、91、93、123、125
if (ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
} else if (stack.isEmpty() || Math.abs(ch - stack.pop()) > 2) {
return false;
}
}
return stack.isEmpty();
}
}
5.收获
-
对于每一道题都应该想到他的特殊情况,比如这道题,奇数不可能匹配,那就为我们的遍历省下了一半的工作
-
学会了栈的定义方式以及pop,push,peek方法的使用,pop是出栈,peek是查看,push是入栈
-
了解了关于Deque接口取代Stack做栈的创建的思想,可以见零散知识点,具体可以见另一篇文章https://blog.csdn.net/humor2020/article/details/123654251
-
匹配的括号应该相差的ASCII值是1或者2,这个结论要了解。
-
一种全新的判断是不是偶数的方式,跟1按位与,结果是1则为奇数,底层逻辑应该是二进制(位与:全1为1)
-
对于containsKey和containsValue方法的理解,是用来判断表中是不是有这个键或者值,而不是判断键或值对应的值或键是否匹配
-
复习了字符串的charAt方法。charAt(i),返回字符串指定索引处的char值,序列的第一个char值是索引0 ,下一个索引为1 ,一次类推,就像数组索引一样
-
复习了字符串的replace方法。s=s.replace(s1,s2),得用一个字符串对象去调用,用s2串去替换s中的所有的s1子串
-
学到的解题思路有:哈希表去存储键值对做匹配,栈的先进后出思路使用,ACSII差值进行判断匹配
-
return stack.isEmpty();或者是return len==0;这样的写法是比较巧妙的,应该学会,用逻辑表达式的值来返回ture或者false,就不用我自己多写一步赋值了,直接返回。
-
Deque stack=new LinkedList<>();
Deque stack = new ArrayDeque<>();
Stack stack=new Stack<>();
以上三种都是创建栈的写法,建议使用第一个,另外两个也不是不可以
3、合并两个有序链表(递归,迭代)
1.问题描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
2.示例

示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
3.提示
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
4.具体解法(递归,迭代)
解法一的辅助理解图:

//解法一:使用递归的方式,下面的图非常清晰的展示了关系。
//两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。
//如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
/**
* 这是链表的内容,展示在这里可以方便去调用
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2){
//我好像明白了为什么必须方法名叫mergeTwoLists,因为我们写的类是解决问题的类,而调用的测试类应该在网页的后台,是写好的,如果不按着人家给的规定好的来写,那么肯定会报错。
if (l1 == null){
return l2;
}
else if(l2 == null){
return l1;//如果s1或者s2是空的,那么就返回非空的那个就ok了
}
else if (l1.val<l2.val){
l1.next=mergeTwoLists(l1.next,l2);
//一开始都是在头结点的位置上,如果l1的头结点小于l2的头结点,那么就让小的那个结点指向剩余部分(继续调用递归)
//然后就是l1的下一个结点跟l2的头结点开始比较了,以此类推的去调用,直到有一个串移动到空的位置了
//这个时候递归调用就会进入到上面那两个if的情况之一,然后就有了返回值,这个返回值再给调用者,就会将所有结点开始连起来了
return l1;//链接完毕后,因为最小的节点是l1的头结点,所以返回l1就是整个连接好的新链
}
else{
l2.next=mergeTwoLists(l1,l2.next);
return l2;//这个就是最小的结点是l2的头结点,所以返回的是这个结点
}
}
//解法二:使用迭代的方式
//我们可以用迭代的方法来实现上述算法。当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//创建一个链表对象,传递的参数是结点的val,next:默认为null,这是一个参数的构造方法
ListNode prehead = new ListNode(-1);
//设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表
//最后的返回值是 preHead.next,也就是说这个哑节点的初始值大小并不重要,
//因为在最后并不会返回它,返回的是它后面链表的头节点preHea.next
//这里可以是-1 也可以是任何值,不影响返回结果。
ListNode prev = prehead;
//这句话的意思是prev和prehead都指向同一个结点,后面prev会不停的移动(游标),那么最一开始的头结点就找不到了,
//所以用prehead定位所以叫哨兵,
//而且是只跟prev的第一个结点指向同一个,后面prev.next再去指向新的,就不会影响prehead了
// prehead头结点是为了记录链表起始位置,prev是要往下next遍历的,一个是游标一个是哨兵。
//只要两个链表有一个为空就跳出循环
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {//开始比较两个链表的头结点
prev.next = l1; // prev.next代表链表的下一个结点是l1的当前结点
l1 = l1.next;//被操作的链表就指向下一个结点继续重复操作
}
else {
prev.next = l2;//与上面的同理
l2 = l2.next;
}
prev = prev.next;
//因为前面有prev.next=l1这样的操作,所以我们再把这个操作的节点往后移动一位,再继续去指新的
//我指向了你,接下来该由你去指向别的结点了。
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 == null ? l2 : l1;
return prehead.next;//返回的是链表所指的下一个结点,恰恰就是第一次比较时两个链表的头结点中小的那个,也是整个新链表中的头结点
}
}
5.收获
-
在链表的题目中,这些l1,l2之类的都是链表的结点,类型是ListNode,不要简单的当成一个变量
-
我理解的递归,首先是递归调用同一个函数,我最外层的函数输出需要用到再调用这个函数去计算的结果,直到某个特殊值,会直接返回一个结果,然后层层返回,最后得到最外层的输出结果
-
第一层龟的输入,是第二层龟的输出,如此类推。
-
对于链表的定义有了新的认识,需要去自定义一个ListNode类,但好像IDEA里自动就有,在力扣里也不需要去定义,更多关于链表的了解可以看另一篇文章https://blog.csdn.net/humor2020/article/details/123683086
-
对于哨兵结点的使用有了新认识,链表题目时经常用到的写法,由于在对链表进行重新排列、打断、合并等等操作时,链表的头节点往往会发生移动变得“破朔迷离”,故在一开始我们设定一个哨兵节点,这可以在最后让我们比较容易地返回合并后的链表。具体使用可以看另一篇文章https://blog.csdn.net/humor2020/article/details/123683097
-
对于迭代的操作也有了新的理解:是一种重复反馈的过程,是一种不断用变量的旧值递推新值的过程,以这道题为例子,就是我指向你了,然后你接替我的身份,你再继续去指,直到满足某种条件停止,最后我们就形成了一个长长的链。
-
见识到了新名词“哑结点”,“游标结点”,“哨兵结点”。
-
ListNode prehead = new ListNode(-1); 创建哑结点的标准代码,执行new ListNode(-1),就是创建一个对象{val: -1, next: null}
-
ListNode prev = prehead; 一定要掌握这个,这个是使pre 和prehead指向同一个结点。常用于一个作为游标,一个作为哨兵。
4、最大子数组和(动态规划,分治,贪心)
1.问题描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
2.示例
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
3.提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
4.进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
5.具体解法(暴力循环,贪心算法,动态规划,分治法)
方法一:暴力循环(不能AC,因为最后一个数组特大,运行时间超出要求)
//自己独立完成的代码,使用了暴力循环的方式,调试了三次终于从只能通过1/3用例到2/3到几乎全完成,到最后一次是超长数组通过不了(超出时间限制)
//对于调试代码有了新的理解和经验,一种是自身逻辑入手,一种是跟随失败案例循环一遍自己的代码看哪里哪一步出现了问题
//对于最后的超出时间,已经是超出我的目前水平了,那个数组得有好几千上万个数据,我这种暴力循环的思路,应该是不能解决这种问题的,而且自己写的是暴力循环中的比较笨的
//逻辑不够清晰,只能缝缝补补似的完成需求,不过这是个好的开始
//自己想出来的最简单的暴力遍历,我们以四个数字为例,所有的连续子数组情况无非是(1,12,123,1234,2,23,234,3,34,4),
所以我们去按照这种思路遍历数组即可,定义一个max存储当前的最大值,
用s去存储每一次计算的新的和,跟max比较,s>max就把s赋值给max,直到遍历结束,返回max
/*
class Solution {
public int maxSubArray(int[] nums) {
//因为我后面是比较每个值跟他的大小,他来存储最大值,有一种数组里面全是负数,那么这个max的初始值就不适合设为0
//后面也学到了,这里可以使用Integer的成员变量:MIN_VALUE,是-(2的31次方),没有比他更小的了。避免像-1000这种又不行。
int max=-999;//最后要返回的最终的最大值
int s=0;//这个s是临时的最大值
for(int i=0;i<nums.length;i++){
if(i==nums.length-1){//如果是最后一个数,因为我会计算它加他后面的数,如果是最后一个数,那就没有它后面的数了,做加法也是不合理的,会报错数组越界,所以我单独把这个拿出来进行判断了
s=nums[nums.length-1];
if(s>max){
max=s;
}
}
else{
int k=nums[i];//这个k的作用是存储刚刚加过的数的和,防止出现每次都是挨着的两个数相加,而不是全部数相加的情况
for(int j=i;j<nums.length-1;j++){
if(nums[j]>max){
max=nums[j];//这里需要单独判断一下是不是第一个数就大于max,要不然如果是[2,-1]的这种情况,那么就会忽略掉max=2
}
s=k+nums[j+1];//因为j=i,而且i判断过了不是数组最后一个数,所以是可以确定必然有nums[j+1]的
k=s;
if(s>max){
max=s;
}
}
}
}
return max;//当所有的都遍历完成,那么就返回此时的最大值max
}
}
*/
//补上一个可以成功提交的暴力解法
//可以对比一下,自己写的有多差
//通过这个可以发现,如果我把我的那个int s=0;写在第一层循环内,第二层循环外,而不是写在第一层循环外的话,就不用去再创建一个k了
//而且人家是加nums[j],所以也不用特别去判断一下最后一个数的情况
/*
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0];
int n = nums.length;
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum > max){
max = sum;
}
}
}
return max;
}
}
*/
//方法二:动态规划
//首先得去理解这种思路:找每个数作为结尾的数字的情况下的最大值即可
//用f(i)来表示以i为结尾的最大值
//为什么呢,以[1,2,3,4]为例,以1为结尾的就是[1],以2为结尾的可以是[1,2]或者[2],而[1,2]可以看做是问题1加上数字2
//再来一个例子,就更清晰了,以3为结尾的是[1,2,3]或[2,3]或[3],可以看做是问题2的两种情况加上数字3
//我们用一个变量pre来维持f(i) 动态规划状态转移式: f(i) = max{f(i-1) + num,num},我们还需要一个变量来存储接结果返回值
/*
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);//这个是用来算每一个数做结尾的时候的最大值
maxAns = Math.max(maxAns, pre);//这个是比较i做结尾的最大值和当前的最大值,取大的作为新的最大值
}
return maxAns;
}
}
*/
//方法三:贪心算法
//动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
//如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
//如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
//每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
//其实这道题可以这么想:
//1.假如全是负数,那就是找最大值即可,因为负数肯定越加越大。
//2.如果有正数,则肯定从正数开始计算和,不然前面有负值,和肯定变小了,所以从正数开始。
//3.当和小于零时,这个区间就告一段落了,然后从下一个正数重新开始计算(也就是又回到 2 了)。而 dp 也就体现在这个地方。
//这个理解也很好,就是我前面的sum如果小于零,那再去加新的数字肯定是不好的,所以直接把它舍掉,从最新的数字这里是最大值继续
//也有人说这种叫贪心,方法二才算dp(动态规划)
//贪心或者说也是一种动态规划的一个代码
/*
class Solution {
public int maxSubArray(int[] nums) {
int ans = nums[0];
int sum = 0;
for(int n

974

被折叠的 条评论
为什么被折叠?



