Вопрос о равенстве строк в Scala из интервью по программированию - PullRequest
0 голосов
/ 09 сентября 2018

Поскольку мне нравилось программировать на Scala, для моего интервью в Google я попросил их задать мне вопрос о стиле Scala / функциональном программировании. Вопрос о функциональном стиле Scala, который я получил, был следующим:

У вас есть две строки, состоящие из буквенных символов, а также специальный символ, представляющий символ возврата на одну позицию. Давайте назовем этот символ возврата / «. Когда вы попадаете на клавиатуру, вы набираете эту последовательность символов, включая символ возврата / удаления. Решение, которое вы должны реализовать, должно проверить, дают ли две последовательности символов одинаковый вывод. Например, «abc», «aa / bc». «abb / c», «abcc /», «/ abc» и «// abc» выдают одинаковый вывод «abc». Поскольку это вопрос Scala / функционального программирования, вы должны реализовать свое решение в идиоматическом стиле Scala.

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

def processString(string: String): List[Char] = {
  string.foldLeft(List[Char]()){ case(accumulator: List[Char], char: Char) =>
    accumulator match {
      case head :: tail => if(char != '/') { char :: head :: tail } else { tail }
      case emptyList => if(char != '/') { char :: emptyList } else { emptyList }
    }
  }
}

def solution(string1: String, string2: String): Boolean = {
  processString(string1) == processString(string2)
}

Пока все хорошо? Затем он спросил о сложности времени, и я ответил линейное время (потому что вы должны обработать каждый символ один раз) и линейное пространство (потому что вы должны скопировать каждый элемент в список). Затем он попросил меня сделать это за линейное время, но с постоянным пространством. Я не мог придумать способ сделать это, который был бы чисто функциональным. Он сказал, чтобы попытаться использовать функцию из библиотеки коллекций Scala, такую ​​как «zip» или «map» (я точно помню, как он произнес слово «zip»).

Вот в чем дело. Я думаю, что физически невозможно сделать это в постоянном пространстве без каких-либо изменчивых состояний или побочных эффектов. Как будто я думаю, что он испортил вопрос. Как вы думаете?

Можете ли вы решить это за линейное время, но с постоянным пространством?

Ответы [ 4 ]

0 голосов
/ 10 сентября 2018

Вот версия с единственной рекурсивной функцией и без дополнительных классов или библиотек. Это линейное время и постоянная память.

def compare(a: String, b: String): Boolean = {
  @tailrec
  def loop(aIndex: Int, aDeletes: Int, bIndex: Int, bDeletes: Int): Boolean = {
    val aVal = if (aIndex < 0) None else Some(a(aIndex))
    val bVal = if (bIndex < 0) None else Some(b(bIndex))

    if (aVal.contains('/')) {
      loop(aIndex - 1, aDeletes + 1, bIndex, bDeletes)
    } else if (aDeletes > 0) {
      loop(aIndex - 1, aDeletes - 1, bIndex, bDeletes)
    } else if (bVal.contains('/')) {
      loop(aIndex, 0, bIndex - 1, bDeletes + 1)
    } else if (bDeletes > 0) {
      loop(aIndex, 0, bIndex - 1, bDeletes - 1)
    } else {
      aVal == bVal && (aVal.isEmpty || loop(aIndex - 1, 0, bIndex - 1, 0))
    }
  }

  loop(a.length - 1, 0, b.length - 1, 0)
}
0 голосов
/ 09 сентября 2018

Этот код занимает O (N) времени и требует только три целых числа дополнительного пробела:

def solution(a: String, b: String): Boolean = {

  def findNext(str: String, pos: Int): Int = {
    @annotation.tailrec
    def rec(pos: Int, backspaces: Int): Int = {
      if (pos == 0) -1
      else {
        val c = str(pos - 1)
        if (c == '/') rec(pos - 1, backspaces + 1)
        else if (backspaces > 0) rec(pos - 1, backspaces - 1)
        else pos - 1
      }
    }
    rec(pos, 0)
  }

  @annotation.tailrec 
  def rec(aPos: Int, bPos: Int): Boolean = {
    val ap = findNext(a, aPos)
    val bp = findNext(b, bPos)
    (ap < 0 && bp < 0) ||
    (ap >= 0 && bp >= 0 && (a(ap) == b(bp)) && rec(ap, bp))
  }

  rec(a.size, b.size)
}

