《剑指 Offer I》刷题笔记 51 ~ 61 题

小标题以 _ 开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。

VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用 java main.java 运行(JDK 11 以后)

Go 的数据结构:LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。

go get github.com/emirpasic/gods

位运算(简单)

51. 二进制中 1 的个数

题目:剑指 Offer 15. 二进制中1的个数

_解法1:逐伟判断

注:Java 中无符号左移是 >>>,且 while 循环条件不能写成 wihle (n > 0)

public class Solution {
    
    
    public int hammingWeight(int n) {
    
    
        int res = 0;
        while (n != 0) {
    
    
            res += n & 1;
            n >>>= 1;
        }
        return res;
    }
}
func hammingWeight(num uint32) int {
    
    
	res := 0
	for num > 0 {
    
    
		if num&1 == 1 {
    
    
			res++
		}
		num >>= 1
	}
	return res
}

解法2:巧用 n&(n-1)

题解:面试题15. 二进制中 1 的个数(位运算,清晰图解)

n & (n - 1) 的作用:将 n 的二进制位最右边的 1 变成 0

思路:利用 n & (n - 1) 每次消去 n 最右边的 1,计算将 n 变成 0 经历的次数。

class Solution {
    
    
		public int hammingWeight(int n) {
    
    
        int res = 0;
        while (n != 0) {
    
    
            res++;
            n &= n - 1; // 消去n最右边的1
        }
        return res;
    }
}

52. 不用加减乘除做加法(背题)

题目:剑指 Offer 65. 不用加减乘除做加法

解法1:位运算

题解:面试题65. 不用加减乘除做加法(位运算,清晰图解)

⭐️ 题解:禁止套娃,如何用位运算完成加法?

^ 亦或:相当于 无进位 的 求和。

想象 10 进制下的模拟情况:19+1=20 无进位求和就是 10,而非 20

& 与:相当于求每位的进位数。

还是想象10进制下模拟情况:9+1=10,如果是用 & 的思路来处理,则 9+1 得到的进位数为 1,而不是 10,所以要用 <<1 向左再移动一位,就得到 10 了。

class Solution {
    
    
    /**
     * 递归
     */
    public int add(int a, int b) {
    
    
        if (b == 0) return a;
        // 转换成 非进位和 + 进位和
        return add(a ^ b, (a & b) << 1);
    }

    /**
     * 迭代
     */
    public int add0(int a, int b) {
    
    
        while (b != 0) {
    
    
            int tempSum = a ^ b; // 计算无进位的临时结果
            int carryNum = (a & b) << 1; // 计算进位结果        
            a = tempSum;
            b = carryNum;
        }
        return a;
    }

    /**
     * 脑筋急转弯
     */
    public int add1(int a, int b) {
    
    
        return Math.addExact(a, b);
    }
    
    /**
     * 脑筋急转弯
     */
    public int add2(int a, int b) {
    
    
        return Integer.sum(a, b);
    }
}

位运算(中等)

53. 数组中数字出现的次数*

题目:数组中数字出现的次数

_解法1:Set(不满足题目要求)

class Solution {
    
    
    public int[] singleNumbers(int[] nums) {
    
    
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
    
    
            if (set.contains(num))
                set.remove(num);
            else
                set.add(num);
        }
        return set.stream().mapToInt(Integer::valueOf).toArray();
    }
}

解法2:位运算:^ + 分组

class Solution {
    
    
    public int[] singleNumbers(int[] nums) {
    
    
        int tmp = 0;
        // 求出两个只出现1次的数的 异或值
        for (int num : nums)
            tmp ^= num;

        // 保留最右的一个 1, 根据此进行分组
        // int group = tmp & (-tmp); // 简单写法
        int group = 1;
        while ((group & tmp) == 0)
            group <<= 1;

        // 分组计算
        int[] res = new int[2];
        for (int num : nums) {
    
    
            // 分组位为0的组
            if ((num & group) == 0)
                res[0] ^= num;
            // 分组位为1的组
            else
                res[1] ^= num;
        }
        return res;
    }
}

