Leetcode热题100

1. 两数之和 ------》哈希表,快速寻找目标元素位置

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        /*
        要找出对应下标,显然暴力不行,不能排序,否则下标会变动
        已经知道target, 假设枚举数值a,这个时候我们知道另外一个数值b = target - a
        那怎么快速找到这个b在数组nums中的下标呢?显然可以使用哈希表
        这里使用的哈希表就是Map,存储键值(b, 对应的索引)
        可以发现,每次如果没有查到的话,就将当前数值放入map中,
        因为只有两个不重复的对应位置,所以从左往右放入map不冲突。不会漏
        */
        int n = nums.length;
        // map进行存储
        Map<Integer, Integer> map = new HashMap<>();
        // 存储结果
        int[] ans = new int[2];
        // 遍历数组
        for (int i = 0; i < n; i++) {
            // 计算b值
            int b = target - nums[i];
            if (map.containsKey(b)) {
                // 说明有,
                // 获取索引
                int j = map.get(b);
                // 存储两个下标
                ans[0] = i;
                ans[1] = j;
                break;
            }
            // }else {
            map.put(nums[i], i);
            // }
        }

        return ans;
        
    }
}

知识点:

Java中的哈希表,通常是指Map<K, V>接口实现的类。

最核心的类有三个:HashMap, LinkedHashMap(可实现队列), ConcurrentHashMap。(HashTable,已被淘汰)。

49 字母异位词分组 ------》哈希表,值存储字符串数组

java 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 使用哈希表,存储排序后的字符串,同一组的原始字符串列表
        Map<String, List<String>> map = new HashMap<>();
        // 遍历字符串数组
        for (String s : strs) {
            // 转换为字符数组
            char[] chs = s.toCharArray();
            Arrays.sort(chs);
            // 变换为字符串
            String key = new String(chs);
            // 维护哈希表
            // 这里因为都要存入map,所以可以先判断如果没有这个键,先创建一个键值对,后续只需要插入就可以了
            if (!map.containsKey(key)) {
                map.put(key, new ArrayList<>());
            }
            map.get(key).add(s);
        }
        // 最后取出来放入结果数组中
        List<List<String>> ans = new ArrayList<>();
        // 直接将map的值转换为列表集合
        // 因为值本身是List<String>类型,因此封装一个List,就是List<List<String>>类型
        return new ArrayList<>(map.values());
    }
}

知识点:

怎么对一个字符串排序:先变为字符数组,然后排序,然后new String()变为字符串

map直接将values变为数组

128 最长连续序列 ------》哈希表光有集合不行,还有逻辑,判断是否是一段新的开始

链接

java 复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        // 将数存入集合
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            set.add(num); // 去重
        }
        // 遍历元素
        int longest = 0;
        // 遍历集合,否则遍历没有去重的nums会超时
        for (int num : set) {
            // 如何判断是新的一段起点
            // 每一次要从断开连续的数字开始
            // 如何判断是不是不连续的可以通过集合判断
            if (!set.contains(num- 1)){
                int curNum = num;
                int curLen = 1;
                while (set.contains(curNum+1)){
                    curNum++;
                    curLen++;
                }
                longest = Math.max(longest, curLen);
            }
        }
        return longest;
    }
}

知识点:

怎么将一个数组转换为Set集合呢?转换为List,注意要使用HashSet构造函数。

Set set = new HashSet<>(Arrays.asList(array));

注意HashSet是无序的不支持排序。

283 移动零 ------》双指针,while循环,找到不为0的数,交换

链接

java 复制代码
class Solution {
    public void moveZeroes(int[] nums) {
        int n = nums.length, left = 0, right = 0;
        while (right < n) {
            if (nums[right] != 0){
                swap(nums, left, right);
                left++;
            }
            right++;
        }
    }

    public void swap(int[] nums, int left, int right){
        int tmp = nums[right];
        nums[right] = nums[left];
        nums[left] = tmp;
    }
}

11 盛最多水的容器 ------》双指针,左右指针,从较低高度一方更新指针

链接

java 复制代码
class Solution {
    public int maxArea(int[] height) {
        // 注意边界,这里题目给定数据是可以的
        int n = height.length;
        // 定义双指针
        int left = 0, right = n - 1;
        // 维护结果
        int ans = 0;
        // 更新双指针
        while (left < right){
            // 先计算当前位置可装水
            // 我要怎么知道是哪边高度低一点呢
            ans = Math.max(ans, (right - left) * Math.min(height[left], height[right]));
            if (height[left] < height[right]){
                left++;
            }else {
                right--;
            }

        }
        return ans;
    }
}

