剑指offerⅣ(Java 持续更新...)

目录

DAY16 

15:二进制中1的个数

65:不用加减乘除做加法(看不懂,理解不了)

46:把数字翻译成字符串

DAY17

56-Ⅰ:数组中数字出现的次数(是人可以想出来的解法吗)

56-Ⅱ:数组中数字出现的次数

DAY18

39:数组中出现次数超过一半的数字(数学不好真吃亏啊)

66:构建乘积数组

DAY19

14-Ⅰ:剪绳子(不行)

57-Ⅱ:和为s的连续整数序列

62:圆圈中最后剩下的数字(好难理解啊我的天 看不懂就手写一下)

DAY20

29:顺时针打印矩阵

31:栈的压入、弹出序列

DAY21

20:表示数值的字符串(这题太可怕了)

67:把字符串转换成整数


DAY16 

15:二进制中1的个数

思路:0&1=0,1&1=1,逐位判断,当为1时候计数器+1。

  • Java中>>>为二进制无符号数向右移动一位
public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int count = 0;
        while(n != 0){
            count += n & 1;
            n >>>= 1;
        }
        return count;
    }
}

65:不用加减乘除做加法(看不懂,理解不了)

思路:两个加数a和b。(计算机系统中数值用补码表示,所以正负数都可计算)

  • a\oplus b,相同的为0,不同的为1。
  • a&b的结果向左移动一位,即可实现同为1的时候与运算的结果为1,左移一位就是进位。

上述两个结果相加就实现了加减运算

(题解中的图)

public int add(int a, int b) {
	while (b != 0) {
		int tempSum = a ^ b;
		int carrySum = (a & b) << 1;
		a = tempSum;
		b = carrySum;
	}
	return a;
}

题解:https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/solution/jin-zhi-tao-wa-ru-he-yong-wei-yun-suan-wan-cheng-j/

DAY17

56-Ⅰ:数组中数字出现的次数(是人可以想出来的解法吗)

思路:位运算,异或操作。时间复杂度为O(n),空间复杂度为O(1),数组中有两个数只出现过一次。0异或任何数=任何数

  • 相同数字异或结果为0,数组中所有元素异或后不为0的结果就是两个不同数字异或的结果n
  • 我们想要把两个不同的数字分别放到两个数组中,这样两个数组各自异或后的结果就分别是这两个数本身
  • 首先异或结果中每一个1都代表着两个数不相等的位,我们只需要任意一个1,就可以将nums分为两个部分,所以用m=1和异或结果n作与运算,找到n中第一位为1的位置。找到之后m中只有一个1,其余全为0
  • 划分两组:nums中的数依次和m作与运算,每一位不是1就是0,所以刚好可以分为该位是1和是0的两组,同时也把不同的两个数分到了不同的组里,每个组里除了一个只出现一次的数之外都是出现两次的数,所以分别进行异或运算。
  • 最后将两个组分别得到的数字以数组形式输出即可
class Solution {
    public int[] singleNumbers(int[] nums) {
        int x = 0, y = 0, n = 0, m = 1;
        for(int num : nums)
            n ^= num;  //nums中全员异或,最后得到两个只出现过一次的数异或的结果。
            //其中至少有一位是1,如果全为0的话那两个数就相等了
        while((n & m) == 0)
            m <<= 1; //m与n进行与运算找到一伙结果n中第一位1
            //也就是第一位不同的位,可能有很多位不同但只需要一个即可将其分为两组
        for(int num :nums){
            //将这两个不同的数分到两个组里,分别在两个组进行异或,每个组里只有一个只出现一次的数
            if((num & m) != 0) x^= num; 
            else y ^= num;
        }
        return new int[]{x, y};
    }
}

56-Ⅱ:数组中数字出现的次数

思路一:哈希依次找

思路二(搞不懂):位运算,状态机。数组中只有一个数出现过一次,其余都出现三次。

