双指针与哈希表算法题解

14 阅读6分钟

题目一:移动零

问题分析

将数组中的所有零移动到末尾,保持非零元素的相对顺序,要求原地操作。

双指针解法

java

class Solution {
    public void moveZeroes(int[] nums) {
        int cur = 0;        // 遍历指针
        int dest = -1;      // 非零元素放置位置
        
        while (cur < nums.length) {
            if (nums[cur] != 0) {
                // 遇到非零元素,交换到dest位置
                dest++;
                int tmp = nums[cur];
                nums[cur] = nums[dest];
                nums[dest] = tmp;
            }
            cur++;
        }
    }
}

优化版本(减少交换次数)

java

class Solution {
    public void moveZeroes(int[] nums) {
        int nonZeroIndex = 0;
        
        // 将所有非零元素移到前面
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 0) {
                nums[nonZeroIndex++] = nums[i];
            }
        }
        
        // 将剩余位置填充为零
        while (nonZeroIndex < nums.length) {
            nums[nonZeroIndex++] = 0;
        }
    }
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

题目二:复写零

问题分析

将数组中的每个零复写一次,其余元素右移,超过数组长度的部分丢弃。

双指针解法

java

class Solution {
    public void duplicateZeros(int[] arr) {
        int n = arr.length;
        int dest = -1;
        int cur = 0;
        
        // 第一步:模拟复写过程,找到原数组中最后保留的元素位置
        while (dest < n - 1) {
            if (arr[cur] == 0) {
                dest += 2;  // 零需要两个位置
            } else {
                dest++;     // 非零只需要一个位置
            }
            
            if (dest < n - 1) {
                cur++;
            }
        }
        
        // 处理边界情况:最后一个元素是零且复写后刚好超出数组
        if (dest == n) {
            arr[n - 1] = 0;
            dest -= 2;
            cur--;
        }
        
        // 第二步:从后往前进行实际复写
        while (cur >= 0) {
            if (arr[cur] == 0) {
                arr[dest] = 0;
                arr[dest - 1] = 0;
                dest -= 2;
            } else {
                arr[dest] = arr[cur];
                dest--;
            }
            cur--;
        }
    }
}

详细步骤说明

  1. 模拟阶段:计算复写后需要的位置,确定哪些元素会被保留
  2. 边界处理:处理最后一个零导致数组溢出的特殊情况
  3. 实际复写:从后往前填充,避免覆盖未处理的元素

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

题目三:快乐数

问题分析

判断一个数经过各位平方和变换后是否能最终变为1,或者进入循环。

快慢指针解法

java

class Solution {
    // 计算数字各位的平方和
    private int getNext(int n) {
        int sum = 0;
        while (n > 0) {
            int digit = n % 10;
            sum += digit * digit;
            n /= 10;
        }
        return sum;
    }
    
    public boolean isHappy(int n) {
        int slow = n;
        int fast = getNext(n);
        
        // 快慢指针判断循环
        while (fast != 1 && slow != fast) {
            slow = getNext(slow);           // 慢指针走一步
            fast = getNext(getNext(fast));  // 快指针走两步
        }
        
        return fast == 1;
    }
}

哈希表解法

java

class Solution {
    private int getNext(int n) {
        int sum = 0;
        while (n > 0) {
            int digit = n % 10;
            sum += digit * digit;
            n /= 10;
        }
        return sum;
    }
    
    public boolean isHappy(int n) {
        Set<Integer> seen = new HashSet<>();
        
        while (n != 1 && !seen.contains(n)) {
            seen.add(n);
            n = getNext(n);
        }
        
        return n == 1;
    }
}

复杂度分析

  • 时间复杂度:O(log n)
  • 空间复杂度:O(log n)

题目四:盛水最多的容器

问题分析

找出两条垂线,使得它们与x轴构成的容器能容纳最多的水。

双指针解法

java

class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int maxArea = 0;
        
        while (left < right) {
            // 计算当前面积
            int currentHeight = Math.min(height[left], height[right]);
            int currentWidth = right - left;
            int currentArea = currentHeight * currentWidth;
            
            // 更新最大面积
            maxArea = Math.max(maxArea, currentArea);
            
            // 移动较短的边
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return maxArea;
    }
}

证明正确性

移动较短边的证明:

  • 面积由较短边的高度和宽度决定
  • 移动较长边不会增加最小高度,但会减少宽度
  • 移动较短边有可能找到更高的边,从而增加面积

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

