数构&算法:经典问题


回文串

9. 回文数

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

  • 例如,121 是回文,而 123 不是。

示例 1:

输入:x = 121
输出:true

示例 2:

输入:x = -121
输出:false
解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:

输入:x = 10
输出:false
解释:从右向左读, 为 01 。因此它不是一个回文数。

  • 如果是负数则一定不是回文数,直接返回 false
  • 如果是正数,则将其倒序数值计算出来,然后比较和原数值是否相等
  • 如果是回文数则相等返回 true,如果不是则不相等 false
  • 比如 123 的倒序 321,不相等;121 的倒序 121,相等
class Solution {
    public boolean isPalindrome(int x) {
        if(x < 0) return false;
        String s = x + "";
        StringBuilder sb = new StringBuilder(s);
        return sb.reverse().toString().equals(s);
    }
}


class Solution {
    public boolean isPalindrome(int x) {
        if(x < 0){
            return false;
        }
        int reverse = 0; // 存储翻转后数字
        int num = x; 
        while(num > 0){
            int last_num = num % 10;
            num = num / 10;
            reverse = reverse * 10 + last_num;
        }
        return reverse == x;
    }
}

最小的回文数

现给你一个正整数N,请你找到比N大的最小的那个回文数P。

样例输入

44
3
175

样例输出

55
4
181

866. 回文素数

求出大于或等于 N 的最小回文素数。

回顾一下,如果一个数大于 1,且其因数只有 1 和它自身,那么这个数是素数

例如,2,3,5,7,11 以及 13 是素数。

回顾一下,如果一个数从左往右读与从右往左读是一样的,那么这个数是回文数。例如,12321 是回文数。

示例 1:

输入:6
输出:7

示例 2:

输入:8
输出:11

示例 3:

输入:13
输出:101

564. 寻找最近的回文数 - 力扣(Leetcode)

125. 验证回文串

剑指 Offer II 018. 有效的回文 - 力扣(Leetcode)

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串

字母和数字都属于字母数字字符。

给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false

示例 1:

输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。

示例 2:

输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。

示例 3:

输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。

Character.isLetterOrDigit(int codePoint) 确定指定字符是字母还是数字。如果 isLetter(codePoint) 或 isDigit(codePoint) 对字符返回 true,则认为该字符是字母或数字。

// 两端向中心的双指针算法
class Solution {
    public boolean isPalindrome(String s) {
        if(s == "" || s.length() == 0){
            return true;
        }
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < s.length(); i++){
            char c = s.charAt(i);
            if(Character.isLetterOrDigit(c)) {
                sb.append(Character.toLowerCase(c));
            }
        }
        s = sb.toString();
        int left = 0; 
        int right = s.length() - 1;
        while(left < right){
            if(s.charAt(left) != s.charAt(right)){
                return false;
            }
            left++;
            right--;
        }
        return true; 
    }
}

680. 验证回文串 II

给你一个字符串 s最多 可以从中删除一个字符。

请你判断 s 是否能成为回文字符串:如果能,返回 true ;否则,返回 false

示例 1:

输入:s = "aba"
输出:true

示例 2:

输入:s = "abca"
输出:true
解释:你可以删除字符 'c' 。

示例 3:

输入:s = "abc"
输出:false

