题目描述

✅ 1163. 按字典序排在最后的子串

image-20260929074825421

题意分析

从字符串中选择一个非空连续子串,使它在字典序中最大,返回这个子串本身。字典序先比较第一处不同字符;若一方是另一方的完整前缀,则更长的一方更大。

任意没有延伸到原字符串末尾的子串,都可以保留相同起点继续延长,得到更大的字符串。因此最大子串一定是某个后缀,只需确定最优起始下标,无需枚举所有起止组合。

解法:双指针比较最大后缀

核心思路

[!blue]

用 i < j 表示两个待比较的后缀起点,k 表示它们已确认相等的前缀长度。比较 s[i + k] 与 s[j + k]:相等就增加 k,直到出现第一处不同,或较短的后缀到达字符串末尾。

一次失配不仅能淘汰两个起点中的较小者,还能淘汰一段连续起点。假设 s[i + k] < s[j + k],对任意 0 <= t <= k,从 i + t 和 j + t 出发的两个后缀仍先匹配 k - t 个字符,然后在同一对失配字符处由前者落败。因此 [i, i + k] 内的起点都不可能产生最大后缀。

此时令 i = max(i + k + 1, j):既越过刚证明失败的范围,也不能退回先前已经处理的候选;再令 j = i + 1,重新比较两个不同起点。反过来,若当前 i 的字符更大,同样可淘汰 [j, j + k],只需令 j += k + 1,保留 i。

每次换候选后都把 k 清零,因为原来的公共前缀只属于旧的一对起点。两个起点只会向右移动,失配前积累的比较长度转化成了这一轮的批量跳跃,避免重复逐一比较长公共前缀。

循环在 j + k 到末尾时结束。若 j 已经越界,说明没有后续候选;若只是匹配到后缀 j 的末尾,因为 i < j,后缀 i 更长且保留了相同前缀,所以它仍然更大。后面尚未逐个比较的起点,也能沿这段相等关系向前平移到更长的同前缀后缀,直到落回已处理区域,不会产生新的最大者。最终返回从 i 到串尾的后缀。

解题步骤

  1. 初始化 i = 0、j = 1、k = 0。
  2. 只要 j + k < n,就比较两个后缀在偏移 k 处的字符。
  3. 字符相等时继续延长公共前缀;i 一侧较小时,将 i 跳到 max(i + k + 1, j) 并重设 j = i + 1。
  4. j 一侧较小时,将 j 跳到 j + k + 1;两种失配处理后都清零 k。
  5. 循环结束后返回 s[i..n)。

代码实现

class Solution {
    public String lastSubstring(String s) {
        int n = s.length();
        int i = 0;
        int j = 1;
        // k 记录两个候选后缀已经确认相等的前缀长度。
        int k = 0;

        while (j + k < n) {
            char a = s.charAt(i + k);
            char b = s.charAt(j + k);

            if (a == b) {
                k++;
            } else if (a < b) {
                // i 输了,[i, i+k] 的起点都可淘汰。
                i = Math.max(i + k + 1, j);
                j = i + 1;
                k = 0;
            } else {
                // j 输了,[j, j+k] 的起点同样被连带否定。
                j = j + k + 1;
                k = 0;
            }
        }

        return s.substring(i);
    }
}
func lastSubstring(s string) string {
    n := len(s)
    // i、j 是候选起点,k 是已经确认相等的前缀长度。
    i, j, k := 0, 1, 0
    for j+k < n {
        a := s[i+k]
        b := s[j+k]
        if a == b {
            k++
        } else if a < b {
            // i 输了,[i, i+k] 的起点都可淘汰。
            if i+k+1 > j {
                i = i + k + 1
            } else {
                i = j
            }
            j = i + 1
            k = 0
        } else {
            // j 输了,[j, j+k] 的起点同样被连带否定。
            j = j + k + 1
            k = 0
        }
    }
    return s[i:]
}

复杂度分析

  • 时间复杂度:$O(n)$。每次失配前的相等比较,可以归入随后至少同规模的指针前移;最后一段连续匹配至多扫描到串尾。Java 构造返回子串的复制也至多为线性。
  • 空间复杂度:搜索辅助空间为 $O(1)$。Java 返回子串另需保存结果内容,Go 返回原字符串的后缀视图。

关键点总结

[!green]

  • 先用前缀较短则字典序较小的规则,将所有子串归约为后缀选择。
  • 已匹配前缀支持平移后的同一失配结论,所以一次比较能排除连续多个起点。
  • 保持两个起点单向前进,每次换候选重新计算公共前缀。
  • 较短候选匹配到末尾时,较早开始的长后缀胜出,无需继续读取越界字符。

易错点总结

[!yellow]

  • 只挑最大首字符的第一个或最后一个位置,忽略首字符相同时还要继续比较后续内容。
  • 失配后只把较小一侧前进一格,会重复比较长前缀,失去线性时间保证。
  • 更新 i 时不与 j 取最大值,可能重新使用此前已被处理或淘汰的起点。
  • 换候选后不清零 k,会把旧后缀的相等结论错误用于新后缀。
  • 循环只判断 j < n,无法阻止公共前缀增长后偏移位置越界。

相似题目

题目 难度 关联与区别
1754. 构造字典序最大的合并字符串 中等 同样需要比较剩余后缀的字典序,而不只是当前首字符,以决定最优结果。
555. 分割连接字符串 中等 原题还允许循环切分与独立翻转每段,本题只在单个固定字符串中选择最大后缀。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/52879251
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!