CookBook

本文深入探讨了数组、链表、二叉树相关的经典算法,包括TwoSum、求最大面积、ThreeSum、组合求和、寻找逆序对数组、树的子结构、最长递增子序列、编辑距离、戳气球问题、二叉树的直径等。文章通过实例详细解析了各种解题思路和方法,如动态规划、二分搜索、回溯、滑动窗口等,旨在帮助读者掌握这些核心算法。
摘要由CSDN通过智能技术生成

CookBook + 剑指Offer

Array

1. TwoSum

package LeetCode.cookbook.Array;

import java.util.HashMap;
import java.util.Map;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-14 19:29
 **/

public class twoSum {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashMap = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int another = target - nums[i];
            if (hashMap.containsKey(target - nums[i])) {
                return new int[]{hashMap.get(another), i};
            }
            hashMap.put(nums[i], i);
        }
        return null;
    }
}

11. maxArea

package LeetCode.cookbook.Array;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-14 19:48
 **/

public class maxArea {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1;
        int res = 0;
        while (left < right) {
            int col = Math.min(height[left], height[right]);
            int row = right - left;
            res = Math.max(res, col * row);
            if (height[left] >= height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return res;
    }
}

15. threeSum

package LeetCode.cookbook.Array;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-14 19:56
 **/

public class threeSum {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();

        if (nums == null || nums.length <= 2) return res;

        Arrays.sort(nums);

        for (int i = 0; i < nums.length - 2; i++) {
            if (nums[i] > 0) break;
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int target = -nums[i];
            int left = i + 1, right = nums.length - 1;

            while (left < right) {
                if (nums[left] + nums[right] == target) {
                    res.add(new ArrayList<>(Arrays.asList(nums[left], nums[right], nums[i])));
                    left++;
                    right--;
                    // 关键是这里的去重的方法
                    while (left < right && nums[left] == nums[left - 1]) left++;
                    while (left < right && nums[right] == nums[right + 1]) right--;
                } else if (nums[left] + nums[right] < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return res;
    }
}

39. combinationSum

基本DFS
注意区分是否可以利用重复的元素

package LeetCode.cookbook.Array;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-16 10:37
 **/

public class combinationSum {
    List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates);
        dfs(candidates, target, 0, 0, res, new ArrayList<>());
        return res;
    }

    public void dfs(int[] nums, int target, int index, int cur, List<List<Integer>> res, List<Integer> temp) {
        if (index == nums.length) return;
        if (cur == target) {
            res.add(new ArrayList<>(temp));
            return;
        }

        if (cur > target) return;

        for (int i = index; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1])
                continue;
            temp.add(nums[i]);
            dfs(nums, target, i, nums[i], res, temp);
            temp.remove(temp.size() - 1);
        }
    }
}

40. combinationSum2

package LeetCode.cookbook.Array;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-16 10:53
 **/

public class combinationSum2 {
    public List<List<Integer>> combinationSum2(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0) return res;
        Arrays.sort(nums);

        dfs(nums, target, 0, 0, res, new ArrayList<>());

        return res;
    }

    public void dfs(int[] nums, int target, int cur, int index, List<List<Integer>> res, List<Integer> temp) {
        if (target == cur) {
            res.add(new ArrayList<>(temp));
            return;
        }
        if (cur > target) return;

        for (int i = index; i < nums.length; i++) {
            if (i > index && nums[i] == nums[i - 1])
                continue;

            temp.add(nums[i]);
            dfs(nums, target, cur + nums[i], i + 1, res, temp);
            temp.remove(temp.size() - 1);
        }
    }
}

41. firstMissingPositive

package LeetCode.cookbook.Array;

/**
 * @program: JavaLife
 * @author: JiaLe Hu
 * @create: 2020-12-16 11:07
 **/


// 原地hash的思想 nums[nums[i] - 1] = nums[i]
public class firstMissingPositive {
    public int firstMissingPositive(int[] nums) {
        int len = nums.length;
        for (int i = 0; i < nums.length; i++) {
        	// 注意小于0和大于len的元素
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                swap(nums, nums[i] - 1, i);
            }
        }
        for (int i = 0; i < len; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        return len + 1;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

42. trap(接雨水)

解法1: 单调栈

// cur <= top 直接入栈
// cur > top 计算面积 并保证cur <= top
public int trap(int[] height){
	int ans = 0, current = 0;
	int size = height.length;
	Deque<Integer> stack = new LinkedList<>();
	while(current < size) {
		while(!stack.isEmpty() && height[stack.peek()] < height[current]){
			int top = stack.pop();
			if(stack.isEmpty()) break;
			int distance = current - stack.peek() - 1;
			int bounded_height = Math.min(heigth[stack.peek()], height[current]) - height[top];
			ans += distance * bounded_height;
		}
		stack.push(current++);
	}
	return ans;
}

解法2:暴力模拟 时间复杂度O(n2)的模拟

public int trap(int[] height){
    int ans = 0, current = 0;
    for (int i = 1; i < height.length - 1; i++) {
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) {
            max_left = Math.max(max_left, height[j]);
        }
        for (int j = i; j < height.length; j++) {
            max_right = Math.max(max_right, height[j]);
        }
        ans += Math.min(max_left, max_right) - height[i];
    }
    return ans;
}

链表

92. 反转链表 II

本题尝试使用递归的思路去解题,但是如果考虑效率的话还是推荐使用迭代

public class ListNodeModel {
	// 记录后驱结点
    public ListNode successor;

    // 反转前n的链表结点
    public ListNode reverseN(ListNode head, int n) {
        if (n == 1) {
            successor = head.next;
            return head;
        }
        ListNode last = reverseN(head.next, n - 1);
        head.next.next = head;
        head.next = successor;
        return last;
    }