class Solution {
    public boolean validPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while(left < right){
            if(s.charAt(left) != s.charAt(right)){
                return isPalindrome(s, left + 1, right)|| isPalindrome(s, left, right - 1);
            }
            left++;
            right--;
        }
        return true;
    }
    public boolean isPalindrome(String s, int left, int right) {
        while(left < right){
            if(s.charAt(left) != s.charAt(right)){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

美团3.18-笔试-回文串

现在小美获得了一个字符串。小美想要使得这个字符串是回文串。小美找到了你。你可以将字符串中至多两个位置改为任意小写英文字符 ’a’ - ‘z’。你的任务是帮助小美在当前制约下,获得字典序最小的回文字符串。
数据保证能在题目限制下形成回文字符串。
输入描述

一行,一个字符串。字符串中仅由小写英文字符构成。
保证字符串不会是空字符串。
字符串长度介于 [ 1 , 100000 ] 之间。

输出描述

一行,一个在题目条件限制下所可以获得的字典序最小的回文字符串。
样例输入: acca
样例输出: aaaa

(26条消息) 美团笔试-3.18_犬兄的海角的博客-CSDN博客

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

亚信安全2024提前批:最长回文子字符串

求最长回文子字符串 (多组回文长度相同则返回第一组),双指针实现

输入:s = “sjdaiwasdesdsfsdsedsaw1h3u238dsahji1”
输出:”wasdesdsfsdsedsaw”

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

方法一:动态规划

回文天然具有「状态转移」性质:一个长度大于 2 的回文去掉头尾字符以后,剩下的部分依然是回文。反之,不是回文。「动态规划」的方法根据这样的性质得到。

dp[i][j] 表示:子串 s[i..j] 是否为回文子串,这里子串 s[i..j] 定义为左闭右闭区间,即可以取到 s[i]s[j]

根据头尾字符是否相等,需要分类讨论:

dp[i][j] = (s[i] == s[j]) and (j - i < 3 || dp[i + 1][j - 1])
  • 当只有一个字符时,比如 a 自然是一个回文串。
  • 当有两个字符时,首尾相等,则必是一个回文串。
  • 当有三个字符时,首尾相等,则必是一个回文串。
  • 当有三个以上字符时,去掉首尾再判断剩下串是否是回文
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        // dp[i][j] 表示:子串 s[i..j] 是否为回文子串
        boolean[][] dp = new boolean[n][n];
        //记录回文长度和起始位置
        int maxLen = 1;
        int begin = 0;
        for (int j = 0; j < n; j++) {
            for (int i = 0; i <= j; i++) {
                //dp[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 3 || dp[i + 1][j - 1]);
                if (s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                }
                //计算最长回文串,只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文
                //对比所有的回文串,取出长度最大的,回文串的起始位置
                if(dp[i][j] && j - i + 1 > maxLen){
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin+maxLen);
    }
}



class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if(n < 2) {
            return s;
        }
        boolean[][] dp = new boolean[n][n];
        char[] charArr = s.toCharArray();
        // 初始化:所有长度为 1 的子串都是回文串
        for(int i = 0; i < n; i++){
            dp[i][i] = true;
        }
        //记录回文长度和起始位置
        int maxLen = 1;
        int begin = 0;
        // 枚举子串长度
        for(int j = 1; j < n; j++){
            for(int i = 0; i < j; i++){
                if(charArr[i] == charArr[j]){
                    //首尾相等且长度为2的是回文
                    if(j - i < 3){
                        dp[i][j] = true;
                    } else{
                        //首尾相等且长度大于2的,去掉首尾再判断
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                } else{
                    //首尾不等的串不是回文
                    dp[i][j] = false;
                }

                //计算最长回文串,只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文
                //对比所有的回文串,取出长度最大的,回文串的起始位置
                if(dp[i][j] && j - i + 1 > maxLen){
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin+maxLen);
    }
}

方法二:中心扩散法

找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧

如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。所以我们可以先实现这样一个函数:

// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
String palindrome(String s, int l, int r) {
    // 防止索引越界
    while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
        // 双指针,向两边展开
        l--; 
        r++;
    }
    // 返回以 s[l] 和 s[r] 为中心的最长回文串
    return s.substring(l + 1, r);
}

如果输入相同的 lr,就相当于寻找长度为奇数的回文串,

如果输入相邻的 lr,则相当于寻找长度为偶数的回文串。

那么回到最长回文串的问题,解法的大致思路就是:

String longestPalindrome(String s) {
    String res = "";
    for (int i = 0; i < s.length(); i++) {
        // 以 s[i] 为中心的最长回文子串
        String s1 = palindrome(s, i, i);
        // 以 s[i] 和 s[i+1] 为中心的最长回文子串
        String s2 = palindrome(s, i, i + 1);
        // res = longest(res, s1, s2)
        res = res.length() > s1.length() ? res : s1;
        res = res.length() > s2.length() ? res : s2;
    }
    return res;
}

最终代码

class Solution {
    public String longestPalindrome(String s) {
        String res = "";
        for (int i = 0; i < s.length(); i++) {
            // 以 s[i] 为中心的最长回文子串
            String s1 = palindrome(s, i, i);
            // 以 s[i] 和 s[i+1] 为中心的最长回文子串
            String s2 = palindrome(s, i, i + 1);
            // res = longest(res, s1, s2)
            res = res.length() > s1.length() ? res : s1;
            res = res.length() > s2.length() ? res : s2;
        }
        return res;
    }

    // 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
    String palindrome(String s, int l, int r) {
        // 防止索引越界
        while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            // 向两边展开
            l--;
            r++;
        }
        // 返回以 s[l] 和 s[r] 为中心的最长回文串
        return s.substring(l + 1, r);
    }
}

647. 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

方法一:动态规划

class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int count = 0;
        for(int j = 0; j < n; j++){
            for(int i = 0; i <= j; i++){
                if(s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1])){
                    dp[i][j] = true;
                    count++;
                }
            }
        }
        return count;
    }
}

方法二:中心扩散法