//思路一:哈希
class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> hash = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            hash.put(nums[i], hash.getOrDefault(nums[i], 0) + 1); 
            //将前面的值放进去,按默认值+1计算,只有当出现重复的时候再按nums[i]的value进行+1
        }
        for(int i = 0; i < nums.length; i++){
            if(hash.get(nums[i]) == 1){
                return nums[i];
            }
        }
        return -1;
    }
}

DAY18

39:数组中出现次数超过一半的数字(数学不好真吃亏啊)

思路一:哈希

思路二:摩尔投票,众数。是众数的话投票为+1,非众数的话投票-1,有众数的话票数最后一定>0,所以每遍历一个数计算投票和,当投票和为0的时候从下一位开始重新计算投票和,每一轮都可以缩小区间

//思路一:哈希
class Solution {
    public int majorityElement(int[] nums) {
        Map<Integer, Integer> hash = new HashMap<>();
        for(int num : nums){
            hash.put(num, hash.getOrDefault(num, 0) + 1);
            if(hash.get(num) > nums.length / 2){
                return num;
            }
        }
        return 0;
    }
}

//思路二:摩尔投票
class Solution {
    public int majorityElement(int[] nums) {
        int mode = 0, votes = 0;
        for(int num : nums){
            if(votes == 0) mode = num;
            votes += num == mode ? 1 : -1;
        }
        return mode;
        // 验证 x 是否为众数
        //for(int num : nums)
            //if(num == x) count++;
        //return count > nums.length / 2 ? x : 0; // 当无众数时返回 0
    }
}

66:构建乘积数组

思路:想要求得数组B中第i位是除了B[i]以外所有数的乘积,那么等同于遍历到B[i]的时候它为1即可。此时便可以以 i 为分界点将数组分为左右部分,第一遍循环求出左部分的乘积,第二遍循环的时候求出右部分的乘积并将其与左部分相乘,最后可得到结果。

B中第一个元素是除了A中第一个数之外所有数的乘积,即只有右部分;最后一个元素只有左部分。所以第一遍循环的时候无需从0开始,直接左部分为1,第二遍循环的时候不从a[ a.length - 1 ]开始,最后一个直接存储第一遍循环得到的左部分乘积即可。

class Solution {
    public int[] constructArr(int[] a) {
        int len = a.length;
        if(len == 0) return new int[0];
        int[] b = new int[len];
        b[0] = 1;
        for(int i = 1; i < len; i++)
            b[i] = b[i - 1] * a[i - 1]; //b中存储的是a[i]左半部分的乘积,相当于下三角
            //这里要注意为什么i != len,当遍历到最后一个数的时候是相当于结果要除最后一个数以外的乘积
        int temp = 1;
        for(int i = len - 2; i >= 0; i--){
            temp *= a[i + 1]; //temp暂时存放a[i]右半部分的乘积
            b[i] *= temp; //这里是将a[i]的左右部分相乘得到除了a[i]以外所有数的乘积
        }
        return b;
    }
}

DAY19

14-Ⅰ:剪绳子(不行)

思路一:动态规划

  • dp[i]存放绳子长度为 i 时被剪成m段后最大乘积,初始化dp[2] = 1
  • j 表示被剪掉的第一段的长度,因为减去长度为1的时候对乘积无影响,所以 j 从2开始
  • 剪了 j 以后剩下的(i - j)可以剪也可以不剪。不剪的话乘积是 j * ( i - j );剪的话乘积是 j * dp[i - j],两者间取最大的。因为 i 是从3开始到绳子总长度,所以 i 相同的时候 j 还在遍历,所以最后dp[ i ]存放的是不同 j 剪完后所有情况的最大值。

思路二:贪心算法(看不懂)

class Solution {
    public int cuttingRope(int n) {
        int[] dp = new int[n + 1];
        dp[2] = 1; //绳子长度为1的时候不用剪 dp[0]=dp[1]=0
        for(int i = 3; i < n + 1; i++){
            for(int j = 2; j < i; j++){
                dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
            }
        }
        return dp[n];
    }
}

57-Ⅱ:和为s的连续整数序列

