LeetCode Hot 100高频算法题精解:哈希表与双指针实战

发布时间:2026/11/6 4:02:14
LeetCode Hot 100高频算法题精解:哈希表与双指针实战

1. 算法刷题实战:LeetCode Hot 100(1-10)精解

算法刷题是每个程序员进阶的必经之路,而LeetCode Hot 100则是其中最经典的题目集合。今天我将分享前10道高频题目的解题思路和Java实现,重点解析哈希表、双指针和滑动窗口这三大核心技巧。

1.1 两数之和:哈希表的经典应用

两数之和(Two Sum)是算法入门的第一道题目,但它的解法却体现了哈希表的核心思想。题目要求找出数组中两个数,使它们的和等于目标值。

class Solution { 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[]{}; } }

关键点解析:

  1. 使用HashMap存储<数值, 索引>的映射关系
  2. 遍历时检查target-nums[i]是否存在于map中
  3. 时间复杂度O(n),空间复杂度O(n)

注意:不要先填充整个哈希表再遍历,这样无法处理重复元素的情况。正确的做法是边遍历边检查。

1.2 字母异位词分组:哈希表的灵活运用

字母异位词(Group Anagrams)问题需要将字母相同但排列不同的字符串归为一组。

class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String,List<String>> map=new HashMap<>(); for(String str:strs){ char[] c=str.toCharArray(); Arrays.sort(c); String key=new String(c); if(!map.containsKey(key)){ map.put(key,new ArrayList<>()); } map.get(key).add(str); } return new ArrayList<>(map.values()); } }

优化技巧:

  1. 使用排序后的字符串作为哈希键
  2. 对于大规模数据,可以用字符计数数组作为键,效率更高
  3. 时间复杂度O(nklogk),其中k是最长字符串长度

1.3 最长连续序列:哈希集合的巧妙使用

最长连续序列(Longest Consecutive Sequence)问题要求在未排序数组中找到最长连续数字序列的长度。

class Solution { public int longestConsecutive(int[] nums) { Set<Integer> set=new HashSet<>(); for(int num:nums) set.add(num); int max=0; for(int num:set){ if(!set.contains(num-1)){ // 确保是序列起点 int current=num; int count=1; while(set.contains(current+1)){ current++; count++; } max=Math.max(max,count); } } return max; } }

算法分析:

  1. 使用HashSet去重并实现O(1)查询
  2. 只从序列起点开始计算,避免重复工作
  3. 时间复杂度O(n),每个元素最多被访问两次

2. 双指针技巧的多样应用

双指针是解决数组/字符串问题的利器,下面几道题展示了它的不同应用场景。

2.1 移动零:快慢指针的经典案例

移动零(Move Zeroes)要求将所有0移动到数组末尾,同时保持非零元素的相对顺序。

class Solution { public void moveZeroes(int[] nums) { int slow=0; for(int fast=0;fast<nums.length;fast++){ if(nums[fast]!=0){ int temp=nums[slow]; nums[slow]=nums[fast]; nums[fast]=temp; slow++; } } } }

操作要点:

  1. slow指针指向下一个非零元素应该放置的位置
  2. fast指针寻找非零元素
  3. 交换而非直接赋值,可以保持原始数据

2.2 盛水最多的容器:左右指针的贪心策略

盛水最多的容器(Container With Most Water)需要找到两条线,使它们与x轴构成的容器能盛最多水。

class Solution { public int maxArea(int[] height) { int left=0, right=height.length-1; int max=0; while(left<right){ int area=(right-left)*Math.min(height[left],height[right]); max=Math.max(max,area); if(height[left]<height[right]){ left++; }else{ right--; } } return max; } }

为什么移动较矮的一边?因为容器的容量由较矮的一边决定,移动高的一边不会增加容量,而移动矮的一边有可能找到更高的边界。

2.3 三数之和:排序+双指针的完美结合

三数之和(3Sum)要求在数组中找到所有不重复的三元组,使其和为0。

class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> res=new ArrayList<>(); Arrays.sort(nums); for(int i=0;i<nums.length-2;i++){ if(i>0&&nums[i]==nums[i-1]) continue; // 去重 int left=i+1, right=nums.length-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]==nums[right-1]) right--; // 去重 left++; right--; }else if(sum<0){ left++; }else{ right--; } } } return res; } }

关键细节:

  1. 必须先排序才能使用双指针
  2. 需要仔细处理重复元素的情况
  3. 时间复杂度O(n²),空间复杂度O(1)

3. 滑动窗口与动态规划

3.1 无重复字符的最长子串:滑动窗口的典型应用

无重复字符的最长子串(Longest Substring Without Repeating Characters)需要找到不包含重复字符的最长子串。

class Solution { public int lengthOfLongestSubstring(String s) { Map<Character,Integer> map=new HashMap<>(); int max=0, left=0; for(int right=0;right<s.length();right++){ char c=s.charAt(right); if(map.containsKey(c)){ left=Math.max(left,map.get(c)+1); } map.put(c,right); max=Math.max(max,right-left+1); } return max; } }

窗口维护策略:

  1. 使用HashMap记录字符最后出现的位置
  2. 当遇到重复字符时,调整窗口左边界
  3. 时间复杂度O(n),空间复杂度O(min(m,n)),m是字符集大小

3.2 找到字符串中所有字母异位词:固定大小的滑动窗口

找到字符串中所有字母异位词(Find All Anagrams in a String)需要在字符串s中找到所有p的字母异位词的起始索引。

class Solution { public List<Integer> findAnagrams(String s, String p) { List<Integer> res=new ArrayList<>(); if(s.length()<p.length()) return res; int[] count=new int[26]; for(char c:p.toCharArray()) count[c-'a']++; int left=0, right=0, diff=p.length(); for(;right<p.length();right++){ char c=s.charAt(right); if(count[c-'a']>0) diff--; count[c-'a']--; } if(diff==0) res.add(0); for(;right<s.length();right++,left++){ char c=s.charAt(left); if(count[c-'a']>=0) diff++; count[c-'a']++; c=s.charAt(right); if(count[c-'a']>0) diff--; count[c-'a']--; if(diff==0) res.add(left+1); } return res; } }

优化技巧:

  1. 使用固定大小的滑动窗口(长度等于p的长度)
  2. 通过字符计数数组来跟踪匹配情况
  3. 时间复杂度O(n),空间复杂度O(1)

3.3 和为K的子数组:前缀和与哈希表

和为K的子数组(Subarray Sum Equals K)需要找到所有连续子数组,其和等于k。

class Solution { public int subarraySum(int[] nums, int k) { Map<Integer,Integer> map=new HashMap<>(); map.put(0,1); int sum=0, count=0; for(int num:nums){ sum+=num; if(map.containsKey(sum-k)){ count+=map.get(sum-k); } map.put(sum,map.getOrDefault(sum,0)+1); } return count; } }

前缀和原理:

  1. sum[i,j] = sum[0,j] - sum[0,i-1]
  2. 使用HashMap记录前缀和出现的次数
  3. 时间复杂度O(n),空间复杂度O(n)

4. 高级数据结构应用

4.1 滑动窗口最大值:双端队列的妙用

滑动窗口最大值(Sliding Window Maximum)需要在每个大小为k的滑动窗口中找到最大值。

class Solution { public int[] maxSlidingWindow(int[] nums, int k) { if(nums==null||nums.length==0) return new int[0]; int[] res=new int[nums.length-k+1]; Deque<Integer> deque=new ArrayDeque<>(); for(int i=0;i<nums.length;i++){ // 移除超出窗口范围的元素 while(!deque.isEmpty()&&deque.peekFirst()<i-k+1){ deque.pollFirst(); } // 移除小于当前元素的元素 while(!deque.isEmpty()&&nums[deque.peekLast()]<nums[i]){ deque.pollLast(); } deque.offerLast(i); if(i>=k-1){ res[i-k+1]=nums[deque.peekFirst()]; } } return res; } }

双端队列策略:

  1. 队列中存储的是索引而非值
  2. 队列中的元素按从大到小排列
  3. 时间复杂度O(n),每个元素最多进出队列一次

5. 算法刷题经验分享

在实际刷题过程中,我总结了以下几点经验:

  1. 理解比记忆更重要:不要死记硬背解法,要理解每个算法背后的思想
  2. 分类练习:按算法类型集中练习,比如一周专攻双指针问题
  3. 反复练习:经典题目要多次练习,直到能独立写出无bug的代码
  4. 分析复杂度:对每个解法都要能分析时间和空间复杂度
  5. 边界条件:特别注意空输入、极端值等边界情况

对于Java开发者,还需要注意:

  • 合理使用集合框架(HashMap, HashSet, Deque等)
  • 注意数组越界和空指针异常
  • 字符串操作要小心性能问题(如频繁拼接)

最后提醒:算法能力的提升需要时间和耐心,建议每天坚持做2-3题,长期积累会有显著进步。