labuladong刷题集锦

一、动态规划

(一)动态规划解题套路框架

1.基础概念

Ⅰ.DP一般是用来解决最值问题,比如最长递增子序列,最小编辑距离等。

Ⅱ.求解DP的核心问题是穷举,穷举所有可行的答案找出最值。

Ⅲ.DP的三要素:

  • 重叠子问题(自顶向下进行备忘录 or 自底向上的DP table)
  • 最优子结构(通过子问题的最值得到原问题的最值)
  • 状态转移方程(重要)
    • 初始化base case
    • 明确状态
    • 明确选择
    • 定义dp数组/函数的含义

Ⅳ.框架:

//初始化base case
dp[0][0][....] = base
//进行状态转移
for 状态1 in 状态1的所有取值:
	for 状态2 in 状态2的所有取值:
		for ...
			dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...)

2.斐波那契数列

主要讲解重叠子问题和状态转移方程

(1)暴力递归
public int fib(int n) {
    if (n < 2) {
        return n;
    }
    return f(n-1) + f(n-2);
}

递归树:
在这里插入图片描述
由此可以看出,存在重叠子问题,比如f(18)这棵庞大的树要计算两次,存在大量重复计算。

递归的时间复杂度:子问题的个数 * 解决一个子问题所需要的时间。

本题中,子问题个数为递归树中的节点的总数,而一棵二叉树的节点总数=2^n-1(n为层数)。所以此处的子问题总数为2^(n-1)-1(n为根节点值)。
解决一个子问题通过f(n-1) + f(n-2)加法解决,所以O(1)
总的来看是O(2^N)时间复杂度。

(2)带备忘录的自顶向下
    public int fib(int n) {
        //备忘录全部初始化为0
        int[] memo = new int[n+1];
        //进行带备忘录的递归
        return helper(memo, n);
    }

    public int helper(int[] memo, int n) {
        //base case
        if (n < 2) {
            return n;
        }
        //计算过的不用再计算
        if (memo[n] != 0) {
            return memo[n];
        }

        memo[n] = (helper(memo, n-1) + helper(memo, n-2)) % 1000000007;

        return memo[n];
    }

实际上通过备忘录进行剪枝。
时间复杂度:O(N) * O(1)

(3)DP数组的迭代解法

状态转移方程:
在这里插入图片描述

class Solution {
    public int fib(int n) {

        if (n < 2) {
            return n;
        }
    
        int[] dp = new int[n+1];

        dp[0] = 0;
        dp[1] = 1;

        for (int i = 2; i <= n; i++) {
            dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
        }

        return dp[n];
    }
}

状态压缩优化:

class Solution {
    public int fib(int n) {

        if (n < 2) {
            return n;
        }

        int a = 0;
        int b = 1;
        int c = 1;    

        for (int i = 2; i <= n; i++) {
            c = (a + b) % 1000000007;
            a = b;
            b = c;
        }

        return c;
    }
}

3.凑零钱问题

主要讲解最优子结构和如何列出状态转移方程

(二)分类题目

1.股票

(1)买卖股票的最佳时机Ⅰ

在这里插入图片描述

class Solution {
    public int maxProfit(int[] prices) {

        int res = 0;
        int min = prices[0];

        for (int i = 1; i < prices.length; i++) {
            if (prices[i] < min) {
                min = prices[i];
            }

            res = Math.max(res, prices[i] - min);
        }

        return res;
    }
}

or

class Solution {
    public int maxProfit(int[] prices) {
        
        int[] dp = new int[prices.length];
        dp[0] = 0;
        int min = prices[0];

        for (int i = 1; i < prices.length; i++) {
            if (prices[i] < min) {
                min = prices[i];
            }

            dp[i] = Math.max(dp[i-1], prices[i] - min);
        }

        return dp[prices.length-1];
    }
}
(2)买卖股票的最佳时机Ⅱ

在这里插入图片描述

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        } 

        return dp[n-1][0];
    }
}

空间优化:

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int a = 0;
        int b = -prices[0];
        
        for (int i = 1; i < n; i++) {
            int tmp1  = Math.max(a, b + prices[i]);
            int tmp2 = Math.max(b, a - prices[i]);
            a = tmp1;
            b = tmp2;
        } 

        return a;
    }
}