Проблема может быть решена за линейное время с постоянным дополнительным пространством: если вы сканируете справа налево, вы можете быть уверены, что символы / слева от текущей позиции не могут влиять на уже обработанные символы ( направо от текущей позиции), так что нет необходимости хранить их. В любой момент вам нужно знать только две вещи:

  1. Где ты в строке?
  2. Сколько символов вы должны выбросить из-за пробелов

Это составляет два целых числа для хранения позиций и одно дополнительное целое число для временного хранения количества накопленных возвратов во время вызова findNext. Это всего три целых числа пространства над головой.

Интуиция

Вот моя попытка сформулировать, почему сканирование справа налево дает вам алгоритм O (1):

Будущее не может влиять на прошлое, поэтому нет необходимости вспоминать будущее.

«Естественное время» в этой проблеме течет слева направо. Поэтому, если вы сканируете справа налево, вы двигаетесь «из будущего в прошлое», и поэтому вам не нужно запоминать символы справа от вашей текущей позиции.

Тесты

Вот рандомизированный тест, который дает мне уверенность, что решение на самом деле правильное:

val rng = new util.Random(0)
def insertBackspaces(s: String): String = {
  val n = s.size
  val insPos = rng.nextInt(n)
  val (pref, suff) = s.splitAt(insPos)
  val c = ('a' + rng.nextInt(26)).toChar
  pref + c + "/" + suff
}

def prependBackspaces(s: String): String = {
  "/" * rng.nextInt(4) + s
}

def addBackspaces(s: String): String = {
  var res = s
  for (i <- 0 until 8) 
    res = insertBackspaces(res)
  prependBackspaces(res)
}

for (i <- 1 until 1000) {
  val s = "hello, world"
  val t = "another string"

  val s1 = addBackspaces(s)
  val s2 = addBackspaces(s)
  val t1 = addBackspaces(t)
  val t2 = addBackspaces(t)

  assert(solution(s1, s2))
  assert(solution(t1, t2))
  assert(!solution(s1, t1))
  assert(!solution(s1, t2))
  assert(!solution(s2, t1))
  assert(!solution(s2, t2))

  if (i % 100 == 0) {
    println(s"Examples:\n$s1\n$s2\n$t1\n$t2")
  }
}

Несколько примеров, которые генерирует тест:

Examples:
/helly/t/oj/m/, wd/oi/g/x/rld
///e/helx/lc/rg//f/o, wosq//rld
/anotl/p/hhm//ere/t/ strih/nc/g
anotx/hb/er sw/p/tw/l/rip/j/ng
Examples:
//o/a/hellom/, i/wh/oe/q/b/rld
///hpj//est//ldb//y/lok/, world
///q/gd/h//anothi/k/eq/rk/ string
///ac/notherli// stri/ig//ina/n/g
Examples:
//hnn//ello, t/wl/oxnh///o/rld
//helfo//u/le/o, wna//ova//rld
//anolq/l//twl//her n/strinhx//g
/anol/tj/hq/er swi//trrq//d/ing
Examples:
//hy/epe//lx/lo, wr/v/t/orlc/d
f/hk/elv/jj//lz/o,wr// world
/anoto/ho/mfh///eg/r strinbm//g
///ap/b/notk/l/her sm/tq/w/rio/ng
Examples:
///hsm/y//eu/llof/n/, worlq/j/d
///gx//helf/i/lo, wt/g/orn/lq/d
///az/e/notm/hkh//er sm/tb/rio/ng
//b/aen//nother v/sthg/m//riv/ng

Кажется, работает просто отлично. Так что, я бы сказал, что парень из Google не испортил, похоже на совершенно правильный вопрос.

0 голосов
/ 10 сентября 2018

Если целью является минимальное использование памяти, с итераторами трудно поспорить.

def areSame(a :String, b :String) :Boolean = {
  def getNext(ci :Iterator[Char], ignore :Int = 0) : Option[Char] =
    if (ci.hasNext) {
      val c = ci.next()
      if (c == '/')        getNext(ci, ignore+1)
      else if (ignore > 0) getNext(ci, ignore-1)
      else                 Some(c)
    } else None

  val ari = a.reverseIterator
  val bri = b.reverseIterator
  1 to a.length.max(b.length) forall(_ => getNext(ari) == getNext(bri))
}

С другой стороны, при аргументации принципов FP трудно защищать итераторы, так как они все о поддержании состояния.

0 голосов
/ 09 сентября 2018