class Solution {
    public int countSubstrings(String s) {
        int a = 0;
        int b = 0;
        for(int i = 0; i < s.length(); i++){
            //奇数中心点
            a += parlinDrome(s, i, i);
            //偶数中心点
            b += parlinDrome(s, i, i+1);
        }
        return a+b;
    }
    public int parlinDrome(String s, int l, int r){
        int count = 0;
        while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)){
            l--;
            r++;
            count++;
        }
        return count;
    }
}



// 中心点有 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。
class Solution {
    public int countSubstrings(String s) {
        int ans = 0;
        for (int center = 0; center < 2 * s.length() - 1; center++) {
            int left = center / 2;
            int right = left + center % 2;
            while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
                ans++;
                left--;
                right++;
            }
        }
        return ans;
    }
}

516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

dp[i][j] 表示 子串 s[i..j] ,最长的回文序列长度是多少。

  • 如果 s 的第 i 个字符和第 j 个字符相同的话

dp[i][j] = dp[i + 1][j - 1] + 2

  • 如果 s 的第 i 个字符和第 j 个字符不同的话

dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])

然后注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。

初始化dp[i][i] = 1 单个字符的最长回文序列是 1 ,结果dp[0][n - 1]

class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        // dp 数组全部初始化为 0
        int[][] dp = new int[n][n];
        // base case
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        // 反着遍历保证正确的状态转移
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                // 状态转移方程
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        // 整个 s 的最长回文子串长度
        return dp[0][n - 1];
    }
}

// 降维dp
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        int[] dp = new int[n];
        for (int i = 0; i < n; i++){
            dp[i] = 1;
        }
        for (int i = n - 1; i >= 0; i--) {
            int pre = 0;
            for (int j = i + 1; j < n; j++) {
                int temp = dp[j];
                if (s.charAt(i) == s.charAt(j)){
                    dp[j] = pre + 2;
                } 
                else {
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                pre = temp;
            }
        }
        return dp[n - 1];
    }
}

约瑟夫环

剑指 Offer 62. 圆圈中最后剩下的数字

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

示例 2:

输入: n = 10, m = 17
输出: 2

这个问题是以弗拉维奥·约瑟夫命名的,称为“约瑟夫环”问题,可使用 动态规划 解决。

最容易想到的方法是:构建一个长度为 n 的链表,各节点值为对应的顺序索引;每轮删除第 m 个节点,直至链表长度为 1 时结束,返回最后剩余节点的值即可。模拟法需要循环删除 n−1 轮,每轮在链表中寻找删除节点需要 m 次访问操作(链表线性遍历),因此总体时间复杂度为 O(nm) 。根据题目给定的 m,n 取值范围,可知此时间复杂度是不可接受的。

动态规划解法

f(n,m) 表示在 n 个数字(0~n-1)中不断删除第 m 个数字最后剩下的数字。删除一个数字后剩下了 n-1 个数字,那么再次删除就是在 n-1 个数字中删除第 m 个数字,可以表示为 f(n-1,m)

但实际上直接这么表示是不对的。因为f(n,m)表示的是从区间[0~n-1]不断进行删除操作,被删除的元素是k=(m-1)%n,而下一次删除时,起点是 k+1(即m%n) ,区间不再满足上述要求,因此从第二步不断删除的结果可以表示为f'(n-1,m)。则有:f(n,m) = f'(n-1,m)

现在就要想办法找到 f'(n-1,m)f(n- 1, m)之间的关系。则二者操作的区间表示如下:

f'(n-1,m)表示序列: k+1 k+2 k+3 ... n-1     0     1  ... k-1
f(n-1,m) 表示序列:  0   1   2  ... n-k-2 n-k-1  n-k ... n-2

由此推出 f'(n-1,m)=(f(n-1,m)+k+1)%n

化简:

因为
f'(n-1,m)=(f(n-1,m)+k+1)%n
k+1= m%n
f(n,m) = f'(n-1,m)
所以
f(n-1,m)=[f(n-1,m)+m]%n

由此可知 f(n,m) 可由 f(n-1,m) 得到,f(n-1,m) 可由 f(n-2,m) 得到,……,f(2,m) 可由 f(1,m) 得到;因此,若给定 f(1,m) 的值,就可以递推至任意 f(n,m) 。而 f(1,m) = 0 恒成立。得到如下递归关系:

动态规划解析:

  1. 状态定义: 设「i,m问题」的解为 dp[i];

  2. 转移方程: 通过以下公式可从 dp[i−1] 递推得到 dp[i] = (dp[i−1] + m) % i;

  3. 初始状态:「1,m 问题」的解恒为 0 ,即 dp[1] = 0;

  4. 返回值: 返回「n,m 问题」的解 dp[n] ;

class Solution {
    public int lastRemaining(int n, int m) {
      int dp[] = new int[n+1]; 
      dp[1] = 0;
      for(int i = 2; i <= n; i++){
        dp[i] = (dp[i-1] + m) % i;
      }
      return dp[n];
    }
}