这里双指针从左右向内收缩,常用的是while(left < right),进行判断

15 三数之和 ------》枚举三数中第一个数,后两个数双指针维护,

java 复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        /*
        关键要厘清逻辑,可以发现只需要数组元素,与索引无关,
        因此可以先排序,然后固定中间的一个数,这样就简化为了左右两边的数,进行双指针查找是否满足条件
        */
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        if (n < 3 || nums == null) return ans;
        // 排序
        Arrays.sort(nums);
        // 枚举
        for (int i = 0; i < n; i++) {
            // 如果当前元素大于0,则后面元素都大于0,不可能和为0
            if (nums[i] > 0) break;
            //如果相邻重复的就需要跳过
            if (i > 0 && nums[i] == nums[i-1]) continue;
            // 定义左右指针
            int L = i+1;
            int R = n - 1;
            while (L < R){
                int sum = nums[i] + nums[L] + nums[R];
                if (sum == 0){
                    // 这个判断,是否存在重复
                    ans.add(Arrays.asList(nums[i], nums[L], nums[R]));
                    while (L < R && nums[L] == nums[L+1]) L++;
                    while (L < R && nums[R] == nums[R-1]) R--;
                    L++;
                    R--;
                }else if (sum < 0) L++;
                else if (sum > 0) R--;
            }
        }
        return ans;
    }
}

42 接雨水 ------》不要陷入区间,结合动规,预处理左右高度,

链接

java 复制代码
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0 || height == null) return 0;
        // 动态规划预处理
        int[] leftMax = new int[n], rightMax = new int[n];
        leftMax[0] = height[0];
        rightMax[n-1] = height[n-1];
        // 从左到右处理leftMax
        for (int i = 1; i < n; i++){
            leftMax[i] = Math.max(leftMax[i-1], height[i]);
        }
        // 从右往左处理rightMax
        for (int i = n-2; i >= 0; i--){
            rightMax[i] = Math.max(rightMax[i+1], height[i]);
        }
        // 然后处理维护接雨水
        int ans = 0;
        for (int i = 0; i < n; i++){
            // 左右两边最大值的最小值
            int tmp = Math.min(leftMax[i], rightMax[i]);
            ans += tmp - height[i];
        }

        return ans;

    }
}

3 无重复字符的最长子串 ------》滑动窗口,右边重复了,从左边删除直到没有重复,维护最长

链接

直接用while循环解决,直到没有重复的,

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 要不含有重复字符可以维护一个窗口保存当前已遍历的字符串
        // 用集合存储如果已经存在该字符则收敛左边界
        // if (s.length() == 0 || s == null) return 0;
        int n = s.length();
        if (n == 0 || n == 1) return n;
        // 创建集合
        Set<Character> set = new HashSet<>();
        // 维护左边界
        int left = 0;
        // 遍历字符串
        int longest = 0;
        for (int i = 0; i < s.length(); i++) {
            if (set.contains(s.charAt(i))){
                // 包含该字符
                while (left < s.length() && set.contains(s.charAt(i))){
                    // 说明存在重复的先从左边删除直到没有重复的
                    // left++;
                    set.remove(s.charAt(left));
                    left++;
                }

            }
            set.add(s.charAt(i));
            // 更新最长长度
            longest = Math.max(longest, i - left + 1);

        }
        return longest;
    }
}

while循环

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        /*
        看到这种就要肌肉记忆,联想出不重复,假设当前有一个子串了,如果下一个字符在这个子串中重复了,一定是子串的最左边的元素重复了,这样我们只需要维护最左边的元素,并更新长长度
        判断字符是否在字符串中,可以直接判断s.contains()
        但由于需要动态维护子串,因此可以使用哈希集合判断是否重复
        */
        int n = s.length();
        if (n == 0 || n == 1) {
            return n;
        }
        int ans = 0;
        // 维护一个滑动窗口
        int left = 0;
        Set<Character> set = new HashSet<>();
        // set.add(s.charAt(0));
        for (int right = 0; right < n; right++) {
            char ch = s.charAt(right);
            while (set.contains(ch)) {
                // 说明存在重复的
                // 先进行删除
                // set.remove(ch);
                // 不是说删除ch,而是从左指针删除,知道没有重复的
                // 例如示例pwwkew
                set.remove(s.charAt(left));
                left++;
            }
            // 不存在重复的,都要进行添加
            set.add(ch);
            // 更新最长长度
            ans = Math.max(ans, set.size());
            // ans = Math.max(ans, right - left + 1);
        }

        return ans;
    }
}

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

