手撕代码题型汇总

前言

如何准备面试手撕代码题

如果你是转专业,或者计算机专业但之前没有参加过acm这种赛事,对于算法代码就比较普通的选手(没错 说的就是我,应该也是大部分选手现状了)。我推荐你可以按我整理的这12篇博客的顺序来刷。时间充足的话,每个类型里面所有题目都刷完,时间不充足的话,确保每个类型的题目至少能会个两三道。

本篇专栏的作用:主要是将高频的面试手撕代码真题进行了一个总结,并划分了11个分类,大家可以根据我这个专栏的1~11博客来依次刷,帮助大家节省了去找题目的一个过程,同时题目后面配有我自己写的Java版的代码解析,部分重点内容还有我自己学习过程中参考的一些学习视频链接。如果学有余力,可以再刷leetcode hot 100中不包含在本专栏中的题目。

刷题总的思路:

1 没思路的直接去看我题解(Java选手的话),题解没理解,就把题目和题解扔给gpt让他给你详细解释一下,基本就能懂了,因为后面一题不止刷一遍,请相信自己的脑子,它会记住这些。

2 从第二天开始,每天上午复习前一天刷过的新题,复习完后再开始刷接下来新的题 (比如我第二天,复习了第一天刷的题,然后同时新刷了10道题。那再第三天我只需要复习第二天新刷的那十道题,然后再接着刷新题就行了)

我是如何准备

准备时间:10月16日 - 10月21日 共经历五天

手撕代码准备:我是参考一些评论,qq群,牛客上的帖子,把题目整理了一下,大概得有个六十来道的样子,然后按顺序刷,刷完后按照题型进行一个分类,按照分类又刷了一遍。

