Leetcode_数学、位运算

目录

*汉明距离

1 题目描述

两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

给你两个整数 x 和 y,计算并返回它们之间的汉明距离。

示例 1

输入:x = 1, y = 4
输出:2
解释:
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑
上面的箭头指出了对应二进制位不同的位置。

示例 2

输入:x = 3, y = 1
输出:1

提示

  • 0 <= x, y <= 231 - 1

2 解题(Java)

2.1 方法一:异或 + 无符号右移

class Solution {
    public int hammingDistance(int x, int y) {
        int res = 0, z = x ^ y;
        while (z != 0) {
            res += z & 1;
            z >>>= 1;
        }
        return res;
    }
}

2.2 方法二:z = z & (z−1)

记 f(x)表示 x 和 x−1 进行与运算所得的结果(即f(x)=x & (x−1)),那么 f(x) 恰为 x 删去其二进制表示中最右侧的 1 的结果。

在这里插入图片描述

代码

class Solution {
    public int hammingDistance(int x, int y) {
        int res = 0, z = x ^ y;
        while (z != 0) {
            z &= z - 1;
            res++;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度:O(log C),其中 C 是元素的数据范围,在本题中 logC=log231 =31;
  • 空间复杂度:O(1);

*比特位计数

1 题目描述

给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。

示例 1:

输入: 2
输出: [0,1,1]

示例 2:

输入: 5
输出: [0,1,1,2,1,2]

进阶:

  • 给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
  • 要求算法的空间复杂度为O(n)。
  • 你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的__builtin_popcount)来执行此操作。

2 解题(Java)

class Solution {
    public int[] countBits(int n) {
        int[] res = new int[n+1];
        for (int i=1; i<=n; i++) {
            if (i % 2 == 0) res[i] = res[i / 2];
            else res[i] = res[i - 1] + 1; 
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度:O(n);
  • 空间复杂度:O(n);

*只出现一次的数字

1 题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]
输出: 1

示例 2:

输入: [4,1,2,1,2]
输出: 4

2 解题(Java)

解题思路

使用位运算。对于这道题,可使用异或运算 ^。异或运算有以下三个性质:

  1. 任何数和 0 做异或运算,结果仍然是原来的数,即 a ^ 0=a;
  2. 任何数和其自身做异或运算,结果是 0,即 a ^ a=0;
  3. 异或运算满足交换律和结合律,即 a ^ b ^ a=b ^ a ^ a=b ^ (a ^ a)=b ^ 0=b;

代码

class Solution {
    public int singleNumber(int[] nums) {
        int res = 0;
        for (int num : nums) {
            res ^= num;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度O(n):其中 n 为数组长度,只需要对数组遍历一次;
  • 空间复杂度O(1)

不用加减乘除做加法*

1 题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:

输入: a = 1, b = 1
输出: 2

提示:

a, b 均可能是负数或 0
结果不会溢出 32 位整数

2 解题(Java)

2.1 解题思路

  1. 设两数字的二进制形式 a, b,其求和 s = a + b,a(i)代表 a 的二进制第 i 位,则分为以下四种情况:
a(i)b(i)无进位和n(i)进位c(i+1)
0000
0110
1010
1101
  1. 观察发现,无进位和异或运算规律相同,进位与运算规律相同(并需左移一位)。因此,无进位和n与进位c的计算公式如下:
    1. n = a ^ b(无进位和,异或运算)
    2. c = a&b<<1(进位,与运算 + 左移一位)
  2. (和s)=(无进位和n) + (进位c)。即可将s = a + b转化为:s = n + c;
  3. 循环求n和c,直至进位c=0;此时s = n,返回n即可;

在这里插入图片描述
:若数字a和b中有负数,则变成了减法,如何处理?

:在计算机系统中,数值一律用补码来表示和存储。补码的优势:加法、减法可以统一处理(CPU只有加法器)。因此,以上方法同时适用于正数和负数的加法。

2.2 代码

class Solution {
    public int add(int a, int b) {
        while(b != 0) { // 当进位为 0 时跳出
            int c = (a & b) << 1;  // c = 进位
            a ^= b; // a = 非进位和
            b = c;
        }
        return a;
    }
}

3 复杂性分析

  • 时间复杂度 O(1): 最差情况下(例如 a= 0x7fffffff , b=1 时),需循环 32 次,使用 O(1) 时间;每轮中的常数次位操作使用 O(1) 时间;
  • 空间复杂度 O(1) : 辅助变量c使用常数大小的额外空间;

*二进制中1的个数

1 题目描述

请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1

输入:00000000000000000000000000001011
输出:3

示例 2

输入:11111111111111111111111111111101
输出:31

2 解题(Java)

方法:逐位判断

根据 与运算 定义,设二进制数字 n ,则有:

  • 若 n&1=0 ,则 n 二进制 最右一位 为 0 ;
  • 若 n&1=1 ,则 n 二进制 最右一位 为 1 。

算法流程

  1. 初始化数量统计变量 res = 0。
  2. 循环逐位判断: 当 n = 0 时跳出。
  3. res += n & 1 : 若 n&1=1 ,则统计数 res 加1。
  4. n >>>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 “>>>” ) 。
  5. 返回统计数量 res。

代码

public class Solution {
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0) {
            res += n & 1;
            n >>>= 1;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度 O(log2n): 此算法循环内部仅有移位、与、加等基本运算,占用 O(1) ;逐位判断需循环log2 n 次,其中log2 n 代表数字n最高位1的所在位数(例如 log 2 4=2, log2 16=4);
  • 空间复杂度 O(1) : 变量 res 使用常数大小额外空间;

*数值的整数次方

1 题目描述

实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

示例 1:

输入: 2.00000, 10
输出: 1024.00000

示例 2:

输入: 2.10000, 3
输出: 9.26100

示例 3:

输入: 2.00000, -2
输出: 0.25000
解释: 2 ^ -2 = (1/2) ^ 2 = 1/4 = 0.25

说明:

-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−2 ^ 31, 2 ^ 31 − 1] 。

2 解题(Java)

2.1 解题思路

  1. 求 x ^ n 最简单的方法是通过循环将 n 个 x 乘起来,依次求 x ^ 1, x ^ 2, …, x ^ (n-1), x ^ n ,时间复杂度为 O(n);
  2. 快速幂法可将时间复杂度降低至 O(log2 n),以下从 “二进制” 和“二分法”两个角度解析快速幂法;
2.1.1 快速幂解析(二进制角度)
  1. 利用十进制数字 n 的二进制表示,可对快速幂进行数学化解释;
  2. 对于任何十进制正整数 n ,设其二进制为“bm…b3b2b1” ( bi为二进制某位值,i∈[1,m] ),则有:
    1. 二进制转十进制: n = 1b1 + 2b2 + 4b3 + … + 2 ^ (m-1)bm;
    2. 幂的二进制展开:x ^ n = x ^ (1b1 + 2b2 + 4b3 + … + 2 ^ (m-1)bm) = x ^ 1b1 * x ^ 2b2 * x ^ 4b3…x ^ (2 ^ (m-1))bm;
  3. 根据以上推导,可把计算 x^n转化为解决以下两个问题:
    1. 计算 x ^ 1, x ^ 2, x ^ 4, …, x ^ (2^(m-1))的值:循环赋值操作x = x ^ 2即可;
    2. 获取二进制各位b1, b2, b3, …,bm的值:循环执行以下操作即可:
      1. n & 1(与操作):判断n二进制最右一位是否为1;
      2. n >> 1(移位操作):n右移一位(即删除最后一位);
    3. 因此,应用以上操作,可在循环中依次计算x ^ (2 ^ 0 * b1),x ^ (2 ^ 1 * b2), …x ^ (2 ^ (m-1) * bm)的值,并将所有x ^ (2 ^ (i - 1) * bi)累计相乘即可:
      1. 当bi = 0时:x ^ (2 ^ (i-1) * bi) = 1;
      2. 当bi = 1时:x ^ (2 ^ (i-1) * bi) = x ^ (2 ^ (i-1));

在这里插入图片描述

2.1.2 快速幂解析(二分法角度)
  1. 快速幂实际上是二分思想的一种应用;
  2. 二分推导:x ^ n = x ^ n/2 * x ^ n/2 = (x ^ 2) ^ n/2,令n/2为整数,需要分为奇偶两种情况(设向下取整除法符号为“//”):
    1. 当n为偶数: x ^ n = (x ^ 2) ^ n//2;
    2. 当n为奇数:x ^ n = x(x ^ 2) ^ n//2,即会多出一项x;
  3. 幂结果获取:
    1. 根据二分推导,可通过x = x ^ 2操作,每次把幂从n降至n//2,直至将幂降为0;
    2. 设res = 1,则初始状态x ^ n = x ^ n * res。在循环二分时,每当n为奇数时,将多出的一项x乘入res,当n=0时,返回res即可;

在这里插入图片描述

  1. 转化为位运算:
    1. 向下整除n//2等价于右移一位n>>1;
    2. 取余数n%2等价于判断二进制最右一位值n & 1;

2.2 算法流程

  1. 当x = 0时:直接返回0(避免后续x = 1/x操作报错);
  2. 初始化res = 1;
  3. 当n<0时:把问题转化至n>=0的范围内,即执行x = 1/x, n = -n;
  4. 循环计算:当n=0时跳出:
    1. 当n&1=1时:将当前x乘入res(即res *= x);
    2. 执行x = x ^ 2(即x *= x);
    3. 执行n右移一位(即n>>=1);
  5. 返回res;

注:

Java 代码中 int32 变量 n∈[−2147483648,2147483647] ,因此当 n = -2147483648时执行 n = -n 会因越界而赋值出错。解决方法是先将 n 存入 long 变量 b ,后面用 b 操作即可。

2.3 代码

class Solution {
    public double myPow(double x, int n) {
        if (x == 0) return 0;
        long b = n;
        double res = 1.0;
        if (b < 0) {
            x = 1 / x;
            b = -b;
        }
        while (b > 0) {
            if((b & 1) == 1) res *= x;
            x *= x;
            b >>= 1;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度O(log 2 N):二分的时间复杂度为对数级别。
  • 空间复杂度O(1):res, b 等变量占用常数大小额外空间。

*数组中数字出现的次数 II

1 题目描述

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4

示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1

限制:

1 <= nums.length <= 10000
1 <= nums[i] < 2^31

2 解题(Java)

class Solution {
    public int singleNumber(int[] nums) {
        /*统计所有数字的各二进制位中1的出现次数,并对3求余,结果就是要求的数在该位的二进制数
        */
        
        int[] counts = new int[32];
        for(int num : nums) {
            for(int i = 0; i <= 31; i++) {
                counts[i] += num & 1;
                num >>= 1;
            }
        }
        int res = 0;
        for(int i = 31; i >= 0; i--) {
            // 左移操作+或运算,可将counts数组中各二进位的值恢复到数字res上,由于最后一步不用左移,所以左移不能在运算之后,应在之前
            res <<= 1;
            res |= counts[i] % 3;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度 O(N): 其中 N 为数组 nums 的长度,遍历数组使用O(N) 时间;
  • 空间复杂度 O(1): 数组 counts 长度恒为 32 ,占用常数大小的额外空间;

*数组中数字出现的次数

1 题目描述

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

限制:

2 <= nums.length <= 10000

2 解题(Java)

位运算:

class Solution {
    public int[] singleNumbers(int[] nums) {
        //第一步异或,相同的数都抵消了,得到两个不同的数异或后的结果
        int res = 0;
        for (int num : nums) {
            res ^= num;
        }
        // 这两数异或的结果肯定至少有一位为1,找到最低位的那个即可,则这两个数一个在此位为0,另一个为1
        int div = 1;
        while ((res & div) == 0) {
            div <<= 1;
        }
        // 按得到的最低位分组并异或操作,由于相同的数异或为0,所以最终两个分组里各自得到那两个不同的数
        int a = 0, b = 0;
        for (int num: nums) {
            if ((num & div) == 0) a ^= num;
            else b ^= num; 
        }
        return new int[]{a, b};
    }
}

3 复杂性分析

  • 时间复杂度:O(n):需要遍历数组两次;
  • 空间复杂度:O(1):需要常数空间存放若干变量;

*子集

1 题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2

输入:nums = [0]
输出:[[],[0]]

提示

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

2 解题(Java)

2.1 解法1(回溯)

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> tmp = new ArrayList<>();
    int[] nums;
    public List<List<Integer>> subsets(int[] nums) {
        this.nums = nums;
        backTrack(0);
        return res;
    }
    void backTrack(int x) {
        res.add(new ArrayList(tmp));
        for (int i = x; i < nums.length; i++) {
            tmp.add(nums[i]);
            backTrack(i+1);
            tmp.remove(tmp.size() - 1);
        }
    }
}

复杂性分析

时间复杂度O(n * 2 ^ n):一共 2 ^ n 个状态,每个状态使用 O(n) 的时间复制到答案列表中;
空间复杂度O(n):临时列表 temp 的空间代价是 O(n),递归时栈空间的代价为 O(n);

2.2 解法2(位运算)

解题思路

  1. 记原序列中元素的总数为 n。原序列中的每个数字 a_i的状态可能有两种,即「在子集中」和「不在子集中」;
  2. 我们用 1 表示「在子集中」,0 表示「不在子集中」,那么每一个子集可以对应一个长度为 n 的 0/1 序列,第 i 位表示 a_i 是否在子集中。例如,n = 3,a = {5,2,9} 时:

在这里插入图片描述

  1. 可以发现 0/1 序列对应的二进制数正好从 0 到 2 ^ (n - 1) 。因此可以枚举mask∈[0, 2 ^ (n - 1)],mask 的二进制表示是一个 0/1 序列,我们可以按照这个 0/1 序列在原集合当中取数。

代码

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();
        int n = nums.length;
        for (int mask = 0; mask < (1 << n); mask++) {
            temp.clear();
            for (int i=0; i<n; i++) {
                if ((mask & (1 << i)) != 0) {
                    temp.add(nums[i]);
                }
            }
            res.add(new ArrayList(temp));
        }
        return res;
    }
}

复杂性分析

  • 时间复杂度O(n * 2 ^ n):一共 2 ^ n 个状态,每个状态需要 O(n) 的时间来构造子集;
  • 空间复杂度O(n):临时列表 temp 的空间代价是 O(n);

*旋转图像

1 题目描述

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1

在这里插入图片描述

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]

输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2

在这里插入图片描述

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]

输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

示例 3

输入:matrix = [[1]]

输出:[[1]]

示例 4

输入:matrix = [[1,2],[3,4]]

输出:[[3,1],[4,2]]

提示

  • matrix.length == n
  • matrix[i].length == n
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

2 解题(Java)

将旋转转换为水平翻转+对角线翻转。

class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        // 先水平翻转
        for (int i = 0; i <= (n-1) / 2; i++) {
            for (int j = 0; j < n; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - 1 - i][j];
                matrix[n - 1 - i][j] = temp;
            }
        }
        // 再主对角线翻转
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
    }
}

3 复杂性分析

  • 时间复杂度O(N ^ 2):两次双层for循环,故为O(N ^ 2);
  • 空间复杂度O(1):数组就地操作,故为O(1);

*剪绳子 II

1 题目描述

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0] * k[1] *…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:

2 <= n <= 1000

2 解题(Java)

2.1 解题思路

  1. 数学推论一:将绳子以相等的长度等分,得到的乘积最大;
  2. 数学推论二:尽可能将绳子以长度3等分时,乘积最大;
  3. 切分规则:当把绳子尽可能切分成多个长度为3的片段时,可能剩余一段长度为1或2的情况:如果最后一段为2,保留,不再拆分为1+1;如果最后一段为1,把一份3+1替换为2+2,因为2 * 2 > 3 * 1;

大数求余解法

  • 大数越界:当a增大时,最后返回的3 ^ a以指数级别增长,可能超出int32甚至int64的取值范围,导致返回值错误;
  • 大数求余问题:在仅使用int32类型存储的前提下,正确计算x ^ a对p求余(即x ^ a % p)的值;
  • 解决方案:循环求余。基于求余运算规则:(xy)% p = [(x % p)(y % p)] % p;

循环求余

由于本题中x = 3 < p,所以x % p = x,于是有

  • x ^ a % p = [(x ^ (a-1) % p)(x % p)] % p = [(x ^ (a-1) % p)x] % p

2.2 算法流程

利用前面得到的公式,可通过循环操作依次求3 ^ 1, 3 ^ 2, …,3 ^ (a-1), 3 ^ a对p的余数,保证每轮中间值都在int64取值范围内。

2.3 代码

class Solution {
    public int cuttingRope(int n) {
        if (n < 4) return n - 1;
        else if (n == 4) return n;
        long res = 1;
        while (n > 4) {
            n -= 3;
            res = res * 3 % 1000000007;
        }
        return (int)(res * n % 1000000007);
    }
}

3 复杂性分析

  • 时间复杂度O(N):while循环时间复杂度为O(N);
  • 空间复杂度O(1):变量res占用常数大小的额外空间;

*数组中出现次数超过一半的数字

1 题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

限制:

1 <= 数组长度 <= 50000

2 解题(Java)

摩尔投票法: 核心理念为票数正负抵消 。

class Solution {
    public int majorityElement(int[] nums) {
        int vote = 0;
        int res = nums[0];
        for (int num : nums) {
            if (vote == 0) res = num;
            vote += res == num ? 1 : -1;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度 O(N): N 为数组 nums 长度,需遍历一次nums;
  • 空间复杂度 O(1): 定义变量占用常数大小的额外空间;

1~n 整数中 1 出现的次数*

1 题目描述

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5

示例 2:

输入:n = 13
输出:6

限制:

1 <= n < 2^31

2 解题(Java)

2.1 解题思路

  1. 将 1 ~ n 的个位、十位、百位、…的 1 出现次数相加,即为 1 出现的总次数;
  2. 设数字 n 是个 x 位数,记 n 的第 i 位为 ni,则可将n写为nxnx-1…n2n1:
    1. 称“ni”为当前位,记为cur;
    2. 将“ni-1ni-2…n2n1”称为低位,记为low;
    3. 将“nxnx-1…ni+2ni+1”称为高位,记为high;
    4. 位因子记为digit;

某位中 1 出现次数的计算方法

根据当前位 cur 值的不同,分为以下三种情况:cur=0/cur=1/cur=2,3,4…8,9

  1. 当cur=0时,此位1的出现次数只由高位high决定,计算公式为:high × digit;如下图所示,以 n = 2304 为例,求 digit = 10(即十位)的 1 出现次数。

在这里插入图片描述

  1. 当 cur = 1 时: 此位 1 的出现次数由高位 high 和低位 low 决定,计算公式为:high×digit+low+1;如下图所示,以 n = 2314 为例,求 digit = 10(即十位)的 1 出现次数。

在这里插入图片描述

  1. 当 cur=2,3,⋯,9 时: 此位 1 的出现次数只由高位 high 决定,计算公式为:(high+1)×digit;如下图所示,以 n = 2324为例,求 digit = 10(即十位)的 1 出现次数。

在这里插入图片描述
变量递推公式:

设计按照 “个位、十位、…” 的顺序计算,则 high / cur / low / digit 应初始化为:

high = n // 10
cur = n % 10
low = 0
digit = 1 # 个位

因此,从个位到最高位的变量递推公式为:

while high != 0 or cur != 0: # 当 high 和 cur 同时为 0 时,说明已经越过最高位,因此跳出
   low += cur * digit # 将 cur 加入 low ,组成下轮 low
   cur = high % 10 # 下轮 cur 是本轮 high 的最低位
   high /= 10 # 将本轮 high 最低位删除,得到下轮 high
   digit *= 10 # 位因子每轮 × 10

2.2 代码

class Solution {
    public int countDigitOne(int n) {
        int digit = 1, res = 0;
        int high = n / 10, cur = n % 10, low = 0;
        while(high != 0 || cur != 0) {
            if(cur == 0) res += high * digit;
            else if(cur == 1) res += high * digit + low + 1;
            else res += (high + 1) * digit;
            low += cur * digit;
            cur = high % 10;
            high /= 10;
            digit *= 10;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度 O(logn) :循环次数为数字 n 的位数,即 log 10 n , 循环内的计算操作使用 O(1) 时间,因此循环使用O(logn) 时间;
  • 空间复杂度 O(1) : 几个变量使用常数大小的额外空间;

表示数值的字符串*

1 题目描述

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、“5e2”、“-123”、“3.1416”、“-1E-16”、“0123"都表示数值,但"12e”、“1a3.14”、“1.2.3”、"±5"及"12e+5.4"都不是。

2 解题(Java)

class Solution {
    public boolean isNumber(String s) {
        //去掉首尾空格
        s = s.trim();
        //标记是否遇到相应的情况
        boolean numSeen = false;
        boolean dotSeen = false;
        boolean eSeen = false;
        for (int i=0; i<s.length(); i++) {
            if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
                numSeen = true;
            }
            else if (s.charAt(i) == '.') {
                if (dotSeen || eSeen) {//"."之前不能有“.”或"e",但“.1”是数字,所以不用加|| !numSeen
                	return false;
                }
                dotSeen = true;//"1."也是数字,所以不用加numSeen = false;
            }
            else if (s.charAt(i) == 'e' || s.charAt(i) == 'E') {
                //e之前不能出现e,e之前必须有数,e的前一位可以是.
                if(eSeen || !numSeen) {
                    return false;
                }
                eSeen = true;
                numSeen = false;//重置numSeen,确保e之后必须出现数
            }
            else if (s.charAt(i) == '-' || s.charAt(i) == '+') {
                //+—必须出现在0位置或者e后面的第一个位置
                if (i != 0 && s.charAt(i-1) != 'e' && s.charAt(i-1) != 'E') {
                    return false;
                }
            }
            else {//其他不合法字符
                return false;
            }
        }
        return numSeen;//必须有数字,且e之后也必须有数字
    }
}

3 复杂性分析

  • 时间复杂度O(N):N为字符串s的长度,判断需遍历字符串,使用O(N)时间;
  • 空间复杂度O(1):numSeen、dotSeen、eSeen占用常数大小的额外空间;

*确定两个字符串是否接近

1 题目描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2 解题(Java)

2.1 解题思路

  1. 根据题意,如果两个字符串word1和word2接近,必须且只要满足如下两个条件:

    1. word1和word2中包含同样种类的字符;
    2. 字符对应个数构成的集合的内容相等;
  2. 考虑用字符运算来完成,定义两个长度为26(题目说明只包含小写字母)的整型数组cnt1和cnt2,由字符串的每个字符进行-‘a’操作构成数组的索引,于是索引0代表‘a’,索引1代表’b’…;

  3. 遍历两字符串中的字符,执行cnt1[word1.charAt(i)-‘a’]++;cnt2[word2.charAt(i)-‘a’]++;于是可以分别得到两字符串中每个字符对应的个数;

  4. 对两数组进行字符包含判断,如果字符在其中一个字符串中个数为0而在另外一个字符串中个数不为0,则判定两字符串不接近;

  5. 经过字符包含判断后,还需判断字符对应个数构成的集合的内容相等,使用内置函数的排序功能先对两数组排序,然后比较两数组内容是否相等即可。

2.2 代码

class Solution {
    public boolean closeStrings(String word1, String word2) {
        if (word1.length() != word2.length()) return false;
        int len = word1.length();
        int[] count1 = new int[26], count2 = new int[26];
        for (int i=0; i<len; i++) {
            count1[word1.charAt(i)-'a']++;
            count2[word2.charAt(i)-'a']++;
        }
        for (int i=0; i<26; i++) {
            if ((count1[i]==0 && count2[i]>0) || (count1[i]>0 && count2[i]==0)) {
                return false;
            }
        }
        Arrays.sort(count1);
        Arrays.sort(count2);
        return Arrays.equals(count1, count2);
    }
}

3 复杂性分析

  • 时间复杂度O(N):字符串长度为N,for遍历字符串使用O(N)时间;
  • 空间复杂度O(1):定义数组及变量占用常数空间;

*把字符串转换成整数

1 题目描述

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2∧31, 2∧31 − 1]。如果数值超过这个范围,请返回 INT_MAX (2∧31 − 1) 或 INT_MIN (−2∧31) 。

示例 1:

输入: "42"
输出: 42

示例 2:

输入: "   -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
     我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42

示例 3:

输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。

示例 4:

输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
     因此无法执行有效的转换。

示例 5:

输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 
     因此返回 INT_MIN (231)

2 解题(Java)

  1. 字符转数字: “此数字的 ASCII 码” 与 “ 0 的 ASCII 码” 相减即可;
  2. 新建一个变量保存符号位,返回前加上正负;
  3. 遇到首个非数字的字符时,跳出循环;
  4. 从左向右遍历字符串,设当前位字符为 c,结果为 res,则res = 10×res+(ascii( c )−ascii(′0′));
  5. 判断越界:bndry = Integer.MAX_VALUE / 10;
    1. res>bndry:越界;
    2. res=bndry,x>Integer.MAX_VALUE % 10:越界或等于Integer的下界;
class Solution {
    public int myAtoi(String str) {
        char[] c = str.trim().toCharArray();
        if (c.length == 0) return 0;
        int res = 0, start = 1, sign = 1;
        if (c[0] == '-') sign = -1;
        else if (c[0] != '+') start = 0;
        for (int i = start; i < c.length; i++) {
            if (c[i] < '0' || c[i] > '9') break;
            if ((res > Integer.MAX_VALUE / 10) || (res == Integer.MAX_VALUE / 10 && c[i] > Integer.MAX_VALUE % 10 + '0')) {
                return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            }
            res = res * 10 + (c[i] - '0');
        } 
        return sign * res;
    }
}

3 复杂性分析

  • 时间复杂度 O(N): 其中 N 为字符串长度,遍历字符串使用 O(N) 时间;
  • 空间复杂度 O(N) : 删除首尾空格后需建立字符数组,最差情况下占用 O(N) 额外空间;

*剪绳子

1 题目描述

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:

2 <= n <= 58

2 解题(动态规划)

2.1 解题思路

  1. 当n >= 2时,可以拆分成至少两个正整数;
  2. 设k是拆分出的第一个正整数,剩下的部分是n-k,n-k可以拆分,也可以不拆分;
  3. 创建数组dp,dp[i]表示将正整数i拆分成至少两个正整数后,这些正整数的最大乘积;
  4. i >= 2,假设对i拆分出的第一个正整数是j(1<=j<i),有以下两种方案:
    1. i - j不再拆分,乘积为j * (i - j);
    2. i - j继续拆分,乘积为j * dp[i - j];
  5. 因此当j固定时,有dp[i] = max(j * (i - j),j * dp[i - j])。由于j的范围是[1, i-1],因此需遍历所有的j才能得到dp[i]的最大值,故而得到状态转移方程如下:
    在这里插入图片描述
  6. 最终得到dp[n]的值即为n拆分成至少两个正整数的和之后,这些正整数的最大乘积;

2.2 代码

class Solution {
    public int cuttingRope(int n) {
        int[] dp = new int[n+1];
        for (int i=2; i<=n; i++) {
            int curMax = 0;
            for (int j=1; j<i; j++) {
                curMax = Math.max(curMax, Math.max(j * (i - j), j * dp[i - j]));
            }
            dp[i] = curMax;
        }
        return dp[n];
    }
}

2.3 复杂性分析

  • 时间复杂度O(N ^ 2):双层for循环使用O(N ^ 2)时间;
  • 空间复杂度O(N):数组dp占用O(N)空间;

3 解题(数学)

3.1 解题思路

  1. 数学推论一:将绳子以相等的长度等分,得到的乘积最大;
  2. 数学推论二:尽可能将绳子以长度3等分时,乘积最大;
  3. 切分规则:当把绳子尽可能切分成多个长度为3的片段时,可能剩余一段长度为1或2的情况:如果最后一段为2,保留,不再拆分为1+1;如果最后一段为1,把一份3+1替换为2+2,因为2 * 2 > 3 * 1;

算法流程

  1. 当n <= 3时,按照规则应不切分,但由于题目要求至少剪成两段,因此剪出一段长度为1的绳子,返回n-1;
  2. 当n > 3时,求n除以3的整数部分a和余数部分b,共有三种情况:
    1. b=0,直接返回3 ^ a;
    2. b=1,将一个1+3转换为2+2,因此返回3 ^ (a-1) * 4;
    3. b=2,返回3 ^ a * 2;

3.2 代码

class Solution {
    public int cuttingRope(int n) {
        if(n <= 3) return n - 1;
        int a = n / 3, b = n % 3;
        if(b == 0) return (int)Math.pow(3, a);
        if(b == 1) return (int)Math.pow(3, a - 1) * 4;
        return (int)Math.pow(3, a) * 2;
    }
}

3.3 复杂性分析

  • 时间复杂度O(1):仅有求整、求余、次方运算;
  • 空间复杂度O(1):a和b占用常数大小额外空间;

*求1+2+…+n

1 题目描述

求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:

输入: n = 3
输出: 6

示例 2:

输入: n = 9
输出: 45

限制:

1 <= n <= 10000

2 解题(Java)

利用逻辑运算符的短路效应:

常见的逻辑运算符有三种,即 “与 && ”,“或 ∣∣ ”,“非 ! ” ;而其有重要的短路效应,如下所示:

if(A && B)  // 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false

if(A || B) // 若 A 为 true ,则 B 的判断不会执行(即短路),直接判定 A || B 为 true

本题需要实现 “当 n = 1 时终止递归” 的需求,可通过短路效应实现。

n > 1 && sumNums(n - 1) // 当 n = 1 时 n > 1 不成立 ,此时 “短路” ,终止后续递归

代码如下:

class Solution {
    int res;
    public int sumNums(int n) {
        boolean x = n > 1 && sumNums(n - 1) > 0;
        res += n;
        return res;
    }
}

3 复杂性分析

  • 时间复杂度 O(n) : 计算 n+(n−1)+…+2+1 需要开启 n 个递归函数;
  • 空间复杂度 O(n) : 递归深度达到 n ,系统使用 O(n) 大小的额外空间;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值