《剑指 Offer I》刷题笔记 31 ~ 40 题

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

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

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

go get github.com/emirpasic/gods

双指针(简单)

31. 调整数组顺序使奇数位于偶数前面

题目:剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

_解法1:头尾指针 + 辅助数组

思路:首尾双指针遍历一次,开辟新数组,奇数从前放,偶数从后放

class Solution {
    
    
    public int[] exchange(int[] nums) {
    
    
        int size = nums.length;
        int[] res = new int[size];
        // p1 - 偶数位置,p2 - 奇数位置
        int p1 = 0, p2 = size - 1;
        for (int i = 0; i < size; i++) {
    
    
            if ((nums[i] & 1) != 0) // 奇数
                res[p1++] = nums[i];
            else // 偶数
                res[p2--] = nums[i];
        }
        return res;
    }
}

解法2:头尾指针 + 交换

思路:首尾双指针遍历一次,分别找到奇偶数后交换位置

class Solution {
    
    
    public int[] exchange(int[] nums) {
    
    
        int p1 = 0, p2 = nums.length - 1;
        while (p1 < p2) {
    
    
            // 从前往后找奇数
            while (p1 < p2 && (nums[p1] & 1) == 1) p1++;
            // 从后往前找偶数
            while (p1 < p2 && (nums[p2] & 1) == 0) p2--;
            // 前后都找到就交换
            int tmp = nums[p1];
            nums[p1] = nums[p2];
            nums[p2] = tmp;
        }
        return nums;
    }
}

解法3:快慢指针

思路:fast 移动,slow 不动,fast 遇到奇数则和 slow 交换(slow++,fast++)

class Solution {
    
    
    public int[] exchange(int[] nums) {
    
    
        int low = 0, fast = 0;
        while (fast < nums.length) {
    
    
            // fast遇到奇数则和slow交换
            if ((nums[fast] & 1) == 1) {
    
    
                int tmp = nums[fast];
                nums[fast] = nums[low];
                nums[low] = tmp;
                low++;
            }
            fast++;
        }
        return nums;
    }
}

32. 和为 s 的两个数字

题目:剑指 Offer 57. 和为 s 的两个数字

_解法1:哈希

思路:遍历时将数据存到 set 中,判断 tartget-nums[i] 是否在 set 中存在

class Solution {
    
    
    public int[] twoSum(int[] nums, int target) {
    
    
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < nums.length; i++) {
    
    
            if (set.contains(target - nums[i]))
                return new int[] {
    
     nums[i], target - nums[i] };
            set.add(nums[i]);
        }
        return null;
    }
}

思路:优化循环开始的位置

class Solution {
    
    
    public int[] twoSum(int[] nums, int target) {
    
    
        Set<Integer> set = new HashSet<>();
      	// 优化循环开始位置
        int mid = nums.length / 2, start = 0;
        if (nums[mid] > target)
            start = mid;
        for (int i = start; i < nums.length; i++) {
    
    
            if (set.contains(target - nums[i]))
                return new int[] {
    
     nums[i], target - nums[i] };
            set.add(nums[i]);
        }
        return null;
    }
}
// go 哈希
func twoSum(nums []int, target int) []int {
    
    
	dic := make(map[int]bool)
	for _, num := range nums {
    
    
		if _, ok := dic[target-num]; ok {
    
    
			return []int{
    
    num, target - num}
		}
		dic[num] = true
	}
	return []int{
    
    }
}

解法2:双指针

思路:对撞双指针,一个从头开始一个从尾开始,根据和 target 比较大小判断 两个指针的走势

class Solution {
    
      
  public int[] twoSum2(int[] nums, int target) {
    
    
        int l = 0, r = nums.length - 1;
        while (l < r) {
    
    
            int num = nums[l] + nums[r];
            if (num < target)
                l++;
            else if (num > target)
                r--;
            else
                return new int[] {
    
     nums[l], nums[r] };
        }
        return null;
    }
}

33. 翻转单词顺序

题目:剑指 Offer 58 - I. 翻转单词顺序

_解法1:调库

思路:使用 \\s+ 实现以空格分隔字符串(且数组中不包含空格),然后倒着拼接即可

