第九篇:贪心算法与双指针(上)

前言

        在算法学习与实际工程中,贪心算法和双指针技巧都是高频出现且非常实用的工具。贪心算法能够以最优或近似最优的策略快速解决问题,双指针技巧则能在有序或可滑动窗口场景下高效处理数据。本文将从理论基础到经典案例,以 Java 示例深入讲解贪心算法与双指针的思路、证明与应用。


一、贪心算法基础

1.1 贪心策略概念

贪心算法(Greedy Algorithm)是一种每一步都选择在当前看来最优(局部最优)的策略,希望通过局部最优的积累达到全局最优或近似最优。

  • 核心思想:在可行解空间中,依次做出局部最优选择。

  • 关键问题:如何保证局部最优能导出全局最优?通常通过贪心选择性质最优子结构证明。

1.2 正确性证明方法

  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) {
    // 找中点,反转后半,比较
}

四、小结与建议

  1. 贪心算法:适用于可用局部最优导出全局最优的问题,需严格证明贪心属性。

  2. 双指针:在有序或窗口场景下高效减少复杂度。

  3. 滑动窗口:动态维护符合条件的子数组/子串边界。

  4. 快慢指针:链表和序列处理中常用技巧,O(n)空间优化。

💡 练习题

  • 区间合并、插入区间、波兰表达式求值、滑动窗口最大值(LeetCode 239)、环形链表 II、分发糖果 II

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stay Passion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值