题目五:有效三角形的个数

问题分析

统计数组中能组成三角形的三元组个数。

排序+双指针解法

java

class Solution {
    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int count = 0;
        int n = nums.length;
        
        // 固定最大的边c
        for (int c = n - 1; c >= 2; c--) {
            int left = 0;
            int right = c - 1;
            
            while (left < right) {
                // 三角形条件:a + b > c
                if (nums[left] + nums[right] > nums[c]) {
                    // 所有left到right-1的数都与right、c能组成三角形
                    count += (right - left);
                    right--;
                } else {
                    left++;
                }
            }
        }
        
        return count;
    }
}

暴力解法(超时)

java

class Solution {
    public int triangleNumber(int[] nums) {
        int count = 0;
        int n = nums.length;
        
        for (int i = 0; i < n - 2; i++) {
            for (int j = i + 1; j < n - 1; j++) {
                for (int k = j + 1; k < n; k++) {
                    if (nums[i] + nums[j] > nums[k] && 
                        nums[i] + nums[k] > nums[j] && 
                        nums[j] + nums[k] > nums[i]) {
                        count++;
                    }
                }
            }
        }
        
        return count;
    }
}

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)(排序使用O(log n))

题目六:两数之和

问题分析

在数组中找到两个数,使它们的和等于目标值。

哈希表解法

java

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> numToIndex = new HashMap<>();
        
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            
            // 检查补数是否在哈希表中
            if (numToIndex.containsKey(complement)) {
                return new int[]{numToIndex.get(complement), i};
            }
            
            // 将当前数和索引存入哈希表
            numToIndex.put(nums[i], i);
        }
        
        return new int[0]; // 题目保证有解,这里不会执行
    }
}

双指针解法(数组有序时)

java

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        
        while (left < right) {
            int sum = nums[left] + nums[right];
            
            if (sum == target) {
                return new int[]{left, right};
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
        
        return new int[0];
    }
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

题目七:三数之和

问题分析

找到所有不重复的三元组,使得三个数之和为0。

排序+双指针解法

java

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        int n = nums.length;
        
        for (int i = 0; i < n - 2; i++) {
            // 跳过重复的a
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            int left = i + 1;
            int right = n - 1;
            int target = -nums[i]; // b + c = -a
            
            while (left < right) {
                int sum = nums[left] + nums[right];
                
                if (sum == target) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    
                    // 跳过重复的b和c
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    
                    left++;
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        
        return result;
    }
}

四数之和扩展

java

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        int n = nums.length;
        
        for (int i = 0; i < n - 3; i++) {
            // 跳过重复的a
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            
            for (int j = i + 1; j < n - 2; j++) {
                // 跳过重复的b
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;
                
                int left = j + 1;
                int right = n - 1;
                long remaining = (long)target - nums[i] - nums[j];
                
                while (left < right) {
                    long sum = (long)nums[left] + nums[right];
                    
                    if (sum == remaining) {
                        result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        
                        // 跳过重复的c和d
                        while (left < right && nums[left] == nums[left + 1]) left++;
                        while (left < right && nums[right] == nums[right - 1]) right--;
                        
                        left++;
                        right--;
                    } else if (sum < remaining) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        
        return result;
    }
}

复杂度分析

  • 三数之和:O(n²)
  • 四数之和:O(n³)

通用解题模板

双指针模板

java

// 通用双指针框架
public void twoPointerTemplate(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    
    while (left < right) {
        // 根据条件移动指针
        if (condition) {
            left++;
        } else {
            right--;
        }
    }
}

哈希表模板

java

// 通用哈希表框架
public void hashMapTemplate(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;
        }
        map.put(nums[i], i);
    }
}

算法技巧总结

1. 双指针适用场景

  • 对撞指针:有序数组的两数之和、盛水容器
  • 快慢指针:判断循环、找中点
  • 滑动窗口:子数组问题

2. 哈希表适用场景

  • 快速查找:两数之和、重复元素检测
  • 频率统计:字符频率、元素出现次数
  • 映射关系:随机链表复制

3. 排序预处理

  • 很多问题在排序后变得简单
  • 注意排序的时间复杂度
  • 考虑稳定性要求

4. 边界条件处理

  • 空数组和单元素数组
  • 重复元素处理
  • 整数溢出(特别是四数之和)

这些题目涵盖了双指针和哈希表的核心应用,掌握这些技巧能够解决大多数相关的算法问题。