Hot 100总结(1-10)

Hot 100总结(逐行注释版)

哈希篇

1.两数之和

用哈希表优化

class Solution {
    public int[] twoSum(int[] nums, int target) {
        // new一个哈希表,键为数组值,值为索引
        Map<Integer,Integer> hs=new HashMap();
        for(int i=0;i<nums.length;i++){
            // 相当与在数组里找到一个数是目标值-第一个数
            int dou=target-nums[i];
            // 如果哈希表里有这个数直接return结果
            if(hs.containsKey(dou)){
                return new int[]{i,hs.get(dou)};
            }
            // 如果没有就把自己加进去(这样可以再判断是否有数的时候避免用自己)
            // (这样可以再判断是否有数的时候避免用自己,相当于先判断有没有再加自己进去)
            hs.put(nums[i], i);
        }
        // 题目要求走不到这里,随便return
        return new int[]{0,0};
    }
}
2.字母异位词分组

因为abc,cba经过排序后都是abc,就可以看排序后是否相同分组,用哈希表优化

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // new一个哈希表,键是排序后的字符串,值为所有排序为键str的字符串集合,也就是一个分组
        Map<String,List<String>> map=new HashMap();
        // 处理每一个字符串
        for(String s:strs){
            // 转成字符数组排序
            char[] ss=s.toCharArray();
            Arrays.sort(ss);
            // 将排序后的数组转为字符串得到键
            String str=new String(ss);
            // 如果map包含这个键
            if(map.containsKey(str)){
                // 就拿到这个键对应的值集合,把当前字符串加进去
                map.get(str).add(s);
                // 这里记得要加continue跳过本次循环
                continue;
            }
            // 如果没有这个键就new一个集合,相当于新建一个分组
            List<String> re=new ArrayList();
            re.add(s);
            // 连键加值一起放到map里面
            map.put(str,re);
        }
        // 结果就是map的值集合
            return new ArrayList<>(map.values());
    }
}

当然如果熟练使用api可以简化代码

可以用map的computeIfAbsent一行搞定

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> m = new HashMap<>();
        for (String str : strs) {
            char[] s = str.toCharArray();
            Arrays.sort(s);
            // s 相同的字符串分到同一组
            m.computeIfAbsent(new String(s), k -> new ArrayList<>()).add(str);
        }
        return new ArrayList<>(m.values());
    }
}
3.最长连续序列
class Solution {
    public int longestConsecutive(int[] nums) {
        int res = 0;
        // 要求时间复杂度O(n),所以用哈希优化
        Set<Integer> set = new HashSet();
        // 先将所有元素都加入set集合
        for (int a : nums) {
            set.add(a);
        }
        // 对set里面的一个元素x来说,如果它作为起点,则x-1一定不在set里面
        for (int x : set) {
            // 初始化长度为1,即只包含一个元素
            int cnt = 1;
            if (set.contains(x - 1)) {
                // 如果里面包含x-1则不讨论
                continue;
            }
            // 从x的下一个数x+1开始
            int y = x + 1;
            // 如果里面包含y
            while (set.contains(y)) {
                // 再看有没有y的下一个元素,且cnt加1以此类推
                y++;
                cnt++;
            }
            res = Math.max(res, cnt);
        }
        return res;
    }
}

双指针篇

4.移动零
class Solution {
    public void moveZeroes(int[] nums) {
        // 思路:双指针,找到第一个不是0的元素交换
        int n = nums.length;
        // 慢指针
        // j要从i开始往后面扫,每扫到一个不为0的数就停下交换,这个时候就能把i+1
        // 并且交换过来的一定是不为0的数,i如果不等于j则i和j之间一定是0
        for (int i = 0, j = i; i < n && j < n; j++) {
            if (nums[j] != 0) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
                i++;
            }
        }
    }
}
5.盛水最多的容器

双指针

class Solution {
    public int maxArea(int[] height) {
        // 相向双指针
        int i = 0, j = height.length - 1;
        int res = 0;
        while (i < j) {
            // 盛水多少主要看矮的那一边
            // 如果右边矮,那么右边的柱子左移可能变更矮也可能变高,变高的话相当于容量可以变大
            // 相反,如果移动高的柱子的话,因为容量与高的柱子无关,无论高的柱子怎么变化因为横坐标减少了容量一定减少
            // 所以我们要移动矮的那一边
            if (height[i] >= height[j]) {
                // 右边矮,先取当前最大容量,然后移动右边
                res = Math.max(res, (j - i) * height[j]);
                j--;
            } else {
                // 左边矮,先取当前最大容量,再移左边
                res = Math.max(res, (j - i) * height[i]);
                i++;
            }
        }
        return res;
    }
}
6.三数之和

