剑指offer刷题笔记

整数

两数相除29

题目:输入2个int型整数,它们进行除法计算并返回商,要求不得使用乘号’*‘、除号’/‘及求余符号’%'。当发生溢出时,返回最大的整数值。假设除数不为0。例如,输入15和2,输出15/2的结果,即7。

解题思路详解

  1. 处理溢出:
    首先,我们需要考虑到整数溢出的情况。在Java中,int类型的范围是从Integer.MIN_VALUE(-231)到`Integer.MAX_VALUE`(231-1)。当被除数为Integer.MIN_VALUE且除数为-1时,按照数学计算,结果应该是2^31,但这超出了int的表示范围,因此按照题目要求返回Integer.MAX_VALUE

  2. 使用长整型:
    为了避免在后续计算中发生溢出,我们将被除数和除数都转换为长整型(long)进行处理。同时,为了简化问题,我们取它们的绝对值进行计算,并记录最终结果应该是正数还是负数。

  3. 位移操作模拟除法:
    本质上,除法可以视为减法的重复执行:我们不断从被除数中减去除数,直到被除数小于除数。但这种方法效率低下,特别是当被除数远大于除数时。

    为了提高效率,我们可以利用位移操作。位移操作可以将数值快速翻倍(左移)或者减半(右移),相当于乘以或除以2的幂次。具体到这个问题中,我们不是每次直接减去除数,而是尝试将除数翻倍(通过左移操作),使其尽可能接近但不超过被除数。

    具体步骤如下:

    • 初始化一个变量quotient(商),表示当前翻倍后的除数是原除数的多少倍。
    • 在不超过被除数的前提下,不断将除数翻倍(即左移),同时quotient也翻倍。
    • 当除数不能再翻倍(即将超过被除数)时,从被除数中减去当前的除数,然后将quotient加到最终结果中。
    • 重复以上步骤,直到被除数小于原始除数。
  4. 结果的正负号:
    我们在最开始记录了结果的正负号,这是通过判断被除数和除数的符号是否相同来实现的。在Java中,可以通过异或操作(^)来判断两个数的符号是否相异,如果相异则结果为负,否则为正。

  5. 返回结果:
    最后,我们根据之前记录的符号,决定返回结果的正负。

示例

假设我们要计算dividend = 15divisor = 2的除法运算结果。

  1. 初始化和符号判断:

    • 因为15和2都是正数,所以最终的结果也应该是正数。
    • dividenddivisor转换为长整型,以避免溢出,并取绝对值:longDividend = 15, longDivisor = 2
  2. 位移操作模拟除法:

    • 我们要计算15除以2。开始时,quotient(当前除数是原始除数的倍数)设置为1。
    • 检查divisor是否可以翻倍而不超过dividend。2可以翻倍多次直到超过15,我们找到最大的不超过15的翻倍值。
  3. 第一轮翻倍:

    • 将2翻倍至4(divisor <<= 1),quotient也翻倍至2。
    • 继续翻倍,4翻倍至8,quotient翻倍至4。此时8仍然不超过15。
    • 再次尝试翻倍,8翻倍至16,但这会超过15,所以不执行这次翻倍。
  4. 减去累积的除数并记录商:

    • 此时,dividend减去最近的divisor(即8),dividend = 15 - 8 = 7
    • quotient(当前为4)累加到结果中。
  5. 重复直到dividend小于原始divisor

    • 由于7仍然大于或等于2,我们继续过程,但这次从原始的divisor开始,没有更多的翻倍空间。
    • 直接减去divisor7 - 2 = 5,将quotient(此时为1)累加到结果中。
    • 再次尝试减去2,5 - 2 = 3,累加quotient(此时为1)到结果中。
    • 继续这个过程,3 - 2 = 1,累加quotient(此时为1)到结果中。
  6. 最终结果:

    • 在这个过程中,每成功减去一次divisor,就累加一次quotient到结果中。因此,累加的quotient总和就是商。在这个例子中,我们共减去了2四次,所以最终结果是4。

代码1

public class Solution {
    public int divide(int dividend, int divisor) {
        // 处理特殊情况:当被除数为Integer.MIN_VALUE且除数为-1时,返回值会溢出int范围,按题目要求返回Integer.MAX_VALUE
        if (dividend == Integer.MIN_VALUE && divisor == -1) {
            return Integer.MAX_VALUE;
        }
        
        // 使用长整型来避免在后续操作中可能发生的整数溢出,并将被除数和除数转换为正数进行处理
        long longDividend = Math.abs((long) dividend);
        long longDivisor = Math.abs((long) divisor);
        
        // 通过异或操作判断最终结果的符号是否为负
        boolean negative = (dividend < 0) ^ (divisor < 0);
        
        long result = 0; // 用来存储最终的商
        // 当被除数大于等于除数时,执行循环
        while (longDividend >= longDivisor) {
            long tempDivisor = longDivisor; // 临时存储除数,用于翻倍
            long quotient = 1; // 存储当前临时除数是原始除数的多少倍
            // 不断左移(翻倍)临时除数,直到它超过了被除数
            while (longDividend >= (tempDivisor << 1)) {
                tempDivisor <<= 1; // 除数翻倍
                quotient <<= 1; // 商也相应翻倍
            }
            // 将翻倍后的除数从被除数中减去,并将对应的倍数加到结果中
            longDividend -= tempDivisor;
            result += quotient;
        }
        
        // 根据最初判断的符号,决定是返回正结果还是负结果
        return negative ? (int)-result : (int)result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        // 测试示例:计算15除以2的结果
        System.out.println(solution.divide(15, 2)); // 应该输出7
    }
}

代码2

package com.wei.manager;

import java.util.Scanner;

public class TestClass {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请输入被除数和除数(以空格分隔),输入 'exit' 退出:");
            String input = scanner.nextLine();

            // 检查是否退出
            if ("exit".equalsIgnoreCase(input.trim())) {
                System.out.println("程序已退出。");
                break;
            }

            String[] parts = input.split("\\s+");
            if (parts.length != 2) {
                System.out.println("请输入两个整数或 'exit'。");
                continue;
            }

