繁体   English   中英

通过预处理在 O(1) 时间内计算子字符串中的字符出现次数

[英]Count character occurrences in a substring in O(1) time with preprocessing

之前没有人问过这个问题,因为在预处理字符串所花费的时间被分摊到许多 freq 操作之后,我特别询问了O(1)常数时间。

一位面试官让我找出子字符串中出现的字符的总数。 例如,如果给你一个字符串、要查找的字符以及要搜索的位置的开始和结束索引,我应该找到最优化的方式,例如:

String s = "abcnc";
char find = "c"
int start = 1;
int end = 4;

应返回结果 2,因为'c''bcnc'的指定子字符串中出现两次。

我所做的很简单,就是

int freq(String s, char c, int start, int end) {
    int result = 0; 
    for(int i = start, i < end; i++) {
        if(s.charAt(i) == c) {
            result++;
        }
    }
    
    return result;
}

其时间复杂度为O(N)

但是,面试官说可以通过先预处理字符串来更优化,其中freq()方法可以有O(1)的复杂度。 我很困惑,因为我不知道除了O(N)之外还可以如何优化它。 面试官告诉我,我应该使用地图或列表或两者同时使用,并首先找到这些字符所在的索引,这将为我提供更优化的解决方案。

您可以使用 O(N) 空间和 O(N) 预处理时间在 O(1)(恒定时间)内完成此操作,并且不使用地图或除基本 java 以外的任何其他内容,如下所示:

对字符串进行单次传递并保留到目前为止看到的每个字母的累积计数:

int[][] counts;

public void process(String input) {
    counts = new int[input.length()][];
    for (int i = 0; i < input.length(); i++) {
        counts[i] = i == 0 ? new int[26] : Arrays.copyOf(counts[i - 1], 26);
        counts[i][input.charAt(i) - 'a']++;
    }
}

然后返回开始和结束计数之间的差异:

public int count( char c, int start, int end) {
    return counts[end][c - 'a'] - counts[start][c - 'a'];
}

首先,将您的输入字符串转换为将字符映射到排序的索引列表的映射。 例如:

String input = "abcnc";
var store = new HashMap<Character, TreeSet<Integer>>();
for (int i = 0; i < input.length(); i++) {
  store.computeIfAbsent(input.charAt(i), k -> new TreeSet<Integer>()).add(i);
}

System.out.println(store);

这使得: {a=[0], b=[1], c=[2, 4], n=[3]}

制作这个东西的成本可以作为“预处理”注销,但如果重要的话,O(nlogn)。

有了store ,您可以在O(log n)时间内完成这项工作。 例如,如果我想知道 3-5 范围内有多少个c ,我会要求 TreeSet 匹配 c (让我得到[2, 4]树集)。 然后我可以使用 treeset 的 headSet 和 tailSet 方法来计算它,它们都是 O(logn) 操作。

这给了我一个O(logn)的总运行时间,它接近于O(1)以至于它变得无关紧要(从某种意义上说,对现代计算机体系结构如何工作的实际关注将使它相形见绌)。 如果面试官不接受这个答案,那么他们要么是不必要的迂腐,要么是对现代计算机工作原理的严重误导,所以现在我们深入研究一个纯粹的学术练习,将其降低到O(1)

O(1)

我们没有将字符映射到 TreeSet,而是将其映射到int[] 这个 int 数组和整个输入一样大(所以在这种情况下,键 'a'、'b'、'c' 和 'n' 的 4 个 int[] 数组都是 5 大,因为输入是 5 大). 这个 int 数组回答了这个问题:如果你问我从这个位置到字符串末尾的答案,正确答案是什么? 因此,对于 c 它将是:[2, 2, 2, 1, 1]。 请注意,最后一个数字 (0) 丢失了,因为我们不需要它(从字符串末尾到字符串末尾的 X 的数量是.. 当然,0,总是,不管我们是什么字符谈论)。 如果字符串输入是 abcnca,则 int 数组有 6 个大,对于 c,将包含 [2, 2, 2, 1, 1, 0]。

有了这个数组,可以在O(1)时间内提供答案:它是“如果你让我从开始索引到字符串结尾我会给出的答案”,减去“如果我会给出的答案”你让我从结束索引到字符串结束'。 当然,考虑到如果问题的结束索引匹配字符串结束,只需在 int 数组中查找答案(不要减去任何内容)。

这意味着预处理后花费的时间为 O(1),但“预处理”数据结构的大小现在相当可观。 例如,如果输入字符串大 1 MB,包含 5000 个不同的字符,则它是一个 20GB 的表(每个数字 4 个字节,映射中有 5000 个条目,每个条目映射到一个包含一百万个条目的数组,每次弹出 4 个字节,是5000*1000000*4 = 20GB )。

之前没有人问过这个问题,因为在预处理字符串所花费的时间被分摊到许多 freq 操作之后,我特别询问了 O(1) 常数时间。

要为此方法实现摊销常数时间,您可以生成一个HashMap ,将每个字符与一个长度等于给定字符串长度的数组相关联,其中每个数组元素表示特定字符从字符串开头出现的次数直到特定索引的字符串。

freq()的第一次调用将填充Map并将在O(n)中运行,后续调用将在常数时间O(1)中执行。

因此,考虑N次操作调用总成本上限的所谓摊销时间复杂度将为O(1)

填充 Map 的算法类似于计数排序算法(我们需要执行这些步骤,由链接提供的伪代码中的两个第一个for循环表示)。

为了填充 Map,字符串中遇到的每个唯一字符都需要与具有给定字符串长度的数组int[]相关联。 在迭代过程中,数组中当前索引下与当前字符对应的元素应该递增(这基本上是计数排序的第一阶段)。

下一步是遍历 Map 的值并计算每个频率数组的累积频率,以便每个元素都代表某个字符从字符串的最开始到特定索引的出现总数(此步骤与计数排序中的第二阶段相同)。

这就是实现的样子:

public static final Map<Character, int[]> FREQ_BY_CHAR = new HashMap<>();

public static int freq(String s, char c, int start, int end) {
    
    if (FREQ_BY_CHAR.isEmpty()) populate(s);

    int[] frequencies = FREQ_BY_CHAR.get(c);
    
    return frequencies[end] - frequencies[start];
}

public static void populate(String s) {
    countFrequencies(s);
    calculateCumulativeFrequencies();
}

public static void countFrequencies(String s) {
    for (int i = 0; i < s.length(); i++) {
        char next = s.charAt(i);
        int[] frequencies = FREQ_BY_CHAR.computeIfAbsent(next, k -> new int[s.length()]);
        frequencies[i]++;
    }
}

public static void calculateCumulativeFrequencies() {
    FREQ_BY_CHAR.values().forEach(freq -> accumulate(freq));
}

public static void accumulate(int[] freq) {
    for (int i = 1; i < freq.length; i++) freq[i] += freq[i - 1];
}

main()

public static void main(String[] args) {
    String s = "abcnc";
    char find = 'c';
    int start = 1;
    int end = 4;

    System.out.println(freq(s, find, start, end));

    FREQ_BY_CHAR.forEach((ch, arr1) -> System.out.println(ch + " -> " + Arrays.toString(arr1))); 
}

输出:

2
// contents of the Map
//    a  b  c  n  c
a -> [1, 1, 1, 1, 1]
b -> [0, 1, 1, 1, 1]
c -> [0, 0, 1, 1, 2]
n -> [0, 0, 0, 1, 1] 

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM