LeetCode题号[1,99]刷题总结

目录


前言

  • 这里是\([1,99]\)范围内的题目
  • 基本按照题号从头到尾刷。
  • 主要刷mid和hard难度的。
  • 扫一眼出答案或麻烦得要死却又没啥技术含量的题目基本就不浪费时间了。
  • 由于决定走Java路线,所以主要就用Java,当然偶尔会用C++刷一下。

2.两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

思路

水题

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(0);
    ListNode listNode = head;

    while (l1 != null || l2 != null){
        if (l1 != null){
            listNode.val += l1.val;
            l1 = l1.next;
        }
        if (l2 != null){
            listNode.val += l2.val;
            l2 = l2.next;
        }

        if (listNode.val > 9 || l1 != null || l2 != null) {
            listNode.next = new ListNode(listNode.val / 10);
            listNode.val %= 10;
            listNode = listNode.next;
        }
    }

    return head;
}

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

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

思路

水题,窗口

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left = 0,right = 0,ans = 0;
        Set<Character> set = new HashSet<>();
        while (right < s.length()) {
            while (set.contains(s.charAt(right))){
                set.remove(s.charAt(left++));
            }
            set.add(s.charAt(right++));
            ans = Integer.max(ans,right - left);
        }
        return ans;
    }
}

4. 寻找两个正序数组的中位数(Hard)

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。

思路

  • 题解很多,这里只给出一个
  • 题目可以变成寻找第k小问题
  • 每次排除一个数组中最前面的k/2个元素(注意越界溢出问题)
  • 举个例子,数组nums1为[1,1,2,3],数nums2为[1,2,2,3]
    • 题目变成寻找第4小和第5小元素
    • 我们先寻找第4小,其中4/2 = 2,所以判断两数组的第2小
    • 判断nums1[1] < nums2[1]
    • 所以这时候可以抛弃nums1[0]和nums1[1]了,why?
    • nums1[0] < nums1[1] < nums2[1]
    • x = nums2[0] < nums2[1]
    • 所以只会出现以下情况:
    • x < nums1[0] < nums1[1] < nums2[1]
    • nums1[0] < x < nums1[1] < nums2[1]
    • nums1[0] < nums1[1] < x < nums2[1]
    • 显然,无论哪种情况,nums1[0]和nums1[1]永远不可能是第k小了
  • 时间复杂度\(O(log(n+m))\)
  • 空间复杂度\(O(1)\)
public int findKth(int[] nums1, int start1,int[] nums2,int start2,int k){
    if (nums1.length-start1 < nums2.length-start2)//让nums1数组的长度始终最大
        return findKth(nums2,start2,nums1,start1,k);
    if (nums2.length == start2)//当nums2数组为空时,直接选择nums1数组
        return nums1[start1+k-1];
    if (k == 1)//需要第1小的元素时,直接选择两数组的最小值
        return Integer.min(nums1[start1],nums2[start2]);

    int i = Integer.min(nums1.length-1,start1+(k>>1)-1);//防止出现越界情况
    int j = Integer.min(nums2.length-1, start2+(k>>1)-1);

    return nums1[i] < nums2[j]
            ? findKth(nums1,i+1,nums2,start2,k - (i - start1 + 1))
            : findKth(nums1,start1,nums2,j+1,k - (j - start2 + 1));
}

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int sum = nums1.length+nums2.length+1;//需要加1,例如总数组长度为3,则中位数是第2个,(3+1)/2 = 2
    double ans1 = findKth(nums1,0,nums2,0,(sum>>1));
    if (sum %2 == 1)//当sum为奇数时,说明总数组长度为偶数,所以需要两个数
        ans1 = (ans1 + findKth(nums1,0,nums2,0,((sum+1)>>1)))*0.5;
    return ans1;
}

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

思路

  • 模拟:以任意点为中心扩散。
  • dp:与模拟相比,时间复杂度不变,空间复杂度反而增加
public String longestPalindrome(String s) {
    String ans = "";
    for(int i = 0;i < s.length()-1;i++){
        String temp1 = s.charAt(i) == s.charAt(i+1) ? check(s,i,i+1) :"";
        String temp2 = check(s,i,i);
        ans = temp1.length() > ans.length()? temp1 : ans;
        ans = temp2.length() > ans.length()? temp2 : ans;
    }
    return s.length() == 1? s : ans;
}
public String check(String s,int l,int r){
    while (l-1 >= 0 && r+1 < s.length() &&s.charAt(l-1) == s.charAt(r+1)){
        l--;
        r++;
    }
    return s.substring(l,r+1);
}

6. Z 字形变换

将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

思路

模拟移动顺序,原字符串从左到右移动,相对于Z字形来说则是从上到下依次移动。

public String convert(String s, int numRows) {
    if (numRows == 1) return s;

    List<StringBuilder> list = new ArrayList<>();
    int go = 1,now = 0;
    StringBuilder ans = new StringBuilder();

    for(int i = Integer.min(numRows,s.length());i >= 0;i--)
        list.add(new StringBuilder());

    for(char i : s.toCharArray()){
        list.get(now).append(i);
        now += go;
        if (now == 0 || now == numRows -1) go = -1*go;
    
    for (StringBuilder i : list)
        ans.append(i);

    return ans.toString();
}

10. 正则表达式匹配(Hard)

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '' 的正则表达式匹配。
'.' 匹配任意单个字符
'
' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

思路

  • 回溯法
public boolean isMatch(String s, String p) {
    if (p.isEmpty()) return s.isEmpty();
    boolean flag = !s.isEmpty()
            && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');

    if (p.length() >= 2 && p.charAt(1) == '*' ){
        return  isMatch(s,p.substring(2))
                || (flag && isMatch(s.substring(1),p));
    }

    return flag && isMatch(s.substring(1),p.substring(1));
}
  • 记忆化搜索(dp)
  • 回溯法时,有些回溯可能重复,将重复的记录下来,下次遇见的时候直接返回结果。
    int dp[][];

    public boolean isMatch(String s,String p){
        dp = new int[s.length()+10][p.length()+10];
        for(int i = 0;i <= s.length();i++)
            Arrays.fill(dp[i],-1);
        return isMatch(0,0,s,p);
    }

    public boolean isMatch(int i,int j,String s, String p) {
        if (dp[i][j] != -1) return dp[i][j] == 1;
        if (p.isEmpty()) return s.isEmpty();
        boolean ans ;
        boolean flag = !s.isEmpty()
                && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');

        if (p.length() >= 2 && p.charAt(1) == '*' ){
            ans =  isMatch(i,j+2,s,p.substring(2))
                    || (flag && isMatch(i+1,j,s.substring(1),p));
        }else {
            ans = flag && isMatch(i+1,j+1,s.substring(1),p.substring(1));
        }
        dp[i][j] = ans ? 1 : 0;
        return ans;
    }
  • 数组dp
  • dp通常有两种实现方式,一种是通过递归的记忆化搜索,一种是通过数组的dp
public boolean isMatch(String s, String p) {
    boolean dp[][] = new boolean[s.length() + 5][p.length() + 5];
    dp[s.length()][p.length()] = true;

    for (int i = s.length(); i >= 0; i--)
        for (int j = p.length() - 1; j >= 0; j--) {
            boolean flag = i < s.length()
                    && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                dp[i][j] = dp[i][j + 2] || (flag && dp[i + 1][j]);
            } else {
                dp[i][j] = flag && dp[i+1][j+1];
            }
        }

    return dp[0][0];
}

11. 盛最多水的容器

给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

