算法集1

136 阅读13分钟

顶部

算法:数组

题目

121. 买卖股票的最佳时机

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0)
            return 0;
        int n =prices.length;
        int min_prices = prices[0];
        int max_profit = 0;

        
        for(int i=0;i<n;i++){
            max_profit = Math.max(max_profit,prices[i]-min_prices);
            min_prices = Math.min(min_prices,prices[i]);

        }
        return max_profit;
    }
}

169. 多数元素

class Solution {
    public int majorityElement(int[] nums) {
        int num = nums[0];
        int count = 1;
        for(int i = 1; i < nums.length; i++){
            if(nums[i] == num){
                count++;
            }else if(nums[i] != num){
                count--;
                if(count == 0){
                    count = 1;
                    num = nums[i];
                }
            }
        }
        return num;
    }
}

581. 最短无序连续子数组

class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int len = nums.length - 1;
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        int start = 0;
        int end = -1;

        for(int i = 0; i < nums.length; i++){
            //从左到右维持最大值,寻找右边界end
            if(nums[i] < max){
                end = i;
            }else{
                max = nums[i];
            }

            //从右到左维持最小值,寻找左边界start
            if(nums[len-i] > min){
                start = len - i;
            }else{
                min = nums[len-i];
            }

        }
        return end - start + 1;
    }
}

算法:链表

题目

2. 两数相加

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        int temp = 0;

        while(l1 != null || l2 != null || temp!=0){
            int x = l1 == null ? 0 : l1.val;
            int y = l2 == null ? 0 : l2.val;
            temp += x;
            temp += y;

            ListNode node = new ListNode(temp % 10);
            cur.next = node;
            cur = cur.next;

            temp /= 10;  

            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }
        return dummy.next;
        }
    }

160. 相交链表

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 解法一:遍历一遍A,再判断A中是否有B(Set有contains方法)
        Set<ListNode> set = new HashSet<>();
        while(headA != null){
            set.add(headA);
            headA = headA.next;
        }
        while(headB != null){
            if(set.contains(headB)){
                return headB;
            }
            headB = headB.next;
        }
        return null;
    }
}

206. 反转链表

掌握程度:★

思路:迭代

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode next = null;
        while(head != null){
            next = head.next;
            head.next = pre;
            pre = head;
            head = next;
            
        }
        return pre;
    }
}

234. 回文链表

class Solution {
    public boolean isPalindrome(ListNode head) {
        //边界判断
        if(head == null || head.next == null)
            return true;
        ListNode slow = head;
        ListNode fast = head;
        //快慢指针,找到中点,快指针速度是慢指针两倍,快指针走完,慢指针走完一半
        while(fast.next != null && fast.next.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }

        //12321,后半段反转后变为12
        ListNode right = reverseNode(slow.next);
        
        while(right != null && head !=null ){
            //要用&&不然会报空指针
            if(right.val != head.val )
                return false;
            else{
                right = right.next;
                head = head.next;
            }
            
        }
        return true;
    }
    
    
    public ListNode reverseNode(ListNode head){
        //别忘记边界条件
        if(head == null || head.next == null)
            return head;
        ListNode pre=null;
        ListNode next;
        while(head != null){
            //这里改为next会错,因为我把上面的next设置为空了......,所以这里填head好
            next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;   
    }   
}

21. 合并两个有序链表

思路:递归/迭代

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 思路:递归
        if(l1 == null) {
            return l2;
        }
        if(l2 == null) {
            return l1;
        }

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

    }
}

19. 删除链表的倒数第 N 个结点

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 思路:快慢指针、栈
        ListNode dummy = new ListNode(0, head);
        ListNode fast = dummy.next;
        ListNode slow = dummy;
        for(int i = 0; i < n; i++){
            fast = fast.next;
        }

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

        slow.next = slow.next.next;
        return dummy.next;

    }
}

141. 环形链表

public class Solution {
    public boolean hasCycle(ListNode head) {
        //边界条件
        if(head == null || head.next == null)
            return false;
        
        ListNode slow = head;
        ListNode fast = head;
        
        while(fast.next != null && fast.next.next != null){
        //fast可能下一个不是空,下下个为空就会报错
            slow = slow.next;
            fast = fast.next.next;
            
            if(slow == fast)
                return true;
                          
        }
        return false;
        
    }
}

142. 环形链表 II

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {     // 若存在环,快指针一定能追上慢指针
                slow = head;
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;    // 若没有环,返回 null 
    }
}

算法:二叉树

原理

思路:

1、遍历(traverse函数)还是分解?

2、前序(自上而下)还是后序(自下而上)?

1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。

无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做(二叉树前序位置是进入一个节点的时候,后序位置是离开一个节点的时候,理解这点很重要)

题目

104. 二叉树的最大深度

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null)
            return 0;
        
        int L = maxDepth(root.left);
        int R = maxDepth(root.right);
        return Math.max(L, R) + 1;
    }
}

543. 二叉树的直径

掌握程度:★★

思路:1、分解问题(根据子问题得到最后解)2、后序遍历(需要接收子树返回的信息)

class Solution {
    int max = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        int result = depth(root);
        return max;
    }
    public int depth(TreeNode root){
        if(root == null){
            return 0;
        }
        int L = depth(root.left);
        int R = depth(root.right);
        max = Math.max(L + R, max);
        return Math.max(L, R) + 1;     
    }
}

226. 翻转二叉树

掌握程度:★★

思路:1、分解问题;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;

    }  
}

617. 合并二叉树

掌握程度:★

思路:分解+前序

class Solution {
  
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        // 思路:分解+前序
        if(root1 == null){
            return root2;
        }
        if(root2 == null){
            return root2;
        }
        TreeNode root = new TreeNode(root1.val + root2.val);
        root.left = mergeTrees(root1.left, root2.left);
        root.right = mergeTrees(root1.right, root2.right);
        return root;
        
    }
}

构造类题目:

二叉树的构造:使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树

654. 最大二叉树

掌握程度:★

思路:分解+前序;单调栈思路未实现

class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        TreeNode root = build(nums, 0, nums.length-1);
        return root;

    }
    TreeNode build(int[] nums, int L, int R){
        if(L > R)
            return null;
        
        int max = -1;
        int index = 0;
        for(int i = L; i <= R; i++){
            if(max < nums[i]){
                max = nums[i];
                index = i;
            } 
        }
        TreeNode node = new TreeNode(max);

        node.left = build(nums, L, index-1);
        node.right = build(nums, index+1, R);
        return node;      
    }
}

101. 对称二叉树