//优化
//根据状态转移方程的递推特性,无需建立状态列表 dp ,而使用一个变量 x 执行状态转移即可。
class Solution {
    public int lastRemaining(int n, int m) {
        int x = 0;
        for (int i = 2; i <= n; i++) {
            x = (x + m) % i;
        }
        return x;
    }
}

1823. 找出游戏的获胜者

共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序1n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。

游戏遵循如下规则:

  1. 从第 1 名小伙伴所在位置 开始
  2. 沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
  3. 你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
  4. 如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
  5. 否则,圈子中最后一名小伙伴赢得游戏。

给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。

亚信安全2024提前批:约瑟夫环

n个人围成一圈,第一个人从1开始报数,报m的将被杀掉,下一个人接着从1开始报,如此反复,最后剩下一个,求最后胜利者。

示例 1:

输入:n = 10, k = 3
输出:4
1 2 3 4 5 6 7 8 9 10   
1 2 4 5 6 7 8 9 10
1 2 4 5 7 8 9 10
1 2 4 5 7 8 10
1 4 5 7 8 10
1 4 5 8 10
4 5 8 10
4 5 10
4 10
4

示例 2:

输入:n = 6, k = 5
输出:1
1 2 3 4 5 6
1 2 3 4 6
1 2 3 6
1 2 3
1 3
1

剑指 Offer 62. 圆圈中最后剩下的数字是:从0~n中,选取第m位淘汰(删除后从下一个数字开始计数)

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

本题是从1~n,选取第m位淘汰(删除后从下一个数字开始计数)

例如,1、2、3、4、5这5个数字组成一个圆圈,从数字1开始每次删除第3个数字,则删除的前4个数字依次是3、1、5、2,因此最后剩下的数字是4。
2、0、4、1  剩下的数字是3
3、1、5、2  剩下的数字是4

每次删除的数都比之前题大一位,所以只需要在剑指 Offer 62. 圆圈中最后剩下的数字的结果上+1即可

class Solution {
    public int findTheWinner(int n, int k) {
        int[] dp = new int[n+1]; 
        dp[1] = 0;
        for(int i = 2; i <= n; i++){
            dp[i] = (dp[i-1] + k) % i;
        }
        return dp[n] + 1;
    }
}

自幂数(超完全数变数)

对于一个正整数而言,长度是n,如果它的各位上的数字的n次方之和正好等于它本身,那么一个n位自然数等于自身各个数位上数字的n次幂之和,则称此数为自幂数(超完全数变数)

例如 153 = 1^3 + 5^3 + 3^3

一位自幂数:独身数: 1,2,3,4,5,6,7,8,9
两位自幂数:没有
三位自幂数:水仙花数:153,370,371,407
四位自幂数:四叶玫瑰数:1634,8208,9474
五位自幂数:五角星数:54748,92727,93084
六位自幂数:六合数:548834
七位自幂数:北斗七星数:1741725,4210818,9800817,9926315
八位自幂数:八仙数:24678050,24678051,88593477
九位自幂数:九九重阳数
十位自幂数:十全十美数
// 输入一个数  求10~该数之间所有的 自幂数 倒序输出
import java.util.Scanner;
 
public class Demo{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();
        for (int i = num; i > 10; i--) {
                int count = 0;	//记录位数
                int tmp = i;	//定义一个临时变量等于i,避免后面要判断与i相等时i的值改变
                //求位数
                while(tmp != 0){
                    tmp /= 10;
                    count++;
                }
                int sum = 0;
                tmp = i;
                while(tmp != 0){
                    sum += Math.pow(tmp % 10, count);
                    tmp /= 10; //去掉最后一位
                }
                if(sum == i){
                    System.out.println(i);
                }
          }
     }
}

数学

进位操作:2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

双指针法

这道题主要考察链表双指针技巧加法运算过程中对进位的处理。注意这个 carry 变量的处理,在我们手动模拟加法过程的时候会经常用到。代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是 dummy 节点。

我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。

具体而言,如果当前两个链表处相应位置的数字为 n1n2,进位值为 carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry) % 10,而新的进位值为 ⌊(n1+n2+carry)/10⌋