思路

  • 双指针
  • 两个指针,一个最左端,一个最右端,每次移动最小的一端
  • 反证法:
    • 设左端高度为x,右端高度为y,两端距离为len,其中x > y,此时面积为y*len(高度以最小的一端为主)。
    • 假设移动最大的一端x,那么len-1,而最小的一端y不变,那么结果只会减小,即y*(len-1)。
class Solution {
    public int maxArea(int[] height) {
        int left = 0,right = height.length - 1,ans = Integer.MIN_VALUE;
        while (left < right) {
            ans = Integer.max(ans,(right - left) * (Integer.min(height[left],height[right])));
            if (height[left] < height[right]) left++;
            else right--;
        }
        return ans;
    }
}

15.三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。

思路

排序,双指针,时间复杂度\(O(n^2)\),需要注意的是,可能出现重复的三元组,所以需要去重。

  • C++版
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
            sort(nums.begin(),nums.end());
            vector<vector<int>> ans;
            int len = nums.size();
            for (int i = 0;i < len-2;i++){
                if (i > 0 &&nums[i] == nums[i-1])
                    continue;
                int left = i+1, right = len-1;

                while(left < right){
                    int now = nums[i] + nums[left] + nums[right];
                    if (now == 0 ){
                        if (ans.empty() ||ans[ans.size()-1][0] != nums[i] || ans[ans.size()-1][1] != nums[left] || ans[ans.size()-1][2] != nums[right])
                            ans.push_back(vector<int>({nums[i],nums[left],nums[right]}));
                        left++;
                        right--;
                    }
                    else if (now < 0) left++;
                    else right--;
                }
            }
            return ans;
    }
};
  • Java版
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> list = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0;i < nums.length;i++) {
            if (i > 0 && nums[i] == nums[i-1] ) continue;//前一个与当前元素相同时直接跳过
            if (nums[i] > 0) break;
            int left = i+1,right = nums.length - 1;
            while (left < right) {
                int value = nums[i] + nums[left] + nums[right];
                if (value == 0) {
                    list.add(Arrays.asList(nums[i],nums[left++],nums[right--]));
                    while (left < right && left - 1 >= 0 && nums[left] == nums[left-1]) left++;
                    while (left < right && right + 1 < nums.length && nums[right] == nums[right+1]) right--;
                }
                else if (value < 0) left++;
                else right--;
            }
        }
        
        return list;
    }  
}

16. 最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

思路

  • 双指针,比上题简单
  • 时间复杂度\(O(n^2)\)
  • 空间复杂度\(O(1)\)
public int threeSumClosest(int[] nums, int target) {
    int ans = Integer.MAX_VALUE;
    Arrays.sort(nums);

    for (int i = 0; i < nums.length && ans != target; i++) {
        int left = i + 1, right = nums.length - 1;
        while (left < right) {
            int now = nums[i] + nums[left] + nums[right];
            if (ans == Integer.MAX_VALUE || Math.abs(target - now) < Math.abs(target - ans)) {
                ans = now;
            }
            if (now < target) {
                left++;
            } else {
                right--;
            }
        }
    }

    return ans;
}

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

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

思路

  • 递归
public int isNthFromEnd(ListNode pre,ListNode head,int n){
    int th = head.next == null ? 1
            :isNthFromEnd(head,head.next,n)+1;
    if (th == n && pre != null){
        pre.next = head.next;
    }
    return th;
}

public ListNode removeNthFromEnd(ListNode head, int n) {
    return isNthFromEnd(null,head,n) == n ? head.next : head;
}

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

思路

  • 水题,略
class Solution {
    private List<String> ans;
    public void dfs(int pre,int back,int n,String value) {
        if (pre == n && back ==  n) {
            ans.add(value);
            return;
        }
        if (pre < n ) dfs(pre+1,back,n,value+"(");
        if (pre > back) dfs(pre,back+1,n,value+")");
    }

    public List<String> generateParenthesis(int n) {
        ans = new ArrayList<>();
        dfs(0,0,n,"");
        return ans;
    }
}

23. 合并K个排序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

思路

  • 优先队列
  • 将k个链表的头节点放入优先队列,每次弹出最小的那个
  • 时间复杂度\(O(k*n*logk)\)
  • 空间复杂度\(O(k)\)
public ListNode mergeKLists(ListNode[] lists) {
    ListNode head = new ListNode(0);
    ListNode current = head;
    Queue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
        @Override
        public int compare(ListNode o1, ListNode o2) {
            return o1.val-o2.val;
        }
    });

    for(ListNode node : lists)
        if (node != null) queue.add(node);
    while(!queue.isEmpty()){
        ListNode top = queue.poll();
        ListNode now = new ListNode(top.val);
        if (top.next != null) queue.add(top.next);
        current.next = now;
        current = now;
    }

    return head.next;
}

25. K 个一组翻转链表(Hard)

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

思路

  • 模拟
  • 总的来说,这题本身肯定不算难,但模拟题嘛,再加上链表,写起来会比较麻烦,尤其是面试要求十几二十分钟左右时间内敲出,难度相对还是挺大的。
public ListNode reverse(ListNode head){
    ListNode pre = null;
    ListNode cur = head;

    while(cur != null){
        ListNode next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }

    return pre;
}

public ListNode reverseKGroup(ListNode head, int k) {
    ListNode newHead = new ListNode(0);
    newHead.next = head;

    ListNode pre = newHead;//表示接下来需要逆转的k个节点的第一个的前面
    ListNode end = newHead;//表示接下来需要逆转的k个节点的最后一个
    
    while(end != null){
        for(int i = 0;i < k&&end != null;i++)
            end = end.next;
        if (end == null) break;//如果需要逆转的k个节点的最后一个为null,显然不足k个

        ListNode start = pre.next;//k个节点的第一个
        ListNode next = end.next;//下一环k节点的第一个

        end.next = null;//k个节点的下一个为null,方便reverse
        pre.next = reverse(start);//reverse返回逆转后的k个节点的第一个

        start.next = next;//逆转后,k个节点的第一个变成了最后一个,此时连接下一环k节点的第一个
        pre = end = start;
    }

    return newHead.next;
}

31. 下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。

思路

public void nextPermutation(int[] nums) {
    int left = nums.length-2;
    //从右往左找出第一个顺序的
    while(left >= 0){
        if (nums[left] < nums[left+1]) break;
        left--;
    }
    //从右往左找出第一个大于left的值,由于前面的阶段,所以保证(left,right]是逆序的
    for(int right = nums.length-1;left != -1 && right > left;right--){
        if (nums[right] > nums[left]){
            swap(nums,left,right);
            break;
        }
    }
    //将逆序翻转成顺序
    reverse(nums,left+1);
}