class Solution {
    //思路一:先中序遍历再判断回文
    //思路二:递归 左树左子结点 == 右树右子节点 && 右树左子节点 == 左树右子节点
    public boolean isSymmetric(TreeNode root) {
        if(root == null)
            return true;
        return isSymmetric(root.left,root.right);
        
    }
    public boolean isSymmetric(TreeNode LNode,TreeNode RNode){
        if(LNode == null && RNode == null)
            //终止条件:左右子树都为空
            return true;
        if(LNode != null && RNode != null && LNode.val == RNode.val)
            return isSymmetric(LNode.left,RNode.right) && isSymmetric(LNode.right,RNode.left);
        return false;
    }
}

94. 二叉树的中序遍历

class Solution {
    List<Integer> list = new LinkedList();
    public List<Integer> inorderTraversal(TreeNode root) {
        
        if(root != null){         
            inorderTraversal(root.left);
            list.add(root.val);
            inorderTraversal(root.right);
        }
            return list;
    }
}

105. 从前序与中序遍历序列构造二叉树

掌握程度:★

思路:分解+前序,脑子里有两个数组,注意细节

image.png

class Solution {
    HashMap<Integer, Integer> map = new HashMap<>();

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        TreeNode root = build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
        return root;
        
    }
    TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){

        // 注意:结束条件
        if(preStart > preEnd)
            return null;
            
        int nodeVal = preorder[preStart];
        int index = map.get(nodeVal);
        
        // 注意:index - inStart 不是index - preStart
        int leftSize = index - inStart;

        TreeNode node = new TreeNode(nodeVal);
        node.left = build(preorder, preStart + 1, preStart + leftSize, inorder, inStart, index - 1);
        node.right = build(preorder, preStart + leftSize + 1, preEnd, inorder, index + 1, inEnd);
        return node;

    }
}

106. 从中序与后序遍历序列构造二叉树

掌握程度:★★

思路:和上一题类似

image.png

class Solution {
    HashMap<Integer, Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        TreeNode root = build(postorder, 0, postorder.length-1, inorder, 0, inorder.length-1);
        return root;
    }

    TreeNode build(int[] postorder, int postStart, int postEnd, int[] inorder, int inStart, int inEnd){

        // 注意:结束条件
        if(postStart > postEnd || inStart > inEnd)
            return null;
        int nodeVal = postorder[postEnd];
        int index = map.get(nodeVal);
        
        // 注意:index - inStart
        int leftSize = index - inStart;

        TreeNode node = new TreeNode(nodeVal);
        node.left = build(postorder, postStart, postStart + leftSize - 1, inorder, inStart, index-1);
        node.right = build(postorder, postStart + leftSize, postEnd - 1, inorder, index+1, inEnd);
        return node;
    }

}

889. 根据前序和后序遍历构造二叉树

暂时不做

掌握程度:

思路:

1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值

2、然后把前序遍历结果的第二个元素作为左子树的根节点的值

3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可

算法:二叉搜索树

原理

BST 相关的问题,要么利用 BST 左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求。

题目

230. 二叉搜索树中第K小的元素

思路:利用中序遍历的思路

class Solution {
    int res = 0;
    int rank = 0;
    public int kthSmallest(TreeNode root, int k) {
        traverse(root, k);
        return res;
        
    }
    public void traverse(TreeNode root, int k){
        if(root == null)
            return;

        traverse(root.left, k);
        rank++;
        if(k == rank){
            res = root.val;
            return;
        }
        traverse(root.right, k);
    }
}

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

思路:利用逆中序遍历

class Solution {
    int sum = 0;
    public TreeNode convertBST(TreeNode root) {
        traverse(root);
        return root;
    }
    public void traverse(TreeNode root){
        if(root == null)
            return;
        
        traverse(root.right);
        sum += root.val;
        root.val = sum;
        traverse(root.left);
    }
}

98. 验证二叉搜索树

思路:验证BST的合法性

class Solution {
    boolean isValidBST(TreeNode root) {
        return isValidBST(root, null, null);
}

