LeetCode 笔记

文章目录

  • 数组
    • 169. 多数元素
    • 15. 三数之和
    • 704. 二分查找
    • 852. 山峰数组的峰顶索引
    • 原地哈希:448. 找到所有数组中消失的数字
    • 75. 颜色分类(荷兰国旗 Partition
    • 128. 最长连续序列
  • 链表
    • 21. 合并两个有序链表
    • 141. 环形链表
    • 160. 相交链表
    • 20. 有效的括号
  • 位运算
    • 102. 二叉树的层序遍历
    • 104. 二叉树的最大深度
    • 101. 对称二叉树
  • 滑动窗口
    • 209. 长度最小的子数组
    • 713. 乘积小于k的子数组
    • 3. 无重复字符的最长子串
    • 1658. 将x减到0的最小操作数
  • 动态规划
    • 70. 爬楼梯
    • 746. 使用最小花费爬楼梯
    • 53. 最大子数组和
    • 62. 不同路径
    • 63. 不同路径II
  • 回溯
    • 22. 括号生成
  • DFS
    • 79. 单词搜索
  • 位运算
    • 136. 只出现一次的数组
  • 单调栈
    • 496. 下一个更大元素
    • 739. 每日温度
    • 42. 接雨水
  • Java
    • 常用
    • 数组
    • 队列
    • 集合
    • 字符串

数组

169. 多数元素

LeetCode 笔记_第1张图片
摩尔投票法

  1. 维护一个候选人ans,初始可以设置成数组第一个元素
  2. 同时维护一个选票cnt
  3. 遍历数组,如果cnt==0,那么选择当前元素作为候选者;否则,如果元素等于候选者,cnt++,否则cnt--
  4. 如果题目不保证一定有候选者,还需要再遍历一次数组,确定候选者是否为真的多数元素
class Solution {
    public int majorityElement(int[] nums) {
        // 摩尔投票法
        int cnt = 1, ans = nums[0];
        int len = nums.length;
        for(int i=1; i<len; ++i){
            if(cnt == 0){
                cnt = 1;
                ans = nums[i];
            }
            else if(ans == nums[i])  ++ cnt;
            else{
                -- cnt;
            }
        }
        return ans;
    }
}

15. 三数之和


思路:

  1. 首先,数组无序,若想要高效地去除重复解,需要先进行排序。排序可调用Arrays.sort(nums)
  2. 固定最左侧元素nums[k],然后在右侧用双指针找满足nums[i]+nums[j]==-nums[k]的元素。注意每一次移动都需要去除重复
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();
        for(int k=0; k<nums.length-2; ++k){
            if(nums[k]>0)   break;	// nums[k]>0,右侧均大于nums[k],因此不可能有答案,直接跳出
            else if(k>0 && nums[k]==nums[k-1])  continue;	// nums[k]==nums[k-1],只能找到重复答案,去除
            int i = k+1, j = nums.length-1;	// 双指针
            while(i < j){
                int sum = nums[i]+nums[j]+nums[k];
                if(sum == 0){
                	// 添加答案
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums[i]);
                    temp.add(nums[j]);
                    temp.add(nums[k]);
                    ans.add(temp);
                    while(i<j && nums[i]==nums[++i]){}	// 跳过重复的元素,并且+1,好好体会
                    while(i<j && nums[j]==nums[--j]){}
                }else if(sum < 0){
                    while(i<j && nums[i]==nums[++i]){}
                }else{
                    while(i<j && nums[j]==nums[--j]){}
                }
            }
        }
        return ans;
    }
}

704. 二分查找

LeetCode 笔记_第2张图片
思路:二分

  1. 利用数组有序的特性,可以使用二分查找
  2. 具体而言:查找数组中第一个>=target的元素。由于是第一个,因此我们碰到nums[m]时,直接l=m+1,采用向下取整,而右边界直接r=m即可