方法:排序加双指针,要比哈希+两数之和快很多,面试就用排序加双指针

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // 此思路需排序,让小的或者说负数在左边,大的或者说正数在右边
        Arrays.sort(nums);
        // 初始化一个结果list
        List<List<Integer>> res = new ArrayList();
        // 如果连三个元素都没有,直接return空集合
        if (nums.length < 3) {
            return res;
        }
        // 开始遍历,nums[i]是第一个数
        for (int i = 0; i < nums.length; i++) {
            // 因为题目要求不重复,所以如果下一个数和这个数相等,直接i++移到下个数,这里体现在continue;
            if (i != 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            // 第一个数是nums[i];
            // 双指针,左指针指向第一个数的右边那个数,右指针指向最右边那个数
            int left = i + 1;
            int right = nums.length - 1;
            // 进入循环
            while (left < right) {
                // 三者的和为sum
                // 如果sum>0则说明右边的数大了,需要左移
                // 如果sum<0则说明左边的数小了,需要右移
                int sum = nums[i] + nums[left] + nums[right];
                if (sum < 0) {
                    left++;
                } else if (sum > 0) {
                    right--;
                } else {   // sum=0说明存在三元组满足要求,此时要记住题目的不重复要求
                    // 判断left<right是防止left加high了不满足外循环条件
                    // 如果左指针右边那个数等于左指针指向的数,则可以右移左指针
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    // 如果右指针左边那个数等于右指针左边那个数,则可以左移左指针
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    // 此时能保证没有重复的
                    res.add(new ArrayList(Arrays.asList(nums[i], nums[left], nums[right])));
                    // 找到一个三元组,正常移动指针
                    left++;
                    right--;
                }
            }
        }
        return res;
    }
}

本题还可以剪枝,即如果nums[0]>0则说明全是正数直接return,同理nums[n-1]<0,return

7.接雨水

本题有很种思路,先给出双指针思路,也是常见思路,后面再补充动态规划、栈等。

双指针:

class Solution {
    public int trap(int[] height) {
        int ans = 0;
        int left = 0;
        int right = height.length - 1;
        // 左右两边最高的墙,保证这两座墙直接的凹处一定能接到水
        int lmax = 0, rmax = 0;
        while (left < right) {
            // 每次移动后都要更新当前的两端最高柱子
            lmax = Math.max(lmax, height[left]);
            rmax = Math.max(rmax, height[right]);
            // 以矮的那边为主,接水高度不会超过矮的那一边
            // 这里有点像第5题
            // 如果左边矮,则移动左边
            if (lmax < rmax) {
                // 如果移动后的柱子比最高柱子低,说明有凹处,且右边一定可以拦住,直接计算当前列的水位
                ans += lmax - height[left];
                // 计算完当前列后右移
                left++;
            } else {
                // 同理
                ans += rmax - height[right];
                right--;
            }
        }
        // 当left=right的时候说明所有列都已经计算完毕,得到最后结果
        return ans;
    }
}

滑动窗口篇

滑动窗口模板总结:滑动窗口模版总结

8.无重复字符的最长子串
class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 判断重复,所用用数组哈希表
        int[] hs = new int[128];
        // 找最长,初始化为0;找最小,初始化为Integer.MAX_VALUE
        int max = 0;
        // 窗口模板:(i = 0, j = 0; j < s.length(); j++)
        // 注意:在for循环内只拓展右边界,让更多元素进来,满足题目要求后收缩左边界。
        for (int i = 0, j = 0; j < s.length(); j++) {
            // 将右边界放进来的元素个数进行计数,char直接转成ASCII码
            hs[s.charAt(j)]++;
            // 因为不含重复字符,所以当放进来的元素已经在窗口内存在的时候,收缩左边界
            // 收缩左边界,此时有两种情况:
            // 1.左边界出去的元素和右边界进来的元素不一致,则继续收缩左边界
            // 2.左边界出去的元素正好是右边界进来的元素,右边界元素个数降为1,退出while循环,继续j++拓展右边界
            while (hs[s.charAt(j)] > 1) {
                // 将左边界元素个数减一
                hs[s.charAt(i)]--;
                // 收缩左边界
                i++;
            }
            // 每次都要对满足条件(退出while)的窗口的长度进行取较大值操作
            // 窗口长度为j-i+1
            max = Math.max(max, j - i + 1);
        }
        return max;
    }
}