    /* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
    boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
        // base case
        if (root == null) return true;
        // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
        if (min != null && root.val <= min.val) return false;
        if (max != null && root.val >= max.val) return false;
        // 限定左子树的最大值是 root.val,右子树的最小值是 root.val
        return isValidBST(root.left, min, root) 
            && isValidBST(root.right, root, max);
    }

}

参考题解:labuladong.github.io/algo/2/21/4…

BST查找

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null) {
            return null;
        }
        // 去左子树搜索
        if (root.val > val) {
            return searchBST(root.left, val);
        }
        // 去右子树搜索
        if (root.val < val) {
            return searchBST(root.right, val);
        }
        return root;
    }
}

BST增加

TreeNode insertIntoBST(TreeNode root, int val) {
    // 找到空位置插入新节点
    if (root == null) return new TreeNode(val);
    // if (root.val == val)
    //     BST 中一般不会插入已存在元素
    if (root.val < val) 
        root.right = insertIntoBST(root.right, val);
    if (root.val > val) 
        root.left = insertIntoBST(root.left, val);
    return root;
}

BST删除

TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    if (root.val == key) {
        // 这两个 if 把情况 1 和 2 都正确处理了
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        // 处理情况 3
        // 获得右子树最小的节点
        TreeNode minNode = getMin(root.right);
        // 删除右子树最小的节点
        root.right = deleteNode(root.right, minNode.val);
        // 用右子树最小的节点替换 root 节点
        minNode.left = root.left;
        minNode.right = root.right;
        root = minNode;
    } else if (root.val > key) {
        root.left = deleteNode(root.left, key);
    } else if (root.val < key) {
        root.right = deleteNode(root.right, key);
    }
    return root;
}

TreeNode getMin(TreeNode node) {
    // BST 最左边的就是最小的
    while (node.left != null) node = node.left;
    return node;
}

96.不同的二叉搜索树

class Solution {
    // 备忘录,解决重叠子问题
    int [][] memo;
    public int numTrees(int n) {
        memo = new int[n+1][n+1];
        return count(1, n);
    }
    public int count(int low, int high){
        if(low > high)
            return 1;
        if(memo[low][high] != 0){
            return memo[low][high];
        }
        int res = 0;
        // mid 为根节点
        for(int mid = low; mid <= high; mid++){
            int left = count(low, mid - 1);
            int right = count(mid + 1, high);
            res += left * right;
        }
        memo[low][high] = res;
        return res;
    }
}

95. 不同的二叉搜索树 II

不是很能理解

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

    public List<TreeNode> build(int low, int high){
        List<TreeNode> res = new LinkedList<>();
        if(low > high){
            res.add(null);
            return res;
        }
        // 1、穷举root节点
        for(int i = low; i <= high; i++){
            // 2、递归构造左右子树的合法BST
            List<TreeNode> leftTree = build(low, i-1);
            List<TreeNode> rightTree = build(i+1, high);
            // 3、给root节点穷举所有左右子树的组合
            for(TreeNode left : leftTree){
                for(TreeNode right : rightTree){
                    // i作为根节点
                    TreeNode root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    res.add(root);
                }
            } 
        }
        return res;
    }
}

算法:字符串

题目

14. 最长公共前缀

class Solution {
    public String longestCommonPrefix(String[] strs) {
        String res = strs[0];

        for(int i = 1; i < strs.length; i++){
            int j = 0;
            for(; j < res.length() && j < strs[i].length(); j++){       
                if(res.charAt(j) != strs[i].charAt(j)){
                    break;
                }
            }
            res = res.substring(0, j);
            
            // 有一个不符合就直接退出
            if(res.equals(""))
                return "";
        }
        return res;

    }
}

165. 比较版本号

思路:分割字符串,逐个元素比较

class Solution {
    public int compareVersion(String version1, String version2) {
        String[] str1 = version1.split("\\.");
        String[] str2 = version2.split("\\.");
        for(int i = 0; i < str1.length || i < str2.length; i++){
            int x = 0;
            int y = 0;
            if(i < str1.length)
                x = Integer.parseInt(str1[i]);
            if(i < str2.length)
                y = Integer.parseInt(str2[i]);
            if(x < y)
                return -1;
            else if (x > y)
                return 1;
        }
        return 0;
    }
}

179. 最大数

思路:整型数组转化为字符串数组,对字符串数组排序,(b + a).compareTo(a + b))降序排序,和s2-s1类似。注意元素都为0时直接返回0,不拼接。

class Solution {
    public String largestNumber(int[] nums) {
        int n = nums.length;
        String[] toStr = new String[n];
        for (int i = 0; i < n; i++){
            toStr[i] = String.valueOf(nums[i]);
        }
        Arrays.sort(toStr, (a, b) -> (b + a).compareTo(a + b));

        if(toStr[0].equals("0"))
            return "0";

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

168. Excel表列名称

十进制转化为其他进制

class Solution {
    public String convertToTitle(int cn) {
        StringBuilder sb = new StringBuilder();
        while (cn > 0) {
            cn--;  // 解决从'B'开始算的问题
            sb.append((char)(cn % 26 + 'A'));  // 一直取余,从右往左算,所以sb要reverse
            cn /= 26;
        }
        sb.reverse();
        return sb.toString();
    }
}

171. Excel 表列序号

其他进制转换为十进制

class Solution {
    public int titleToNumber(String columnTitle) {
        int ans = 0;
        int num = 0;
        for(int i = 0; i < columnTitle.length(); i++){
            num = columnTitle.charAt(i) - 'A' + 1;
            ans = ans * 26 + num;
        }
        return ans;
    }
}

394.字符串编码

难。自己写不出来

public String decodeString(String s) {
        // 参考题解:https://leetcode.cn/problems/decode-string/solutions/19447/decode-string-fu-zhu-zhan-fa-di-gui-fa-by-jyd/
        
        // 1. 初始化倍数和res 及其对应栈
        int multi = 0;
        StringBuilder res = new StringBuilder();
        Deque<Integer> multi_stack = new LinkedList<>();
        Deque<String> res_stack = new LinkedList<>();

        // 2. 遍历字符
        char[] chars = s.toCharArray();
        for (char ch : chars) {
            // 3. 统计倍数
            if (ch >= '0' && ch <= '9')
                multi = multi * 10 + (ch - '0');
            // 4. 统计res
            else if (ch >= 'a' && ch <= 'z') 
                res.append(ch);
            // 5. 入栈并重置临时变量
            else if (ch == '[') {
                multi_stack.push(multi);
                res_stack.push(res.toString());
                // 重置开始下一轮重新统计
                multi = 0;
                res = new StringBuilder();
            // 6. 出栈做字符串乘法和加法
            } else {
                int cur_multi = multi_stack.pop();
                StringBuilder temp = new StringBuilder();
                // 乘以当前统计字符串res
                for (int i = 0; i < cur_multi; i++)
                    temp.append(res);
                // 加上前一个统计字符串作为当前res
                res = new StringBuilder(res_stack.pop() + temp);
            }
        }
        return res.toString();
    }

算法:哈希

题目

1. 两数之和

class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 思路:哈希存target-nums[i]的值
        HashMap<Integer,Integer> map= new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            if(map.containsKey(nums[i])){
                return new int[]{map.get(nums[i]),i};
            }
            map.put(target-nums[i],i);
        }
        
        return null;
    }
}

12. 整数转罗马数字

class Solution {
    public String intToRoman(int num) {
        int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
        String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
        StringBuffer str = new StringBuffer();

        for(int i = 0; i < values.length; i++){

            int value = values[i];
            String symbol = symbols[i];

            while(num >= value){
                num -= value;
                str.append(symbol);
            }
        }
        return str.toString();
    }
}

13. 罗马数字转整数

class Solution {
    public int romanToInt(String s) {
        //类型是Character
        Map<Character,Integer> map = new HashMap<>();
        //细节:要用单引号
        map.put('I',1);
        map.put('V',5);
        map.put('X',10);
        map.put('L',50);
        map.put('C',100);
        map.put('D',500);
        map.put('M',1000);
        
        int res = 0;
        int n = s.length();

        // 思路:右边比左边大就减
        for(int i = 0; i < n; i++){

            int value = map.get(s.charAt(i));

            // 最后一个不用判断,直接加
            if(i < n - 1 && value < map.get(s.charAt(i + 1))){
                res -= value;
            }else{
                res += value;
            }
        }

        return res;
        
    }
}

219.存在重复元素 II

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            if(map.containsKey(nums[i]) && Math.abs(i - map.get(nums[i])) <= k){
                return true;
            }else{
                map.put(nums[i], i);
            }
        }
        return false;
    }
}

算法:排序

原理

排序

题目

15. 三数之和

思路:排序+双指针逼近+去重

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if(nums.length == 0)
            return res;
        Arrays.sort(nums);

        for(int i=0;i<nums.length;i++){
            // i去重
            if(i>0 &&nums[i] == nums[i-1]) 
                continue;
            int target = -nums[i];
            int j = i + 1;
            int k = nums.length - 1;
            while(j < k){
                // j k 去重
                if(nums[j] + nums[k] == target){
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[j]);
                    list.add(nums[k]);
                    res.add(list);
                
                j++;
                k--;
                while(j<k && nums[j] == nums[j-1])
                    j++;
                while(k>j && nums[k] == nums[k+1])
                    k--;
                }
                else if(nums[j] + nums[k] > target)
                    k--;
                else
                    j++;
        
            }
        }
        return res;
    }
}

16. 最接近的三数之和

思路:排序+双指针逼近

class Solution {
    public int threeSumClosest(int[] nums, int target) {
        Arrays.sort(nums);
        // 最小的了
        int count = nums[0] + nums[1] + nums[2];
        for(int i = 0; i < nums.length; i++){
            int j = i+1;
            int k = nums.length-1;
            while(j < k){
                int sum = nums[i] + nums[j] + nums[k];
                // 看离target的距离多远
                if(Math.abs(sum-target) < Math.abs(count-target))
                    count = sum;
                else if(sum < target)
                    j++;
                else if(sum > target)
                    k--;  
                else
                    // count最小,比count大直接返回count
                    return count;
            }
        }
        return count;
    }
}

49. 字母异位词分组

思路:对字符串进行排序,然后作为map的键,将键相同的放在一起

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for(String str : strs){
            char[] array = str.toCharArray();
            // 排序
            Arrays.sort(array);
            String s = new String(array);
            List<String> list = map.getOrDefault(s, new ArrayList<>());
            list.add(str);
            map.put(s, list);
        }
        return new ArrayList<List<String>>(map.values());
    }
}

56. 合并区间

思路:区间左端点排序,判断是否相交

class Solution {
    public int[][] merge(int[][] intervals) {
         List<int[]> res = new ArrayList<int[]>();
         if (intervals.length == 0 )  return new int[0][0];
         
         // 排序区间
         Arrays.sort(intervals,(x,y) -> x[0] - y[0]); 

         int l = intervals[0][0], r = intervals[0][1];
         for(int i = 1; i < intervals.length; i ++)
         {
             // 不重叠,则把前一个添加,然后移动指针
             if(intervals[i][0] > r)
             {
                 res.add(new int[]{l, r});
                 l = intervals[i][0];
                 r = intervals[i][1];
             }
             else
             {
                // 相交,即第二个左边小于第一个右边,取右边最大的
                 r = Math.max(r, intervals[i][1]);
             }
         }

         // 必须添加最后一个,否则会漏掉
         res.add(new int[]{l,r});      
         return res.toArray(new int[res.size()][]);
    } 

}

算法:双指针

原理

数组问题中比较常见的快慢指针技巧,是让你原地修改数组

双指针有快慢、两边往中间靠两种

题目

11. 盛最多水的容器

class Solution {
    public int maxArea(int[] height) {
        if(height.length == 0) return 0;
        int area = 0;
        int i = 0;
        int j = height.length-1;
        while(i < j){
            area = Math.max(area, (j-i)*Math.min(height[i],height[j]));
            if(height[i] < height[j])
                i++;
            else
                j--;
        }
        return area;
    }
}

283. 移动零

class Solution {
    public void moveZeroes(int[] nums) {
        // 思路:双指针,非零元素往前移,最后补0
        // slow记录非零个数,fast遇到0跳过,非0时将值给slow
        int slow = 0;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] != 0){
                nums[slow] = nums[i];
                slow++;
            }
        }
        for(int i = slow; i < nums.length; i++){
            nums[i] = 0;
        }
    }
}

算法:滑动窗口

原理

子串问题用滑动窗口解决。

滑动的窗口以长的字符串进行滑动,然后判断是否包含子串,进行增增减减。

滑动窗口分定长窗口和不定长窗口。

模板:

//模板
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    Map<Character, Integer> need = new HashMap<>();
    Map<Character, Integer> window = new HashMap<>();
    for (char c : t.toCharArray()) 
        need.put(c,need.getOrDefault(c,0)+1);
	int left = 0, right = 0;
	int count = 0; 
	while (right < s.size()) {
    	// c 是将移入窗口的字符
   	 	char inChar = s.charAt(right);
    	// 右移窗口
    	right++;
    	// 进行窗口内数据的一系列更新
    	...

    	/*** debug 输出的位置 ***/
    	System.out.println("window: ["+left+","+ right+")");
    	/********************/
    