链接

中等

下面是自己枚举的做法,击败5%

java 复制代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        /**
        要求找出所有p的异位词的子串,显然子串长度一定等于p的长度,所以只需要枚举等长的子串
        然后拿到子串变换为字符数组排序变换回字符串判断是否相等
         */
        int len1 = s.length(), len2 = p.length();
        // 对p先进行排序
        char[] pchs = p.toCharArray();
        Arrays.sort(pchs);
        p = new String(pchs);
        // 枚举len2长度的子串
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i <= len1 - len2; i++) {
            String sub = s.substring(i, i + len2);
            char[] subchs = sub.toCharArray();
            // 排序
            Arrays.sort(subchs);
            sub = new String(subchs);
            if (sub.equals(p)){
                ans.add(i);
            }
        }
        return ans;
    }
}

实际上可以在窗口大小固定的情况下,枚举当前窗口中对应的字符数量是否和p中的字符数量相同,如果相同则记录子串的起始索引,反之则窗口向前滑动,更新当前窗口的字符数量。

java 复制代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        // 边界判断
        int n1 = s.length(), n2 = p.length();
        if (n1 < n2) {
            return new ArrayList<>();
        }
        // 存储结果
        List<Integer> ans = new ArrayList<>();
        /**
        如何判断是否两个字符串相等,由于这里都是小写字母,所以可以维护两个长为26的数组
        如果两个字符串中字符频率数组相同则说明两个字符相同
        相比频繁创建substring,创建子串会消耗内存和时间
         */
        int[] sCnt = new int[26];
        int[] pCnt = new int[26];
        // 初始化第一个窗口出现的频率
        for(int i = 0; i < n2; i++){
            sCnt[s.charAt(i) - 'a']++;
            pCnt[p.charAt(i) - 'a']++;

        }
        // 判断第一个窗口是否匹配
        if (matches(pCnt, sCnt)) {
            ans.add(0);
        }

        // 滑动窗口
        // 窗口范围,索引为n2到n1-1
        // 每次加一个,左边界对应字符频率减一个
        for (int i = n2; i < n1; i++) {
            // 新字符
            sCnt[s.charAt(i) - 'a']++;
            // 左边界旧字符
            sCnt[s.charAt(i-n2) - 'a']--;
            // 判断是否匹配
            if (matches(pCnt, sCnt)){
                ans.add(i-n2+1);
            }
        }
        return ans;
    }

    public boolean matches(int[] pCnt, int[] sCnt){
        // 判断对应字符的频率是否相同
        for (int i = 0; i < 26; i++) {
            if (pCnt[i] != sCnt[i]){
                return false;
            }
        }
        return true;
    }
}

Java中判断两个字符串是否相等。

560 和为K的子数组

中等

解题思路:

从暴力循环------》想到前缀和,进一步将前缀和数组理解为两数只和,利用哈希表统计 ------》

java 复制代码
class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length;
        int cnt = 0, pre = 0;

        // 创建map 存储
        Map<Integer, Integer> map = new HashMap<>();
        // 初始化第一个
        map.put(0, 1);
        // 边计算前缀和,然后更新map
        for (int i = 0; i < n; i++){
            pre += nums[i];
            if (map.containsKey(pre - k)) {
                cnt += map.get(pre - k);
            }
            map.put(pre, map.getOrDefault(pre, 0) + 1);
        }
        return cnt;
    }
}

239 滑动窗口最大值

或者自己实现一个双端队列,自己保持队列的单调性

java 复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new int[0];
        }
        int n = nums.length;
        // 初始化结果数组
        int[] res = new int[n - k + 1];
        // 不用优先队列自己利用双端队列实现
        Deque<Integer> deque = new ArrayDeque<>();

        for (int i = 0; i < n; i++) {
            // 1. 判断队首已经滑出窗口的过期元素
            while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            // 2. 维护单调递减特性移除队尾所有比当前元素小的元素
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            // 3. 将当前元素的下标加入队尾
            deque.offerLast(i);

            // 4. 然后当前队列中就是保持单调的
            if (i >= k - 1) {
                res[i-k+1] = nums[deque.peekFirst()];
            }
        }

        return res;
        
    }
}