思路:双指针,滑动窗口。指针i和j分别从第0和第1位开始移动,且两个指针都只能向右移动,sum等于指针内所有数字之和,包括指针。

  • sum > target时,说明需要减去数字,左移指针 i 向右移动
  • sum < target时需要加数字,右指针 j 向右移动
  • sum = target时将此时指针圈住的序列存入结果中,然后 i 向右移动继续寻找其他符合条件的范围(操作相同所以与情况1合并处理)
class Solution {
    public int[][] findContinuousSequence(int target) {
        int i = 1, j = 2,sum = 3;
        List<int[]> res = new ArrayList<>();
        while(i < j){
            if(sum == target){
                int[] temp = new int[j - i + 1];
                for(int k = i; k <= j; k++)
                    temp[k - i] = k; //temp数组从0开始存放数
                res.add(temp);
            }
            if(sum >= target){
                sum -= i; //因为不需要了,先将和减去需要丢掉的数字,再移动指针
                i++;
            }else{
                j++; //因为需要下个数字,所以先移动指针,再将新加入的数字加进sum里
                sum += j;
            }
        }
        return res.toArray(new int[0][]);
    }
}

62:圆圈中最后剩下的数字(好难理解啊我的天 看不懂就手写一下)

思路:约瑟夫环。m为隔几个删除,n为上一轮数组长度(循环里就是 i),m可能大于n。以下是正推过程。每一轮得到的索引都是在本轮中已经换了起始元素重新排列的数组中的索引。

因为索引是从0开始,首先我们要删除的元素索引是(0 + m-1) % n,而下一轮起始元素在这一轮中的索引为m % n。(假设n初始为5)

第二轮:删除元素索引:(m % n + m - 1) % n,下一轮起始元素索引:(m % n + m) % n (这一轮n=5)

第三轮:删除元素索引:((m % n + m) % n + m - 1) % n,下一轮起始元素索引:((m % n + m) % n + m ) % n (这一轮n=4)

(递归到最后数组长度为1的时候计算用n=2,所以代码中倒推时 i 是从2开始的)

由上述规律可以看出我们需要记录的就是m%n

实在不行就记公式吧:(当前index + m) % 上一轮剩余数字的个数

class Solution {
    public int lastRemaining(int n, int m) {
        //数组是从0到n-1的n个数字,所以fin即使索引又是初始数组里对应的值即在初始数组中a[fin] = fin
        int fin = 0; //留到最后的数在最后一轮的索引一定是0,所以相当于整个过程是倒推
        for(int i = 2; i <= n; i++)
        //倒推的话是从数组长度为1开始一次推,i相当于数组长度
        //因为长度为1时候里面只有最后留下的数字,所以i直接从2开始递增到初始数组长度
            fin = (fin + m) % i;
        return fin;
    }
}

DAY20

29:顺时针打印矩阵

思路:模拟。左到右,上到下,右到左,下到上

  • 从左到右:行不变,遍历结束后缩小上边界的范围即high+1(向下移动),用high > low判断是否打印完毕
  • 从上到下:列不变,一轮循环结束后缩小右边界,right向左移动一个
  • 从右到左
  • 从下到上
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int left = 0, right = matrix[0].length - 1, high = 0, low = matrix.length - 1;
        int k = 0;
        int[] res = new int[matrix.length * matrix[0].length];
        while(true){
            for(int i = left; i <= right; i++) res[k++] = matrix[high][i]; //从左到右
            if(++high > low) break; //先自加再返回
            for(int i = high; i <= low; i++) res[k++] = matrix[i][right]; //从上到下
            if(--right < left) break;  //边界收缩后判断是否打印完毕
            for(int i = right; i >= left; i--) res[k++] = matrix[low][i]; //从右到左
            if(--low < high) break;
            for(int i = low; i >=high; i--) res[k++] = matrix[i][left]; //从下到上
            if(++left > right) break;
        }
        return res;
    }
}

31:栈的压入、弹出序列