    	// 判断左侧窗口是否要收缩
    	while (window needs shrink) {
        	// d 是将移出窗口的字符
        	char outChar = s[left];
        	// 左移窗口
        	left++;
        	// 进行窗口内数据的一系列更新
        	...
    	}
    }
}

题目

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

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if(s.length() == 0) return 0;
        Map<Character, Integer> map = new HashMap();
        int max = 0;
        int left = 0;
        for(int i = 0; i < s.length(); i++){

            // 如果包含,map中的字符索引+1,然后取left最大值,更新左窗口
            if(map.containsKey(s.charAt(i)))
                left = Math.max(left, map.get(s.charAt(i)) + 1);

            // 无论如何,更新一下字符当前索引,即更新右窗口
            map.put(s.charAt(i), i);

            // i - left + 1为窗口大小
            max = Math.max(max, i-left+1);
        }
        return max;
    }
    
}

76. 最小覆盖子串

体会:还是蛮难的,细节太多,重新做一遍不一定能完整做出来,也归结自己理解不透彻

class Solution {
    public String minWindow(String s, String t) {
        HashMap<Character, Integer> need = new HashMap<>();
        HashMap<Character, Integer> window = new HashMap<>();

        // 填充need
        for(char word : t.toCharArray()){
            need.put(word, need.getOrDefault(word, 0)+1);
        }

        int left = 0, right = 0;
        int count = 0; // 记录窗口满足need的字符个数
        int start = 0;
        int len = Integer.MAX_VALUE;
        while(right < s.length()){
            char word = s.charAt(right);
            right++;
            if(need.containsKey(word)){ // 并不需要将s中所有的都加入window,只需要将need中的加入即可
                window.put(word, window.getOrDefault(word, 0)+1);
                if(window.get(word).equals(need.get(word))){
                    count++;   
                }
            }

            while(count == need.size()){ // 已包含所有的字符,现开始收缩
                if(right - left < len){ // 记录最短的子串的起始位序start
                    len = right - left;
                    start = left;
                }
               
               // 以下步骤和扩张窗口的代码类似
                char c = s.charAt(left);
                left++;
                if(need.containsKey(c)){  // need包含s[left]的字符,则可以继续往下走
                    if(window.get(c).equals(need.get(c))){
                        count--; // 满足字符之后收缩一个字符,则count-1
                    }
                    window.put(c, window.get(c)-1);  // 收缩window
                }
            }
        }
        return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len);
    } 
}

239. 滑动窗口最大值

import java.util.*; 
class Solution {

    public int[] maxSlidingWindow(int[] nums, int k) {
        MonotonicQueue window = new MonotonicQueue();
        List<Integer> res = new ArrayList<>();
        for(int i = 0; i < nums.length; i++){
            if(i < k - 1){
                window.push(nums[i]);  // 填充window前k-1个
            }else{ // 开始收缩
                window.push(nums[i]);
                res.add(window.max());
                // 窗口的前k-1个,i-(k-1)即窗口第一个的索引
                //粗心:nums[i - k + 1]写成i - k + 1
                window.pop(nums[i - k + 1]);
            }
        }
        return res.stream().mapToInt(i->i).toArray(); // List<Integer>转int数组
    }
}

// 单调队列实现
class MonotonicQueue {
    LinkedList<Integer> maxQueue = new LinkedList<>();
    public void pop(int n){
        if(n == maxQueue.getFirst()){
            maxQueue.pollFirst();
        }
    }
    public int max(){
        return maxQueue.getFirst();
    }
    public void push(int n){
        while(!maxQueue.isEmpty() && maxQueue.getLast() < n){
            maxQueue.pollLast();
        }
        maxQueue.addLast(n);
    }
}

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

题目同76

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        HashMap<Character, Integer> need = new HashMap<>();
        HashMap<Character, Integer> window = new HashMap<>();
        List<Integer> res = new ArrayList<>();

        // 填充need
        for(char word : p.toCharArray()){
            need.put(word, need.getOrDefault(word, 0)+1);
        }

        int left = 0, right = 0;
        int count = 0;
 
        while(right < s.length()){
            char word = s.charAt(right);
            right++;
            if(need.containsKey(word)){
                window.put(word, window.getOrDefault(word, 0)+1);
                if(window.get(word).equals(need.get(word))){
                    count++;   
                }
            }

            while(right - left == p.length()){  //只有在right - left == need.size()的情况下才有可能有符合条件的解
                if(count == need.size())
                    res.add(left); // 满足条件则添加

                char c = s.charAt(left);
                left++;
                if(need.containsKey(c)){  
                    if(window.get(c).equals(need.get(c))){
                        count--; 
                    }
                    window.put(c, window.get(c)-1); 
                }
            }
        }
        return res;
    }
}

567. 字符串的排列

题目同76

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

        // 填充need
        for(char word : s1.toCharArray()){
            need.put(word, need.getOrDefault(word, 0)+1);
        }

        int left = 0, right = 0;
        int count = 0;
 
        while(right < s2.length()){
            char word = s2.charAt(right);
            right++;
            if(need.containsKey(word)){
                window.put(word, window.getOrDefault(word, 0)+1);
                if(window.get(word).equals(need.get(word))){
                    count++;   
                }
            }

            while(right - left == s1.length()){  //只有在right - left == need.size()的情况下才有可能有符合条件的解
                if(count == need.size())
                    return true; // 满足条件则返回

                char c = s2.charAt(left);
                left++;
                if(need.containsKey(c)){  
                    if(window.get(c).equals(need.get(c))){
                        count--; 
                    }
                    window.put(c, window.get(c)-1); 
                }
            }
        }
        return false;
    }
}

1004. 最大连续1的个数 III

class Solution {
    public int longestOnes(int[] nums, int k) {
        int left = 0;
        int right = 0;
        int n = nums.length;
        int sum = 0;
        int res = 0;
        
        // 右指针右移
        while(right < n){
            if(nums[right] == 0){
                sum++;
            }
            // 条件不满足左指针右移
            while(sum > k){
                if(nums[left] == 0){
                    sum--; 
                }
                left++;
            }
            res = Math.max(res, right - left + 1);
            right++;
        }
        return res;
    }
}

算法:单调队列

239. 滑动窗口最大值

import java.util.*;
class Solution {

    public int[] maxSlidingWindow(int[] nums, int k) {
        MonotonicQueue window = new MonotonicQueue();
        List<Integer> res = new ArrayList<>();
        for(int i = 0; i < nums.length; i++){
            if(i < k - 1){
                window.push(nums[i]);
            }else{
                window.push(nums[i]);
                res.add(window.max());
                // 窗口的前k-1个,i-(k-1)即窗口第一个的索引
                //粗心:nums[i - k + 1]写成i - k + 1
                window.pop(nums[i - k + 1]);
            }
        }
        return res.stream().mapToInt(i->i).toArray();
    }
}

// 单调队列实现
class MonotonicQueue {
    LinkedList<Integer> maxQueue = new LinkedList<>();
    public void pop(int n){
        if(n == maxQueue.getFirst()){
            maxQueue.pollFirst();
        }
    }
    public int max(){
        return maxQueue.getFirst();
    }
    public void push(int n){
        while(!maxQueue.isEmpty() && maxQueue.getLast() < n){
            maxQueue.pollLast();
        }
        maxQueue.addLast(n);
    }
}

算法:二分查找

场景

注意:

right = nums.length - 1

while(left <= right)

模板:

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

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 最后要检查 left 越界的情况
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

题目

33. 搜索旋转排序数组

class Solution {
    public int search(int[] nums, int target) {
        int l = 0;
        int r = nums.length-1;
        int num = nums[0];

        while(l <= r){
            int mid = l + (r - l)/2;

            if(nums[mid] == target)
                return mid;

            // 中间比第一个大,则在左半边二分
            if(nums[mid] >= num){
                if(num <= target && target < nums[mid])
                    r = mid - 1;
                else
                    l = mid + 1;
            }

            // 中间比第一个小,则在右半边二分
            else if(nums[mid] < num){
                if(nums[mid] < target && target <= nums[r])
                    l = mid + 1;
                else
                     r = mid - 1;
            }
        }
    return -1;
    }
}

34. 在排序数组中查找元素的第一个和最后一个位置

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] res = new int[2];
        if(nums.length == 0) return new int[]{-1,-1};
        res[0] = leftSearch(nums,target);
        res[1] = rightSearch(nums,target);
        return res;
    }
    // 往左找
    public static int leftSearch(int[] nums, int target){
        int l = 0;
        int r = nums.length-1;

        while(l <= r){
            int mid = l + (r - l)/2;
            if(nums[mid] >= target)
                r = mid - 1;
            else if(nums[mid] < target)
                l = mid + 1;
        }

        // 注意:target小,right左移,虽然r < 0,只要nums[l] != target 就能返回-1
        //       target大,left右移,l > l >= nums.length,返回-1

        if(l >= nums.length || nums[l] != target) return -1;
        return l;

    }
    	// 往右找
    public static int rightSearch(int[] nums, int target){
        int l = 0;
        int r = nums.length - 1;
        while(l <= r){
            int mid = l + (r - l)/2;
            if(nums[mid] <= target)
                l = mid + 1;
            else
                r = mid - 1;
        }

        // 注意:target小,right左移,r < 0,返回-1
        //       target大,left右移,只要nums[r] != target,返回-1

        if(r < 0 || nums[r] != target) return -1;
        return r;
    }
}