    // 递归的版本
    public ListNode reverseBetween(ListNode head, int m, int n) {
        if (m == 1)
            return reverseN(head, n);
        head.next = reverseBetween(head.next, m - 1, n - 1);
        return head;
    }

    // 迭代的版本
    public ListNode reverseBetween_helper(ListNode head, int m, int n) {
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode pre = dummyNode;
        for (int i = 0; i < m - 1; i++) {
            pre = pre.next;
        }
        ListNode next;
        ListNode cur = pre.next;

        for (int i = 0; i < n - m; i++) {
            next = cur.next;
            cur.next = next.next;
            next.next = pre.next;
            pre.next = next;
        }

        return dummyNode.next;
    }
}

寻找链表的中间节点

public ListNode findMidListNode(ListNode head) {
	if (head == null) {
		return null;
	}
	ListNode slow = head, fast = head;
	// 偶数 寻找的是中间左边的点
	while (fast.next != null && fast.next.next != null) {
		slow = slow.next;
		fast = fast.next.next;
	}
	// 偶数 寻找的是中间右边的点
	while (fast != null && fast.next != null) {
		slow = slow.next;
		fast = fast.next.next;
	}
	// 寻找 中间前面的点
	ListNode slow = head, fast = head.next;
	ListNode pre = slow;
	while (fast != null && fast.next != null) {
		pre = slow;
		slow = slow.next;
		fast = fast.next.next;
	}
}

Binary Search

模块思路

寻找一个数

	public int binary_search(int[] nums, int t) {
		int left = 0;
		int right = nums.length -1;
		
		while(left < right) {
			int mid = left + (right - left) / 2;
			if (nums[mid] == target) {
				return mid;
			} else if (nums[mid] < target) {
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
	}

寻找左侧边界的二分搜索

while(left < right) 终止的条件是left == right
while(left <= right) 终止条件为left == right + 1

为什么是left = mid + 1,right = mid

搜索区间为[left, right) ,所以当前的mid被剔除掉之后,下一步应该被分为两个区间,分别为[left, mid) 和 [left + 1, right)

	public int left_bound (int[] nums, int target) {
		if (nums.length == 0)
            return -1;
        int left = 0;
        int right = nums.length;

        while (left < right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] == target)
                right = mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        if (left == nums.length) return -1;
        return nums[left] == target ? left : -1;
	}

寻找右侧边界的二分搜索

	public static int right_bound(int[] nums, int target) {
        if (nums.length == 0)
            return -1;
        int left = 0, right = nums.length;

        while (left < right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] == target) {
                left = mid + 1;
            } else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        if (left - 1 == 0) return -1;
        return nums[left - 1] == target ? left - 1 : -1;
    }

快速幂

public double myPow(double x, int n) {
	if (x == 0.0d) return 0.0d;
	long b = n;
	double res = 1.0;
	if (b < 0) {
		x = 1 / x;
		n = -n;
	}
	while(b > 0){
		if ((b & 1) == 1) res *= x;
		x *= x;
		b >>= 1;
	}
	return res;
}

DP

70. 爬楼梯 O(logn)解法

将状态转化为矩阵相乘的问题 后面补坑

public int climbStairs(int n){
	if(n < 1) return 0;
	if(n == 1 || n == 2) return n;
	int[][] base = new int{{1, 1}, {1, 0}};
	int[][] res = matrixPower(base, n - 2);
	return 2 * res[0][0] + res[1][0];
}

300. 最长递增子序列

dp[i]为前i项最长递增子序列
求第i项时,会使得前i项都会依次改变,所以需要二重循环

public int lengthOfLIS(int[] nums) {
        if (null == nums || nums.length == 0) return 0;

        int[] dp = new int[nums.length];

        dp[0] = 1;
        int ans = 1;
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }

72. 编辑距离

很自然想到dp[i][j]代表着word1前i个字符和word2前j个字符之间的关系
状态转化关系为
word1[i] == word2[j]

  • dp[i][j]=dp[i - 1][j - 1]

word1[i] != word2[j]

  • dp[i][j] = dp[i - 1][j - 1] + 1 代表前i-1项和前j-1项保持相同,则需要替换
  • dp[i][j] = dp[i - 1][j] + 1代表前i-1项和前j项保持相同,则需要删除
  • dp[i][j] = dp[i][j - 1] + 1 代表前i项和前j-1项保持相同,则需要插入
	public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len2 + 1][len1 + 1];
        // 边界
        dp[0][0] = 0;
        for (int i = 0; i < len1 + 1; i++) {
            dp[0][i] = i;
        }
        for (int i = 0; i < len2 + 1; i++) {
            dp[i][0] = i;
        }
        // 循环
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else{
                    dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
                }
            }
        }
        // 返回结果
        return dp[len1][len2];
    }

312. 戳气球

  • 正确思路
    dp[i][j]:代表的是开区间(i,j)中选取一个k属于[i+1, j-1]的区间所得的分数最大,即dp[i][j] = dp[i, k] + dp[k, j] + nums[i] * nums[j] * nums[k]
  • 错误思路
    从[i,j]中取一个k值,分为dp[i, k - 1]以及dp[k + 1, j]的值,子问题存在重叠,子问题不是独立的问题。例:先戳破k-1则对k+1的影响就是左边界变成是k-2
	public int maxCoins_dp(int[] nums) {
		int n = nums.length;
		int[][] rec = new int[n + 2][n + 2];
		int[] val = new int[n + 2];
		val[0] = val[n + 1] = 1;
		for(int i = 1; i <= n; i++) {
			val[i] = nums[i - 1];
		}
		for (int i = n - 1; i >= 0; i--) {
			for (int j = i + 2; j <= n + 1; j ++) {
				for (int k = i + 1; k < j; k++) {
					int sum = val[i] * val[j] * val[k];
					sum += rec[i][k] * rec[k][j];
					rec[i][j] = Math.max(rec[i][j], sum)
				}
			}
		}
		return rec[0][n + 1];
	}

1458. 两个子序列的最大点积

dp[i][j]有两种解释方法

  • 以nums1[i]和nums2[j]为结尾的子序列的最大点积
  • 以nums1前i项和nums2前j项的最大点积
    如果以第一种方法思考,dp[i][j] = Math.max(nums1[i] * nums2[j], nums[i] * nums[j] + maxValue ) 此时需要获取前i项和前j项的最大点积,就将思路转化到第二种思路上了
	// dp[i][j] 代表以i结尾的 前j结尾的 nums1 和 nums2 的最大值
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        int[][] dp = new int[n1 + 1][n2 + 1];
        for(int i = 0; i < n1; i++) {
            Arrays.fill(dp[i], Integer.MIN_VALUE);
        }
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                // 选择 nums1[i] 和 nums2[j] 但是不选择前面
                dp[i][j] = nums1[i - 1] * nums2[j - 1];
                // 选择 nums1[i] 和 nums2[j] 但是选择前面
                dp[i][j] = Math.max(dp[i][j], nums1[i - 1] * nums2[j - 1] + dp[i - 1][j - 1]);
                dp[i][j] = Math.max(dp[i][j], dp[i][j - 1]);
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1]);
            }
        }
        return dp[n1][n2];
    }

629. K个逆序对数组

定义dp方程

dp[i][j] 为前i个数中逆序对存在j个的数量

假设 #### 为前4个所组成的序列,现在要添加第五个序列则第5个添加的范围为
5#### :增加的逆序对的数量为4
#5### :…为3
##5## :…为2
###5# :…为1
####5 :…为0
所以得到的状态转化方程为dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + dp[i - 1][j - 2] + ... + dp[i - 1][j - i + 1]

  • 第一种想法
    根据上述内容写出状态转换方程,时间复杂度为 O ( n 2 ∗ k ) O(n^2*k) O(n2k)导致超时
  • 第二种想法
    发现后面其实是求和

dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + ... + dp[i - 1][j - i + 1]

dp[i][j - 1] = dp[i - 1][j - 1] + dp[i - 1][j - 2] + ... + dp[i - 1][j - i)]

dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i -1][j - i]

	 public int kInversePairs(int n, int k) {
	 	int[][] f = new int[n + 1][k + 1];
	 	for(int i = 1; i <= n; i++) {
	 		f[i][0] = 1;
	 	}
	 	// dp[i - 1][j] - (j >= i ? dp[i - 1][j - i] : 0) + mod)
	 	// 这里可能为负数,所以需要 + mod
	 	// i * (i - 1) / 2 为总数
	 	for(int i = 2; i <= n; i++) {
	 		int cnt = i * (i - 1) / 2;
	 		for (int j = 1; j <= Math.max(cnt, k); j++) {
                dp[i][j] = (dp[i][j - 1] % mod + (dp[i - 1][j] - (j >= i ? dp[i - 1][j - i] : 0) + mod) % mod) % mod;
            }
	 	}
	 	return dp[n][k];
	 }

Tree

549. 二叉树中最长的连续序列

  • 最长的连续序列=递增序列+递减序列-1
  • 对于每一个节点都属于一个最长递增序列和最长递减序列,计算的过程要相互独立
  • 不一定要从根节点出发
public int longestConsecutive(TreeNode root) {
        return root == null ? 0 : Math.max(dfs(root), Math.max(longestConsecutive(root.left), longestConsecutive(root.right)));
    }

    public int dfs(TreeNode root) {
        return up_dfs(root) + down_dfs(root) - 1;
    }

    public int up_dfs(TreeNode root){
    	int res = 0;
    	if(root.left != null && root.left.val == root.val + 1) {
    		res = Math.max(res, up_dfs(root.left)); 
    	}
    	if(root.right != null && root.right.val == root.val + 1) {
    		res = Math.max(res, up_dfs(root.right));
    	}
    	return ++res;
    }

    public int down_dfs(TreeNode root) {
        int res = 0;
        if (root.left != null && root.left.val + 1 == root.val) {
            res = Math.max(res, down_dfs(root.left));
        }
        if (root.right != null && root.right.val + 1 == root.val) {
            res = Math.max(res, down_dfs(root.right));
        }
        return ++res;
    }

1530. 好叶子节点对的数量

  • 后序遍历可以获取当前节点的左右子树的情况
  • 遍历非叶子结点时,需要获取它左右子树所有叶子节点的情况(注意此情况不存在重复的情况,因为以当前节点为根节点的最短路径只有一条)
    public int ans = 0;

    public int countPairs(TreeNode root, int distance) {
        if (root == null) return 0;
        dfs(root, distance);
        return ans;
    }

    List<Integer> dfs(TreeNode root, int distance) {
        if (root == null) return new ArrayList<>();

        if (root.left == null && root.right == null) {
            List<Integer> res = new ArrayList<>();
            res.add(1);
            return res;
        }

        List<Integer> ret = new ArrayList<>();

        List<Integer> left = dfs(root.left, distance);
        for (Integer e : left) {
            if (++e < distance) {
                ret.add(e);
            }
        }

        List<Integer> right = dfs(root.right, distance);
        for (Integer e : right) {
            if (++e < distance) {
                ret.add(e);
            }
        }

        System.out.println("left is " + left);
        System.out.println("right is " + right);
        System.out.println("----------");

        for (Integer l : left) {
            for (Integer r : right) {
                if (l + r <= distance) {
                    ans++;
                }
            }
        }

        return ret;
    }