            try {
                int dividend = Integer.parseInt(parts[0]);
                int divisor = Integer.parseInt(parts[1]);

                int result = divide(dividend, divisor);
                System.out.println("结果是:" + result);
            } catch (NumberFormatException e) {
                System.out.println("输入错误,请输入整数。");
            } catch (ArithmeticException e) {
                System.out.println("数学错误:" + e.getMessage());
            }
        }
        scanner.close();
    }

    public static int divide(int dividend, int divisor) {
        // 特殊情况处理:最小整数除以-1的情况。
        if (dividend == Integer.MIN_VALUE && divisor == -1) {
            return Integer.MAX_VALUE;
        }

        // 使用异或来确定结果的符号是否为负。
        boolean isNegative = (dividend ^ divisor) < 0;

        // 将被除数和除数转换为负数,避免溢出。
        dividend = dividend > 0 ? -dividend : dividend;
        divisor = divisor > 0 ? -divisor : divisor;

        int result = 0;
        while (dividend <= divisor) {
            int tempDivisor = divisor;
            int quotient = 1;
            // 检查是否溢出以及是否可以继续翻倍
            while (dividend - tempDivisor <= tempDivisor) {
                tempDivisor <<= 1; // 使用位移来翻倍
                quotient <<= 1; // 商的翻倍也使用位移
            }
            result += quotient;
            dividend -= tempDivisor;
        }

        // 根据符号返回结果。如果isNegative为true,结果是负的,因此返回-result;否则返回result。
        return isNegative ? -result : result;
    }

}

二进制加法67

题目:输入两个表示二进制的字符串,请计算它们的和,并以二进制字符串的形式输出。例如,输入的二进制字符串分别是"11"和"10",则输出"101"。

代码

public class BinaryStringAddition {
    public static String addBinary(String a, String b) {
        // 初始化结果字符串
        StringBuilder result = new StringBuilder();
        // i 和 j 分别为 a 和 b 的末尾索引
        int i = a.length() - 1, j = b.length() - 1;
        // carry 为进位
        int carry = 0;
        
        // 当任一字符串没有遍历完或还有进位时,继续循环
        while (i >= 0 || j >= 0 || carry == 1) {
            // 如果当前索引有效,则加上 a 或 b 的当前位,否则加0
            int sum = carry;
            if (i >= 0) sum += a.charAt(i--) - '0';
            if (j >= 0) sum += b.charAt(j--) - '0';
            
            // sum 的值只可能是 0, 1, 2 或 3
            // sum % 2 给出当前位的值,sum / 2 给出新的进位值
            result.append(sum % 2);
            carry = sum / 2;
        }
        
        // 最终的结果是倒序的,需要反转
        return result.reverse().toString();
    }

    public static void main(String[] args) {
        String a = "1010";
        String b = "1011";
        System.out.println(addBinary(a, b)); // 输出结果
    }
}

比特位计数338

题目:输入一个非负数n,请计算0到n之间每个数字的二进制形式中1的个数,并输出一个数组。例如,输入的n为4,由于0、1、2、3、4的二进制形式中1的个数分别为0、1、1、2、1,因此输出数组[0,1,1,2,1]。

算法理解

这个算法基于动态规划思想,其中每个数x的二进制中1的个数是根据已知结果递推得到的。算法的核心在于发现一个重要的性质,这个性质有助于我们减少计算量,快速找到任何数的二进制中1的个数。

关键观察

对于任意正整数x,我们可以将其分为两部分来观察:

  • x / 2:相当于将x的二进制表示向右移动一位(丢弃最低位)。例如,10(二进制1010)右移一位是5(二进制101)。
  • x % 2:确定x的最低位是否为1。如果x是奇数,则最低位为1;如果x是偶数,则最低位为0。
算法解释

利用上述观察,我们可以得出结论:一个数x的二进制中1的个数可以通过查看x / 2的二进制中1的个数,并加上x % 2的结果(即x的最低位)来计算。这是因为:

  • 当我们将x除以2时(或者说右移一位),我们实际上是把x的二进制表示中除了最低位以外的部分的1的个数找出来了。
  • 然后,我们通过x % 2判断x的最低位是否为1,如果是,就需要在之前的基础上加1。
算法步骤
  1. 初始化结果数组:创建一个长度为n+1的数组result,因为我们要计算从0n每个数字的二进制中1的个数。result[0]自然是0,因为0的二进制表示中没有1
  2. 遍历并计算:从1n遍历,对于每个数i,计算其二进制表示中1的个数。计算方式为:result[i] = result[i >> 1] + (i & 1)。这里,i >> 1相当于i / 2i & 1用于判断i的最低位是否为1
    • result[i >> 1]找出了i右移一位后(即i/2)的二进制中1的个数。
    • (i & 1)判断i的最低位是否为1,是则加1,不是则加0
  3. 返回结果:数组result即为从0n每个数的二进制中1的个数。
示例

假设n=4,我们来一步步计算result数组的值:

  • result[0] = 0(初始化)
  • result[1] = result[1/2] + 1 % 2 = result[0] + 1 = 1
  • result[2] = result[2/2] + 2 % 2 = result[1] + 0 = 1
  • result[3] = result[3/2] + 3 % 2 = result[1] + 1 = 2
  • result[4] = result[4/2] + 4 % 2 = result[2] + 0 = 1

代码

public class CountBits {
    public static int[] countBits(int n) {
        int[] result = new int[n + 1]; // 初始化结果数组
        for (int i = 1; i <= n; i++) {
            // i >> 1 等价于 i / 2,得到i的一半对应的1的个数
            // i & 1 等价于 i % 2,判断i的最低位是否为1
            result[i] = result[i >> 1] + (i & 1);
        }
        return result;
    }

    public static void main(String[] args) {
        int n = 4;
        int[] bitsCount = countBits(n);
        for (int count : bitsCount) {
            System.out.print(count + " ");
        }
    }
}

只出现一次的数字137

