-1

Can anyone please help me to understand the time and space complexity of algo to balance parenthesis

def isValid(s: String): Boolean = {
@annotation.tailrec
def go(i: Int, stack: List[Char]): Boolean = {
  if (i >= s.length) {
    stack.isEmpty
  } else {
    s.charAt(i) match {
      case c @ ('(' | '[' | '{')                     => go(i + 1, c +: stack)
      case ')' if stack.isEmpty || stack.head != '(' => false
      case ']' if stack.isEmpty || stack.head != '[' => false
      case '}' if stack.isEmpty || stack.head != '{' => false
      case _                                         => go(i + 1, stack.tail)
    }
  }
}
go(0, Nil)

}

As per my undertanding, tail recursion reduces space to 0(1) complexity but here I am using additional data structure of List as accumulator, can anyone please explain how the space complexity and time complexity can be calculated

coder25
  • 2,363
  • 12
  • 57
  • 104

2 Answers2

1

There is a bug in your code: you are pushing only parentheses on stack, but pop everything, so this implementation only works for strings that only contain parentheses ... not sure if that was the intent. With the proper implementation, it should be liner in time, and the space complexity would be linear too, but not on the length of the entire string, only on the number of parentheses it contains.

    val oc = "([{" zip ")]}"
    object Open {  def unapply(c: Char) = oc.collectFirst { case (`c`, r) => r }}
    object Close { def unapply(c: Char) = oc.collectFirst { case (_, `c`) => c }}
    object ## { def unapply(s: String) = s.headOption.map { _ -> s.tail }}


    def go(s: String, stack: List[Char] = Nil): Boolean = (s, stack) match {
       case ("", Nil) => true
       case ("", _) => false
       case (Open(r) ## tail, st) => go(tail, r :: st)
       case (Close(r) ## tail, c :: st) if c == r => go(tail, st)
       case (Close(_) ## _, _) => false
       case (_ ## tail, st) => go(tail, st)
     }
    
     go(s)

(to be fair, this is actually linear in space because of s.toList :) The esthete inside me couldn't resist. You can turn it back to s.charAt(i) if you'd like, it just wouldn't look as pretty anymore ... or use s.head and `s.

Dima
  • 39,570
  • 6
  • 44
  • 70
0

I don't think that there would be any advantage in the order that would make a difference in time and space complexity when you implement an algo as a tail recursive function as opposed to non tail recursive function or with loops, otherwise everyone would be doing it. Tail recursion just prevents you from going into deeply nested recursive calls that would lead to stack overflows.

Your current algos's time complexity should O(n) and auxiliary space complexity should be O(n), tail recursive or not.

You could still reduce the auxiliary space complexity to O(1) with counters instead of a stack of parentheses, but that has nothing to do with tail recursion. O(1) auxiliary space complexity is only possible if you are only dealing with 1 type of parentheses where you track with a counter instead of a stack. However, non tail-call optimized recursion might still be bounded to O(n) if you consider the stack frame size.

Aside from the bugs mention by @Dima, if I were to refactor your solution I would go with:

def isValid(s: String): Boolean = {
  @annotation.tailrec
  def go(l: List[Char], stack: List[Char] = Nil): Boolean = (l, stack) match {
    case ((c @ ('(' | '[' | '{')) :: cs, xs)  =>  go(cs, c :: xs)
    case (')' :: cs, '(' :: xs)               =>  go(cs, xs)
    case (']' :: cs, '[' :: xs)               =>  go(cs, xs)
    case ('}' :: cs, '{' :: xs)               =>  go(cs, xs)
    case ((')' | ']' | '}') :: _, _)          =>  false
    case (_ :: cs, xs)                        =>  go(cs, xs)
    case (Nil, xs)                            =>  xs.isEmpty
  }

  go(s.toList)
}
yangzai
  • 962
  • 5
  • 11
  • non-tail recursion is always at least linear in space because of the stack. – Dima Apr 03 '21 at 17:30
  • @Dima I will add that in but I'm still not sure if we should be counting the stack frames. – yangzai Apr 03 '21 at 17:46
  • Why not? Stack frames _are_ space ... and you need `N` of them ... I can't think of a reason why they shouldn't be counted – Dima Apr 03 '21 at 18:29
  • Yeah, I guess, but now the analysis will be dependent on the language and optimisations. – yangzai Apr 03 '21 at 18:32
  • it kinda always is. Replace `c +: stack` with `stack :+ c`, and it becomes quadratic (in time) :) Or, notice how OP wrote `c +: stack` ? It looks like it is ok in 2.13, but I think, in 2.11 (and maybe 2.12), this would also be quadratic because `+:` wasn't overridden for `Lists`, you had to use `::` to do prepend in constant time – Dima Apr 03 '21 at 18:50