222. 完全二叉树的节点个数

  • 完全二叉树是指只有最后一层没有填满节点,其他的各个层都填满节点,最后一层从最左边开始填节点
  • 左子树和右子树深度相同,则左子树的个数已经达到满二叉树则节点个数为 2 k 2^k 2k包括根节点
  • 左子树和右子树深度不同,则倒数第二层的右子树达到满二叉树则节点个数为 2 k 2^k 2k包括根节点
	public int countNode(TreeNode root) {
		if (root == null) return 0;
		
		int left = countLevel(root.left);
		int right = countLevel(root.right);
		if (left != right) {
			return countNode(root.left) + 1 << right;
		} else {
			return countNode(root.right) + 1 << left;
		}
	}
	public int countLevel(TreeNode root){
		if (root == null) return 0;
		int level = 0;
		while (root != null) {
			level += 1;
			root = root.left;
		} 
		return level;
	}

剑指 Offer 26. 树的子结构

  • 数的子结构需要考虑值重复的问题
	public boolean isSubStructure(TreeNode A, TreeNode B) {
        if (B == null || A == null) return false;
        if (A.val == B.val && helper(A.left, B.left) && helper(A.right, B.right)) {
            return true;
        }
        return isSubStructure(A.left, B) || isSubStructure(A.right, B);
    }

    public boolean helper(TreeNode node1, TreeNode node2) {
        if (node2 == null) return true;

        if (node1 == null) return false;

        if (node1.val == node2.val) {
            return helper(node1.left, node2.left) && helper(node1.right, node2.right);
        } else {
            return false;
        }
    }

面试题0412.求和路径

  • 递归写法
  • 前缀和优化写法
    前缀和回溯的原因是所求的当前路径只能在一条路径上(理解一下)

递归写法

	private int res = 0;
	
	public int pathSum(TreeNode root, int sum) {
		if (root == null) return 0;
		dfs(root, sum);
		pathSum(root.left, sum);
		pathSum(root.right, sum);
		return res; 	
	}
	
	public void dfs(TreeNode root, int sum) {
		if (root == null) return ;
		sum = sum - root.val;
		if (sum == 0) {
			res += 1;
		}
		dfs(root.left, sum);
		dfs(root.right, sum);
	}

前缀和

	public int pathSum_helper(TreeNode root, int sum) {
        Map<Integer, Integer> prefixSumCount = new HashMap<>();
        // key 为前缀和 value 是大小为key的前缀和出现的次数
        prefixSumCount.put(0, 1);

        return recursionPathSum(root, prefixSumCount, sum, 0);
    }

    public int recursionPathSum(TreeNode node, Map<Integer, Integer> prefixSumCount, int target, int currSum) {
        if (node == null) return 0;

        int res = 0;

        currSum += node.val;

        res += prefixSumCount.getOrDefault(currSum - target, 0);

        prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);

        res += recursionPathSum(node.left, prefixSumCount, target, currSum);
        res += recursionPathSum(node.right, prefixSumCount, target, currSum);

        prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);

        return res;
    }

236. 二叉树的最近公共祖先

  • 后序遍历
  • p为q的父节点 q为p的父节点 p q 有最近的父节点
	public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;

        if (root.val == p.val || root.val == q.val) return root;

        TreeNode leftRoot = lowestCommonAncestor(root.left, p, q);

        TreeNode rightRoot = lowestCommonAncestor(root.right, p, q);

        // p q 在两侧
        if (leftRoot != null && rightRoot != null) return root;
        // p q 在一侧
        //   p 为 q 的父节点
        //   p 为 p 的父节点
        //   p q 有最近的父节点
        if (leftRoot != null) return leftRoot;
        // p q 在一侧
        //   p 为 q 的父节点
        //   p 为 p 的父节点
        //   p q 有最近的父节点
        if (rightRoot != null) return rightRoot;

        return null;
    }

235. 二叉搜索树的最近公共祖先

  • 二叉搜索树的性质,如果root > left, right则从right子树查找 如果root < left,right则从left子树查找 如果相等,或者处于left,right之间则break
	public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        TreeNode ancestor = root;

        while (true) {
            if (p.val < ancestor.val && q.val < ancestor.val)
                ancestor = ancestor.left;
            else if (p.val > ancestor.val && q.val > ancestor.val)
                ancestor = ancestor.right;
            else break; // 处于中间 或 等于
        }

        return ancestor;
    }

1110. 删点成林

  • 典型的二叉树后序遍历
	public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
        List<TreeNode> res = new ArrayList<>();
        List<Integer> toDelete = Arrays.stream(to_delete).boxed().collect(Collectors.toList());
        if (!toDelete.contains(root.val)) {
            res.add(root);
        }
        dfs(root, toDelete, res);
        return res;
    }

    public TreeNode dfs(TreeNode root, List<Integer> toDelete, List<TreeNode> res) {
        if (root == null) return null;
        root.left = dfs(root.left, toDelete, res);
        root.right = dfs(root.right, toDelete, res);
        if (toDelete.contains(root.val)) {
            if (root.left != null)
                res.add(root.left);
            if (root.right != null)
                res.add(root.right);
            root = null;
        }
        return root;
    }

742. 二叉树最近的叶节点

  • 转化为图,然后BFS宽度优先搜索结束