240. 搜索二维矩阵 II

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        for(int[] nums : matrix){
            int res = binaraySearch(nums, target);
            if(res >= 0){
                return true;
            }
        }
        return false;   
    }
    public int binaraySearch(int[] nums, int target){

        // 注意: int right = nums.length - 1
        
        int left = 0;
        int right = nums.length - 1;

        // 注意:left <= right

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

}

287. 寻找重复数

class Solution {

    public int findDuplicate(int[] nums) {
        int len = nums.length; // n + 1 = len, n = len - 1

        // 在 [1..n] 查找 nums 中重复的元素
        int left = 1;
        int right = len - 1;
        while (left < right) {
            int mid = (left + right) / 2;

            // nums 中小于等于 mid 的元素的个数
            int count = 0;
            for (int num : nums) {
                if (num <= mid) {
                    count++;
                }
            }
            if (count > mid) {
                // 下一轮搜索的区间 [left..mid]
                right = mid;
            } else {
                // 下一轮搜索的区间 [mid + 1..right]
                left = mid + 1;
            }
        }
        return left;
    }
}

算法:分治

场景

待补充...

题目

23. 合并K个升序链表

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) return null;
        return merge(lists, 0, lists.length - 1);
    }

    private ListNode merge(ListNode[] lists, int left, int right) {
        if (left == right) return lists[left];
        int mid = left + (right - left) / 2;
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid + 1, right);
        return mergeTwoLists(l1, l2);
    }

    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 合并两个链表
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1,l2.next);
            return l2;
        }
    }
}

215. 数组中的第K个最大元素

思路:求第K最大,即将数组逆序,求第K大

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int low = 0;
        int high = nums.length - 1;
        while (true) {
            int index = partition(nums, low, high);
            if (index == k - 1) {
                return nums[index];
            }
            else if (index < k - 1) {
                low = index + 1;
            }
            else {
                high = index - 1;
            }
        }
    }
    public int partition(int[] nums, int low, int high){
        // 注意:降序排序
        int pivot = nums[low];
        while (low < high) {
            while (low < high && nums[high] <= pivot) {
                high--;
            }
            nums[low] = nums[high];
            while (low < high && nums[low] >= pivot) {
                low++;
            }
            nums[high] = nums[low];
        }
        nums[low] = pivot;
        return low;
    }
}

算法:递归

题目

395. 至少有 K 个重复字符的最长子串

class Solution {
    public int longestSubstring(String s, int k) {
        //首先可以确定的一点就是这题滑动窗口不好用,因为我们无法判断是否在没遍历完之前,这个字符是不是小于k的。
        if(s.length() < k) 
            return 0;

        HashMap<Character, Integer> map = new HashMap<>();
        for(int i = 0; i < s.length(); i++){
            map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0)+1);
        }
        for(char word : map.keySet()){
            if(map.get(word) < k){
                int res = 0;
                for(String str : s.split(String.valueOf(word))){  // 注意char转化为string:String.valueOf(word)
                    res = Math.max(res, longestSubstring(str, k));
                }
                return res; // 遍历完,返回最大长度
            }
        }
        return s.length();
    }
}

337. 打家劫舍 III

 // 参考题解:https://leetcode.cn/problems/house-robber-iii/solutions/47828/san-chong-fang-fa-jie-jue-shu-xing-dong-tai-gui-hu/
class Solution {
    public int rob(TreeNode root) {
        HashMap<TreeNode, Integer> memo = new HashMap<>();
        return robInternal(root, memo);
    }

    public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
        if (root == null) return 0;

        // 避免重复计算 
        if (memo.containsKey(root)) return memo.get(root);
        int money = root.val;

        if (root.left != null) {
            money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
        }
        if (root.right != null) {
            money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
        }
        int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
        memo.put(root, result);
        return result;
    }
}

139.单词拆分

思路:备忘录防止冗余计算

class Solution {
    Set<String> wordDict;
    int[] memo;
    public boolean wordBreak(String s, List<String> wordDict) {
        // 注意:这要要用this.wordDict而不是wordDict
        this.wordDict = new HashSet<>(wordDict);
        // 注意:这里是s.length()不是wordDict.size()
        this.memo = new int[s.length()];
        Arrays.fill(memo, -1);
        return dp(s, 0);  
    }

    public boolean dp(String s, int index){
         // base case
        if(index == s.length()){
            return true;
        }
        // 防止冗余计算
        if(memo[index] != -1){
            return memo[index] == 0 ? false : true;
        }
        for(int len = 1; index+len <= s.length(); len++){
            String prefix = s.substring(index, index+len);
            if(wordDict.contains(prefix)){
                // 注意:这里是dp(s, index+len)不是dp(prefix, index+len)
                boolean subProblem = dp(s, index+len);
                if(subProblem == true){
                    memo[index] = 1;
                    return true;
                }
                
            }
        }
        memo[index] = 0;
        return false;
    }
}

算法:前缀和

场景

遇到「区间和」问题使用前缀和

适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和

这个前缀和数组 preSum 的含义也很好理解,preSum[i] 就是 nums[0..i-1] 的和。那么如果我们想求 nums[i..j] 的和,只需要一步操作 preSum[j+1]-preSum[i] 即可,而不需要重新去遍历数组了。

模板:

class PrefixSum {
    // 前缀和数组
    private int[] prefix;
    
    /* 输入一个数组,构造前缀和 */
    public PrefixSum(int[] nums) {
        // 注意前缀和要申请n+1个空间
        prefix = new int[nums.length + 1];
        // 计算 nums 的累加和
        for (int i = 0; i < prefix.length; i++) {
            // 
            prefix[i+1] = prefix[i] + nums[i];
        }
    }

    /* 查询闭区间 [i, j] 的累加和 */
    public int query(int i, int j) {
        return prefix[j + 1] - prefix[i];
    }
}

题目

560. 和为 K 的子数组

思路:前缀和+哈希,哈希的形式和两数之和类似

public class Solution {
    public int subarraySum(int[] nums, int k) {
        // 思路:前缀和+哈希
        int count = 0, pre = 0;
        HashMap < Integer, Integer > mp = new HashMap < > ();
        
        //  注意:这里不好理解
        mp.put(0, 1);
        for (int i = 0; i < nums.length; i++) {
            pre += nums[i];
            if (mp.containsKey(pre - k)) {
                count += mp.get(pre - k);
            }
            mp.put(pre, mp.getOrDefault(pre, 0) + 1);
        }
        return count;
    }
}

303. 区域和检索 - 数组不可变

