Loading

🔥 LeetCode 热题 HOT 100(1-10)

1. 两数之和

思路一:暴力遍历所有组合

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {
                    return new int[] {i, j};
                }
            }
        }
        //没找到
        return new int[] {-1, -1};
    }
}

思路二:利用map,key存储元素凑成target所需的差值,value存储元素下标

class Solution {
    public int[] twoSum(int[] nums, int target) {
        //value代表下标,key代表target与当前元素只差,即:target - nums[value]
        Map<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};
            } else {
                map.put(target - nums[i], i);
            }
        }
        //没找到
        return new int[] {-1, -1};
    }
}

2. 两数相加

思路:直接模拟加法

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode cur1 = l1;
        ListNode cur2 = l2;
        //哑结点
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;

        //进位
        int carry = 0;
        while (cur1 != null || cur2 != null || carry != 0) {
            int val1 = cur1 != null ? cur1.val : 0;
            int val2 = cur2 != null ? cur2.val : 0;
            int sum = val1 + val2 + carry;
            int reminder = sum % 10;
            carry = sum / 10;

            //尾插法
            ListNode tempNode = new ListNode(reminder);
            cur.next = tempNode;
            cur = cur.next;;

            if (cur1 != null) cur1 = cur1.next;
            if (cur2 != null) cur2 = cur2.next;
        }


        return dummy.next;
    }
}

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

class Solution {
    public int lengthOfLongestSubstring(String s) {
        //窗口左右边界,左闭右开
        int left = 0, right = 0;
        //存储字符及其在窗口中的个数
        Map<Character, Integer> window = new HashMap<>();
        int maxLen = 0;

        while (right < s.length()) {
            //窗口扩大
            char rightChar = s.charAt(right);
            right++;
            window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);

            //如果新加入窗口的字符个数大于1次,窗口应该收缩
            while(window.get(rightChar) > 1) {
                char leftChar = s.charAt(left);
                left++;
                window.put(leftChar, window.get(leftChar) - 1);
            }

            maxLen = Math.max(maxLen, right - left);
        }

        return maxLen;
    }
}

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

思路一:直接合并两个有序数组,根据数组长度求中位数。时间复杂为O(m+n),空间复杂度为O(m+n)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;

        int[] tempArr = new int[m + n];
        int i = 0, j = 0;
        int k = 0;
        while(i < m && j < n) {
            if (nums1[i] < nums2[j]) {
                tempArr[k++] = nums1[i++];
            } else {
                tempArr[k++] = nums2[j++];
            }
        }

        //nums1还有剩余
        while (i < m) {
            tempArr[k++] = nums1[i++];
        }

        //nums2还有剩余
        while (j < n) {
            tempArr[k++] = nums2[j++];
        }

        int midIndex = (m + n) / 2;
        double mid = 0;
        //长度为偶数
        if ((m + n) % 2 == 0) {
            mid = (tempArr[midIndex - 1] + tempArr[midIndex]) / 2.0;
        } else {
            mid = tempArr[midIndex];
        }

        return mid;
    }
}

思路二:无需真正合并数组,先根据两个数组的长度确定中位数是合并后数组中的第几个数,然后根据规则将指向两个数组的下标移动对应次数就好。时间复杂为O(m+n),空间复杂度为O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;

        //i, j分别表示nums1和nums2的分割线,i为[0, m],j为[0, n];
        //[0, i)表示nums1中已经遍历过的下标,对应j类似
        int i = 0, j = 0;
        //记录最新遍历的两个数
        int newest = 0, secondNew = 0;
        //奇数:中位数是第(m+n)/2 + 1个数(下标从1开始)
        //偶数:中位数是第(m+n)/2和第(m+n)/2 + 1数的平均数(下标从1开始)
        int cnt = (m + n) / 2 + 1;
        for (int k = 0; k < cnt; k++) {
            secondNew = newest;
            // j >= n 表示指向nums2已经没有元素 
            if (j >= n || (i < m && nums1[i] < nums2[j])) {
                newest = nums1[i];
                i++;
            } else {
                newest = nums2[j];
                j++;
            }
        }
        //偶数
        if ((m + n) % 2 == 0) {
            return (newest + secondNew) / 2.0;
        } else {
            return newest;
        }
    }
}

终于说服自己

思路三:使用二分法直接在两个数组中找中位数分割线,使得nums1nums2中分割线满足以下性质即可根据分割线左右的数来确定中位数:

前置:m = nums1.lengthn = nums2.length。设inums1中分割线,则取值为[0, m],表示分割线左侧元素下标为[0, i-1],分割线右侧元素下标为[i, m-1];设jnums2中分割线,....。

  • m+n为偶数: i + j = (m + n )/2 ,为奇数:i + j = (m + n)/2 + 1

  • 分割线左侧元素小于等于分割线右侧元素。由于两个数组均为正序数组,则只需要要求:nums1[i-1] <= nums2[j] && nums2[j-1] <= nums1[i];由于该条件等价于在[0, m]中找到最大的i使得nums1[i-1] <= nums2[j],因此可以使用二分查找。(证明:假设我们已经找到了满足条件的最大i,使得nums1[i-1] <= nums2[j],那么此时必有nums[i] > nums2[j],进而有nums[i] > nums2[j-1]

分割线找到后,若m+n为奇数,分割线左侧的最大值即为中位数;若为偶数,分割线左侧的最大值与分割线右侧的最小值的平均数即为中位数。时间复杂度:O(log(min(m, n))),空间复杂度:O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 始终保证nums1为较短的数组,nums1过长可能导致j为负数而越界
        if (nums1.length > nums2.length) {
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }
        
        int m = nums1.length;
        int n = nums2.length;

        // m+n 为奇数,分割线要求左侧有 (m+n)/2 + 1 个元素
        // m+n 为偶数,分割线要求左侧有 (m+n)/2     个元素
        // 两种情况其实可以统一写作 (m+n+1)/2,表示对(m+n)/2向上取整
        // 对整数来说,向上取整等于:(被除数 + (除数 - 1)) / 除数
        // 也可以使用Math类中提供的库函数
        int leftTotal = (m + n + 1) / 2;
        int left = 0, right = m;
        while (left < right) {
            // +1 向上取整避免 left + 1 = right 时可能无法继续缩小区间而陷入死循环
            int i = left + (right - left + 1) / 2;
            int j = leftTotal - i;
            
            //要找最大i,使得nums1[i-1] <= nums2[j]
            //使用对立面缩小区间
            if (nums1[i - 1] > nums2[j]) {
                // [i+1, m]均不满足
                right = i - 1;
            } else {
                // i满足说明[0, i-1]均不为满足条件的最大i,舍去以缩小区间
                left = i;
            }
        }

        //退出循环时left=right,表示最终nums1中分割线的位置
        int i = left;
        //nums2中分割线的位置
        int j = leftTotal - left;
        System.out.println(i);

        //判断极端情况
        int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];  //nums1分割线左边没有元素
        int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];  //nums2分割线左边没有元素
        int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];     //nums1分割线右边没有元素
        int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];     //nums2分割线右边没有元素

        if ((m + n) % 2 == 0) {
            return (Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin)) / 2.0;
        } else {
            return Math.max(nums1LeftMax, nums2LeftMax);
        }
    }
}

备注:这里使用的二分法和二分法查找x的平方根使用的方法很像,都是要查找满足条件的最大值。

参考:官方题解:方法二wei哥:二分查找定位短数组的「分割线」(Java )

5. 最长回文子串

思路:动态规划获取任意两个区间的子串是否为回文子串,如果是则记录开始下标和长度

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        int maxLen = 0;
        int start = 0;

        //dp[i][j]: 字符串s[i, j]是否为回文字符串
        boolean[][] dp = new boolean[len][len];

        for (int j = 0; j < len; j++) {
            for (int i = 0; i <= j; i++) {
                if (s.charAt(i) == s.charAt(j) && (j - i <= 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    if (j - i + 1 > maxLen) {
                        maxLen = j - i + 1;
                        start = i;
                    }
                }
            }
        }

        return s.substring(start, start + maxLen);
    }
}

10. 正则表达式匹配

class Solution {
    public boolean isMatch(String s, String p) {
        int sLen = s.length();
        int pLen = p.length();

        //状态:dp[i][j] 表示s的前i个字符和p的前j个字符能否匹配,即 s[0, i-1] 和 p[0, j-1] 能否匹配
        boolean[][] dp = new boolean[sLen + 1][pLen + 1];

        //前置:i, j分别代表dp的横、纵下标,对应的s、p的下标都应减去1
        //初始值:dp 默认都为flase
        //dp[0][0] = true, 即s和p都为空
        //dp[i][0] = false, 其中i >= 1, 即s不为空p为空
        //dp[0][1] = false, 由于p[0]不能为*,s为空,p只有一个字符且不为'*'的情况下必然不能匹配成功
        //s为空,p不为空且p[0, j-1]以'*'结尾时,还不能直接断定dp的值。
        //因为'*'可以选择将它前面的字符匹配零次以消除'*'前面的字符。
        //因此 dp[0][j] = dp[0][j - 2], j >= 2。若s为空,p不为空且 p[0, j-1] 不以'*'结尾,那么有:
        //dp[0][j] = false, j >= 1。
        dp[0][0] = true;
        for (int j = 2; j < pLen + 1; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2];
            }
        }

        //状态转移:
        //当 s[0, i-1] 和 p[0, j-1] 的末尾字符相等或p的末尾字符为'.',则有:dp[i][j] = dp[i - 1][j - 1];
        //当 p[0, j-1] 的末尾字符即 p[j - 1] 为'*'需要讨论:
        //  若 s[i - 1] 与 p[j - 2] 相等,如:s(ab), p(cab*),那么*可以选择将b 重复一次 则s末尾的b和p末尾的b*匹配抵消,
        //    则有:dp[i][j] = dp[i - 1][j - 2];
        //    同时*也可以选择将b 重复多次 以匹配抵消s中最后一个b,此时p虽然也损失一个b但任然还剩多个b,可以将其看成b*
        //    故有:dp[i][j] = dp[i - 1][j];
        //    此外*也可以选择将b 去掉,故:dp[i][j] = dp[i][j - 2]
        //    综上:dp[i - j] = dp[i - 1][j - 2] || dp[i - 1]dp[j] || dp[i][j - 2]
        //  若 p[j - 2] 为 '.',同上
        //  若 s[i - 1] 与 p[j - 2] 不等且后者不为'.',如:s(ab), p(eabd*),那么*可以选择将d 去掉 ,
        //    则有:dp[i][j] = dp[i][j - 2]
        for (int j = 1; j < pLen + 1; j++) {
            for (int i = 1; i < sLen + 1; i++) {
                if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.') {
                    dp[i][j] = dp[i - 1][j - 1];
                } else if (p.charAt(j - 1) == '*') {
                    if (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
                        dp[i][j] = dp[i - 1][j -2] || dp[i - 1][j] || dp[i][j - 2];
                    } else {
                        dp[i][j] = dp[i][j - 2];
                    }
                }
            }
        } 

        return dp[sLen][pLen];
    }
}