class Solution {
    
    
    public String reverseWords1(String s) {
    
    
        // 以空格分隔字符串
        String[] tmps = s.split("\\s+");
        // 倒着拼接字符串
        StringBuffer sb = new StringBuffer();
        for (int i = s.length - 1; i > 0; i--) {
    
    
            sb.append(tmps[i]);
            sb.append(" ");
        }
        return sb.toString().trim();
    }
}

解法2:双指针

面试题58 - I. 翻转单词顺序(双指针 / 库函数,清晰图解)

class Solution {
    
    
    public String reverseWords(String s) {
    
    
        s = s.trim(); // 删除首尾空格
        int l = s.length() - 1, r = l;
        StringBuilder sb = new StringBuilder();
        while (l >= 0) {
    
    
            while (l >= 0 && s.charAt(l) != ' ') l--; // 搜索首个空格
            sb.append(s.subSequence(l+1, r+1) + " "); // 添加单词
            while (l >= 0 && s.charAt(l) == ' ') l--; // 跳过单词间空格
            r = l; // r 指向下个单词的尾字符
        }
        return sb.toString().trim();
    }
}

搜索与回溯算法(中等)

34. 矩阵中的路径*

题目:剑指 Offer 12. 矩阵中的路径

题解1:DFS + 剪枝

题解:面试题12. 矩阵中的路径( DFS + 剪枝 ,清晰图解)

难点:递归搜索匹配字符串过程中,需要 board[i][j] = '#' 来防止 ”走回头路“ 。当匹配字符串不成功时,会回溯返回,此时需要 board[i][j] = word[k] 来 ”取消对此单元格的标记”。 在 DFS 过程中,每个单元格会多次被访问的,board[i][j] = '#' 只是要保证在当前匹配方案中不要走回头路,不复原。

利用 boolean[][] visited 记录当前 DFS 中访问过的位置。

class Solution {
    
    
    public boolean exist(char[][] board, String word) {
    
    
      	// 记录访问过的位置
        boolean[][] visited = new boolean[board.length][board[0].length];
        char[] words = word.toCharArray();
        for (int i = 0; i < board.length; i++)
            for (int j = 0; j < board[0].length; j++)
                if (dfs(board, words, visited, i, j, 0))
                    return true;
        return false;
    }

    /**
     * 深度优先搜索
     * 
     * @param words   搜索目标
     * @param board   搜索范围
     * @param visited 已经搜索过的路径
     * @param i       当前元素的行索引
     * @param j       当前元素的列索引
     * @param k       搜索目标在wrod中的索引
     * @return
     */
    boolean dfs(char[][] board, char[] words, boolean[][] visited, int i, int j, int k) {
    
    
        // 边界情况判断:行越界、列越界、矩阵元素已经访问过
        if (i >= board.length || i < 0 // 行越界
                || j >= board[0].length || j < 0 // 列越界
                || board[i][j] != words[k] // 当前元素不是想要搜索的元素
                || visited[i][j]) // 矩阵元素已经访问过
            return false;
        // 全部搜索完毕,返回true
        if (k == words.length - 1)
            return true;
        // 剪枝, 防止下次递归往回查找
        visited[i][j] = true;
        boolean res = dfs(board, words, visited, i + 1, j, k + 1)
                || dfs(board, words, visited, i - 1, j, k + 1)
                || dfs(board, words, visited, i, j + 1, k + 1)
                || dfs(board, words, visited, i, j - 1, k + 1);
        // 回溯,恢复原来的字符
        visited[i][j] = false;
        return res;
    }
}

优化:不使用额外空间来记录访问过的位置

class Solution {
    
    
    public boolean exist(char[][] board, String word) {
    
    
        char[] words = word.toCharArray();
        for (int i = 0; i < board.length; i++)
            for (int j = 0; j < board[0].length; j++)
                if (dfs(board, words, i, j, 0))
                    return true;
        return false;
    }

    /**
     * 深度优先搜索
     * 
     * @param words  搜索目标
     * @param board 搜索范围
     * @param i     当前元素的行索引
     * @param j     当前元素的列索引
     * @param k     搜索目标在wrod中的索引
     * @return
     */
    boolean dfs(char[][] board, char[] words, int i, int j, int k) {
    
    
        // 边界情况判断:行越界、列越界、矩阵元素已经访问过 
        if (i >= board.length || i < 0
                || j >= board[0].length || j < 0
                || board[i][j] != words[k])
            return false;
        // 全部搜索完毕,返回true
        if (k == words.length - 1)
            return true;
        // 剪枝,将当前节点赋值 '#', 防止下次递归往回查找
        board[i][j] = '#';
        boolean res = dfs(board, words, i + 1, j, k + 1)
                || dfs(board, words, i - 1, j, k + 1)
                || dfs(board, words, i, j + 1, k + 1)
                || dfs(board, words, i, j - 1, k + 1);
        // 回溯,恢复原来的字符
        board[i][j] = words[k];
        return res;
    }
}