class Solution {
    public int search(int[] nums, int target) {
        int l = 0, r = nums.length-1;
        while(l < r){
            int m = l+(r-l)/2;
            if(nums[m] < target){
                l = m+1;
            }else{
                r = m;
            }
        }
        return nums[r] == target ? r : -1;
    }
}

852. 山峰数组的峰顶索引

LeetCode 笔记_第3张图片
思路:二分查找

  1. 观察到,在山峰的左侧,有arr[i]。而在右侧,有arr[i]>arr[i+1]
  2. 峰顶索引i就是第一个满足arr[i]>arr[i+1]的索引,满足二分的条件
  3. 注意这里需要计算i+1,因此右边界r=len-2
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        // 找到第一个满足 arr[i]>arr[i+1]的i
        int l = 0, r = arr.length-2;
        while(l < r){
            int m = l+((r-l)>>1);
            if(arr[m]<arr[m+1]){
                l = m+1;
            }else{
                r = m;
            }
        }
        return r;
    }
}

原地哈希:448. 找到所有数组中消失的数字

LeetCode 笔记_第4张图片
思路:

  1. 很容易想到用一个哈希表
  2. 由于nums[i]在区间[1,n]内,所以可以用原数字做哈希表
  3. 具体而言,用nums[i-1]取负数,代表i出现在数组中,因此在遍历的时候,数组可能被修改过,需要对元素取绝对值
  4. 第二次遍历,如果nums[i-1]>0,说明i不存在数组中
class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
        List<Integer> ans = new ArrayList<>();
        for(int i : nums){
            int num = Math.abs(i);
            if(nums[num-1]>0)   nums[num-1] = -nums[num-1];	// 取负数,表示num存在
        }
        for(int i=0;i<nums.length;++i){
            if(nums[i] > 0){
                ans.add(i+1);
            }
        }
        return ans;
    }
}

75. 颜色分类(荷兰国旗 Partition

class Solution {
    public void sortColors(int[] nums) {
        int left = -1, right = nums.length; // 左右边界,包含
        int i = 0;
        while(i<right){
            if(nums[i] == 0){
                int temp = nums[++left];
                nums[left] = 0;
                nums[i] = temp;
                ++ i;
            }else if(nums[i] == 2){
                int temp = nums[--right];
                nums[right] = 2;
                nums[i] = temp;
                // 这里不能++i,因为可能把0换到前面
            }else{
                ++ i;
            }
        }
    }
}

128. 最长连续序列

LeetCode 笔记_第5张图片
思路:哈希表

  1. 注意到要求时间复杂为O(n)
  2. 暴力做法是,对于数组中某个数字x,我们判断数组中是否存在x+1x+2…。这个判断是否存在的过程,就可以用哈希表,从O(n)降到O(1)
  3. 但是需要考虑,数组中x遍历后,对于x+1x+2等不应该开启新的遍历,需要跳过。因此我们在只有判断得数组中不存在x-1时,才进行第二步
class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();	// 集合,存储数组出现的数字
        for(int n : nums){
            set.add(n);
        }
        int ans = 0;
        for(int num : set){
            if(!set.contains(num-1)){	// 只有在连续序列最前面,才会进入遍历
                int temp = 1;	// 记录连续序列长度
                int cur = num;	// 记录当前遍历的数字
                while(set.contains(cur+1)){	// 只要哈希表中存在cur+1,就继续遍历
                    ++ cur;
                    ++ temp;
                }
                ans = Math.max(ans, temp);	// 更新答案
            }
        }
        return ans;
    }
}

链表

  1. 链表的题目一般都不太难,画图,别怕麻烦

21. 合并两个有序链表

LeetCode 笔记_第6张图片

