前言
在算法学习与实际工程中,贪心算法和双指针技巧都是高频出现且非常实用的工具。贪心算法能够以最优或近似最优的策略快速解决问题,双指针技巧则能在有序或可滑动窗口场景下高效处理数据。本文将从理论基础到经典案例,以 Java 示例深入讲解贪心算法与双指针的思路、证明与应用。
一、贪心算法基础
1.1 贪心策略概念
贪心算法(Greedy Algorithm)是一种每一步都选择在当前看来最优(局部最优)的策略,希望通过局部最优的积累达到全局最优或近似最优。
-
核心思想:在可行解空间中,依次做出局部最优选择。
-
关键问题:如何保证局部最优能导出全局最优?通常通过贪心选择性质与最优子结构证明。
1.2 正确性证明方法
-
贪心选择性质:证明一次局部最优选择不会影响到全局最优解(可以通过交换论证)。
-
最优子结构:假设问题规模为 n 的最优解包含子问题规模为 k(k<n)的最优解。
示例:区间调度
-
贪心策略:每次选择结束时间最早的区间,可以证明该策略满足贪心选择性质与最优子结构。
1.3 模板伪代码
// 通用贪心模板
public Result greedySolve(Input data) {
// 1. 将可选元素或策略排序/按某规则组织
sort(data, comparator);
Result res = initResult();
for (Element e : data) {
if (canTake(e, res)) {
take(e, res);
}
}
return res;
}
二、经典贪心案例
2.1 区间调度(会议室安排)
问题:给定一组会议的开始和结束时间,选择最多互不冲突的会议数量。
贪心策略:按结束时间升序排序,每次选择下一个结束最早且开始不冲突的会议。
int intervalSchedule(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
int count = 0, lastEnd = Integer.MIN_VALUE;
for (int[] in : intervals) {
if (in[0] >= lastEnd) {
count++;
lastEnd = in[1];
}
}
return count;
}
-
时间复杂度:O(n log n)
-
空间复杂度:O(1)
2.2 活动选择问题
核心与区间调度相同,可扩展为带权活动选择或带价值的场景,需动态规划或贪心近似。
2.3 零钱兑换(Coin Change)
问题:给定不同面额的硬币,求金额最少硬币数。货币面额不具备良好的贪心性质时,贪心可能不正确。
贪心策略(适用时):从最大面额硬币开始,尽可能多使用,再次至下一个。
int coinChangeGreedy(int[] coins, int amount) {
Arrays.sort(coins);
int count = 0;
for (int i = coins.length - 1; i >= 0 && amount > 0; i--) {
count += amount / coins[i];
amount %= coins[i];
}
return amount == 0 ? count : -1;
}
仅当硬币系统为“Canonical”时贪心正确,否则需要动态规划。
2.4 背包问题(贪心近似 vs 完全背包)
-
0/1 背包:需动态规划求解,贪心不一定最优。
-
分数背包(Fractional Knapsack):可拆分物品,贪心按单位价值排序最优。
double fractionalKnapsack(Item[] items, double capacity) {
Arrays.sort(items, (a,b)->Double.compare(b.value/a.weight, a.value/a.weight));
double total = 0;
for(Item it : items) {
if(capacity >= it.weight) {
total += it.value;
capacity -= it.weight;
} else {
total += it.value * (capacity/it.weight);
break;
}
}
return total;
}
2.5 分发饼干 / 分发糖果问题
LeetCode 455: 给孩子和饼干满足度,按满足度排序;贪心地给最不挑剔的孩子最小足够的饼干。
int findContentChildren(int[] g, int[] s) {
Arrays.sort(g); Arrays.sort(s);
int i = 0, j = 0;
while (i < g.length && j < s.length) {
if (s[j] >= g[i]) { i++; j++; }
else j++;
}
return i;
}
-
时间复杂度:O(n log n)
-
空间复杂度:O(1)
三、双指针技巧
双指针(Two Pointers)技术常用于有序数组或链表,以及滑动窗口场景,能将 O(n^2) 降至 O(n) 或 O(n log n)。
3.1 左右指针收缩
两数之和(2Sum)
-
场景:有序数组中找和为目标的两个数。
-
思路:
l=0, r=n-1
,若 sum>target 则 r--,若 sum<target 则 l++,直至找到或交错。
int[] twoSum(int[] nums, int target) {
int l=0, r=nums.length-1;
while(l<r) {
int sum = nums[l]+nums[r];
if(sum==target) return new int[]{l,r};
else if(sum<target) l++;
else r--;
}
return new int[]{};
}
三数之和(3Sum)
-
固定第一个指针,再对剩余区间做双指针。
List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for(int i=0;i<nums.length-2;i++){
if(i>0 && nums[i]==nums[i-1]) continue;
int l=i+1, r=nums.length-1;
while(l<r) {
int sum=nums[i]+nums[l]+nums[r];
if(sum==0){
res.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 r--;
}
}
return res;
}
3.2 滑动窗口
用于在数组/字符串中寻找满足条件的最长或计数问题。
子数组之和问题
-
固定窗口: 和为固定值 → 双指针
-
可变窗口: 最长不含重复字符 → 动态维护左右边界
int maxSubArrayLen(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();
map.put(0,-1);
int sum=0, res=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
if(map.containsKey(sum-k)) res=Math.max(res, i-map.get(sum-k));
map.putIfAbsent(sum,i);
}
return res;
}
长度最长无重复子串
int lengthOfLongestSubstring(String s) {
int[] last = new int[128];
Arrays.fill(last, -1);
int res=0, start=0;
for(int i=0;i<s.length();i++){
start = Math.max(start, last[s.charAt(i)]+1);
res = Math.max(res, i-start+1);
last[s.charAt(i)] = i;
}
return res;
}
3.3 快慢指针
-
链表:找中点、判断回文、Floyd 判圈。
// 链表中点
ListNode midNode(ListNode head) {
ListNode slow=head, fast=head;
while(fast!=null && fast.next!=null) {
slow=slow.next;
fast=fast.next.next;
}
return slow;
}
// 判断回文链表
boolean isPalindrome(ListNode head) {
// 找中点,反转后半,比较
}
四、小结与建议
-
贪心算法:适用于可用局部最优导出全局最优的问题,需严格证明贪心属性。
-
双指针:在有序或窗口场景下高效减少复杂度。
-
滑动窗口:动态维护符合条件的子数组/子串边界。
-
快慢指针:链表和序列处理中常用技巧,O(n)空间优化。
💡 练习题:
区间合并、插入区间、波兰表达式求值、滑动窗口最大值(LeetCode 239)、环形链表 II、分发糖果 II