public class _303区域和数组 {
    int[] sum;
    public void NumArray(int[] nums) {
        int n = nums.length;
        sum = new int[n+1];
        for(int i = 0; i < n; i++){
        // 求前缀和
            sum[i+1] = sum[i] + nums[i];
        }
    }

    public int sumRange(int left, int right) {
        return sum[right+1] - sum[left];
    }
}

304. 二维区域和检索 - 矩阵不可变

思路:二维的前缀和

class NumMatrix {
    int[][] sums;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length;
        if (m > 0) {
            int n = matrix[0].length;
            sums = new int[m + 1][n + 1];
            // 注意:从0开始
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                // 求前缀和
                    sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
                }
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
    }
}

209. 长度最小的子数组

前缀和用来确定左边界,二分查找寻找右边界

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int[] sums = new int[n+1];
        int ans = Integer.MAX_VALUE;
        // 注意:从1开始,求前缀和
        for(int i = 1; i <= n; i++){
            sums[i] = sums[i-1] + nums[i-1];
        }
        for(int i = 1; i <= n; i++){
            int value = target + sums[i-1];
            int bound = Arrays.binarySearch(sums, value);
            if(bound < 0){
                bound = -bound - 1;
            }  
            // 注意:判断条件不能直接else
            if(bound <= n){
                ans = Math.min(ans, bound-(i-1));
            } 
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}

算法:差分

场景

差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。

步骤:

  1. 先求差分数组
  2. 对差分数组求前缀和

差分工具类包含:

  1. 传入一个原始数组,构造函数构造一个差分数组
  2. 差分数组d[i]加val,区间d[j+1]减val
  3. 叠加返回数组结果

题目

1109. 航班预订统计

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] nums = new int[n];
        Difference d = new Difference(nums);
        for(int[] booking : bookings){
            int i = booking[0] - 1;
            int j = booking[1] - 1;
            int val = booking[2];
            d.increment(i, j, val);
        }
        return d.result();
    
    }
}

// 差分工具类
class Difference {
    private int[] diff;

    Difference(int[] nums) {
        diff = new int[nums.length];
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
    }

    public void increment(int i, int j, int val) {
        // 差分数组d[i]加val,区间d[j+1]减val
        diff[i] += val;
        if (j + 1 < diff.length) {
            diff[j + 1] -= val;
        }
    }

    public int[] result() {
        int[] res = new int[diff.length];
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {
            res[i] = res[i - 1] + diff[i];
        }
        return res;

    }
}

1094. 拼车

class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
        int[] nums = new int[1001];
        Differences d = new Differences(nums);
        for (int[] trip : trips) {
            // 下标不需要减一!细节是魔鬼
            int i = trip[1];
            int j = trip[2] - 1;
            int val = trip[0];
            d.increment(i, j, val);
        }
        return d.result(capacity);
    }
}

// 查分工具类
class Differences {
    public int[] diff;
    Differences(int[] nums) {
        diff = new int[nums.length];
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
    }

    public void increment(int i, int j, int val) {
        // 差分数组d[i]加val,区间d[j+1]减val
        diff[i] += val;
        if (j+1 < diff.length) {
            diff[j+1] -= val;
        }
    }

    public boolean result(int capacity) {
        int[] res = new int[diff.length];
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {
            res[i] = res[i - 1] + diff[i];
        }
        for (int i = 0; i < diff.length; i++) {
            if (res[i] > capacity)
                return false;
        }
        return true;

    }
}

算法:单调栈

参考博客:labuladong.gitee.io/algo/2/23/6…

场景

单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。

题目

739. 每日温度

思路:单调栈,从后往前遍历,遇到比栈顶大的,则弹出,再将较大值压入栈。

单调栈模板也如下:


int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] res = new int[n];
    // 这里放元素索引,而不是元素
    Stack<Integer> s = new Stack<>(); 
    /* 单调栈模板 */
    for (int i = n - 1; i >= 0; i--) {
        while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
            s.pop();
        }
        // 得到索引间距
        res[i] = s.isEmpty() ? 0 : (s.peek() - i); 
        // 将索引入栈,而不是元素
        s.push(i); 
    }
    return res;
}

496. 下一个更大元素 I

思路:单调栈

int[] nextGreaterElement(int[] nums) {
    int n = nums.length;
    // 存放答案的数组
    int[] res = new int[n];
    Stack<Integer> s = new Stack<>(); 
    // 倒着往栈里放
    for (int i = n - 1; i >= 0; i--) {
        // 判定个子高矮
        while (!s.isEmpty() && s.peek() <= nums[i]) {
            // 矮个起开
            s.pop();
        }
        // nums[i] 身后的更大元素
        res[i] = s.isEmpty() ? -1 : s.peek();
        s.push(nums[i]);
    }
    return res;
}

int[] nextGreaterElement(int[] nums1, int[] nums2) {
    // 记录 nums2 中每个元素的下一个更大元素
    int[] greater = nextGreaterElement(nums2);
    
    // 转化成映射:元素 x -> x 的下一个最大元素
    HashMap<Integer, Integer> greaterMap = new HashMap<>();
    
    for (int i = 0; i < nums2.length; i++) {
        greaterMap.put(nums2[i], greater[i]);
    }
    // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果
    int[] res = new int[nums1.length];
    for (int i = 0; i < nums1.length; i++) {
        res[i] = greaterMap.get(nums1[i]);
    }
    return res;
}

503. 下一个更大元素 II

思路:环形数组技巧:翻倍数组并取模


class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Stack<Integer> s = new Stack<>();
        // 数组长度加倍模拟环形数组
        for (int i = 2 * n - 1; i >= 0; i--) {
            // 索引 i 要求模,其他的和模板一样
            while (!s.isEmpty() && s.peek() <= nums[i % n]) {
                s.pop();
            }
            res[i % n] = s.isEmpty() ? -1 : s.peek();
            s.push(nums[i % n]);
        }
        return res;
    }
}

遗留问题

上一个更小元素/上一个更大元素/下一个更小元素/下一个更大元素 分别怎么改?

1、上一个更小元素

int[] func(int[] temperatures) {
    int n = temperatures.length;
    int[] res = new int[n];
    Stack<Integer> s = new Stack<>();
    //注意:从前往后遍历
    for (int i = 0; i < n; i++) {
        // 注意:单调栈,栈里大的弹出,保证小栈
        while (!s.isEmpty() && temperatures[s.peek()] >= temperatures[i]) {
            s.pop();
        }
        res[i] = s.isEmpty() ? -1 : (s.peek());
        s.push(i);
    }
    for (int i : res) {
        System.out.println(i);
    }
    return res;
}

2、上一个更大元素