子串

9.和为 K 的子数组

刷完滑动窗口后这个题第一反应就是用滑动窗口,但实际上因为数组有正有负,无法满足滑动窗口所需要的单调性,所以无法使用,这里引用评论区大佬的一句话以及思路:

如果全部是负数也是可以的。全是正数的情况下滑动窗口主要是保证了右窗口入数据时,窗口内的和是增加的,左窗口出数据时,窗口内的和是减少的,这样就保证了单调性,这样我们就可以通过控制左右窗口在让窗口内的和接近目标值,也就是可以确保我们找都目标值。而如果数值有正有负的情况,单调性就被打破,入数据和不一定增加,出数据和也不一定减少,这样就无法人窗口内的和接近目标值了。

所以这题用前缀和加哈希表

思路:
和为k的子数组,看到子数组的和,首先想到的就是滑动窗口或者前缀和,但是因为这里的数据有负数,不满足滑动窗口的单调性,那么我们来看前缀和。
前缀和:p[i] = p[i-1]+nums[i] (i>0,p[0] = nums[0])
有了前缀和,我们可以求到所有的子数组的和。
比如:我们要求l到r区间的和
可以使用p[r]-p[l-1]求得。
了解这个原理后,我们在来看题目要求:和为k的数组的数目。
也就是p[r]-p[l-1] = k
p[l-1] = p[r] - k
替换l-1为i,r为j,(i<j)
p[i] = p[j] - k
得到这个式子,你有想法了吗
p[i]是p[j]前面的前缀和,那么我们可以使用一个哈希表来存储p[i],val为p[i]的数目,因为p[j]是之后的前缀和,所有我们可以去哈希表中去找有有没有满足p[j]-k的之前的前缀和

class Solution {
    public int subarraySum(int[] nums, int k) {
        Map<Integer,Integer> map=new HashMap();
        // 这个地方很重要,一定要把键0的初始化为1
        // 它的作用是处理那些从数组开头到某个位置的子数组和为k的情况。如果不进行这样的初始化,算法可能会遗漏这些重要的子数组。
        map.put(0,1);
        // 初始化答案
        int cnt=0;
        // 原地修改成前缀和数组
        for(int i=0,j=1;j<nums.length;i++,j++){
            nums[j]=nums[i]+nums[j];
        }
        // 对于满足要求的子数组就转换成了满足nums[i]-k
        for(int i=0;i<nums.length;i++){
            // 对于满足要求的子数组就转换成了满足nums[i]-k
            int target=nums[i]-k;
            // 下面就用两数之和的思路
            if(map.containsKey(target)){
                cnt+=map.get(target);
            }
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
        }
        return cnt;
    }
}

有不用原地修改数组,边用边拿和的版本就不贴了

10.滑动窗口的最大值

这题需要用到双向队列(单调版),也就是单调队列,也就是说我们需要新建一个队列,然后维护它里面的数值是单调递减的,这样每次都能保证单调队列的队首元素就是当前窗口中的最大值。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // new一个单调队列
        Deque<Integer> q = new LinkedList();
        // new一个结果数组,结果数组的长度应该是n-k+1,即窗口个数
        int[] res = new int[nums.length - k + 1];
        // 循环遍历元素
        // 注意!队列里装的不是元素值,而是索引,因为根据索引取元素是数组的正常操作,远远简单与根据元素取索引
        for (int i = 0; i < nums.length; i++) {
            // 如果当前队列不为空,且当前遍历的元素要大于队尾元素,就把队尾元素弹出,因为队尾元素肯定不是答案
            // 并且要满足单调性
            while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) {
                q.pollLast();
            }
            // 然后就可以把现在的索引加进去
            q.addLast(i);
            // 此时窗口尾索引就是遍历的i,则窗口头索引是i-k+1,如果队列的头的小于窗口头索引,说明队列头的元素已经不在窗口内
            // 所以弹出队列头的元素
            if (q.peek() < i - k + 1) {
                q.poll();
            }
            // 这里判断窗口有没有形成,即窗口头有没有出现,即大于0.
            // 假设窗口大小为3,则只有i遍历到2的时候,才形成了大小为3的窗口
            if (i + 1 >= k) {
                // 窗口形成后,每次的队列最大元素,即队首元素,就是当前窗口的最大值元素索引
                // 把这份元素加到结果宿主
                res[i + 1 - k] = nums[q.peek()];
            }
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值