查找表达式在字符串中连续和非连续出现的次数

Finding the Number of Times an Expression Occurs in a String Continuously and Non Continuously

我在 phone 上进行了编码面试,并被问到这个问题:

Given a String (for example):

"aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc"

和一个表达式(例如):

"a+b+c-"

其中:

+: means the char before it is repeated 2 times

-: means the char before it is repeated 4 times

求给定表达式在字符串中出现的操作数非连续和连续出现的次数。

以上表达式出现4次:

1) aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc
        ^^       ^^       ^^^^                    
        aa       bb       cccc
2) aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc
        ^^       ^^                               ^^^^
        aa       bb                               cccc

3) aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc
        ^^                                ^^      ^^^^
        aa                                bb      cccc

4) aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc
                                       ^^ ^^      ^^^^
                                       aa bb      cccc

我不知道该怎么做。我开始使用带有大量索引标记的迭代蛮力方法,但意识到在中途编写代码会有多么混乱和困难:

import java.util.*;

public class Main {

    public static int count(String expression, String input) {
        int count = 0;
        ArrayList<char[]> list = new ArrayList<char[]>();

        // Create an ArrayList of chars to iterate through the expression and match to string
        for(int i = 1; i<expression.length(); i=i+2) {
            StringBuilder exp = new StringBuilder();
            char curr = expression.charAt(i-1);
            if(expression.charAt(i) == '+') {
                exp.append(curr).append(curr);
                list.add(exp.toString().toCharArray());
            }
            else { // character is '-'
                exp.append(curr).append(curr).append(curr).append(curr);
                list.add(exp.toString().toCharArray());
            }
        }

        char[] inputArray = input.toCharArray();
        int i = 0; // outside pointer
        int j = 0; // inside pointer
        while(i <= inputArray.length) {
            while(j <= inputArray.length) {
                for(int k = 0; k< list.size(); k++) {
                    /* loop through 
                     * all possible combinations in array list
                     * with multiple loops
                     */
                }
                j++;
            }
            i++;
            j=i;
        }
        return count;
    }

    public static void main(String[] args) {
        String expression = "a+b+c-";
        String input = "aaksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc";
        System.out.println("The expression occurs: "+count(expression, input)+" times");
    }
}

在花了很多时间反复地做之后,他提到了递归,我仍然看不出递归的明确方法,我无法解决这个问题。我现在正在尝试解决它 post-interview 但我仍然不确定如何解决这个问题。我应该如何解决这个问题?解决方案是否显而易见?我认为这对于编码 phone 面试来说是一个非常难的问题。

递归可能如下(伪代码):

int search(String s, String expression) {

     if expression consists of only one token t /* e. g. "a+" */ {
         search for t in s
         return number of occurrences
     } else {
         int result = 0
         divide expression into first token t and rest expression
         // e. g. "a+a+b-" -> t = "a+", rest = "a+b-"
         search for t in s
         for each occurrence {
             s1 = substring of s from the position of occurrence to the end
             result += search(s1, rest) // search for rest of expression in rest of string
         }
         return result
     }
}   

将此应用于整个字符串,您将获得非连续出现的次数。要获得连续出现,您根本不需要递归——只需将表达式转换为字符串并通过迭代搜索。

如何预处理 aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc?

这变成a1k1s1d1b1a2l1a1s1k1d1h1f1b2l1a1j1d1f1h1a1c4a1o1u1d1g1a1l1s1a2b2l1i1s1d1f1h1c4

现在查找出现的 a2、b2、c4。

尝试了下面的代码,但现在它只给出了基于深度优先的第一个可能的匹配。

需要进行更改以进行所有可能的组合,而不是首先进行

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

public class Parsing {
    public static void main(String[] args) {
        String input = "aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc";
        System.out.println(input);

        for (int i = 0; i < input.length(); i++) {
            System.out.print(i/10);
        }
        System.out.println();

        for (int i = 0; i < input.length(); i++) {
            System.out.print(i%10);
        }
        System.out.println();

        List<String> tokenisedSearch = parseExp("a+b+c-");
        System.out.println(tokenisedSearch);

        parse(input, 0, tokenisedSearch, 0);
    }

    public static boolean parse(String input, int searchFromIndex, List<String> tokensToSeach, int currentTokenIndex) {
        if(currentTokenIndex >= tokensToSeach.size())
            return true;
        String token = tokensToSeach.get(currentTokenIndex);
        int found = input.indexOf(token, searchFromIndex);
        if(found >= 0) {
            System.out.println("Found at Index "+found+ " Token " +token);
            return parse(input, searchFromIndex+1, tokensToSeach, currentTokenIndex+1);
        }
        return false;
    }