int[] func(int[] temperatures) {
    int n = temperatures.length;
    int[] res = new int[n];
    Stack<Integer> s = new Stack<>();
    //注意:从前往后遍历
    for (int i = 0; i < n; i++) {
        // 注意:单调栈,栈里小的弹出,保证大栈
        while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
            s.pop();
        }
        res[i] = s.isEmpty() ? -1 : (s.peek());
        s.push(i);
    }
    for (int i : res) {
        System.out.println(i);
    }
    return res;
}

3、下一个更小元素

int[] func(int[] temperatures) {
    int n = temperatures.length;
    int[] res = new int[n];
    Stack<Integer> s = new Stack<>();
    //注意:从后往前遍历
    for (int i = n - 1; i >= 0; i--) {
        // 注意:单调栈,栈里大的弹出,保证小栈
        while (!s.isEmpty() && temperatures[s.peek()] >= temperatures[i]) {
            s.pop();
        }
        res[i] = s.isEmpty() ? -1 : (s.peek());
        s.push(i);
    }
    for (int i : res) {
        System.out.println(i);
    }
    return res;
}

4、下一个更大元素

    int[] func(int[] temperatures) {
        int n = temperatures.length;
        int[] res = new int[n];
        Stack<Integer> s = new Stack<>();
        // 思路:从后往前遍历
        for (int i = n - 1; i >= 0; i--) {
            // 注意:单调栈,栈里小的弹出,保证大栈
            while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
                s.pop();
            }    
            res[i] = s.isEmpty() ? -1: (s.peek());
            s.push(i);
        }
        for(int i : res){
            System.out.println(i);
        }
        return res;
    }

算法:堆(优先队列)

场景

最值要想到用堆

一般来说需要构造一个优先队列,比较器需要根据实际需求写;然后再添加到堆中

比较器如果有多条件比较,即第一个条件相等,比较第二个条件,可以写成以下三元组写法:

(int[] a, int[] b) -> a[0] != b[0] ? b[0] - a[0] : a[1] - b[1])

优先队列的写法:

image.png

题目

23. 合并K个升序链表

import java.util.PriorityQueue;
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>((a,b)->(a.val - b.val));
        ListNode dummy = new ListNode(-1);
        // 注意这里不是p = dummy.next
        ListNode p = dummy;
        for(ListNode node : lists){
            if(node != null)
                pq.add(node);
        }

        while(!pq.isEmpty()){
            // 注意:这里需要临时变量node
            ListNode node =  pq.poll();
            p.next = node;
            if(node.next != null){
                pq.add(node.next);
            }
            // 注意:这里不能放在if语句内
            p = p.next;
        }
        return dummy.next;
    }
}

215. 数组中的第K个最大元素

思路:第K个最值,想到用堆

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2)->o2 - o1);
        for(int n : nums){
            queue.offer(n);
        }
        int res = 0;
        for (int i = 0; i < k; i++){
            res = queue.poll();
        }
        return res;
    }
}

253. 会议室II

class Solution {
    public int minMeetingRooms(int[][] intervals) {
        if (intervals.length == 0) return 0;

        // 最小堆
        PriorityQueue<Integer> allocator = new PriorityQueue<Integer>(intervals.length, (a, b) -> a - b);

        // 对时间表按照开始时间从小到大排序
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);

        // 添加第一场会议的结束时间
        allocator.add(intervals[0][1]);

        // 遍历除第一场之外的所有会议
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] >= allocator.peek()) {
                // 如果当前会议的开始时间大于前面已经开始的会议中最晚结束的时间
                // 说明有会议室空闲出来了,可以直接重复利用
                // 当前时间已经是 intervals[i][0],因此把已经结束的会议删除
                allocator.poll();
            }
            // 把当前会议的结束时间加入最小堆中
            allocator.add(intervals[i][1]);
        }

        // 当所有会议遍历完毕,还在最小堆里面的,说明会议还没结束,此时的数量就是会议室的最少数量
        return allocator.size();
    }
}

347. 前 K 个高频元素

经典题目

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        int[] res = new int[k];
        // 思路:key为值,value为次数,以次数排序
        for(int n: nums){
            // 注意: getOrDefault的使用
            map.put(n, map.getOrDefault(n, 0) + 1);
        }

        // 注意:PriorityQueue的写法要带一个比较器
        PriorityQueue<int[]> queue = new PriorityQueue<>((int[] a, int[] b) -> b[1] - a[1]);

        // 注意:map的遍历
        for(Map.Entry<Integer, Integer> entry : map.entrySet()){
            int num = entry.getKey();
            int count = entry.getValue();
            queue.offer(new int[]{num, count});
        }
        for(int i = 0; i < k; i++){
            res[i] = queue.poll()[0];
        }

        return res;
    }
}

239. 滑动窗口最大值

思路:我们将数组 nums 的前 k 个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。


class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        // 1. 优先队列存放的是二元组(num,index) : 大顶堆(元素大小不同按元素大小排列,元素大小相同按下标进行排列)
            // num :   是为了比较元素大小
            // index : 是为了判断窗口的大小是否超出范围
        PriorityQueue<int[]> queue = new PriorityQueue<>((int[] a, int[] b) -> a[0] != b[0] ? b[0] - a[0] : a[1] - b[1]);

        // 2. 优选队列初始化 : k个元素的堆
        for(int i = 0;i < k;i++){
            pq.offer(new int[]{nums[i],i});
        }

        // 3. 处理堆逻辑
        int[] res = new int[n - k + 1];         // 初始化结果数组长度 :一共有 n - k + 1个窗口
        res[0] = pq.peek()[0];                  // 初始化res[0] : 拿出目前堆顶的元素
        for(int i = k;i < n;i++){               // 向右移动滑动窗口
            pq.offer(new int[]{nums[i],i});     // 加入大顶堆中
            while(pq.peek()[1] <= i - k){       // 将下标不在滑动窗口中的元素都干掉
                pq.poll();                      // 维护:堆的大小就是滑动窗口的大小
            }   
            res[i - k + 1] = pq.peek()[0];      // 此时堆顶元素就是滑动窗口的最大值
        }
        return res;
    }
}

算法:矩阵

场景

题目

48. 旋转数组

class Solution {
    public void rotate(int[][] matrix) {
        // /对角线翻转
        for(int i = 0; i < matrix.length; i++){
            for(int j = 0; j < i; j++){
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
        // 水平翻转,注意这里改变一位数组也会改变二维数组
        for(int[] arr : matrix){
            reverse(arr);
        }
    }
    public void reverse(int[] arr){
        int i = 0;
        int j = arr.length - 1;
        while(i < j){
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
            i++;
            j--;
        }
    }
   
}

Tips关键词

二分:时间复杂度log、数组有序

贪心、动态规划:“最”值问题

滑动窗口:“最”子串