public void swap(int[] nums,int i,int j){
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

public void reverse(int[] nums,int left){
    int right = nums.length-1;

    while(left < right){
        swap(nums,left,right);
        left++;
        right--;
    }
}

32. 最长有效括号

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

思路

  • 动态规划,dp[i]表示以该位置为最后一个字符时的最大有效括号子串长度
class Solution {
    public int longestValidParentheses(String s) {
        int dp[] = new int[s.length()];
        int ans = 0;
        for (int i = 0;i < s.length();i++) {
            if (s.charAt(i) == '(' || i == 0){//以'('结尾必然不是有效括号
                dp[i] = 0;
            } else if (s.charAt(i-1) == '(' && s.charAt(i) == ')') {//"......()"的形式
                dp[i] = (i >= 2 ? dp[i-2] : 0) + 2;
            } else if (i - dp[i - 1] - 1 >= 0 && s.charAt(i - dp[i - 1] - 1) == '(' ){//"((.....))"的形式
                dp[i] = dp[i - 1] + dp[Integer.max(i - dp[i - 1] - 2,0)] + 2;
            }
            ans = Integer.max(ans,dp[i]);
        }
        return ans;
    }
}
  • 栈,只存储'('的位置
  • 如果是'(',推入栈中
  • 如果是')',将栈中的'('推出一个,然后len = 当前位置 - 接下来的栈顶(相当于原本栈中的第二个'('位置)
  • Why?
  • 栈顶匹配了当前遍历的位置i的')',然后他们之间此时也必然也匹配成功了,所以长度就是第二个还未匹配成功的'('与当前位置的距离
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(n)\)
public int longestValidParentheses(String s) {
    int ans = 0;
    Stack<Integer> stack = new Stack<>();
    stack.push(-1);
    
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '('){
            stack.push(i);
        } else{
            stack.pop();
            if (stack.isEmpty()){
                stack.push(i);
            } else{
                ans = Integer.max(ans,i - stack.peek());
            }
        }
    }

    return ans;
}
  • 左右各扫一遍
  • 设置left,right变量,分别表示从起点到当前点的'('和')'数量
  • 当从左边开始扫时,right > left时,表示当前括号不合法,置为0。
  • 从右边扫同理
  • 为什么要左右各扫一遍?举个例子
  • "()(()()",从左边扫结果为2【因为"(()()"中left和right一直不相同】,而从右边扫结果为4
public int longestValidParentheses(String s) {
    int ans = 0;
    int left = 0, right = 0;

    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '(') {
            left++;
        } else {
            right++;
        }
        if (right > left) {
            left = right = 0;
        } else if (right == left) {
            ans = Integer.max(ans, left << 1);
        }
    }

    left = right = 0;
    for (int i = s.length() - 1; i >= 0; i--) {
        if (s.charAt(i) == '(') {
            left++;
        } else {
            right++;
        }
        if (right < left) {
            left = right = 0;
        } else if (right == left) {
            ans = Integer.max(ans, right << 1);
        }
    }
    return ans;
}

33. 搜索旋转排序数组

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 \(O(log n)\) 级别。

思路8

  • 二分
  • 虽然整体无序,但部分有序
  • 每次选中一个mid后,要么左边有序,要么右边有序
  • 那么,只要看target是否在有序部分即可,若在,则取有序部分,否则取另一部分
  • 若nums[left] <= nums[mid],说明左段有序,看target是否在左段范围内。
  • 反之,则右端有序,看target是否在右段范围内
public int search(int[] nums, int target) {
    //左闭右开区间
    int left = 0,right = nums.length;
    while(left + 1 < right ){
        int mid = (left+right) >> 1;
        if (target == nums[mid]){
            return mid;
        } else if (nums[left] <= nums[mid]){
            if (target >= nums[left] && target < nums[mid]){
                right = mid;
            } else{
                left = mid;
            }
        } else {
            if (target >= nums[mid] && target <= nums[right-1]){
                left = mid;
            } else{
                right = mid;
            }
        }
    }
    return (nums.length != 0 && nums[left] == target) ? left : -1;
}

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

你的算法时间复杂度必须是 O(log n) 级别。

如果数组中不存在目标值,返回 [-1, -1]。

思路

  • 二分基操,查两次,一次开始位置,一次结束位置
class Solution {

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

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

    public int[] searchRange(int[] nums, int target) {
        return new int[]{findLeft(nums,target,true),findLeft(nums,target,false)};
    }
}

39. 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

思路

  • dfs遍历
class Solution {
    private List<List<Integer>> ans ;

    public void dfs(int[] candidates,int curIndex,int target,int sum,List<Integer> list) {
        if (sum == target) {
            ans.add(new ArrayList<>(list));
            return ;
        }
        if (sum > target) {
            return ;
        }
        for (int i = curIndex;i < candidates.length;i++) {
            list.add(candidates[i]);
            dfs(candidates,i,target,sum+candidates[i],list);
            list.remove(list.size() - 1);
        }
    }

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        ans = new ArrayList<>();
        dfs(candidates,0,target,0,new ArrayList<>());
        return ans;
    }
}

41. 缺失的第一个正数(Hard)

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。
你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。

思路

  • 对于长度n的数组来说,其缺失的最小正整数小于等于n+1(当n个数分别为1--n时)。
  • 所以,我们可以对原数组先进行一次伪排序,使得数与下标对应
  • 例如,nums[0] = 1,nums[1] = 2......
  • 这样,当遇到第一个nums[i] != i+1时,显然此时缺失了数i+1,这便是答案。
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(1)\)
public int firstMissingPositive(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        while (nums[i] >= 1 && nums[i] <= nums.length && nums[i] != nums[nums[i] - 1]) {
            swap(nums, i , nums[i] - 1);
        }
    }
    for (int i = 0; i < nums.length; i++)
        if (nums[i] != i + 1)
            return i + 1;
    return nums.length + 1;
}

public void swap(int[] nums, int l, int r) {
    int temp = nums[l];
    nums[l] = nums[r];
    nums[r] = temp;
}

42. 接雨水(Hard)

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

思路

  • 单调栈
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(n)\)
  • 设置一个单调递减的栈,遍历一遍数组
  • 当前元素比栈顶大时,弹出栈顶,若此后栈不为空,则计算雨水量
public int trap(int[] height) {
    int ans = 0;
    Stack<Integer> stack = new Stack<>();
    for(int i = 0;i < height.length; i++) {
        while (!stack.empty() && height[i] > height[stack.peek()]) {
            int now = stack.pop();
            if (stack.empty()) {
                continue;
            }
            int h = Integer.min(height[i], height[stack.peek()]) - height[now];
            int l = i - stack.peek() - 1;
            ans += h * l;
        }
        stack.push(i);
    }

    return  ans;
}
  • 动态规划,时间和空间复杂度同上
  • 当前位置的水量 = min(左右最高点的最小值 - 当前位置,0)
  • 因此需要用数组存左右最高点
class Solution {
    public int trap(int[] height) {
        int max_left[] = new int[height.length];
        int max_right[] = new int[height.length];
        int ans = 0;
        for (int i = 0;i < height.length;i++) {
            max_left[i] = Integer.max(i == 0 ? 0 :max_left[i-1],height[i]);
        }
        for (int i = height.length - 1;i >= 0;i--) {
            max_right[i] = Integer.max(i == height.length - 1 ? 0 : max_right[i+1],height[i]);
        }
        for (int i = 0;i < height.length;i++) {
            int min = Integer.min(max_left[i],max_right[i]);
            ans += Integer.max(0,min - height[i]);
        }
        return ans;
    }
}
  • 动态规划通过双指针进行空间复杂度优化
  • 将dp数组用两个变量存储,从而将空间复杂度降到\(O(1)\)
class Solution {
    public int trap(int[] height) {
        int maxLeft = 0,maxRight = 0;
        int left = 0,right = height.length - 1;
        int ans = 0;
        while (left <= right) {
            if (maxLeft < maxRight) {
                if (height[left] <= maxLeft){
                    ans += maxLeft - height[left++];
                } else {
                    maxLeft = height[left++];
                }
            } else {
                if (height[right] <= maxRight) {
                    ans += maxRight - height[right--];
                } else {
                    maxRight = height[right--];
                }
            }
        }
        return ans;
    }
}
  • 其他人的比较巧妙的题解,双指针按层计算,总面积减去柱子面积
  • 时间复杂度\(O(n)\)??题解上写的,这部分存疑,因为每次循环只计算一层的面积,所以如果高度m>n的话,时间复杂度\(O(m)\)
  • 空间复杂度\(O(1)\)
  • 雨水和柱子的总面积减去柱子的面积便是雨水的面积,也就是答案
  • 柱子的面积很好求,就是数组之和,重点在于雨水和柱子的总面积
  • 雨水和柱子的形状从中间某个最高层,向左右递减
  • 采用双指针,左指针初始化为0,右指针为数组最右端
  • 接下来左指针向右移,右指针向左移,并加上每层的总面积
