leetcode刷题笔记java版

leetcode刷题笔记java版,持续更新中....20220327

leetcode热题 HOT 100

题目分类

分类题号
深度搜索207、297、437
广度搜索207、399
拓扑排序210
二叉树226、236、538
动态规划5、221、279、300、309、322、337、338、416、 647
前缀和238、437
滑动窗口239、438、3
最大最小堆239、347
搜索240
二分查找240
双指针283、42
递归23、394
Manacher 算法5、647
399
并查集399
排序406
背包问题279、322、416
单调栈42

3. 无重复字符的最长子串

【方法1滑动窗口】
class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        int start = 0;
        int max = 1;
        Set<Character> set = new HashSet<>();
        set.add(s.charAt(0));
        for (int i = 1; i < s.length(); i++) {
            while (start <= i && set.contains(s.charAt(i))) {
                set.remove(s.charAt(start));
                start++;
            }
            set.add(s.charAt(i));
            max = Math.max(max, i - start + 1);
        }
        return max;
    }
}

5. 最长回文子串

【方法1】中心扩展法
class Solution {
    public String longestPalindrome(String s) {
        int max = 0;
        int start = 0;
        int end = start;
        for (int i = 0; i <= 2 * (s.length() - 1); i++) {
            int left = i / 2;
            int right = (i % 2 == 0) ? left : left + 1;
            while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
                if (right - left + 1 > max) {
                    start = left;
                    end = right;
                    max = right - left + 1;
                }
                left--;
                right++;
            }
        }
        return s.substring(start, end + 1);
    }
}
【方法2】manacher算法
class Solution {
    public String longestPalindrome(String s) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            stringBuilder.append("#").append(s.charAt(i));
        }
        s = stringBuilder.append("#").toString();

        int start = 0;
        int end = 0;
        int[] f = new int[s.length()];
        // 标记历史最右端力臂所到位置
        int right = -1;
        // 对称中心 j
        int j = -1;
        for (int i = 0; i < s.length(); i++) {
            // 当前i的力臂长度
            int cur_len;
            if (right >= i) {
                // i关于j的对称点k
                int k = j - (i - j);
                // 根据历史f[k],得到最小长度
                int min_len = Math.min(f[k], right - i);
                // 从最小长度往外继续扩展
                cur_len = expand(s, i - min_len, i + min_len);
            } else {
                cur_len = expand(s, i, i);
            }
            // 记录当前i的力臂信息
            f[i] = cur_len;
            if (i + cur_len > right) {
                j = i;
                right = i + cur_len;
            }
            if (cur_len * 2 + 1 > end - start) {
                start = i - cur_len;
                end = i + cur_len;
            }
        }
        StringBuilder ans = new StringBuilder();
        for (int i = start; i <= end; i++) {
            if (s.charAt(i) != '#'){
                ans.append(s.charAt(i));
            }
        }
        return ans.toString();
    }

    private int expand(String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }
        return (right - left - 2) / 2;
    }
}

23. 合并K个升序链表

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        Queue<ListNode> queue = new LinkedList<>(Arrays.asList(lists));
        while (!queue.isEmpty() && queue.size() > 1) {
            int size = queue.size();
            for (int i = 0; i < size; i += 2) {
                queue.offer(mergeList(queue.poll(), queue.poll()));
            }
        }
        System.out.println(queue.size());
        return queue.poll();
    }

    public ListNode mergeList(ListNode pHead, ListNode qHead) {
        ListNode head = new ListNode();
        ListNode node = head;
        while (pHead != null && qHead != null) {
            if (pHead.val > qHead.val) {
                node.next = qHead;
                qHead = qHead.next;
            } else {
                node.next = pHead;
                pHead = pHead.next;
            }
            node = node.next;
        }
        if (pHead != null) {
            node.next = pHead;
        } else {
            node.next = qHead;
        }
        return head.next;
    }
}

42. 接雨水

【方法1 双指针】
class Solution {
    public int trap(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int leftMax = 0;
        int rightMax = 0;
        int sum = 0;
        while (left <= right) {
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);
            if (height[left] > height[right]) {
                sum += rightMax - height[right];
                right--;
            } else {
                sum += leftMax - height[left];
                left++;
            }
        }
        return sum;
    }
}
【方法2 单调栈】
class Solution {
    public int trap(int[] height) {
        Deque<Integer> stack = new LinkedList<>();
        int sum = 0;
        for (int i = 0; i < height.length; i++) {
            while (!stack.isEmpty() && height[stack.peek()] <= height[i]) {
                int min = stack.pop();
                if (stack.isEmpty()) {
                    break;
                }
                int curHeight = Math.min(height[stack.peek()], height[i]) - height[min];
                int curWidtd = i - stack.peek() - 1;
				sum += curHeight * curWidtd;
            }
            stack.push(i);
        }
        return sum;
    }
}

207. 课程表

【方法1】拓扑排序、深度搜索
class Solution {

    private List<List<Integer>> edges;
    // 0-未搜索,1-搜索中,2-已完成
    private int[] visited;
    // 图中是否存在环,即此题是否有解
    private boolean valid = true;

    private List<List<Integer>> buildGraph(int nodes, int[][] relationship) {
        List<List<Integer>> edges = new ArrayList<>();
        for (int i = 0; i < nodes; i++) {
            edges.add(new ArrayList<>());
        }
        for (int[] a : relationship) {
            edges.get(a[1]).add(a[0]);
        }
        return edges;
    }

    private void dfs(int u) {
        // 搜索u时现将u标记为搜索中
        visited[u] = 1;
        // 遍历所有与u相连的节点
        for (int v : edges.get(u)) {
            if (visited[v] == 0) {
                // v节点未搜索时,则深度搜索v
                dfs(v);
                // 如果无效则直接返回
                if (!valid) {
                    return;
                }
            } else if (visited[v] == 1) {
                // v节点搜索中时,图中存在环,无效
                valid = false;
                return;
            }
        }
        // 对节点u完成搜索,标记为已完成
        visited[u] = 2;
    }

    public boolean canFinish(int numCourses, int[][] prerequisites) {

        edges = buildGraph(numCourses, prerequisites);
        visited = new int[numCourses];
        // 遍历节点,如果无效直接退出
        for (int i = 0; i < numCourses & valid; i++) {
            if (visited[i] == 0) {
                // 节点未搜索,则深度搜索此节点
                dfs(i);
            }
        }
        return valid;
    }
}
【方法2】拓扑排序、广度搜索
class Solution {

    private List<List<Integer>> edges;

    private int[] inDeg;

    private List<List<Integer>> buildGraph(int nodes, int[][] relationship) {
        List<List<Integer>> edges = new ArrayList<>();
        for (int i = 0; i < nodes; i++) {
            edges.add(new ArrayList<>());
        }
        for (int[] a : relationship) {
            edges.get(a[1]).add(a[0]);
            ++inDeg[a[0]];
        }
        return edges;
    }

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        inDeg = new int[numCourses];
        edges = buildGraph(numCourses, prerequisites);

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDeg[i] == 0) {
                queue.offer(i);
            }
        }
        List<Integer> stack = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            stack.add(u);
            for (int v : edges.get(u)) {
                --inDeg[v];
                if (inDeg[v] == 0) {
                    queue.offer(v);
                }
            }
        }
        return stack.size() == numCourses;
    }
}

