代码之家  ›  专栏  ›  技术社区  ›  Michael Lafayette

编程访谈中的Scala字符串相等问题

  •  18
  • Michael Lafayette  · 技术社区  · 6 年前

    因为我喜欢用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 collections库中的一个函数,比如“zip”或“map”(我清楚地记得他说的是“zip”)。

    事情是这样的。我认为在物理上不可能在恒定的空间里,没有任何可变的状态或副作用。我觉得他把问题搞砸了。你怎么认为?

    你能用线性时间,但用常数空间来解吗?

    4 回复  |  直到 6 年前
        1
  •  6
  •   Andrey Tyukin    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)
    }
    

    / -当前位置左侧的符号不能以任何方式影响已处理的符号(当前位置右侧),因此不需要存储它们。 在每一点上,你只需要知道两件事:

    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
    

        2
  •  5
  •   Dici gla3dr    6 年前

    不必创建输出就可以找到答案。您可以同时迭代这两个序列,并在第一个差异处停止。如果没有发现差异,并且两个序列同时终止,那么它们是相等的,否则它们是不同的。

    但现在考虑一下这样的序列: aaaa/// 比较 a 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/"))
    

    注:代码本身有几点需要注意:

    • Scala类型推断非常好,因此您可以在 foldLeft
    • Nil

    奖金:

    在实现我的想法之前,我已经想到了Tim的soltion,但是我很早就开始只对字符进行模式匹配,而且它不太适合,因为有些情况需要返回空间的数量。最后,我认为一种更简洁的方法是模式匹配和if条件的混合。下面是我更长的原始解决方案,上面我给出的是经过重构的Later:

    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)
        }
    }
    
        3
  •  4
  •   jwvh    6 年前

    如果目标是最小化内存占用,那么很难反对迭代器。

    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主体时,很难为迭代器辩护,因为它们都是关于维护状态的。

        4
  •  2
  •   Tim    6 年前

    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)
    }