此外,如果链表遍历结束后,有 carry > 0,还需要在答案链表的后面附加一个节点,节点的值为 carry

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // 在两条链表上的指针
        ListNode p1 = l1, p2 = l2;
        // 虚拟头结点(构建新链表时的常用技巧)
        ListNode dummy = new ListNode(-1);
        // 指针 p 负责构建新链表
        ListNode p = dummy;
        // 记录进位
        int carry = 0;
        // 开始执行加法,两条链表走完且没有进位时才能结束循环
        while (p1 != null || p2 != null) {
            // 先加上上次的进位
            int val = carry;
            if (p1 != null) {
                val += p1.val;
                p1 = p1.next;
            }
            if (p2 != null) {
                val += p2.val;
                p2 = p2.next;
            }
            // 处理进位情况
            carry = val / 10;
            val = val % 10;
            // 构建新节点
            p.next = new ListNode(val);
            p = p.next;
        }
        //还有进位
        if (carry > 0) {
            p.next = new ListNode(carry);
        }
        // 返回结果链表的头结点(去除虚拟头结点)
        return dummy.next;
    }
}

进位操作:415. 字符串相加

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

示例 1:

输入:num1 = "11", num2 = "123"
输出:"134"

示例 2:

输入:num1 = "456", num2 = "77"
输出:"533"

示例 3:

输入:num1 = "0", num2 = "0"
输出:"0"

  • 算法流程: 设定 ij 两指针分别指向 num1num2 尾部,模拟人工加法;
    • 计算进位: 计算 carry = tmp // 10,代表当前位相加是否产生进位;
    • 添加当前位: 计算 tmp = n1 + n2 + carry,并将当前位 tmp % 10 添加至 res 头部;
    • 索引溢出处理: 当指针 ij 走过数字首部后,给 n1n2 赋值为 0,相当于给 num1num2 中长度较短的数字前面填 0,以便后续计算。
    • 当遍历完 num1num2 后跳出循环,并根据 carry 值决定是否在头部添加进位 1,最终返回 res 即可。
  • 复杂度分析:
    • 时间复杂度 O(max(M,N))):其中 M,N 为num1 和 num2长度;
    • 空间复杂度 O(1):指针与变量使用常数大小空间。

char 转 int :字符的ASCII码值减去0的ASCII码值等于数值本身。 即num1.charAt(i) - '0'

415. 字符串相加 -题解

class Solution {
    public String addStrings(String num1, String num2) {
        StringBuilder res = new StringBuilder("");
        int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
        while(i >= 0 || j >= 0){
            int n1 = i >= 0 ? num1.charAt(i) - '0' : 0;
            int n2 = j >= 0 ? num2.charAt(j) - '0' : 0;
            int tmp = n1 + n2 + carry;
            carry = tmp / 10; //进位
            res.append(tmp % 10);
            i--; j--;
        }
        if(carry == 1) res.append(1);
        return res.reverse().toString();
    }
}

排列组合:357. 统计各位数字都不同的数字个数

给你一个整数 n ,统计并返回各位数字都不同的数字 x 的个数,其中 0 <= x < 10^n

示例 1:

输入:n = 2
输出:91
解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。 

示例 2:

输入:n = 0
输出:1

首先考虑两种边界情况:

  • 当 n=0时,0 ≤ x < 10 ,x 只有 1 种选择,即 0。

  • 当 n=1 时,0 ≤ x < 10,x 有 10 种选择,即 0∼9。

当 n=2 时,0 ≤ x < 100,x 的选择可以由两部分构成:

只有一位数的 x 和有两位数的 x。只有一位数的 x 可以由上述的边界情况计算。

有两位数的 x 可以由组合数学进行计算:第一位的选择有 9 种,即 1∼9,第二位的选择也有 9 种,即 0∼9 中除去第一位的选择。

更一般地,含有 d (2≤d≤10)位数的各位数字都不同的数字 x 的个数可以由公式 9×A9d−1 计算。再加上含有小于 d 位数的各位数字都不同的数字 x 的个数,即可得到答案。

class Solution {
    public int countNumbersWithUniqueDigits(int n) {
        if (n == 0) {
            return 1;
        }
        if (n == 1) {
            return 10;
        }
        int res = 10, cur = 9;
        for (int i = 0; i < n - 1; i++) {
            // n = 2 :9*9 + 10
            // n = 3 :9*9*8 + 91
            cur *= 9 - i;  
            res += cur;
        }
        return res;
    }
}

括号问题

如何解决括号相关的问题 :: labuladong的算法小抄

22. 括号生成-DFS

剑指 Offer II 085. 生成匹配的括号

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

有关括号问题,你只要记住以下性质,思路就很容易想出来:

  • 1、一个「合法」括号组合的左括号数量一定等于右括号数量。

  • 2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量。

class Solution {
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        dfs(n, n, "");
        return res;
    }

    private void dfs(int left, int right, String curStr) {
        if (left == 0 && right == 0) { // 左右括号都不剩余了,递归终止
            res.add(curStr);
            return;
        }

        if (left > 0) { // 如果左括号还剩余的话,可以拼接左括号
            dfs(left - 1, right, curStr + "(");
        }
        if (right > left) { // 如果右括号剩余多于左括号剩余的话,可以拼接右括号
            dfs(left, right - 1, curStr + ")");
        }
    }
}


