leetcode
leetcode 951 ~ 1000
最长重复子串

最长重复子串

难度:

标签:

题目描述

代码结果

运行时间: 362 ms, 内存: 21.0 MB


/*
 * Problem: Find the longest duplicate substring in a given string using Java Streams.
 * 
 * Approach:
 * 1. Utilize binary search to determine the length of the longest duplicate substring.
 * 2. Use the Stream API and a hash set to find duplicates efficiently.
 * 3. The Rabin-Karp rolling hash algorithm is used for hashing substrings.
 */

import java.util.*;
import java.util.stream.*;

public class SolutionStream {
    public String longestDupSubstring(String s) {
        int left = 1, right = s.length();
        String result = "";
        while (left < right) {
            int mid = left + (right - left) / 2;
            String dup = findDuplicateUsingStream(s, mid);
            if (dup != null) {
                result = dup;
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return result;
    }

    private String findDuplicateUsingStream(String s, int length) {
        Set<Long> seen = new HashSet<>();
        long base = 26, mod = (long) Math.pow(2, 32);
        return IntStream.range(0, s.length() - length + 1)
                .mapToObj(i -> s.substring(i, i + length))
                .mapToLong(sub -> sub.chars().mapToLong(c -> c - 'a').reduce(0, (acc, c) -> (acc * base + c) % mod))
                .filter(hash -> !seen.add(hash))
                .mapToObj(hash -> s.substring(seen.stream().mapToInt(seen::hashCode).boxed().collect(Collectors.toList()).indexOf(hash),
                        seen.stream().mapToInt(seen::hashCode).boxed().collect(Collectors.toList()).indexOf(hash) + length))
                .findFirst().orElse(null);
    }
}

解释

方法:

这个题解使用了后缀数组的思想。主要步骤如下: 1. 通过 SA-IS 算法构建后缀数组 sa 和 rk 数组。sa[i] 表示将所有后缀排序后第 i 小的后缀的起始位置,rk[i] 表示后缀 s[i:] 在 sa 中的排名。 2. 通过 sa 和 rk 计算 height 数组。height[i] 表示 sa[i] 和 sa[i-1] 这两个后缀的最长公共前缀(LCP)的长度。 3. 在 height 数组中找到最大值 mh,则最长重复子串长度为 mh,起始位置为 sa[height.index(mh)]。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在构建后缀数组时,为什么选择SA-IS算法而不是其他算法如后缀树或Burrows-Wheeler变换?
SA-IS算法在构建后缀数组时具有高效和低内存消耗的优点。与后缀树相比,后缀数组更加简洁,占用内存更少,且SA-IS算法的时间复杂度为O(n),非常适合处理大规模数据。相较于Burrows-Wheeler变换,SA-IS直接构建后缀数组,适用性更广,且在实现上更直接。此外,SA-IS算法适用于多种字符集和复杂度较低,这使得它在实际应用中非常受欢迎。
🦆
后缀数组sa、排名数组rk和最长公共前缀数组height之间有什么具体的关系和作用?
后缀数组sa包含了字符串的所有后缀的起始位置的排序索引,即sa[i]是第i小的后缀的起始位置。排名数组rk是后缀数组的逆序数组,即rk[i]表示后缀s[i:]在所有后缀中的排序位置。最长公共前缀数组height记录了排序后相邻两个后缀的最长公共前缀长度,具体来说,height[i]是sa[i]和sa[i-1]的最长公共前缀长度。这三个数组互相配合,可以高效地解决字符串的多种问题,如最长重复子串的查找。
🦆
为什么在计算height数组时需要在字符串末尾添加一个特殊字符`#`?
在字符串末尾添加一个特殊字符`#`(通常是一个在字符串中未出现过的最小字符)的目的是为了保证计算最长公共前缀时边界的正确性。这个特殊字符确保了字符串的任何后缀都不会与其它后缀完全相同,从而避免了在计算过程中对字符串末尾的越界访问,同时简化了比较逻辑,因为加入特殊字符后,不需要额外的条件判断来停止比较。
🦆
在计算最长公共前缀(LCP)时,变量k的作用是什么,为什么初始化时k为0,并在每次循环时递减k?
变量k用于记录当前比较中已匹配的字符数。在计算最长公共前缀时,k的初始值设为0,表示开始时没有任何匹配。递减k的操作是为了利用已知的信息减少重复比较。具体来说,如果上一次比较已经找到了k个匹配字符,那么下一次比较可以从第k个字符开始,而不是重新从头开始比较。这种技术称为LCP数组的'phi'技巧,可以有效减少比较的总次数,提高算法效率。

相关问题