《剑指 Offer I》刷题笔记 20 ~ 30 题

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

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

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

go get github.com/emirpasic/gods

动态规划(简单)

20. 斐波那契数列

题目:剑指 Offer 10- I. 斐波那契数列

_解法1:迭代

// go
func fib(n int) int {
    
    
	first, second := 0, 1
	for i := 0; i < n; i++ {
    
    
		second = first + second
		first = second - first
		first %= 1000000007
	}
	return first
}

// go 平行赋值
func fib2(n int) int {
    
    
	a, b := 0, 1
	for i := 0; i < n; i++ {
    
    
		b, a = a+b, b%1000000007
	}
	return a
}

解法2:记忆化递归

参考我的博客:【恋上数据结构】递归

// 记忆化递归
func fib(n int) int {
    
    
	if n <= 1 {
    
    
		return n
	}
	arr := make([]int, n+1)
	arr[1], arr[2] = 1, 1
	return helper(arr, n)
}

func helper(arr []int, n int) int {
    
    
	if arr[n] == 0 {
    
    
		arr[n] = helper(arr, n-1) + helper(arr, n-2)
	}
	return arr[n] % 1000000007
}

解法3:动态规划

题解:面试题10- I. 斐波那契数列(动态规划,清晰图解)

// go
// 更好理解的动态规划
func fib(n int) int {
    
    
	if n <= 1 {
    
    
		return n
	}
	dp := make([]int, )
	dp[0], dp[1] = 0, 1
	for i := 2; i <= n; i++ {
    
    
    // 递推方程:f(n) = f(n-1) + f(n-2)
		dp[i] = (dp[i-1] + dp[i-2]) % 1000000007
	}
	return dp[n]
}
// java
// 优化过的动态规划
class Solution {
    
    
    public int fib(int n) {
    
    
        int a = 0, b = 1;
        for (int i = 0; i < n; i++) {
    
    
            int sum = (a + b) % 1000000007;
            a = b;
            b = sum;
        }
        return a;
    }
}

21. 青蛙跳台阶问题

题目:剑指 Offer 10- II. 青蛙跳台阶问题

_解法1:动态规划

// go
func numWays(n int) int {
    
    
	if n <= 1 {
    
    
		return 1
	}
	dp := make([]int, n+1)
	dp[0], dp[1] = 1, 1
	for i := 2; i <= n; i++ {
    
    
		dp[i] = (dp[i-1] + dp[i-2]) % 1000000007
	}
	return dp[n]
}

题解:面试题10- II. 青蛙跳台阶问题(动态规划,清晰图解)

func numWays1(n int) int {
    
    
	a, b := 1, 1
	for i := 0; i < n; i++ {
    
    
		sum := (a + b) % 1000000007
		a = b
		b = sum
	}
	return a
}
func numWays2(n int) int {
    
    
	if n <= 1 {
    
    
		return 1
	}
	a, b := 1, 2
	for i := 2; i < n; i++ {
    
    
		b = (a + b)
		a = b - a
		b %= 1000000007
	}
	return b
}

22. 股票的最大利润*

此题的动规写法与下面一题【连续子数组的最大和】比较着看

题目:剑指 Offer 63. 股票的最大利润

_解法1:暴力

// go
func maxProfit(prices []int) int {
    
    
	max := 0
	for i := len(prices) - 1; i >= 0; i-- {
    
    
		for j := 0; j < i; j++ {
    
    
			tmp := prices[i] - prices[j]
			if tmp > max {
    
    
				max = tmp
			}
		}
	}
	return max
}

解法2:动态规划

// java
class Solution {
    
    
 		// dp(n) 代表第n天的最大利润
  	// dp(n) = Math.max(dp(n - 1), prices[n] - min)
  	// 第n天最大利润等于 第n-1天最大利润 跟 第n天股票价格和股票历史最低价的差值 相比
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        if (len < 2) return 0;
        int[] dp = new int[len];
        dp[0] = 0;
        int min = prices[0]; 
        for (int i = 1; i < len; i++) {
    
    
            min = Math.min(min, prices[i-1]);
            // 状态转移方程
            dp[i] = Math.max(dp[i-1], prices[i] - min);
        }
        return dp[len - 1];
    }
}

有个细节,循环中找最小值时,这样更快:

for (int i = 1; i < len; i++) {
     
     
		if (prices[i] < min)
    		min = prices[i];
  	dp[i] = Math.max(dp[i-1], prices[i] - min);
}

像这种无需记录每次的状态值,可以使用一个变量取代 dp 数组,直接记录结果:

// java
class Solution {
    
    
    /**
     * 动态规划优化 - 使用一个变量即可记录结果
     */
    public int maxProfit1(int[] prices) {
    
    
        if (prices.length < 2) return 0;
        int min = prices[0], profit = 0;
        for (int i = 0; i < prices.length; i++) {
    
    
            if (prices[i-1] < min)
            		min = prices[i-1];
            profit = Math.max(profit, prices[i] - min);
        }
        return profit;
    }
}

解法3:迭代

思路:记录一个 max,从后往前遍历,如果比 max 大就更新 max,否则计算利润。

// java
class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        if (prices.length < 2) return 0;
        int max = 0, res = 0;
        for (int i = prices.length - 1; i >= 0; i--) {
    
    
            if (prices[i] > max)
                max = prices[i];
            else
                res = Math.max(res, max - prices[i]);
        }
        return res;
    }
}

动态规划(中等)

23. 连续子数组的最大和*

此题的动规写法与上面一题【股票的最大利润】比较着看

题目:剑指 Offer 42. 连续子数组的最大和

[4, 1, -6, 2] 的情况有助于理解动态规划的思想。

解法1:动态规划

标准的动态规划思想:

// java
// 使用if语句写状态转移方程
class Solution {
    
    
    public int maxSubArray(int[] nums) {
    
    
        // dp[i]的含义:以nums[i]结尾的连续子数组的最大和
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
    
    
            if (dp[i-1] > 0) dp[i] = dp[i-1] + nums[i];
            else dp[i] = nums[i];
            max = Math.max(max, dp[i]);
        }
      	// 求dp数组的max
        int max = dp[0];
        for (int i = 0; i < dp.length; i++) {
    
    
           if (dp[i] > max) max = dp[i];
        }
        return max;
        return max;
    }
}
// java
// 使用Math.max写状态转移方程
class Solution {
    
    
    public int maxSubArray(int[] nums) {
    
    
        // dp[i]的含义: 以nums[i]结尾的连续子数组的最大和
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
    
    
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        }
      	// 求dp数组的max
        int max = dp[0];
        for (int i = 0; i < dp.length; i++) {
    
    
           if (dp[i] > max) max = dp[i];
        }
        return max;
    }
}

代码优化:将求最大值的循环给优化掉

// java
class Solution {
    
    
    public int maxSubArray(int[] nums) {
    
    
        // dp[i]的含义: 以nums[i]结尾的连续子数组的最大和
        int[] dp = new int[nums.length];
        int max = dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
    
    
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

// go
func maxSubArray(nums []int) int {
    
    
	var dp = make([]int, len(nums))
	dp[0] = nums[0]
	max := dp[0]
	for i := 1; i < len(nums); i++ {
    
    
		if dp[i-1] > 0 {
    
    
            dp[i] = dp[i-1] + nums[i]
		} else {
    
    
			dp[i] = nums[i]
		}
		max = int(math.Max(float64(max), float64(dp[i])))
	}
	return max
}

24. 礼物的最大价值*

题目:剑指 Offer 47. 礼物的最大价值

_解法1:递归

普通递归:(超时)

// java
class Solution {
    
    
    public int maxValue(int[][] grid) {
    
    
        if (grid.length == 1 && grid[0].length == 1)
            return grid[0][0];
        return helper(grid, grid.length - 1, grid[0].length - 1);
    }

    int helper(int[][] grid, int x, int y) {
    
    
        if (x < 0 || y < 0)
            return 0;
        if (x == 0) {
    
    
            int res = 0;
            for (int i = 0; i <= y; i++)
                res += grid[0][i];
            return res;
        }
        if (y == 0) {
    
    
            int res = 0;
            for (int i = 0; i <= x; i++)
                res += grid[i][0];
            return res;
        }
        return Math.max(helper(grid, x - 1, y), helper(grid, x, y - 1)) + grid[x][y];
    }
}

记忆化搜索:利用额外的存储空间来存储递归时的中间值

// java
class Solution {
    
    
    public int maxValue(int[][] grid) {
    
    
        if (grid.length == 1 && grid[0].length == 1)
            return grid[0][0];
        // 存储递归结果
        int[][] tmp = new int[grid.length][grid[0].length];
        return helper(grid, grid.length - 1, grid[0].length - 1, tmp);
    }

    int helper(int[][] grid, int x, int y, int[][]tmp) {
    
    
        if (tmp[x][y] != 0)
            return tmp[x][y];
        if (x < 0 || y < 0)
            return 0;
        if (x == 0) {
    
    
            int res = 0;
            for (int i = 0; i <= y; i++)
                res += grid[0][i];
            tmp[x][y] = res; // 存储递归结果
            return res;
        }
        if (y == 0) {
    
    
            int res = 0;
            for (int i = 0; i <= x; i++)
                res += grid[i][0];
            tmp[x][y] = res; // 存储递归结果1
            return res;
        }
        tmp[x][y] = Math.max(helper(grid, x - 1, y, tmp), helper(grid, x, y - 1, tmp)) 
          + grid[x][y];
        return tmp[x][y];
    }
}

解法2:动态规划

题解:面试题47. 礼物的最大价值(动态规划,清晰图解)

基础的动态规划思路:

// java
class Solution {
    
    
    public int maxValue(int[][] grid) {
    
    
        int m = grid.length, n = grid[0].length;
      	// dp[i][j] 表示从 grid[0][0] 到 grid[i-1][j-1] 时的最大价
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        // 初始化dp最左边一列,从上到下累加
        for (int i = 1; i < m; i++)
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        // 初始化dp最上边一行,从左到右累加
        for (int i = 1; i < n; i++)
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        // 递推公式的计算
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
        return dp[m - 1][n - 1];
    }
}

优化:原地修改,空间复杂度优化为 O(1)。

class Solution {
    
    
    public int maxValue(int[][] grid) {
    
    
        int m = grid.length, n = grid[0].length;
      	// 初始化第一列
        for (int j = 1; j < n; j++)
            grid[0][j] += grid[0][j - 1];
      	// 初始化第一行
        for (int i = 1; i < m; i++)
            grid[i][0] += grid[i - 1][0];
      	// 递推公式的计算
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
        return grid[m - 1][n - 1];
    }
}

简化代码的技巧:多开一行一列的空间,经常会使代码更简洁

class Solution {
    
    
    public int maxValue(int[][] grid) {
    
    
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++)
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
        return dp[m][n];
    }
}

25. 把数字翻译成字符串

题目:剑指 Offer 46. 把数字翻译成字符串

题解1:递归

思路:这题是 [青蛙跳台阶问题] 的抽象升级版。

// go
func translateNum(num int) int {
    
    
	if num < 10 {
    
    
		return 1
	}
	if num%100 >= 10 && num%100 <= 25 {
    
    
		return translateNum(num/100) + translateNum(num/10)
	} else {
    
    
		return translateNum(num / 10)
	}
}

题解2:动态规划

做动态规划的时候还是要尝试以递归的思路去思考。

func translateNum(num int) int {
    
    
	// dp[i] - 以Xi结尾的数字的翻译方案
	numStr := strconv.Itoa(num)
	var dp = make([]int, len(numStr)+1)
	dp[0], dp[1] = 1, 1
	for i := 2; i <= len(numStr); i++ {
    
    
		// 最后两位组成的数字
		tmp := int(numStr[i-2]-'0')*10 + int(numStr[i-1]-'0')
		if tmp >= 10 && tmp <= 25 {
    
    
			dp[i] = dp[i-1] + dp[i-2]
		} else {
    
    
			dp[i] = dp[i-1]
		}
	}
	return dp[len(numStr)]
}

26. 最长不含重复字符的子字符串

题目:剑指 Offer 48. 最长不含重复字符的子字符串

_题解1:动态规划

// go
func lengthOfLongestSubstring(s string) int {
    
    
	if s == "" {
    
    
		return 0
	}
	// dp[i] 代表以 s[i]结尾的最长不含重复字符的子字符串
	var dp = make([]string, len(s))
	dp[0] = s[:1]
	max := len(dp[0])
	for i := 1; i < len(s); i++ {
    
    
		idx := strings.IndexByte(dp[i-1], s[i])
		if idx == -1 {
    
    
			dp[i] = dp[i-1] + string(s[i])
		} else {
    
    
			dp[i] = string(dp[i-1][idx+1:]) + string(s[i])
		}
		max = maxInt(max, len(dp[i]))
	}
	return max
}

func maxInt(a int, b int) int {
    
    
	if a < b {
    
    
		return b
	}
	return a
}

题解2:滑动窗口

class Solution {
    
    
    public int lengthOfLongestSubstring(String s) {
    
    
        int res = 0;
        Set<Character> set = new HashSet<>();
        for (int l = 0, r = 0; r < s.length(); r++) {
    
    
            char c = s.charAt(r);
          	// 让左指针移动到满足条件为止
            while (set.contains(c))
                set.remove(s.charAt(l++));
            set.add(c);
            res = Math.max(res, r - l + 1);
        }
        return res;
    }
}

双指针(简单)

27. 删除链表的节点

题目:剑指 Offer 18. 删除链表的节点

_解法1:迭代 + 双指针

// java
class Solution {
    
    
    public ListNode deleteNode(ListNode head, int val) {
    
    
        if (head.val == val) return head.next;
        // cur 进行遍历, pre 保存遍历的前一位置
        ListNode cur = head, pre = head;
        while (cur != null) {
    
    
            if (cur.val == val) {
    
    
                pre.next = cur.next;
                break;
            }
            pre = cur; // 保存遍历的前一位置
            cur = cur.next;
        }
        return head;
    }
}

_解法2:递归

评论区看到有人用递归做,立马试了试,一下就写出来了!看来刷了这么多题还是有效果的!

// java
class Solution {
    
    
    public ListNode deleteNode(ListNode head, int val) {
    
    
        if (head.val == val) return head.next;
        ListNode node = deleteNode(head.next, val);
        head.next = node;
        return head;
    }
}

28. 链表中倒数第 k 个节点

题目:剑指 Offer 22. 链表中倒数第k个节点

_解法1:暴力迭代

思路:遍历两次,第一次遍历获取链表长度,第二次遍历 n - k 次

class Solution {
    
    
    public ListNode getKthFromEnd(ListNode head, int k) {
    
    
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
    
    
            cur = cur.next;
            n++;
        }
        cur = head;
        while (n > k) {
    
    
            cur = cur.next;
            n--;
        }
        return cur;
    }
}

_解法2:哈希表

思路:利用哈希表存储遍历的索引对应的链表头节点,遍历完获取到长度 n 后直接根据 n -k 取

class Solution {
    
    
    public ListNode getKthFromEnd(ListNode head, int k) {
    
    
        Map<Integer, ListNode> tmp = new HashMap<>();
        ListNode cur = head;
        int n = 0;
        while (cur != null) {
    
    
            tmp.put(n++, cur);
            cur = cur.next;
        }
        return tmp.get(n - k);
    }
}

解法3:快慢指针

题解:面试题22. 链表中倒数第 k 个节点(双指针,清晰图解)

思路:提前设置 fast 指针比 slow 指针先走 k 步,当 fast 走完,slow 就是倒数第 k 个节点

class Solution {
    
    
    public ListNode getKthFromEnd(ListNode head, int k) {
    
    
        ListNode slow = head, fast = head;
        while (k-- > 0)
            fast = fast.next;
        while (fast != null) {
    
    
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

29. 合并两个排序的链表

题目:剑指 Offer 25. 合并两个排序的链表

_解法1:伪头节点

题解:面试题25. 合并两个排序的链表(伪头节点,清晰图解)

思路:指针轮流遍历

// java
class Solution {
    
    
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        ListNode cur = new ListNode(0), resNode = cur;

        while (l1 != null && l2 != null) {
    
    
            if (l1.val >= l2.val) {
    
    
                cur.next = new ListNode(l2.val);
                l2 = l2.next;
            } else {
    
    
                cur.next = new ListNode(l1.val);
                l1 = l1.next;
            }
            cur = cur.next;
        }
        cur.next = (l1 == null) ? l2 : l1;
        return resNode.next;
    }
}

题解中的写法:每次不用 new ListNode

class Solution {
    
    
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        ListNode dum = new ListNode(0), cur = dum;
        while (l1 != null && l2 != null) {
    
    
            if (l1.val < l2.val) {
    
    
                cur.next = l1;
                l1 = l1.next;
            } else {
    
    
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = (l1 != null) ? l1 : l2;
        return dum.next;
    }
}

解法2:递归

class Solution {
    
    
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        if (l1.val <= l2.val) {
    
    
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
    
    
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

30. 两个链表的第一个公共节点

题目:剑指 Offer 52. 两个链表的第一个公共节点

_解法1:哈希 / Set

思路:哈希 / Set 的思路是一样的,都是遍历第一个链表后把所有节点存起来,在遍历第二个链表的时候,尝试从 Map / Set 中去取,能取到则是公共节点。

class Solution {
    
    
    /**
     * 哈希
     */
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    
        Map<Integer, ListNode> map = new HashMap<>();
        ListNode cur = headA;
        while (cur != null) {
    
    
            map.put(cur.val, cur);
            cur = cur.next;
        }
        cur = headB;
        while (cur != null) {
    
    
            if (map.get(cur.val) == cur)
                return cur;
            cur = cur.next;
        }
        return null;
    }
    /**
     * set
     */
    public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
    
    
        Set<ListNode> set = new HashSet<>();
        ListNode cur = headA;
        while (cur != null) {
    
    
            set.add(cur);
            cur = cur.next;
        }
        cur = headB;
        while (cur != null) {
    
    
            if (set.contains(cur))
                return cur;
            cur = cur.next;
        }
        return null;
    }
}

解法2:双指针 + 交叉 ❤️

题解:图解 双指针法,浪漫相遇

class Solution {
    
    
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    
        if (headA == null || headB == null) return null;
        ListNode A = headA, B = headB;
        while (A != B) {
    
    
            A = (A != null) ? A.next : headB;
            B = (B != null) ? B.next : headA;
        }
        return A;
    }
}

猜你喜欢

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