class Solution {
    private List<String> res = new ArrayList<>();

    public List<String> generateParenthesis(int n) {
        StringBuilder cur = new StringBuilder();
        dfs(cur, n, n);
        return res;
    }

    private void dfs(StringBuilder cur, int left, int right) {
        if(left > right) return; // 若左括号剩下的多,说明不合法
        if(left < 0 || right < 0) return;  // 数量小于 0 肯定是不合法的
        if(left == 0 && right == 0) { // 当所有括号都恰好用完时,得到一个合法的括号组合
            res.add(cur.toString());
            return;
        }

        // 尝试放一个左括号
        cur.append("(");
        dfs(cur, left-1, right);
        cur.deleteCharAt(cur.length()-1); // 撤消选择

        // 尝试放一个右括号
        cur.append(")");
        dfs(cur, left, right-1);
        cur.deleteCharAt(cur.length()-1); // 撤消选择
    }
}

20. 有效的括号-栈

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

栈是一种先进后出的数据结构,处理括号问题的时候尤其有用。

遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配。

class Solution {
    public boolean isValid(String s) {
        int n = s.length();
        // 如果s的长度为奇数,一定不符合要求
        if (n % 2 == 1) return false;
        Map<Character, Character> map = new HashMap<Character, Character>() {{
            put(')', '(');
            put(']', '[');
            put('}', '{');
        }};
        Deque<Character> stack = new LinkedList<>();
        for(int i = 0; i < n; i++){
            //遇到右括号则将对应左括号出栈
            if(map.containsKey(s.charAt(i))){
                // 去栈中寻找最近的左括号,看是否匹配。
                if (!stack.isEmpty() && stack.peek() == map.get(s.charAt(i))) {  
                    // 匹配上就出栈
                    stack.pop();
                } else {
                    // 没匹配上
                    return false;
                }
                
            } else {
                stack.push(s.charAt(i)); //遇到左括号直接入栈
            }
        }
        return stack.isEmpty();
    }
}

// 不使用 Map
class Solution {
    public boolean isValid(String str) {
        Stack<Character> stack = new Stack<>();
        for (char c : str.toCharArray()) {
            // 字符 c 是左括号入栈
            if (c == '(' || c == '{' || c == '['){
                stack.push(c);
            }
            else {
                // 字符 c 是右括号,寻找最近的左括号,看是否匹配。
                if (!stack.isEmpty() && rev(c) == stack.peek()){
                    stack.pop();
                } else {
                    // 和最近的左括号不匹配
                    return false;
                } 
            }
        }
        // 是否所有的左括号都被匹配了
        return stack.isEmpty();
    }

    char rev(char c) {
        if (c == '}') return '{';
        if (c == ')') return '(';
        return '[';
    }
}

301. 删除无效的括号

921. 使括号有效的最少添加

只有满足下面几点之一,括号字符串才是有效的:

  • 它是一个空字符串,或者
  • 它可以被写成 ABAB 连接), 其中 AB 都是有效字符串,或者
  • 它可以被写作 (A),其中 A 是有效字符串。

给定一个括号字符串 s ,在每一次操作中,你都可以在字符串的任何位置插入一个括号

  • 例如,如果 s = "())" ,你可以插入一个开始括号为 "(())""()()"

返回 为使结果字符串 s 有效而必须添加的最少括号数

示例 1:

输入:s = "())"
输出:1

示例 2:

输入:s = "((("
输出:3

class Solution {
    public int minAddToMakeValid(String s) {
        // res 记录插入次数
        int res = 0;
        // need 变量记录右括号的需求量
        int need = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                // 对右括号的需求 + 1
                need++;
            } 
            if (s.charAt(i) == ')') {
                // 对右括号的需求 - 1
                need--;
                if (need == -1) {
                    need = 0;
                    // 需插入一个左括号
                    res++;
                }
            } 
        }
        // 插入剩余所需的右括号
        return res + need;
    }
}

//栈实现
class Solution {
    public int minAddToMakeValid(String s) {
        int res = 0;
        Stack<Character> stack = new Stack<>();
        for (char c : s.toCharArray()) {
            if (c == '(') {
                stack.push(c);
            } else {
                if (!stack.isEmpty()) {
                    stack.pop();
                } else {
                    res++; // 没有配对的左括号,插入一个
                }
            }
        }
        // 插入的右括号数量 + 多余的左括号数量
        return res + stack.size();
    }
}

1541. 平衡括号字符串的最少插入次数

给你一个括号字符串 s ,它只包含字符 '('')' 。一个括号字符串被称为平衡的当它满足:

  • 任何左括号 '(' 必须对应两个连续的右括号 '))'
  • 左括号 '(' 必须在对应的连续两个右括号 '))' 之前。