221. 最大正方形

【方法1】动态规划

状态转移方程:
定义dp[i][j]为以(i, j) 为有下角的正方形的最大边长,有:
d p [ i ] [ j ] = { m a t r i x [ i ] [ j ] i = 0 或 j = 0 0 m a t r i x [ i ] [ j ] = 0 min ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j − 1 ] ) + 1 其 他 dp[i][j] = \begin{cases} matrix[i][j] & i = 0 或 j = 0 \\ 0 & matrix[i][j] = 0 \\ \min(dp[i-1][j], dp[i][j - 1], dp[i-1][j-1]) +1 & 其他 \end{cases} dp[i][j]=matrix[i][j]0min(dp[i1][j],dp[i][j1],dp[i1][j1])+1i=0j=0matrix[i][j]=0

class Solution {
    public int maximalSquare(char[][] matrix) {
        int[][] side = new int[matrix.length][matrix[0].length];
        int maxSide = 0;
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (i == 0 || j == 0) {
                    side[i][j] = matrix[i][j] - '0';
                } else {
                    if (matrix[i][j] == '0') {
                        side[i][j] = 0;
                    } else {
                        side[i][j] = Math.min(Math.min(side[i - 1][j], side[i][j - 1]), side[i - 1][j - 1]) + 1;
                    }
                }
                maxSide = Math.max(maxSide, side[i][j]);
            }
        }
        return maxSide * maxSide;
    }
}

226. 翻转二叉树

【方法1】使用队列按层遍历二叉树
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.peek();
            TreeNode tmp = node.left;
            node.left = node.right;
            node.right = tmp;
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
            queue.poll();
        }
        return root;
    }
}
【方法2】递归遍历二叉树
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        TreeNode left = invertTree(root.left);
        TreeNode right = invertTree(root.right);
        root.left = right;
        root.right = left;
        return root;
    }
}

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

【方法1】递归

递归条件:定义 f x f_x fx表示 x x x节点的子树中是否包含 p 节点或 q节点,则公共祖先一定满足
( f l s o n & & f r s o n ) ∣ ∣ ( ( x = = p ∣ ∣ x = = q ) & & ( f l s o n ∣ ∣ f r s o n ) ) (f_{lson} \&\& f_{rson})||((x==p||x==q)\&\&(f_{lson}||f_{rson})) (flson&&frson)((x==px==q)&&(flsonfrson))

  • 公共祖先左子树和右子树同时包含p节点或者q节点,则一个子树包含一个节点,另外一个节点必在另一子树;
  • 节点本身是p或q的一个节点,且其子树包含另一个节点
  • 只要从叶子节点开始搜索,则可以保证深度最大的公共祖先
class Solution {

	private TreeNode ans;
	
	public Solution() {
	    this.ans = null;
	}
	
	private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
	    if (root == null) return false;
	    boolean lson = dfs(root.left, p, q);
	    boolean rson = dfs(root.right, p, q);
	    if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {
	        ans = root;
	    }
	    return lson || rson || (root.val == p.val || root.val == q.val);
	}
	
	public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
	    this.dfs(root, p, q);
	    return this.ans;
	}
}
【方法二】存储前序遍历节点的路径
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        LinkedList<TreeNode> pPath = new LinkedList<>();
        LinkedList<TreeNode> qPath = new LinkedList<>();
        findPath(root, p, pPath);
        findPath(root, p, qPath);
        int len = Math.min(pPath.size(), qPath.size());
        int i = 0;
        for (; i < len; i++) {
            if (pPath.get(i) != qPath.get(i)) {
                break;
            }
        }
        return pPath.get(i - 1);
    }

    private boolean findPath(TreeNode root, TreeNode p, LinkedList<TreeNode> path) {
        if (root == null) {
            return false;
        }
        path.add(root);
        if (root == p) {
            return true;
        }
        if (findPath(root.left, p , path) || findPath(root.right, p, path)) {
            return true;
        }
        path.removeLast();
        return false;
    }
}

238. 除自身以外数组的乘积

【方法1】利用前缀乘积和后缀乘积数组
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int[] prefix = new int[nums.length];
        int[] suffix = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            if (i == 0) {
                prefix[0] = 1;
                suffix[nums.length - 1] = 1;
            } else {
                prefix[i] = prefix[i - 1] * nums[i - 1];
                suffix[nums.length - 1 - i] = suffix[nums.length - i] * nums[nums.length - i];
            }
        }
        int[] answer = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            answer[i] = prefix[i] * suffix[i];
        }
        return answer;
    }
}
【方法2】优化,去掉数组prefix和suffix
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int length = nums.length;
        int[] answer = new int[length];

        // answer[i] 表示索引 i 左侧所有元素的乘积
        // 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
        answer[0] = 1;
        for (int i = 1; i < length; i++) {
            answer[i] = nums[i - 1] * answer[i - 1];
        }

        // R 为右侧所有元素的乘积
        // 刚开始右边没有元素,所以 R = 1
        int R = 1;
        for (int i = length - 1; i >= 0; i--) {
            // 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
            answer[i] = answer[i] * R;
            // R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
            R *= nums[i];
        }
        return answer;
    }
}

239. 滑动窗口最大值

【方法1】优先队列实现最大堆
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (k == 1) {
            return nums;
        }
        List<Integer> result = new LinkedList<>();
        Queue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[0] == o2[0] ? o2[1] - o1[1] : o2[0] - o1[0]);
        for (int i = 0; i < k; i++) {
            queue.add(new int[]{nums[i], i});
        }
        result.add(queue.peek()[0]);
        for (int i = k; i < nums.length; i++) {
            while (queue.peek()[1] <= i - k) {
                queue.poll();
            }
            queue.offer(new int[] {nums[i], i});
            result.add(queue.peek()[0]);
        }
        return result.stream().mapToInt(Integer::intValue).toArray();
    }
}

240. 搜索二维矩阵II

【方法1】暴力解法,遍历搜索O(mn)不推荐
【方法2】按行二分查找O(mlogn)
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        for (int i = 0; i < matrix.length; i++) {
            int start = 0;
            int end = matrix[0].length;
            while (start < end) {
                int middle = (end - start) / 2 + start;
                if (matrix[i][middle] > target) {
                    end = middle;
                } else if (matrix[i][middle] == target) {
                    return true;
                } else {
                    start = middle + 1;
                }
            }
        }
        return false;
    }
}
【方法3】Z字型查找
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int x = 0, y = n - 1;
        while (x < m && y >= 0) {
            if (matrix[x][y] == target) {
                return true;
            }
            if (matrix[x][y] > target) {
                --y;
            } else {
                ++x;
            }
        }
        return false;
    }
}

279. 完全平方数

【方法1】动态规划