解法一:迭代

  1. 用一个指针cur跟踪当前节点,每次从list1list2中选取小的节点,链接起来
  2. 建立一个头节点,可以简化最开始的判断,也可以简化对空链表的判断
  3. 最后会有一个链表先遍历完,而另一个链表直接链接在后面即可
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 建立一个头节点,可以简化代码
        ListNode preHead = new ListNode(0);
        ListNode cur = preHead; // 当前指针,用于更新链表

        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                cur.next = list1;
                list1 = list1.next;
            }else{
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        cur.next = list1 == null ? list2 : list1;   // 最后只有一个链表没走完,连接到后面即可
        return preHead.next;    // 头节点下一个节点即为所求
    }
}

解法二:递归

  1. 可以把链接的过程看作:list1list2中较小的节点,与其余的所有节点合并的过程
  2. 考虑边界条件:list1list2null时,返回另一个
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    	// 边界条件,一个为null
        if(list1 == null)   return list2;
        if(list2 == null)   return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next, list2);	// list1.next = 其余所有节点合并的结果
            return list1;
        }else{
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

141. 环形链表

LeetCode 笔记_第7张图片
思路一:快慢指针

  1. 设置两个指针,初始分别指向headhead.next
  2. 快指针一次走两步,慢指针一次走一步
  3. 如果不存在环,那么快指针一定先走完
  4. 如果存在环,那么快慢指针肯定会相遇
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head == null || head.next == null)   return false;
        // 快慢指针
        ListNode slow = head;
        ListNode fast = head.next;
        // 快指针不走到null时,进行循环
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast)    return true;    // 相遇说明存在环
        }
        return false;   // 快指针走完,说明没有环
    }
}

思路二:哈希表

最简单直接的想法,遍历链表,将节点存储在哈希表中,如果检测到哈希表中已经存在该节点,说明有环

public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        while(head != null){
            if(!set.add(head))  return true;
            head = head.next;
        }
        return false;
    }
}

160. 相交链表

LeetCode 笔记_第8张图片
思路:双指针(哈希表也可以

  1. 开始需要判断是否有一个链表为空
  2. 设置两个指针l1l2,初始指向headAheadB
  3. l1l2每次往后移动一步,当l1变为null时,l1下一步指向headB继续遍历;相同的,l2变为null时下一步指向headA
  4. 第三步经过一次后,必定有l1==l2:当存在相交节点时,l1l2都走了相同的长度,指向第一个相交节点;不存在相交节点时,走了相同长度,两者都为null
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null)  return null;
        ListNode l1 = headA, l2 = headB;
        while(l1 != l2){
            l1 = l1 == null ? headB : l1.next;
            l2 = l2 == null ? headA : l2.next;
        }
        return l1;
    }
}

20. 有效的括号