比方说 "())""())(())))""(())())))" 都是平衡的, ")()""()))""(()))" 都是不平衡的。

你可以在任意位置插入字符 ‘(‘ 和 ‘)’ 使字符串平衡。

请你返回让 s 平衡的最少插入次数。

示例 1:

输入:s = "(()))"
输出:1
解释:第二个左括号有与之匹配的两个右括号,但是第一个左括号只有一个右括号。我们需要在字符串结尾额外增加一个 ')' 使字符串变成平衡字符串 "(())))" 。

示例 2:

输入:s = "())"
输出:0
解释:字符串已经平衡了。

示例 3:

输入:s = "))())("
输出:3
解释:添加 '(' 去匹配最开头的 '))' ,然后添加 '))' 去匹配最后一个 '(' 。

示例 4:

输入:s = "(((((("
输出:12
解释:添加 12 个 ')' 得到平衡字符串。

示例 5:

输入:s = ")))))))"
输出:5
解释:在字符串开头添加 4 个 '(' 并在结尾添加 1 个 ')' ,字符串变成平衡字符串 "(((())))))))" 。

class Solution {
    public int minInsertions(String s) {
        // res 记录插入次数
        int res = 0;
        // need 变量记录右括号的需求量
        int need = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                need += 2;
                if (need % 2 == 1) { // 若对右括号的需求量为奇数
                    res++; // 插入一个右括号
                    need--;// 对右括号的需求减一
                }
            } 
            if (s.charAt(i) == ')') {
                need--;
                if (need == -1) { // 说明右括号太多了
                    res++;  // 需要插入一个左括号
                    need = 1;  // 同时,对右括号的需求变为 1
                }
            }
            
        }
        // 插入剩余所需的右括号
        return res + need;
    }
}

32. 最长有效括号-动规

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

示例 3:

输入:s = ""
输出:0

判断括号串是否合法的算法如下:

 Deque<Integer> stk = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
    if (s.charAt(i) == '(') {
        // 遇到左括号,记录索引
        stk.push(i);
    } else {
        // 遇到右括号
        if (!stk.isEmpty()) {
            // 配对的左括号对应索引,[leftIndex, i] 是一个合法括号子串
            int leftIndex = stk.pop();
            // 这个合法括号子串的长度
            int len = i - leftIndex + 1;
        } else {
            // 没有配对的左括号
        }
    }
}

但如果多个合法括号子串连在一起,会形成一个更长的合法括号子串,而上述算法无法适配这种情况。

所以需要一个 dp 数组,记录 leftIndex 相邻合法括号子串的长度,才能得出题目想要的正确结果。

class Solution {
    public int longestValidParentheses(String s) {
        Deque<Integer> stack = new ArrayDeque<>();
        // dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度
        int[] dp = new int[s.length() + 1];
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                stack.push(i);   // 遇到左括号,入栈对应索引
                dp[i + 1] = 0;  // 左括号不可能是合法括号子串的结尾
            } else {
                if (!stack.isEmpty()) { // 遇到右括号
                    // 配对的左括号对应索引,[leftIndex, i] 是一个合法括号子串
                    int leftIndex = stack.pop();
                    // 以这个右括号结尾的最长子串长度
                    int len = i - leftIndex + 1 + dp[leftIndex];
                    dp[i + 1] = len;
                } else {
                    // 没有配对的左括号
                    dp[i + 1] = 0;
                }
            }
        }
        // 计算最长子串的长度
        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

比较

❶Comparable

Comparable是排序接口。若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sortArrays.sort进行自动排序。

// 该接口定义如下:
package java.lang;
import java.util.*;
public interface Comparable<T> {
    public int compareTo(T o);
}

此接口只有一个方法compareTo,比较当前对象与指定对象的顺序,如果该对象小于、等于或大于指定对象,则分别返回负数、零或正数。

现在我们假设一个Person类,代码如下:

public class Person{
    String name;
    int age;
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public String getName(){
        return name;
    }
  public int getAge(){
        return age;
    }
}

现在有两个Person类的对象,我们如何来比较二者的大小呢?我们可以通过让Person实现Comparable接口:

public class Person implements Comparable<Person>{
    String name;
    int age;
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public String getName(){
        return name;
    }
    public int getAge(){
        return age;
    }
    @Override
    public int compareTo(Person p){
        return this.age - p.getAge();
    }
  
    public static void main(String[] args){
        Person[] people = new Person[]{new Person("小明", 20),new Person("小红", 10)};
        Arrays.sort(people);
    }
}

❷Comparator

Comparator是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。

