Java力扣热门题目简单部分合集(共23道)


前言

涵盖热门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.示例

img

示例 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.具体解法(递归,迭代)

解法一的辅助理解图:

img

//解法一:使用递归的方式,下面的图非常清晰的展示了关系。

//两个链表头部值较小的一个节点与剩下元素的 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值