八股文准备:[Java面试题介绍 | 小林coding (xiaolincoding.com)](https://xiaolincoding.com/interview/spring.html) 直接看这个八股文,然后我也把牛客上od面经常问到的八股总结了一下 ,两个搭配使用

day1: 一整天下来大概刷了13道题左右,第一天效率不是很高 可以理解

day2:上午把day1刷的题重刷一遍,下午和晚上接着刷剩下的题(下午和晚上加起来刷了得有14道题左右)

day3:上午把day2新刷的题重刷一遍,下午和晚上开始刷剩下的题(下午和晚上加起来刷了得有18道题左右),

day4:上午把day3新刷的题重刷一遍,下午和晚上开始刷剩下的题(下午和晚上加起来刷了得有12道题左右),刷完还有点时间开始看八股,八股文用的是小林八股文,并且搭配牛客上一些华为od面经来使用(这个我也有总结,后续可以分享出来)

day5:花一整天时间,从头到尾,把所有题目刷一遍,这个刷的时候就很快了,并且刷完一个按分类来记录下来(这个记录过程就是我这个专栏博客的雏形)。同时晚上看八股

day6:上午用整理的笔记,按题型分类把手撕代码的真题都看一遍。下午有空的时候看看八股,然后就是静候面试

以上的准备过程只是我个人的一个过程,针对像我这样的普通人,如果acm选手肯定不用这样!!!

字符串,String,char[]

将string转成char[] 方便根据下标进行反转操作

然后再将反转后的char[] 通过new String(arr)的方式 构成成一个新的String

 char[] arr = s.toCharArray();
        for (int i = 0; i < n; i += 2 * k){
            if (i + k <= n){ // 反转i到i+k 字符
                reverse(arr, i, i + k - 1); // [] 区间
            }else{ // 否则则到末尾 且小于k个字符 将剩余字符全部反转
                reverse(arr, i, n - 1);
            }
        }
        return new String(arr);

常规字符串,数组,矩阵

1 实现超长数字减1

  • 思路:Java中用BigInteger类
public String subOne(String s){
	BigInteger bi = new BigInteger(s);
    bi = bi.subtract(BigInteger.ONE);
    return bi.toString();
}

2 十八进制数比较大小

任意进制的字符串a,转成十进制的数 : Integer aTen = Integer.valueOf(a, 任意进制)

十进制的数aTen,转成任意进制的字符串 :String str = Integer.toString(aTen, 任意进制)

public String compareTenEight20241020(String a, String b){
        Integer aten = Integer.valueOf(a, 18);
        Integer bten = Integer.valueOf(b, 18);
        return aten > bten ? a : b;
 }

3 最长公共前缀

14. 最长公共前缀 - 力扣(LeetCode)

思想:取第一个字符串作为前缀,然后遍历数组中的每个字符串,不断缩小前缀长度,直到它匹配字符串的前缀为止

public String longestCommonPrefix(String[] strs) {
        int n = strs.length;

        String prefix = strs[0];

        for (int i = 1; i < n; i ++){
            String cur = strs[i];
            //如果当前字符串不是以prefix开头,则不断缩小prefix的长度,直到它匹配当前字符串的前缀为止
            while (cur.indexOf(prefix) != 0){ 
                prefix = prefix.substring(0, prefix.length() - 1);
            }
        }
        return prefix;
    }

4 八进制求和

67. 二进制求和 - 力扣(LeetCode)

不难想到用Integer类提供的方法,但当a,b过长超出Integer型范围时会报错。

public String addBinary(String a, String b) { //会超时
       int aten = Integer.valueOf(a,2);
       int bten = Integer.valueOf(b,2);

       int sum = aten + bten;

       return Integer.toString(sum,2);
    }

还得用常规运算

//二进制求和   
public String addBinary(String a, String b) {
        int aLen = a.length();
        int bLen = b.length();
        
        int i = aLen - 1;
        int j = bLen - 1;
        
        StringBuilder sb = new StringBuilder();


        int jw = 0; //表示进位
        
        //从a,b末尾开始运算 
        while (i >= 0 || j >= 0){
            int sum = jw;
            //若a还有数字
            if (i >= 0){
                sum += a.charAt(i) - '0';
                i --;
            }
            //若b还有数字
            if (j >= 0){
                sum += b.charAt(j) - '0';
                j --;
            }
            //考虑进位
            jw = sum / 2;
            sb.insert(0,sum % 2);

            if (i < 0 && j < 0 && jw > 0){
                sb.insert(0,jw);
            }
        }
        return sb.toString();
    }

八进制求和代码类似上面二进制求和

 //八进制求和
    public static String addEightJZ(String a, String b){
        int aLen = a.length();
        int bLen = b.length();

        int i = aLen - 1;
        int j = bLen - 1;

        int jw = 0; //表示进位

        StringBuilder sb = new StringBuilder();

        while (i >=0 || j >= 0){
            int sum = jw;
            if (i >= 0){
                sum += a.charAt(i) - '0';
                i --;
            }
            if (j >= 0){
                sum += b.charAt(j) - '0';
                j --;
            }

            //sum % 8表示当前位置值, sum / 8表示进位
            jw = sum / 8; //进位
            sb.insert(0, sum % 8);

            //如果当前是最高位,且进位大于0 则把进位也加进去
            if (i < 0 && j < 0 && jw > 0){
                sb.insert(0,jw);
            }
        }
        return sb.toString();
    }

5 O(n)时间内求和为target两个数

1. 两数之和 - 力扣(LeetCode)

思路:用hashmap

    public int[] twoSum(int[] nums, int target) {
        int n = nums.length;

        //map存放<nums[i],i> 也就是<值,下标>
        HashMap<Integer,Integer> map = new HashMap<>(); 

        for (int i = 0; i < n; i ++){
            int cur = nums[i];
            int other = target - nums[i];
		   //看other是否在map中 在则直接返回
            if (map.containsKey(other)){
                return new int[]{i,map.get(other)};
            }
            map.put(cur, i);
        }
        return new int[2];
    }

6 判断回文串

125. 验证回文串 - 力扣(LeetCode)

public boolean isPalindrome(String s) {
        //将所有大写字母转为小写字母
        s = s.toLowerCase();
    	//知识点:s.toUpperCase();//将所有小写字母转为大写字母
    
        //将所有非小写字母 数字字符替换成空字符(移除所有非小写字母 数字字符)
        s = s.replaceAll("[^0-9a-z]","");

        int n = s.length();

        int left = 0, right = n - 1;

        while(left < right){
            char lc = s.charAt(left);
            char rc = s.charAt(right);

            if (lc != rc) return false;
            
            left ++;
            right --;
        }
        return true; 
    }

7 字母异位词分组

49. 字母异位词分组 - 力扣(LeetCode)

    public List<List<String>> groupAnagrams(String[] strs) {
        int n = strs.length;

        //map中 key为排序后的字符串,value排序前的字符串
        Map<String, List<String>> map = new HashMap<>();

        for (int i = 0; i < n; i ++ ){
            String str = strs[i];

            //将str进行排序变成key的过程
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            // String key = chars.toString(); 这个不行
            String key = new String(chars); //这个可以

            //若map包含key 则将str直接加入
            if (map.containsKey(key)){
                map.get(key).add(str);
            }
            else{ //否则新建一个list,将str加到list 组成键值对<key ,list>加到map中
                List<String> list = new ArrayList<>();
                list.add(str);
                map.put(key, list);
            }
        }
        return new ArrayList<>(map.values());
    }

8分发糖果

135. 分发糖果 - 力扣(LeetCode)

思路:两次循环,第一次从左到右遍历 若右边的孩子比左边孩子评分高,则右孩子糖数为左孩子糖+1,否则为1。第二次从右到左遍历 若左边孩子比右边孩子评分高,则左边孩子糖 = max(左孩子第一轮糖,右孩子糖 + 1)

    public int candy(int[] ratings) {
       int n = ratings.length;
       int[] candy = new int[n]; //candy[i] 表示第i个孩子收到的糖数
       candy[0] = 1; //初始化

       int cnt = 0; //用来统计糖总数
       
       //从左往右遍历 若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1,否则右孩子糖数为1
       for (int right = 1; right < n; right ++){
            int leftCandy = candy[right - 1];
            int leftRate = ratings[right - 1];
            int rightRate = ratings[right];

            if (rightRate > leftRate) {
                candy[right] = leftCandy + 1; //若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1
            }else{
                candy[right] = 1;//否则右孩子糖数为1
            }
       }

       //从右往左遍历 若左孩子评分比右孩子高 左孩子糖数 = max(左孩子糖数,右孩子糖数 + 1)
       for (int left = n - 2; left >= 0; left --){
            int rightRate = ratings[left + 1];
            int rightCandy = candy[left + 1];
            int leftRate = ratings[left];

            if (leftRate > rightRate){
                candy[left] = Math.max(candy[left], rightCandy + 1);
            }
       }
        
        //统计糖总数
        for (int a : candy){
            cnt += a;
        }
        return cnt;
    }

9 计算字符串的数字和

2243. 计算字符串的数字和 - 力扣(LeetCode)

public String digitSum(String s, int k) {
        //递归终止条件
        if (s.length() <= k) return s;
        int n = s.length();
        StringBuilder sb = new StringBuilder(); //记录变化中的字符串

        //i以k位间隔
        for (int i = 0; i < n; i += k){
            int num = 0;
            for (int j = 0; j < k && i + j < n; j ++){
                char c = s.charAt(i + j);
                num += c - '0';
            }
            sb.append(num);
        }
        return digitSum(sb.toString(), k);
    }

2 在排序数组中查找元素的第一个和最后一个位置

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

   public int[] searchRange(int[] nums, int target) {
       int n = nums.length;
       int start = binsearch(nums, target);
        
        if (start == n || nums[start] != target){
            return new int[]{-1, -1};
        }
        int end = binsearch(nums, target + 1) - 1;
        
        return new int[]{start,end};
    }
	//闭区间二分查找
    public int binsearch(int[] nums, int target){
        int n = nums.length;
        int left = 0, right = n - 1;
        
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return left; 
    }
    

10 字符串压缩

面试题 01.06. 字符串压缩 - 力扣(LeetCode)

public String compressString(String S) {
        int n = S.length();

        //用来记录字符以及次数
        Map<Character, Integer> map = new HashMap<>();

        StringBuilder sb = new StringBuilder();


        for (int i = 0; i < n; i ++){
            char c = S.charAt(i);
            map.put(c, map.getOrDefault(c, 0) + 1);

            if (i > 0){ //i > 0确保左边有字符
                char lc = S.charAt(i - 1); //拿到左边字符
                if (lc != c){ //若c左边的字符不等于c 则将左边的字符以及个数存到sb中
                    sb.append(lc).append(map.get(lc));
                    map.put(lc,0);
                }
            }
            //若到了最后字符
            if (i == n - 1){
                sb.append(c).append(map.get(c));
            }
        }
        //sb S 哪个短返回哪个
        return sb.length() < n ? sb.toString() : S;
    }

11 判断是ipv4还是ipv6地址

468. 验证IP地址 - 力扣(LeetCode)

正常解法

 public String validIPAddress(String queryIP) {
       if (isIPv4(queryIP)){
            return "IPv4";
       }else if (isIPv6(queryIP)){
            return "IPv6";
       }else{
            return "Neither";
       }
    }
    
    public boolean isIPv4(String ip){
        String[] split = ip.split("\\.");
        //若分割后的数组长度不为4,则不是ipv4地址
        if (split.length != 4 || ip.charAt(ip.length() - 1) == '.'){
            return false;
        }
        //遍历分割后的每个数,是否符合Ipv4的要求
        for (String s : split) {
            //IP为172.16.254.1 则s分别代表172 16 254 1

            //1、若s的长度为0或者大于3,则不是ipv4地址
            if (s.length() == 0 || s.length() > 3){
                return false;
            }

            //2、判断首位0是否合理。172.16.254.01 不是ipv4地址,但172.0.0.1是ipv4地址
            if (s.charAt(0) == '0' && s.length() != 1){
                return false;
            }

            //3、判断是否为数字
            for (int i = 0; i < s.length(); i++) {
                if (!Character.isDigit(s.charAt(i))){
                    return false;
                }
            }

            //4、判断是否在0~255之间(经3之后得知是数字)
            int sint = Integer.parseInt(s);
            if (sint < 0 || sint > 255){
                return false;
            }
        }

        //若上述都满足则返回true
        return true;
    }

    public boolean isIPv6(String ip){
        if (ip.contains("::")){
            return false;
        }
        String[] split = ip.split(":");
        //若分割后的数组长度不为8,则不是ipv6地址
        if (split.length != 8 || ip.charAt(ip.length() - 1) == ':'){
            return false;
        }
        //遍历分割后的每个数,判断每个数的长度是否小于等于4,是否为16进制数
        for (String s : split) {
            //"2001:0db8:85a3:0:0:8A2E:0370:7334" 是ipv6地址
            //s分别代表2001 0db8 85a3 0 0 8A2E 0370 7334
            //1、若s的长度大于4,则不是ipv6地址
            if (s.length() > 4){
                return false;
            }

            //2、判断是否为16进制数
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                if (!Character.isDigit(c) && (c < 'a' || c > 'f') && (c <'A' || c > 'F')){
                    return false;
                }
            }
        }
        return true;
    }

正则表达式解法 (如何写出来的待学习)

  public String validIPAddress(String queryIP) {
        String ipv4Pattern = "((2(5[0-5]|[0-4]\\d))|(1([0-9][0-9]))|[1-9][0-9]?|[0-9])(.((2(5[0-5]|[0-4]\\d))|(1([0-9][0-9]))|[1-9][0-9]?|[0-9])){3}";
        String ipv6Pattern = "([0-9a-fA-F]{1,4})(:[0-9a-fA-F]{1,4}){7}";

        if(queryIP.indexOf(".") > 0 && (queryIP.matches(ipv4Pattern))){
            return "IPv4";
        }
        if(queryIP.indexOf(":") > 0 && (queryIP.matches(ipv6Pattern))){
            return "IPv6";
        }
        return "Neither";
    }

12旋转矩阵旋转90度

面试题 01.07. 旋转矩阵 - 力扣(LeetCode)

思路:先转置,再沿着中间列翻转