Вам не нужно создавать вывод, чтобы найти ответ. Вы можете повторять две последовательности одновременно и останавливаться на первой разнице. Если вы не найдете никакой разницы, и обе последовательности завершаются одновременно, они равны, в противном случае они разные.

Но теперь рассмотрим такие последовательности: aaaa/// для сравнения с a. Вам нужно использовать 6 элементов из левой последовательности и один элемент из правой последовательности, прежде чем вы сможете утверждать, что они равны. Это означает, что вам нужно будет хранить как минимум 5 элементов в памяти, пока вы не убедитесь, что все они удалены. Но что, если вы перебрали элементы с конца? Затем вам нужно будет просто подсчитать количество возвратов, а затем просто проигнорировать столько элементов, сколько необходимо в левой последовательности, без необходимости хранить их в памяти, поскольку вы знаете, что они не будут присутствовать в конечном выводе. Используя эти два совета, вы можете получить O(1) памяти.

Я пробовал, и, кажется, работает:

def areEqual(s1: String, s2: String) = {
    def charAt(s: String, index: Int) = if (index < 0) '#' else s(index)

    @tailrec
    def recSol(i1: Int, backspaces1: Int, i2: Int, backspaces2: Int): Boolean = (charAt(s1, i1), charAt(s2, i2)) match {
        case ('/',  _) => recSol(i1 - 1, backspaces1 + 1, i2, backspaces2)
        case (_,  '/') => recSol(i1, backspaces1, i2 - 1, backspaces2 + 1)
        case ('#' , '#') => true
        case (ch1, ch2)  => 
            if      (backspaces1 > 0) recSol(i1 - 1, backspaces1 - 1, i2    , backspaces2    )
            else if (backspaces2 > 0) recSol(i1    , backspaces1    , i2 - 1, backspaces2 - 1)
            else        ch1 == ch2 && recSol(i1 - 1, backspaces1    , i2 - 1, backspaces2    )
    }
    recSol(s1.length - 1, 0, s2.length - 1, 0)
}

Некоторые тесты (все пройдено, дайте мне знать, если у вас есть несколько крайних случаев):

// examples from the question
val inputs = Array("abc", "aa/bc", "abb/c", "abcc/", "/abc", "//abc")
for (i <- 0 until inputs.length; j <- 0 until inputs.length) {
    assert(areEqual(inputs(i), inputs(j)))
}

// more deletions than required
assert(areEqual("a///////b/c/d/e/b/b", "b")) 
assert(areEqual("aa/a/a//a//a///b", "b"))
assert(areEqual("a/aa///a/b", "b"))

// not enough deletions
assert(!areEqual("aa/a/a//a//ab", "b")) 

// too many deletions
assert(!areEqual("a", "a/"))

PS: всего несколько заметок о самом коде:

  • Вывод типа Scala достаточно хорош, так что вы можете отбрасывать типы в функции part внутри вашей foldLeft
  • Nil - идиоматический способ ссылки на случай пустого списка

Бонус:

Я имел в виду что-то вроде решения Тима перед тем, как реализовать мою идею, но я рано начал с сопоставления с образцом только для символов, и это не подходило, потому что в некоторых случаях требуется количество обратных пробелов. В конце концов, я думаю, что более точный способ написать это - сочетание соответствия шаблонов и условий. Ниже мое более длинное оригинальное решение, которое я дал выше, было переделано laater:

def areEqual(s1: String, s2: String) = {
    @tailrec
    def recSol(c1: Cursor, c2: Cursor): Boolean = (c1.char, c2.char) match {
        case ('/',  '/') => recSol(c1.next, c2.next)
        case ('/' ,   _) => recSol(c1.next, c2     )
        case (_   , '/') => recSol(c1     , c2.next)
        case ('#' , '#') => true
        case (a   ,   b) if (a == b) => recSol(c1.next, c2.next)
        case _           => false
    }
    recSol(Cursor(s1, s1.length - 1), Cursor(s2, s2.length - 1))
}

private case class Cursor(s: String, index: Int) {
    val char = if (index < 0) '#' else s(index)
    def next = {
      @tailrec
      def recSol(index: Int, backspaces: Int): Cursor = {
          if      (index < 0      ) Cursor(s, index)
          else if (s(index) == '/') recSol(index - 1, backspaces + 1)
          else if (backspaces  > 1) recSol(index - 1, backspaces - 1)
          else                      Cursor(s, index - 1)
      }
      recSol(index, 0)
    }
}
...