// 宽度优先搜索 将树转换为图
    public int findClosestLeaf(TreeNode root, int k) {
        if (root == null) return 0;

        Map<TreeNode, List<TreeNode>> graph = new HashMap<>();
        dfs(graph, root, null);

        Queue<TreeNode> queue = new LinkedList<>();
        Set<TreeNode> seen = new HashSet<>();

        for (TreeNode node : graph.keySet()) {
            if (node != null && node.val == k) {
                queue.add(node);
                seen.add(node);
            }
        }

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node != null) {
                if (graph.get(node).size() <= 1) { // 叶子节点
                    return node.val;
                }
                for (TreeNode nei : graph.get(node)) {
                    if (!seen.contains(nei)) {
                        seen.add(nei);
                        queue.add(nei);
                    }
                }
            }
        }
        return 0;
    }

    public void dfs(Map<TreeNode, List<TreeNode>> graph, TreeNode node, TreeNode parent) {
        if (node != null) {
            if (!graph.containsKey(node)) graph.put(node, new LinkedList<>());
            if (!graph.containsKey(parent)) graph.put(parent, new LinkedList<>());
            graph.get(node).add(parent);
            graph.get(parent).add(node);
            dfs(graph, node.left, node);
            dfs(graph, node.right, node);
        }
    }

114. 二叉树展开为链表

  • 后序遍历 右左根 递归时保存最右边的根节点
	private TreeNode last = null;
	
	public void flatten(TreeNode root) {
		if(root == null) return ;
		flatten(root.right);
		flatten(root.left);
		root.right = last;
		root.left = null;
		last = root;
	}

776. 拆分二叉搜索树

	public TreeNode[] splitBST(TreeNode root, int value) {
		if(root == null) return new TreeNode[]{null, null};
		if(root.val <= value) {
			// 说明左子树已经全部满足 但右子树可能还存在比value小的节点
			TreeNode[] res = splitBST(root.right, value);
			root.right = res[0];
			res[0] = root;
			return res;
		} else{
			TreeNode[] res = splitBST(root.left, value);
			root.left = res[1];
			res[1] = root;
			return res;	
		}
	}

1123. 最深叶节点的最近公共祖先

最深叶子节点满足根结点到最深的叶子节点的深度相同,如果左边大则在左子树,否则在右子树。

	public TreeNode lcaDeepestLeaves(TreeNode root) {
        if (root == null) return null;
        int leftHeight = getHeight(root.left);
        int rightHeight = getHeight(root.right);
        if (leftHeight == rightHeight)
            return root;

        if (leftHeight > rightHeight)
            return lcaDeepestLeaves(root.left);
        else
            return lcaDeepestLeaves(root.right);
    }

    public int getHeight(TreeNode root) {
        if (root == null) return 0;
        return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
    }

1372. 二叉树中的最长交错路径(简单树形dp)

最容易想到的解法是dfs暴力搜索,自顶向下的递归
树形dp,看了清华大佬的题解,感到愧疚。。。。。

	private int ans = 0;

    public int longestZigZag(TreeNode root) {
        if (root == null) return 0;
        dfs(root.left, true, 1);
        dfs(root.right, false, 1);
        return ans;
    }

    // 自顶向下 记录中间变量 dfs
    public void dfs(TreeNode root, boolean condition, int depth) {
        if (root == null) return;
        ans = Math.max(ans, depth);
        if (condition) {
            dfs(root.right, !condition, depth + 1);
            dfs(root.left, condition, 1);
        } else {
            dfs(root.left, !condition, depth + 1);
            dfs(root.right, condition, 1);
        }
    }
    // 简单树形dp
	// res[0]: 表示当前节点向左走的最大值
    // res[1]: 表示当前节点向右走的最大值
    // res[0] = 1 + left[1] 当前节点下一步向左走带来的最大收益
    public int[] helper(TreeNode root) {
        int[] res = new int[2];
        if (root == null) {
            res[0] = -1;
            res[1] = -1;
            return res;
        }

        int[] left = helper(root.left);
        int[] right = helper(root.right);

        res[0] = left[1] + 1;
        res[1] = right[0] + 1;

        ans = Math.max(ans, Math.max(res[0], res[1]));
        return res;
    }

1245. 树的直径

  • 直接暴力会导致超时TTL
  • 树的直径,两次dfs,第一次从随机一个端点触发到达最深的端点,第二次再从当前端点触发到最深的端点即为直径
	private int res = 0;
    private int index = 0;

    private Map<Integer, List<Integer>> graph;

    public int treeDiameter(int[][] edges) {
        if (edges == null || edges.length == 0) return 0;

        init(edges);

        dfs(0, -1, 0);
        dfs(index, -1, 0);
        return res;
    }

    public void dfs(int node, int pre, int sum) {
        for (int i = 0; i < graph.get(node).size(); i++) {
            int next = graph.get(node).get(i);
            if (next == pre)
                continue;
            dfs(next, node, sum + 1);
        }
        if (sum > res) {
            res = sum;
            index = node;
        }
    }


    public void init(int[][] edges) {
        graph = new HashMap<>();
        int size = edges.length + 1;
        for (int i = 0; i < size; i++) {
            graph.put(i, new ArrayList<>());
        }
        for (int[] edge : edges) {
            graph.get(edge[0]).add(edge[1]);
            graph.get(edge[1]).add(edge[0]);
        }
    }

701. 二叉搜索树的插入操作

	public TreeNode insertIntoBST(TreeNode root, int val) {
		if (root == null)
			return new TreeNode(val);
		if (root.val > val)
			root.left = insertIntoBST(root.left, val);
		else 
			root.right = insertIntoBST(root.right, val);
		return root;
	}