    public static List<String> parseExp(String exp) {
        List<String> list = new ArrayList<String>();
        String runningToken = "";
        for (int i = 0; i < exp.length(); i++) {
            char at = exp.charAt(i);
            switch (at) { 
            case '+' :
                runningToken += runningToken;
                list.add(runningToken);
                runningToken = "";
                break;
            case '-' :
                runningToken += runningToken;
                runningToken += runningToken;
                list.add(runningToken);
                runningToken = "";
                break;
            default :
                runningToken += at;
            }
        }
        return list;
    }
}

我们称字符串的长度为n,查询表达式的长度(根据"units"的个数,如a+b-)m.

不清楚你所说的"continuously"和"non-continuously"是什么意思,但是如果"continuously"意味着查询字符串单元之间不能有任何间隙,那么你可以使用 KMP algorithm 在 O(m+n) 时间内找到所有实例。

我们可以在 O(nm) 时间内求解 "non-continuous" 版本,用 dynamic programming 求解 space。基本上,我们要计算的是一个函数:

f(i, j) = the number of occurrences of the subquery consisting of the first i units
          of the query expression, in the first j characters of the string.

因此对于您的示例,f(2, 41) = 2,因为在您的示例字符串的前 41 个字符中有 2 个单独出现的子模式 a+b+

最终答案将是 f(n, m)。

我们可以递归计算如下:

f(0, j) = 0
f(i, 0) = 0
f(i > 0, j > 0) = f(i, j-1) + isMatch(i, j) * f(i-1, j-len(i))

其中 len(i) 是表达式中第 i 个单元的长度(始终为 2 或 4),isMatch(i, j) 是一个函数 returns 1 如果表达式中的第 i 个单元匹配位置 j 处的文本 ending,否则为 0。例如,在您的示例中 isMatch(15, 2) = 1,因为 s[14..15] = bb。这个函数只需要常数时间 运行,因为它永远不需要检查超过 4 个字符。

上面的递归已经按原样工作了,但是我们可以通过确保只解决每个子问题一次来节省时间。因为函数 f() 只依赖于它的 2 个参数 i 和 j,它们的范围分别在 0 和 m 之间,以及 0 和 n 之间,我们可以计算所有 n*m 可能的答案并将它们存储在 table.

[编辑:正如 Sasha Salauyou 指出的那样,space 要求实际上可以减少到 O(m)。我们永远不需要访问 k < j-1 的 f(i, k) 的值,所以不用在 table 中存储 m 列,我们可以只存储 2,并通过始终访问列 [=19] 在它们之间交替=].]

想亲自尝试一下,并认为我也可以分享我的解决方案。当表达式中确实存在 char 0 时,parse 方法显然存在问题(尽管这本身可能是更大的问题),find 方法将因空 [=15] 而失败=] 数组,我不确定 ab+c- 是否应该被视为有效模式(我这样对待它)。请注意,这仅涵盖到目前为止的非连续部分。

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

public class Matcher {

  public static void main(String[] args) {
    String haystack = "aksdbaalaskdhfbblajdfhacccc aoudgalsaa bblisdfhcccc";
    String[] needles = parse("a+b+c-");
    System.out.println("Needles: " + Arrays.toString(needles));
    System.out.println("Found: " + find(haystack, needles, 0));
    needles = parse("ab+c-");
    System.out.println("Needles: " + Arrays.toString(needles));
    System.out.println("Found: " + find(haystack, needles, 0));
  }

  private static int find(String haystack, String[] needles, int i) {
    String currentNeedle = needles[i];
    int pos = haystack.indexOf(currentNeedle);
    if (pos < 0) {
      // Abort: Current needle not found
      return 0;
    }
    // Current needle found (also means that pos + currentNeedle.length() will always
    // be <= haystack.length()
    String remainingHaystack = haystack.substring(pos + currentNeedle.length());
    // Last needle?
    if (i == needles.length - 1) {
      // +1: We found one match for all needles
      // Try to find more matches of current needle in remaining haystack
      return 1 + find(remainingHaystack, needles, i);
    }
    // Try to find more matches of current needle in remaining haystack
    // Try to find next needle in remaining haystack
    return find(remainingHaystack, needles, i) + find(remainingHaystack, needles, i + 1);
  }