定义dp(n)为完全平方数的和为n的个数,则
n = i 2 + n − i 2 , 其 中 i = [ 1 , n ] , 且 为 整 数 n = i^2 + n - i^2, 其中i = [1,\sqrt{n}], 且为整数 n=i2+ni2,i=[1,n ],


KaTeX parse error: Got function '\sqrt' with no arguments as superscript at position 14: dp(n) = min^\̲s̲q̲r̲t̲{n}_idp(n-i*i) …

class Solution {
    public int numSquares(int n) {
        int[] f = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            int min = Integer.MAX_VALUE;
            for (int j = 1; j * j <= i; j++) {
                min = Math.min(min, f[i - j * j]);
            }
            f[i] = min + 1;
        }
        return f[n];
    }
}

283.移动零

【方法1】双指针
class Solution {
    public void moveZeroes(int[] nums) {
        if (nums == null || nums.length == 0) {
            return;
        }
        int end = nums.length;
        for (int i = nums.length - 1; i >= 0; i--) {
            if (nums[i] == 0) {
                end--;
                int j = i;
                while (j < end) {
                    nums[j] = nums[j + 1];
                    j++;
                }
                nums[end] = 0;
            }
        }
    }
}

297. 二叉树的序列化与反序列化

【方法1】深度搜索、二叉树中序遍历
public class Codec {
    public String serialize(TreeNode root) {
        return rserialize(root, "");
    }
  
    public TreeNode deserialize(String data) {
        String[] dataArray = data.split(",");
        List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
        return rdeserialize(dataList);
    }

    public String rserialize(TreeNode root, String str) {
        if (root == null) {
            str += "None,";
        } else {
            str += str.valueOf(root.val) + ",";
            str = rserialize(root.left, str);
            str = rserialize(root.right, str);
        }
        return str;
    }
  
    public TreeNode rdeserialize(List<String> dataList) {
        if (dataList.get(0).equals("None")) {
            dataList.remove(0);
            return null;
        }
  
        TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
        dataList.remove(0);
        root.left = rdeserialize(dataList);
        root.right = rdeserialize(dataList);
    
        return root;
    }
}

300. 最长递增子序列

【方法1】动态规划

定义dp(i)为下标为i结尾的子数组的最长递增数组长度,则
d p ( i ) = m a x ( d p ( j ) ) + 1 , 其 中 0 = < j < i 且 n u m s [ j ] < n u m s [ i ] dp(i) = max(dp(j)) + 1, 其中 0=<j<i 且 nums[j] < nums[i] dp(i)=max(dp(j))+1,0=<j<inums[j]<nums[i]

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int max = 1;
        for (int i = 0; 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);
                }
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

309.买卖股票的最佳时机含冷冻期

【方法1】动态规划

买入=负收益,卖出=正收益,开始时必须先买入。

我们用 dp[i] 表示第 i天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:

  • ①我们目前持有一支股票,对应的「累计最大收益」记为 d[i][0];

  • ②我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 d[i][1];

  • ③我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 d[i][2];

d p [ i ] = { d p [ i ] [ 0 ] 持 有 股 票 d p [ i ] [ 1 ] 不 持 有 股 票 , 处 于 冷 冻 期 d p [ i ] [ 2 ] 不 持 有 股 票 , 不 处 于 冷 冻 期 dp[i] = \begin{cases} dp[i][0] & 持有股票 \\ dp[i][1] & 不持有股票,处于冷冻期 \\ dp[i][2] & 不持有股票,不处于冷冻期 \\ \end{cases} dp[i]=dp[i][0]dp[i][1]dp[i][2]

  • 第①种,第i天持有股票有两种情况,1)第i天之前买入的,此时dp[i][0] = dp[i-1][0]。2)第i天买入的,即dp[i-1][2] - prices[i]。则dp[i][0]应为二者较大值
  • 第②种,第i天不持有股票且处于冷冻期,即第i-1天持有股票,第i天卖出。dp[i][1] = dp[i-1][0] + prices[i]。
  • 第③种,第i天不持有股票且不处于冷冻期,即第i-1天处于冷冻期或不处于冷冻期的最大值。

因此,状态转移方程:
{ d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 2 ] − p r i c e s [ i ] ) d p [ i ] [ 1 ] = d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 2 ] ) \begin{cases} dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])\\ dp[i][1] = dp[i-1][0] + prices[i]\\ dp[i][2] = max(dp[i-1][1], dp[i-1][2]) \end{cases} dp[i][0]=max(dp[i1][0],dp[i1][2]prices[i])dp[i][1]=dp[i1][0]+prices[i]dp[i][2]=max(dp[i1][1],dp[i1][2])

则最大利润为:
p r o f i t = m a x ( d p [ i ] [ 0 ] , d p [ i ] [ 1 ] , d p [ i ] [ 2 ] ) profit = max(dp[i][0], dp[i][1], dp[i][2]) profit=max(dp[i][0],dp[i][1],dp[i][2])
因为持有股票必须要先买入股票,所以边界条件为:
{ d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] d p [ 0 ] [ 1 ] = 0 d p [ 0 ] [ 2 ] = 0 \begin{cases} dp[0][0] = -prices[0]\\ dp[0][1] = 0\\ dp[0][2] = 0\\ \end{cases} dp[0][0]=prices[0]dp[0][1]=0dp[0][2]=0

class Solution {
    public int maxProfit(int[] prices) {
        // 分别对应dp[i-1][0]、dp[i-1][1]、dp[i-1][2]
        int last0 = -prices[0];
        int last1 = 0;
        int last2 = 0;
        int maxProfit = 0;
        for (int i = 0; i < prices.length; i++) {
            int current0 = Math.max(last0, last2 - prices[i]);
            int current1 = last0 + prices[i];
            int current2 = Math.max(last1, last2);
            maxProfit = Math.max(Math.max(current0, current1), current2);
            last0 = current0;
            last1 = current1;
            last2 = current2;
        }
        return maxProfit;
    }
}

322.零钱兑换

【方法1】动态规划

定义dp[i]为总金额为i的最小零钱数量,则有:
d p [ i ] = ∑ j = 0 n m i n ( d p [ i − c o i n s [ j ] ] ) + 1 dp[i] = \sum_{j=0}^n min(dp[i - coins[j]]) + 1 dp[i]=j=0nmin(dp[icoins[j]])+1

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (i - coins[j] >= 0) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

337. 打家劫舍III

【方法一】动态规划