public void rotate(int[][] matrix) {
        int n = matrix.length;

        //1转置 a[i][j] = a[j][i]
        for (int i = 0; i < n; i ++){
            for (int j = 0; j < i; j ++){
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = tmp; 
            }
        }

        //2 沿着中间列倒转 a[i][j] = a[i][n - j - 1]  当j=0时 a[i][0] = a[i][n - 1]
        for(int i = 0; i < n; i ++){
            for (int j = 0; j < n / 2; j ++){
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[i][n - j - 1];
                matrix[i][n - j - 1] = tmp;
            }
        }
    }

数学题

0 求1~n的最小公倍数

思路:先将1~n的数存入数组arr中,遍历arr数组,将arr数组中的数进行约分,将约分后的arr数组中所有数相乘存放到BigInteger

public static BigInteger getMinGBNumberSelf(int n){
        //定义数组arr,存放1~n的数
        int[] arr = new int[n + 1];
        //初始化arr数组,将1~n的数存入arr数组
        for (int i = 1; i <= n ; i++) {
            arr[i] = i;
        }
        //遍历arr数组,将arr数组中的数进行约分
        for (int i = 1; i < n; i++) {
            for (int j = i + 1; j <= n; j++) {
                int a = arr[i];
                int b = arr[j];
                if (b % a == 0){
                    arr[j] = b / a;
                }
            }
        }
        //使用BigInteger存储最小公倍数,防止越界
        BigInteger minGB = new BigInteger("1");
        for (int i = 1; i <= n; i++) {
            minGB = minGB.multiply(BigInteger.valueOf(arr[i]));
        }
        return minGB;
    }

1 求两数a,b的最小公倍数

  • 思路: a,b的最大公倍数 = a * b / gcd(a, b) 这里gcd(a,b)表示a,b的最大公因数。gcd通过辗转相除法代码很简单如下
public int maxGGNum(int a, int b){
        return a * b / gcdHere(a,b);
}
public int gcd(int a, int b){ //可令a=12 b=8代入模拟下
    if (b == 0) return a;
    else return gcd(b, a % b);
}

2 最简分数

1447. 最简分数 - 力扣(LeetCode)

public List<String> simplifiedFractions(int n) {
    List<String> list = new ArrayList<>();

    //分母
    for (int i = 2; i <= n; i ++){
        //分子
        for (int j = 1; j < i; j ++){
            if (gcd(i, j) == 1){
                String str = j + "/" + i;
                list.add(str);
            }
        }
    }
    return list;
}
public int gcd(int a, int b){
    if (b == 0) return a;
    else return gcd(b, a % b);
} 

3 求n中素数的个数

题目:给定整数n,返回 所有小于非负整数的素数的数量。

注意:素数是只能被1和自身整除的正整数

public static int countPrimes20241020(int n){
    if (n < 2) return 0; //当n小于等于2时,没有素数
    int cnt = 0; //记录素数个数
    for (int i = 2; i < n; i ++ ) {
        if (isPrime20241020(i)){
            cnt ++;
        }
    }
    return cnt;
}
//判断num是否是素数
public static boolean isPrime20241020(int num){
    if (num < 2) return false;

    for (int i = 2; i * i <= num; i ++){
        if (num % i == 0) return false;
    }
    return true;
}

4 判断一个数能否分解为几个连续自然数之和

思路:

若存在数a 是的 a+0 a+1 a+2 … a +(n-1) = num

则推出 num = n * a + n * (n-1)/2

则推出 n * a = num - n * (n-1)/2 把n*a记为remain

我们的思路就是计算式子中 n * a的值称为remain 然后判断remain能否整出n, 能说明存在数a使得num可以分解为n个连续自然数之和

public boolean isPossibleDivide20241020(int num) {
        if (num <= 2) return false; //当num小于等于2时无法分解为多个连续自然数之和

        //奇数一定可以分解成连续两个数之和 如15 = 7 + 8
        if (num % 2 == 1) return true;

        for (int n = 3; n < num ; n ++){
            int remain = num - (n * (n - 1)) / 2;

            if (remain % n == 0) {
                System.out.println(remain / n + " " + n); //表示从 remain/n开始 有n个连续自然数之和为num
                return true;
            }
        }
        return false;
    }

二分查找

1 非减序列查找目标值

image-20241020142027933

思路:直接使用闭区间二分查找,闭区间二分查找优点,若查找的数不在,返回的是插入位置

public int searchBin20241020(int[] nums, int target){
        int n = nums.length;
        int left = 0, right = n - 1;

        while (left <= right){
            int mid = left + (right - left) / 2;
            
            if (nums[mid] < target){
                left = mid + 1;
            }else {
                right = mid - 1;
            }
        }
        return left;
    }

2 在排序数组中查找元素的第一个和最后一个位置

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

这里可以看一下:二分查找 红蓝染色法_哔哩哔哩_bilibili 这位up的讲解,讲的很到位 ,看完能知道 >= > < <= 之间的转换。对这题至关重要

public int[] searchRange(int[] nums, int target) {
        int start = binsearch(nums, target); //>= 注意整数中 >x 可转成 >=x+1;<x 可转成 (>=x)-1; <=x 可转成 (>x)-1 ==> (>=x+1)-1
        if (start == nums.length || nums[start] != target){
            return new int[]{-1,-1};
        }
        int end = binsearch(nums, target + 1) - 1; //<=
        return new int[]{start, end};
    }
    public int binsearch(int[] nums, int target){
        int left = 0, right = nums.length - 1;
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return left;
    }

链表

1 单链表相交

160. 相交链表 - 力扣(LeetCode)

解法一 指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历。如果 pA 到了末尾,则 pA = headB 继续遍历、如果 pB 到了末尾,则 pB = headA 继续遍历。如此 长度差就消除了

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p = headA;
        ListNode q = headB;

        while(p != q){
            if (p == null) {
                p = headB;
            }else{
                p = p.next;
            }
               
            if (q == null){
                q = headA;
            }else{
                q = q.next;
            }
        }
        return p == null ? null : p;
    }

解法二 先得到headA,headB的长度,让长的先走长度差的单位,然后再一起走并判断节点是否相同。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;

        ListNode p = headA;
        ListNode q = headB;

        int lenA = 0;
        int lenB = 0;
        //获取链表长度
        while (p != null){
            p = p.next;
            lenA ++;
        }
        while (q != null){
            q = q.next;
            lenB ++;
        }

        //p,q重新指向链表头
        p = headA;
        q = headB;
        if (lenA >= lenB){
            int n = lenA -lenB;
            while (n -- > 0){ //让p先走n步
                p = p.next;
            }
        }else {
            int n = lenB -lenA;
            while (n -- > 0){ //让q先走n步
                q = q.next;
            }
        }

        //然后一起走
        while (p != null && q != null){
            if (p == q) return p;
            p = p.next;
            q = q.next;
        }
        return null;
    }

2 判断链表是否有环

思路:双指针

876. 链表的中间结点 - 力扣(LeetCode)

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

876 题解

 public ListNode middleNode(ListNode head) {
       ListNode slow = head;
       ListNode fast = head;
	//fast != null && fast.next != null 最后slow指的就是中点(偶数的话中偏右)
       while (fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
       }
       return slow;
   }

141 题解

 public boolean hasCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;

        while (fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }

142题解

public ListNode detectCycle(ListNode head) {
       ListNode slow = head;
       ListNode fast = head;

       while (fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast){ //达到相交点后 head到环头的长度等于快慢指针相交点到环头的距离

                while (head != slow){
                    head = head.next;
                    slow = slow.next;
                }
                return slow;
            }
       }
       return null;
    }

二叉树

1 二叉树层次遍历

102. 二叉树的层序遍历 - 力扣(LeetCode)

思路:用一个队列,队列中保存的是同层所有节点,先让root进队列(第一层只有root),然后while(队列不为空的情况下),一直从队列中取出同层节点,再将该节点对应的下次节点入队。由于一开始拿的个数是int size = qu.size(); 规定好了个数,所以不会发生跨层拿节点的问题。

 public List<List<Integer>> levelOrder(TreeNode root){
		List<List<Integer>> lists = new ArrayList<>();

        if (root == null) return lists;

        Queue<TreeNode> qu = new LinkedList<>();

        qu.add(root);

        while (qu.size() > 0){
            //当前层的节点个数
            int size = qu.size();
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                TreeNode node = qu.poll();
                list.add(node.val);
                if (node.left != null) qu.add(node.left);
                if (node.right != null)qu.add(node.right);
            }
            lists.add(list);
        }

        return lists;
    }

