LeetcodeTop100 刷题总结(一)

LeetCode 热题 100:https://leetcode.cn/studyplan/top-100-liked/


文章目录

  • 一、哈希
    • [1. 两数之和](#1. 两数之和)
    • [49. 字母异位词分组](#49. 字母异位词分组)
    • [128. 最长连续序列](#128. 最长连续序列)
  • 二、双指针
    • [283. 移动零](#283. 移动零)
    • [11. 盛水最多的容器](#11. 盛水最多的容器)
    • [15. 三数之和](#15. 三数之和)
    • [42. 接雨水(待完成)](#42. 接雨水(待完成))
  • 三、滑动窗口
    • [3. 无重复字符的最长子串](#3. 无重复字符的最长子串)
    • [438. 找到字符串中所有字母异位词](#438. 找到字符串中所有字母异位词)
  • 四、子串
    • [560. 和为 K 的子数组](#560. 和为 K 的子数组)
    • [239. 滑动窗口最大值](#239. 滑动窗口最大值)
    • [76. 最小覆盖子串](#76. 最小覆盖子串)
    • [补充:209. 长度最小的子数组](#补充:209. 长度最小的子数组)
  • 五、普通数组
    • [53. 最大子数组和](#53. 最大子数组和)
    • [56. 合并区间](#56. 合并区间)
    • [189. 轮转数组](#189. 轮转数组)
    • [238. 除自身以外数组的乘积](#238. 除自身以外数组的乘积)
    • [41. 缺失的第一个正数(待完成)](#41. 缺失的第一个正数(待完成))
  • 六、矩阵
    • [73. 矩阵置零](#73. 矩阵置零)
    • [54. 螺旋矩阵](#54. 螺旋矩阵)
    • [48. 旋转图像](#48. 旋转图像)
    • [240. 搜索二维矩阵 II](#240. 搜索二维矩阵 II)
  • 七、链表
    • [160. 相交链表](#160. 相交链表)
    • [206. 反转链表](#206. 反转链表)
    • [234. 回文链表](#234. 回文链表)
    • [141. 环形链表](#141. 环形链表)
    • [142. 环形链表 II](#142. 环形链表 II)
    • [21. 合并两个有序链表](#21. 合并两个有序链表)
    • [2. 两数相加](#2. 两数相加)
    • [19. 删除链表的倒数第 N 个结点](#19. 删除链表的倒数第 N 个结点)
    • [24. 两两交换链表中的节点](#24. 两两交换链表中的节点)
    • [25. K 个一组翻转链表(待完成)](#25. K 个一组翻转链表(待完成))
    • [138. 随机链表的复制](#138. 随机链表的复制)
    • [148. 排序链表](#148. 排序链表)
    • [23. 合并 K 个升序链表](#23. 合并 K 个升序链表)
    • [146. LRU 缓存](#146. LRU 缓存)

一、哈希

1. 两数之和

思路:设置一个 map 容器,用于存储当前元素和索引。遍历时一边将数据存入 map,一边比从map中查找满足加和等于 target 的另一个元素。

java 复制代码
class Solution {
	/**
     * 输入:nums = [2,7,11,15], target = 9
     * 输出:[0,1]
     * 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
     */
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(target - nums[i])) {
                return new int[] {map.get(target - nums[i]), i};
            }
            map.put(nums[i], i);
        }
        return new int[] {};
    }
}

49. 字母异位词分组

思路:设置一个 map 容器,key是排序后的字符组合,value是字母异位词的集合。

java 复制代码
class Solution {
	/**
     * 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
     * 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
     */
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String str : strs) {
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String sortStr = Arrays.toString(chars);
            // 如果存在key,即:new String(chars),那么返回对应的 value;
            // 否则将执行先初始化 key:new String(chars),value: new ArrayList<>(),然后在返回value。
            map.computeIfAbsent(new String(chars), s -> new ArrayList<>()).add(str);
        }
        return new ArrayList<>(map.values());
    }
}

128. 最长连续序列

思路:因为题目要求O(n)的时间复杂度,因此使用set对数组进行转存,并利用滑动窗口一次遍历即可得出连续序列的最长长度。

java 复制代码
class Solution {
	/**
     * 输入:nums = [100,4,200,1,3,2]
     * 输出:4
     * 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
     */
    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        Set<Integer> set = new TreeSet<>();
        for (int num : nums) {
            set.add(num);
        }
        int co = 0;
        for (Integer num : set) {
            nums[co++] = num;
        }
        return sliderWindow(nums);
    }

    private int sliderWindow(int[] nums) {
        int left = 0;
        int len = nums.length;
        int max = 1;
        for (int right = 1; right < len; right++) {
            if (nums[right] - nums[right - 1] != 1) {
                left = right;
            }
            max = Math.max(right - left + 1, max);
        }
        return max;
    }
}

二、双指针

283. 移动零

java 复制代码
class Solution {
	/**
     * 输入: nums = [0,1,0,3,12]
     * 输出: [1,3,12,0,0]
     */
    public void moveZeroes(int[] nums) {
        int j = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 0) {
                nums[j++] = nums[i];
            }
        }
        for (; j < nums.length; j++) {
            nums[j] = 0;
        }
    }
}

11. 盛水最多的容器

思路:定义双指针,分别指向数组的最左边和最右边,每次往里移动较短的元素的指针。这里解释为什么要移动短的?

根据木桶原理,整个木桶盛水的最大体积取决于小的那一段木板。如果移动短的指针,体积可能变大,也可能不变,还有可能变小。但如果移动长的指针,体积一定会变小。因此在指针不断往里移动的同时,移动指向较短元素的指针能得出盛水最大的容量。

java 复制代码
class Solution {
    public int maxArea(int[] height) {
        int len = height.length;
        int left = 0;
        int right = len - 1;
        int maxArea = 0;
		// 面积 = 短板 * 底边
        // 向内移动短板,水槽短板 min(h[i], h[j]) 可能变大,下个水槽面积可能增大
        // 向内移动长板,水槽短板 min(h[i], h[j]) 可能变小或不变,下个水槽面积一定减小(因为底边长变小)
        while (left < right) {
            maxArea = Math.max(Math.min(height[left], height[right]) * (right - left), maxArea);
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return maxArea;
    }
}

15. 三数之和

思路:将数组排完序后进行遍历,遍历时选取当前元素的后一个元素和数组的最后一个元素为双指针。(注意对重复元素进行去重)

java 复制代码
class Solution {
	/**
     * 输入:nums = [-1,0,1,2,-1,-4]
     * 输出:[[-1,-1,2],[-1,0,1]]
     * 解释:
     * nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
     * nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
     * nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
     * 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
     * 注意,输出的顺序和三元组的顺序并不重要。
     */
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int len = nums.length;
        Arrays.sort(nums);

        for (int i = 0; i < len; i++) {
            if (nums[i] > 0) {
                break;
            }
            if (i != 0 && nums[i] == nums[i - 1]) { // 去除重复元素
                continue;
            }
            int left = i + 1;
            int right = len - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    while (left < right && nums[left] == nums[left + 1]) { // 去除重复元素
                        left++;
                    }
                    while (left < right && nums[right - 1] == nums[right]) {
                        right--;
                    }
                    left++;
                    right--;
                } else if (sum > 0) {
                    right--;
                } else {
                    left++;
                }
            }
        }
        return res;
    }
}

42. 接雨水(待完成)


三、滑动窗口

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

思路:定义一个 map 容器, key 存储字符,value 存储当前字符索引。使用滑动窗口计算最长字串,当窗口内存在重复字符时,调整窗口的左边界,调整为重复元素索引的下一位,并且注意左边界不能向左移动。

java 复制代码
class Solution {
    /**
     * 输入: s = "abcabcbb"
     * 输出: 3 
     * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
     */
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> map = new HashMap<>(); // key:字符,value:当前字符索引
        int len = s.length();
        int start = 0;
        int max = 0;
        for (int end = 0; end < len; end++) {
            char ch = s.charAt(end);
            if (map.containsKey(ch)) {
                start = Math.max(map.get(ch) + 1, start); 
                // 处理 'abba',如果不用max比较当遍历到最后一个a时,start将会指向第一个b,即start-end范围是 bba
            }
            map.put(ch, end);
            max = Math.max(end - start + 1, max);
        }
        return max;
    }
}

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

思路:使用数组统计字符串中26个字符的出现次数,固定滑动窗口大小,并使用 Arrays.equals(...) 方法一边遍历一边比较。

java 复制代码
class Solution {
	/**
     * 输入: s = "cbaebabacd", p = "abc"
     * 输出: [0,6]
     * 解释:
     * 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
     * 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
     */
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res = new ArrayList<>();
        int sLen = s.length();
        int pLen = p.length();
        if (sLen < pLen) {
        	return res;
        }
        int[] sWin = new int[26];
        int[] pWin = new int[26];

        for (int i = 0; i < pLen; i++) {
            sWin[s.charAt(i) - 'a']++;
            pWin[p.charAt(i) - 'a']++;
        }
        if (Arrays.equals(sWin, pWin)) {
            res.add(0);
        }
        for (int i = pLen; i < sLen; i++) {
            sWin[s.charAt(i - pLen) - 'a']--;
            sWin[s.charAt(i) - 'a']++;
            if (Arrays.equals(sWin, pWin)) {
                res.add(i - pLen + 1);
            }
        }
        return res;
    }
}

四、子串

560. 和为 K 的子数组

思路:首先计算前缀和,利用前缀和的差值确定子数组的和是否等于K。

如:下面数组求子数组和为 6,pre[4] - pre[1] == 6 就代表:num[1:3] 加和等于 6。

ind 0 1 2 3 4 5 6 7
value 4 1 2 3 0 6 2 4
前缀和 pre 0 4 5 7 10 10 16 18 22

注:这里我们预留第一个位置为0,代表索引为 0 的元素前缀和为 0。

java 复制代码
class Solution {
	/**
     * 输入:nums = [1,2,3], k = 3
     * 输出:2
     */
    public int subarraySum(int[] nums, int k) {
        int res = 0;
        int len = nums.length;
        int[] pre = new int[len + 1];
        // 计算前缀和
        for (int i = 0; i < len; i++) {
            pre[i + 1] = pre[i] + nums[i];
        }
        for (int left = 0; left < len; left++) {
            for (int right = left; right < len; right++) {
                if (pre[right + 1] - pre[left] == k) {
                    res++;
                }
            }
        }
        return res;
    }
}

上面做法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因此用哈希表进行优化。

思路:设置一个 map 容器用于存储前缀和以及前缀和的个数,当计算前缀和的同时来查找是否存在 前缀和 - 目标和,如果存在则说明存在子数组和等于 k。如:上述例子中,求子数组和为 6,当遍历到索引 4 时前缀和为 10, map 中存在键 key="10-6"=4 {key=4,value=1},说明当前元素存在前缀和为 4 的情况。

ind 0 1 2 3 4 5 6 7
value 4 1 2 3 0 6 2 4
累加的前缀和 4 5 7 10 10 16 18 22
java 复制代码
class Solution {
	/**
     * 输入:nums = [1,2,3], k = 3
     * 输出:2
     */
    public int subarraySum(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>(); // key:前缀和,value: 前缀和的个数
        int res = 0;

        map.put(0, 1); // 前缀和为 0 的个数有一个
        int sum = 0; // 记录前缀和
        for (int num : nums) {
            sum += num;
            if (map.containsKey(sum - k)) {
                res += map.get(sum - k);
            }
            map.put(sum, map.getOrDefault(sum, 0) + 1);
        }
        return res;
    }
}

239. 滑动窗口最大值

思路:设置一个大顶堆,固定窗口大小,遍历时首先清除过期元素,然后将元素入堆。
值得注意的是,有些比较小的元素由于不在堆顶,不会立即删除。但是在后面如果到了堆顶,也会删除。

java 复制代码
class Solution {
	/**
     * 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
     * 输出:[3,3,5,5,6,7]
     * 解释:
     * 滑动窗口的位置                最大值
     * ---------------               -----
     * [1  3  -1] -3  5  3  6  7       3
     *  1 [3  -1  -3] 5  3  6  7       3
     *  1  3 [-1  -3  5] 3  6  7       5
     *  1  3  -1 [-3  5  3] 6  7       5
     *  1  3  -1  -3 [5  3  6] 7       6
     *  1  3  -1  -3  5 [3  6  7]      7
     */
    public int[] maxSlidingWindow(int[] nums, int k) {
        PriorityQueue<Elem> heap = new PriorityQueue<>((elem1, elem2) -> elem2.value - elem1.value);// 初始化大顶堆
        int len = nums.length;
        int[] res = new int[len - k + 1];

        for (int i = 0; i < k; i++) {
            heap.add(new Elem(nums[i], i));
        }
        res[0] = heap.element().value;

        int co = 1;
        for (int i = k; i < len; i++) {
            while (!heap.isEmpty() && heap.element().index <= i - k) { // 处理不在窗口的元素
                // 有些比较小的元素由于不在堆顶,不会立即删除。但是在后面如果到了堆顶,也会删除
                // 如:nums = [5,6,-1,-2,3], k = 3
                // 当窗口在[6,-1,-2]时,5还在堆内,但是当窗口在[-1,-2,3]时,会在堆顶被删除
                heap.remove();
            }
            heap.add(new Elem(nums[i], i));
            res[co++] = heap.element().value;
        }
        return res;
    }

    class Elem {
        int value;

        int index;

        public Elem() {
        }

        public Elem(int value, int index) {
            this.value = value;
            this.index = index;
        }
    }
}

76. 最小覆盖子串

思路:分别设置两个数组用来存储字符的出现次数,利用滑动窗口边一边右移一边检查模式串是否被覆盖。

java 复制代码
 class Solution {
	/**
     * 输入:s = "ADOBECODEBANC", t = "ABC"
     * 输出:"BANC"
     * 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
     */
     public String minWindow(String s, String t) {
        if (s.length() < t.length()) {
            return "";
        }
        int[] sChars = new int[128];
        int[] tChars = new int[128];
        for (char ch : t.toCharArray()) {
            tChars[ch]++;
        }
        int left = 0;
        int sLen = s.length();
        int resLeft = -1;
        int resRight = sLen;
        for (int right = 0; right < sLen; right++) {
            sChars[s.charAt(right)]++;
            while (left <= right && isCovered(sChars, tChars)) {
                if (right - left < resRight - resLeft) {
                    resLeft = left;
                    resRight = right;
                }
                sChars[s.charAt(left)]--;
                left++;
            }
        }
        return resLeft == -1 ? "" : s.substring(resLeft, resRight + 1);
    }

    private boolean isCovered(int[] sChars, int[] tChars) {
        for (int i = 'A'; i <= 'Z'; i++) {
            if (sChars[i] < tChars[i]) {
                return false;
            }
        }
        for (int i = 'a'; i <= 'z'; i++) {
            if (sChars[i] < tChars[i]) {
                return false;
            }
        }
        return true;
    }
}

上面代码在每次遍历的时候都需要检查子串是否被覆盖,因此可以考虑设置两个变量 sNum 和 tNum。tNum 用于记录 t 中不同字符的数量, sNum 用于记录 s 指定字符达到覆盖 t 的程度数量。如:当 s 的子串中如果 'a' 的数量等于 t 中 'a' 字符的数量时 sNum + 1,否则不变。

java 复制代码
class Solution {
	/**
     * 输入:s = "ADOBECODEBANC", t = "ABC"
     * 输出:"BANC"
     * 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
     */
    public String minWindow(String s, String t) {
        int[] sChars = new int[128];
        int[] tChars = new int[128];
        int sNum = 0; // 记录 s 中的指定字符数量达到覆盖 t 程度的数量
        int tNum = 0; // 记录 t 中有多少不同字符
        for (char ch : t.toCharArray()) {
            if (tChars[ch]++ == 0) {
                tNum++;
            }
        }
        int len = s.length();
        int resLeft = -1;
        int resRight = len;
        int left = 0;
        for (int right = 0; right < len; right++) {
            if (++sChars[s.charAt(right)] == tChars[s.charAt(right)]) {
                sNum++; // s中的该字符数量达到覆盖 t 中该字符的程度
            }
            while (left <= right && sNum == tNum) {
                if (right - left < resRight - resLeft) { // 更新结果左右边界
                    resLeft = left;
                    resRight = right;
                }
                if (sChars[s.charAt(left)]-- == tChars[s.charAt(left)]) {
                    sNum--;
                }
                left++;
            }
        }
        return resLeft == -1 ? "" : s.substring(resLeft, resRight + 1);
    }
}

补充:209. 长度最小的子数组

最小覆盖子串题目类似:209. 长度最小的子数组

java 复制代码
class Solution {
	/**
     * 输入:target = 7, nums = [2,3,1,2,4,3]
     * 输出:2
     * 解释:子数组 [4,3] 是长度最小且总和大于等于 target 的子数组。
     */
    public int minSubArrayLen(int target, int[] nums) {
        int len = nums.length;
        int sum = 0;
        int left = 0;
        int res = len + 1;
        for (int right = 0; right < len; right++) {
            sum += nums[right];
            while (left <= right && sum >= target) {
                res = Math.min(right - left + 1, res);
                sum -= nums[left++];
            }
        }
        return res == len + 1 ? 0 : res;
    }
}

五、普通数组

53. 最大子数组和

思路:设置变量 curr 用于记录子数组和,遍历数组时,当子数组和大于零时累加当前元素,否则令子数组和等于当前数组元素。

java 复制代码
class Solution {
	/**
     * 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
     * 输出:6
     * 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
     */
    public int maxSubArray(int[] nums) {
        int curr = 0;
        int max = nums[0];
        for (int i = 0; i < nums.length; i++) {
            if (curr >= 0) {
                curr += nums[i];
            } else {
                curr = nums[i];
            }
            max = Math.max(curr, max);
        }
        return max;
    }
}

56. 合并区间

思路:定义内部类用于记录区间的左右端点,对二维数组按照左端点递增,左端点相同时右端点递增的规则排序。将数组第一个元素加入集合后进行遍历,若发现当前 数组元素左端点和集合最后一个元素的左端点相同 或者 集合最后一个元素的右端点大于数组的左端点,则将集合的最后一个元素的右端点进行取大处理,否则将数组元素加入集合。

java 复制代码
class Solution {
	/**
     * 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
     * 输出:[[1,6],[8,10],[15,18]]
     * 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
     */
     public int[][] merge(int[][] intervals) {
        int len = intervals.length;
        List<Range> list = new ArrayList<>();

        Arrays.sort(intervals,
            (range1, range2) -> range1[0] != range2[0] ? range1[0] - range2[0] : range1[1] - range2[1]);
        list.add(new Range(intervals[0][0], intervals[0][1]));

        for (int i = 1; i < len; i++) {
            Range range = list.get(list.size() - 1);
            if (range.begin == intervals[i][0] || range.end >= intervals[i][0]) {
                range.end = Math.max(intervals[i][1], range.end); // Max比较大小是为了处理这种情况 [[1,4],[2,3]]
            } else {
                list.add(new Range(intervals[i][0], intervals[i][1]));
            }
        }
        int size = list.size();
        int[][] res = new int[size][2];
        for (int i = 0; i < size; i++) {
            res[i][0] = list.get(i).begin;
            res[i][1] = list.get(i).end;
        }

        return res;
    }

    class Range {
        int begin;

        int end;

        public Range() {
        }

        public Range(int begin, int end) {
            this.begin = begin;
            this.end = end;
        }
    }
}

189. 轮转数组

思路:先将数组全部翻转,然后对前 k 个元素和其余的元素分别做翻转。

java 复制代码
class Solution {
	/**
     * 输入: nums = [1,2,3,4,5,6,7], k = 3
     * 输出: [5,6,7,1,2,3,4]
     * 解释:
     * 向右轮转 1 步: [7,1,2,3,4,5,6]
     * 向右轮转 2 步: [6,7,1,2,3,4,5]
     * 向右轮转 3 步: [5,6,7,1,2,3,4]
     */
    public void rotate(int[] nums, int k) {
        int len = nums.length;
        k %= len;
        reverseArr(nums, 0, len - 1);
        reverseArr(nums, 0, k - 1);
        reverseArr(nums, k, len - 1);
    }

    private void reverseArr(int[] nums, int begin, int end) {
        while (begin < end) {
            int temp = nums[begin];
            nums[begin] = nums[end];
            nums[end] = temp;
            begin++;
            end--;
        }
    }
}

238. 除自身以外数组的乘积

思路:将数组元素累乘以后逐个相除可能会存在除零异常。因此,考虑分别求当前元素的左侧累乘积和右侧累乘积,最后再将两侧数组做累乘。

java 复制代码
class Solution {
	/**
     * 输入: nums = [1,2,3,4]
     * 输出: [24,12,8,6]
     */
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        int[] left = new int[len];
        int[] right = new int[len];
        int[] res = new int[len];
        left[0] = 1;
        right[len - 1] = 1;

        // nums: [1, 2, 3, 4]
        // left: [1, 1, 2, 6]
        // right: [24,12,4, 1]

        for (int i = 1; i < len; i++) {
            left[i] = nums[i - 1] * left[i - 1];
        }
        for (int i = len - 2; i >= 0; i--) {
            right[i] = nums[i + 1] * right[i + 1];
        }
        for (int i = 0; i < len; i++) {
            res[i] = left[i] * right[i];
        }

        return res;
    }
}

41. 缺失的第一个正数(待完成)


六、矩阵

73. 矩阵置零

思路:设置矩阵行列大小的两个数组,用于对矩阵元素为零的行列进行标记。再次遍历矩阵,然后将标记过的行和列进行置零。

java 复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[] rows = new int[m];
        int[] columns = new int[n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 0) {
                    rows[i] = 1;
                    columns[j] = 1;
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (rows[i] == 1 || columns[j] == 1) {
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

54. 螺旋矩阵

思路:初始化矩阵的上下左右四个边界,按照 "从左向右、从上向下、从右向左、从下向上" 四个方向循环打印,每次都需要更新边界,并判断结束条件。

java 复制代码
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        int left = 0;
        int right = matrix[0].length - 1;
        int up = 0;
        int down = matrix.length - 1;
        while (true) {
            for (int i = left; i <= right; i++) {
                res.add(matrix[up][i]);
            }
            if (++up > down) {
                break;
            }
            for (int i = up; i <= down; i++) {
                res.add(matrix[i][right]);
            }
            if (left > --right) {
                break;
            }
            for (int i = right; i >= left; i--) {
                res.add(matrix[down][i]);
            }
            if (up > --down) {
                break;
            }
            for (int i = down; i >= up; i--) {
                res.add(matrix[i][left]);
            }
            if (++left > right) {
                break;
            }
        }
        return res;
    }
}

48. 旋转图像

思路:先将矩阵转置,然后将左右对称的两列互换元素,即可达到顺时针旋转 90 度的效果。

java 复制代码
class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;

        for (int i = 0; i < n; i++) { // 矩阵转置
            for (int j = i + 1; j < n; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }

        for (int i = 0; i < n; i++) { // 左右对称的两列互换
            for (int j = 0; j < n / 2; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[i][n - 1 - j];
                matrix[i][n - 1 - j] = temp;
            }
        }
    }
}

240. 搜索二维矩阵 II

思路:利用 "每行的所有元素从左到右升序排列,每列的所有元素从上到下升序排列" 这个特点,从右上角开始向左下角的方向查找,当元素大于目标元素,这一列下面的元素都大于目标元素;当元素小于目标元素,这一行前面的元素都小于目标元素。

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;
        int x = 0; // 右上角
        int y = n - 1;

        while (x < m && y >= 0) {
            if (matrix[x][y] > target) { // 当前元素大于target,这一列下面的元素都大于target
                y--;
            } else if (matrix[x][y] < target) { // 当前元素小于target,这一行前面的元素都小于target
                x++;
            } else {
                return true;
            }
        }
        return false;
    }
}

也可以从左下角开始查找,代码如下:

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;
        int x = m - 1; // 左下角
        int y = 0;

        while (x >= 0 && y < n) {
            if (matrix[x][y] > target) { // 当前元素大于target,这一行后面的元素都大于target
                x--;
            } else if (matrix[x][y] < target) { // 当前元素小于target,这一列上面的元素都小于target
                y++;
            } else {
                return true;
            }
        }
        return false;
    }
}

七、链表

160. 相交链表

思路:利用乘法交换律,设两个链表相交前分别有 A B 个节点,相交部分有 C 个节点,那么 A+C+B=B+C+A。设置两个指针分别指向两个链表的头部,同时向后移动。当其中一个指针移动到结尾时,则转向指向另一个链表的头部,另一个指针步骤同上,最终两个指针会在相交处会面。

java 复制代码
/**
 * 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) {
        ListNode pa = headA;
        ListNode pb = headB;
        while (pa != pb) {
            pa = pa == null ? headB : pa.next;
            pb = pb == null ? headA : pb.next;
        }
        return pa;
    }
}

注:如果两个链表不相交,也适合以上规律,最终两个指针都会指向空,也会跳出循环。


206. 反转链表

思路:链表头插法。

java 复制代码
/**
 * 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 reverseList(ListNode head) {
        ListNode pre = new ListNode();
        ListNode p;
        p = head;
        pre.next = null;
        
        while(p != null){
            ListNode temp = p.next;
            p.next = pre.next;
            pre.next = p;
            p = temp;
        }
        return pre.next;
    }
}

234. 回文链表

思路:本地的实现很多,这里采用栈进行辅助判断回文。

java 复制代码
/**
 * 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 boolean isPalindrome(ListNode head) {
        Deque<ListNode> stack = new LinkedList<>();
        ListNode p = head;
        while (p != null) {
            stack.push(p);
            p = p.next;
        }
        while (head != null) {
            p = stack.pop();
            if (p.val != head.val) {
                return false;
            }
            head = head.next;
        }
        return true;
    }
}

141. 环形链表

思路1:使用 hash 表进行辅助判断是否存在环。

java 复制代码
/**
 * 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) {
        Set<ListNode> set = new HashSet<>();

        ListNode p = head;
        while (p != null) {
            if (set.contains(p)) {
                return true;
            }
            set.add(p);
            p = p.next;
        }
        return false;
    }
}

思路2:使用快慢指针,slow 每次向前走一步,fast 每次向前走两步。

① 当存在环时,fast 由于走得快,会发生扣圈的情况,且最终与 slow 相遇。

② 当不存在环时,fast 可能在某次循环后,发生当前位置为空,或下一位置为空的两种情况,当然由于走的快,最终会返回 false。

总之,循环的结束条件,要么出现环 slow == fast,要么 fast 先一步为空。下面列举两种实现方式:

java 复制代码
/**
 * 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) {
        ListNode slow = head;
        ListNode fast = head;
        while (true) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true;
            }
        }
    }
}

// 推荐
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true;
            }
        }
        return false;
    }
}

142. 环形链表 II

思路1:使用 hash 表。

java 复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        ListNode p = head;
        while (p != null) {
            if (set.contains(p)) {
                return p;
            }
            set.add(p);
            p = p.next;
        }
        return null;
    }
}

思路2:使用快慢指针,思路如下:

  • fast 每次走两个节点, slow 每次走一个节点。环外有 a 个结点,环内有 b 个结点。
  • 第一次相遇时,fast 走了 f 步,slow 走了 s 步。
    f = 2s
    f = s + nb 表示 fs 多走了 n*b 步,即 n 圈。这样表示的原因在于扣圈。
    化简得:f = 2nb, s = nbn 代表扣圈的次数,可能等于1,2,3,...
  • 设刚开始 slow 指针从开始到环的入口要走 k 步:k = a + tbt 代表在环中循环的次数,可能等于0,1,2,3,...。因此当发生第一次相遇时,再走 a 步即可重新回到入环的起点。
java 复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                fast = head; // 令 fast 指针指向链表头部
                break;
            }
        }
        if (fast.next == null || fast.next.next == null) {
            return null;
        }
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

21. 合并两个有序链表

思路:设置两个指针,分别指向链表头部,逐个比较向后迭代即可。

java 复制代码
/**
 * 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 p1 = list1;
        ListNode p2 = list2;
        ListNode pre = new ListNode();
        ListNode p = pre;
        while (p1 != null && p2 != null) {
            if (p1.val < p2.val) {
                p.next = p1;
                p = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p = p2;
                p2 = p2.next;
            }
        }
        if (p1 != null) {
            p.next = p1;
        }
        if (p2 != null) {
            p.next = p2;
        }
        return pre.next;
    }
}

2. 两数相加

思路:设置两个指针和进位标志,逐个向后相加迭代即可。

输入:l1 = [2,4,3], l2 = [5,6,4]

输出:[7,0,8]

解释:342 + 465 = 807

java 复制代码
/**
 * 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 addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode pre = new ListNode();
        ListNode p = pre;
        ListNode p1 = l1;
        ListNode p2 = l2;
        int sign = 0;
        int sum;
        while (p1 != null || p2 != null) {
            if (p1 != null && p2 != null) {
                sum = p1.val + p2.val + sign;
                p1 = p1.next;
                p2 = p2.next;
            } else if (p1 != null) {
                sum = p1.val + sign;
                p1 = p1.next;
            } else {
                sum = p2.val + sign;
                p2 = p2.next;
            }
            p.next = new ListNode(sum % 10);
            p = p.next;
            sign = sum / 10;
        }
        if (sign != 0) {
            p.next = new ListNode(sign);
        }
        return pre.next;
    }
}

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

思路:让前面的指针先移动 n 步,之后前后指针共同移动直到前面的指针到尾部为止。

java 复制代码
/**
 * 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 pre = new ListNode();
        pre.next = head;
        ListNode p = pre;
        ListNode q = pre;
        int co = 0;
        while (p.next != null) {
            if (++co > n) {
                q = q.next;
            }
            p = p.next;
        }
        q.next = q.next.next;
        return pre.next;
    }
}

24. 两两交换链表中的节点

思路:链表节点两两交换位置,逐个向后迭代。

java 复制代码
/**
 * 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 swapPairs(ListNode head) {
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode p = head;
        ListNode q = pre;

        while (p != null && p.next != null) {
            ListNode temp = p.next.next;
            q.next = p.next;
            q.next.next = p;
            p.next = null;
            q = p;
            p = temp;
        }
        if (p != null) {
            q.next = p;
        }

        return pre.next;
    }
}

25. K 个一组翻转链表(待完成)


138. 随机链表的复制

思路:题意是让我们把下面的随机链表做整体复制,这里我们设置一个 map 容器,用于对应原始节点和复制的节点,存储以后再处理 next 指针和 random 指针。

java 复制代码
/*
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        Map<Node, Node> map = new HashMap<>();
        Node p = head;
        while (p != null) {
            Node copyNode = new Node(p.val);
            map.put(p, copyNode);
            p = p.next;
        }
        p = head;
        while (p != null) {
            Node copyNode = map.get(p);
            if (p.random != null) {
                copyNode.random = map.get(p.random);
            }
            if (p.next != null) {
                copyNode.next = map.get(p.next);
            }
            p = p.next;
        }
        return map.get(head);
    }
}

148. 排序链表

思路:这里我们采用堆结构辅助链表排序,将大顶堆构造好以后,一边出堆一边利用头插法对链表结构进行重塑。

java 复制代码
/**
 * 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 sortList(ListNode head) {
        PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> b.val-a.val); // 大顶堆
        while(head != null){
            queue.offer(head); // 从堆底插入
            head = head.next;
        }
        ListNode pre = new ListNode(0);
        while(!queue.isEmpty()){
            ListNode p = queue.poll(); // 出队列并调整堆
            p.next = pre.next; // 头插法倒序
            pre.next = p;
        }
        return pre.next;
    }
}

23. 合并 K 个升序链表

思路:K 个有序链表重复调用两个有序链表的算法。

java 复制代码
/**
 * 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 {
	/**
     * 输入:lists = [[1,4,5],[1,3,4],[2,6]]
     * 输出:[1,1,2,3,4,4,5,6]
     */
    public ListNode mergeKLists(ListNode[] lists) {
        int len = lists.length;
        ListNode pre = null;      
        for (int i = 0; i < len; i++) {
            pre = mergeTwoLists(pre, lists[i]);
        }
        return pre;
    }

    private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode p1 = list1;
        ListNode p2 = list2;
        ListNode pre = new ListNode();
        ListNode p = pre;
        while (p1 != null && p2 != null) {
            if (p1.val < p2.val) {
                p.next = p1;
                p = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p = p2;
                p2 = p2.next;
            }
        }
        if (p1 != null) {
            p.next = p1;
        }
        if (p2 != null) {
            p.next = p2;
        }
        return pre.next;
    }
}

146. LRU 缓存

输入:
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]

输出:
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释:

LRUCache lRUCache = new LRUCache(2);

lRUCache.put(1, 1);  // 缓存是 {1=1}

lRUCache.put(2, 2);  // 缓存是 {1=1, 2=2}

lRUCache.get(1);   // 返回 1

lRUCache.put(3, 3);  // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}

lRUCache.get(2);   // 返回 -1 (未找到)

lRUCache.put(4, 4);  // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}

lRUCache.get(1);   // 返回 -1 (未找到)

lRUCache.get(3);   // 返回 3

lRUCache.get(4);   // 返回 4

思路:参考灵神的思路,想象有一摞书。

get:时将一本书(key) 抽出来,放在最上面。

put:放入一本新书,如果已经有这本书(key),把他抽出来放在最上面,并替换它的 value。如果没有这本书(key),就放在最上面。如果超出了 capacity 本书,就把最下面的书移除。

题目要求 get 和 put 都是 O(1) 的时间复杂度,因此考虑双向链表实现。

java 复制代码
class LRUCache {
    class Node {
        int key, value;

        Node prev, next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    Map<Integer, Node> map;

    Node dummy;

    int capacity;

    public LRUCache(int capacity) {
        map = new HashMap<>();
        dummy = new Node(0, 0); // 头结点
        this.capacity = capacity;
        dummy.next = dummy;
        dummy.prev = dummy;
    }

    public int get(int key) {
        Node node = getNode(key);
        return node != null ? node.value : -1;
    }

    public void put(int key, int value) {
        Node node = getNode(key);
        if (node != null) {
            node.value = value; // 如果存在,则在getRoot方法里面已经放到了头部
            return;
        }
        node = new Node(key, value);
        map.put(key, node);
        pushFirst(node); // 放在链表头部
        if (map.size() > capacity) {
            map.remove(dummy.prev.key);
            remove(dummy.prev);
        }
    }

    private Node getNode(int key) {
        if (!map.containsKey(key)) {
            return null;
        }
        Node node = map.get(key);
        remove(node); // 删除旧节点
        pushFirst(node); // 将新节点加到链表头部
        return node;
    }

    private void pushFirst(Node node) {
        node.next = dummy.next;
        node.prev = dummy;
        dummy.next.prev = node;
        dummy.next = node;
    }

    private void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}
/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
相关推荐
是程序喵呀10 分钟前
SpringMVC详解
java·spring·spring-mvc
疯一样的码农16 分钟前
Apache Maven 标准文件目录布局
java·maven·apache
一只小透明啊啊啊啊29 分钟前
Leetcode100子串
算法
木向29 分钟前
leetcode:114. 二叉树展开为链表
算法·leetcode·链表
sky_smile_Allen34 分钟前
[C#] 关于数组的详细解释以及使用注意点
开发语言·算法·c#
Felix666yy35 分钟前
设计模式之建造者模式
java
Angindem36 分钟前
子矩阵的和(矩阵前缀和)
线性代数·矩阵
界面开发小八哥36 分钟前
「Java EE开发指南」如何使用Visual JSF编辑器设计JSP?(一)
java·ide·java-ee·编辑器·myeclipse
先睡38 分钟前
javaEE
java·java-ee
程序猿小D1 小时前
第三百三十一节 Java网络教程 - Java网络UDP多播
java·网络·udp