对于一个根节点root,可以分为选择此节点或者不选择则:
{ d p [ r o o t ] [ 0 ] = m a x ( d p [ l e f t ] [ 0 ] , d p [ l e f t ] [ 1 ] ) + m a x ( d p [ r i g h t ] [ 0 ] , d p [ r i g h t ] [ 1 ] ) d p [ r o o t ] [ 1 ] = d p [ l e f t ] [ 0 ] + d p [ r i g h t ] [ 0 ] + r o o t . v a l \begin{cases} dp[root][0] = max(dp[left][0], dp[left][1]) + max(dp[right][0], dp[right][1])\\ dp[root][1] = dp[left][0] + dp[right][0] + root.val \end{cases} {dp[root][0]=max(dp[left][0],dp[left][1])+max(dp[right][0],dp[right][1])dp[root][1]=dp[left][0]+dp[right][0]+root.val

class Solution {
    public int rob(TreeNode root) {
        int[] sum = search(root);
        return Math.max(sum[0], sum[1]);
    }

    private int[] search(TreeNode root) {
        if (root == null) {
            return new int[] {0, 0};
        }
        int[] left = search(root.left);
        int[] right = search(root.right);
        int sum0 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        int sum1 = left[0] + right[0] + root.val;
        return new int[] {sum0, sum1};
    }
}

338. 比特位计数

【方法1】动态规划

b i t [ i ] = { 1 , i 是 2 k b i t [ i / 2 ] , i 是 偶 数 b i t [ i − 1 ] + 1 , i 是 奇 数 bit[i] = \begin{cases} 1, & i 是2^k\\ bit[i/2], &i是偶数\\ bit[i-1] + 1, &i是奇数 \end{cases} bit[i]=1,bit[i/2],bit[i1]+1,i2kii

class Solution {
    public int[] countBits(int n) {
        int[] bits = new int[n + 1];
        int k = 1;
        bits[0] = 0;
        for (int i = 1; i <= n; i++) {
            if (i == 2 * k) {
                bits[i] = 1;
                k = 2 * k;
            } else {
                if (i % 2 == 0) {
                    bits[i] = bits[i/2];
                } else {
                    bits[i] = bits[i - 1] + 1;
                }
            }
        }
        return bits;
    }
}

347. 前 K 个高频元素

【方法1】最大堆
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums) {
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] m, int[] n) {
                return m[1] - n[1];
            }
        });
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {
                    queue.poll();
                    queue.offer(new int[]{num, count});
                }
            } else {
                queue.offer(new int[]{num, count});
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k; ++i) {
            ret[i] = queue.poll()[0];
        }
        return ret;
    }
}

394. 字符串解码

【方法1】递归
class Solution {
    public String decodeString(String s) {
        StringBuilder result = new StringBuilder(s);
        Deque<Integer> stack = new LinkedList<>();
        for (int i = 0; i < result.length(); i++) {
            if (result.charAt(i) == '[') {
                stack.push(i);
            } else if (result.charAt(i) == ']') {
                int index = stack.pop();
                int start = index - 1;
                while (start >= 0 && result.charAt(start) >= '0' && result.charAt(start) <= '9') {
                    start--;
                }
                int num = Integer.parseInt(result.substring(start + 1, index));
                StringBuilder head = new StringBuilder(result.substring(0, start + 1));
                String tail = result.substring(i + 1);
                for (int j = 0; j < num; j++) {
                    head.append(result, index + 1, i);
                }
                head.append(tail);
                result = head;
                i = start + num * (i - index - 1) - 1;
            }
        }
        return result.toString();
    }
}

406. 根据身高重建队列

【方法1】排序
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o2[1] - o1[1] : o1[0] - o2[0]);
        int[][] ans = new int[people.length][];
        for (int i = 0; i < people.length; i++) {
            int space = people[i][1] + 1;
            for (int j = 0; j < ans.length ; j++) {
                if (ans[j] == null) {
                    space--;
                }
                if (space == 0) {
                    ans[j] = people[i];
                    break;
                }
            }
        }
        return ans;
    }
}

416. 分割等和子集

【方法1】动态规划,01背包问题

此问题对于每一个数字都由选取和不选取的情况,是一个01背包问题。即在数组中选取一定的数字,它们的和为数组和的一半(target = sum/2)。所以要先求出数组和,在求解数组和sum的时候,可以快速做出一些判断

  • 如果数组和为奇数,则和的一半是小数,正整数数组无法分割,返回FALSE。
  • 如果数组最大值maxNum>sum/2,则除maxNum之外的数无法等于sum/2,返回FALSE。

构建n行target+1列的二维数组dp,其中dp[i][j]表示在数组nums的[0, i]范围内选取一定的数(可以一个都不选),其和是否能等于j。由此可见

  • 对于dp[i][0]=true,对于0<=i<n。因为可以一个都不选;

  • 对于dp[0][num[0]]=true,对于i = 0且j = num[0]。只选择num[0]的时候dp[0][num[0]]为true.

  • 对于其他情形有

    • 当j>=num[i]时,因为和大于num[i],可以选择num[i]也可以不选择。

      不选择num[i]时,
      d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , 当 j > = n u m [ i ] 且 不 选 择 n u m [ i ] 时 dp[i][j] = dp[i-1][j], 当j >= num[i]且不选择num[i]时 dp[i][j]=dp[i1][j],j>=num[i]num[i]
      选择num[i]时,
      d p [ i ] [ j ] = d [ i − 1 ] [ j − n u m [ i ] ] , 当 j > = n u m [ i ] 且 选 择 n u m [ i ] 时 dp[i][j] = d[i-1][j-num[i]], 当j >= num[i]且选择num[i]时 dp[i][j]=d[i1][jnum[i]],j>=num[i]num[i]

  • 当j<nums[i]时,此时不能选择num[i],
    d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , 当 j < n u m [ i ] 时 dp[i][j] = dp[i-1][j], 当j<num[i]时 dp[i][j]=dp[i1][j],j<num[i]

综上有:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] ∣ ∣ d p [ i − 1 ] [ j − n u m [ i ] ] , j > = n u m [ i ] d p [ i − 1 ] [ j ] , j < n u m [ i ] dp[i][j] = \begin{cases} dp[i-1][j] || dp[i-1][j-num[i]], && j>=num[i]\\ dp[i-1][j], && j<num[i] \end{cases} dp[i][j]={dp[i1][j]dp[i1][jnum[i]],dp[i1][j],j>=num[i]j<num[i]
则dp[n][target]的结果即为所求值

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        int max = nums[0];
        for (int i : nums) {
            sum += i;
            max = Math.max(max, i);
        }
        if (sum % 2 == 1 || max > sum / 2) {
            return false;
        }
        int target = sum / 2;
        boolean[][] dp = new boolean[nums.length][target + 1];
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < target + 1; j++) {
                if (j == 0) {
                    dp[i][j] = true;
                    continue;
                }
                if (i == 0) {
                    if (j == nums[0]) {
                        dp[i][j] = true;
                    }
                    continue;
                }
                if (j >= nums[i]) {
                    dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]];
                } else {
                    dp[i][j] = dp[i-1][j];
                }

            }
        }
        return dp[nums.length - 1][target];
    }
}
优化空间复杂度