35. 机器人的运动范围

题目:剑指 Offer 13. 机器人的运动范围

解法1:DFS

题解:剑指 Offer 13. 机器人的运动范围( 回溯算法,DFS / BFS ,清晰图解)

这个是我的思路,不过当时我踩了个坑,没有把 computeSum(i, j) > k 放到边界情况中判断…

class Solution {
    
    
    int res = 0;
  
    public int movingCount(int m, int n, int k) {
    
    
        boolean[][] visited = new boolean[m][n];
        dfs(m, n, visited, 0, 0, k);
        return res;
    }

    void dfs(int m, int n, boolean[][] visited, int i, int j, int k) {
    
    
        // 边界情况
        if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || computeSum(i, j) > k)
            return;
        res++;
        visited[i][j] = true;
        dfs(m, n, visited, i + 1, j, k);
        dfs(m, n, visited, i - 1, j, k);
        dfs(m, n, visited, i, j + 1, k);
        dfs(m, n, visited, i, j - 1, k);
    }

    /**
     * 计算 m 与 n 的数位和
     */
    int computeSum(int m, int n) {
    
    
        int sum = 0;
        while (m > 0 || n > 0) {
    
    
            sum += (m % 10 + n % 10);
            m /= 10;
            n /= 10;
        }
        return sum;
    }
}

题解中 K 神的 DFS:将返回结果写到递归中

class Solution {
    
    
    public int movingCount(int m, int n, int k) {
    
    
        boolean[][] visited = new boolean[m][n];
        return dfs(m, n, visited, 0, 0, k);
    }

    int dfs(int m, int n, boolean[][] visited, int i, int j, int k) {
    
    
        if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || computeSum(i, j) > k)
            return 0;
        visited[i][j] = true;
        int right = dfs(m, n, visited, i + 1, j, k);
        int down = dfs(m, n, visited, i, j + 1, k);
        return down + right + 1;
    }

    /**
     * 计算 m 与 n 的数位和
     */
    int computeSum(int m, int n) {
    
    
        int sum = 0;
        while (m > 0 || n > 0) {
    
    
            sum += (m % 10 + n % 10);
            m /= 10;
            n /= 10;
        }
        return sum;
    }
}

解法2:BFS

题解:DFS 和 BFS 两种解决方式

BFS 做的操作其实和 DFS 差不多,区别是遍历方式不一样。

class Solution {
    
    
    public int movingCount(int m, int n, int k) {
    
    
        boolean[][] visited = new boolean[m][n];
        int res = 0;
        Queue<int[]> queue = new LinkedList<>();
        // 从左上角坐标[0,0]点开始访问,add方法表示把坐标点加入到队列的队尾
        queue.add(new int[] {
    
     0, 0 });
        while (queue.size() > 0) {
    
    
            int[] x = queue.poll();
            int i = x[0], j = x[1];
            // 判断边界条件
            if (i >= m || j >= n || computeSum(i, j) > k || visited[i][j])
                continue;
            visited[i][j] = true;
            res++;
            queue.add(new int[] {
    
     i + 1, j });
            queue.add(new int[] {
    
     i, j + 1 });
        }
        return res;
    }

    /**
     * 计算 m 与 n 的数位和
     */
    int computeSum(int m, int n) {
    
    
        int sum = 0;
        while (m > 0 || n > 0) {
    
    
            sum += (m % 10 + n % 10);
            m /= 10;
            n /= 10;
        }
        return sum;
    }
}

36. 二叉树中和为某一值的路径

题目:剑指 Offer 34. 二叉树中和为某一值的路径

本题二叉树的定义:

class TreeNode {
    
    
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode() {
    
    
    }

    TreeNode(int val) {
    
    
        this.val = val;
    }