题目:输入一个整数数组,数组中只有一个数字出现了一次,而其他数字都出现了3次。请找出那个只出现一次的数字。例如,如果输入的数组为[0,1,0,1,0,1,100],则只出现一次的数字是100。

算法

这个解法的核心思路是利用位操作和计数原理。对于整数的每一位,我们统计所有数字中这一位上1出现的次数。如果某个数字在这一位上是1,那么它将贡献到这一位的计数中。由于除了那个唯一的数字外,其他数字都是出现3次的,因此对于任何位来说,如果1的出现次数不能被3整除,那么那个只出现一次的数字在这一位上必然是1。通过检查所有32位,我们可以重建出那个唯一的数字。

这种方法的时间复杂度是O(N),空间复杂度是O(1),其中N是数组nums的长度。

示例

假设输入数组为nums = [5, 3, 5, 4, 5, 3, 3],我们需要找到只出现一次的数字,即4

示例数组:[5, 3, 5, 4, 5, 3, 3]
  • 数字5的二进制表示为101
  • 数字3的二进制表示为011
  • 数字4的二进制表示为100
步骤分解
  1. 初始化结果result = 0

  2. 遍历每一位(共32位,但为简化,我们只考虑数字的最低3位):

    • 第0位

      • 数字5、3和4在第0位的值分别为1、1和0。
      • 第0位上1的总次数为5(出现3次)+ 3(出现3次)= 6次,6对3取余为0,因此第0位不贡献到result
    • 第1位

      • 数字5和4在第1位的值为0,数字3在第1位的值为1。
      • 第1位上1的总次数为3(出现3次)= 3次,3对3取余为0,因此第1位不贡献到result
    • 第2位

      • 数字5在第2位的值为1,数字3和4在第2位的值分别为0和1。
      • 第2位上1的总次数为5(出现3次)+ 4(出现1次)= 4次,4对3取余为1,因此第2位贡献到result
  3. 最终结果:只有第2位贡献到result,因此result = 100(二进制),即十进制中的4

代码

public class Solution {
    public int singleNumber(int[] nums) {
        int result = 0;
        for (int i = 0; i < 32; i++) { // 对于整数的每一个位
            int sum = 0;
            for (int num : nums) {
                if ((num >> i & 1) == 1) { // 将每个数右移i位, 检查最低位是否为1
                    sum++; // 计算1出现的次数
                }
            }
            sum %= 3; // 对3取余,如果某位上的结果是1,说明那个只出现一次的数字在这个位上是1
            if (sum != 0) {
                result |= sum << i; // 将这个位上的1加到结果中
            }
        }
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {0, 1, 0, 1, 0, 1, 100};
        System.out.println(solution.singleNumber(nums)); // 输出100
    }
}

最大单词长度乘积318

题目:输入一个字符串数组words,请计算不包含相同字符的两个字符串words[i]和words[j]的长度乘积的最大值。如果所有字符串都包含至少一个相同字符,那么返回0。假设字符串中只包含英文小写字母。例如,输入的字符串数组words为[“abcw”,“foo”,“bar”,“fxyz”,“abcdef”],数组中的字符串"bar"与"foo"没有相同的字符,它们长度的乘积为9。"abcw"与"fxyz"也没有相同的字符,它们长度的乘积为16,这是该数组不包含相同字符的一对字符串的长度乘积的最大值。

算法步骤

  1. 首先,我们将每个字符串转换为一个整数,表示该字符串中出现的字符。这可以通过遍历字符串中的每个字符并将其映射到一个26位的整数上来实现,每一位代表一个字母。例如,如果字符串包含’a’,则第0位为1;如果包含’b’,则第1位为1,以此类推。这样,我们可以用一个整数表示一个字符串。
  2. 然后,我们检查任意两个字符串表示的整数,通过位与操作(AND)来判断这两个字符串是否有公共字符。如果结果为0,表示没有公共字符。
  3. 最后,我们计算这样的字符串对的长度乘积,并更新最大值。

代码

public class Solution {
    public int maxProduct(String[] words) {
        int len = words.length;
        int[] value = new int[len];
        for (int i = 0; i < len; i++) {
            String temp = words[i];
            for (int j = 0; j < temp.length(); j++) {
                value[i] |= 1 << (temp.charAt(j) - 'a');
            }
        }
        int maxProduct = 0;
        for (int i = 0; i < len; i++) {
            for (int j = i + 1; j < len; j++) {
                if ((value[i] & value[j]) == 0 && (words[i].length() * words[j].length() > maxProduct)) {
                    maxProduct = words[i].length() * words[j].length();
                }
            }
        }
        return maxProduct;
    }
}

数组

知识总结

双指针

方向相反的双指针经常用来求排序数组中的两个数字之和。

方向相同的双指针通常用来求正数数组中子数组的和或乘积。

排序数组中的两个数字之和

题目:输入一个递增排序的数组和一个值k,请问如何在数组中找出两个和为k的数字并返回它们的下标?假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。例如,输入数组[1,2,4,6,10],k的值为8,数组中的数字2与6的和为8,它们的下标分别为1与3。

算法

一个指针P1指向数组的第1个数字,另一个指针P2指向数组的最后一个数字,然后比较两个指针指向的数字之和及一个目标值。如果两个指针指向的数字之和大于目标值,则向左移动指针P2;如果两个指针指向的数字之和小于目标值,则向右移动指针P1。此时两个指针的移动方向是相反的。

代码

public class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int left = 0, right = numbers.length - 1;
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum == target) {
                return new int[] {left, right}; // 题目要求的是下标,根据实际情况可能需要+1
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
        return new int[] {-1, -1}; // 如果没有找到
    }
}

三数之和15

题目:输入一个数组,如何找出数组中所有和为0的3个数字的三元组?需要注意的是,返回值中不得包含重复的三元组。例如,在数组[-1,0,1,2,-1,-4]中有两个三元组的和为0,它们分别是[-1,0,1]和[-1,-1,2]。