450. 二叉搜索树的删除操作

  • 如果当前节点大于key则在右子树中删除
  • 如果当前节点小于key则在左子树中删除
  • 如果当前节点等于key
    • 其无左子:其右子顶替其位置,删除了该节点;
    • 其无右子:其左子顶替其位置,删除了该节点;
    • 其左右子节点都有:其左子树转移到其右子树的最左节点的左子树上,然后右子树顶替其位置,由此删除了该节点。
	public TreeNode deleteNode(TreeNode root, int val) {
		if (root == null) return null;
        if (key > root.val) root.right = deleteNode(root.right, key);
        else if (key < root.val) root.left = deleteNode(root.left, key);
        else {
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            TreeNode node = root.right;
            while (node.left != null)
                node = node.left;
            node.left = root.left;
            root = root.right;
        }
        return root;
	}

95. 不同的二叉搜索树 I

输出总共有多少情况即可

G(n):代表前n有多少情况
F(i, n):代表以i为根的总数为n的有多少种情况
F(i, n) = G(i - 1) * G(n - i)

∑ 1 n F ( i , n ) = ∑ 1 n G ( i − 1 ) ∗ G ( n − i ) \sum_{1}^{n}F(i, n)=\sum_{1}^{n}G(i - 1) * G(n - i) 1nF(i,n)=1nG(i1)G(ni)

即从小到大遍历G(i)即可

	public int numTree() {
		int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
	}

或者通过卡塔兰数为 C n + 1 = 2 ∗ 2 n + 1 n + 2 ∗ C n C_{n + 1}=2 * \frac{2n + 1}{n + 2} * C_n Cn+1=2n+22n+1Cn其中 ( C 0 = 1 ) (C_0 = 1) (C0=1)直接求得

96. 不同的二叉搜索树 II

	public List<TreeNode> generateTrees(int n) {
        if (n == 0)
            return new LinkedList<>();
        return generateTrees(1, n);
    }

    public List<TreeNode> generateTrees(int start, int end) {
        List<TreeNode> allTrees = new LinkedList<>();
        if (start > end) {
            allTrees.add(null);
            return allTrees;
        }

        for (int i = start; i <= end; i++) {
            List<TreeNode> leftTrees = generateTrees(start, i - 1);
            List<TreeNode> rightTrees = generateTrees(i + 1, end);

            for (TreeNode left : leftTrees) {
                for (TreeNode right : rightTrees) {
                    TreeNode currTree = new TreeNode(i);
                    currTree.left = left;
                    currTree.right = right;
                    allTrees.add(currTree);
                }
            }
        }
        return allTrees;
    }

114. 二叉树的前序遍历(非递归方法)

如果当前节点的左子树不为null则继续迭代

public List<Integer> preorderTraversal(TreeNode root) {	
	List<Integer> res = new ArrayList<>();
	if (root == null) return res;
	Stack<Integer> s = new Stack<>();
	TreeNode node = root;
	while (!s.isEmpty() || node != null) {
		while (node != null) {
			res.add(node.val);
			stack.push(node);
			node = node.left;
		}
		node = stack.pop();
		node = node.right;
	}
	return res;
}

二叉树后序遍历(非递归方法)

使用prev来记录之前访问的节点,让当前节点的右节点如果等于prev时 则说明右子节点已经访问过了

public List<Integer> postorderTraversal(TreeNode root) {
	List<Integer> res = new ArrayList<>();
	if (root == null) return res;
	
	Stack<TreeNode> s = new Stack<>();
	
	TreeNode prev = null;
	while (root != null || s.isEmpty()) {
		while (root != null) {
			stack.push(root);
			root = root.left;
		}
		root = s.pop();
		if (root.right == null || root.right == prev) {
			res.add(root.val);
			prev = root;
			root = null;
		} else {
			s.push(root);
			root = root.right;
		}
	}
	return res;
}

116. 二叉树的中序遍历

public List<Integer> inorderTraversal(TreeNode root) {
	List<Integer> res = new ArrayList<>();
	if (root == null)
		return res;
	
	Stack<TreeNode> s = new Stack<>();
	while ( root != null || !s.isEmpty()) {
		while (root != null) {
			stack.push(root);
			root = root.left;
		}
		root = stack.pop();
		res.add(root.val);
		root = root.right;
	}
	return res;
}

backtrack

写backtrack函数时,需要维护走过的路径和当前可以做出的选择列表,当触发结束条件时,将路径记录进入结果集

320. 列举单词的全部缩写

  • 解法一,直接对数字进行计数(注意toStr函数中for循环的写法)
	List<String> res = new ArrayList<>();

    public List<String> generateAbbreviations(String word) {
        backtrack(word.toCharArray(), 0);
        return res;
    }
	
	public void backtrack(char[] chars, int start) {
		if (start >= chars.length())
			return ;
		for ( int i = start; i < chs.length; i++){
			char ch = chars[i];
			chars[i] = '@';
			res.add(toStr(chars));
			backtrack(chars, i + 1);
			chars[i] = ch;
		}
	}
	
	public String toStr(char[] chars) {
		StringBuilder sb =  new StringBuilder();
		for(int i = 0; i < chars.length(); ){
			int j = i;
			if(chars[j] == '@') {
				while(j < chars.length && chars[j] == '@'){
					j ++;
				}
				sb.append(j - i);
				i = j;
			} else {
				sb.append(chars[j]);
				i ++;
			}
		}
	}
  • 解法二,对于每一位数字都有两种选择,一是进行缩写,二是进行保留
	public void dfs(List<String> res, StringBuilder sb, String word, int i, int k) {
		int len = sb.length();
		
		if(i == word.length()) {
			if(k != 0) sb.append(k);
			res.add(sb.toString());
		}else {
			// 选择缩写
			dfs(res, sb, word, i + 1, k + 1);
			if(k != 0) sb.append(k);
			sb.append(word.charAt(i));
			// 选择保留当前位
			dfs(res, sb, word, i + 1, 0);
		}
		sb.setLength(len);
	}
  • 解法三 位运算,转化为 0 ~ 2 n 0 ~ 2^n 02n的位运算,毕竟使用 >> 操作来统计1的个数
	public List<String> generateAbbreviations_helper(String word) {
        List<String> ans = new ArrayList<>();
        for (int i = 0; i < (1 << word.length()); ++i) {
            ans.add(abbr(word, i));
        }
        return ans;
    }

    public String abbr(String word, int x) {
        StringBuilder sb = new StringBuilder();
        int k = 0, n = word.length();
        for (int i = 0; i < n; i++, x >>= 1) {
            if ((x & 1) == 0) {
                if (k != 0) {
                    sb.append(k);
                    k = 0;
                }
                sb.append(word.charAt(i));
            } else {
                ++k;
            }
        }
        if (k != 0) sb.append(k);
        return sb.toString();
    }

