位运算技巧总结
n & (n - 1),移除二进制数n最低位的1,可用来判断是否为2的幂,配合逻辑右移可以计算二进制数中1的个数
n & (-n),获取二进制数n最低位的1,若n&(-n) = n,说明是2的幂
n & 1,取得二进制数n最低位1
s |= (1 << cur),用s来记录已经用过的数字
数字的位操作
7.整数反转(简单)★
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] ,就返回 0。 假设环境不允许存储 64 位整数(有符号或无符号)。
//使用取余,除法,乘法交替得到反转值
//本题重点在于如何判断是否超过范围,最终结论是
//res > Integer.MAX_VALUE/10 || res < Integer.MIN_VALUE/10时超出范围,推理见LeetCode题解
class Solution {
public int reverse(int x) {
int res = 0;
while(x != 0){
int temp = x % 10;
x = x / 10;
if(res > Integer.MAX_VALUE/10 || res < Integer.MIN_VALUE/10){
return 0;
}
res = res * 10 + temp;
}
return res;
}
}
9.回文数(简单)
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。注意负数都不是回文数
//数字反转,仍然有溢出问题
class Solution {
public boolean isPalindrome(int x) {
if(x < 0) return false;
return x == reverse(x);
}
public int reverse(int x){
int res = 0;
while(x != 0){
if(res > Integer.MAX_VALUE/10 || res < Integer.MIN_VALUE/10){
return 0;
}
res = res * 10 + x % 10;
x /= 10;
}
return res;
}
}
class Solution {
//由于字符串全部反转有溢出问题,故可以只反转后半部分
//即后半部分反转后与前半部分相同即可说明是回文串
public boolean isPalindrome(int x) {
if(x < 0 || (x != 0 && x%10 == 0)) return false; //注意个位为0的情况需要特殊处理
int res = 0;
while(x > res){ //x>res说明res还未到达一半
res = res * 10 + x % 10;
x /= 10;
}
return x == res || x == res / 10; //有奇数位和偶数位两种情况,如121,1221
}
}
479.最大回文数乘积(困难)
给定一个整数 n ,返回可表示为两个 n
位整数乘积的最大回文整数。因为答案可能非常大,所以返回它对 1337
取余 。
输入: n = 2
输出: 987
解释: 99 x 91 = 9009, 9009 % 1337 = 987
class Solution {
//枚举回文串,由于对称,故只需要从大到小枚举左半边即可,这是关键所在
//两个n位数相乘,结果最多为2n位数,故左半边只需要枚举n位数即可
//对每一个回文串分解因子,判断是否能够被两个n位数整除
public int largestPalindrome(int n) {
if(n == 1) return 9;
int upper = (int)Math.pow(10, n) - 1; //左边界上限
int ans = 0;
for(int left = upper; ans == 0; left--){ //枚举左半部分
long num = left; //完整回文串
int temp = left;
while(temp != 0){ //从左半边转换为完整的回文串
num = num * 10 + temp % 10;
temp /= 10;
}
for(long i = upper; i * i >= num; i--){ //从大到小枚举因子,直到num的平方根即可
if(num % i == 0){
ans = (int)(num % 1337);
break;
}
}
}
return ans;
}
}
564.寻找最近的回文数(困难)★
给定一个表示整数的字符串 n
,返回与它最近的回文整数(不包括自身)。如果不止一个,返回较小的那个。“最近的”定义为两个整数差的绝对值最小。
输入: n = "123"
输出: "121"
tip:注意本解法如何使用len&1来解决奇偶长度判断问题
class Solution {
//分情况讨论,枚举所有可能情况并比较大小
//1.原字符串前半部分替换后半部分,如1234->1221
//2.原字符串前半部分加一替换后半部分,如1299->1331
//3.原字符串前半部分减一替换后半部分,如1900->1881
//4.特殊情况,位数变动,如9999和1001
public String nearestPalindromic(String n) {
long num = Long.parseLong(n);
long ans = -1;
int len = n.length();
List<Long> resList = getRes(n);
for(long res : resList){
if(res != num){
if(ans == -1 || Math.abs(res - num) < Math.abs(ans - num) ||
(Math.abs(res - num) == Math.abs(ans - num) && res < ans)){
ans = res;
}
}
}
return Long.toString(ans);
}
public List<Long> getRes(String n){
int len = n.length();
List<Long> res = new ArrayList<>(){{
add((long)(Math.pow(10,len-1) - 1)); //特殊情况,9999类型
add((long)(Math.pow(10,len) + 1)); //特殊情况,1001类型
}};
long pre = Long.parseLong(n.substring(0,(len+1)/2)); //取出字符串前半部分
for(long i = pre-1; i <= pre+1; i++){ //枚举情况1-3
StringBuffer sb = new StringBuffer();
String prefix = String.valueOf(i); //前半部分
sb.append(prefix); //装入前半部分
StringBuffer suffix = new StringBuffer(prefix).reverse();//前半部分反转为后半部分
//按位与,当len为奇数,则prefix长度为偶数,len&1为1,substring(1),不取第一个数
//当len为偶数,则prefix长度为奇数,len&1为0,substring(0)取自身
sb.append(suffix.substring(len & 1));
res.add(Long.parseLong(sb.toString()));
}
return res;
}
}
231.2的幂(简单)★
给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。 如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。
遍历解法
class Solution {
public boolean isPowerOfTwo(int n) {
if(n <= 0) return false;
if(n == 1) return true;
while(n > 1){
if(n % 2 != 0){
return false;
}
n /= 2;
}
return true;
}
}
位运算解法
//n & (n - 1),移除二进制数n最低位的1,若移除后为0,说明是2的幂
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n-1)) == 0;
}
}
//n & (-n),获取二进制数n最低位的1,若n&(-n) = n,说明是2的幂
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (-n)) == n;
}
}
342.4的幂(简单)
给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == 4^x
class Solution {
//首先判断是否为2的幂,然后判断是否为4的幂
//若为4的幂,则其二进制为1后接偶数个0,设为n
//取一个mask,奇数位为0,偶数位为1,n&mask可以取出奇数位
public boolean isPowerOfFour(int n) {
return n > 0 && ((n & n-1)) == 0 && (n & 0xaaaaaaaa) == 0;
}
}
326.3的幂(简单)
给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。 整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3x
class Solution {
public boolean isPowerOfThree(int n) {
if(n < 1) return false;
while(n != 1){
if(n%3 != 0){
return false;
}
n /= 3;
}
return true;
}
}
504.七进制数(简单)
给定一个整数 num
,将其转化为 7 进制,并以字符串形式输出。
class Solution {
public String convertToBase7(int num) {
if(num == 0) return "0";
boolean flag = num < 0;
num = Math.abs(num);
StringBuffer res = new StringBuffer();
while(num > 0){
res.append(String.valueOf(num%7));
num /= 7;
}
if(flag == true){
res.append("-");
}
return res.reverse().toString();
}
}
263.丑数(简单)
丑数 就是只包含质因数 2、3 和 5 的正整数。给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false,1是第一个丑数。
class Solution {
public boolean isUgly(int n) {
if(n <= 0) return false;
while(n > 0 && n%2 == 0){
n/=2;
}
while(n > 0 && n%3 == 0){
n/=3;
}
while(n > 0 && n%5 == 0){
n/=5;
}
if(n == 1) return true;
return false;
}
}
190.颠倒二进制位(简单)★
颠倒给定的 32 位无符号整数的二进制位。
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
注意输入的n是长度为32的二进制字符串
逐位逆序
public class Solution {
//从低位到高位枚举n的每一位,枚举时从高位到低位添加到res中,n每次要逻辑右移
public int reverseBits(int n) {
int res = 0;
for(int i = 0; i < 32 && n != 0; i++){
//n&1取得n的最低位,<<(31-i)将这个位移动到逆序对应的位置
//res|= ...即res = res | ...,每次将新的位和之前的res整合到一起
res |= (n & 1) << (31 - i);
n >>>= 1; //n每次逻辑右移一位
}
return res;
}
}
使用API
public class Solution {
public int reverseBits(int n) {
return Integer.reverse(n);
}
}
位运算分治
要反转一个二进制串,可以将其均分为左右两部分
左右部分再分别递归进行反转,然后左边拼接到右边后面即可。
对于递归的最底层,我们需要交换所有奇偶位:
取出所有奇数位和偶数位;
将奇数位移到偶数位上,偶数位移到奇数位上。
类似地,对于倒数第二层,每两位分一组,按组号取出所有奇数组和偶数组
然后将奇数组移到偶数组上,偶数组移到奇数组上。以此类推。
public class Solution {
private static final int M1 = 0x55555555; // 01010101010101010101010101010101
private static final int M2 = 0x33333333; // 00110011001100110011001100110011
private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
public int reverseBits(int n) {
n = n >>> 1 & M1 | (n & M1) << 1;
n = n >>> 2 & M2 | (n & M2) << 2;
n = n >>> 4 & M4 | (n & M4) << 4;
n = n >>> 8 & M8 | (n & M8) << 8;
return n >>> 16 | n << 16;
}
}
191.位1的个数(简单)
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数.
public class Solution {
// 逐位计算1的个数
public int hammingWeight(int n) {
int count = 0;
for(int i = 0; i < 32 && n != 0; i++){
if((n & 1) == 1){
count++;
}
n >>>= 1;
}
return count;
}
}
public class Solution {
// n & n-1可以把n最低位的1变为0,故n的转化次数即为1的个数
public int hammingWeight(int n) {
int count = 0;
while(n != 0){
n &= n-1;
count++;
}
return count;
}
}
476.数字的补数(简单)
对整数的二进制表示取反(0 变 1 ,1 变 0)后,再转换为十进制表示,可以得到这个整数的补数。例如,整数 5 的二进制表示是 "101" ,取反后得到 "010" ,再转回十进制表示得到补数 2 。
class Solution {
//首先要找到num二进制表示时最高位的1,设最高位的1为i位时,2^i <= num < 2^(i+1)
//再构造2^(i+1)-1与num进行异或,即可得到补数的二进制数
public int findComplement(int num) {
int i = 1; //num二进制最高位1的位置
while(i <= 30 && num >= 1<<i){ //i等于31时会越界
i++;
}
int mask = i == 31 ? 0x7fffffff : (1 << i)-1;
return num ^ mask;
}
}
461.汉明距离(简单)
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x
和 y
,计算并返回它们之间的汉明距离
输入:x = 1, y = 4
输出:2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
class Solution {
//两数异或,再遍历统计位为1的个数
public int hammingDistance(int x, int y) {
int n = x ^ y;
int count = 0;
while(n != 0){
if((n & 1) == 1){
count++;
}
n >>>= 1;
}
return count;
}
}
//此方法比上一个方法好,因为此法有多少个1就计算多少次,而前者的计算包括了0
class Solution {
public int hammingDistance(int x, int y) {
int s = x ^ y, ret = 0;
while (s != 0) {
s &= s - 1; //s & s-1 为删除s最右侧1的结果,删除次数即为1的个数
ret++;
}
return ret;
}
}
477.汉明距离总和(中等)★
两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。 给你一个整数数组 nums,请你计算并返回 nums 中任意两个数之间 汉明距离的总和 。
class Solution {
//暴力解法
public int totalHammingDistance(int[] nums) {
int sum = 0;
for(int i = 0; i < nums.length; i++){
for(int j = i+1; j < nums.length; j++){
sum += HammingDistance(nums[i], nums[j]);
}
}
return sum;
}
public int HammingDistance(int n1, int n2){
return Integer.bitCount(n1 ^ n2);
}
}
//按位遍历解法
class Solution {
//可以按照位来计算数组中所有元素的汉明距离
//对于第i位,若数组中第i位为1的元素有x个,为0的有y个,则其汉明距离总和为xy
//10^9 < 2^30,故可以直接枚举到第30位
public int totalHammingDistance(int[] nums) {
int res = 0;
int n = nums.length;
for(int i = 0; i < 30; i++){ //枚举位数
int count = 0;
for(int num : nums){
count += ((num >> i) & 1); //取到第i位的值,count记录有多少个1
}
res += count * (n-count);
}
return res;
}
}
693. 交替位二进制数(简单)
给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。
class Solution {
public boolean hasAlternatingBits(int n) {
int flag = n & 1; //标记n的最低位
while(n != 0){
n >>>= 1;
if(flag == (n & 1)) return false; //若当前位于前一位不同,则返回false
flag = (n & 1);
}
return true;
}
}
class Solution {
public boolean hasAlternatingBits(int n) {
int a = n ^ (n >> 1); //当n为交替01时,n>>1与n异或可以得到全1
return (a & (a + 1)) == 0; //当a为全1时,a&(a+1)==0
}
}
393.UTF-8编码验证(中等)★
给定一个表示数据的整数数组 data ,返回它是否为有效的 UTF-8 编码。UTF-8 中的一个字符可能的长度为 1 到 4 字节,遵循以下的规则:
对于 1 字节 的字符,字节的第一位设为 0 ,后面 7 位为这个符号的 unicode 码。
对于 n 字节 的字符 (n > 1),第一个字节的前 n 位都设为1,第 n+1 位设为 0
后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。
这是 UTF-8 编码的工作方式:
Number of Bytes | UTF-8 octet sequence
| (binary)
--------------------+---------------------------------------------
1 | 0xxxxxxx
2 | 110xxxxx 10xxxxxx
3 | 1110xxxx 10xxxxxx 10xxxxxx
4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
示例:
输入:data = [197,130,1]
输出:true
解释:数据表示字节序列:11000101 10000010 00000001。
这是有效的 utf-8 编码,为一个 2 字节字符,跟着一个 1 字节字符。
class Solution {
static final int MASK1 = 1 << 7;
static final int MASK2 = (1<<7) + (1<<6);
//模拟,遍历每个头字节,判断它需要的10xxxxxx个数是否符合规则
public boolean validUtf8(int[] data) {
int index = 0;
int len = data.length;
while(index < len){
int num = data[index];
int need = getNeed(num); //获取当前数字需要多少个10xxxxxx
if(need < 0 || (index+need) >= len) return false;
for(int i = 1; i <= need; i++){ //遍历后面的need个数字
if(!isVaild(data[index+i])) return false;
}
index += (need+1); //跳到下一个头字节
}
return true;
}
public int getNeed(int n){
int mask = MASK1;
int res = 0;
if((n & mask) == 0) return 0;
while((n & mask) != 0){ //从高位到低位检测是否为1
res++;
if(res > 4){ //超过4个1,错误
return -1;
}
mask >>= 1; //mask右移,判断下一位
}
return res > 1 ? res-1 : -1; //1的位数为1时返回-1,1的位数大于1时返回需要的10xxxx个数
}
public boolean isVaild(int n){ //判断是否为10xxxxx
int mask = MASK2;
return (n & mask) == MASK1;
}
}
172.阶乘后的零(中等)★
给定一个整数 n ,返回 n! 结果中尾随零的数量。
分析:尾随零只能由10和5 * 2得到,10实际上也是5 * 2, 因数2的数量必然多于5,故尾随0的数量取决于因数5的数量
class Solution {
//找出n的因数5的个数,5的倍数如10,15会贡献一个因数5
//25的倍数会额外多贡献一个因数5....
public int trailingZeroes(int n) {
int res = 0;
while(n != 0){
n /= 5; //除5计算出当前层贡献的因数5,
res+=n;
}
return res;
}
}
458.可怜的小猪(困难)★
有 buckets 桶液体,其中 正好有一桶 含有毒药,其余装的都是水。它们从外观看起来都一样。为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 minutesToTest 分钟时间来确定哪桶液体是有毒的。
喂猪的规则如下:
选择若干活猪进行喂养
可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。
小猪喝完水后,必须有 minutesToDie 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。
过了 minutesToDie 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。
重复这一过程,直到时间用完。
给你桶的数目 buckets ,minutesToDie 和 minutesToTest ,返回 在规定时间内判断哪个桶有毒所需的 最小 猪数 。
class Solution {
//使用信息论的信息熵概念解题,当全部是等概率事件时,信息熵为log(n),n为事件可能的状态
//药水有buckets种状态,n只小猪在m轮实验中有(m+1)^n种状态(在每一轮死去或者活到最后,m+1种)
//故(m+1)^n >= buckets,用信息熵计算就是n * log(m+1) >= log(buckets)
//在本题中,小猪带来的信息熵 >= 判断药瓶需要的信息熵 是 能够找到实际操作方法 的充分必要条件
//也可以将其看成编码方式,buckets为需要编码的数,m+1为编码进制,得到的编码长度为小猪数量
public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
int m = minutesToTest / minutesToDie;
return (int)Math.ceil(Math.log(buckets) / Math.log(m+1) - 1e-5);//需要处理浮点数精度问题
}
}
//tip:本题也可以使用动态规划的方法,但是条件判断和逻辑过于复杂
258.各位相加(简单)
给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
输入: num = 38
输出: 2
解释: 各位相加的过程为:
38 --> 3 + 8 --> 11
11 --> 1 + 1 --> 2
由于 2 是一位数,所以返回 2。
模拟
class Solution {
public int addDigits(int num) {
while(num >= 10){
int sum = 0;
while(num != 0){
sum += num % 10;
num /= 10;
}
num = sum;
}
return num;
}
}
数学方法★
若num>0,且是9的倍数,则数根为9
class Solution {
public int addDigits(int num) {
if(num == 0) return 0;
if(num % 9 == 0) return 9;
return num%9;
}
}
319.灯泡开关(中等)
初始时有 n 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。
第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 i 轮,你每 i 个灯泡就切换第 i 个灯泡的开关。直到第 n 轮,你只需要切换最后一个灯泡的开关。
找出并返回 n 轮后有多少个亮着的灯泡。
class Solution {
//根据题意,最后只有数值为i²的灯亮着
//若为素数,如7,则只能分解为1×7,第7轮时关闭
//若不为素数,可以拆分为两不同数的乘积,如12,所有因子为2,3,4,6,12,在这些轮次都会被改变,奇数个轮次,最后为关闭状态
//若不为素数,可以拆分为i×i,如16,因子为2,4,8,16,偶数个轮次,最后为开启状态
public int bulbSwitch(int n) {
return (int)Math.sqrt(n+0.5);//+0.5防止出现精度问题
}
}
405.数字转换为十六进制数(简单)
给定一个整数,编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用 补码运算 方法。注意不能有前导0.如26->1a, -1->ffffffff
class Solution {
public String toHex(int num) {
if(num == 0) return "0";
StringBuffer sb = new StringBuffer();
for(int i = 7; i >= 0; i--){ //以2进制的4位为一个单位,遍历高位到低位
int val = (num >> i * 4) & 0xf; //获取当前的4位数值,相当于直接从2进制转为16进制,不用额外处理负数
if(val > 0 || sb.length() > 0){ //防止出现前导0
char ch = val < 10 ? (char)('0'+val) : (char)('a'+(val-10));
sb.append(ch);
}
}
return sb.toString();
}
}
实际上计算机存储的就是二进制数,进制转换时直接从二进制开始转换即可,可以不用%/操作
171.Excel表列序号(简单)
给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
class Solution {
public int titleToNumber(String columnTitle) {
int res = 0;
for(int i = 0; i < columnTitle.length(); i++){
res = res * 26 + (columnTitle.charAt(i) - 'A' + 1);
}
return res;
}
}
168.Excel表列名称(简单)★
给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
//相当于将10进制数转换为26进制数,但是不同的是这里没有0,但有26
//故在进行转换时要对取余操作和整除操作做特殊处理
class Solution {
public String convertToTitle(int columnNumber) {
StringBuffer res = new StringBuffer();
while(columnNumber != 0){
int temp = (columnNumber - 1) % 26 + 1; //这样即可将余数控制在1-26
res.append((char)('A' + temp - 1));
columnNumber = (columnNumber - temp) / 26;//要减去上一轮已经计算过的数,因为可能出现26
}
return res.reverse().toString();
}
}
670.最大交换(中等)
给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。
输入: 2736
输出: 7236
解释: 交换数字2和数字7
class Solution {
//把所有位中最大的数字换到第一位,从后往前遍历,优先换低位的数
public int maximumSwap(int num) {
if(num <= 11) return num;
String str = String.valueOf(num);
int len = str.length();
char[] arr = str.toCharArray();
for(int i = 0; i < len - 1; i++){ //i指向数字的最高位
char max = arr[i]; //本轮中所有低位数字的最大值
int index = i; //最大值对应的下标
for(int j = len - 1; j > i; j--){ //j从数字最低位到i
if(arr[j] > max){
max = arr[j];
index = j;
}
}
if(index != i){ //若有比高位更大的数字,交换并退出循环
char temp = arr[i];
arr[i] = max;
arr[index] = temp;
break;
}
}
return Integer.parseInt(new String(arr));
}
}
数位dp
233.数字1的个数(困难)★
给定一个整数 n
,计算所有小于等于 n
的非负整数中数字 1
出现的个数。
class Solution {
//依次计算每一位上1的个数
//以123456计算百位上的1为例,从[100,999]百位上包含了100个1,故123456/1000 * 100
//同时还余下123456%1000=456,在余下的这个数m
//若100<=m<200,则1的个数为,m-100+1
//若m<100,则1的个数为0,此时m-100+1<0,要限制其为0,故max(m-100+1, 0)
//若m>=200,则1的个数为100,此时m-100+1>100,要限制其为100.故min(m-100+1, 100)
//整合为123456/1000 * 100 + min(max(m-100+1, 0), 100)
//其他位上的计算同理,变化基数100即可
public int countDigitOne(int n) {
int count = 0;
int base = 1;
while(n >= base){
count += (n/(base*10) * base + Math.min(Math.max(n%(base*10)-base+1, 0), base));
base *= 10;
}
return count;
}
}
数位Dp思路
一般数位Dp的思路:
构造函数f(i,mask,isLimit,isNum),返回所求项目
i:从i开始从左到右填的数字
mask:前面填的数字的集合
isLimit:表示对当前位填的数字是否有限制,比如限制s=123,第二位填了2,则第三位只能填到s[i]=3
isNum:表明前面是否填了数字,防止出现前导0,若为true则当前位可以从0开始,若为false则从1开始
利用一个dp数组进行记忆化,将遍历过的结果记录下来
//具体到本题
class Solution {
//数位dp解法
char s[]; //将数字n转换为s字符数组
int dp[][]; //dp[i][j]用来记忆化
public int countDigitOne(int n) {
s = String.valueOf(n).toCharArray();
int len = s.length;
dp = new int[len][len];
for(int i = 0; i < len; i++) Arrays.fill(dp[i], -1);
return f(0, 0, true);
}
//f函数返回n以下有多少个1
//i表示从左到右第i位,count表示累积了多少个1,isLimit表示当前位上填的数是否受到限制
//若为true,则只能填到s[i],若为false,则可以填到9
//只需要记忆化i和count,故只有当isLimit为false时才进行记忆化,因为每个isLimit=True只会出现一次
int f(int i, int count, boolean isLimit){
if(i == s.length) return count;
if(!isLimit && dp[i][count] >= 0) return dp[i][count]; //若之前已经有相同的f计算过且记忆化了,直接使用其结果,注意必须是无限制的
int res = 0;
for(int n = 0, up = isLimit ? s[i] - '0' : 9; n <= up; n++){ //枚举填入的数字n
res += f(i+1, count + (n==1 ? 1 : 0), isLimit && n == up); //前一位是限制的且当前位填最大值时才会继续限制,res不断累加下一种情况
}
if(!isLimit) dp[i][count] = res; //只有无限制时,才将结果进行记忆化
return res;
}
}
902.最大为N的数字组合(困难)★
给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = ['1','3','5'],我们可以写数字,如 '13', '551', 和 '1351315'。返回 可以生成的小于或等于给定整数 n 的正整数的个数 。
输入:digits = ["1","3","5","7"], n = 100
输出:20
解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.
class Solution {
//数位dp解法
int dp[]; //本题的dp只需要一维
char s[];
String d[];
public int atMostNGivenDigitSet(String[] digits, int n) {
s = String.valueOf(n).toCharArray();
int len = s.length;
dp = new int[len];
for(int i = 0; i < len; i++) Arrays.fill(dp, -1);
d = digits;
return f(0, true, true);
}
//i记录从左到右第i位,isLimit记录当前位所填的数是否受到限制,isJump表示当前位是否可以为0即跳过
int f(int i, boolean isLimit, boolean isJump){
if(i == s.length) return isJump ? 0 : 1;//遍历到最后一位时,当前位若跳过则不计数
if(!isLimit && !isJump && dp[i] != -1) return dp[i];
int res = 0;
int up = isLimit ? s[i]-'0' : 9;
if(isJump) res += f(i+1, false, true);
for(String n : d){
int num = Integer.parseInt(n);
if(num > up) break;
res += f(i+1, isLimit && num==up, false);
}
if(!isLimit && !isJump) dp[i]=res;
return res;
}
}
357.统计各位数字都不同的数字个数(中等)★
给你一个整数 n ,统计并返回各位数字都不同的数字 x 的个数,其中 0 <= x < 10^n 。
输入:n = 2
输出:91
解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。
普通解法
//每位数字都不同,可以应用乘法原理直接计算出有多少种组合,最高位可以有9种可能,后面的次高位也有9种可能,每位可以选的数依次递减
class Solution {
public int countNumbersWithUniqueDigits(int n) {
if(n == 0) return 1;
int last = 9;
int res = 10; //一位数有10种
for(int i = 2; i <= n; i++){
int cur = last * (10 - i + 1); //cur累乘
res += cur; //res累加
last = cur;
}
return res;
}
}
进阶解法
普通解法的局限在于只能够根据给定的位数计算,而不能根据给定的任意上限计算
若给定一个任意区间[a, b],可以根据数位dp方法进行计算,count(a,b) = count(0,b) - count(0,a)
设count(0,a)为dp(a),对于a,分三种情况
1.位数比a少,此时数字的选择没有限制,直接按位数计算即可
2.位数与a相同,但是最高位小于a,其他低位同样没有限制,直接按位数计算
3.位数与a相同,但是最高位等于a,设第k位为x(不为最高位),为了满足大小关系限制,
只能在[0,x-1]范围内取数(不能与x一样),且还要满足相同数字只使用一次的限制,
故需要用一个变量来记录数字的使用情况,统计同时符合两个条件的数字数量
tip:为了快速得出剩下的n-k位有多少种选择,还需要预处理乘积数组f[i][j]=i*(i+1)*...*j
class Solution {
static int[][] f = new int[10][10]; //预定义数组
static {
for(int i = 1; i < 10; i++){
for(int j = i; j < 10; j++){
int cur = 1;
for(int k = i; k <= j; k++){
cur *= k;
}
f[i][j] = cur; //f[i][j] = i * ... * j
}
}
}
int dp(int x){
int t = x;
List<Integer> nums = new ArrayList<>(); //将x按位数拆分为nums
while(t != 0){
nums.add(t%10);
t /= 10;
}
int n = nums.size(); //n为x的位数
if(n <= 1) return x+1; //[0, x]
int ans = 0;
//遍历当前位,先取到小于当前位的数值个数,后续累乘数组快速得到其他位的所有情况
//下一个循环相当于上一位已经确定与x一样,再计算下一位小于x数值个数,累乘,循环下一位相当于确定了上一位
//i为位数,从最高位开始,p为已经确定了多少位数,s记录使用过的数字
for(int i = n - 1, p = 1, s = 0; i >= 0; i--, p++){
int cur = nums.get(i); //cur为x当前位数的值
int count = 0;
//计算出当前位有多少数字能用(比当前位小,没有用过)
for(int j = cur - 1; j >= 0; j--){ //j为除了当前位数外,可以选择的值
if(i == n-1 && j == 0) continue; //当前位为首位且选择的数值为0
if(((s >> j) & 1) == 0) count++;//s记录了使用过的数,判断当前选择的数是否使用过
}
//确定了p个位,剩下的位可以任选,a为上界,b为下界,已经确定的位数的情况有count种,乘剩下的位数b * ... * a即f[b][a]
int a = 10 - p, b = a - (n - p) + 1;
ans += b <= a ? count * f[b][a] : count;
if(((s >> cur) & 1) == 1) break; //若s已经记录过,则跳过
s |= (1 << cur); //用s记录使用过的数字,如当前位为7,则s=10000000
if (i == 0) ans++; //若i到最后一位所有数字都不一样,即x本身也符合条件,加上1
}
//情况1,位数比n少
ans += 10; //只有个位数的情况
for(int i = 2, last = 9; i < n; i++){ //i代表位数,last代表可选数字
int cur = last * (10 - i + 1);
ans += cur;
last = cur;
}
return ans;
}
public int countNumbersWithUniqueDigits(int n) {
return dp((int)Math.pow(10, n) - 1);
}
}
400.第N位数字(中等)
给你一个整数 n
,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...]
中找出并返回第 n
位上的数字。
输入:n = 11
输出:0
解释:第 11 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 0 ,它是 10 的一部分。
class Solution {
public int findNthDigit(int n) {
int i = 1; //记录当前是几位数
int count = 9; //记录当前位数所有数位数的总和
while(n > (long)i * count){
n -= i * count;
count *= 10;
i++;
}
int index = n - 1;
int start = (int)Math.pow(10, i-1);
int num = start + index / i; //当前数字,注意之前的index为什么要n-1
int digitIndex = index % i; //当前数字要取的位数的位置
int digit = (num / (int)(Math.pow(10, i - digitIndex - 1))) % 10;//把要取数字移到最低位再对10取余,即取出指定index位置的数字
return digit;
}
}
简单数学题
492.构造矩形(简单)
设计有一个矩形
- 你设计的矩形页面必须等于给定的目标面积。
- 宽度
W
不应大于长度L
,换言之,要求L >= W
。 - 长度
L
和宽度W
之间的差距应当尽可能小。
返回一个 数组 [L, W]
,其中 L
和 W
是你按照顺序设计的网页的长度和宽度。
输入: 4
输出: [2, 2]
解释: 目标面积是 4, 所有可能的构造方案有 [1,4], [2,2], [4,1]。
但是根据要求2,[1,4] 不符合要求; 根据要求3,[2,2] 比 [4,1] 更能符合要求. 所以输出长度 L 为 2, 宽度 W 为 2。
class Solution {
public int[] constructRectangle(int area) {
int w = (int)Math.sqrt(area);
while(area % w != 0){ //从根号area开始枚举较小的w,直到找到可以被整除的w的最大值
w--;
}
return new int[]{area / w, w};
}
}
29.两数相除(中等)
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。返回被除数 dividend 除以除数 divisor 得到的商。且结果要截去小数部分。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。
基础解法
//用减法代替乘法,被除数逐个减去除数,结果累加,直到被除数小于除数
//注意要先对溢出的情况进行判断,且将被除数和除数都转为负数,保证不会出现越界的情况
class Solution {
public int divide(int dividend, int divisor) {
//先处理可能越界的情况
if(dividend == Integer.MIN_VALUE){
if(divisor == 1){
return Integer.MIN_VALUE;
}
if(divisor == -1){
return Integer.MAX_VALUE;
}
}
if(divisor == Integer.MIN_VALUE){
if(dividend == divisor){
return 1;
}else{
return 0;
}
}
if(dividend == 0) return 0;
int res = 0;
boolean flag = false; //结果的符号是否需要改变
if(dividend > 0){
dividend = -dividend;
flag = !flag;
}
if(divisor > 0){
divisor = -divisor;
flag = !flag;
}
while(dividend <= divisor){
dividend -= divisor;
res++;
}
return flag == true ? -res : res;
}
}
进阶解法
class Solution {
public int divide(int dividend, int divisor) {
//先处理可能越界的情况
if(dividend == Integer.MIN_VALUE){
if(divisor == 1){
return Integer.MIN_VALUE;
}
if(divisor == -1){
return Integer.MAX_VALUE;
}
}
if(divisor == Integer.MIN_VALUE) return dividend == divisor ? 1 : 0;
if(dividend == 0) return 0;
boolean rev = false; //记录符号是否需要取反
if (dividend > 0) {
dividend = -dividend;
rev = !rev;
}
if (divisor > 0) {
divisor = -divisor;
rev = !rev;
}
//设被除数为x,除数为y,结果为z,要求y*z <= x < y*(z+1),用二分查找找到满足条件的最大的z
//由于结果为2^32的情况已经判断过,故结果最大为2^32 - 1,从1开始二分查找,若未找到则结果为0
//用快速乘的方式实现乘法(类似快速幂),且要注意越界问题(判断y+y>x时用用减法)
//注意这里的divisor和dividend都是负数,故divisor*mid>=dividend时mid要变大
int left = 1, right = Integer.MAX_VALUE, ans = 0;
while(left <= right){
int mid = left + ((right - left)>>1);
boolean check = quickAdd(divisor, mid, dividend);//检查是否divisor*mid>=dividend
if(check){
ans = mid;
//判断是否溢出
if(mid == Integer.MAX_VALUE) break;
left = mid + 1;
}else{
right = mid - 1;
}
}
return rev ? -ans : ans;
}
//快速乘(使用加法实现乘法运算),类似于快速幂(使用乘法实现幂运算)
//判断是否 y * z >= x,注意x和y是负数,z是正数
public boolean quickAdd(int y, int z, int x){
int ans = 0;
while(z != 0){
if((z & 1) != 0){ //若z的最低位为1,需要加上对应的数字
if(ans < x - y){ //溢出判断时使用减法,需要保证 ans + y >= x
return false;
}
ans += y;
}
if(z != 1){
if(y < x - y){ //需要 y + y >= x
return false;
}
y += y;
}
//不能使用除法
z >>= 1;
}
return true;
}
}
507.完美数(简单)
对于一个 正整数,如果它和除了它自身以外的所有 正因子 之和相等,我们称它为 「完美数」。给定一个 整数 n, 如果是完美数,返回 true;否则返回 false。
输入:num = 28
输出:true
解释:28 = 1 + 2 + 4 + 7 + 14
1, 2, 4, 7, 和 14 是 28 的所有正因子。
class Solution {
public boolean checkPerfectNumber(int num) {
if(num == 1) return false;
int sum = 1;
for(int i = 2; i * i <= num; i++){ //将num开方转化为i的乘法
if(num % i == 0){
sum += i;
if(num/i != i){ //num = i *i时只能计算一个i
sum += num/i;
}
}
}
return sum == num;
}
}
快速幂
50.Pow(x,n)(中等)
实现 pow(x, n),即计算 x
的整数 n
次幂函数
快速幂方法
class Solution {
//快速幂解法
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickPow(x, N) : 1.0 / quickPow(x, -N);
}
public double quickPow(double x, long N){
double res = 1;
while(N > 0){
if(N % 2 != 0){ //二进制对应位数为1的需要乘上对应的数
res *= x;
}
x *= x; //不断平方,对应每个二进制位
N /= 2; //每次取N的二进制最低位
}
return res;
}
}
超级次方(中等)★
你的任务是计算 ab
对 1337
取模,a
是一个正整数,b
是一个非常大的正整数且会以数组形式给出。
输入: a = 2147483647, b = [2,0,0]
输出: 1198
注意 1 <= b.length <= 2000
class Solution {
//设b数组代表的数为k,a^k=a^((k/10)*10 + k%10))=(a^(k/10))^10 * a^(k%10)
//k/10即b的0~(len-2)位,k%10即b的最后一位,问题规模被不断缩小,可以使用递归
int MOD = 1337;
public int superPow(int a, int[] b) {
return dfs(a, b, b.length - 1);
}
public int dfs(int a, int[] b, int index){
if(index == -1) return 1;
return quickPow(dfs(a, b, index - 1), 10) * quickPow(a, b[index]) % MOD;
}
public int quickPow(int a, int n){
int res = 1;
a = a % MOD;
while(n > 0){
if((n & 1) != 0){
res = res * a % MOD;
}
a = a * a % MOD;
n >>= 1;
}
return res;
}
}