解法步骤

  1. 排序:首先对数组进行排序,这是使用双指针技术的前提。
  2. 遍历:遍历排序后的数组,对于每个元素,我们将使用双指针来寻找两个数,使得这三个数的和为0。需要注意的是,为了避免重复的三元组,我们只对第一个数字进行遍历,如果当前数字和前一个数字相同,则跳过这个数字。
  3. 双指针搜索:对于当前选定的元素nums[i],设置两个指针,left = i + 1right = nums.length - 1。如果nums[i] + nums[left] + nums[right] == 0,找到了一个三元组。如果三数之和小于0,则移动left指针;如果大于0,则移动right指针。如果找到一组解,还需要移动指针跳过所有重复的元素。
  4. 去重:在找到一个有效的三元组后,需要跳过所有重复的元素。

代码

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums); // 排序
        List<List<Integer>> res = new LinkedList<>();
        for (int i = 0; i < nums.length - 2; i++) {
            if (i == 0 || (i > 0 && nums[i] != nums[i - 1])) { // 跳过重复元素
                int lo = i + 1, hi = nums.length - 1, sum = 0 - nums[i];
                while (lo < hi) {
                    if (nums[lo] + nums[hi] == sum) {
                        res.add(Arrays.asList(nums[i], nums[lo], nums[hi]));
                        while (lo < hi && nums[lo] == nums[lo + 1]) lo++; // 跳过重复元素
                        while (lo < hi && nums[hi] == nums[hi - 1]) hi--; // 跳过重复元素
                        lo++; hi--;
                    } else if (nums[lo] + nums[hi] < sum) lo++;
                    else hi--;
                }
            }
        }
        return res;
    }
}

长度最小的子数组209

题目:输入一个正整数组成的数组和一个正整数k,请问数组中和大于或等于k的连续子数组的最短长度是多少?如果不存在所有数字之和大于或等于k的子数组,则返回0。例如,输入数组[5,1,4,3],k的值为7,和大于或等于7的最短连续子数组是[4,3],因此输出它的长度2。

基本思路

这个问题可以通过使用双指针(也称为滑动窗口)技术来解决。基本思路是维护一个当前和大于等于k的最短连续子数组的窗口。开始时,窗口大小为0,然后逐步扩大窗口(通过移动右指针)直到窗口内元素的总和大于等于k。一旦找到这样的窗口,就尝试通过移动左指针来缩小窗口大小,同时保持窗口内元素的总和大于等于k,以找到可能的最短窗口。重复这个过程直到右指针到达数组的末尾。

算法步骤

  1. 初始化两个指针left = 0right = 0以及窗口内元素的总和sum = 0
  2. 扩大窗口:移动right指针(增加元素到窗口)直到sum >= k
  3. 缩小窗口:当sum >= k时,移动left指针(从窗口移除元素)来尝试找到更短的满足条件的连续子数组,并更新最短长度。
  4. 重复步骤2和3,直到右指针到达数组末尾。
  5. 如果找到了满足条件的子数组,则返回最短长度;否则,如果没有找到,则返回0。

具体代码

public class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int n = nums.length;
        int ans = Integer.MAX_VALUE; // 存储最短连续子数组的长度
        int left = 0; // 左指针
        int sum = 0; // 窗口内元素的总和
        for (int right = 0; right < n; right++) {
            sum += nums[right]; // 向窗口内添加元素
            while (sum >= s) { // 当窗口内的总和大于等于s时
                ans = Math.min(ans, right - left + 1); // 更新最短长度
                sum -= nums[left++]; // 移除窗口左边的元素
            }
        }
        return (ans != Integer.MAX_VALUE) ? ans : 0; // 如果找到了满足条件的子数组,返回最短长度,否则返回0
    }
}

乘积小于K的子数组713

题目:输入一个由正整数组成的数组和一个正整数k,请问数组中有多少个数字乘积小于k的连续子数组?例如,输入数组[10,5,2,6],k的值为100,有8个子数组的所有数字的乘积小于100,它们分别是[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]和[5,2,6]。

基本思路

维护一个乘积小于k的最大连续子数组窗口,通过移动右指针扩大窗口并更新乘积,如果乘积达到或超过k,则通过移动左指针缩小窗口并更新乘积,直到乘积再次小于k。每次向右移动右指针时,窗口内新增的子数组数量等于右指针和左指针的距离。

算法步骤

  1. 初始化两个指针left = 0right = 0以及当前乘积prod = 1
  2. 向右移动right指针,每移动一次,就将nums[right]乘到prod上。
  3. 如果prod >= k,则向右移动left指针,并从prod中除去nums[left],直到prod < k
  4. 对于每个位置的right,如果prod < k,则以nums[right]结尾的满足条件的子数组数量为right - left + 1,将这个值累加到结果中。
  5. 重复步骤2-4,直到right遍历完数组。

具体代码

public class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) return 0;
        int count = 0;
        int prod = 1;
        for (int left = 0, right = 0; right < nums.length; right++) {
            prod *= nums[right];
            while (prod >= k) {
                prod /= nums[left++];
            }
            count += right - left + 1;
        }
        return count;
    }
}

注意事项

  • 这里因为要求连续子数组,所以不需要对原数组进行排序。
  • 假如数组第一个值就大于k,那么count += 0-1+1=0,还是为0。

和为K的子数组560

题目:输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1,1,1],k的值为2,有2个连续子数组之和等于2。

基本思路

这个问题可以通过使用累积和与哈希表的方法来解决,这种方法的基本思想是在遍历数组的同时,计算从开始到当前元素的累积和,并使用哈希表记录所有不同的累积和出现的次数。然后,对于每个累积和,查看哈希表中是否存在一个之前的累积和等于当前累积和减去k的值,如果存在,这意味着从那个旧的累积和的位置到当前位置的子数组之和等于k。

解法步骤

  1. 初始化:创建一个哈希表来存储累积和及其出现的次数,以及一个变量来记录总数。将0的累积和(代表没有元素的情况)初始化在哈希表中出现1次,以处理累积和直接等于k的情况。

  2. 遍历数组:遍历数组,对于每个元素,更新累积和,并查找哈希表中累积和减去k的数量,将这个数量加到总数上。

  3. 更新哈希表:将当前的累积和添加到哈希表中,如果这个累积和之前出现过,增加其计数。

  4. 返回总数:遍历完成后,返回总数,即所有满足条件的连续子数组的数量。