注:以下两种写法等价,都是求出 tmp 最右边一位的 1,如 1001000110100010

int group = 1;
while ((group & tmp) == 0)
 		group <<= 1;
int group = tmp & (-tmp);

54. 数组中数字出现的次数 II

题目:剑指 Offer 56 - II. 数组中数字出现的次数 II

_解法1:map

class Solution {
    
    
    public int singleNumber(int[] nums) {
    
    
        Map<Integer, Boolean> map = new HashMap<>();
        for (int num : nums)
            map.put(num, map.containsKey(num));

        for (Integer i : map.keySet())
            if (!map.get(i))
                return i;

        return -1;
    }
}

解法2:位运算(待续)

看不懂,哭了。

数学(简单)

55. 数组中出现超过一半的数字

题目:数组中出现次数超过一半的数字

_解法1:map

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

_解法2:巧妙解法

思路:排序后直接取中间位置的数字。如果某个数字的出现次数大于一半,那么排序后中间数字必然是它。

class Solution {
    
        
    public int majorityElement1(int[] nums) {
    
    
        Arrays.sort(nums);
        return nums[nums.length / 2];
    }
}

解法3:摩尔投票法

所谓摩尔投票法,核心就是对拼消耗

玩一个诸侯争霸的游戏,假设你方人口超过总人口一半以上,并且能保证每个人口出去干仗都能一对一同归于尽。最后还有人活下来的国家就是胜利。那就大混战呗,最差所有人都联合起来对付你(对应你每次选择作为计数器的数都是众数),或者其他国家也会相互攻击(会选择其他数作为计数器的数),但是只要你们不要内斗,最后肯定你赢。最后能剩下的必定是自己人。

class Solution {
    
    
    /**
     * 摩尔投票法
     * 混战中一换一, vote 为冠军队伍人数, 遇到自己人则 1, 遇到敌人则 -1
     * vote 为 0 则下一个来的人成为冠军团队
     */
    public int majorityElement(int[] nums) {
    
    

        // vote 冠军队伍人数, selectNum 冠军队伍代表的数字
        int vote = 0, selectNum = 0;
        // 所有人轮流上台, 其中有 自己人 也有 敌人
        for (int num : nums) {
    
    
            // 冠军队伍人数为 0, 此时没有冠军队伍, 则下一个上台的人成为冠军队伍
            if (vote == 0) selectNum = num;
            // 判断是敌是友, 来决定增加人数还是减少人数
            vote += (num == selectNum) ? 1 : -1;
        }
        // 返回最终的冠军队伍
        return selectNum;
    }
}

56. 构建乘积数组

解法1:暴力

解法2:技巧

class Solution {
    
    
    /**
     * [1, 2, 3, 4, 5]
     * 左乘:[1, 1, 2, 6, 24]
     * 右乘:[120, 60, 20,  5, 1] (从右往左写的)
     * 最终结果:[120, 60, 40, 30, 24]
     */
    public int[] constructArr(int[] a) {
    
    
        int[] res = new int[a.length];
        for (int i = 0, product = 1; i < a.length; i++) {
    
    
            res[i] = product; // 左乘(不包含自己)
            product *= a[i];
        }
        for (int i = a.length - 1, product = 1; i >= 0; i--) {
    
    
            res[i] *= product; // 右乘(不包含自己)
            product *= a[i];
        }
        return res;
    }
}

数学(中等)

57. 剪绳子

题目:剑指 Offer 14- I. 剪绳子

解法1:动态规划