public int trap(int[] height) {
    int left = 0, right = height.length - 1;
    int allSum = 0, allHeight = 0, rowHeight = 1;

    while (left <= right) {
        while (left <= right && height[left] < rowHeight) {
            allHeight += height[left];
            left++;
        }
        while (left < right && height[right] < rowHeight) {
            allHeight += height[right];
            right--;
        }
        if (left <= right) {
            allSum += right - left + 1;
            rowHeight++;
        }
    }

    return allSum - allHeight;
}

44. 通配符匹配(Hard)

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '' 的通配符匹配。
'?' 可以匹配任何单个字符。
'
' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

思路

  • 记忆化搜索
  • 时间复杂度\(O(NP)\)(记忆化搜索的优化)
  • 空间复杂度\(O(NP)\)
  • 当前字符相同或p字符为'?'时,两字符串都向右移动
  • 当前p字符为'*'时,s字符串向右移动或p字符串向右移动
  • 通过dp字符串保存数据,下次递归遇到时直接返回
  • 优化:多个连续'*'可优化成一个,从而减小P的长度,但时间复杂度本质上还是\(O(NP)\)
int dp[][] = null;

public boolean isMatch(String s, String p, int startS, int startP) {
    if (dp[startS][startP] != -1)
        return dp[startS][startP] == 1;
    boolean flag = false;
    if (startS == s.length() && startP == p.length())
        flag = true;
    else if (s.length() == startS) {
        flag = true;
        for (int i = startP; i < p.length(); i++)
            if (p.charAt(i) != '*')
                flag = false;
    } else if (p.length() == startP)
        flag = false;
    else if (s.charAt(startS) == p.charAt(startP) || p.charAt(startP) == '?')
        flag = isMatch(s, p, startS + 1, startP + 1);
    else if (p.charAt(startP) == '*') {
        flag = isMatch(s, p, startS + 1, startP) || isMatch(s, p, startS, startP + 1);
    }
    dp[startS][startP] = flag ? 1 : 0;
    return flag;
}

public boolean isMatch(String s, String p) {
    if (dp == null)
        dp = new int[s.length() + 1][p.length() + 1];
    for (int i = 0; i < s.length() + 1; i++)
        Arrays.fill(dp[i], -1);
    return isMatch(s, p, 0, 0);
}

45. 跳跃游戏 II(Hard)

给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。

思路

  • 队列
  • 时间复杂度\(O(n^2)\)
  • 空间复杂度\(O(n)\)
  • 假设当前位置在x,nums[x] = y,那么就将x+1 ~ x+y加入队列中,其中已经走过的点就跳过
  • 在Leetcode中超时
int jump(vector<int>& nums) {
    queue<int> q;
    int visi[nums.size()+5];
    memset(visi,0,sizeof(visi));
    q.push(0);
    while(!q.empty()){
        int now = q.front();
        q.pop();
        for(int i = 1;i <= nums[now] && i+now < nums.size();i++){
            if (now+i == nums.size() -1 ) return visi[now]+1;
            if (visi[now+i] == 0){
                visi[now+i] = visi[now]+1;
                q.push(now+i);
            }
        }
    }
    return 0;
}
  • 贪心
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(1)\)
  • 每次跳最远的距离
  • 举个例子,nums[0] = 5,即从起点跳的最近距离为1,最远距离为5。
  • 那么,走到对于点[1,5]来说,他们的最小步数是1。
  • 然后我们遍历一遍[1,5],更新最长距离,例如通过[1,5]可以走到点9,那么,走到点5时最小步数+1,则[6,9]的最小步数为2。
int jump(vector<int>& nums) {
    int maxn = 0, ans = 0,end = 0;

    for(int i = 0;i < nums.size() - 1 ;i++){
        maxn = max(maxn,i+nums[i]);
        if (i == end){
            end = maxn;
            ans++;
        }
    }
    
    return ans;
}

46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

思路

  • dfs遍历即可
  • 当前值每次都与后面的数字(包括本身)交换一遍,交换完后交换回去
void dfs(int now,vector<int>& nums, vector<vector<int>> &ans){
    if (now == nums.size() - 1){
        ans.push_back(nums);
        return ;
    }

    for(int i = now;i < nums.size();i++){
        swap(nums[now],nums[i]);
        dfs(now+1,nums,ans);
        swap(nums[now],nums[i]);
    }
}

vector<vector<int>> permute(vector<int>& nums) {
    sort(nums.begin(),nums.end());
    vector<vector<int>> ans;
    dfs(0,nums,ans);
    return ans;
}
class Solution {
    private List<List<Integer>> ans = null;
    public void swap(int[] nums,int x,int y) {
        int temp = nums[x];
        nums[x] = nums[y];
        nums[y] = temp;
    }
    public void dfs(int[] nums,int curIndex) {
        if (curIndex == nums.length - 1) {
            ans.add(Arrays.stream(nums).boxed().collect(Collectors.toList()));
            return ;
        }
        for (int i = curIndex;i < nums.length;i++) {
            swap(nums,curIndex,i);
            dfs(nums,curIndex+1);
            swap(nums,curIndex,i);
        }
    }
    public List<List<Integer>> permute(int[] nums) {
        Arrays.sort(nums);
        ans = new ArrayList<>();
        dfs(nums,0);
        return ans;
    }
}

47. 全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列。

思路

  • 相对于前面一题,只多了三行代码
  • 只要禁止相同元素的交换即可
  • 使用个set判断后面是否有相同元素,如果有,则跳过相同元素
void dfs(int now,vector<int>& nums, vector<vector<int>> &ans){
    if (now == nums.size() - 1){
        ans.push_back(nums);
        return ;
    }
    
    unordered_set<int> vis;//代码1
    for(int i = now;i < nums.size();i++){
        if (vis.find(nums[i]) == vis.end()){//代码2
            swap(nums[now],nums[i]);
            dfs(now+1,nums,ans);
            swap(nums[now],nums[i]);
            vis.insert(nums[i]);//代码3
        }
    }
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
    sort(nums.begin(),nums.end());
    vector<vector<int>> ans;
    dfs(0,nums,ans);
    return ans;
}

48. 旋转图像

给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

思路

  • 找规律
  • 每次交换都等同于四个元素之间的交换,所以只要找出四个元素的索引即可
void rotate(vector<vector<int>>& matrix) {
    int n = matrix.size();
    for(int i = 0;i < (n>>1);i++){
        for(int j = i;j < n - i - 1;j++){
            swap(matrix[i][j],matrix[j][n-1-i]);
            swap(matrix[i][j],matrix[n-1-i][n-1-j]);
            swap(matrix[i][j],matrix[n-1-j][i]);
        }
    }
    return ;
}
class Solution {
    public void swap(int[][] matrix,int x1,int y1,int x2,int y2) {
        int temp = matrix[x1][y1];
        matrix[x1][y1] = matrix[x2][y2];
        matrix[x2][y2] = temp;
    }
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        for (int i = 0;i < ((n+1)>>1);i++) {
            for (int j = 0;j < (n>>1);j++) {
                swap(matrix,i,j,j,n-1-i);
                swap(matrix,i,j,n-1-i,n-1-j);
                swap(matrix,i,j,n-1-j,i);
            }
        }
    }
}