示例

考虑输入数组 [1, 2, 3]k = 3。我们的目标是找出所有连续子数组,其元素之和等于3。

初始化
  • 累积和到出现次数的映射:{0:1}。这表示,到目前为止,有一个“虚拟”的累积和为0(在数组开始之前),出现了1次。
  • sum = 0count = 0
第一步:处理元素1
  • sum = 1
  • 检查 sum - k = -2 在映射中不存在,不做任何操作。
  • 更新映射:{0:1, 1:1}
第二步:处理元素2
  • sum = 3(累积到现在的和)。
  • 检查 sum - k = 0 在映射中存在,意味着从数组的开始到当前位置的累积和正好是3。我们找到了一个符合条件的子数组:[1, 2]
  • 更新映射:{0:1, 1:1, 3:1}
  • count = 1
第三步:处理元素3
  • sum = 6(累积到现在的和)。
  • 检查 sum - k = 3 在映射中存在,意味着存在一个之前的累积和(3),从那个点到当前点的子数组之和正好是3。我们找到了另一个符合条件的子数组:[3]
  • 更新映射:{0:1, 1:1, 3:1, 6:1}
  • count = 2
结束
  • 遍历完数组,找到了总共2个连续子数组的和为3:[1, 2][3]
  • 返回 count = 2

具体代码

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0, sum = 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1); // 初始化为累积和为0的情况出现一次
        
        for (int num : nums) {
            sum += num; // 更新累积和
            if (map.containsKey(sum - k)) {
                count += map.get(sum - k); // 如果存在之前的累积和使得当前累积和减去它等于k,更新总数
            }
            map.put(sum, map.getOrDefault(sum, 0) + 1); // 更新哈希表
        }
        return count;
    }
}

0和1个数相同的子数组525

题目:输入一个只包含0和1的数组,请问如何求0和1的个数相同的最长连续子数组的长度?例如,在数组[0,1,0]中有两个子数组包含相同个数的0和1,分别是[0,1]和[1,0],它们的长度都是2,因此输出2。

解题思路

首先把输入数组中所有的0都替换成-1,那么题目就变成求包含相同数目的-1和1的最长子数组的长度。在一个只包含数字1和-1的数组中,如果子数组中-1和1的数目相同,那么子数组的所有数字之和就是0,因此这个题目就变成求数字之和为0的最长子数组的长度。

算法步骤

  1. 初始化:创建一个哈希表来存储每个累积和首次出现的索引。由于累积和为0时对应的最长子数组可能从索引0开始,因此我们先在哈希表中添加一个条目{0: -1}
  2. 遍历数组:初始化累积和为0,遍历数组,将遇到的0视为-1,1保持不变。更新累积和。
  3. 检查累积和:如果当前的累积和在哈希表中已经存在,说明从哈希表中累积和对应的索引+1到当前索引的子数组是一个0和1个数相同的子数组,更新最长长度。
  4. 更新哈希表:如果当前的累积和在哈希表中不存在,将其添加到哈希表中,记录当前索引。
  5. 返回最长长度:遍历完成后,返回找到的最长子数组的长度。

示例

考虑输入数组 [0, 1, 0, 1, 1, 0, 0]

为了解决这个问题,我们将遇到的0替换为-1,并计算累积和。同时,我们使用一个哈希表来存储每个累积和首次出现的索引。下面是详细的步骤:

初始化
  • 哈希表:map = {0: -1},表示累积和为0时,其索引为虚拟的-1(在数组开始之前)。
  • 最长长度:maxLength = 0
  • 累积和:sum = 0
遍历数组
  • 索引0:遇到0,替换为-1,sum = -1。累积和-1首次出现,更新哈希表:map = {0: -1, -1: 0}
  • 索引1:遇到1,sum = 0(-1 + 1)。累积和回到0,查找哈希表发现累积和为0时的虚拟索引为-1,因此子数组长度为1 - (-1) = 2。更新maxLength = 2
  • 索引2:遇到0,替换为-1,sum = -1。已存在于哈希表中,但我们仅更新首次出现,不更新maxLength
  • 索引3:遇到1,sum = 0。同索引1,已处理。
  • 索引4:遇到1,sum = 1。累积和1首次出现,更新哈希表:map = {0: -1, -1: 0, 1: 4}
  • 索引5:遇到0,替换为-1,sum = 0。找到从索引-15的子数组[0, 1, 0, 1, 1, 0]长度为5 - (-1) = 6,更新maxLength = 6
  • 索引6:遇到0,替换为-1,sum = -1。不更新maxLength,因为最长的已找到。
结果

遍历结束后,最长的0和1个数相同的连续子数组长度为6,对应的子数组是[0, 1, 0, 1, 1, 0]

这个例子展示了如何通过将0替换为-1,使用累积和和哈希表来找到数组中0和1个数相同的最长连续子数组。

具体代码

import java.util.HashMap;

public class Solution {
    public int findMaxLength(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, -1); // 初始化累积和为0的情况
        int maxLength = 0, sum = 0;
        
        for (int i = 0; i < nums.length; i++) {
            sum += (nums[i] == 1 ? 1 : -1); // 将0视为-1以转化为累积和问题
            
            if (map.containsKey(sum)) {
                maxLength = Math.max(maxLength, i - map.get(sum)); // 如果当前累积和之前出现过,更新最长长度
            } else {
                map.put(sum, i); // 记录当前累积和首次出现的索引
            }
        }
        return maxLength;
    }
}

左右两边子数组的和相等

题目:输入一个整数数组,如果一个数字左边的子数组的数字之和等于右边的子数组的数字之和,那么返回该数字的下标。如果存在多个这样的数字,则返回最左边一个数字的下标。如果不存在这样的数字,则返回-1。例如,在数组[1,7,3,6,2,9]中,下标为3的数字(值为6)的左边3个数字1、7、3的和与右边两个数字2和9的和相等,都是11,因此正确的输出值是3。

解题思路