76 最小覆盖子串

困难

滑动窗口思路到位,就很好理解。

java 复制代码
class Solution {
    public String minWindow(String s, String t) {
        /**
        首先判断s长度是否大于等于t的长度,
        有点类似滑动窗口最大值、无重复的最长子串的做法思路可以维护一个窗口,每次窗口先向右扩充,判断当前窗口字符数能否满足包含t
        如果满足则左边收敛
         */
        int m = s.length();
        int n = t.length();
        // 判断边界条件
        if (m < n) return ""; 
        // 使用哈希表存储字符及其对应次数
        Map<Character, Integer> tMap = new HashMap<>();
        Map<Character, Integer> sMap = new HashMap<>();
        // 统计t
        for (char c : t.toCharArray()) {
            tMap.put(c, tMap.getOrDefault(c, 0) + 1);
        }
        // // 初始化sMap
        // for (int i = 0; i < n; i++) {
        //     sMap.put(s.charAt(i), sMap.getOrDefault(s.charAt(i), 0) + 1);
        // }
        // 直接定义左右指针
        int left = 0;
        int valid = 0; // 记录窗口中满足条件的字符种类数量
        int start = 0, minLen = Integer.MAX_VALUE; // 记录最小子串的起点和终点
        // 右指针向前移动
        for (int right = 0; right < m; right++) {
            char cright = s.charAt(right);
            // 判断当前字符是否是在t中
            if (tMap.containsKey(cright)){
                // 存入当前窗口中
                sMap.put(cright, sMap.getOrDefault(cright, 0) + 1);
                // 并统计当前窗口中包含t的字符数量
                if (sMap.get(cright).intValue() <= tMap.get(cright).intValue()){
                    valid++;
                }
            }
            // 判断是否valid == n说明此时已经包含了t了
            // 向左收缩窗口
            while (valid == n) {
                if (right - left + 1 < minLen) {
                    // 更新
                    start = left;
                    minLen = right - left + 1;

                }

                // 收缩左边界
                char cleft = s.charAt(left);
                // 判断左边界的字符是不是包含t的字符
                if (tMap.containsKey(cleft)){
                    // 判断移除该字符是否会导致包含t的字符减少也就是当窗口内字符数与t相同的情况下
                    if (sMap.get(cleft).intValue() == tMap.get(cleft).intValue()){
                        valid--;
                    }
                    // 窗口内变化
                    sMap.put(cleft, sMap.getOrDefault(cleft, 0)  - 1);
                }
                left++;
            }

        }
        // 判断第一个窗口是否满足

        // 遍历后续的每加入一个都要重新判断吗?

        return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
    }
}

53 最大子数组和

中等

解题思路:

动态规划