49. 字母异位词分组

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

思路

  • 计算字符串的每个字母的数量,然后组合成String作为key即可
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String,List> map = new HashMap<>();
        int cnt[] = new int[26];
        for (String str : strs) {
            Arrays.fill(cnt,0);
            StringBuffer key = new StringBuffer("");
            for (int i = 0;i < str.length();i++){
                cnt[str.charAt(i) - 'a']++;
            }
            for (int i = 0;i < 26;i++){
                key.append(","+cnt[i]);
            }
            if (map.containsKey(key.toString())) {
                map.get(key.toString()).add(str);
            } else {
                List<String> value = new ArrayList<String>();
                value.add(str);
                map.put(key.toString(),value);
            }
        }
        return new ArrayList(map.values());
    }
}

50. Pow(x, n)

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

思路

  • 快速幂
  • 坑点:int的最小值转化为最大值时会爆int
double myPow(double x, int n) {
    double ans = 1;
    long long m = n;
    if (m < 0){
        m = m*-1;
    }
    while(m){
        if (m&1) ans *= x;
        m>>=1;;
        x *= x;
    }
    return n < 0 ? 1.0/ans : ans;
}

51. N皇后(Hard)

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

思路

  • dfs
  • 使用四个数组,分别代表行,列,两条斜线,判断是否有其他棋子
  • 斜线:行-列唯一,行+列唯一
bool row[1005];
bool colm[1005];
bool ele1[3000];
bool ele2[3000];
void change(int x,int y,bool flag,int n){
        row[x] = flag;
        colm[y] = flag;
        ele1[x-y+n] = flag;
        ele2[x+y] = flag;
}

void dfs(int x,int y, vector<string> now,vector<vector<string>> &ans,int n){
    if (x == n ){
        ans.push_back(now);
        return ;
    }
    for(int i= y;i < n;i++){
        if (!row[x] && !colm[i] && !ele1[x-i+n] && !ele2[x+i]){
            change(x,i,true,n);
            string temp = now[x];
            now[x] +=  "Q";
            while(now[x].size() < (unsigned int)n) now[x]+=".";
            dfs(x+1,0,now,ans,n);
            change(x,i,false,n);
            now[x] = temp;
        }
        now[x] += ".";
    }
}

vector<vector<string>> solveNQueens(int n) {
    vector<vector<string>> ans;
    vector<string> now;
    memset(row,false,sizeof(row));
    memset(colm,false,sizeof(colm));
    memset(ele1,false,sizeof(ele1));
    memset(ele2,false,sizeof(ele2));
    for(int i = 0;i < n;i++)
    	now.push_back("");
    dfs(0,0,now,ans,n);
    return ans;
}

52. N皇后 II(Hard)

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回 n 皇后不同的解决方案的数量。

思路

  • 因为只需要方案数目,所以相对于上一次反而简单很多
  • 简化一下上一题的代码即可
int ans;
bool row[1005];
bool colm[1005];
bool ele1[3000];
bool ele2[3000];
void change(int x,int y,bool flag,int n){
        row[x] = flag;
        colm[y] = flag;
        ele1[x-y+n] = flag;
        ele2[x+y] = flag;
}

void dfs(int x,int y,int n){
    if (x == n ){
        ans++;
        return ;
    }
    for(int i= y;i < n;i++){
        if (!row[x] && !colm[i] && !ele1[x-i+n] && !ele2[x+i]){
            change(x,i,true,n);
            dfs(x+1,0,n);
            change(x,i,false,n);
        }
    }
}

int totalNQueens(int n) {
	ans = 0;
    memset(row,false,sizeof(row));
    memset(colm,false,sizeof(colm));
    memset(ele1,false,sizeof(ele1));
    memset(ele2,false,sizeof(ele2));
    dfs(0,0,n);
    return ans;
}

54. 螺旋矩阵

给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。

思路

  • 每次按照顺时针遍历最外层的元素
public List<Integer> spiralOrder(int[][] matrix) {
    List<Integer> list = new LinkedList<>();
    if (matrix.length == 0) return list;

    int left = 0,right = matrix[0].length-1;
    int up = 0,down = matrix.length-1;

    while(left <= right && up <= down){
        for(int i = left;i <= right;i++)
            list.add(matrix[up][i]);
        for(int i = up+1;i <= down;i++)
            list.add(matrix[i][right]);
        if (left < right && up < down) {
            for (int i = right - 1; i >= left; i--)
                list.add(matrix[down][i]);
            for (int i = down - 1; i >= up + 1; i--)
                list.add(matrix[i][left]);
        }
        left++;
        up++;
        down--;
        right--;
    }

    return list;

}

55. 跳跃游戏

思路

  • 用一个maxn变量记录下可到达的最远距离即可
  • 可优化点:maxn >= len-1时可以提前return
bool canJump(vector<int>& nums) {
    int len = nums.size();
    int maxn = 0;

    for(int i = 0;i < len && i <= maxn;i++){
        maxn = max(maxn,i+nums[i]);
    }

    return maxn >= len-1;
}

56. 合并区间

给出一个区间的集合,请合并所有重叠的区间。

思路

  • 先按左边界排序,再按右边界排序
  • 当前区间的左边界大于前一个区间有边界时,说明分开了,将前一个区间放进答案,否则将当前区间扩展到前一个区间
vector<vector<int>> merge(vector<vector<int>>& intervals) {
    int n = intervals.size();
    vector<vector<int>> ans ;
    if (n == 0) return ans;
    sort(intervals.begin(),intervals.end());
    int left = intervals[0][0],right = intervals[0][1];

    for(int i = 1;i < n;i++){
        if ( intervals[i][0] > right ){
            ans.push_back({left,right});
            left = intervals[i][0];
            right = intervals[i][1];
        }
        else{
            right = max(right,intervals[i][1]);
        }
    }
    ans.push_back({left,right});
    return ans;
}
  • 由于是hot100的题,四个月后又来刷了一次,用的Java,没想到和以前的C++代码几乎一模一样
class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0) return new int[0][2];
        Arrays.sort(intervals,(a,b)->{
            return a[0] - b[0];
        });
        List<int[]> list = new ArrayList<>();
        int left = intervals[0][0],right = intervals[0][1];
        
        for (int i = 1;i < intervals.length;i++) {
            if (intervals[i][0] > right) {
                list.add(new int[]{left,right});
                left = intervals[i][0];
                right = intervals[i][1];
            } else if (intervals[i][1] > right){
                right = intervals[i][1];
            }
        }
        list.add(new int[]{left,right});
        return list.toArray(new int[list.size()][]);
    }
}

57. 插入区间(Hard)

给出一个无重叠的 ,按照区间起始端点排序的区间列表。
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。

思路

  • 跟前面那题思路差不多,其实算不上Hard级别
  • 使用一个滑动窗口,初始化为新的区间
  • 一旦旧区间与新区间重叠就合并即可
  • 注意事项:到最后如果滑动窗口还在合并阶段时,别忘了将滑动窗口加入区间集合中
class Solution {
public:
    vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
        if (intervals.empty()){
            return vector<vector<int>>({newInterval});
        }
        else if (newInterval.empty()){
            return intervals;
        }
        
        int n = intervals.size();
        int left = newInterval[0],right = newInterval[1];
        bool flag = true;
        vector<vector<int>> ans;