贪心:

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

        return ans;
    }
}

2.打家劫舍

(1)打家劫舍Ⅰ

在这里插入图片描述

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);

        for (int i = 2; i < nums.length; i++) {
            dp[i] = Math.max(dp[i-2]+nums[i], dp[i-1]);
        }

        return dp[nums.length-1];
    }
}
class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }

        int a = nums[0];
        int b = Math.max(nums[0], nums[1]);
        int c = b;

        for (int i = 2; i < nums.length; i++) {
            c = Math.max(a+nums[i], b);
            a = b;
            b = c;
        }

        return b;
    }
}
(2)打家劫舍Ⅱ

在这里插入图片描述

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        } else if (n == 2) {
            return Math.max(nums[0], nums[1]);
        }
        int a = robByRange(nums, 0, n-2);
        int b = robByRange(nums, 1, n-1);

        return Math.max(a, b);
    }

    public int robByRange(int[] nums, int start, int end) {
        int[] dp = new int[end-start+1];
        dp[0] = nums[start];
        dp[1] = Math.max(nums[start], nums[start+1]);

        for (int i = 2; i <= (end-start); i++) {
            dp[i] = Math.max(dp[i-2]+nums[i+start], dp[i-1]);
        }

        return dp[end-start];
    }
}
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        } else if (n == 2) {
            return Math.max(nums[0], nums[1]);
        }
        int a = robByRange(nums, 0, n-2);
        int b = robByRange(nums, 1, n-1);

        return Math.max(a, b);
    }

    public int robByRange(int[] nums, int start, int end) {

        int a = nums[start];
        int b = Math.max(nums[start], nums[start+1]);
        int c = b;

        for (int i = start+2; i <= end; i++) {
            c = Math.max(a+nums[i], b);
            a = b;
            b = c;
        }

        return c;
    }
}
(3)打家劫舍Ⅲ

在这里插入图片描述

二、二叉树

(一)递归解题思想概述

递归二叉树第一期
递归二叉树第二期
二叉树递归遍历框架:

/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    // 前序遍历
    traverse(root.left)
    // 中序遍历
    traverse(root.right)
    // 后序遍历
}

快排和归并排序:

类似于前序遍历

void sort(int[] nums, int lo, int hi) {
    /****** 前序遍历位置 ******/
    // 通过交换元素构建分界点 p
    int p = partition(nums, lo, hi);
    /************************/

    sort(nums, lo, p - 1);
    sort(nums, p + 1, hi);
}

类似于后序遍历

void sort(int[] nums, int lo, int hi) {
    int mid = (lo + hi) / 2;
    sort(nums, lo, mid);
    sort(nums, mid + 1, hi);

    /****** 后序遍历位置 ******/
    // 合并两个排好序的子数组
    merge(nums, lo, mid, hi);
    /************************/
}

写递归算法的关键是要明确函数的定义是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要试图跳入递归。
写树相关的算法,简单说就是,先搞清楚当前root节点该做什么,然后根据函数定义****递归调用子节点,递归调用会让孩子节点做相同的事情。
最主要的是明确函数的功能,也就是该节点要做什么。
比如:计算一棵二叉树共有几个节点
count函数是计算以root为根的树有多少个节点,那其实就是算左子树和右子树分别有多少个节点,然后加上root本身就行,正好count函数就可以实现这个功能。

// 定义:count(root) 返回以 root 为根的树有多少节点
int count(TreeNode root) {
    // base case
    if (root == null) return 0;
    // 自己加上子树的节点数就是整棵树的节点数
    return 1 + count(root.left) + count(root.right);
}

练习题:
1.翻转二叉树
在这里插入图片描述

// 将整棵树的节点翻转
TreeNode invertTree(TreeNode root) {
    // base case
    if (root == null) {
        return null;
    }

    /**** 前序遍历位置 ****/
    // root 节点需要交换它的左右子节点
    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;

    // 让左右子节点继续翻转它们的子节点
    invertTree(root.left);
    invertTree(root.right);

    return root;
}

重点是交换每个节点的左右子节点。
前序、后序都可以,但是中序不行,
因为中序遍历是左根右的遍历顺序,相当于左侧节点交换了两次,而右侧节点没有换:
遍历左侧节点时,左侧节点的子节点交换,遍历到root之后,root交换两左右侧节点,再去遍历右侧节点时,又把节点交换回来了。

2.填充二叉树节点的右侧指针
在这里插入图片描述
在这里插入图片描述
输入是一棵「完美二叉树」,形象地说整棵二叉树是一个正三角形,除了最右侧的节点next指针会指向null,其他节点的右侧一定有相邻的节点。

刚开始这样写的:

Node connect(Node root) {
    if (root == null || root.left == null) {
        return root;
    }

    root.left.next = root.right;

    connect(root.left);
    connect(root.right);

    return root;
}

这样写的话,只能让兄弟节点相连,而跨父节点的两个节点不能相连。
二叉树的问题难点在于,如何把题目的要求细化成每个节点需要做的事情,但是如果只依赖一个节点的话,肯定是没办法连接「跨父节点」的两个相邻节点的。
所以增加参数,细化功能为连接相邻的两个节点,而不是连接同一层的节点

// 主函数
Node connect(Node root) {
    if (root == null) return null;
    connectTwoNode(root.left, root.right);
    return root;
}

// 定义:输入两个节点,将它俩连接起来
void connectTwoNode(Node node1, Node node2) {
    if (node1 == null || node2 == null) {
        return;
    }
    /**** 前序遍历位置 ****/
    // 将传入的两个节点连接
    node1.next = node2;

    // 连接相同父节点的两个子节点
    connectTwoNode(node1.left, node1.right);
    connectTwoNode(node2.left, node2.right);
    // 连接跨越父节点的两个子节点
    connectTwoNode(node1.right, node2.left);
}

3.二叉树展开为链表
在这里插入图片描述
flatten函数定义:给flatten函数输入一个节点root,那么以root为根的二叉树就会被拉平为一条链表。
所以按照题目,得到结果需要以下几步:
1、将root的左子树和右子树拉平。-> 按照flatten函数的定义,对root的左右子树递归调用flatten函数即可
2、将root的右子树接到左子树下方,然后将整个左子树作为右子树。
在这里插入图片描述
因为要先拉平左右子树才能进行拼接,所以是后序遍历。

// 定义:将以 root 为根的树拉平为链表
void flatten(TreeNode root) {
    // base case
    if (root == null) return;

    flatten(root.left);
    flatten(root.right);

    /**** 后序遍历位置 ****/
    // 1、左右子树已经被拉平成一条链表
    TreeNode left = root.left;
    TreeNode right = root.right;

    // 2、将左子树作为右子树
    root.left = null;
    root.right = left;

    // 3、将原先的右子树接到当前右子树的末端
    TreeNode p = root;
    while (p.right != null) {
        p = p.right;
    }
    p.right = right;
}

写二叉树的算法题,都是基于递归框架的,我们先要搞清楚root节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。

二叉树题目的难点在于如何通过题目的要求思考出每一个节点需要做什么,这个只能通过多刷题进行练习了。

4.构造最大二叉树
![在这里插入图片描述](https://img-blog.csdnimg.cn/c2b54b3b30f74d9f9b512b952c3439cf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmFt55qEYmY=,size_20,color_FFFFFF,t_70,g_se,x_16
在这里插入图片描述

TreeNode constructMaximumBinaryTree([3,2,1,6,0,5]) {
    // 找到数组中的最大值
    TreeNode root = new TreeNode(6);
    // 递归调用构造左右子树
    root.left = constructMaximumBinaryTree([3,2,1]);
    root.right = constructMaximumBinaryTree([0,5]);
    return root;
}
TreeNode constructMaximumBinaryTree(int[] nums) {
    if (nums is empty) return null;
    // 找到数组中的最大值
    int maxVal = Integer.MIN_VALUE;
    int index = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > maxVal) {
            maxVal = nums[i];
            index = i;
        }
    }

    TreeNode root = new TreeNode(maxVal);
    // 递归调用构造左右子树
    root.left = constructMaximumBinaryTree(nums[0..index-1]);
    root.right = constructMaximumBinaryTree(nums[index+1..nums.length-1]);
    return root;
}
/* 主函数 */
TreeNode constructMaximumBinaryTree(int[] nums) {
    return build(nums, 0, nums.length - 1);
}