java 复制代码
class Solution {
    public int maxSubArray(int[] nums) {
        /**
        暴力循环可以求得最大子数组和
        如何优化呢使用动态规划?
        使用i标识当前位置的最大子数组和是多少
        应该是dp[i] = Math.max(dp[i-1] + nums[i], nums[i])
        可以这样的原因是前面的都小了肯定起点从当前开始
         */
        // 使用dp[i]表示当前位置的最大子数组和
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int ans = nums[0];
        for (int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

56 合并区间

中等

解题思路:

合并区间问题,可以先将区间按照起始位置进行排序,涉及到二维数组的排序

初始化合并区间时,没有必要先存入第一个元素,而是初始化为变量,当第一个合并区间截止,才存入结果数组中,然后更新变量。

java 复制代码
class Solution {
    public int[][] merge(int[][] intervals) {
        // 可以理解为是二维数组将二维数组先按照起始位置排序
        // 遍历从第二个开始看第一个结尾和第二个起始位置是否重叠,重叠则合并反之则加入

        // 先排序
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
        // 创建一个动态二维数组
        List<int[]> ans = new ArrayList<>();
        // 存储第一个
        // ans.add(intervals[0]);
        // 先不要存到结果数组中,而是初始化
        int start = intervals[0][0], end = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {
            // int start = intervals[i][0];
            // int end = intervals[i][1];
            // 判断是否有交集
            if (intervals[i][0] <= end) {
                end = Math.max(end, intervals[i][1]);
            }else {
                // 无重叠
                // 将当前区间存入
                ans.add(new int[]{start, end});
                // 更新
                start = intervals[i][0];
                end = intervals[i][1];

            }
        }
        // 结束后还有最后一个区间
        ans.add(new int[]{start, end});
        return ans.toArray(new int[ans.size()][]);
    }
}

或者直接从下标0元素遍历即可

java 复制代码
class Solution {
    public int[][] merge(int[][] intervals) {
        /**
        显然要合并区间,需要先对区间进行排序
        然后依次遍历区间,假设起始结尾区间大小为pre = 0,
        如果当前区间的inter[0] <= pre,则要合并当前区间,
        反之存储上一段区间,然后这一段区间作为新的区间
         */
        // 注意边界
        int n = intervals.length;
        // 排序
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
        // 假设当前一个区间的起点和终点start, end
        // int start = inter[0], end = inter[1];
        // List<List<Integer> ans = new ArrayList<>();
        // for (int i = 1; i < n; i++) {
        //     if (intervals[i][0] <= end) {
        //         // 说明需要合并
        //         end = Math.max(end, intervals[i][1]);
        //     }else {
        //         // 保存,更新
        //         List<Integer> tmp = 
        //         start = intervals[i][0];
        //         end = intervals[i][1];
        //     }
        // }
        // 但是末尾的怎么处理呢?
        List<int[]> ans = new ArrayList<>();
        for (int[] inter : intervals) {
            // 这种思路解决了动态添加区间的问题
            int m = ans.size();
            if (m > 0 && inter[0] <= ans.get(m-1)[1]) {
                // 可以合并
                ans.get(m-1)[1] = Math.max(ans.get(m-1)[1], inter[1]);
            }else {
                ans.add(inter);
            }
        }
        // 转换为int[][]
        return ans.toArray(new int[ans.size()][]);
    }
}

基础知识:创建二维数组List<int[]> ans = new ArrayList<>(),文章解析。

189 轮转数组

中等

有多种思路,可以额外用一个数组进行存储变换后的数组。此时空间复杂度O(n),为了空间复杂度为O(1),可以直接在原地操作。

规律是先整体翻转,然后是按照k的位置,分别进行翻转。需要制定一个翻转方法,将指定区间进行翻转。

java 复制代码
class Solution {
    public void rotate(int[] nums, int k) {
        reverse(nums, 0, nums.length - 1);
        // reverse(nums, 0, k-1);
        // reverse(nums, k, nums.length - 1);
        // k求余之后的位置
        reverse(nums, 0, k % nums.length - 1);
        reverse(nums, k% nums.length, nums.length - 1);
    }

    // 指定区间翻转
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int tmp = nums[start];
            nums[start] = nums[end];
            nums[end] = tmp;
            start++;
            end--;
        }
    }
}
相关推荐
我是一颗柠檬1 小时前
【Java项目技术亮点】滑动窗口限流算法
java·开发语言·算法
无限码力1 小时前
华为非AI方向笔试真题 - 楼内救人
算法·华为·华为非ai方向笔试真题·华为笔试真题·华为算法题
一切皆是因缘际会1 小时前
隐层表征解构:LLM感知式幻觉稀疏成因
算法·数学建模·ai·架构
Irissgwe1 小时前
二叉树进阶
数据结构·c++·算法·c·二叉搜索树
无限码力1 小时前
华为非AI方向笔试真题 - 容器镜像平均大小统计
算法·华为·华为非ai方向笔试真题·华为笔试真题·华为非ai笔试真题·华为0612非ai笔试真题
无限码力1 小时前
华为非AI方向0612笔试真题-循环异或加密器(详细思路+多语言题解)
算法·华为·华为非ai方向笔试真题·华为笔试真题·华为0612笔试真题
凌波粒1 小时前
LeetCode--1584. 连接所有点的最小费用(最小生成树/Prim算法/Kruskal算法)
算法·leetcode·职场和发展
simidagogogo1 小时前
生产环境推荐系统最隐蔽的坑:Training-Serving Skew 详解与实战
算法·spark·推荐算法
大白话_NOI2 小时前
【二分答案】附通用模板
c++·算法