    TreeNode(int val, TreeNode left, TreeNode right) {
    
    
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

_解法1:DFS + 回溯

我的思路:每次递归时参数多传一个 sum,用于计算到当前节点为止的值

class Solution {
    
    
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root, int target) {
    
    
        dfs(root, 0, new ArrayList<>(), target);
        return res;
    }

    void dfs(TreeNode node, int sum, List<Integer> visited, int target) {
    
    
        if (node == null) return;

        sum += node.val;
        visited.add(node.val);

      	// 注意题目要求必须是叶子节点 
        if (target == sum && node.left == null && node.right == null) {
    
    
          	// 不 new 则存到 res 中的 visited 也会跟着改
            res.add(new ArrayList<>(visited));
        }

        // 向左搜索
        dfs(node.left, sum, visited, target);
        // 向右搜索
        dfs(node.right, sum, visited, target);

        // 回溯
        visited.remove(visited.size() - 1);
        sum -= node.val;
    }

}

题解:面试题34. 二叉树中和为某一值的路径(回溯法,清晰图解)

思路:不需要维护一个 sum 在参数中传递,直接利用题目给的 target,每次减 node.val,变为 0 则满足条件。

class Solution {
    
    
    LinkedList<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> visited = new LinkedList<>();

    public List<List<Integer>> pathSum(TreeNode root, int target) {
    
    
        dfs(root, target);
        return res;
    }

    void dfs(TreeNode node, int target) {
    
    
        if (node == null) return;

        visited.add(node.val);
        target -= node.val;
	
        if (target == 0 && node.left == null && node.right == null) {
    
    
            res.add(new LinkedList<>(visited));
        }

        dfs(node.left, target);
        dfs(node.right, target);

        // 回溯
        visited.removeLast();
    }
}

37. 二叉搜索树与双向链表

题目:剑指 Offer 36. 二叉搜索树与双向链表

_解法1:一次 DFS + 遍历 list

思路:中序遍历后将节点加到 list,再遍历 list 处理成双向链表。

class Solution {
    
    
    List<Node> list = new LinkedList<>();

    public Node treeToDoublyList(Node root) {
    
    
        helper(root);
        Node hNode = list.get(0);
        // pre 一开始指向最后一个节点
        Node pre = list.get(list.size() - 1);
        for (int i = 0; i < list.size(); i++) {
    
    
            Node node = list.get(i);
            // 当前节点和前一节点,构造成双向循环链表
            node.left = pre;
            pre.right = node;
            pre = node;
        }       
        return hNode; 
    }

    void helper(Node node) {
    
    
        if (node == null) return;
        helper(node.left);
        list.add(node);
        helper(node.right);
    }
}

解法2:一次 DFS

题解:剑指 Offer 36. 二叉搜索树与双向链表(中序遍历,清晰图解)

思路:在中序遍历的同时,完成将二叉搜索树处理成双向链表

class Solution {
    
    
    Node pre, head;

    public Node treeToDoublyList(Node root) {
    
    
        if (root == null)
            return null;
        helper(root);
        // 头尾节点相互指向
        head.left = pre;
        pre.right = head;
        return head;
    }
    /**
     * 中序遍历将二叉搜索树 中间部分 处理成双向链表
     */
    void helper(Node cur) {
    
    
        if (cur == null)
            return;
        helper(cur.left);

        // 处理头部
        if (pre == null) {
    
    
            head = cur;
            pre = head;
        } else {
    
    
            pre.right = cur;
            cur.left = pre;
            pre = cur;
        }

        helper(cur.right);
    }
}

38. 二叉搜索树的第 k 大节点

题目:剑指 Offer 54. 二叉搜索树的第k大节点

_解法1:中序遍历

性质:二叉搜索树的中序遍历为 递增序列

思路:完整的中序遍历一次,遍历到的元素是递增的,然后直接计算位置取元素即可。

class Solution {
    
    

    public int kthLargest(TreeNode root, int k) {
    
    
        List<Integer> list = new ArrayList<>();
        helper(root, list);
        return list.get(list.size() - k);
    }
  
    void helper(TreeNode node, List<Integer> list) {
    
    
        if (node == null) return;
        helper(node.left, list);
        list.add(node.val);
        helper(node.right, list);
    }
}

解法2:倒中序遍历

思路:对二叉搜索树来说,中序遍历为递增序列,那么倒中序遍历就是递减序列,直接遍历到 k 次即可。

class Solution {
    
    
    int res, k;

    public int kthLargest(TreeNode root, int k) {
    
    
        this.k = k;
        helper(root);
        return res;
    }