  private static String[] parse(String expression) {
    List<String> searchTokens = new ArrayList<String>();
    char lastChar = 0;
    for (int i = 0; i < expression.length(); i++) {
      char c = expression.charAt(i);
      char[] chars;
      switch (c) {
        case '+':
          // last char is repeated 2 times
          chars = new char[2];
          Arrays.fill(chars, lastChar);
          searchTokens.add(String.valueOf(chars));
          lastChar = 0;
          break;
        case '-':
          // last char is repeated 4 times
          chars = new char[4];
          Arrays.fill(chars, lastChar);
          searchTokens.add(String.valueOf(chars));
          lastChar = 0;
          break;
        default:
          if (lastChar != 0) {
            searchTokens.add(String.valueOf(lastChar));
          }
          lastChar = c;
      }
    }
    return searchTokens.toArray(new String[searchTokens.size()]);
  }
}

输出:

Needles: [aa, bb, cccc]
Found: 4
Needles: [a, bb, cccc]
Found: 18

非递归算法,需要 O(m) space 并在 O(n*m) 中运行,其中 m 是查询中的标记数:

@Test
public void subequences() {

    String input = "aabbccaacccccbbd";
    String query = "a+b+";

    // here to store tokens of a query: e.g. {a, +}, {b, +}
    char[][] q = new char[query.length() / 2][];

    // here to store counts of subsequences ending by j-th token found so far
    int[] c =  new int[query.length() / 2];   // main
    int[] cc = new int[query.length() / 2];   // aux        

    // tokenize
    for (int i = 0; i < query.length(); i += 2)
        q[i / 2] = new char[] {query.charAt(i), query.charAt(i + 1)};

    // init
    char[] sub2 = {0, 0};        // accumulator capturing last 2 chars
    char[] sub4 = {0, 0, 0, 0};  // accumulator capturing last 4 chars

    // main loop
    for (int i = 0; i < input.length(); i++) {

        shift(sub2, input.charAt(i));
        shift(sub4, input.charAt(i));

        boolean all2 = sub2[1] != 0 && sub2[0] == sub2[1];  // true if all sub2 chars are same
        boolean all4 = sub4[3] != 0 && sub4[0] == sub4[1]   // true if all sub4 chars are same
              && sub4[0] == sub4[2] && sub4[0] == sub4[3];

        // iterate tokens
        for (int j = 0; j < c.length; j++) {

            if (all2 && q[j][1] == '+' && q[j][0] == sub2[0]) // found match for "+" token
                cc[j] = j == 0             // filling up aux array
                      ? c[j] + 1           // first token, increment counter by 1
                      : c[j] + c[j - 1];   // add value of preceding token counter

            if (all4 && q[j][1] == '-' && q[j][0] == sub4[0]) // found match for "-" token
                cc[j] = j == 0 
                      ? c[j] + 1 
                      : c[j] + c[j - 1];
        }
        if (all2) sub2[1] = 0;  // clear, to make "aa" occur in "aaaa" 2, not 3 times
        if (all4) sub4[3] = 0;
        copy(cc, c);            // copy aux array to main 
        }
    }
    System.out.println(c[c.length - 1]);
}


// shifts array 1 char left and puts c at the end
void shift(char[] cc, char c) {
    for (int i = 1; i < cc.length; i++)
        cc[i - 1] = cc[i];
    cc[cc.length - 1] = c;
}

// copies array contents 
void copy(int[] from, int[] to) {
    for (int i = 0; i < from.length; i++)
        to[i] = from[i];
}

主要思想是从输入中一个一个地捕获字符,将它们保存在 2 字符和 4 字符的累加器中,并检查它们是否与查询的某些标记匹配,记住我们得到了多少匹配项到目前为止以这些标记结尾的子查询。

查询 (a+b+c-) 被拆分为标记 (a+b+c-)。然后我们在累加器中收集字符并检查它们是否匹配某些标记。如果我们找到第一个标记的匹配项,我们将其计数器加 1。如果我们找到另一个 j-th 标记 的匹配项,我们可以创建与 子查询匹配的其他子序列由标记 [0...j] 组成,因为现在存在许多标记 对于由标记 [0...j-1] 组成的子查询,因为此匹配可以附加到它们中的每一个。

例如,我们有:

a+ : 3  (3 matches for a+)
b+ : 2  (2 matches for a+b+)
c- : 1  (1 match for a+b+c-) 

cccc 到达时。然后 c- 计数器应该增加 b+ 计数器值,因为到目前为止我们有 2 个 a+b+ 子序列并且 cccc 可以附加到它们。

如果您首先使用简单的 parser/compiler 转换搜索字符串,因此 a+ 变为 aa 等,那么您可以简单地使用此字符串和 运行 一个正则表达式与你的干草堆相匹配。 (抱歉,我不是 Java 程序员,所以无法提供任何真正的代码,但这并不难)