/* 将 nums[lo..hi] 构造成符合条件的树,返回根节点 */
TreeNode build(int[] nums, int lo, int hi) {
    // base case
    if (lo > hi) {
        return null;
    }

    // 找到数组中的最大值和对应的索引
    int index = -1, maxVal = Integer.MIN_VALUE;
    for (int i = lo; i <= hi; i++) {
        if (maxVal < nums[i]) {
            index = i;
            maxVal = nums[i];
        }
    }

    TreeNode root = new TreeNode(maxVal);
    // 递归调用构造左右子树
    root.left = build(nums, lo, index - 1);
    root.right = build(nums, index + 1, hi);

    return root;
}

5.前序中序构造二叉树

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return builder(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
    }
    public TreeNode builder(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
        if (preStart > preEnd) {
            return null;
        }

        int rootVal = preorder[preStart];
        int inIndex = 0;
        for (int i = inStart; i <=inEnd; i++) {
            if (inorder[i] == rootVal) {
                inIndex = i;
                break;
            }
        }

        int leftSize = inIndex - inStart;

        TreeNode root = new TreeNode(rootVal);

        root.left = builder(preorder, preStart+1, preStart+leftSize, inorder, inStart, inIndex-1);
        root.right = builder(preorder, preStart+leftSize+1, preEnd, inorder, inIndex+1, inEnd);

        return root;
    }
}

中序的根索引使用map获取:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private Map<Integer, Integer> map;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        map = new HashMap<>();
        int n = preorder.length;
        for (int i = 0; i < n; i++) {
            map.put(inorder[i],i);
        }

        return builder(preorder, 0, n-1, inorder, 0, n-1);
    }
    public TreeNode builder(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
        if (preStart > preEnd) {
            return null;
        }

        int rootVal = preorder[preStart];
        int inIndex = map.get(rootVal);

        int leftSize = inIndex - inStart;

        TreeNode root = new TreeNode(rootVal);

        root.left = builder(preorder, preStart+1, preStart+leftSize, inorder, inStart, inIndex-1);
        root.right = builder(preorder, preStart+leftSize+1, preEnd, inorder, inIndex+1, inEnd);

        return root;
    }
}

6.后序和中序构造二叉树

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        int n = inorder.length;
        return builder(inorder, 0, n-1, postorder, 0, n-1);
    }
    public TreeNode builder(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {
        if (inStart > inEnd) {
            return null;
        }

        int rootVal = postorder[postEnd];
        int inIndex = 0;
        for (int i = inStart; i <= inEnd; i++) {
            if (inorder[i] == rootVal) {
                inIndex = i;
                break;
            }
        }

        int leftSize = inIndex - inStart;

        TreeNode root = new TreeNode(rootVal);

        root.left = builder(inorder, inStart, inIndex-1, postorder, postStart, postStart+leftSize-1);
        root.right = builder(inorder, inIndex+1, inEnd, postorder, postStart+leftSize, postEnd-1);

        return root;
    }
}
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private Map<Integer, Integer> map;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        int n = inorder.length;
        map = new HashMap<>();

        for (int i = 0; i < n; i++) {
            map.put(inorder[i], i);
        }

        return builder(inorder, 0, n-1, postorder, 0, n-1);
    }
    public TreeNode builder(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {
        if (inStart > inEnd) {
            return null;
        }

        int rootVal = postorder[postEnd];
        int inIndex = map.get(rootVal);

        int leftSize = inIndex - inStart;

        TreeNode root = new TreeNode(rootVal);

        root.left = builder(inorder, inStart, inIndex-1, postorder, postStart, postStart+leftSize-1);
        root.right = builder(inorder, inIndex+1, inEnd, postorder, postStart+leftSize, postEnd-1);

        return root;
    }
}

二、链表

(一)链表的六大技巧

1.合并两个有序链表