        for(int i = 0;i < n;i++){
            if (intervals[i][1] < left || intervals[i][0] > right){
                if (intervals[i][0] > right && flag){
                    ans.push_back({left,right});
                    flag = false;     
                }
                ans.push_back(intervals[i]);
            }
            else{
                left = min(left,intervals[i][0]);
                right = max(right,intervals[i][1]);
            }
            if (i == n-1 && flag){
                ans.push_back({left,right});
            }
        }

        return ans;
    }
};

60. 第k个排列

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记。
给定 n 和 k,返回第 k 个排列。
当 n = 3 时, 所有排列如下:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"

思路

  1. 全排序遍历出所有结果后排序
  2. 初始化第一个排列,然后依次求下一个排列,直到第k个
  3. 康拓展开(最佳方法),重点讲这个
  • 时间复杂度\(O(n^2)\)
  • 对于数字1~n组成的字符串来说,其全排列总数为n!
  • 以任意数为开头,其全排列总数为(n-1)!,例如1xxx,2xxx,3xxx,4xxx
  • 我们只要通过k/ (n-1)! 便可知道第一个数是哪个数
    • 需要注意的是为了方便运算,k需要先减1
    • 举个例子
      • 假设n = 5,k = 120
      • 我们需要求的第一个数是 120/4! = 5 ,也就是第6个数
      • 显然n = 5时不存在数字6
      • 或者数组nums下标04存放15,显然无法获取nums[5]
  • 同理,k %= (n-1)!后,第2个数就可以通过k / (n-2)! 获取
int A(int n){
    int ans = 1;
    while(n > 0){
        ans *= n--;
    }
    return ans ;
}

void delete_i(int n,int k,char nums[]){
    for(int i = k;i < n-1;i++)
        nums[i] = nums[i+1];
}

string getPermutation(int n, int k) {
    string ans = "";
    char nums[20];
    for(int i = 0;i < n;i++)
        nums[i] = '1'+i;
    k--;
    for(int i = 0;i < n;i++){
        int factor = A(n - i - 1);
        int pre = k/factor;
        k %= factor;
        ans += nums[pre];
        delete_i(n-i,pre,nums);
    }
    return ans;
}

61. 旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

思路

  • 将链表分成两部分,将后部分连接到前部分即刻
  • 例如,{1,2,3,4,5},k = 2,将链表分成{{1,2,3},{4,5}}
  • 将后部分连接到前部分,得{{4,5},{1,2,3}}
public ListNode rotateRight(ListNode head, int k) {
    if (head == null || head.next == null)
        return head;
    ListNode temp = head;
    int len = 0;
    while(temp != null){
        len++;
        temp = temp.next;
    }
    k %= len;
    if (k == 0)
        return head;
    ListNode nowHead = head;
    while(--len != k){
        nowHead = nowHead.next;
    }
    ListNode newHead = nowHead.next;
    nowHead.next = null;
    temp = newHead;
    while(temp.next != null){
        temp = temp.next;
    }
    temp.next = head;

    return newHead;
}

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?

思路

  • 数学法:机器人向右走m-1步,向下走n-1步后到达目的地,总步数为m+n-2,那么我们只要在(m+n-2)个数中取出m-1(或者n-1)个表示向右走,那么剩下的就是向下走了。(有个坑就是越界问题

    class Solution {
        public int uniquePaths(int m, int n) {
            int min = Integer.min(n,m);
            int max = Integer.max(n,m);
            long ans = 1L;
            int j = 2;
            int i = m+n-2;
            for (int cur = 0;cur < min -1;cur++){
                ans *= i--;
                while (j <= min - 1 &&  ans % j == 0) ans /= j++;
            }
            return (int)ans;
        }
    }
    
  • 动态规划:\(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];
            for (int i = 0;i < m;i++){
                for (int j = 0;j < n;j++) {
                    dp[i][j] = i == 0 && j == 0 ? 1 : 0;
                    dp[i][j] += i != 0 ? dp[i-1][j] : 0;
                    dp[i][j] += j != 0 ? dp[i][j-1] : 0;
                }
            }
            return dp[m-1][n-1];
        }
    }
    
    • 下一题多了障碍物,所以走到障碍物位置的次数为0即可,公式如下
    • \(dp[i][j] = (grid[i][j] == '1' ? 0 : dp[i-1][j]+dp[i][j-1])\)

75. 颜色分类

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

思路

  • 百度一面的时候出过这道题,可惜没做过,当初刷到这里的时候不知道为什么跳过了,血亏。
  • 然后面试官改成0和1两种颜色,我也想到了双指针,思路跟这差不多,可惜代码有点问题
class Solution {
    public void swap(int[] nums,int x,int y) {
        int temp = nums[x];
        nums[x] = nums[y];
        nums[y] = temp;
    }
    public void sortColors(int[] nums) {
        int p0 = 0,p1 = 0,p2 = nums.length - 1;
        while (p1 <= p2) {
            if (nums[p1] == 0) swap(nums,p0++,p1++);//p0只会在p1位置或后面,只可能是0和1
            else if (nums[p1] == 2) swap(nums,p2--,p1);//p2位置的数值可能是0,1,2,因此需要再遍历
            else p1++;
        }
    }
}

72. 编辑距离(Hard)

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

思路

  • 动态规划
  • 时间复杂度\(O(n^2\))
  • 空间复杂度\(O(n^2)\)
public int minDistance(String word1, String word2) {
    //dp[i][j]表示word1的前i个组成word2的前j个的最少操作多少操作
    int dp[][] = new int[word1.length() + 1][word2.length() + 1];

    //当word1或word2为0时,需要添加/删除对方Word的长度
    for (int i = 0; i <= word1.length(); i++)
        dp[i][0] = i;
    for (int j = 0; j <= word2.length(); j++)
        dp[0][j] = j;

    for (int i = 1; i <= word1.length(); i++) {
        for (int j = 1; j <= word2.length(); j++) {
            dp[i][j] = (word1.charAt(i - 1) == word2.charAt(j - 1) ? dp[i - 1][j - 1]
                    : (1 + Integer.min(dp[i - 1][j - 1], Integer.min(dp[i][j - 1], dp[i - 1][j]))));
        }
    }

    return dp[word1.length()][word2.length()];
}

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。

思路

  • \(matrix[i][j] = 0\)时,将\(matrix[i][0]\)\(matrix[0][j]\)置为0,表示该行该列为0
  • 需要特判下第一行和第一列的情况
public void setZeroes(int[][] matrix) {
    boolean isRow = false;
    boolean isColm = false;

    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            if (i == 0 && matrix[i][j] == 0)
                isRow = true;
            if (j == 0 && matrix[i][j] == 0)
                isColm = true;
            if (matrix[i][j] == 0)
                matrix[i][0] = matrix[0][j] = 0;
        }
    }

    for (int i = 1; i < matrix.length; i++) {
        for (int j = 1; j < matrix[i].length; j++) {
            if (matrix[i][0] == 0 || matrix[0][j] == 0)
                matrix[i][j] = 0;
        }
    }

    if (isRow) {
        for (int i = 0; i < matrix[0].length; i++)
            matrix[0][i] = 0;
    }

    if (isColm) {
        for (int i = 0; i < matrix.length; i++)
            matrix[i][0] = 0;
    }
}

76. 最小覆盖子串(Hard)

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。

思路

  • 双指针滑动窗口
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(1)\)(char字符最多256种)
  • 对于任意区间,其左边界无限延申都可以,所以可以先确定右边界,再将左边界逐渐向右移动,减少距离
  • 难点:left与right的边界设置问题,容易出现死循环
class Solution {
    public boolean check(Map<Character,Integer> map){
        for(Integer value : map.values()){
            if (value > 0)
                return false;
        }
        return true;
    }