class Solution {
    
    
    public int cuttingRope(int n) {
    
    
        // dp[i] 表示长度为 i 的绳子剪过程 m 段后长度的最大乘积(m>1)
        int[] dp = new int[n + 1];
        dp[2] = 1; // 初始化
        // 目标: 求 dp[n]
        for (int i = 3; i <= n; i++)
            // 首先对绳子剪长度为 j 的一段, j 范围是 2 <= j < i
            // 剪掉的长度为 1 的话, 对乘积没有任何增益, 所以从 2 开始剪
            for (int j = 2; j < i; j++) {
    
    
                // j * (i - j) 表示剪了长度 j 以后,剩下 (i-j) 不剪
                // j * dp[i - j] 表示剪了长度 j 以后, 剩下 (i-j) 继续剪, 从之前 dp 数组找最大值
                int nowBigger = Math.max(j * (i - j), j * dp[i - j]);
                // 对于同一个 i, 内层循环对不同的 j 拿到的 max 不同, 每次循环要更新 max
                dp[i] = Math.max(dp[i], nowBigger);
            }
        return dp[n];
    }
}

58. 和为 s 的连续正数序列

题目:剑指 Offer 57 - II. 和为s的连续正数序列

_解法1:暴力 二维 List -> 二维 int 数组

有点滑动窗口的影子,可惜还是功夫不到家!

  • 始终应该把 滑动窗口的区域 当作一个 整体 对待,不能随便重置 right
  • 应当灵活的控制左右指针的移动来控制整个区间
  • 双重 List 转 int[][] 也不是个明智的做法。。
class Solution {
    
    
    public int[][] findContinuousSequence(int target) {
    
    
        int left = 1;
        List<List<Integer>> res = new ArrayList<>();
        while (left <= (target / 2)) {
    
    
            List<Integer> temp = new ArrayList<>();
            int sum = 0;
            int right = left;
            while (sum < target) {
    
    
                sum += right;  
                temp.add(right++);
                if (sum == target) 
                    res.add(temp);
            }
            left++;
        }
        // List<List<Integer>> ---> int[][]
        int [][] resArr = new int[res.size()][];
        for (int i = 0; i < res.size(); i++) {
    
    
            List<Integer> temp = res.get(i);
            int[] tempArr = new int[temp.size()];
            for (int j = 0; j < temp.size(); j++)
                tempArr[j] = temp.get(j);
            resArr[i] = tempArr;
        }
        return resArr;
    }
}

解法2:滑动窗口

题解:什么是滑动窗口,以及如何用滑动窗口解这道题