在这里插入图片描述

ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    // 虚拟头结点
    ListNode dummy = new ListNode(-1), p = dummy;
    ListNode p1 = l1, p2 = l2;

    while (p1 != null && p2 != null) {
        // 比较 p1 和 p2 两个指针
        // 将值较小的的节点接到 p 指针
        if (p1.val > p2.val) {
            p.next = p2;
            p2 = p2.next;
        } else {
            p.next = p1;
            p1 = p1.next;
        }
        // p 指针不断前进
        p = p.next;
    }

    if (p1 != null) {
        p.next = p1;
    }

    if (p2 != null) {
        p.next = p2;
    }

    return dummy.next;
}

注意下,不要更改题目原有节点指向。

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

2.合并K个有序链表

在这里插入图片描述

ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) return null;
    // 虚拟头结点
    ListNode dummy = new ListNode(-1);
    ListNode p = dummy;
    // 优先级队列,最小堆
    PriorityQueue<ListNode> pq = new PriorityQueue<>(
        lists.length, (a, b)->(a.val - b.val));
    // 将 k 个链表的头结点加入最小堆
    for (ListNode head : lists) {
        if (head != null)
            pq.add(head);
    }

    while (!pq.isEmpty()) {
        // 获取最小节点,接到结果链表中
        ListNode node = pq.poll();
        p.next = node;
        if (node.next != null) {
            pq.add(node.next);
        }
        // p 指针不断前进
        p = p.next;
    }
    return dummy.next;
}

优先队列pq中的元素个数最多是k,所以一次poll或者add方法的时间复杂度是O(logk);
所有的链表节点都会被加入和弹出pq,所以算法整体的时间复杂度是O(Nlogk)
其中k是链表的条数,N是这些链表的节点总数。

3.删除链表的倒数第K个节点

在这里插入图片描述

    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1, head);

        ListNode fast = dummy;
        ListNode slow = dummy;

        for (int cnt = 0; cnt < n; cnt++) {
            fast = fast.next;
        }

        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }

        slow.next = slow.next.next;

        return dummy.next;
    }

4.链表的中间节点

在这里插入图片描述

ListNode middleNode(ListNode head) {
    // 快慢指针初始化指向 head
    ListNode slow = head, fast = head;
    // 快指针走到末尾时停止
    while (fast != null && fast.next != null) {
        // 慢指针走一步,快指针走两步
        slow = slow.next;
        fast = fast.next.next;
    }
    // 慢指针指向中点
    return slow;
}

5.环形链表

在这里插入图片描述
每当慢指针slow前进一步,快指针fast就前进两步。

如果fast最终遇到空指针,说明链表中没有环;如果fast最终和slow相遇,那肯定是fast超过了slow一圈,说明链表中含有环。

boolean hasCycle(ListNode head) {
    // 快慢指针初始化指向 head
    ListNode slow = head, fast = head;
    // 快指针走到末尾时停止
    while (fast != null && fast.next != null) {
        // 慢指针走一步,快指针走两步
        slow = slow.next;
        fast = fast.next.next;
        // 快慢指针相遇,说明含有环
        if (slow == fast) {
            return true;
        }
    }
    // 不包含环
    return false;
}

6.环形链表起点

在这里插入图片描述
在这里插入图片描述
2(a+b) = a + b + n(b+c)
= a + (n+1)b + nc
推出:a = c + (n-1)(b+c)

public ListNode detectCycle(ListNode head) {
    
    ListNode fast = head;
    ListNode slow = head;

    ListNode node = null;

    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        
        if (fast == slow) {
            node = head;
            break;
        }
    }

    //if (fast != null && fast.next != null)也行
    if (node == null) {
        return null;
    }

    while (node != slow) {
        node = node.next;
        slow = slow.next;
    }

    return node;
}

7.相交链表

在这里插入图片描述

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    ListNode first = headA;
    ListNode second = headB;
    
    while (first != second) {
        first = first == null ? headB : first.next;
        second = second == null ? headA : second.next;
    }

    return first;
}

三、双指针

(一)、滑动窗口

模板:

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;

    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/

        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

1.最小覆盖子串

在这里插入图片描述