上述过程可以看出状态转移过程中,只与上一行有关系,故只需维护一维数组即可。
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] ∣ ∣ d p [ i − 1 ] [ j − n u m [ i ] ] , j > = n u m [ i ] d p [ i − 1 ] [ j ] , j < n u m [ i ] dp[i][j] = \begin{cases}dp[i-1][j] || dp[i-1][j-num[i]], && j>=num[i]\\dp[i-1][j], && j<num[i]\end{cases} dp[i][j]={dp[i1][j]dp[i1][jnum[i]],dp[i1][j],j>=num[i]j<num[i]
且需要注意的是第二层的循环我们需要从大到小计算,因为如果我们从小到大更新dp 值,那么在计算dp[j] 值的时候,dp[j−nums[i]] 已经是被更新过的状态,不再是上一行的dp 值。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        int max = nums[0];
        for (int i : nums) {
            sum += i;
            max = Math.max(max, i);
        }
        if (sum % 2 == 1 || max > sum / 2) {
            return false;
        }
        int target = sum / 2;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            for (int j = target; j >= num; j--) {
                dp[j] = dp[j] || dp[j - num];
            }
        }
        return dp[target];
    }
}

437. 路径总和 III

【方法1】深度搜索dfs
class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return 0;
        }
        int ans = dfs(root, targetSum);
        ans += pathSum(root.left, targetSum);
        ans += pathSum(root.right, targetSum);
        return ans;
    }

    public int dfs(TreeNode root, int sum) {
        if (root == null) {
            return 0;
        }
        int path = 0;
        if (sum == root.val) {
            path++;
        }
        path += dfs(root.left, sum - root.val);
        path += dfs(root.right, sum - root.val);
        return path;
    }
}

【方法2】前缀和

可以记录以某一节点为结尾的路径之和,则两个前缀和的差值为target时,也就存在一条路径和为target。

如10->5->2->1,target为8时,节点5的前缀和为15,节点1的前缀和为23,因为23-15=8,所以存在一条和为8的路径。为此需要记录前缀和为x时的路径个数y,可以使用hashmap。这里注意两点:

  • 初始时hashmap.put(0,1),保证找到两个前缀和之差为0的必有一条路径(它本身)
  • 当我们退出当前节点时,我们需要及时更新已经保存的前缀和
class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        Map<Integer, Integer> prefix = new HashMap<>();
        prefix.put(0, 1);
        return prefixSum(root, prefix, 0, targetSum);
    }

    public int prefixSum(TreeNode root, Map<Integer, Integer> prefix, int sum, int target) {
        if (root == null) {
            return 0;
        }
        sum += root.val;
        // 历史路径中是否存在前缀和为sum - target的路径数
        int path = prefix.getOrDefault(sum - target, 0);
        prefix.put(sum, prefix.getOrDefault(sum, 0) + 1);
        path += prefixSum(root.left, prefix, sum, target);
        path += prefixSum(root.right, prefix, sum, target);
        prefix.put(sum, prefix.getOrDefault(sum, 0) - 1);
        return path;
    }
}

438. 找到字符串中所有字母异位词

【方法1】暴力解法
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new LinkedList<>();
        int[] hash = new int[26];
        for (int i = 0; i < p.length(); i++) {
            hash[p.charAt(i) - 'a']++;
        }
        int[] map = new int[26];
        for (int i = 0; i < s.length(); i++) {
            if (s.length() - i < p.length()) {
                break;
            }
            for (int j = i; j < i + p.length(); j++) {
                map[s.charAt(j) - 'a']++;
            }
            boolean equal = true;
            for (int j = 0; j < hash.length; j++) {
                if (map[j] != hash[j]) {
                    equal = false;
                    break;
                }
            }
            if (equal) {
                ans.add(i);
            }
            Arrays.fill(map, 0);
        }
        return ans;
    }
}
【方法2】滑动窗口

可以考虑利用p.length()长度作为一个窗口,在字符串s上进行滑动,每次将窗口的头部滑出一个元素,在尾部划入一个元素。利用数组sHash的字符串编码值作为窗口内子串的异位词编码,数组pHash为字符串p的异位词编码,当二者相等时即为一个子串。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new LinkedList<>();
        if (s.length() < p.length()) {
            return ans;
        }
        int[] pHash = new int[26];
        for (int i = 0; i < p.length(); i++) {
            pHash[p.charAt(i) - 'a']++;
        }
        int[] sHash = new int[26];
        for (int i = 0; i < p.length(); i++) {
            sHash[s.charAt(i) - 'a']++;
        }
        if (Arrays.equals(sHash, pHash)) {
            ans.add(0);
        }
        for (int i = 0; i < s.length() - p.length(); i++) {
            sHash[s.charAt(i) - 'a']--;
            sHash[s.charAt(i + p.length()) - 'a']++;
            if (Arrays.equals(sHash, pHash)) {
                ans.add(i + 1);
            }
        }
        return ans;
    }
}

538. 把二叉搜索树转换为累加树

【方法1】反序中序遍历

这里比较重要的是要用一个全局变量记录当前的和,用当前和+当前节点的值即为累加值。

class Solution {
    int sum = 0; // 记录当前和
    public TreeNode convertBST(TreeNode root) {
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        dfs(root.right);
        root.val += sum;
        sum = root.val;
        dfs(root.left);
    }
}

647. 回文子串

【方法1】中心扩展

对于一个回文字符串来说,如果长度为奇数则其回文中心有两个,长度为偶则其回文中心有一个,假如字符串长度为4,可以分析出:

回文中心1回文中心2编号
000
011
112
123
224
235
336

可以看出:

对于长度为n的字符串共有2(n-1)个回文中心组和,可以直接遍历0-2(n-1)即可得出所有回文中心,然后分别对回文中心向左、向右扩展即可得出所有回文子串。

class Solution {
    public int countSubstrings(String s) {
        int num = 0;
        for (int i = 0; i <= 2 * (s.length() - 1); i++) {
            int left = i / 2;
            int right = (i % 2 == 0) ? left : left + 1;
            while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
                left--;
                right++;
                num++;
            }
        }
        return num;
    }
}
【方法2】Manacher 算法

Manacher 算法应用的一个前提是回文字符串长度为奇数,即只能有一个回文中心。可以在每个字符中间和开头与结尾都插入一个特殊字符,使得回文字符串长度永远为奇数。如回文串abba,回文中心为(a,b)。插入特殊字符#(任意特殊字符即可)后变为#a#b#b#a#仍然为回文字符串,但回文中心变为(#),且字符串长度为奇数。

Manacher 算法的核心在于提出了臂长概念:

对于字符串ebabababe

|e|b|a|b|a|b|a|b|e|
-------------------
|0|1|2|3|4|5|6|7|8|
-------------------
|--len--|j|--len--|

当回文中心j = 4时,以i为中心最长回文子串的半径为臂长len, 记为f[j]。则当遍历到j时,求解f[i]时,可以找出与i关于对称的点k

-------------------
|e|b|a|b|a|b|a|b|e|
-------------------
|0|1|2|3|4|5|6|7|8|
-------------------
|---|k|-|j|-|i|---|
-------------------

由于对称性关系,f[i]至少为min(f[k], j + len - i)。下面分析这个结论的原因,当我们把k点对称过去后,可以直到k的臂长为f[k],而以j为回文中心的臂长为len,在len长度内,以i为回文中心的字符串长度最大为 j + len - i,所以有
f [ i ] = m i n ( f ( k ) , j + l e n − i ) , 其 中 k = j − ( i − j ) ; f[i] = min(f(k), j+len-i), 其中k = j - (i - j); f[i]=min(f(k),j+leni),k=j(ij);
然后以右臂最长的回文中心作为j,即i的对称中心。这样我们利用历史信息f,得到了i的最小的力臂长度,再进行中心扩展时,可以直接从最小力臂长度开始往外扩展,而不用每次从i开始扩展