    void helper(TreeNode node) {
    
    
        if (node == null) return;
        helper(node.right);
     		// 遍历到满足条件直接结束
        if (--k == 0) {
    
     
            res = node.val;
            return;
        }
        helper(node.left);
    }
}

排序(简单)

39. 把数组排成最小的数++

第一次做到排序相关的题目,甚至不确定该不该用语言内置排序 API

给新手的建议:一开始刷到这种题,直接排序 API 搞起来,后面再尝试手撕各种排序算法

题目:剑指 Offer 45. 把数组排成最小的数

解法1:排序 API + 自定义规则

题解:剑指 Offer 45. 把数组排成最小的数(自定义排序,清晰图解)

思路:Java 中实现字典序是利用 A.compareTo(B),这题主要是想到 a + b > b + a

class Solution {
    
    
    /**
     * 系统API + 自定义排序
     */
    public String minNumber(int[] nums) {
    
    
        String[] strs = new String[nums.length];
        for (int i = 0; i < nums.length; i++)
            strs[i] = String.valueOf(nums[i]);

        Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));

        StringBuilder res = new StringBuilder();
        for (String s : strs)
            res.append(s);
        return res.toString();
    }

    /**
     * Lambda 一行写法
     */
    public String minNumber2(int[] nums) {
    
    
        return Arrays.stream(nums)
                .mapToObj(String::valueOf)
                .sorted((a, b) -> (a + b).compareTo(b + a))
                .reduce("", (a, b) -> a + b);
    }
}

Go 语言中的排序利用 sort.Slice() 实现:

func minNumber(nums []int) string {
    
    
	// 将整数数组按字符串形式排序
	sort.Slice(nums, func(i, j int) bool {
    
    
		x := fmt.Sprintf("%d%d", nums[i], nums[j])
		y := fmt.Sprintf("%d%d", nums[j], nums[i])
		return x < y
	})
	res := ""
	for _, v := range nums {
    
    
		res += fmt.Sprintf("%d", v)
	}
	return res
}

解法2:冒泡排序

题解:利用冒泡排序理解这道题

这种解法相对比较好理解:

class Solution {
    
    
    public String minNumber(int[] nums) {
    
    
        String[] strs = new String[nums.length];
        for (int i = 0; i < nums.length; i++)
            strs[i] = String.valueOf(nums[i]);

        for (int i = 0; i < strs.length; i++) {
    
    
            for (int j = 0; j < strs.length - 1; j++) {
    
    
                // 比如 34,3 -----> 343 > 334 所以两个数需要交换位置
                if ((strs[j] + strs[j + 1]).compareTo(strs[j + 1] + strs[j]) > 0) {
    
    
                    String tmp = strs[j + 1];
                    strs[j + 1] = strs[j];
                    strs[j] = tmp;
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < strs.length; i++)
            sb.append(strs[i]);

        return sb.toString();
    }
}

解法3:快速排序

这个暂且放一放,记录几个模板,后面再看:

Java 快速排序模板

40. 扑克牌中的顺子

解法1:Set + 遍历

思路:

  • 找出 max, min 并判断 max - min >= 5 为 false 否则为 true
  • 遍历时通过 Set 判断是否有 0 以外的重复元素,有则 false
class Solution {
    
    
    public boolean isStraight(int[] nums) {
    
    
        Set<Integer> set = new HashSet<>();
        int min = 14, max = 0;
        for (int num : nums) {
    
    
            // 跳过大小王
            if (num == 0)  continue;
            // 若有重复,直接返回 false
            if (set.contains(num)) return false;
            set.add(num); 
            min = Math.min(num, min);
            max = Math.max(num, min);
        }
        return max - min < 5;
    }
}

解法2:排序 + 遍历

题解:面试题61. 扑克牌中的顺子(集合 Set / 排序,清晰图解)

class Solution {
    
    
    public boolean isStraight(int[] nums) {
    
    
        int joker = 0;
        Arrays.sort(nums);
        for (int i = 0; i < 4; i++) {
    
    
            // 统计大小数量
            if (nums[i] == 0)
                joker++;
            // 若有重复,提前返回 false
            else if (nums[i] == nums[i++])
                return false;
        }
        // 最大牌 - 最小牌 < 5 则满足条件
        return nums[4] - nnums[joker] < 5;
    }
}

猜你喜欢

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