可用层次遍历解决的题目

2 二叉树中的最大路径和

124. 二叉树中的最大路径和 - 力扣(LeetCode)

class Solution {
    int sum = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return sum;
    }

    public int dfs(TreeNode root){
        if (root == null) return 0;
        
        //递归计算左右子节点的最大贡献值,只有在最大贡献值大于0时,才会选取对应子节点
        int leftV = Math.max(dfs(root.left), 0);
        int rightV =  Math.max(dfs(root.right),0);
		
        //更新最大路径
        sum = Math.max(root.val + leftV + rightV, sum);
        //返回当前节点的最大贡献值
        return root.val + Math.max(leftV, rightV);
    }
}

传统双指针

1 三数之和

15. 三数之和 - 力扣(LeetCode)

public List<List<Integer>> threeSum(int[] nums) {
    //对数组nums排序
    Arrays.sort(nums);
    //lists保存结果
    List<List<Integer>> lists = new ArrayList<>();
    int n = nums.length;
    
    for (int i = 0;i < n; i ++){
        int cur = nums[i];

        //如果cur大于0,则直接退出循环 
        if (cur > 0) break;

        //如果cur与之前一个元素相同,则跳过
        if (i > 0 && cur == nums[i - 1]) continue;

        //转成在[i+1,n-1]中求两数之和为target的问题
        int target = -nums[i];
	    //定义左右指针
        int left = i + 1, right = n - 1;
        while (left < right){
            int lNum = nums[left];
            int rNum = nums[right];
            if (lNum + rNum == target){
                List<Integer> list = new ArrayList<>();
                list.add(cur);list.add(lNum);list.add(rNum);
                lists.add(list);
                
                //接着左指针右移 右指针左移 同时要跳过相同的数
                while(left + 1 < right && nums[left + 1] == lNum) left ++;
                while(left < right - 1 && nums[right - 1] == rNum) right --;
                left ++;
                right --;
            }else if (lNum + rNum > target){
                right --;
            }else{
                left ++;
            }
        }
    }
    return lists;
}

2 最长回文字符串

5. 最长回文子串 - 力扣(LeetCode)

思路**:两重for**循环 拿到所有可能长度的子串 根据下标判断子串是否回文串 若是判断是否需要更新 最长回文子串的[begin,end)

暴力解,用到了双指针

public String longestPalindrome(String s) {
    int n = s.length();

    char[] chars = s.toCharArray();
    //记录最长回文子串的长度
    int max = 0;

    //记录最长回文子串的起始下标
    int begin = 0;
    int end = 0;

    //i遍历所有起始位置   [i,j]
    for(int i = 0; i < n; i ++) {
        //j遍历所有结束位置
        for (int j = i; j < n; j ++){

            //若s下标[i,j]是回文串 
            if (isHW(chars,i,j)){
                //判断是否需要更新
                if (j - i + 1 > max){
                    max = j - i + 1;
                    begin = i;
                    end = j + 1;
                }
            }
        }
    }
    return s.substring(begin, end);
}

public boolean isHW(char[] chars,int left,int right){
    while(left <= right){
        if (chars[left] != chars[right]) return false;
        left ++;
        right --;
    }
    return true;
}

3 判断回文串

125. 验证回文串 - 力扣(LeetCode)

public boolean isPalindrome(String s) {
        //将所有大写字母转为小写字母
        s = s.toLowerCase();
        //将所有非小写字母 数字字符替换成空字符(移除所有非小写字母 数字字符)
        s = s.replaceAll("[^0-9a-z]","");

        int n = s.length();

        int left = 0, right = n - 1;

        while(left < right){
            char lc = s.charAt(left);
            char rc = s.charAt(right);

            if (lc != rc) return false;
            
            left ++;
            right --;
        }
        return true; 
    }

4 回文子串

本人二面手撕代码原题,最优解要用到动态规划思想,我没有想出来,本人最后写出暴力解如下,暴力解思路就是拿到所有子串,判断子串是否是回文串,是数量就+1

647. 回文子串 - 力扣(LeetCode)

    public int getSumHw(String s){
        int n = s.length();
        char[] chars = s.toCharArray();

        int cnt = 0; //记录回文子串的数量

        for(int start = 0; start < n; start ++){
            for (int end = start + 1; end <= n; end++) {

                //判断s中[start,end) 是否是回文串
                if (isHW(chars, start, end)){
                    cnt ++;
                }
            }
        }
        return cnt;
    }
	
	//判断chars数组中[start,end-1]下标组成的字符串是否是回文串
    private boolean isHW(char[] chars, int start, int end) { 
        int left = start, right = end - 1;
        while (left < right){
            if (chars[left] != chars[right]) {
                return false;
            }
            left ++;
            right --;
        }
        return true;
    }

滑动窗口

思路: 连续 最大 长度 要想起滑动窗口

可以去看下这个up讲滑动窗口的视频,我就是看了他讲的,讲的很不错:滑动窗口【基础算法精讲 03】_哔哩哔哩_bilibili

1 最长不重复子串

3. 无重复字符的最长子串 - 力扣(LeetCode)

    public int lengthOfLongestSubstring(String s) {
       int n = s.length();
       //左指针
       int left = 0;
       //最长子串的长度
       int maxLen = 0;
        //定义map存放[left,right]区间中各个字符出现的个数
        HashMap<Character, Integer> map = new HashMap<>();
        for (int right = 0; right < n; right++) {
            //右指针指向的字符
            char rightChar = s.charAt(right);
            //存到map中,该字符出现的次数加1
            map.put(rightChar, map.getOrDefault(rightChar, 0) + 1);

            //map.get(rightChar) > 1 表示右字符在前面接上的字符串中已经出现过,
            //此时需要移动左指针
            while (map.get(rightChar) > 1){
                //左指针指向的字符
                char leftChar = s.charAt(left);
                //将左指针指向的字符在map中的个数减1
                map.put(leftChar, map.get(leftChar) - 1);
                //同时左指针左移一个单位
                left++;
            }
            //上述while之后,得到的[left,right]区间中的字符串是不重复的
            maxLen = Math.max(maxLen, right - left + 1);
        }
        return maxLen;
    }

2 长度最小的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

public int minSubArrayLen20241016(int target, int[] nums) {
        int n = nums.length;
        int minLen = Integer.MAX_VALUE;
        int sum = 0;
        int left = 0;

        for (int right = 0; right < n; right ++){
            //加入当前right值到sum中
            sum += nums[right];

            //若此时sum大于target,则向右移动左指针尝试找到长度最小的总和大于等于target的子数组
            while (sum >= target){
                minLen = Math.min(minLen, right - left + 1);
                sum -= nums[left];
                left ++;
            }
        }
        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }

3 乘积小于 K 的子数组

713. 乘积小于 K 的子数组 - 力扣(LeetCode)

    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) return 0;
        int n = nums.length;
        //左指针
        int left = 0;
        //乘积初始化时要为1而不是0
        int sum = 1;
        //符合要求的数组个数
        int cnt = 0;

        for (int right = 0; right < n; right++) {
            sum *= nums[right];
            //当乘积大于等于k时,左指针右移,直到符合要求(乘积小于k)
            while (sum >= k){
                //当不符合要求时,左指针右移
                sum /= nums[left];
                left++;
            }
            //当右节点为right,此时符合要求的数组个数如下式
            cnt += right - left + 1;
        }
        return cnt;
    }

4 最长的连续绿色衣服士兵

题目(华为可信科目二):一队士兵排成一排,身穿绿色跟黑色衣服,如果可以随便将一个士兵移除队伍,那么求最长的连续绿色衣服的士兵队伍长度,示例:绿色表示1,黑色表示0

输入:10111

输出: 4