这个问题可以高效地通过一次遍历解决。算法的核心思路是首先计算整个数组的总和,然后从左到右遍历数组,同时维护一个累积和来表示当前位置左侧所有数字的和。在每个步骤中,我们可以计算右侧所有数字的和,通过从总和中减去左侧数字的和再减去当前位置的数字。如果左侧和等于右侧和,我们就找到了一个符合条件的下标。

算法步骤

  1. 首先,遍历一次数组,计算数组的总和totalSum
  2. 初始化一个变量leftSum为0,用于存储当前遍历到的位置左侧所有数字的累积和。
  3. 再次遍历数组,对于每个位置i
    • 计算右侧数字的和,即totalSum - leftSum - nums[i],其中nums[i]是当前位置的数字。
    • 如果leftSum等于右侧数字的和,则返回当前位置i
    • 否则,将nums[i]加到leftSum上,继续遍历。
  4. 如果遍历完整个数组都没有找到符合条件的下标,则返回-1。

具体代码

public class Solution {
    public int pivotIndex(int[] nums) {
        int totalSum = 0, leftSum = 0;
        // 计算数组总和
        for (int num : nums) {
            totalSum += num;
        }
        // 遍历数组,寻找符合条件的中心索引
        for (int i = 0; i < nums.length; i++) {
            if (leftSum == totalSum - leftSum - nums[i]) {
                return i;
            }
            leftSum += nums[i];
        }
        // 不存在符合条件的中心索引
        return -1;
    }
}

二维子矩阵的数字之和304

题目:输入一个二维矩阵,如何计算给定左上角坐标和右下角坐标的子矩阵的数字之和?对于同一个二维矩阵,计算子矩阵的数字之和的函数可能由于输入不同的坐标而被反复调用多次。例如,输入图2.1中的二维矩阵,以及左上角坐标为(2,1)和右下角坐标为(4,3)的子矩阵,该函数输出8。

image-20240408130830807

解题思路

这个问题可以通过预计算一个辅助的二维数组(称为前缀和数组)来高效解决,这样对于每次查询可以在O(1)的时间复杂度内得到子矩阵的和。核心思想是前缀和数组sum[i][j]存储了原矩阵从(0,0)(i,j)形成的子矩阵的所有元素之和。有了这个前缀和数组,我们可以通过简单的数学运算来计算任何子矩阵的和。

解题步骤

  1. 计算前缀和数组: 对于给定的二维矩阵matrix,计算前缀和数组sum,其中sum[i][j]代表从(0,0)(i,j)的子矩阵的元素总和。这可以通过动态规划完成,每个元素的前缀和可以根据其上方和左侧的元素的前缀和计算得出。

  2. 查询子矩阵的和: 给定左上角(row1, col1)和右下角(row2, col2)的坐标,子矩阵的和可以用前缀和表示为:
    s u m [ r o w 2 ] [ c o l 2 ] − s u m [ r o w 2 ] [ c o l 1 − 1 ] − s u m [ r o w 1 − 1 ] [ c o l 2 ] + s u m [ r o w 1 − 1 ] [ c o l 1 − 1 ] sum[row2][col2] - sum[row2][col1-1] - sum[row1-1][col2] + sum[row1-1][col1-1] sum[row2][col2]sum[row2][col11]sum[row11][col2]+sum[row11][col11]
    这里需要注意边界条件,即当row1col1为0时,相应的减去的部分应当为0。

代码实现

class NumMatrix {
    private int[][] sum;

    public NumMatrix(int[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) return;
        int m = matrix.length, n = matrix[0].length;
        sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = matrix[i-1][j-1] + sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return sum[row2+1][col2+1] - sum[row2+1][col1] - sum[row1][col2+1] + sum[row1][col1];
    }
}

字符串

字符串中的变位词567

题目:输入字符串s1和s2,如何判断字符串s2中是否包含字符串s1的某个变位词?如果字符串s2中包含字符串s1的某个变位词,则字符串s1至少有一个变位词是字符串s2的子字符串。假设两个字符串中只包含英文小写字母。例如,字符串s1为"ac",字符串s2为"dgcaf",由于字符串s2中包含字符串s1的变位词"ca",因此输出为true。如果字符串s1为"ab",字符串s2为"dgcaf",则输出为false。

解题思路

这个问题可以高效地通过一次遍历解决。算法的核心思路是使用滑动窗口的方式来检查s1的任一变位词是否为s2的子串。具体方法是通过比较s1中字符的频率与s2中某长度等于s1长度的滑动窗口内字符的频率。

算法步骤

  1. 初始化频率统计数组:首先初始化两个长度为26的数组,分别用于统计s1的字符频率和s2中当前滑动窗口的字符频率。

  2. 填充s1的频率统计:遍历s1的每个字符,更新其在s1频率统计数组中的计数。

  3. 初始化s2的首个窗口:遍历s2的前s1.length()个字符,更新s2频率统计数组。

  4. 滑动窗口比较

    :使用滑动窗口遍历s2:

    • 如果当前窗口内的字符频率与s1的字符频率相匹配,则返回true
    • 窗口向右滑动:加入新字符的频率,移除窗口最左侧字符的频率。
  5. 结束判断:如果整个s2遍历完毕都没有找到匹配的窗口,则返回false

示例