LeetCode 笔记_第9张图片
思路

  1. 典型的栈的应用,合法的字符串中,右括号需要与左边与其最近的左括号匹配,这与栈LIFO的性质符合。我们可以利用一个栈储存左括号
  2. 遇到左括号,则将其入栈,等待右括号与其匹配。在代码实现上,可以入栈相应的右括号,方便后续匹配(这点之前没有学习到
  3. 遇到右括号,检查栈顶是否匹配。这里有两种情况不合法:一是栈空,二是栈顶不是对应括号。直接返回false;匹配,则出栈顶。
  4. 最后检查栈是否空,非空说明有左括号没有匹配,返回false;空说明均匹配,返回true
  5. ps:奇数长度的字符串,一定是非法的,可以提前排除
class Solution {
    public boolean isValid(String s) {
        int len = s.length();
        if(len % 2 == 1)    return false;   // s长度为奇数,可以直接判错

        Stack<Character> stack = new Stack<>();
        for(int i=0;i<len;++i){
            char ch = s.charAt(i);
            // 这里,遇到左括号后,将对应正确的右括号入栈,方便后面判断
            if(ch == '(')   stack.push(')');
            else if(ch == '[')   stack.push(']');
            else if(ch == '{')   stack.push('}');
            else{
                if(stack.empty() || stack.peek() != ch)   return false; // 右括号:栈空或者不匹配,则不合法,直接返回
                stack.pop();    // 匹配,则出栈顶
            }
        }

        return stack.empty();   // 最后检查栈空
    }
}

位运算

  • x = x & (x-1):将x的二进制中,最后一个1变为0

102. 二叉树的层序遍历


思路

  1. 树的层序遍历,是经典的队列应用。这道题只是增加了一个要求:把每一层的节点放在一起
  2. 首先将根节点入队
  3. 队列非空的时候,进行如下循环:取出队头元素,访问,将其左右孩子依次入队(如果有的话)
  4. 注意到第i次迭代时,队列的长度就是当前这一层的长度
/**
 * 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 List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> q = new LinkedList<>(); // 创建一个队列
        List<List<Integer>> ans = new ArrayList<List<Integer>>();   // 返回的列表
        if(root == null)    return ans; // 空树,直接返回空
        q.offer(root);  // 根节点入队
        while(!q.isEmpty()){    // 队列非空时,进行BFS
            List<Integer> temp = new ArrayList<Integer>();  // 存储当前所在层的节点
            int size = q.size();    // 当前层的节点个数,就是队列目前的长度
            for(int i=0; i<size; ++i){
                TreeNode t = q.poll();
                temp.add(t.val);
                // 孩子节点入队
                if(t.left != null)  q.offer(t.left);
                if(t.right != null) q.offer(t.right);
            }
            ans.add(temp);
        }
        return ans;
    }
}

104. 二叉树的最大深度

LeetCode 笔记_第10张图片

思路一:BFS

二叉树的深度也就是层数,可以用102题相似的思路,数层数

/**
 * 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 int maxDepth(TreeNode root) {
        // BFS 同102题
        if(root == null)    return 0;
        int ans = 0;
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        while(!q.isEmpty()){
            int size = q.size();
            for(int i=0; i<size; ++i){
                TreeNode node = q.poll();
                if(node.left != null)   q.offer(node.left);
                if(node.right != null)  q.offer(node.right);
            }
            ++ ans;
        }
        return ans;
    }
}

思路二:递归

  1. 每个节点的深度,就是它左右孩子深度较大值+1(节点自己)
  2. 递归退出条件,null返回0
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null)    return 0;
        return 1+Math.max(maxDepth(root.left), maxDepth(root.right));
    }
}

101. 对称二叉树


思路一:递归

  1. 一棵对称二叉树需要满足,根节点相同,并且左边节点的左子树和右边节点的右子树、左边节点的右子树和右边节点的左子树相同
  2. 递归退出条件:两个节点都空,返回true;一个节点非空一个为空,返回false
/**
 * 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 boolean isSymmetric(TreeNode root) {
        return func(root, root);
    }

    public boolean func(TreeNode root1, TreeNode root2){
        if(root1 == null && root2 == null)  return true;
        if(root1 == null || root2 == null)  return false;
        return root1.val == root2.val && func(root1.left, root2.right) && func(root1.right, root2.left);
    }
}

思路二:迭代

  1. 引入一个队列,将递归改为迭代(常见
  2. 初始时,将根节点入队两次
  3. 每次取出队头两个元素进行比较;在比较完后,按照相反的次序将其孩子节点入队
/**
 * 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 boolean isSymmetric(TreeNode root) {
        return func(root, root);
    }

    public boolean func(TreeNode root1, TreeNode root2){
        Queue<TreeNode> q = new LinkedList<>();
        // 初始,入队两次根节点
        q.offer(root1);
        q.offer(root2);
        while(!q.isEmpty()){    // 队列非空时,判断
            // 取出两个队头,poll方法在队空时会返回null
            root1 = q.poll();   
            root2 = q.poll();
            if(root1 == null && root2 == null)  continue;
            if((root1 == null || root2 == null) || root1.val != root2.val)  return false;
            // 按照相反次序入队
            q.offer(root1.left);
            q.offer(root2.right);

            q.offer(root1.right);
            q.offer(root2.left);
        }
        return true;
    }
}

滑动窗口

209. 长度最小的子数组

LeetCode 笔记_第11张图片
思路:滑动窗口

利用数组元素均为正数的性质

  1. 枚举窗口右边界,不断向右扩展
  2. 当窗口内元素和大于target时,尝试缩小左窗口,以求得最小长度的满足要求的子数组。可以缩小左窗口的判断是:sum - nums[left] >= target
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int sum = 0;
        int len = nums.length;
        int left = 0;
        int ans = len+1;
        for(int right = 0; right<len; ++right){
            sum += nums[right];
            while(sum-nums[left] >= target){
                sum -= nums[left];
                ++ left;
            }
            if(sum >= target)
                ans = Math.min(ans, right-left+1);
        }
        return ans > len ? 0 : ans;
    }
}

713. 乘积小于k的子数组

LeetCode 笔记_第12张图片
思路:滑动窗口

  1. 和上一题思路类似,找到一个元素乘积的最长子数组,子数组中有若干个数组都满足题意
  2. 推导[left...right]有多少个子数组满足:我们固定的是right[left...right]、[left+1...right]...[right...right]都满足,数量正好是right-left+1
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if(k <= 1)  return 0;
        int ans = 0;
        int left = 0;
        int prod = 1;
        int len = nums.length;
        for(int right=0; right<len; ++right){
            prod *= nums[right];
            while(prod >= k){
                prod /= nums[left];
                ++ left;
            }
            ans += right-left+1;
        }
        return ans;
    }
}

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

LeetCode 笔记_第13张图片

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Set<Character> seen = new HashSet<>();
        int ans = 0;
        int left = 0;
        int len = s.length();
        for(int right=0; right<len; ++right){
            while(!seen.add(s.charAt(right))){
                seen.remove(s.charAt(left));
                ++ left;
            }
            ans = Math.max(ans, right-left+1);
        }
        return ans;
    }
}

1658. 将x减到0的最小操作数

LeetCode 笔记_第14张图片
思路:滑动窗口

  1. 根据题意,选取前缀和后缀,令他们之和为lsum+rsum == x
  2. 初始时我们选取前缀为空,left=-1;后缀为整个数组,right=0
  3. 列举所有前缀的情况,在其中,当lsum+rsum > x时,说明后缀长度太大,应不断缩短后缀长度;当lsum+rsum < x,说明应该移动扩大前缀长度
class Solution {
    public int minOperations(int[] nums, int x) {
        int sum = Arrays.stream(nums).sum();    // api数组求和
        if(sum < x) return -1;  // 特殊情况直接排除

        int len = nums.length;
        int right = 0;
        int lsum = 0, rsum = sum;
        int ans = len+1;
        // 滑动窗口
        for(int left=-1; left < len; ++left){
            // 左窗口滑动,前缀加
            if(left != -1){
                lsum += nums[left];
            }
            // 右窗口滑动,后缀减
            while(right<len && lsum+rsum>x){
                rsum -= nums[right];
                ++ right;
            }
            // 记录答案
            if(lsum+rsum==x){
                ans = Math.min(ans, (left+1)+(len-right));
            }
        }
        // 最后需要判断是否存在答案
        return ans>len ? -1 : ans;
    }
}

动态规划

70. 爬楼梯


思路

典型的动态规划,我们用dp[i]表示走到第i级台阶的方法数

  1. 显然,n==1只有一种方法,n==2有两种。那么初始条件dp[1]=1dp[2]=2
  2. 每次只能走1级或2级台阶,那么走到n级台阶,可以从第n-1级台阶走1级,或者从第n-2级台阶走2级。由此可得到状态方程dp[i]=dp[i-1]+dp[i-2]
  3. 由于dp[i]只和dp[i-1]dp[i-2]有关,因此可以用两个变量代替,就可以将空间从 O ( n ) O(n) O(n)降到 O ( 1 ) O(1) O(1)。这种滚动数组的思想在动态规划中挺常用
class Solution {
    public int climbStairs(int n) {
        if(n < 3)   return n;

        // - 数组,比较好理解 dp[i]表示走到i级台阶有多少种方法
        // int[] dp = new int[n+1];
        // dp[1] = 1;
        // dp[2] = 2;
        // for(int i=3; i<=n; ++i){
        //     dp[i] = dp[i-1]+dp[i-2];
        // }
        // return dp[n];

        // - 滚动数组优化空间
        int a = 1, b = 2;   // a代替dp[i-2],b代替dp[i-1]
        int c = 0;
        for(int i=3; i<=n; ++i){
            c = a+b;
            a = b;
            b = c;
        }
        return c;
    }
}

746. 使用最小花费爬楼梯

LeetCode 笔记_第15张图片

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;
        if(len == 2)    return Math.min(cost[0], cost[1]);
        // int[] dp = new int[len+1];  // dp[i]表示到达i层的最小花费
        // for(int i=2;i<=len;++i){
        //     dp[i] = Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
        // }
        // return dp[len];

        // 滚动变量优化
        int a = 0, b = 0, c = 0;    // i-2, i-1, i
        for(int i=2; i<=len; ++i){
            c = Math.min(a+cost[i-2], b+cost[i-1]);
            a = b;
            b = c;
        }
        return c;
    }
}

53. 最大子数组和

LeetCode 笔记_第16张图片
思路

  1. 这道题里面,连续是很关键的一点
  2. 我们可以把问题拆分成子问题:以nums[i]结尾的数组和最大值为多少?我们以dp[i]来记录
  3. 可以发现,如果dp[i-1]<=0,那么以dp[i] = nums[i],一个数加上负数,结果肯定是变小的。这也是状态转移方程。这个方程显然,可以用滚动变量节约空间
  4. 初始条件也很显然,dp[0]=nums[0]
class Solution {
	// 这里没有滚动变量化简空间复杂度
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];    // dp[i]表示以nums[i]结尾的连续子数组的最大值
        dp[0] = nums[0];
        int ans = nums[0];
        for(int i=1;i<nums.length;++i){
            dp[i] = dp[i-1]>0 ? dp[i-1]+nums[i] : nums[i];
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

62. 不同路径


思路:动态规划

用一个m*n的数组,dp[i][j]表示到达地图(i,j)位置总共有几种方法

  1. 边界条件:由于只能向下和向右走,因此第一行和第一列dp=1
  2. 转移方程:在下标允许的前提下,dp[i][j] = dp[i-1][j]+dp[i][j-1],分别表示从上面、下面走到当前格子
class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        Arrays.fill(dp[0], 1);
        for(int i=1;i<m;++i){
            for(int j=0;j<n;++j){
                if(j == 0)  dp[i][j] = 1;
                else{
                    dp[i][j] = dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[m-1][n-1];
    }
}

63. 不同路径II


思路:动态规划

和上一道题思路类似,多了一个障碍物

  1. 首先,格子出现障碍物,那么dp=0
  2. 对于第一行第一列,单独判断
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = obstacleGrid[0][0] == 0 ? 1 : 0;	// dp[0][0]初始化
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(i == 0 && j == 0)    continue;
                if(obstacleGrid[i][j] == 1) dp[i][j] = 0;	// 碰到障碍物,直接0
                else
                {
                    if(i == 0)  dp[i][j] = dp[i][j-1];	// 第一行
                    else if(j == 0) dp[i][j] = dp[i-1][j];	// 第一列
                    else    dp[i][j] = dp[i-1][j]+dp[i][j-1];	// 其他
                }
            }
        }
        return dp[m-1][n-1];
    }
}

回溯

22. 括号生成

LeetCode 笔记_第17张图片
思路

  1. 典型的回溯
  2. 这道题主要研究一下怎么剪枝:合法的括号序列,左括号个数必须等于n,并且从左往右添加时,右括号个数始终要小于左括号个数。据此可以省略掉isValid和大量无效搜索
class Solution {
    List<String> ans = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    public List<String> generateParenthesis(int n) {
        bt(n, 0, 0);
        return ans;
    }
    public void bt(int n, int left, int right){
        if(sb.length() == n*2){
            ans.add(sb.toString());
        }
        // 左括号个数不够,尝试添加
        if(left < n){
            sb.append('(');
            bt(n, left+1, right);
            sb.deleteCharAt(sb.length()-1);
        }
        // 右括号个数小于左括号的,尝试添加
        if(right < left){
            sb.append(')');
            bt(n, left, right+1);
            sb.deleteCharAt(sb.length()-1);
        }

    }
}

DFS

79. 单词搜索

class Solution {
    final static int[] xDirection = {0,0,-1,1};
    final static int[] yDirection = {-1,1,0,0};
    boolean[][] visited;
    boolean flag = false;
    public boolean exist(char[][] board, String word) {
        int m = board.length, n = board[0].length;
        visited = new boolean[m][n];
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(word.charAt(0) == board[i][j]){
                    dfs(board, word, i, j, 0);
                }
            }
        }
        return flag;
    }
    public void dfs(char[][] board, String word, int x, int y, int index){
        if(board[x][y] != word.charAt(index)){
            return;
        }else if(index == word.length()-1){
            flag = true;
            return;
        }
        visited[x][y] = true;
        for(int i=0;i<4;++i){
            int nx = x+xDirection[i];
            int ny = y+yDirection[i];
            if(nx>=0 && nx<board.length && ny>=0 && ny<board[0].length && visited[nx][ny] == false)
                dfs(board, word, nx, ny, index+1);
        }
        visited[x][y] = false;
    }
}

位运算

136. 只出现一次的数组

LeetCode 笔记_第18张图片

  1. 异或的性质:两个相同的数异或,结果为0a^a=0;任意数和0异或等于它本身:a^0=a;异或具有交换性
  2. 根据以上的性质,用一个ans=0,与数组中所有元素异或一次,最终ans即为只出现一次的数字
class Solution {
    public int singleNumber(int[] nums) {
        int ans = 0;
        for(int i=0; i<nums.length; ++i){
            ans ^= nums[i];
        }
        return ans;
    }
}

单调栈

496. 下一个更大元素

思路:单调栈模板

唯一多的是,用一个Map方便获取

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        Map<Integer, Integer> map = new HashMap<>();
        // nums2
        Stack<Integer> stk = new Stack<>();
        for(int num : nums2)    map.put(num, -1);
        // 单调递减的栈
        for(int num2 : nums2){
            while(!stk.isEmpty() && stk.peek()<num2){
                map.put(stk.pop(), num2);
            }
            stk.push(num2);
        }
        int[] ans = new int[nums1.length];
        for(int i=0; i<nums1.length; ++i){
            ans[i] = map.get(nums1[i]);
        }
        return ans;
    }
}

739. 每日温度

LeetCode 笔记_第19张图片
思路:单调栈

  1. 暴力的做法就是一个二重循环,不赘述
  2. 题目要求找到i右边第一个j满足arr[j]>arr[i],那么我们可以维护一个单调递减的单调栈,这样当我们遍历到比栈顶更大的元素时,就可以尝试出栈
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        // 维护一个单调递减的栈
        Stack<Integer> stk = new Stack<>();
        int[] ans = new int[temperatures.length];
        Arrays.fill(ans, 0);
        for(int i=0; i<temperatures.length; ++i){
            while(!stk.isEmpty() && temperatures[stk.peek()]<temperatures[i]){
                ans[stk.peek()] = i-stk.pop();
            }
            stk.push(i);
        }
        return ans;
    }
}

42. 接雨水


思路:单调栈

  1. 维护一个单调递减的栈:当出现比栈顶元素大的元素时,说明可能出现“凹槽”
  2. 出现凹槽时,栈顶元素为凹槽底部高度(需要出栈),次栈顶元素为凹槽左边高度(不需要出栈),当前遍历元素为凹槽右侧高度(最后需要入栈),这个凹槽接的雨水高度为: ( m i n ( 左边高度,右边高度 ) − 凹槽底部高度 ) ∗ 宽度 (min(左边高度,右边高度)-凹槽底部高度)*宽度 (min(左边高度,右边高度)凹槽底部高度)宽度
  3. 宽度如何得到呢?我们可以让入栈的数据为元素下标,那么宽度为i-left-1
class Solution {
    public int trap(int[] height) {
        int len = height.length;
        if(len <= 2)  return 0;   // 特殊情况

        int ans = 0;
        // 维护一个单调递减的栈,栈内存储下标
        Stack<Integer> stk = new Stack<>();
        stk.push(0);	// 初始最左边入栈
        for(int i=1; i<len; ++i){
            int top = stk.peek();	// 与栈顶元素比较
            if(height[i] < height[top]){	// 如果更小,那么直接入栈,待处理
                stk.push(i);
            }else if(height[i] == height[top]){	// 如果高度相等,相等的高度没法接雨水,那么需要出栈栈顶,更新成当前的
                stk.pop();
                stk.push(i);
            }else{
                while(!stk.isEmpty() && height[i] > height[stk.peek()]){	// while 接雨水
                    int mid = stk.pop();
                    if(!stk.isEmpty()){	// 出栈后,需要栈非空,也就是有凹槽左边
                        int left = stk.peek();
                        ans += (Math.min(height[left], height[i])-height[mid])*(i-left-1);
                    }
                }
                stk.push(i);
            }
        }
        return ans;
    }
}

Java

常用

  • Math.min() / Math.max()
  • Integer.MAX_VALUE / Integer.MIN_VALUE

数组

  • List:Java列表,是一个接口,不可以实例化
  • ArrayListLinkedList:实现了List接口,是类,可以用于实例化

因此创建一个列表,可以用List l = new ArrayList()

  • Stack stk = new Stack<>():创建一个存储Character类型值的栈
  • bool empty():判断栈是否空
  • peek():返回栈顶元素
  • push():入栈
  • pop():出栈

队列

  • Queue q = new LinkedList<>():创建一个队列
  • Deque dq = new LinkedList<>():创建一个双端队列

Queue为例,一些方法

  • add / offer:队尾添加元素。对于长度有限制的队列,队列满时,add会抛出uncheck异常,offer会返回false
  • remove / poll:移除队头元素,并返回该元素。队列空时,remove会抛出异常,poll会返回null
  • element / peek:返回队头,但是不删除。队列空时,和上述类似
  • isEmpty:判断队列是否为空
  • size:返回队列长度

集合

Set接口,HashSetTreeSet实现了该接口,可用于实例化

关于HashSet

  • Set set = new HashSet<>():创建一个集合。集合里的元素不能重复
  • add():往集合中添加元素,如果元素已经存在,返回false
  • remove():从集合中删除一个元素

字符串

  • s.startsWith(pref):检查s是否以pref开头
  • s.charAt(index):返回index位置处的字符
  • s.toCharArray():将字符串转化为char[]

字符串截取

  • s.substring(int beginIndex):返回一个字符串,等于从beginIndex截取到末尾
  • s.substring(int beginIndex, int endIndex):返回一个字符串,等于s[beginIndex...endIndex-1],注意不包括endIndex

字符串替换

  • s.replace(old, new):返回一个字符串,用new替换掉old

StringBuilder可变长度

  • deleteCharAt(i):移除i位置的字符
  • toString():转化为String
  • append(ch):末尾添加ch

你可能感兴趣的:(LeetCode,leetcode,算法)