// 该接口定义如下:
package java.util;
public interface Comparator<T>
 {
    int compare(T o1, T o2);
    boolean equals(Object obj);
 }
  • 若一个类要实现Comparator接口:它一定要实现compare() 函数,但可不实现equals() 函数。

  • compare(T o1, T o2) 是“比较o1和o2的大小”。

    • 返回“负数”,意味着“o1比o2小”
    • 返回“零”,意味着“o1等于o2”
    • 返回“正数”,意味着“o1大于o2”

现在假如上面的Person类没有实现Comparable接口,该如何比较大小呢?我们可以新建一个类,让其实现Comparator接口,从而构造一个“比较器”。

public class PersonCompartor implements Comparator<Person>{
    @Override
    public int compare(Person o1, Person o2){
        return o1.getAge() - o2.getAge();
    }
}

现在我们就可以利用这个比较器来对其进行排序:

public class Test{
    public static void main(String[] args){
        Person[] people=new Person[]{new Person("小明", 20),new Person("小红", 10)};
        Arrays.sort(people,new PersonCompartor());
    }
}

//写法2
public class Test2{
    public static void main(String[] args){
        Person[] people=new Person[]{new Person("小明", 20),new Person("小红", 10)};
        Arrays.sort(people,new Comparator<Person>(){
                public int compare(Person o1, Person o2){
                        return o1.getAge() - o2.getAge();
                    }
        });
    }
}

❸Comparable和Comparator区别

Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。

Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。

Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

  • java.lang.Comparable:在类定义的时候,可以实现的接口,里面有compareTo这个方法需要实现
  • java.util.Comparator:是挽救的比较接口,需要单独定义一个比较类,里面有compare比较方法。

两种方法各有优劣, 用Comparable简单, 只要实现Comparable接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。 用Comparator的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

交换元素

普通方法

private void swap(int[] a, int i, int j) {
    int tmp = a[i]
    a[i] = a[j]
    a[j] = tmp
    System.out.println("swap:" + a[j] + " <-> " + a[i]);
}

异或方法

private void swap(int[] a, int i, int j) {
    if (i == j){return;}//关键 两个一样的数异或结果为0
    a[i] = a[i] ^ a[j];
    a[j] = a[i] ^ a[j];
    a[i] = a[i] ^ a[j];
    System.out.println("swap:" + a[j] + " <-> " + a[i]);
}

KMP算法

KMP 算法是一个快速查找匹配串的算法,它的作用是:如何快速在「原字符串」中找到「匹配字符串」。

暴力查找复杂度是 O(m∗n),KMP 算法的复杂度为 O(m+n),其中m为原字符串长度,m为匹配字符串长度

【自用数据结构】王道4.2.4 串的模式匹配 KMP算法+求Next数组(手算)+求Nextval数组(手算)

【KMP】从原理上详解next数组和nextval数组

【天勤考研】KMP算法易懂版_哔哩哔哩_bilibili

KMP字符串匹配算法1_哔哩哔哩_bilibili

28. 找出字符串中第一个匹配项的下标 - 力扣(Leetcode)

算法数据结构系列-理论篇-字符串匹配(一)

class Solution {
    public static int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        if (m == 0) return 0;

        // 1.构建 next 数组
        int[] next = new int[m]; //next[0]=0
        for (int i = 1, j = 0; i < m; i++) {
            // 匹配不成功的话,j = next(j-1)
            while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
                j = next[j - 1];
            }
            // 匹配成功的话,j++
            if (needle.charAt(i) == needle.charAt(j)) {
                j++;
            }
            // 更新 next[i]
            next[i] = j;
        }

        // 2.匹配过程
        for (int i = 0, j = 0; i < n; i++) {
            // 匹配不成功 j = next(j-1)
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = next[j - 1];
            }
            // 匹配成功的话,j++
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == m) {
                return i - m + 1;
            }
        }
        return -1;
    }
}

❤️Sponsor

您的支持是我不断前进的动力,如果您恰巧财力雄厚,又感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰

支付宝支付 微信支付

文章作者: 简简
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 简简 !
评论
填上邮箱会收到评论回复提醒哦!!!
 上一篇
数构&算法:常见算法 数构&算法:常见算法
①排序⓿ 复杂度 排序算法 平均时间 最差时间 稳定性 空间 备注 冒泡排序 O(n2) O(n2) 稳定 O(1) n较小时好 选择排序 O(n2) O(n2) 不稳定 O(1) n较小时好 插入排序 O(n2) O(
2020-03-28
下一篇 
数构&算法:手撕算法 数构&算法:手撕算法
手撕快排快速排序使用分治法策略来把一个序列分为较小和较大的 2 个子序列,然后递回地排序两个子序列。具体算法描述如下: 先把数组中的一个数当做基准数(pivot),一般会把数组中最左边的数当做基准数。 然后从两边进行检索。 先从右边检索比
2020-03-28
  目录