单调栈

用于找出左/右边最小/大的第一个元素的位置

	// 找出右边的第一个比其小的元素
	public int[] findRightSmall (int[] A) {
		if (A == null || A.length == 0)
			return 0;
		Stack<Integer> t = new Stack<>();
		int[] ans = new int[A.length];
		for (int i = 0; i < A.length; i++) {
			while (!t.isEmpty() && A[t.peek()] > A[i]) {
				ans[t.peek()] = i;
				t.pop();
			}
			t.push(i);
		}
		while (!t.isEmpty()) {
			ans[t.peek()] = -1;
			t.pop();
		}
		return ans;
	}
	
	// 找到左边第一个小的元素 两种写法
	public int[] findLeftSmall (int[] A) {
		if (A == null || A.length == 0)
			return new int[]{};
		Stack<Integer> t = new Stack<>();
		int[] ans = new int[A.length];
		for (int i = 0; i < A.length; i++) {
			while (!t.isEmpty() && A[i] < A[t.peek()]) {
				t.pop();
			}
			ans[i] = !t.isEmpty() ? t.peek() : -1;
			t.push(i);
		}
		return ans;
	}
	
	public int[] findLeftSmall_helper (int[] A) {
		if (A == null || A.length == 0)
			return new int[]{};
		
		Stack<Integer> t = new Stack<>();
		int[] ans = new int[A.length];
		for (int i = A.length - 1; i >= 0; i--) {
			while (!t.isEmpty() && A[t.peek()] > A[i]) {
				ans[t.peek()] = i;
				t.pop();
			}
			t.push(i);
		}
		while (!t.isEmpty()) {
			ans[t.peek()] = -1;
			t.pop();
		}
		return ans;
	}

84. 柱状图中最大的矩形

其实可以一遍遍历就可以求出当前的形状,左边的第一个比其小的下标和右边的第一个比其小的下标。

	public int largestRectangleArea (int[] A) {
		if (A == null || A.length == 0) {
			return 0;
		}
		
		int[] left = new int[A.length];
		int[] right = new int[A.length];
		Stack<Integer> t = new Stack<>();
		Arrays.fill(right, n);
		
		for (int i = 0; i < A.length; i++) {
			while (!t.isEmpty() && A[i] < A[t.peek()]) {
				right[t.peek()] = i;
				t.pop();
			}
			left[i] = !t.isEmpty() ? -1 : t.peek();
			t.push(i);
		}
		int ans = 0;
		// 计算面积
		for (int i = 0; i < A.length; i++) {
			ans = Math.max(ans, (right[i] - left[i] - 1) * A[i]);
		}
		return ans;
	}

Sliding Windows

母题1

求一个数组的连续子数组总个数
总的连续子数组个数等于以索引为0结尾的子数组个数+索引为1结尾的子数组个数+…+索引为n-1结尾的子数组个数

母题2

求出不大于k的子数组的个数,不大于k指的是子数组的全部元素都不大于k

public int notGreater(int[] nums, int k) {
	int ans = 0;
	int pre = 0;
	for (int num : nums) {
		if (num <= k) 
			pre += 1;
		else 
			pre = 0;
		ans += pre;
	}
	return ans;
}

母题3

求出不同整数个数小于k个的子数组的个数

public int subArrayWithMostK(int[] nums, int k){
	Map<Integer, Integer> hashMap = new HashMap<>();
	int count = 0, res = 0, l = 0, r = 0;

	while (r < nums.length) {
		hashMap.put(nums[r], hashMap.getOrDefault(nums[r], 0) + 1);
		if (hashMap.get(nums[r]) == 1) {
			count++;
		}
		while (count > k) {
			hashMap.put(nums[l], hashMap.get(nums[l]) - 1);
			if (hashMap.get(nums[l]) == 0) 
				count--;
			l++;
		}
		res += r - l + 1;
		r++;
	}
	return res;
}

未排序数组中累加和为定值的最长子数组

前缀和 + 哈希表

	public int maxLength(int[] arr, int k) {
        if (arr == null || arr.length == 0)
            return 0;

        Map<Integer, Integer> map = new HashMap<>();
        int len = 0;
        int sum = 0;
        // 初始化很重要
        map.putIfAbsent(0, -1);

        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
            if (map.containsKey(sum - k)) {
                len = Math.max(len, i - map.get(sum - k));
            }
            // 保存第一次出现sum的位置
            if (!map.containsKey(sum)) {
                map.put(sum, i);
            }
        }
        return len;
    }

1371. 每个元音包含偶数次的最长子字符串

  • 遇到求最长的连续子串使得和为k (前缀和 + 哈希表)
  • 奇偶个数校验,XOR
  • 遇到有限的参数表的状态,状态压缩
class Solution {
    private static final String VOWELS = "aeiou";