    public String minWindow(String s, String t) {
        String ans = "";
        int left = -1,right = 0;
        Map<Character,Integer> map = new HashMap<>();

        for(int i = 0;i < t.length();i++){
            if (map.containsKey(t.charAt(i))) {
                map.put(t.charAt(i), map.get(t.charAt(i)) + 1);
            } else {
                map.put(t.charAt(i), 1);
            }
        }

        while (right < s.length()){
            while(right < s.length() && !check(map)){
                if (map.containsKey(s.charAt(right)) ){
                    map.put(s.charAt(right),map.get(s.charAt(right))-1);
                }
                right++;
            }
            while(left < right && check(map)){
                if (right > left && (ans.equals("") || ans.length() > right-left-1)){
                    ans = s.substring(left+1,right);
                }
                left++;
                if (map.containsKey(s.charAt(left))){
                    map.put(s.charAt(left),map.get(s.charAt(left))+1);
                }
            }
        }

        return ans;
    }
}
  • 优化,不用每次都遍历一次map,而是用个sum变量存储t的字符个数
class Solution {
    public String minWindow(String s, String t) {
        HashMap<Character,Integer> map = new HashMap<>();
        int sum = 0,left = -1,right = 0;
        char key;
        String ans = "";
        for (int i = 0;i < t.length();i++){
            if (map.containsKey(key = t.charAt(i))) {
                map.put(key,map.get(key)+1);
            } else {
                map.put(key,1);
            }
            sum++;
        }
        while (right < s.length()) {
            while (right < s.length() && sum > 0) {
                if (map.containsKey(key = s.charAt(right))) {
                    if (map.get(key) > 0) {
                        sum--;
                    }
                    map.put(key,map.get(key)-1);
                }
                right++;
            }
            while (left < right && sum == 0 ) {
                if (ans.equals("") || ans.length() > right - left - 1) {
                    ans = s.substring(left + 1,right);
                }
                left++;
                if (map.containsKey(key = s.charAt(left))) {
                    if (map.get(key) == 0) {
                        sum++;
                    }
                    map.put(key,map.get(key)+1);
                }
            }

        }
        return ans;
    }
}

77. 组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

思路

  • dfs
List<List<Integer>> ans ;
void dfs(List<Integer> cur,int now,int n,int k){
    if (k == 0) ans.add(new ArrayList<>(cur));
    
    for(int i = now+1;i <= n;i++){
        cur.add(i);
        dfs(cur,i,n,k-1);
        cur.remove(cur.size()-1);
    }
}

public List<List<Integer>> combine(int n, int k) {
    ans = new ArrayList<List<Integer>>();
    dfs(new ArrayList<Integer>(),0,n,k);
    return ans;
}

78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

思路

  • dfs
  • 跟前面一题基本一致吧
public class code78 {
    List<List<Integer>> ans;

    void dfs(List<Integer> cur, int now, int[] nums) {
        ans.add(new ArrayList<>(cur));

        for (int i = now + 1; i < nums.length; i++) {
            cur.add(nums[i]);
            dfs(cur, i, nums);
            cur.remove(cur.size() - 1);
        }
    }

    public List<List<Integer>> subsets(int[] nums) {
        ans = new ArrayList<>();
        dfs(new ArrayList<>(), -1, nums);
        return ans;
    }
}

79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

思路

  • dfs暴力搜索
  • 可优化点:dfs传入遍历到的word的当前位置,而不是直接传一个substring
class Solution {
    public boolean dfs(char[][] board, String word, int x,int y) {
        if (word.length() == 0) return true;
        if (x  < 0 || x >= board.length || y < 0 || y >= board[0].length) return false;
        if (board[x][y] != word.charAt(0) || board[x][y] == '#') return false;
        char temp = board[x][y];
        board[x][y] = '#';
        boolean ret = dfs(board,word.substring(1),x+1,y) || dfs(board,word.substring(1),x-1,y)
        || dfs(board,word.substring(1),x,y+1) || dfs(board,word.substring(1),x,y-1);
        board[x][y] = temp;
        return ret;
    }

    public boolean exist(char[][] board, String word) {
        if (board.length == 0) return false;
        for (int i = 0;i < board.length;i++) {
            for (int j = 0;j < board[0].length;j++) {
                if (dfs(board,word,i,j)) return true;
            }
        }
        return false;
    }
}

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

思路

  • 双指针
  • 时间复杂度\(O(n)\)
  • 空间复杂度\(O(1)\)
  • 当遇到第三个相同数时,左指针停,右指针向前移动
  • 否则,将右指针的数赋值左指针的数中
  • 简单解释原理,就是右指针表示旧数组,左指针表示新数组,当遇到第三个相同数时,新数组不变(无视该数),旧数组继续向前遍历
  • 最后左指针的位置便是数组最终长度
public int removeDuplicates(int[] nums) {
    int left = 1,right = 1,cnt = 1;

    while(right < nums.length){
        if (nums[right] == nums[right-1]){
            cnt++;
        }
        else{
            cnt = 1;
        }
        if (cnt <= 2){
            nums[left++] = nums[right];
        }
        right++;
    }

    return left;
}

81. 搜索旋转排序数组 II

思路

  • 伪二分算法
  • 时间复杂度\(O(log n )\) ~\(O(n)\)
  • 这题目也没什么官方题解,在特殊情况下只能\(O(n)\)
  • 例如[1,3,1,1,1]和[1,1,1,3,1],你没法通过二分的形式排除任意一边,就只能排除一个,所以只能伪二分
public boolean search(int[] nums, int target) {
    int left = 0,right = nums.length - 1;

    while(left <= right){
        int mid = (left+right)>>1;
        if (target == nums[mid]) 
            return true;
        if (nums[left] == nums[mid])//特例只能+1
            left++;
        else if (nums[left] <= nums[mid]){
            if (target >= nums[left] && target < nums[mid])
                right = mid - 1;
            else
                left = mid + 1;
        }
        else{
            if (target > nums[mid] && target <= nums[right])
                left = mid + 1;
            else 
                right = mid - 1;
        }
    }

    return false;
}

82. 删除排序链表中的重复元素 II

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

思路

  • 水题,虽然标着Medium难度,但可以看作Easy
  • 时间复杂度\(O(n)\)
  • 当遇到一个节点与下一节点相同时,遍历掉这些相同的节点即刻
public ListNode deleteDuplicates(ListNode head) {
    if (head == null)
        return head;
    ListNode newHead = new ListNode(0);
    ListNode nowNode = newHead;
    while(head != null){
        while (head != null && head.next != null && head.val == head.next.val){
            int nowVal = head.val;
            while(head != null && head.val == nowVal)
                head = head.next;
        }
        nowNode.next = head;
        nowNode = nowNode.next;
        head = (head ==  null ? null :head.next);
    }
    return newHead.next;
}

84. 柱状图中最大的矩形(Hard)

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。

思路

  • 单调栈
  • 时间复杂度 = 空间复杂度 = \(O(n)\)
  • 以任意一个柱子为中心,找距离该柱子最近的两个端点,且端点高度小于该柱子,这两端为开区间,不计算在面积内。
  • 设置一个单调递增的栈,当前元素小于栈头时,此时矩形长度为(第二个元素位置,当前元素位置),高度为栈顶,注意是开区间。
  • 为了方便,建议在原数组的前后加上两个0
  • 否则,当遍历柱子结束时,假设栈不为空,还是要继续判断