字符串 s1 = "abc"s2 = "eidbacoo"

  1. 初始化字符频率统计数组

    • s1Count 对 ‘a’, ‘b’, ‘c’ 的频率进行计数:[1, 1, 1, 0, ..., 0],代表 ‘a’, ‘b’, ‘c’ 各出现一次,其余字母出现次数为0。
    • s2Count 初始化为与 s1Count 同样长度的数组,开始时同样为 [0, 0, 0, 0, ..., 0]
  2. 设置初始窗口

    • 我们需要将 s2 的前三个字符 “eid” 纳入考虑,更新 s2Count
      • ‘e’ -> s2Count[4](‘e’ - ‘a’ = 4)
      • ‘i’ -> s2Count[8](‘i’ - ‘a’ = 8)
      • ‘d’ -> s2Count[3](‘d’ - ‘a’ = 3)
    • 初始 s2Count 更新后为 [0, 0, 0, 1, 1, 0, 0, 0, 1, 0, ..., 0]
  3. 滑动窗口遍历

    • s2 的第四个字符开始向右滑动窗口,每次窗口向右移动一个字符,更新 s2Count 并检查是否匹配 s1Count
  4. 滑动窗口操作

    • 索引 3 (字符 ‘b’): 加 ‘b’ (s2Count[1]++), 减 ‘e’ (s2Count[4]--)
      • s2Count becomes [0, 1, 0, 1, 0, 0, 0, 0, 1, 0, ..., 0]
    • 索引 4 (字符 ‘a’): 加 ‘a’ (s2Count[0]++), 减 ‘i’ (s2Count[8]--)
      • s2Count becomes [1, 1, 0, 1, 0, 0, 0, 0, 0, 0, ..., 0]
    • 索引 5 (字符 ‘c’): 加 ‘c’ (s2Count[2]++), 减 ‘d’ (s2Count[3]--)
      • s2Count becomes [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, ..., 0]
  5. 检查是否找到变位词

    • 在索引 5 时,s2Counts1Count 完全匹配,表明 s2 中从索引 3 (‘b’) 到 5 (‘c’) 的子串 “bac” 是 s1 的一个变位词。
  6. 输出结果

    • 函数返回 true,因为找到了匹配的变位词。

代码实现

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        
        int[] s1Count = new int[26];
        int[] s2Count = new int[26];
        
        // 初始化s1的字符频率统计
        for (int i = 0; i < s1.length(); i++) {
            s1Count[s1.charAt(i) - 'a']++;
            s2Count[s2.charAt(i) - 'a']++;
        }
        
        // 检查第一个窗口是否匹配
        if (matches(s1Count, s2Count)) {
            return true;
        }
        
        // 滑动窗口
        int n = s1.length();
        for (int i = n; i < s2.length(); i++) {
            s2Count[s2.charAt(i) - 'a']++;  // 加入新字符到当前窗口
            s2Count[s2.charAt(i - n) - 'a']--;  // 移除窗口外的字符
            
            // 检查更新后的窗口是否匹配
            if (matches(s1Count, s2Count)) {
                return true;
            }
        }
        
        // 如果所有窗口都不匹配,返回false
        return false;
    }
    
    private boolean matches(int[] s1Count, int[] s2Count) {
        for (int i = 0; i < 26; i++) {
            if (s1Count[i] != s2Count[i]) {
                return false;
            }
        }
        return true;
    }
}

字符串中的所有变位词438

题目:输入字符串s1和s2,如何找出字符串s2的所有变位词在字符串s1中的起始下标?假设两个字符串中只包含英文小写字母。例如,字符串s1为"cbadabacg",字符串s2为"abc",字符串s2的两个变位词"cba"和"bac"是字符串s1中的子字符串,输出它们在字符串s1中的起始下标0和5。

算法步骤

  1. 初始化频率数组:创建一个大小为26的数组来存储 s2 中每个字符的频率。
  2. 设置初始窗口:遍历 s1 的前 s2.length() 个字符,更新窗口内的字符频率统计。
  3. 遍历 s1:从 s2.length() 开始,向右滑动窗口。对于每个新字符进入窗口,增加其频率;对于每个离开窗口的字符,减少其频率。
  4. 检查匹配:如果当前窗口的字符频率与 s2 的字符频率相匹配,则记录当前窗口的起始索引。
  5. 返回结果:遍历结束后,返回所有记录的起始索引。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        if (s.length() < p.length()) return result;
        
        int[] pCount = new int[26];
        int[] sCount = new int[26];
        
        // 填充p的字符频率
        for (int i = 0; i < p.length(); i++) {
            pCount[p.charAt(i) - 'a']++;
            sCount[s.charAt(i) - 'a']++;
        }
        
        // 检查首个窗口
        if (matches(pCount, sCount)) {
            result.add(0);
        }
        
        // 开始滑动窗口
        int n = p.length();
        for (int i = n; i < s.length(); i++) {
            sCount[s.charAt(i) - 'a']++;
            sCount[s.charAt(i - n) - 'a']--;
            
            if (matches(pCount, sCount)) {
                result.add(i - n + 1);
            }
        }
        
        return result;
    }
    
    private boolean matches(int[] pCount, int[] sCount) {
        for (int i = 0; i < 26; i++) {
            if (pCount[i] != sCount[i]) {
                return false;
            }
        }
        return true;
    }
}

不含重复字符的最长子字符串3

题目:输入一个字符串,求该字符串中不含重复字符的最长子字符串的长度。例如,输入字符串"babcca",其最长的不含重复字符的子字符串是"abc",长度为3。

注意点

  • 是求最长子字符串而不是最长子序列,字符串babcca的最长不含重复字符的子字符串是从下标1到下标3之间的子串,所以对应的最长子串长度为3。
  • 即使在字符串变成了babccad,那么最长不含重复字符的子字符串也是abc或者cad,长度也还是3。

解题思路

使用滑动窗口技术结合哈希表来管理窗口内字符的出现。滑动窗口可以灵活地扩大和缩小以保持其内的所有字符不重复。

算法步骤

  1. 初始化数据结构:使用一个哈希表来存储每个字符的最新索引位置,以及两个指针,startend,代表当前考虑的子字符串的边界。
  2. 遍历字符串:通过一个指针 end 遍历字符串,逐个检查字符。
  3. 调整窗口
    • 如果字符已存在于哈希表中,并且其索引位置在当前窗口的 start 指针之后,将 start 更新为该重复字符索引的下一个位置,这样可以移除前一个重复的字符。
    • 更新每个字符的索引到哈希表中。
  4. 更新结果:在每次迭代中,如果当前窗口的长度(end - start + 1)大于之前记录的最长长度,则更新最长长度。
  5. 返回结果:遍历完成后,返回记录的最长长度。

具体代码