推荐题解:「手画图解」动态规划,需要仔细的分情况讨论

11. 盛最多水的容器

class Solution {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1;

        int maxArea = 0;
        while (left < right) {
            int width = right - left; 
            int high = Math.min(height[left], height[right]);
            int area = width * high;
            maxArea = Math.max(area, maxArea);
            
            //左边小则直接去掉,因为它和右边剩余的任意一个元素组成的面积都不会比当前更大
            if (height[left] < height[right]) {
                left++;
            //去掉右边
            } else {
                right--;
            }
        }

        return maxArea;
    }
}

推荐题解:O(n) 双指针解法:理解正确性、图解原理(C++/Java)

15. 三数之和

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new LinkedList<>();
        if (nums == null || nums.length < 3) {
            return res;
        }

        //排序很重要
        Arrays.sort(nums);

        int len = nums.length;
        for (int i = 0; i < len - 2; i++) { //最后两个数不用判断
            //三数之和一定大于0,后序必然不存在为0的组合
            if (nums[i] > 0) break;
            //去重
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            
            // left right 表示了 i 的右侧闭合区间
            int left = i + 1;
            int right = len - 1;

            while (left < right) {
                int target = nums[i] + nums[left] + nums[right];

                if (target == 0) {
                    res.add(Arrays.asList(new Integer[] {nums[i], nums[left], nums[right]}));
                    //去重
                    while(left < right && nums[left] == nums[left + 1]) left++;
                    while(left < right && nums[right] == nums[right - 1]) right--;

                    left++;
                    right--;
                } else if (target < 0) {
                    left++;
                } else if (target > 0) {
                    right--;
                }
            }
        }

        return res;
    }
}

推荐题解:画解算法:15. 三数之和

17. 电话号码的字母组合

回溯法即可

class Solution {
    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() < 1) {
            return res;
        }

        Map<Character, Character[]> map = new HashMap<>() {
            {
                put('2', new Character[] {'a', 'b', 'c'});
                put('3', new Character[] {'d', 'e', 'f'});
                put('4', new Character[] {'g', 'h', 'i'});
                put('5', new Character[] {'j', 'k', 'l'});
                put('6', new Character[] {'m', 'n', 'o'});
                put('7', new Character[] {'p', 'q', 'r', 's'});
                put('8', new Character[] {'t', 'u', 'v'});
                put('9', new Character[] {'w', 'x', 'y', 'z'});
            }
        };
        StringBuilder track = new StringBuilder();

        dfs(map, digits, 0, track);
        return res;
    }

    //存放最终的结果
    private List<String> res = new LinkedList<>();
    //回溯法获取所有结果
    private void dfs(Map<Character, Character[]> map, String digits, int index, StringBuilder track) {
        if (digits.length() == index) {
            res.add(track.toString());
            return;
        }

        Character[] charArr = map.get(digits.charAt(index));
        for (int i = 0; i < charArr.length; i++) {
            //选择
            track.append(charArr[i]);
            dfs(map, digits, index+1, track);
            //撤销选择
            track.deleteCharAt(index);
        }
    }
}

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

思路:快慢指针都指向头,快指针先移动n,接着快慢指针一起向后移动,直至快指针到达末尾,此时根据满指针即可删除倒数第n个结点。

/**
 * 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 removeNthFromEnd(ListNode head, int n) {
        //删除的可能是头结点,所以需要哑结点
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;

        ListNode fast =dummyNode, slow = dummyNode;
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }

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

        //删除结点
        slow.next = slow.next.next;

        return dummyNode.next;
    }
}
posted @ 2021-06-25 14:19  WINLSR  阅读(204)  评论(0编辑  收藏  举报