表示移出第二个位置的黑色士兵,最大连续长度为4.

public static int longestOnes(int[] nums){
    int n = nums.length;
    int cnt0 = 0; //表示滑动窗口里的0的个数
    int max1 = 0; //表示最大连续1的个数
    int left = 0; //左指针

    //最长可翻转1 也就是变成了求滑动窗口内0的数量小于等于1时最大滑动窗口大小
    for (int right = 0; right < n; right++) {
        int rNum = nums[right];
        if (rNum == 0) cnt0 ++;

        //当cnt0 滑动窗口左移
        while (cnt0 > 1){
            int lNum = nums[left];
            if (lNum == 0) cnt0 --;
            left ++;
        }

        max1 = Math.max(max1, right - left + 1);
    }

    return cnt0 == 1 ? max1 - 1 : max1;
}

5 最大连续1的个数|||

1004. 最大连续1的个数 III - 力扣(LeetCode)

思路:转换为求滑动窗口内0的个数小于等于k时,滑动窗口的最大值

    public int longestOnes(int[] nums, int k) {
        //思路 转换成当cnt0 <= k时,滑动窗口长度最大值

        int n = nums.length;
        int cnt0 = 0; //表示当前滑动窗口内0的个数
        int maxlen = 0; //表示当前最长的滑动窗口
        int left = 0;

        for (int right = 0; right < n; right ++){
            int rNum = nums[right];
            if (rNum == 0) cnt0 ++;

            //若前面操作使得cnt0 > k,则需要让left右移,直到cnt0 <= k
            while (cnt0 > k){
                int lNum = nums[left];
                if (lNum == 0) cnt0 --;
                left ++;
            }
            //经上面操作后 cnt0 <= k
            //判断是否更新maxlen。 right - left + 1为当前滑动窗口大小与max1比较,取最大值
            maxlen = Math.max(maxlen, right - left + 1);
        }
        return maxlen;
    }

6 加油站

134. 加油站 - 力扣(LeetCode)

思路:目的 找到以left开始 长度为n的滑动窗口即是正确的流程,用rest来表示剩余油量

 public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;

        int rest = 0; //表示剩余汽油量 
        int left = 0; //左指针

        //目的 找到以left开始 长度为n的滑动窗口即是正确的流程
        for (int right = 0; right - left < n; right ++){
            rest += gas[right % n] - cost[right % n]; //因为right可能回调 从 4调到1

            //若当前滑动窗口 剩余油量小于0说明 不能以该left为起点 让left右移
            while (rest < 0 && left < n){
                rest -= gas[left] - cost[left];
                left ++;
            }

        }
        return rest >= 0 ? left : - 1;
    }

7 求字符串中同一字母连续出现的最大次数

题目描述: 给你一个字符串,只包含大写字母,求同一字母连续出现的最大次数。

例如”AAAABBCDHHH”,同一字母连续出现的最大次数为4,因为一开始A连续出现了4次。

ADDDDBDDDCAAAAAAA 7

这是本人一面手撕代码原题

public static int maxLX(String s){
        int n = s.length();
        int max = 0;
        int left = 0;
        char win = s.charAt(0); //初始 将滑动窗口内的字符设置为最左边的字符
        for (int right = 0; right < n; right++) {
            char rChar = s.charAt(right); 
            while (rChar != win){  //若右字符与滑动窗口内字符不一致 
                char lChar = s.charAt(left);
                if (lChar == win) left ++; //若当前左指针指向的字符和win相同 则让左指针右移
                else { //否则更新滑动窗口字符以及左指针位置
                    win = rChar;
                    left = right;
                }
            }
            max = Math.max(right - left + 1, max);
        }
        return max;
    }

8 找最大连续子串 (遇到换题)

题目(华为可信科目二):一串字符串由"1-10"“A-Z"组成,各字符之间由”"隔开,找到最大连续的子串。(10后面的字符是A)

输入:1,2,3,4,5,7,8 输出1 2 3 4 5

这题,我做了挺久但苦于没有输入输出用例,所以想着如果遇到了就换题

1 Linux路径处理

原题:71. 简化路径 - 力扣(LeetCode)

思路:
1.使用栈来存储路径

2.遍历路径,遇到**…则弹出栈顶元素,遇到.**或者空字符串则跳过,其他情况则入栈