思路:确定了入栈和出栈序列,那么压入和弹出的顺序是唯一的。将入栈序列中的元素一次压入栈中,每次压入都要判断是否与出栈序列中的元素相同,若相同即刻弹出,这里要循环判断弹出,直至栈顶元素与出栈序列中元素不同,则继续将入栈序列里的元素压入。以此类推。

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Stack<Integer> stack = new Stack<>();
        int i = 0;
        for(int num : pushed){
            stack.push(num); //入栈
            while(!stack.isEmpty() && stack.peek() == popped[i]){
                stack.pop();
                i++;
            }
        }
        return stack.isEmpty();
    }
}

DAY21

20:表示数值的字符串(这题太可怕了)

思路:字符串,有限状态自动机。

状态描述
0开始的空格
1幂符号前的正负号
2小数点前的数字
3小数点、小数点后的数字
4当小数点前为空格时,小数点、小数点后的数字
5幂符号
6幂符号后的正负号
7幂符号后的数字
8结尾的空格

2、3、7、8是合法的结尾

class Solution {
    public boolean isNumber(String s) {
        //状态表,<key, value>表示key当前所处的states[i]可以转到states[value]
        Map[] states = {
            new HashMap<>(){{put(' ', 0); put('s', 1); put('d', 2); put('.', 4);}}, //0
            new HashMap<>(){{put('d', 2); put('.', 4);}},                           //1
            new HashMap<>(){{put('d', 2); put('.', 3); put('e', 5); put(' ', 8);}}, //2
            new HashMap<>(){{put('d', 3); put('e', 5); put(' ', 8);}},              //3
            new HashMap<>(){{put('d', 3);}},                                        //4
            new HashMap<>(){{put('s', 6); put('d', 7);}},                           //5
            new HashMap<>(){{put('d', 7);}},                                        //6
            new HashMap<>(){{put('d', 7); put(' ', 8);}},                           //7
            new HashMap<>(){{put(' ', 8);}},                                        //8
        };
        int p = 0; //当前状态
        char t;
        for(char c : s.toCharArray()){
            if(c >= '0' && c <='9') t = 'd'; //数字
            else if(c == '+' || c == '-') t = 's'; //正负号
            else if(c == 'e' || c == 'E') t = 'e'; //幂符号
            else if(c == '.' || c == ' ') t = c; //空格和小数点
            else t = '?';
            if(!states[p].containsKey(t)) return false; //不在状态表里的直接返回false
            p = (int)states[p].get(t); //字符串中的元素对应状态表的,状态转移
        }
        return p == 2 || p == 3 || p == 7 || p== 8;
    }
}

67:把字符串转换成整数

思路:字符串。情况如下

  • 首部空格:删除
  • 符号位:判断“+”“-”号即可,无符号位即为正数,“-”则给结果乘以-1
  • 非数字字符:遇到立刻返回
  • 数字字符:该数字字符对应ASC码 - 字符‘0’的ASC码即为该数字(int);拼接的时候*10向左挪一位,加上当前数字

越接处理:

因为res每次都会*10,所以bndry = int最大值2147483647 / 10。分两种情况

  • 当前res > bndry,那么*10拼接以后最小也是2147483650,一定会越界
  • 当前res = bndry,并且当前个位数 > 7,那么拼接后的结果为2147483648或者2147483649,也会越界
class Solution {
    public int strToInt(String str) {
        char[] c = str.trim().toCharArray(); //删除开头空格
        if(c.length == 0) return 0;
        //res的边界值为2147483647,设置边界为bndry = 2147483647 // 10 = 214748364
        int res = 0, bndry = Integer.MAX_VALUE / 10; 
        int i = 1, sign = 1;
        if(c[0] == '-') sign = -1; //负号开头要给结果乘以-1
        else if(c[0] != '+') i = 0; //若开头不是正负号那就从第0位开始遍历
        for(int j = i; j < c.length; j++){
            if(c[j] < '0' || c[j] >'9') break; //遇到非数字直接跳出
            if(res > bndry || res == bndry && c[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            res = res * 10 + (c[j] - '0'); //数字字符转化成数字就是用其ASC码 - 数字0的ASC码
        }
        return sign * res;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值