对于力臂长度为f[i]的正常回文串,其回文子串有f[i]个,又因为**回文串要求都为奇数,会对偶数回文串插入#变为奇数,所以子串为f[i]/2。**比如#a#a#,力臂长度为2,回文子串个数为1,即aa。为了不让下标越界,一个很简单的办法,就是在开头加一个 $,并在结尾加一个 !。

class Solution {
    public int countSubstrings(String s) {
        StringBuilder stringBuilder = new StringBuilder("$#");
        for(int i = 0; i < s.length(); i++) {
            stringBuilder.append(s.charAt(i));
            stringBuilder.append('#');
        }
        s = stringBuilder.append("!").toString();
        int count = 0;
        int[] dp = new int[s.length()];
        int right = -1;
        int j = -1;
        for (int i = 1; i < s.length(); i++) {
            if (right >= i) {
                int k = j - (i - j);
                int minLen = Math.min(dp[k], right - i);
                dp[i] = expand(s, i - minLen, i + minLen) + 1;
            } else {
                dp[i] = expand(s, i, i) + 1;
            }
            if (i + dp[i] > right) {
                right = i + dp[i];
                j = i;
            }
            count += dp[i] / 2;
        }
        return count;
    }
    private int expand (String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }
        return (right - left - 2) / 2;
    }
}

高频知识点

分类题号
二叉树94、145、226、236、113、100、226、590、103
背包问题474、494、416、518、322、139、377、1049、1449、279
并查集547、684、1319、1631、959、1202、947、721、803、1579、778
单调栈
动态规划221、1277、121、122、123
dfs113
图路径算法Dijkstra399、1631

二叉树专题

94. 二叉树的中序遍历

【二叉树中序遍历——递归】
class Solution {
    List<Integer> list = new LinkedList<>();

    public List<Integer> inorderTraversal(TreeNode root) {
        dfs(root);
        return list;
    }

    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        dfs(root.left);
        list.add(root.val);
        dfs(root.right);
    }
}
【二叉树中序遍历——栈】
class Solution{
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ans = new LinkedList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            ans.add(root.val);
            root = root.right;
        }
        return ans;
    }
}
【二叉树中序遍历——Morris算法】

O(1)空间复杂度算法的Morris算法;

  • 节点x无左孩子
    • x加入结果
    • x = x.right
  • 节点x有左孩子,找到predecessor(当前节点左子树最右节点)
    • predecessor无右孩子,predecessor.right = x, x = x.left
    • predecessor有右孩子,x加入结果,x = x.right
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        morris(root);
        return list;
    }

    private void morris(TreeNode root) {
        while (root != null) {
            if (root.left == null) {
                list.add(root.val);
                root = root.right;
            } else {
                TreeNode predecessor = getPredecessor(root);
                if (predecessor.right == null) {
                    predecessor.right = root;
                    root = root.left;
                } else {
                    list.add(root.val);
                    root = root.right;
                }
            }
        }
    }
	// predecessor为当前节点向左走一步,然后向右走到无法再走为止
    private TreeNode getPredecessor(TreeNode node) {
        TreeNode predecessor = node.left;
        while (predecessor.right != null && predecessor.right != node) {
            predecessor = predecessor.right;
        }
        return predecessor;
    }
}

145. 二叉树的后序遍历

【二叉树后序遍历——递归】
class Solution{
     List<Integer> list = new LinkedList<>();
    public List<Integer> postorderTraversal1(TreeNode root) {
        dfs(root);
        return list;
    }

    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        dfs(root.left);
        dfs(root.right);
        list.add(root.val);
    }
}
【二叉树后序遍历——栈】
class Solution {
    List<Integer> list = new LinkedList<>();
    Deque<TreeNode> stack = new LinkedList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        TreeNode prev = null;
        while(root != null || !stack.isEmpty()) {
            while(root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            if (root.right == null || root.right == prev) {
                list.add(root.val);
                prev = root;
                root = null;
            } else {
                stack.push(root);
                root = root.right;
            }
        }
        return list;
    }
}

100. 相同的树

【两个树的遍历】
class Solution {
    boolean isSame = true;
    public boolean isSameTree(TreeNode p, TreeNode q) {
        dfs(p, q);
        return isSame;
    }

    private void dfs(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return;
        }
        if (root1 == null || root2 == null) {
            isSame = false;
            return;
        }
        dfs(root1.left, root2.left);
        if (root1.val != root2.val) {
            isSame = false;
        }
        dfs(root1.right, root2.right);
    }
}

226. 翻转二叉树

【后序遍历】
class Solution {
    public TreeNode invertTree(TreeNode root) {
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        dfs(root.left);
        dfs(root.right);
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
    }
}

590. N 叉树的后序遍历

【后序遍历】
public class Solution590 {
    List<Integer> ans = new LinkedList<>();
    public List<Integer> postorder(Node root) {
        dfs(root);
        return ans;
    }

    private void dfs(Node root) {
        if (root == null) {
            return;
        }
        if (root.children != null && root.children.size() > 0) {
            root.children.forEach(this::dfs);
        }
        ans.add(root.val);
    }
}

103. 二叉树的锯齿形层序遍历

【二叉树的按层次遍历——队列】
class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        if (root == null) {
            return new LinkedList<>();
        }
        List<List<Integer>> ans = new LinkedList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        boolean isOrderLeft = false;
        queue.offer(root);
        while (!queue.isEmpty()) {
            LinkedList<Integer> cur = new LinkedList<>();
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode top = queue.poll();
                if (top.left != null) {
                    queue.offer(top.left);
                }
                if (top.right != null) {
                    queue.offer(top.right);
                }
                if (isOrderLeft) {
                    cur.addFirst(top.val);
                } else {
                    cur.addLast(top.val);
                }
            }
            ans.add(cur);
            isOrderLeft = !isOrderLeft;
        }
        return ans;
    }
}

124. 二叉树中的最大路径和

【节点的最大贡献】
class Solution {
    int max = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return max;
    }

    private int dfs(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = Math.max(dfs(root.left), 0);
        int right = Math.max(dfs(root.right), 0);
        int path = left + right + root.val;
        max = Math.max(max, path);
        return root.val + Math.max(left, right);
    }
}

背包问题

474. 一和零

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i < strs.length; i++) {
            int one = getSum(strs[i]);
            int zero = strs[i].length() - one;
            for (int j = m; j >= zero; j--) {
                for (int k = n; k >= one; k--) {
                    dp[j][k] = Math.max(dp[j][k], dp[j - zero][k - one] + 1);
                }
            }
        }
        return dp[m][n];
    }

    public int getSum(String str) {
        int sum = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == '1') {
                sum++;
            }
        }
        return sum;
    }
}

1049. 最后一块石头的重量 II

【方法1 0-1背包】