3.最后将栈中元素用**/*连接起来

public String simplifyPath(String path) {
    //将path用"\"切割
    String[] sp = path.split("/");

    Deque<String> stack = new ArrayDeque<>();
    for(String str : sp){
        if (str.equals("..")){ //当前字符串为".." 若栈不为空则栈顶出栈一个元素
            if(!stack.isEmpty())stack.pop();
        }else if(str.equals(".") || str.equals("")){ //若当前字符为"." 或是""空字符串(切割可能产生空字符串) 则跳过
            continue;
        }else{ //入栈
            stack.push(str);
        }
    }

    //用栈底开始出栈,拼接到sb中
    StringBuilder sb = new StringBuilder();

    while(!stack.isEmpty()){
        sb.append("/").append(stack.pollLast());
    }
    return sb.length() == 0 ? "/" : sb.toString();
}

2 小行星碰撞

735. 小行星碰撞 - 力扣(LeetCode)

 public int[] asteroidCollision(int[] asteroids) {
        ArrayDeque<Integer> stack = new ArrayDeque<>();
        int n = asteroids.length;


        for(int i = 0; i < n; i ++){
            int cur = asteroids[i];
            boolean alive = true; //表示cur是否存在,默认为true

            //栈不为空,且cur与栈顶元素方向相反 发生碰撞
            while (alive && !stack.isEmpty() && (cur < 0 && stack.peek() > 0)){
                int absCur = Math.abs(cur);
                int absPeek = Math.abs(stack.peek());

                if (absCur < absPeek){//若cur的绝对值< peek,则碰撞后cur不存在, peek仍然存在
                    alive = false;
                }
                else if(absCur > absPeek){ //若cur的绝对值 > peek 则碰撞后cur存在,peek不存在
                    alive = true;
                    stack.pop();
                }else{ //若cur绝对值 == peek 则两者都不存在
                    alive = false;
                    stack.pop();
                }  
            }

            if (alive){
                stack.push(cur);
            }
        }

        int[] ans = new int[stack.size()];

        int j = 0;
        while (!stack.isEmpty()){
            ans[j] = stack.pollLast();
            j ++;
        }
        return ans;
    }

3 括号内字符串翻转

1190. 反转每对括号间的子串 - 力扣(LeetCode)

思路:

左括号,则将之前准备好的sb入栈,并重置sb

右括号 将当前sb翻转,并出栈一个字符串插到翻转后的sb最前面

    public String reverseParentheses(String s) {
        int n = s.length();

        StringBuilder sb = new StringBuilder();
        Deque<String> stack = new ArrayDeque<>();

        for (int i = 0; i < n; i ++){
            char c = s.charAt(i);

            if (c == '('){ //入栈'('之前的sb,然后重置sb
                stack.push(sb.toString());
                sb = new StringBuilder();
            }else if (c == ')'){//翻转')'之前的sb,并将栈顶元素出栈插到翻转后sb最前面
                sb.reverse();
                sb.insert(0, stack.pop());
            }else{ //普通字符 则加入sb
                 sb.append(c);
            }
        }
        return sb.toString();
    }

4 有效的括号

20. 有效的括号 - 力扣(LeetCode)

思路:遇到左括号则入栈,遇到右括号则出栈(出栈时需进行括号匹配)

  public boolean isValid(String s) {
       int n = s.length();

       Deque<Character> stack = new ArrayDeque<>();
       
       for (int i = 0; i < n; i++){
            char c = s.charAt(i);

            //当c为左括号 入栈
            if (c == '(' || c == '{' || c =='['){
                stack.push(c);
            }else{//当c为右括号,进行括号匹配
                
                //如果此刻栈为空,则没有左括号与该右括号匹配 返回false
                if (stack.isEmpty()) return false;

                //先将栈顶元素拿到(不是出栈)
                char peek = stack.peek();
                
                //括号匹配方法1,左右括号是同一类型时,匹配失败,返回false
                if (peek == '(' && c != ')') return false;
                if (peek == '[' && c != ']') return false;
                if (peek == '{' && c != '}') return false;
                //到这就是匹配成功 出栈
                stack.pop();
                
                //括号匹配方法2,左右括号是同一类型时,匹配成功,栈顶元素(左括号)出栈,若一直没匹配到最后返回false
//                if (peek == '(' && c == ')'){
//                    //匹配成功,栈顶元素出栈
//                    stack.pop();
//                }
//                if  (peek =='{' && c == '}'){
//                    stack.pop();
//                }
//                if (peek == '[' && c == ']'){
//                    stack.pop();
//                }
//                return false;
                
            }
       }
       //最后栈是空 才表明括号匹配全部成功,否则表示有括号匹配失败 返回false
        return stack.isEmpty() ? true : false;
    }

单调栈

1 移除k位数字,求最小数 (遇到换题)

402. 移掉 K 位数字 - 力扣(LeetCode)

思路:通过单调栈,保证每次新加入的字符都比前面的字符小,遇到大的字符则移除,这样可以形成最小的数。代码通过遍历和判断来移除尽量大的数字,并在必要时移除队列尾部的元素。

public String removeKdigits(String num, int k) {
        if (num.length() <= k) return "0";

        //使用双端队列来保存可能的最小数字
        LinkedList<Character> deque = new LinkedList<>();
        int n = num.length();

        //遍历num中的每个字符
        for (int i = 0; i < n; i++) {
            char c = num.charAt(i);
            //当队列不空且 k > 0 且队尾字符大于当前字符时,移除队尾字符
            while (!deque.isEmpty() && k >0 && deque.peekLast() > c){
                deque.pollLast(); //移除队尾元素
                k --; //移除的字符数减少一个
            }
            //将当前字符添加到队列的末尾
            deque.offerLast(c);
        }
        //如果还没够删够k位数字,继续删除队列尾部的字符
        for (int i = 0; i < k; i++) {
            deque.pollLast();
        }

        //构建最终结果
        StringBuilder sb = new StringBuilder();
        boolean leadingZero = true; //是否遇到前导零
        while (!deque.isEmpty()){
            char digit = deque.pollFirst(); //取出队列中的第一个字符
            //如果遇到前导零,则跳过
            if (leadingZero && digit == '0'){
                continue;
            }
            leadingZero = false; //一旦遇到非零字符,就不再是前导零
            sb.append(digit); //将非前导零字符添加到结果中
        }
        //如果结果为空,则返回0
        return sb.length() == 0 ? "0" : sb.toString();
    }

深搜dfs,广搜bfs

1 岛屿数量

200. 岛屿数量 - 力扣(LeetCode)

思路:若当前是岛屿,岛屿数+1,然后调用dfs进行深搜并置为0

public int numIslands(char[][] grid) {
    int n = grid.length; //行
    int m = grid[0].length; //列

    int cnt = 0; //记录岛屿数

    for (int i = 0; i < n; i ++){
        for(int j = 0; j < m; j ++){
            if (grid[i][j] == '1'){ //当前岛屿为1时
                cnt ++;
                dfs(grid,i,j);
            }
        }
    }
    return cnt;
}

public void dfs(char[][]grid, int i, int j){
    int n = grid.length; //行
    int m = grid[0].length; //列

    grid[i][j] = 0; //把该岛屿置为0

    //上 grid[i - 1][j]
    if (i - 1 >= 0 && grid[i - 1][j] == '1'){
        dfs(grid, i - 1, j);
    }

    //下 grid[i + 1][j]
    if (i + 1 <= n - 1 && grid[i + 1][j] == '1'){
        dfs(grid, i + 1, j);
    }

    //左 grid[i][j - 1]
    if(j - 1 >= 0 && grid[i][j - 1] == '1'){
        dfs(grid, i, j - 1);
    }

    //右 grid[i][j + 1]
    if (j + 1 <= m - 1 && grid[i][j + 1] == '1'){
        dfs(grid, i ,j + 1);
    }
}

2 岛屿的周长

463. 岛屿的周长 - 力扣(LeetCode)

思路:依次判断每个岛的上下左右四边是否临海,临海则周长加1,本人觉得这样也很好理解

public int islandPerimeter(int[][] grid) {
        int n = grid.length; //行
        int m = grid[0].length; //宽
        int cnt = 0;//临近海的总边数 == 周长

        for (int i = 0; i < n; i ++){
            for (int j = 0; j < m; j ++){
                int cur = grid[i][j]; //当前
                if (cur == 1){ //若当前是岛屿
                    int lines = 4; //一个岛屿最多能贡献4个边长(四面临海的情况下)

                    //上 grid[i - 1][j]
                    if (i - 1 >= 0 && grid[i - 1][j] == 1) lines --; //若上面是岛则贡献边减1
                    //下 grid[i + 1][j]
                    if(i + 1 <= n - 1 && grid[i + 1][j] == 1) lines--; //若下面是岛则贡献边减1
                    //左 grid[i][j - 1]
                    if(j - 1 >= 0 && grid[i][j - 1] == 1) lines--;
                    //右 grid[i][j + 1]
                    if(j + 1 <= m - 1 && grid[i][j + 1] ==1) lines --;

                    cnt += lines;
                }
            }
        }
        return cnt;
    }

贪心

局部最优 推出全局最优

比如从一堆钱中拿十张,求拿到的总和最大。 局部最优就是每次都拿最大面值的钱 --> 凑一起就是全局最优总和最大

题目如下

455. 分发饼干 - 力扣(LeetCode)

376. 摆动序列 - 力扣(LeetCode)

动态规划,回溯,贪心

这类题型是我的薄弱点,我好多也不会,感觉需要花比较久的时间才能掌握,时间允许的话读者可以看从这里开始看下去 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili

但我因为时间有限,有的一下理解不了的,我就想着遇到了就换题。

1 最大数组和

lc 53 最大子数组和 https://leetcode.cn/problems/maximum-subarray/description/

  • 思路:一开始看题目求最大,连续,想到滑动窗口。

    但实际上该题不能直接通过滑动窗口的方法来解决,原因在于这道题,我们不明确知道最佳的窗口长度是多少,且子数组的大小和位置是动态变化的,而滑动窗口通常适用于固定长度的窗口,或者窗口的变化与某些条件相关。

a 贪心解法:若当前指针所指元素之前的和小于0,则丢弃当前元素之前的数列

public int maxSubArray(int[] nums) {
    //贪心思想 若当前指针所指元素之前的和小于0,则丢弃当前元素之前的数列
    int n = nums.length;
    int preSum = 0; //之前元素的和
    int maxSum = Integer.MIN_VALUE;

    for(int i = 0; i < n; i ++){
        int cur = nums[i];

        if(preSum < 0){
            preSum = cur;
        }else{
            preSum += cur;
        }
        maxSum = Math.max(preSum, maxSum);
    }
    return maxSum;
}

b 动态规划解法

核心思想是:

  • 维护一个变量 currentSum 来记录当前的子数组和;
  • 每次遍历到一个新的元素时,判断是要将其加入现有子数组,还是重新开始一个新的子数组;
  • 维护另一个变量 maxSum 来记录全局最大子数组和。

动态规划的具体步骤:

  1. 初始化 currentSum = nums[0]maxSum = nums[0]
  2. 从数组第二个元素开始,逐个遍历数组:
    • 对于每个元素,更新 currentSum = max(currentSum + nums[i], nums[i]),即判断是继续加入之前的子数组,还是从当前位置重新开始计算子数组。
    • 更新 maxSum = max(maxSum, currentSum),记录当前出现的最大和。
  3. 返回 maxSum 作为结果。
 public int maxSubArray(int[] nums) {
     int currentSum = nums[0];
     int maxSum = nums[0];

     for (int i = 1; i < nums.length; i++) {
         // 更新当前的子数组和,决定是继续加还是从当前元素重新开始
         currentSum = Math.max(currentSum + nums[i], nums[i]);
         // 更新全局最大子数组和
         maxSum = Math.max(maxSum, currentSum);
     }

     return maxSum;
    }

2 换零钱

322. 零钱兑换 - 力扣(LeetCode)

思路:dp[i] 表示凑i元钱需要的最少硬币数, 当j硬币可以用来凑i元钱时,dp[i] = Math.min(dp[i], dp[i - coinNum] + 1);

public int coinChange(int[] coins, int amount) {
        int k = coins.length;
        int[] dp = new int[amount + 1];
        
        Arrays.fill(dp, amount + 1);

        dp[0] = 0; //表示凑0元钱最少需要0个硬币


        //遍历从1到amout元钱 的凑法
        for (int i = 1; i <= amount; i ++){ 
            for (int j = 0; j < k; j ++){
                int coinNum = coins[j]; //表示j硬币的面值
                
                //如果j硬币的面值小于i元钱,则说明可以尝试用j硬币来凑i元钱
                if (coinNum <= i){
                    //i - coinNum表示用j硬币凑i元 剩下的钱,dp[i - coinNum]表示凑剩下的钱需要的硬币数,+ 1是因为用了一个j硬币 
                    //因此dp[i - coinNum] + 1表示为凑剩下来的钱需要的硬币数加上用的一个j硬币
                    dp[i] = Math.min(dp[i], dp[i - coinNum] + 1);
                } 
            }
        }
        //若dp[amount] > amount 则表示凑不成
        return dp[amount] > amount ? -1 : dp[amount];
    }

2 硬币兑换 (遇到换题)

这题就是本人一面遇到的第一个题目,然后本人一下想不起来就让面试官换题了

题目:在一个国家仅有1分,2分,3分硬币,将钱N兑换成硬币有很多种兑法。请你编程序计算出共有多少种兑法。

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int[] coins = {1, 2, 3};
    int Max = 32768; //题目规定的N的上限

    //dp[i] 表示凑成i元钱需要的最少硬币数
    int[] dp = new int[Max + 1];
    //初始化
    dp[0] = 1;// 表示凑0元钱的方法只有1种,就是什么也不凑

    //动态规划计算所有可能的兑换方式
    for (int coin : coins) {
        for (int i = coin; i <= Max; i ++){
            
            dp[i] = dp[i] + dp[i - coin];
        }
    }

    //处理输入
    while (sc.hasNextInt()){
        int N = sc.nextInt();
        //输出换N元钱有多少个兑换方式
        System.out.println(dp[N]);
    }
    sc.close();
}

时间复杂度:O(N * m) N是要换的钱,m是硬币的种类数

空间复杂度:O(N),只需要一个大小为 N 的数组来存储 dp 值。

3 给定一个字符串,判断是否能够分为三个回文子串 (遇到换题)

1745. 分割回文串 IV - 力扣(LeetCode)

时间原因,没有理解这道题,想着遇到就换题。

4 切割回文串最小切割次数 (遇到换题)

132. 分割回文串 II - 力扣(LeetCode)

时间原因,没有理解这道题,想着遇到就换题。

5 救生艇

881. 救生艇 - 力扣(LeetCode)

贪心思想:先对数字排序,然后尝试让最轻的人和最重的人坐一个船,若能坐下则一起坐一个船,否则最重的人单独坐一条船

public int numRescueBoats(int[] people, int limit) {
    Arrays.sort(people);

    int n = people.length;
    int left = 0, right = n - 1;
    int cnt = 0; //cnt用来记录要用多少搜救生艇

    while(left <= right){
        int lv = people[left];
        int rv = people[right];

        if (lv + rv <= limit){//若两个人的重量小于等于船最大承载
            cnt ++; //两个人用一条船
            left ++; 
            right --;
        }else{ //若两个人的重量大于船最大承载
            cnt ++; //右边重量大的人用一条船
            right --;
        }
    }
    return cnt;
}

6 单词拆分 (遇到换题,考前可看看)

139. 单词拆分 - 力扣(LeetCode)

public boolean wordBreak(String s, List<String> wordDict) {
        //思路 用dp[i]表示s中[0,i-1] i前面字符能否由字典中的单词组成

        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true; //空字符串能由字典中的单词组成

        Set<String> set = new HashSet<>(wordDict);

        //由子串[start,end) 推出 dp[end]
        for (int end = 1; end <= n; end ++){ 
            for (int start = 0; start < end; start ++){
                String substr = s.substring(start, end);
                 //检查子串s[j,i]是否在字典中,并且dp[j]是否为true
                if (dp[start] == true && set.contains(substr)){ 
                    dp[end] = true;
                    break;
                }
            }
        }
        return dp[n];
    }

7分发糖果

135. 分发糖果 - 力扣(LeetCode)

思路:两次循环,第一次从左到右遍历 若右边的孩子比左边孩子评分高,则右孩子糖数为左孩子糖+1,否则为1。第二次从右到左遍历 若左边孩子比右边孩子评分高,则左边孩子糖 = max(左孩子第一轮糖,右孩子糖 + 1)

    public int candy(int[] ratings) {
       int n = ratings.length;
       int[] candy = new int[n]; //candy[i] 表示第i个孩子收到的糖数
       candy[0] = 1; //初始化

       int cnt = 0; //用来统计糖总数
       
       //从左往右遍历 若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1,否则右孩子糖数为1
       for (int right = 1; right < n; right ++){
            int leftCandy = candy[right - 1];
            int leftRate = ratings[right - 1];
            int rightRate = ratings[right];

            if (rightRate > leftRate) {
                candy[right] = leftCandy + 1; //若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1
            }else{
                candy[right] = 1;//否则右孩子糖数为1
            }
       }

       //从右往左遍历 若左孩子评分比右孩子高 左孩子糖数 = max(左孩子糖数,右孩子糖数 + 1)
       for (int left = n - 2; left >= 0; left --){
            int rightRate = ratings[left + 1];
            int rightCandy = candy[left + 1];
            int leftRate = ratings[left];

            if (leftRate > rightRate){
                candy[left] = Math.max(candy[left], rightCandy + 1);
            }
       }
        
        //统计糖总数
        for (int a : candy){
            cnt += a;
        }
        return cnt;
    }

8 字符串所有排列(遇到换题)

输入一个字符串,打印出该字符串中字符的所有排列。可以任意顺序返回这个字符串数组,但里面不能有重复元素。

LCR 157. 套餐内商品的排列顺序 - 力扣(LeetCode)

场景题

1 座位预约管理系统

1845. 座位预约管理系统 - 力扣(LeetCode)

  • 思路: 用最小堆(优先队列来解决)

  • 步骤1:在SeatManager中加入一个优先队列queue;

  • 步骤2:SeatManager(n)方法将1~n个数放入最小堆,表示初始n个座位都可以预约;

  • 步骤3:预约reserve()就是拿出最小堆堆头的元素(座位编号),归还unreserve(seatNumber)就是将seatNumber重新插到最小堆中

#Java
class SeatManager {
    PriorityQueue<Integer> queue;
    public SeatManager(int n) {
        queue = new PriorityQueue<>();
        //将1到n加入优先队列(最小堆)
        for(int i = 1; i <= n; i ++){
            queue.offer(i);
        }
    }
    public int reserve() {
        //预约目前可用最小位置编号座位
        return queue.poll();
    }
    public void unreserve(int seatNumber) {
        //将seatNumber重新插入到优先队列
        queue.offer(seatNumber);
    }
}

2 完美答案 (遇到换题)

暂时没思路 遇到直接让面试官换题

image-20241020084439653

3 乘电梯最小的花费时间 (遇到换题)

暂时没思路 遇到直接让面试官换题

image-20241020100824432

4 实现一个简易版计算器

面试题 16.26. 计算器 - 力扣(LeetCode)

public int calculate(String s) {
    ArrayDeque<Integer> stack = new ArrayDeque<>();
    
    int preSign = '+'; //首个数字前的加号
    int n = s.length();
    int num = 0;

    for (int i = 0; i < n; i++){
        char c = s.charAt(i);
        //若当前字符为数字 则计算
        if (Character.isDigit(c)){
            num = num * 10 + c - '0';
        }
        //若c是运算符 或是最后一个字符
        if (c == '+' || c == '-' || c == '*' || c == '/' || i == n - 1){
            if (preSign == '+'){//若num前的运算符为+ 则把num加入栈中
                stack.push(num);
            }else if (preSign == '-'){//若num前的运算符为- 则把-num加入栈中
                stack.push(-num);
            }else if (preSign == '*'){
                stack.push(stack.pop() * num);
            }else{
                stack.push(stack.pop() / num);
            }
            preSign = c;
            num = 0;
        }
    }
    int sum = 0;
    while(!stack.isEmpty()){
        sum += stack.pop();
    }
    return sum;
}

5 实现备忘录 (遇到换题)

image-20241020171354399

6 LRU缓存

2025/3/5 不用换了

146. LRU 缓存 - 力扣(LeetCode)

用双端队列加map实现
class LRUCache {
    int capacity;

    // 队列中存放key 和map中的key完全保持一致
    Deque<Integer> deque =  new LinkedList<>();
    Map<Integer,Integer> map = new ConcurrentHashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    
    public int get(int key) {
        if (map.containsKey(key)){
            // 进行LRU操作 deque.remove(key)时间复杂度On 
            deque.remove(key);
            deque.offer(key);
            // 返回结果
            return map.get(key);
        }
        return -1;
    }

    public void put(int key, int value) {
        // 若key已存在,则更新value就行
        if (map.containsKey(key)){
            map.put(key,value);

            //LRU
            deque.remove(key);
            deque.offer(key);
        }else{ // 若是新键值对
            // 先判断是否超大小
            if (deque.size() == capacity){
                // 若队列满了 则移除队首 即最近最少使用的key
                int delKey = deque.poll();
                map.remove(delKey);
            }
            map.put(key,value);
            deque.offer(key);
        }
        
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
使用双端队列加map实现的缺点

deque.remove(key) 操作性能问题:在 getput 方法中,使用了 deque.remove(key) 来移除队列中的元素 以保正符合LRU。LinkedList 实现的 Deque 中,remove(Object o) 方法的时间复杂度是 O(n),因为它需要遍历链表来找到要移除的元素。这会导致在频繁调用 getput 方法时,性能下降。

改进方案

可以使用 LinkedHashMap 来替代 DequeHashMap 的组合,LinkedHashMap 可以维护元素的插入顺序或访问顺序,并且在访问元素时可以将其移动到链表尾部,删除元素时可以直接删除链表头部元素,时间复杂度都是 O(1)。

用LinkedHashMap实现

class LRUCache {
    private LinkedHashMap<Integer, Integer> cache;
    private int capacity;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        // 初始化 LinkedHashMap,0.75表示负载因子 
        // 访问顺序标志,true 表示按访问顺序排序,false 表示按插入顺序排序
        this.cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                // 当缓存大小超过容量时,移除最旧的元素
                return size() > capacity;
            }
        };
    }

    public int get(int key) {
        // 若 key 存在于缓存中,则返回其值
        return cache.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        // 若 key 不存在或已存在,都将其放入缓存中
        cache.put(key, value);
    }
}

7 带过期时间的LRU缓存

字节技术二面

实现方式:双端队列+map+线程池 设置过期时间为5秒
class LRUCache {
    // LRU格子数
    int capacity;

    // 双端队列 ,队头表示最先移除的 队尾表示最近使用的
    // offer(队尾进) offerFirst offerLast
    // poll(队头出) pollFirst pollLast
    // remove(key) 删除双端队列中指定元素
    Deque<Integer> deque = new LinkedList<>();

    // hashmap存储键值对
    Map<Integer, Integer> map = new HashMap<>();
    // 存储每个键的过期时间
    Map<Integer, Long> expirationMap = new HashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        // 启动一个定时任务,每秒检查一次过期数据
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleAtFixedRate(this::removeExpiredEntries, 1, 1, TimeUnit.SECONDS);
    }

    // 移除过期的条目
    private void removeExpiredEntries() {
        long currentTime = System.currentTimeMillis();
        List<Integer> expiredKeys = new ArrayList<>();
        for (Map.Entry<Integer, Long> entry : expirationMap.entrySet()) {
            int key = entry.getKey();
            long expirationTime = entry.getValue();
            if (currentTime >= expirationTime) {
                expiredKeys.add(key);
            }
        }
        for (int key : expiredKeys) {
            deque.remove(key);
            map.remove(key);
            expirationMap.remove(key);
        }
    }

    public int get(int key) {
        // 若key存在map中
        if (map.containsKey(key)) {
            // 检查是否过期
            if (System.currentTimeMillis() < expirationMap.get(key)) {
                // 在队列中移除key
                deque.remove(key);
                // 再将key添加到最后,表示最近使用过
                deque.offer(key);
                // 更新过期时间
                expirationMap.put(key, System.currentTimeMillis() + 5000);
                // 返回key的值
                return map.get(key);
            } else {
                // 若已过期,移除该键值对
                deque.remove(key);
                map.remove(key);
                expirationMap.remove(key);
            }
        }
        // 若不存在或已过期 直接返回-1
        return -1;
    }

    public void put(int key, int value) {
        // 若map中包含该key 则更新key
        if (map.containsKey(key)) {
            deque.remove(key);
            deque.offer(key);
        } else {
            // 若队列满了 移除队首元素
            if (deque.size() == capacity) {
                int removedKey = deque.poll();
                map.remove(removedKey);
                expirationMap.remove(removedKey);
            }
        }
        map.put(key, value);
        deque.offer(key);
        // 设置过期时间为当前时间加上5秒
        expirationMap.put(key, System.currentTimeMillis() + 5000);
    }
}
实现方式:LinkedHashMap+线程池 设置过期时间为5秒
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

class LRUCache {
    private LinkedHashMap<Integer, CacheEntry> cache;
    private int capacity;
    private final long EXPIRATION_TIME = 5000; // 过期时间 5 秒

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new LinkedHashMap<Integer, CacheEntry>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, CacheEntry> eldest) {
                return size() > capacity;
            }
        };

        // 启动定时任务,每秒检查一次过期数据
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleAtFixedRate(this::removeExpiredEntries, 1, 1, TimeUnit.SECONDS);
    }

    // 内部类,用于存储值和过期时间
    private static class CacheEntry {
        int value;
        long expirationTime;

        CacheEntry(int value, long expirationTime) {
            this.value = value;
            this.expirationTime = expirationTime;
        }
    }

    // 移除过期的条目
    private void removeExpiredEntries() {
        long currentTime = System.currentTimeMillis();
        // 遍历缓存,找到过期的键并移除
        for (Map.Entry<Integer, CacheEntry> entry : cache.entrySet()) {
            if (currentTime >= entry.getValue().expirationTime) {
                cache.remove(entry.getKey());
            }
        }
    }

    public int get(int key) {
        CacheEntry entry = cache.get(key);
        if (entry != null) {
            if (System.currentTimeMillis() < entry.expirationTime) {
                // 更新过期时间
                entry.expirationTime = System.currentTimeMillis() + EXPIRATION_TIME;
                return entry.value;
            } else {
                // 若已过期,移除该键值对
                cache.remove(key);
            }
        }
        return -1;
    }

    public void put(int key, int value) {
        CacheEntry entry = new CacheEntry(value, System.currentTimeMillis() + EXPIRATION_TIME);
        cache.put(key, entry);
    }
}

测试代码

public class Main {
    public static void main(String[] args) throws InterruptedException {
        LRUCache cache = new LRUCache(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1)); // 返回 1
        cache.put(3, 3); // 该操作会使得密钥 2 作废
        System.out.println(cache.get(2)); // 返回 -1 (未找到)
        cache.put(4, 4); // 该操作会使得密钥 1 作废
        System.out.println(cache.get(1)); // 返回 -1 (未找到)
        System.out.println(cache.get(3)); // 返回 3
        System.out.println(cache.get(4)); // 返回 4
        // 等待 6 秒,让数据过期
        Thread.sleep(6000);
        System.out.println(cache.get(3)); // 返回 -1 (已过期)
        System.out.println(cache.get(4)); // 返回 -1 (已过期)
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值