由于我喜欢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”一词)。
这是东西。我认为,在没有任何可变状态或副作用的情况下,在恒定的空间中进行此操作实际上是不可能的。就像我认为他搞砸了这个问题。你怎么看?
您可以在线性时间内但空间不变的情况下求解它吗?
答案 0 :(得分:6)
此代码需要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)
}
可以在线性时间内以恒定的额外空间解决问题:如果从右向左扫描,则可以确保当前位置左侧的/
符号不会影响已经处理过的位置符号(位于当前位置的右侧),因此无需存储它们。
在每一点上,您只需要知道两件事:
这使得两个整数用于存储位置,另外一个整数用于临时存储在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-guy并没有搞砸,看起来是一个非常有效的问题。
答案 1 :(得分:5)
您无需创建输出即可找到答案。您可以同时迭代两个序列,并在第一个差异处停止。如果您发现没有差异,并且两个序列都同时终止,则它们是相等的,否则它们是不同的。
但是现在考虑这样的序列: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:关于代码本身的几点说明:
foldLeft
内的部分函数中删除类型Nil
是引用空列表案例的惯用方式奖金:
在实现我的想法之前,我曾想到过Tim的解决方法,但我很早就开始对字符进行模式匹配,由于某些情况需要退格键,所以它不太适合。最后,我认为一种更整洁的编写方式是模式匹配和if条件的混合。以下是我较长的原始解决方案,上面给出的是重构的:
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)
}
}
答案 2 :(得分:4)
如果目标是最小化内存占用,则很难反对迭代器。
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主体时,很难捍卫迭代器,因为它们都是关于维护状态的。
答案 3 :(得分:2)
这里是具有单个递归函数且没有其他类或库的版本。这是线性时间和恒定的内存。
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)
}