public int largestRectangleArea(int[] heights) {
    int ans = 0;
    int[] newHeights = new int[heights.length+2];
    Deque<Integer> stack = new ArrayDeque<>();
    for(int i = 0;i < newHeights.length;i++)
        newHeights[i] = (i == 0 || i == newHeights.length-1)? 0 : heights[i-1];

    for(int i = 0;i < newHeights.length;i++){
        while (!stack.isEmpty() && newHeights[stack.getLast()] > newHeights[i]){
            int h = newHeights[stack.pollLast()];
            ans = Integer.max(ans,h*(i - stack.getLast() - 1));
        }
        stack.addLast(i);
    }

    return ans;
}
  • 四个月后的优化
  • 不用重新弄个数组并在头尾加0
class Solution {
    public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<>();
        int ans = 0;
        for (int i = 0;i <= heights.length;i++) {
            int temp = i == heights.length ? 0 : heights[i];
            while (!stack.isEmpty() && heights[stack.peek()] > temp) {
                int h = heights[stack.pop()];
                int len = i - (stack.isEmpty() ? -1 : stack.peek()) - 1;
                ans = Integer.max(ans,h * len);
            }
            stack.push(i);
        }
        return ans;
    }
}

85. 最大矩形(Hard)

给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

思路

  • 基于上一题的思路即可
public int largestRectangleArea(int[] heights) {
    int ans = 0;
    int[] newHeights = new int[heights.length+2];
    Deque<Integer> stack = new ArrayDeque<>();
    for(int i = 0;i < newHeights.length;i++)
        newHeights[i] = (i == 0 || i == newHeights.length-1)? 0 : heights[i-1];

    for(int i = 0;i < newHeights.length;i++){
        while (!stack.isEmpty() && newHeights[stack.getLast()] > newHeights[i]){
            int h = newHeights[stack.pollLast()];
            ans = Integer.max(ans,h*(i - stack.getLast() - 1));
        }
        stack.addLast(i);
    }

    return ans;
}
public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0)
        return 0;
    int[] dp = new int[matrix[0].length];
    Arrays.fill(dp,0);
    int ans = 0;
    for(int i = 0;i < matrix.length;i++){
        for(int j = 0;j < matrix[i].length;j++){
            dp[j] = (matrix[i][j] == '1' ? dp[j]+1: 0);
        }
        ans = Integer.max(ans,largestRectangleArea(dp));
    }
    return ans;
}

90. 子集 II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

思路

  • 当前元素等于前面一个元素 && 当前元素不是起点时跳过
  • 举个例子,
  • 先把1加入cur,然后dfs遍历{1,1,2},移除1
  • 接下来遇到的两个1跳过,即不再以1为起点
  • 把2加入cur,dfs遍历{},移除2
class Solution {
    List<List<Integer>> ans;
    void dfs(int[] nums,int now,List<Integer> cur){
        ans.add(new ArrayList(cur));

        for(int i = now;i < nums.length;i++){
            if (i == now ||  nums[i] != nums[i-1] ){
                cur.add(nums[i]);
                dfs(nums,i+1,cur);
                cur.remove(cur.size()-1);
            }
        }
    }

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        ans = new ArrayList<>();
        Arrays.sort(nums);
        dfs(nums,0,new ArrayList<>());
        return ans;
    }
}

91. 解码方法

一条包含字母 A-Z 的消息通过以下方式进行了编码,给定一个只包含数字的非空字符串,请计算解码方法的总数。

  • 'A' -> 1
  • 'B' -> 2
  • ...
  • 'Z' -> 26

思路

  • 动态规划
  • 注意事项:没有字母能直接表示0,只能通过表示10和20的字母间接表示0
  • \(s[i] == '0'\)时,若\(s[i-1] != '1'\)\(s[i-1] != '2'\)时,直接返回0,否则\(dp[i] = dp[i-2]\)(这两个数字只能合并,跳过这两个数字)
  • \(s[i-1] == '1'\)时,则\(dp[i] = dp[i-1] + dp[i-2]\)
  • \(s[i-1] == '2'\)\('1'<=s[i]<='6'\)时,同上。
public int numDecodings(String s) {
    if (s.charAt(0) == '0')
        return 0;
    int pre = 1,now = 1;

    for(int i = 1;i < s.length();i++){
        int temp = now;
        if (s.charAt(i) == '0'){
            if (s.charAt(i - 1) != '1' && s.charAt(i - 1) != '2' )
                return 0;
            else now = pre;
        }
        else if (s.charAt(i - 1) == '1' || (s.charAt(i - 1) == '2' && s.charAt(i) >= '1' && s.charAt(i) <= '6'))
            now += pre;
        pre = temp;
    }
    return now;
}

92. 反转链表 II

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

思路

  • 模拟
public ListNode reverseBetween(ListNode head, int m, int n) {
    int k = 1;
    ListNode newHead = new ListNode(0);
    ListNode temp = newHead;
    while(head != null && k != m){//先查找到需要反转的起点
        temp.next = head;
        temp = head;
        head = head.next;
        k++;
    }
    ListNode end = head;//起点是反转链表的终点
    ListNode start = null;
    ListNode pre = null;
    
    while(head != null && k != n+1){//一直遍历到反转链表的下一节点
        if (k == n) //当遍历到反转链表的终点时,作为起点连接
            temp.next = head;
        ListNode nextNode = head.next;
        head.next = pre;
        pre = head;
        head = nextNode;
        k++;
    }
    end.next = head;
    return newHead.next;
}

94. 二叉树的中序遍历

给定一个二叉树,返回它的中序 遍历。

要求:迭代

思路

  • 递归
class Solution {
    private List<Integer> ans = null;
    public void dfs(TreeNode root) {
        if (root == null) return ;
        dfs(root.left);
        ans.add(root.val);
        dfs(root.right);
    }
    
    public List<Integer> inorderTraversal(TreeNode root) {
        ans = new ArrayList<>();
        dfs(root);
        return ans;
    }
}
  • 迭代版
  • 递归的本质就是栈,所以一切递归基本都能用迭代来表达
class Solution {
    private List<Integer> ans = null;
    
    public List<Integer> inorderTraversal(TreeNode root) {
        ans = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.peek();
            if (node.left != null) {
                stack.push(node.left);
                node.left = null;
                continue;
            }
            ans.add(node.val);
            stack.pop();
            if (node.right != null) stack.push(node.right);
        }
        return ans;
    }
}

96. 不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

思路

  • 动态规划,设\(dp[i]\)\(i\)个节点的二叉树种树
  • 以i为根节点,左子树就是\([1,i-1]\),右子树就是\([i+1,n]\)
  • 那么当前的种数就是左右子树所组成的二叉树种树的积
class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = dp[1] = 1;
        for (int i = 2;i <= n;i++) {
            dp[i] = 0;
            for (int j = 0;j < i ;j++) {
                dp[i] += dp[j] * dp[i - j - 1];
            }
        }
        return dp[n];
    }
}

98. 验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

思路

  • 遍历,参数存储左右子树的范围
  • 遍历左子树时,左子树的最大值 < 根节点
  • 遍历右子树时,右子树的最小值 > 根节点
class Solution {
    public boolean dfs(TreeNode root,long min,long max) {
        if (root == null)
            return true;
        if (root.val <= min || root.val >= max)
            return false;
        return dfs(root.left,min,root.val) && dfs(root.right,root.val,max);
    }

    public boolean isValidBST(TreeNode root) {
    	//这里用了long的最大最小值,表示初始范围无穷大(这并不是一个较好的方法,建议使用包装类的null表示范围无限)
        return dfs(root,Long.MIN_VALUE,Long.MAX_VALUE);
    }
}
posted @ 2020-06-23 13:07  MMMMMMMW  阅读(440)  评论(0编辑  收藏  举报