LeetCode 763. 划分字母区间 - 贪心与双指针详解

一、文章标题 LeetCode 763. 划分字母区间 - 贪心与双指针详解

二、文章内容

1. 题目概述
  • 题目描述:给定一个仅包含小写字母的字符串 s,将其划分为尽可能多的片段,使得每个字母最多只会出现在其中的一个片段中。返回一个表示每个片段大小的列表。
  • 原题链接:https://leetcode.cn/problems/partition-labels/
  • 难度等级:Medium
  • 相关标签:贪心、双指针、哈希表、字符串、区间合并
2. 文章目录

目录

  1. 题目概述
  2. 解题思路
  3. 算法详解
  4. 解法对比
  5. 最优解推荐
3. 解题思路
  • 问题分析:
    • 输入:字符串 s(仅含小写字母)。
    • 输出:整型列表,依次为每个划分片段的长度。
    • 目标:尽可能多地划分,且每个字母只属于某一个片段;片段拼接仍为原顺序的全覆盖划分。
  • 核心难点:
    • 如何确定当前片段的“最远右边界”,以保证片段内所有字符在该边界之后不再出现。
    • 动态维护边界并在恰当时机切分,保证切分数量最大化且正确。
  • 解题方向:
    1. 贪心 + 双指针:预处理每个字母的最后出现位置,线性扫描时不断扩张当前片段右边界为所有已见字符的最右位置,当索引走到该右边界即刻切分。
    2. 区间合并:对每个字母构造 [first,last] 区间,按起点排序后合并重叠区间,合并后的每个区间长度即答案。
    3. 其他(不推荐):暴力尝试切分点或频繁检查后缀是否包含当前片段内字符,时间复杂度较高。
4. 算法详解

解法一:贪心 + 双指针(记录最后出现位置)

算法原理

  • 预处理 last[c] 为字符 c 在 s 中的最后出现索引。
  • 从左到右扫描,用两个指针:
    • start 表示当前片段起点;
    • end 表示当前片段必须覆盖到的最远右边界(初始化为当前字符的最后位置)。
  • 遍历过程中,遇到新字符就用其 last 位置更新 end;当索引 i 走到 end,说明从 start 到 end 的片段已“自成闭环”,可以切分。
  • 适用场景:仅小写字母,数组 O(26) 存 last,时间 O(n)、空间 O(1)。

具体实现

  • 步骤1:边界处理,若 s 为空或长度为 0,返回空列表。
  • 步骤2:预处理 last[26],记录每个字母的最后出现位置。
  • 步骤3:线性扫描维护 end;当 i == end 时记录片段长度 end - start + 1,并更新 start = i + 1。

复杂度分析

  • 时间复杂度:O(n)。两次线性扫描(一次预处理、一次切分)。
  • 空间复杂度:O(1)。固定大小数组 last[26]。

Java代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {
    // 解法一:贪心 + 双指针(记录最后出现位置)
    public List<Integer> partitionLabels(String s) {
        List<Integer> res = new ArrayList<>();
        // 边界处理:空或长度为 0,直接返回空结果
        if (s == null || s.length() == 0) {
            return res; // 安全返回,不抛异常
        }
        int n = s.length();
        // 仅小写字母:预处理每个字符的最后出现位置
        int[] last = new int[26];
        for (int i = 0; i < n; i++) {
            last[s.charAt(i) - 'a'] = i;
        }
        int start = 0; // 当前片段起点
        int end = 0;   // 当前片段的最远右边界
        for (int i = 0; i < n; i++) {
            // 动态扩张右边界到已见字符的最右出现位置
            end = Math.max(end, last[s.charAt(i) - 'a']);
            // 当走到右边界,切分一个片段
            if (i == end) {
                res.add(end - start + 1);
                start = i + 1; // 下一个片段的起点
            }
        }
        return res;
    }
}

解法二:区间合并(按字母首末位置构造区间)

算法原理

  • 对每个字母记录其首次出现位置 first 和最后出现位置 last,形成 0~26 个区间。
  • 将这些区间按起点排序,按常规区间合并:
    • 若新区间起点 > 当前合并区间的右端,则输出当前合并区间长度并开启新区间;
    • 否则合并为右端的较大值。
  • 合并得到的每个区间长度即为一个划分片段的长度。
  • 适用场景:当字符集较小(26)时,排序与合并开销极小,逻辑直观。

具体实现

  • 步骤1:一次遍历记录每个字母的 first 与 last;只为出现过的字母建立区间。
  • 步骤2:对区间按 first 升序排序。
  • 步骤3:线性合并并输出每个合并区间的长度。

复杂度分析

  • 时间复杂度:O(n + Σ log Σ),Σ 为出现的不同字母数(最多 26),因此整体近似 O(n)。
  • 空间复杂度:O(Σ)。存储少量区间与辅助数组。

Java代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class SolutionIntervals {
    // 解法二:区间合并(按字母首末位置构造区间)
    public List<Integer> partitionLabels(String s) {
        List<Integer> res = new ArrayList<>();
        if (s == null || s.length() == 0) {
            return res; // 安全返回
        }
        int n = s.length();
        int[] first = new int[26];
        int[] last = new int[26];
        Arrays.fill(first, -1);
        Arrays.fill(last, -1);
        // 记录每个字母的首末出现位置
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            if (first[c] == -1) first[c] = i;
            last[c] = i;
        }
        // 收集有效区间
        List<int[]> intervals = new ArrayList<>();
        for (int c = 0; c < 26; c++) {
            if (first[c] != -1) {
                intervals.add(new int[]{first[c], last[c]});
            }
        }
        // 按起点排序
        intervals.sort((a, b) -> Integer.compare(a[0], b[0]));
        // 合并区间
        int curL = -1, curR = -1;
        for (int[] it : intervals) {
            if (curL == -1) {
                curL = it[0];
                curR = it[1];
            } else if (it[0] > curR) { // 不重叠,输出上一个区间长度
                res.add(curR - curL + 1);
                curL = it[0];
                curR = it[1];
            } else { // 重叠,扩张右端
                curR = Math.max(curR, it[1]);
            }
        }
        // 收尾
        if (curL != -1) {
            res.add(curR - curL + 1);
        }
        return res;
    }
}
5. 解法对比与总结

| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 | |------|------------|------------|------|------|----------| | 贪心 + 双指针 | O(n) | O(1) | 一次扫描、实现最简、常数小 | 依赖字符集已知且较小 | 面试与工程首选 | | 区间合并 | O(n + Σ log Σ) | O(Σ) | 逻辑直观、易理解 | 需额外区间收集与排序 | 作为对照或教学实现 | | 暴力检查(不推荐) | O(n^2) | O(1) | 思路直接 | 性能差 | 仅小数据或教学 |

最优解推荐:基于时间、空间与可读性的综合考虑,推荐“贪心 + 双指针(记录最后出现位置)”方案,线性时间、常数空间,适合实际生产与面试书写。

三、文章标签 算法,贪心,LeetCode,中等,双指针,区间合并,字符串

四、文章简述 本题为字符串贪心划分:目标是将字符串切分为尽可能多的片段且每个字母只出现于一个片段。核心用贪心+双指针通过记录最后出现位置动态扩张右边界,线性时间完成;并提供区间合并对照实现,附Java注释代码与复杂度分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

辰辰星

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

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

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

打赏作者

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

抵扣说明:

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

余额充值