import java.util.HashMap;

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int maxLength = 0;
        int start = 0;

        for (int end = 0; end < s.length(); end++) {
            char currentChar = s.charAt(end);
			// 检查当前字符是否已经存在于哈希表中且是否在当前窗口内
            if (map.containsKey(currentChar) && map.get(currentChar) >= start) {
                // 如果是,更新窗口的起始位置到当前字符的上一个位置的下一个位置
                start = map.get(currentChar) + 1;
            }

            map.put(currentChar, end);
            maxLength = Math.max(maxLength, end - start + 1);
        }
        return maxLength;
    }
}

包含所有字符的最短字符串76

题目:输入两个字符串s和t,请找出字符串s中包含字符串t的所有字符的最短子字符串。例如,输入的字符串s为"ADDBANCAD",字符串t为"ABC",则字符串s中包含字符’A’、'B’和’C’的最短子字符串是"BANC"。如果不存在符合条件的子字符串,则返回空字符串""。如果存在多个符合条件的子字符串,则返回任意一个。

解题思路

使用滑动窗口技术来解决“最小覆盖子串”问题是非常有效的。这个方法允许我们以线性时间处理字符串,同时动态地调整窗口大小以找到最短的包含所有目标字符的子串。

注意事项

  • 目标字符串t中的字符可以重复的,所以要单独用一个HashMap来记录t的字符,不仅要检查 s 中的子字符串是否包含了 t 中所有的字符,还要确保这些字符的数量至少与 t 中出现的数量相等。

算法步骤

  1. 初始化:定义两个哈希表,一个用于统计 t 的字符需求,另一个用于跟踪当前窗口内的字符。
  2. 扩展窗口:扩展 end 指针直到窗口包含了所有需要的字符。
  3. 收缩窗口:一旦窗口满足所有字符需求,尝试移动 start 指针以缩小窗口大小直到窗口不满足条件。
  4. 更新结果:每次窗口满足条件时,检查并更新最小窗口的记录。
  5. 返回结果:根据记录的最小窗口返回最终结果。

具体代码

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public String minWindow(String s, String t) {
        // 当输入字符串s或t为空时,无法形成任何窗口
        if (s.length() == 0 || t.length() == 0) return "";

        // 哈希表用于存储t中字符的频率
        Map<Character, Integer> dictT = new HashMap<>();
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            dictT.put(c, dictT.getOrDefault(c, 0) + 1);
        }

        // required用于记录t中有多少个独特字符需要被窗口覆盖
        int required = dictT.size();

        // 左右指针初始化,用于定义当前检查的窗口边界
        int l = 0, r = 0;
        // formed用于跟踪窗口中已经满足频率要求的字符数量
        int formed = 0;

        // 存储当前窗口中字符的频率
        Map<Character, Integer> windowCounts = new HashMap<>();

        // 数组ans记录最小覆盖子串的长度和起始索引,初始化为(-1, 0, 0)
        // 如果ans[0]是-1,表示没有找到符合条件的子串
        int[] ans = {-1, 0, 0};

        // 开始滑动窗口的过程
        while (r < s.length()) {
            // 从s中取出字符,增加到窗口的字符计数哈希表中
            char c = s.charAt(r);
            int count = windowCounts.getOrDefault(c, 0);
            windowCounts.put(c, count + 1);

            // 如果当前字符的数量达到了t中该字符的需求,增加formed计数器
            // 注意是判断dictT中有没有c
            if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
                formed++;
            }

            // 尝试并收缩窗口直到它不再满足包含t的所有字符的要求
            while (l <= r && formed == required) {
                c = s.charAt(l);
                // 更新最小子串的记录
                if (ans[0] == -1 || r - l + 1 < ans[0]) {
                    ans[0] = r - l + 1;
                    ans[1] = l;
                    ans[2] = r;
                }

                // 从窗口中移除当前位置l的字符,更新窗口计数
                windowCounts.put(c, windowCounts.get(c) - 1);
                // 注意是判断dectT中有没有c
                if (dictT.containsKey(c) && windowCounts.get(c) < dictT.get(c)) {
                    formed--;
                }
                l++;  // 收缩窗口的左边界
            }
            r++;  // 扩大窗口的右边界
        }

        // 根据ans数组中记录的索引和长度,返回最终的最小覆盖子串
        return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
    }
}

有效的回文125

算法步骤

  1. 初始化:创建一个新的字符串用于存放过滤和转换后的字符。
  2. 过滤非字母数字字符:遍历原字符串,将所有字母和数字添加到新字符串中。
  3. 转换大小写:将新字符串的所有字母转换为小写(或大写)。
  4. 双指针比较:使用两个指针分别从新字符串的首尾进行遍历,逐个比较字符是否相同。
  5. 返回结果:如果所有字符都匹配,则返回 true;否则,返回 false

具体代码

#include <cctype> // 包含函数isalnum和tolower
#include <string>

class Solution {
public:
    bool isPalindrome(std::string s) {
        int left = 0, right = s.length() - 1;

        while (left < right) {
            // 移动左指针直到它指向一个字母或数字
            while (left < right && !std::isalnum(s[left])) {
                left++;
            }
            // 移动右指针直到它指向一个字母或数字
            while (left < right && !std::isalnum(s[right])) {
                right--;
            }

            // 比较字符,忽略大小写
            if (std::tolower(s[left]) != std::tolower(s[right])) {
                return false;
            }

            left++;
            right--;
        }

        return true;
    }
};
  • isalnum可以用来判断是否是数字和字母。
  • tolower可以让字母变成小写。
#include <cctype> // 包含tolower和isalnum
#include <string>
#include <algorithm> // 包含reverse

class Solution {
public:
    bool isPalindrome(std::string s) {
        std::string filteredS;

        // 过滤非字母数字字符并转换为小写
        for (char c : s) {
            if (std::isalnum(c)) {
                filteredS.push_back(std::tolower(c));
            }
        }

        // 创建一个反转的字符串进行比较
        std::string reversedS = filteredS; // 复制原始过滤字符串
        std::reverse(reversedS.begin(), reversedS.end()); // 反转字符串

        // 比较过滤后的字符串与其反转版本
        return filteredS == reversedS;
    }
};
  • std::string也可以使用push_back
  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值