class Solution {
    
    
    public int[][] findContinuousSequence(int target) {
    
    
        int left = 1, right = 1, sum = 0;
        List<int[]> res = new ArrayList<>();
        while (left <= (target / 2)) {
    
    
            if (sum < target)
                // 右指针向右移动
                sum += right++;
            else if (sum > target)
                // 左指针向右移动
                sum -= left++;
            else {
    
    
                int[] arr = new int[right - left];
                for (int i = left; i < right; i++)
                    arr[i - left] = i;
                res.add(arr);
                sum -= left;
                // 左边界向右移动
                left++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}

59. 圆圈中最后剩下的数字

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

_解法1:暴力模拟

思路:完全按照题目意思进行编程模拟整个操作。。

  • Java 中使用 LinkedList 会超时,使用 ArrayList 可以通过(耗时高)
class Solution {
    
    
    public int lastRemaining(int n, int m) {
    
    
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n; i++)
            list.add(i);
        int cur = 0;
        while (list.size() > 1) {
    
    
            cur += m - 1;
            if (cur >= list.size())
                cur = cur % list.size();
            list.remove(cur);
        }
        return list.get(0);
    }
}

解法2:数学解法(todo)

这题是有数学解法的,不过我看不懂,先放着。。。

模拟(中等)

60. 顺时针打印矩阵

题目:剑指 Offer 29. 顺时针打印矩阵

_解法1:暴力模拟

思路:走过就设置已访问过的标志,右下左上方向循环走。

class Solution {
    
    
    // 已访问标志
    public static final int FLAG = -952861280; 
    public int[] spiralOrder(int[][] matrix) {
    
    
        // 边界情况
        if (matrix.length == 0 || matrix[0].length == 0)
            return new int[] {
    
    };

        // x - row, y - column
        int x = matrix.length, y = matrix[0].length;
        // 当前访问的位置
        int curX = 0, curY = 0;
        // 存储结果的数组
        int[] res = new int[x * y];
        // 从 [0][0] 开始, 默认已访问
        res[0] = matrix[0][0];
        matrix[0][0] = FLAG;

        // idx - 结果数组的索引, direction - 0123 右下左上
        int idx = 0, direction = 0; 
        while (true) {
    
    
            if (direction == 0) {
    
     // 右
                // 当前方向满足条件就一直走
                while (curY + 1 < y && matrix[curX][curY + 1] != FLAG) {
    
    
                    curY++;
                    res[++idx] = matrix[curX][curY];
                    matrix[curX][curY] = FLAG;
                }
                // 下个方向满足条件才走, 否则直接结束
                if (curX + 1 < x && matrix[curX + 1][curY] != FLAG)
                    direction = 1;
                else break;
            } else if (direction == 1) {
    
     // 下
                while (curX + 1 < x && matrix[curX + 1][curY] != FLAG) {
    
    
                    curX++;
                    res[++idx] = matrix[curX][curY];
                    matrix[curX][curY] = FLAG;
                }
                if (curY - 1 >= 0 && matrix[curX][curY - 1] != FLAG)
                    direction = 2;
                else break;
            } else if (direction == 2) {
    
     // 左
                while (curY - 1 >= 0 && matrix[curX][curY - 1] != FLAG) {
    
    
                    curY--;
                    res[++idx] = matrix[curX][curY];
                    matrix[curX][curY] = FLAG;
                }
                if (curX - 1 >= 0 && matrix[curX - 1][curY] != FLAG)  
                    direction = 3;
                else break;
            } else if (direction == 3) {
    
     // 上
                while (curX - 1 >= 0 && matrix[curX - 1][curY] != FLAG) {
    
    
                    curX--;
                    res[++idx] = matrix[curX][curY];
                    matrix[curX][curY] = FLAG;
                }
                if (curY + 1 < y && matrix[curX][curY + 1] != FLAG) 
                    direction = 0;
                else break;
            }
        }
        return res;
    }
}

解法2:设定遍界的模拟

class Solution {
    
    
    public int[] spiralOrder(int[][] matrix) {
    
    
        if (matrix.length == 0) return new int[0];
        // 左右上下 边界
        int left = 0, right = matrix[0].length - 1, top = 0, bottom = matrix.length - 1;

        int idx = 0;
        int[] res = new int[(right + 1) * (bottom + 1)];

        while (true) {
    
    
            // 左 -> 右
            for (int i = left; i <= right; i++)
                res[idx++] = matrix[top][i];
            // 收缩上边界, 同时判断能否往下走
            if (++top > bottom) break;
            // 上 -> 下
            for (int i = top; i <= bottom; i++) 
                res[idx++] = matrix[i][right];
          	// 收缩右边界, 同时判断能否往左走
            if (--right < left) break;
            // 右 -> 左
            for (int i = right; i >= left; i--)
                res[idx++] = matrix[bottom][i];
          	// 收缩下边界, 同时判断能否往上走
            if (--bottom < top) break;
            // 下 -> 上
            for (int i = bottom; i >= top; i--)
                res[idx++] = matrix[i][left];
          	// 收缩左边界, 同时判断能否往右走
            if (++left > right) break;
        }
        return res;
    }
}

61. 栈的压入、弹出序列

题目:剑指 Offer 31. 栈的压入、弹出序列

解法1:暴力模拟

class Solution {
    
    
    public boolean validateStackSequences(int[] pushed, int[] popped) {
    
    
        Stack<Integer> stack = new Stack<>();
        int p1 = 0, p2 = 0, idx = 0;
        while (p2 < popped.length && idx < pushed.length + 1) {
    
    
            while (!stack.isEmpty() && stack.peek() == popped[p2]) {
    
    
                stack.pop();
                p2++;
                continue;
            }
            if (p1 < pushed.length)
                stack.push(pushed[p1++]);
            idx++;
        }
        return stack.isEmpty();
    }
}

题解2:优化的模拟

题解:面试题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();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/123500783