    public int findTheLongestSubstring(String s) {
        Map<Integer, Integer> map = new HashMap<>();
        int size = s.length();
        int state = 0; // 00000
        int maxSize = 0;
        map.putIfAbsent(0, -1); // 没有数则前缀为0
        for (int i = 0; i < size; i++) {
            for (int k = 0; k < VOWELS.length(); k++) {
                if (s.charAt(i) == VOWELS.charAt(k)) {
                    state ^= (1 << VOWELS.length() - k - 1);
                    break;
                }
            }
            // 如果子串 [0, i] 和 [0, j]状态相同,那么字符串[i + 1, j]的状态一定是0
            if (map.containsKey(state)) {
                maxSize = Math.max(maxSize, i - map.get(state));
            }
            map.putIfAbsent(state, i);
        }
        return maxSize;
    }
}

1109. 航班预订统计

前缀和 [i, j] 进行计算的时候 [i - 1] + num 并且 [ j + 1] - num 相当于给区间[i, j] 都增加num数值

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] counter = new int[n];
        for (int[] booking : bookings) {
            counter[booking[0] - 1] += booking[2];
            if (booking[1] < n) {
                counter[booking[1]] -= booking[2];
            }
        }
        for (int i = 1; i < n; i++) {
            counter[i] += counter[i - 1];
        }
        return counter;
    }
}

467. 环绕字符串中唯一的子字符串

求以每一位结尾的连续子字符串的个数,并且后面的可以覆盖前面的字符

class Solution {
    public int findSubstringInWraproundString(String p) {
        int n = p.length();
        if (n < 1) return 0;

        int ret = 0;
        int[] count = new int[26];

        char[] str = p.toCharArray();
        int curMaxLen = 1;

        for (int i = 0; i < n; i++) {
            if (i > 0 && (str[i] - str[i - 1] == 1 || str[i - 1] - str[i] == 25)) {
                curMaxLen++;
            } else {
                curMaxLen = 1;
            }
            count[str[i] - 'a'] = Math.max(count[str[i] - 'a'], curMaxLen);
        }
        for (int temp : count) {
            ret += temp;
        }
        return ret;
    }
}

795. 区间子数组个数

区间子数组个数,母题2的思想

public int numSubarrayBoundedMax(int[] A, int L, int R) {
        if (A == null || A.length == 0) return 0;

        return notGreater(A, R) - notGreater(A, L - 1);
    }

    public int notGreater(int[] A, int value) {
        int res = 0, cnt = 0;
        for (int item : A) {
            if (item <= value)
                cnt += 1;
            else
                cnt = 0;
            res += cnt;
        }
        return res;
    }

992. K 个不同整数的子数组

给定一个正整数数组A,不同整数的个数恰好为K

public int subarraysWithKDistinct(int[] A, int K) {
   if (A == null || A.length == 0)
        return 0;
    return subArrayWithMostK(A, K) - subArrayWithMostK(A, K - 1);
}

public int subArrayWithMostK(int[] nums, int k){
	Map<Integer, Integer> hashMap = new HashMap<>();
	int count = 0, res = 0, l = 0, r = 0;

	while (r < nums.length) {
		hashMap.put(nums[r], hashMap.getOrDefault(nums[r], 0) + 1);
		if (hashMap.get(nums[r]) == 1) {
			count++;
		}
		while (count > k) {
			hashMap.put(nums[l], hashMap.get(nums[l]) - 1);
			if (hashMap.get(nums[l]) == 0) 
				count--;
			l++;
		}
		res += r - l + 1;
		r++;
	}
	return res;
}

80. 删除有序数组中的重复项

删除K个重复元素的通解

public int removeDuplicate (int[] nums) {
	return helper(nums, 2);
}


public int helper(int[] nums, int k) {
	int i = 0;

	for (int n : nums) {
		if (i < k || n > nums[i - k]) {
			nums[i++] = n;
		}
	}
	return i;
}

剑指Offer

51. 数组中的逆序对

	private int res = 0;

    public void merge_sort(int[] nums, int l, int r) {
        if (l >= r) return;

        int mid = (r - l) / 2 + l;
        merge_sort(nums, l, mid);
        merge_sort(nums, mid + 1, r);

        int k = 0;
        int left = l, right = mid + 1;
        int[] temp = new int[r - l + 1];
        while (left <= mid && right <= r) {
            if (nums[left] <= nums[right]) {
                temp[k++] = nums[left++];
            } else {
                temp[k++] = nums[right++];
                // 主要是添加了这一步
                res += mid - left + 1;
            }
        }
        while (left <= mid) {
            temp[k++] = nums[left++];
        }
        while (right <= r) {
            temp[k++] = nums[right++];
        }

        for (int i = l, j = 0; i <= r; i++) {
            nums[i] = temp[j++];
        }
    }

    public int reversePairs(int[] nums) {
        merge_sort(nums, 0, nums.length - 1);
        return res;
    }

59-I 滑动窗口中的最大值

单调队列的应用

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length == 0 || k == 0)
            return new int[0];

        int n = nums.length;
        Deque<Integer> deque = new LinkedList<>();
        int[] res = new int[n - k + 1];

        int l = 0, r = 0, i = 0;

        while (r < n) {
            if (!deque.isEmpty() && r - l == k) {
                if (deque.peekFirst() == nums[l])
                    deque.pollFirst();
                l++;
            }
            while (!deque.isEmpty() && deque.peekLast() < nums[r]) {
                deque.pollLast();
            }
            deque.offerLast(nums[r]);
            if (!deque.isEmpty() && r >= k-1) {
                res[i++] = deque.peekFirst();
            }
            r++;
        }
        return res;
    }
}

布隆过滤器

它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

布隆过滤器博客

基本计算器

总结是两个栈的运用

只有加减和括号的运算

	public int calculate1(String s) {
		Stack<Integer> stack = new Stack<>();
		int sign = 1, res = 0;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WeiXiao_Hyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值