class Solution {
    public String minWindow(String s, String t) {
        char[] sChs = s.toCharArray();
        char[] tChs = t.toCharArray();

        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> windows = new HashMap<>();

        for (char ch : tChs) {
            need.put(ch, need.getOrDefault(ch, 0)+1);
        }

        int left = 0, right = 0;
        int valid = 0;
        int start = 0, len = Integer.MAX_VALUE;
        while (right < sChs.length) {
            char c = sChs[right];
            right++;
            if (need.containsKey(c)) {
                windows.put(c, windows.getOrDefault(c, 0)+1);
                if (Objects.equals(windows.get(c), need.get(c)))
                    valid++;
            }

            while (valid == need.size()) {
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                char k = sChs[left];
                left++;

                if (need.containsKey(k)) {
                    if (Objects.equals(windows.get(k), need.get(k))) {
                        valid--;
                    }
                    windows.put(k, windows.get(k)-1);
                }
            }
        }

        return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len);
    }
}

2.字符串的排列

在这里插入图片描述

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> windows = new HashMap<>();

        char[] s1Chs = s1.toCharArray();
        char[] s2Chs = s2.toCharArray();

        for (char ch : s1Chs) {
            need.put(ch, need.getOrDefault(ch,0) + 1);
        }

        int left = 0, right = 0;
        int valid = 0;

        while (right < s2Chs.length) {
            char c = s2Chs[right];
            right++;

            if (need.containsKey(c)) {
                windows.put(c, windows.getOrDefault(c,0) + 1);
                if (Objects.equals(windows.get(c), need.get(c))) {
                    valid++;
                }
            }

            while (right - left >= s1Chs.length) {
                if (valid == need.size()) {
                    return true;
                }
                char b = s2Chs[left];
                left++;

                if (need.containsKey(b)) {
                    if (Objects.equals(windows.get(b), need.get(b))) {
                        valid--;
                    }
                    windows.put(b, windows.get(b) - 1);
                }
            }
        }
        return false;
    }
}

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

在这里插入图片描述

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        char[] sChs = s.toCharArray();
        char[] pChs = p.toCharArray();

        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> windows = new HashMap<>();
        
        for (char ch : pChs) {
            need.put(ch, need.getOrDefault(ch, 0) + 1);
        }

        int left = 0, right = 0;
        int valid = 0;
        List<Integer> start = new LinkedList<>();
        
        while (right < sChs.length) {
            char c = sChs[right];
            right++;

            if (need.containsKey(c)) {
                windows.put(c, windows.getOrDefault(c, 0) + 1);
                if (Objects.equals(windows.get(c), need.get(c))) {
                    valid++;
                }
            }

            while (right - left >= pChs.length) {
                if (valid == need.size()) {
                    start.add(left);
                }

                char b = sChs[left];
                left++;

                if (need.containsKey(b)) {
                    if (Objects.equals(windows.get(b), need.get(b))) {
                        valid--;
                    }
                    windows.put(b, windows.get(b) - 1);
                }
            }
        }

        return start;
    }
}

4.最长无重复字串

在这里插入图片描述

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> window = new HashMap<>();
        char[] chs = s.toCharArray();

        int left = 0, right = 0;
        int max = 0;

        while (right < s.length()) {
            char c = chs[right];
            right++;
            window.put(c, window.getOrDefault(c, 0) + 1);

            while (window.get(c) > 1) {
                char b = chs[left];
                left++;
                window.put(b, window.get(b) - 1);
            }
            //在这里更新是重点
            max = Math.max(max, right-left);
        }
        return max;
    }
}

DP解法:

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length() == 0) {
            return 0;
        }
        int[] dp = new int[s.length()];
        dp[0] = 1;
        int max = 1;

        Map<Character, Integer> index = new HashMap<>();
        index.put(s.charAt(0), 0);

        for (int i = 1; i < s.length(); i++) {
            int j = index.getOrDefault(s.charAt(i), -1);
            index.put(s.charAt(i), i);

            int tmp = i - j;
            if (tmp > dp[i-1]) {
                dp[i] = dp[i-1] + 1;
            } else if (tmp <= dp[i-1]) {
                dp[i] = tmp;
            }

            max = Math.max(max, dp[i]);
        }

        return max;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值