对于stones[]数组来说,假设最后剩余石头重量为left,石头开始总重量为sum,则sum-left后所有的石头可以完全粉碎,即将石头分为两部分重量相等为neg,即sum - left = 2*neg。则neg = (sum - left)/2,要想left尽可能小,则neg需要尽可能接近sum/2。因此问题转化为,在stone中挑选石头使其总重量尽可能接近sum/2的背包问题。

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int i = 0; i < stones.length; i++) {
            sum += stones[i];
        }
        int n = sum / 2;
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for (int i = 0; i < stones.length; i++) {
            for (int j = n; j >= stones[i]; j--) {
                dp[j] = dp[j - stones[i]] || dp[j];
            }
        }
        int neg = 0;
        for (int i = n; i >= 0; i--) {
            if (dp[i]) {
                neg = i;
                break;
            }
        }
        System.out.println(neg);
        return sum - 2 * neg;
    }
}

并查集问题

并查集算法

路径压缩

在查询的时候将节点的父节点直接指向根节点:

weight1
weight2
weight3
a
b
root
c

路径压缩后

weight1 * weight2
weight2
weight3
a
root
b
c
public int find(int x) {
    if (parent[x] != x) {
        int origin = parent[x];
        // 找到x父节点的根节点,并将x父节点指向根节点
        parent[x] = find(parent[x]);
        weight[x] *= weight[origin];
    }
    return parent[x];
}

路径压缩后,并查集的高度为2.

合并路径
weight1
weight2
input
weight
a
rootA
b
rootB
weight * weight1 = weight2 * input ==>  weight = weight2 * input / weight1
public class UnionFind {
    
    private int[] parent;
    
    private double[] weight;
    
    public UnionFind(int n) {
        parent = new int[n];
        weight = new double[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            weight[i] = 1.0;
        }
    }

    public int find(int x) {
        if (parent[x] != x) {
            int origin = parent[x];
            parent[x] = find(parent[x]);
            weight[x] *= weight[origin];
        }
        return parent[x];
    }

    public void union(int x, int y, double value) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) {
            return;
        }
        parent[rootX] = rootY;
        weight[rootX] = weight[y] * value / weight[x];
    }
}

399. 除法求值

【方法1 带权重的并查集】
class Solution {
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int size = equations.size();
        UnionFind unionFind = new UnionFind(2 * size);
        HashMap<String, Integer> map = new HashMap<>();
        int index = 0;
        for (int i = 0; i < equations.size(); i++) {
            String var1 = equations.get(i).get(0);
            String var2 = equations.get(i).get(1);
            if (map.get(var1) == null) {
                map.put(var1, index);
                index++;
            }
            if (map.get(var2) == null) {
                map.put(var2, index);
                index++;
            }
            unionFind.union(map.get(var1), map.get(var2), values[i]);
        }
        double[] ans = new double[queries.size()];
        for (int i = 0; i < ans.length; i++) {
            String var1 = queries.get(i).get(0);
            String var2 = queries.get(i).get(1);
            if (map.get(var1) != null && map.get(var2) != null) {
                ans[i] = unionFind.getWeight(map.get(var1), map.get(var2));
            } else {
                ans[i] = -1.0;
            }
        }
        return ans;

    }

    class UnionFind {
        private int[] parent;
        private double[] weight;

        public UnionFind(int n) {
            parent = new int[n];
            weight = new double[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                weight[i] = 1.0;
            }
        }

        public int find(int x) {
            if (parent[x] != x) {
                int origin = parent[x];
                parent[x] = find(parent[x]);
                weight[x] *= weight[origin];
            }
            return parent[x];
        }

        public void union(int x, int y, double value) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            parent[rootX] = rootY;
            weight[rootX] = weight[y] * value / weight[x];
        }

        public double getWeight(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return weight[x] / weight[y];
            } else {
                return -1.0;
            }
        }
    }
}

1319. 连通网络的操作次数

【方法1并查集, 使用count表示集合个数】
class Solution {
    public int makeConnected(int n, int[][] connections) {
        UnionFind unionFind = new UnionFind(n);
        for (int i = 0; i < connections.length; i++) {
            unionFind.union(connections[i][0], connections[i][1]);
        }
        System.out.println(n);
        System.out.println(unionFind.count);
        System.out.println(unionFind.left);
        if (unionFind.left >= unionFind.count - 1) {
            return unionFind.count - 1;
        } else {
            return -1;
        }
    }

    class UnionFind {

        private int[] parent;

        private int count;
        private int left;

        public UnionFind(int n) {
            parent = new int[n];
            count = n;
            left = 0;
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }

        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                left++;
                return;
            }
            parent[rootX] = rootY;
            count--;
        }
    }

959. 由斜杠划分区域

按区域的最小粒度划分

class Solution {
    public int regionsBySlashes(String[] grid) {
        int N = grid.length;
        int size = 4 * N * N;

        UnionFind unionFind = new UnionFind(size);
        for (int i = 0; i < N; i++) {
            char[] row = grid[i].toCharArray();
            for (int j = 0; j < N; j++) {
                // 二维网格转换为一维表格,index 表示将单元格拆分成 4 个小三角形以后,编号为 0 的小三角形的在并查集中的下标
                int index = 4 * (i * N + j);
                char c = row[j];
                // 单元格内合并
                if (c == '/') {
                    // 合并 0、3,合并 1、2
                    unionFind.union(index, index + 3);
                    unionFind.union(index + 1, index + 2);
                } else if (c == '\\') {
                    // 合并 0、1,合并 2、3
                    unionFind.union(index, index + 1);
                    unionFind.union(index + 2, index + 3);
                } else {
                    unionFind.union(index, index + 1);
                    unionFind.union(index + 1, index + 2);
                    unionFind.union(index + 2, index + 3);
                }

                // 单元格间合并
                // 向右合并:1(当前)、3(右一列)
                if (j + 1 < N) {
                    unionFind.union(index + 1, 4 * (i * N + j + 1) + 3);
                }
                // 向下合并:2(当前)、0(下一行)
                if (i + 1 < N) {
                    unionFind.union(index + 2, 4 * ((i + 1) * N + j));
                }
            }
        }
        return unionFind.count;
    }

    class UnionFind {
        private int[] parent;
        private int count;

        public UnionFind(int n) {
            parent = new int[n];
            count = n;
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }

        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            parent[rootX] = rootY;
            count--;
        }
    }
}

字节后端

25. K 个一组翻转链表

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode slow = head;
        ListNode fast = head;
        ListNode newHead = new ListNode();
        ListNode prev = newHead;
        while (fast != null) {
            int i = 0;
            for (; i < k && fast != null; i++) {
                fast = fast.next;
            }
            if (i < k) {
                break;
            }
            prev.next = reverse(slow, fast);
            slow.next = fast;
            prev = slow;
            slow = fast;
        }
        return newHead.next;
    }

    private ListNode reverse(ListNode head, ListNode tail) {
        ListNode newHead = new ListNode();
        while (head != tail) {
            ListNode tmp = newHead.next;
            newHead.next = head;
            head = head.next;
            newHead.next.next = tmp;
        }
        return newHead.next;
    }
}

113. 路径总和 II

【方法1】深度搜索
class Solution {
    List<List<Integer>> paths = new LinkedList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        dfs(root, targetSum);
        System.out.println(paths);
        System.out.println(path);
        return paths;
    }

    public void dfs(TreeNode root, int targetSum) {
        if (root == null) {
            return;
        }
        path.offerLast(root.val);
        targetSum -= root.val;
        if (root.left == null && root.right == null && targetSum == 0) {
            paths.add(new LinkedList<>(path));
        }
        dfs(root.left, targetSum);
        dfs(root.right, targetSum);
        //用完当前节点要进行清理!!
        path.pollLast();
    }
}

【】

121. 买卖股票的最佳时机

状态转移方程:
{ d p [ i ] = d p [ i − 1 ] − p r i c e s [ i − 1 ] + p r i c e s [ i ] 第 i 天 卖 出 时 收 益 \begin{cases} dp[i] = dp[i-1] - prices[i-1] + prices[i] & 第i天卖出时收益 \end{cases} {dp[i]=dp[i1]prices[i1]+prices[i]i
最大利润
m a x ( d p [ i ] ) max(dp[i]) max(dp[i])

class Solution {
    public int maxProfit(int[] prices) {
        int last0 = 0;
        int maxProfit = 0;
        for (int i = 1; i < prices.length; i++) {
            int current0 = Math.max(last0 - prices[i - 1] + prices[i], 0);
            maxProfit = Math.max(current0, maxProfit);
            last0 = current0;
        }
        return maxProfit;
    }
}

122. 买卖股票的最佳时机II

状态转移方程:
{ d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) 第 i 天 持 有 股 票 的 最 大 收 益 d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] ) 第 i 天 不 持 有 股 票 的 最 大 收 益 \begin{cases} dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) & 第i天持有股票的最大收益\\ dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) & 第i天不持有股票的最大收益 \end{cases} {dp[i][0]=max(dp[i1][0],dp[i1][1]prices[i])dp[i][1]=max(dp[i1][1],dp[i1][0]+prices[i])ii
边界条件:
{ d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] 第 1 天 只 能 买 入 股 票 , 收 益 为 负 d p [ 0 ] [ 1 ] = 0 \begin{cases} dp[0][0] = -prices[0] & 第1天只能买入股票,收益为负\\ dp[0][1] = 0 \end{cases} {dp[0][0]=prices[0]dp[0][1]=01

最大利润
m a x ( d p [ i ] [ 0 ] , d p [ i ] [ 1 ] ) max(dp[i][0], dp[i][1]) max(dp[i][0],dp[i][1])

class Solution {
    public int maxProfit(int[] prices) {
        int last0 = -prices[0];
        int last1 = 0;
        int maxProfit = 0;
        for (int i = 0; i < prices.length; i++) {
            int current0 = Math.max(last0, last1 - prices[i]);
            int current1 = Math.max(last1, last0 + prices[i]);
            maxProfit = Math.max(current0, current1);
            last0 = current0;
            last1 = current1;
        }
        return maxProfit;
    }
}

123. 买卖股票的最佳时机III

状态转移方程:
{ d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , − p r i c e [ i ] ) 第 i 天 完 成 0 笔 交 易 且 持 有 股 票 的 最 大 收 益 d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 0 ] + p r i c e [ i ] , d p [ i − 1 ] [ 1 ] ) 第 i 天 完 成 1 笔 交 易 且 不 持 有 股 票 的 最 大 收 益 d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 1 ] − p r i c e [ i ] , d p [ i − 1 ] [ 2 ] ) 第 i 天 完 成 1 笔 交 易 且 持 有 股 票 的 最 大 收 益 d p [ i ] [ 3 ] = m a x ( d p [ i − 1 ] [ 2 ] + p r i c e [ i ] , d p [ i − 1 ] [ 3 ] ) 第 i 天 完 成 2 笔 交 易 的 最 大 收 益 \begin{cases} dp[i][0] = max(dp[i-1][0], -price[i]) & 第i天完成0笔交易且持有股票的最大收益\\ dp[i][1] = max(dp[i-1][0] + price[i], dp[i-1][1]) & 第i天完成1笔交易且不持有股票的最大收益\\ dp[i][2] = max(dp[i-1][1] - price[i], dp[i-1][2]) & 第i天完成1笔交易且持有股票的最大收益\\ dp[i][3] = max(dp[i-1][2] + price[i], dp[i-1][3]) & 第i天完成2笔交易的最大收益\\ \end{cases} dp[i][0]=max(dp[i1][0],price[i])dp[i][1]=max(dp[i1][0]+price[i],dp[i1][1])dp[i][2]=max(dp[i1][1]price[i],dp[i1][2])dp[i][3]=max(dp[i1][2]+price[i],dp[i1][3])i0i1i1i2
边界条件:
{ d p [ i ] [ 0 ] = − p r i c e [ 0 ] 第 1 天 完 成 0 笔 交 易 且 持 有 股 票 的 最 大 收 益 d p [ i ] [ 1 ] = 0 第 1 天 完 成 1 笔 交 易 且 不 持 有 股 票 的 最 大 收 益 d p [ i ] [ 2 ] = − p r i c e [ 0 ] 第 1 天 完 成 1 笔 交 易 且 持 有 股 票 的 最 大 收 益 d p [ i ] [ 3 ] = 0 第 1 天 完 成 2 笔 交 易 的 最 大 收 益 \begin{cases} dp[i][0] = -price[0] & 第1天完成0笔交易且持有股票的最大收益\\ dp[i][1] = 0 & 第1天完成1笔交易且不持有股票的最大收益\\ dp[i][2] = - price[0] & 第1天完成1笔交易且持有股票的最大收益\\ dp[i][3] = 0 & 第1天完成2笔交易的最大收益\\ \end{cases} dp[i][0]=price[0]dp[i][1]=0dp[i][2]=price[0]dp[i][3]=010111112
最大利润:
m a x ( d p [ i ] [ 0 ] , d p [ i ] [ 1 ] , d p [ i ] [ 2 ] , d p [ i ] [ 3 ] ) max(dp[i][0], dp[i][1], dp[i][2], dp[i][3]) max(dp[i][0],dp[i][1],dp[i][2],dp[i][3])

class Solution {
    public int maxProfit(int[] prices) {
        int last0 = -prices[0];
        int last1 = 0;
        int last2 = -prices[0];
        int last3 = 0;
        int maxProfit = 0;
        for (int i = 0; i < prices.length; i++) {
            int current0 = Math.max(last0, -prices[i]);
            int current1 = Math.max(last0 + prices[i], last1);
            int current2 = Math.max(last1 - prices[i], last2);
            int current3 = Math.max(last3, last2 + prices[i]);
            maxProfit = Math.max(Math.max(Math.max(current0, current1), current2), current3);
            last0 = current0;
            last1 = current1;
            last2 = current2;
            last3 = current3;
        }
        return maxProfit;
    }
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值