Эффективный в пространстве алгоритм проверки, равны ли строки с пробелами?

Мне недавно задали этот вопрос в интервью:

Если заданы две строки s и t, они возвращаются, если они равны, когда обе они введены в пустые текстовые редакторы. # означает символ возврата на одну позицию.

Input: S = "ab#c", T = "ad#c"
Output: true
Explanation: Both S and T become "ac".

Я придумал следующее решение, но оно не экономит место:

  public static boolean sol(String s, String t) {
    return helper(s).equals(helper(t));
  }

  public static String helper(String s) {
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
      if (c != '#')
        stack.push(c);
      else if (!stack.empty())
        stack.pop();
    }
    return String.valueOf(stack);
  }

Я хотел посмотреть, есть ли лучший способ решить эту проблему, который не использует стек. Я имею в виду, можем ли мы решить это в O (1) пространстве сложности?

Примечание: у нас также может быть несколько символов возврата.

Ответ 1

Для достижения сложности пространства O(1), используйте Two Pointers и начинайте с конца строки:

public static boolean sol(String s, String t) {
    int i = s.length() - 1;
    int j = t.length() - 1;
    while (i >= 0 || j >= 0) {
        i = consume(s, i);
        j = consume(t, j);
        if (i >= 0 && j >= 0 && s.charAt(i) == t.charAt(j)) {
            i--;
            j--;
        } else {
            return i == -1 && j == -1;
        }
    }
    return true;
}

Основная идея заключается в том, чтобы сохранить # counter: increment cnt если символ #, в противном случае уменьшить его. И если cnt > 0 и s.charAt(pos) != '#' - пропустить символ (позиция уменьшения):

private static int consume(String s, int pos) {
    int cnt = 0;
    while (pos >= 0 && (s.charAt(pos) == '#' || cnt > 0)) {
        cnt += (s.charAt(pos) == '#') ? +1 : -1;
        pos--;
    }
    return pos;
}

Временная сложность: O(n).

Источник 1, Источник 2.

Ответ 2

Исправленный псевдокод templatetypedef

// Index of next spot to read from each string
let sIndex = s.length() - 1
let tIndex = t.length() - 1
let sSkip = 0
let tSkip = 0

while sIndex >= 0 and tIndex >= 0:
    if s[sIndex] = #:
        sIndex = sIndex - 1
        sSkip = sSkip + 1
        continue
    else if sSkip > 0
        sIndex = sIndex - 1
        sSkip = sSkip - 1
        continue

    // Do the same thing for t.
    if t[tIndex] = #:
        tIndex = tIndex - 1
        tSkip = tSkip + 1
        continue
    else if tSkip > 0
        tIndex = tIndex - 1
        tSkip = tSkip - 1
        continue

    // Compare characters.
    if s[sIndex] != t[tIndex], return false

    // Back up to the next character
    sIndex = sIndex - 1
    tIndex = tIndex - 1

// The strings match if weve exhausted all characters.
return sIndex < 0 and tIndex < 0

Ответ 3

Я полагаю, что вы можете сделать это в пространстве O (1), изменив способ итерации по строкам. Вместо того, чтобы идти вперед-назад, сканируйте сзади-вперед, сравнивая пары символов. Как исключение, если один из символов для сканирования является символом #, отсканируйте назад, считая пробелы и пропуская соответствующее количество символов. Это либо обнаружит несоответствие между строками, либо подтвердит, что они равны.

В псевдокоде:

// Index of next spot to read from each string
let sIndex = s.length() - 1
let tIndex = t.length() - 1

while sIndex >= 0 and tIndex >= 0:
    // Scan backwards in s and t to find the next actual character
    let sSkip = 0
    while s[sIndex] = #:
        sIndex = sIndex - 1
        sSkip = sSkip + 1
    sIndex = sIndex - sSkip

    // Do the same thing for t.
    let tSkip = 0
    while t[tIndex] = #:
        tIndex = tIndex - 1
        tSkip = tSkip + 1
    tIndex = tIndex - tSkip

    // Break the loop if we have nothing to compare
    if sIndex < 0 or tIndex < 0, break

    // Compare characters.
    if s[sIndex] != t[tIndex], return false

    // Back up to the next character
    sIndex = sIndex - 1
    tIndex = tIndex - 1

// The strings match if weve exhausted all characters.
return sIndex < 0 and tIndex < 0

Ответ 4

Вы можете сделать это с помощью простого цикла for. Рассмотрим следующий метод:

private static String cleanedString(String string, char bsChar) {

    char[] chars = string.toCharArray();
    StringBuilder output = new StringBuilder();

    int bsCount = 0;
    for (int i = 0; i < chars.length; i++) {

        char c = chars[i];

        // When encountering the bsChar, skip it and remove the preceding character.
        if (c == bsChar && i > 0) {
            bsCount++;

            // Is the following char also a bsChar? If so, add to our bsCount and skip
            // to next iteration of the loop
            if (chars[i + 1] == bsChar) {
                continue;
            }

            // Now that we know how many consecutive bsChars we have, delete that many
            // characters from the end of our output
            output.delete(output.length() - bsCount, output.length());

            // Reset the bsCount as all have been accounted for so far
            bsCount = 0;
        } else {
            // This is a valid character, add it to our output
            output.append(c);
        }
    }

    return output.toString();
}

Если вы должны были вызвать System.out.println(cleanedString(a, '#')); , выход будет ac.

Таким образом, вы могли бы использовать это, чтобы сначала получить очищенную строку, которая удаляет любые символы "backspaced". Тогда просто сравните два:

return cleanedString(a, '#').equals(cleanedString(b, '#'));

Ответ 5

Вы можете использовать регулярные выражения, чтобы заменить символы непосредственно перед "#", а затем сравнить результаты:

public static boolean sol(String s, String t) {
    return s.replaceAll(".(?=#)", "").equals(t.replaceAll(".(?=#)", ""));
}

Ответ 6

Давайте начнем с более простой задачи: удаление символов из String.

Предположим, что мы знаем, что символ 0 (NUL) не будет присутствовать во входной строке. (Или выберите другого персонажа...)

char NOT_USED = 0;
char[] chars = s.toCharArray();

# Pass 1: erase characters by setting them to NOT_USED
for (int i = 0; i < chars.length; i++) {
    if (chars[i] == '#') {
        chars[i] = NOT_USED;
        for (int j = i - 1; j >= 0; j--) {
            if (chars[j] != NOT_USED) {
                chars[j] = NOT_USED;
                break;
            }
        }
    }
}

# Pass 2: compaction
int pos = 0;
for (int i = 0; i < chars.length; i++) {
    if (chars[i] != NOT_USED) {
        chars[pos++] = chars[i];
    }
}

String result = new String(chars, 0, pos);

Эта версия O(N) во времени, и использует скромное количество дополнительного пространства: массив из N символов.

Существует потенциальная небольшая оптимизация, которая может быть сделана выше. Если вы посчитаете количество символов, "стертое" в первом проходе:

  • Вы можете пропустить 2-й проход и создание новой строки, если не были удалены символы, и
  • в противном случае 2-й проход может chars.length - nos_erased в chars.length - nos_erased

Существует более простой способ, чтобы стереть символы, используя StringBuilder и String.delete для удаления стертых символов, как они будут найдены. Однако производительность равна O(max(1, N) * E) где N - длина строки, а E - количество стертых символов.

Математически невозможно сделать это со сложностью, которая лучше, чем O(N). Каждый символ в строке должен быть проверен, чтобы увидеть, является ли он символом # или нет.

Также невозможно использовать временное хранилище в Java меньше чем O(N) потому что класс String является неизменным.

Может быть, есть возможность использовать рекурсию (!), Чтобы скрыть хранилище, используемое от учета кучи, но это дым и зеркала. На самом деле вы будете использовать больше памяти в кадрах стека, чем при использовании явного временного массива.


Теперь к актуальной проблеме: тестирование, если две последовательности символов будут равны после стирания.

Прямое решение этого заключается в том, чтобы фактически выполнить стирание (см. Выше) обеих строк, а затем сравнить их. Это будет O(max(N1, N2)) во времени и дополнительном пространстве (где N1 и N2 - длины сравниваемых строк).

Можем ли мы применить рекурсию, чтобы получить решение, которое быстрее или использует меньше памяти?

Непонятно... но, вероятно, нет.

Если бы это были простые строки (без стирания), вы бы использовали логику "короткого замыкания", чтобы прекратить сравнение, как только увидите символы, которые не совпадают. Здесь сложнее. Проблема в том, что вы не знаете, каким будет i й символ в стертой строке, пока у вас не будет много символов после позиции i в исходной строке.

(Подумайте об этом по-другому: когда вы доберетесь до позиции p в строке длиной n, может быть до n - p удалите символы, следующие за текущим символом, поэтому соответствующее количество позиций символов перед p все еще не определено. Если я выполнив математику правильно, вы можете быть уверены в действительном значении i го символа, только когда i < 2*p - n. Другими словами, прежде чем вы сможете проверить, по крайней мере, половину символов обеих строк обнаружить любые возможные различия.)