Recursion

CS-A1120 Programming 2

Lukas Ahrenberg

Department of Computer Science
Aalto University

After this round, you

  • have reviewed recursion
    • can define a recursive function
    • can utilise inner functions in your programming
  • are aware of stack overflow errors
  • can explain tail recursion, and
    • write tail recursive functions
    • show recursive calls by call stacks and expansion
  • can define recursive data structures, such as
    • the list
    • the tree
  • can make use of recursive problem solving, in particular
    • backtracking search

Recursion recalled

  • "Defines something in terms of itself"
    • Recursion: see recursion
    • GNU stands for "GNU's Not Unix"
    • \(f(x) = x + f\left(\frac{x}{2}\right)\)
    • A function that calls itself
  • In practice, our recursive functions usually have two cases:
    • The recursive case, when the function calls itself
    • The base case, which stops the recursion

Kernighan-Ritchie-recursion.svg

Recursion recalled: isPalindrome

// Checks if String s is a palindrome.
// Assumes s is only lower case and does
// not contain any spaces or punctuation.
def isPalindrome(s: String): Boolean =
  // empty or one character?
  if s.length <= 1 then true 
  // start and end differ?
  else if s.head != s.last then false 
  // substring palindrome?
  else isPalindrome(s.substring(1, s.length - 1)) 
end isPalindrome

One or more base cases, and the function applied to a smaller problem instance

Recursion recalled: inner functions

It is common that the recursive step is only part of what we want to do, in which case it is useful to define it as an inner function.

// Checks if String s with punctuation and spaces removed
// is a palindrome. Ignores case.
def isPalindrome(s: String): Boolean = 
  // Helper inner function that encapsulates the recursion.
  // Assumes that the string contains only lower case chars
  // and no spaces or punctuation.
  def actualRecursion(t: String): Boolean = 
    if t.length <= 1 then true
    else if t.head != t.last then false
    else actualRecursion(t.substring(1, t.length - 1))
  end actualRecursion
  // Remove spaces and punctuation, transform to lower case
  val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower)
  // Run the actual recursion and return the result
  actualRecursion(sPlain)
end isPalindrome

Recursion recalled: call-stacks

  • Functions take parameters, can have side effects and keep a state
  • When executing a sub-routine (function) the program often needs to track state
  • Programming languages support this by using a call stack
    • A portion of memory is reserved by the program to keep track of program state during function calls
  • State is pushed (saved) when entering a function, and popped (returned) when the function is done
  • A Stack Trace is a list (most recent call first) of the current call stack
  • It is usually retrieved and printed when a run-time error/exception occurs
    • (So you have seen them before.)

  • In Scala the method Thread.currentThread.getStackTrace will get us the stack trace as an array
  • We could do the following experiment in the console:

// Calls itself recursively n times.
// Returns a string of the stack trace as base case
def rec(n: Int): String =
  if n == 1 then Thread.currentThread.getStackTrace.mkString("\n")
  else rec(n-1)+"" // Note: +"" to create need to keep state
scala> rec(4)
val res0: String = java.base/java.lang.Thread
  .getStackTrace(Thread.java:2451)
rs$line$1$.rec(rs$line$1:4)
rs$line$1$.rec(rs$line$1:5)
rs$line$1$.rec(rs$line$1:5)
rs$line$1$.rec(rs$line$1:5)
rs$line$2$.<clinit>(rs$line$2:1)
rs$line$2.res0(rs$line$2)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor
  .invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
dotty.tools.repl.Rendering.$anonfun$4(Rendering.scala:119)
scala.Option.flatMap(Option.scala:283)
dotty.tools.repl.Rendering.valueOf(Rendering.scala:119)
// .. (Plus many more lines left out.)

Illustrating isPalindrome recursion using manual call stack

// Checks if String s with punctuation and spaces removed
// is a palindrome. Ignores case.
def isPalindrome(s: String): Boolean = 
  // Helper inner function that encapsulates the recursion.
  // Assumes that the string contains only lower case chars
  // and no spaces or punctuation.
  def actualRecursion(t: String): Boolean = 
    if t.length <= 1 then true
    else if t.head != t.last then false
    else actualRecursion(t.substring(1, t.length - 1))
  end actualRecursion
  // Remove spaces and punctuation, transform to lower case
  val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower)
  // Run the actual recursion and return the result
  actualRecursion(sPlain)
end isPalindrome

(This is the same isPalindrome as before.)

  • Write out stack from input to base case for some input (here: "ufo tofu") to get an idea of function behaviour.
  • Call stack:

    isPalindrome("ufo tofu"):
     return actualRecursion("ufotofu")
      actualRecursion("ufotofu"):
       return actualRecursion("fotof")
        actualRecursion("fotof"):
         return actualRecursion("oto"):
          actualRecursion("oto"):
           return actualRecursion("t"):
            actualRecursion("t"):
             return true
    
  • Note:
    • Our 'stack' in this case is usually drawn with the most recent call last for convenience.)
    • The call to s.filter(_.isLetterOrDigit).map(_.toLower) is hidden

Using expansion to illustrate a recursive function

  • If the recursion function is side-effect-free we can expand the calls instead
  • Should be familiar from mathematics, for example:
    • \(f(x) = x + f\left(\frac{x}{2}\right) = x + \frac{x}{2} + \frac{x}{4} + \frac{x}{8} + \ldots\)
  • Or, in the case of isPalindrome, until we find a base case:
isPalindrome("ufo tofu") =
actualRecursion("ufo tofu".filter(_.isLetterOrDigit).map(_.toLower)) =
actualRecursion("ufotofu".map(_.toLower)) =
actualRecursion("ufotofu") =
actualRecursion("fotof") =
actualRecursion("oto") =
actualRecursion("t") =
true

Stack overflow

Example: factorial function

\begin{equation} n! = \begin{cases} 1 & \text{if }n=1;\\ n\cdot (n-1)! & \text{if }n\geq 2\,. \end{cases} \end{equation}
 def fact(n : Int): BigInt = 
   require(n >= 1, "n should be a positive integer")
   if(n == 1) then BigInt(1)
   else n * fact(n-1)
 end fact
scala> fact(10)
val res0: BigInt = 3628800
scala> fact(1000000)
java.lang.StackOverflowError
 at fact(<console>:2)
 at fact(<console>:4)
 ...

What is going on here?

Stack overflow

  • Pushing a state to the call stack costs memory
  • There is only a limited amount of memory for the call stack at run time
  • Too many recursive calls will result in a StackOverflowError

 def fact(n : Int): BigInt = 
   require(n >= 1, "n should be a positive integer")
   if(n == 1) then BigInt(1)
   else n * fact(n-1)
 end fact

Expanding the calls we see:

fact(4) =
 (4 * fact(3)) =
 (4 * (3 * fact(2))) =
 (4 * (3 * (2 * fact(1)))) =
 (4 * (3 * (2 * 1))) =
 (4 * (3 * 2)) =
 (4 * 6) =
 24

Scala cannot simplify in this case, and also cannot multiply values until returning from the recursion (when both values to multiply are known).

In this case, memory use is \(\mathcal{\Omega}(n)\).

Tail recursion

  • Surely it should be possible to do this in space \(\mathcal{O}(1)\)?
  • For example, the iterative solution can do this:

def fact(n : Int) : BigInt =
 require(n >= 1, "n should be a positive integer")
 var result = BigInt(1)
 for i <- 2 to n do
   result = result * i
 end for
 result
end fact

  • Solution: write function using tail recursion
    • Scala optimises this to iteration
  • Tail recursion means that the recursive call a tail call: the last operation of a function (except for the return)
  • However, in the current recursive implementation * is the last operation.

A tail recursive function does not take up stack space

  • The recursion can be 'expanded' by the compiler akin to a loop
  • Recall the count-down and print stack trace function:

// Calls itself recursively n times.
// Returns a string of the stack trace as base case
def rec(n: Int): String =
  if n == 1 then Thread.currentThread.getStackTrace.mkString("\n")
  else rec(n-1)+"" // Note: +"" to create need to keep state

  • Expression rec(n-1)+"" makes sure the function is not tail-recursive (+ is the tail call)
  • If instead we write it without the unnecessary concatenation

// Calls itself recursively n times.
// Returns a string of the stack trace as base case
def rec(n: Int): String =
  if n == 1 then Thread.currentThread.getStackTrace.mkString("\n")
  else rec(n-1) // Note: recursion is now a tail call!

  • Then we re-run the experiment with new rec:

scala> rec(4)
val res1: String = java.base/java.lang.Thread
  .getStackTrace(Thread.java:2451)
rs$line$3$.rec(rs$line$3:4)
rs$line$4$.<clinit>(rs$line$4:1)
rs$line$4.res1(rs$line$4)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor
  .invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
dotty.tools.repl.Rendering.$anonfun$4(Rendering.scala:119)
scala.Option.flatMap(Option.scala:283)
dotty.tools.repl.Rendering.valueOf(Rendering.scala:119)
dotty.tools.repl.Rendering.renderVal(Rendering.scala:159)
dotty.tools.repl.ReplDriver.$anonfun$7(ReplDriver.scala:421)
scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
scala.collection.immutable.List.foreach(List.scala:334)
dotty.tools.repl.ReplDriver
  .extractAndFormatMembers$1(ReplDriver.scala:421)
// ... Plus many more lines left out.

  • Now, there is only a single call to rec! The compiler has detected the tail recursive structure and removed the need for multiple stack frames.

Tail recursion

A tail-recursive version of factorial performs the multiplication before the call:

import scala.annotation.tailrec

def fact(n : Int) : BigInt =
  require(n >= 1, "n should be a positive integer")
  // Inner recursive function using two parameters:
  // - i keep track of the recursion 'level
  // - result is the value of (i-1)!
  @tailrec def iterate(i: Int, result: BigInt): BigInt =
    if i > n then result
    else iterate(i+1, result*i)
  end iterate

  iterate(2, BigInt(1))
end fact

The annotation @tailrec is not strictly necessary, but will help Scala know that you intend the function to be tail recursive, and the compiler will warn you in case this is not possible.

Recursive data structures

  • In addition to functions, the data structure can be recursively defined
  • These are most naturally manipulated with recursive functions
  • In this lecture, we will discuss two examples in detail:
    • Linked lists (similar to List in Scala)
    • Symbolic arithmetic expressions (e.g. \(2x + 4y - 7\))
      • This is an example of a more general tree-like data structure

Linked lists

  • A linked list is a data structure that can represent a sequence
  • It can be defined recursively, because a list is
    • Either empty, or
    • An element followed by a list
  • More formally, define a T-List as a list containing elements of type T:
    • Nil : which denotes an empty T-list, or
    • Cons(e,l) : where e is an element of type T and l is a T-List
      • We call e the head, and l the tail of the list Cons(e,l)

conga-list.webp

  • For example:
    • Nil is an empty list []
    • Cons(1,Nil) is the Int-List [1]
    • Cons("c",Cons("a",Cons("b",Nil))) is the String-List ["c","a","b"]

"Cons" and "Nil" comes from the lisp language and refers to 'construction' and the number zero respectively.

cons-list.svg

Linked lists - why bother?

  • Dynamic : prepending element is \(\mathcal{O}(1)\)
    • Inserting and removing does not require reorganising data (But, \(\mathcal{O}(n)\))
  • Versatile: Can be used as basis for other data structures
    • Stack
    • Queue
    • Tree

But, as always, not one data structure for all needs. We implement linked lists to learn.

A linked list class in Scala

  • A type T list is
    • Nil or,
    • consists of a head and a tail
      • where the head is an element of type T and the tail another T list
abstract class LinkedList[T]:
  // List empty (Nil)
  // or not (Cons)?
  def isEmpty: Boolean
  // Data
  def head: T
  // Rest of the list
  def tail: LinkedList[T]
end LinkedList

Use case classes to specialise Nil and Cons:

case class Nil[T]() extends LinkedList[T]:
  // Nil is empty
  def isEmpty = true
  // head is undefined in Nil
  def head = throw new java.util
    .NoSuchElementException("head of empty list")
  // tail is undefined in Nil
  def tail = throw new java.util
    .NoSuchElementException("tail of empty list")
end Nil
case class Cons[T](val head: T,
  val tail: LinkedList[T])
    extends LinkedList[T]:

  // Cons is not empty
  def isEmpty = false

  // Rest is created automatically
  // in the case class
end Cons

A linked list class in Scala - usage

Now we can build lists like

scala> val l = Cons(5, Cons(4, Cons(-7, Nil())))
val l: Cons[Int] = Cons(5,Cons(4,Cons(-7,Nil())))

scala> l.head
val res7: Int = 5

scala> l.tail
val res8: LinkedList[Int] = Cons(4,Cons(-7,Nil()))

Notice the recursive structure?

one-list.png

A linked list class in Scala - usage

Prepending an element is easy

scala> val m = Cons(3,l)
val m: Cons[Int] = Cons(3,Cons(5,Cons(4,Cons(-7,Nil()))))

scala> m.head
val res9: Int = 3

scala> m.tail == l
val res10: Boolean = true

Note that the previous list, l, is not copied!

two-lists.png

A linked list class in Scala - methods

What are some useful methods to have in a Linked list class?

What would we like to be able to do with it?

Linked list length

It is always useful to find the number of elements in a sequence.

  • Base case: an empty list has length 0
  • Recursion: length is 1 + the length of the tail
abstract class LinkedList[T]:
  // Previous code left out ...

  def length: Int = if isEmpty then 0 else 1 + tail.length

end LinkedList

Is length tail recursive?

No. Evident by expansion:

Cons(1, Cons(2, Cons(3, Nil()))).length =
1 + Cons(2, Cons(3, Nil())).length =
1 + (1 + Cons(3, Nil()).length) =
1 + (1 + (1 + Nil().length)) =
1 + (1 + (1 + 0)) =
1 + (1 + 1) =
1 + 2 =
3

Linked list length

  • Base case: an empty list has length 0
  • Recursion: length is 1 + the length of the tail

A tail recursive version:

def length: Int =
  // Tail-recursive inner function, accumulating the result
  @tailrec def inner(remaining: LinkedList[A], result:Int): Int =
    if remaining.isEmpty then result
    else inner(remaining.tail, result+1)
  end inner

  inner(this, 0)
end length

Expansion:

Cons(1, Cons(2, Cons(3, Nil()))).length =
inner(Cons(1, Cons(2, Cons(3, Nil()))), 0) =
inner(Cons(2, Cons(3, Nil())), 1) =
inner(Cons(3, Nil()), 2) =
inner(Nil(), 3) =
3

Linked list contains

Check if some element e is in the list

abstract class LinkedList[T]:
  // Previous code left out ...

  def contains(e: T): Boolean =
    if isEmpty then false
    else if head == e then true
    else tail.contains(e)
  end contains

end LinkedList
abstract class LinkedList[T]:
  // Previous code left out ...

  @tailrec final def contains(e: T): Boolean =
    if isEmpty then false
    else if head == e then true
    else tail.contains(e)
  end contains

end LinkedList

Note: @tailrec methods must be declared final.

  • Base case: an empty list can not contain e
  • Recursion: is the head equal to e? Else, check if the tail contains e

Linked list contains - alternative implementation

We can do the same using pattern matching as well:

abstract class LinkedList[T]:
  // Previous code left out ...

  @tailrec final def contains(e: T): Boolean =
    this match
      case Nil() => false
      case Cons(h, t) => if (h == e) then true else t.contains(e)
    end match
  end contains

end LinkedList

Linked list reverse

How could we go about reversing the order of a list, e.g. 1,2,3 → 3,2,1 ?

abstract class LinkedList[T]:
  // Previous code left out ...
  def reverse: LinkedList[T] =

    @tailrec def inner(remaining: LinkedList[T],
      result: LinkedList[T]): LinkedList[T] =
      remaining match
        case Nil() => result
        case Cons(h, t) => inner(t, Cons(h, result))
      end match
    end inner

    inner(this, Nil())
  end reverse

end LinkedList

list-reversal-steps_1.png

list-reversal-steps_2.png

list-reversal-steps_3.png

list-reversal-steps_4.png

Expressions

Another type of recursively defined data structure is the symbolic arithmetic expression.

  • These are made up from
    • numerical constants
    • variables
    • operations

  • Examples:
    • 2.5
    • x
    • \(3*x + 8*(x -y) - 5\)

Expressions - definition

Assume that the operations we want to be able to use are addition, subtraction, multiplication, and negation. Then we have the following recursive definition of an expression:

  1. A constant (such as \(3.0\)) is an expression
  2. A variable (such as \(x\)) is an expression
  3. If \(e_1\) and \(e_2\) are expressions, then \((e_1 + e_2)\), \((e_1 - e_2)\), \((e_1 * e_2)\), as well as \(-e_1\) are also expressions
  4. There are no other expressions

Expressions - example:

  • \(2.0\) is an expression
  • \(x\) is an expression
  • \(y\) is an expression
  • \((2.0 * x)\) is an expression
  • So, \((2.0 * x) + y\) is an expression
  • So, \(-(2.0 * x + y)\) is an expression

Expressions - implementation in Scala

We use an abstract base class and case classes for the different types of expressions:

abstract class Expr:
  // More here soon..
end Expr

/** Variable expression, like "x" or "y". */
case class Var(name: String) extends Expr:
  override def toString = name
end Var
/** Constant expression like "2.1" or "-1.0" */
case class Num(value: Double) extends Expr:
  override def toString = value.toString
end Num
/** Expression formed by multiplying two expressions, like "x * (y+3)" */
case class Multiply(left: Expr, right: Expr) extends Expr:
  override def toString = "(" + left + "*" + right + ")"
end Multiply
/** Expression formed by adding two expressions, like "x + (y*3)" */
case class Add(left: Expr, right: Expr) extends Expr:
  override def toString = "(" + left + " + " + right + ")"
end Add
/** Expression formed by subtracting an expression from another, "x - (y*3)" */
case class Subtract(left: Expr, right: Expr) extends Expr:
  override def toString = "(" + left + " - " + right + ")"
end Subtract
/** Negation of an expression, like "-((3*y)+z)" */
case class Negate(p: Expr) extends Expr:
  override def toString = "-" + p
end Negate

Expressions - implementation in Scala

Now we can declare \(-((2.0*x) + y)\) as

scala> val e = Negate(Add(Multiply(Num(2.0),
     |   Var("x")),
     |   Var("y")))
     | 
val e: Negate = -((2.0*x) + y)

  • Thanks to the overridden toString methods in the case classes it even gets pretty-printed.
  • Note that the 'head' is of type Negate, because this is the outermost expression.
  • As addition, subtraction, and multiplication have two operands, some nodes now have two 'tails'
    • ⇒ This is an example of a tree-like data structure

exprs-objects.svg

Structure of the expression \(-((2.0*x) + y)\)

Expressions - implementation in Scala

We can make the expressions easier to write by overriding operators in Expr:

abstract class Expr:
  /* Overriding these operators enable us to construct
  expressions with infix notation */
  def +(other: Expr) = Add(this, other)
  def -(other: Expr) = Subtract(this, other)
  def *(other: Expr) = Multiply(this, other)
  def unary_- = Negate(this)
end Expr

// Case-classes as before here...

We can now do:

scala> -( Num(2.0) * Var("x") + Var("y"))
val res: Negate = -((2.0*x) + y)

Expression quiz

expression-tree-quiz.svg

Which Scala expression creates this tree?

https://presemo.aalto.fi/prog

35c9e48f86af625bba6aa15dfb73713f-300.svg

Expressions - evaluation

It would be useful to have a method to evaluate an expression. That is, given an assignment of values to all variables in an Expr return the numerical value.

  • Given: map from variable names to values
  • Base case: the value of Num is the value
  • Base case: the value of Var is the value in the given map
  • Recursion: The value of an operator expression is the operator applied to the values of the operand expressions

abstract class Expr:
  // Previous code left out ...

  /** Custom exception for when a variable isn't assigned. */
  class VariableNotAssignedException(message: String)
      extends java.lang.RuntimeException(message)

  def evaluate(p: Map[String, Double]): Double = this match
    case Var(n) => p.get(n) match {
      case Some(v) => v
      case None => throw new VariableNotAssignedException(n)
    }
    case Num(v) => v
    case Multiply(l, r) => l.evaluate(p) * r.evaluate(p)
    case Add(l, r) => l.evaluate(p) + r.evaluate(p)
    case Subtract(l, r) => l.evaluate(p) - r.evaluate(p)
    case Negate(t) => -t.evaluate(p)
  end evaluate

end Expr

Expressions - evaluation example

Evaluate -((2.0*x) + y), given \(x = 4.5\), \(y = 1.2\):

scala> val e1 = -( Num(2.0) * Var("x") + Var("y"))
val e1: Negate = -((2.0*x) + y)

scala> val p = Map("x" -> 4.5, "y" -> 1.2)
val p: scala.collection.immutable.Map ...

scala> e1.evaluate(p)

Expressions - evaluation example

Evaluate -((2.0*x) + y), given \(x = 4.5\), \(y = 1.2\)

(val e1 = -( Num(2.0) * Var("x") + Var("y")); val p = Map("x" -> 4.5, "y"->1.2))

expr-evaluation-example_01.png

expr-evaluation-example_02.png

expr-evaluation-example_03.png

expr-evaluation-example_04.png

expr-evaluation-example_05.png

expr-evaluation-example_06.png

expr-evaluation-example_07.png

expr-evaluation-example_08.png

expr-evaluation-example_09.png

expr-evaluation-example_10.png

expr-evaluation-example_11.png

expr-evaluation-example_12.png

expr-evaluation-example_13.png

expr-evaluation-example_14.png

Recursive problem solving

  • Some type of problems naturally lend themselves to a recursive approach when thinking about how to find a solution
  • Of the type "consistently arrange \(x\) 'pieces' in \(y\) 'locations/steps' to reach a target"

Sudoku-example-wikimedia.svg

Given as input a set \(W = \{w_1,w_2,...,w_n\}\) of \(n\) integers (the steps) and an integer \(t\) (the target), is there a subset \(S \subseteq W\) with \(\sum_{z\in S}z = t\)?

(The subset sum problem, see notes)

sched-calendar-ex.svg

And many other kinds of mathematical problems, logical games, and puzzles

Recursive problem solving

"consistently arrange \(x\) 'pieces' in \(y\) 'locations/steps' to reach a target"

  • Consistent means adhering to some kind of rules or constraints
    • "John can not sit next to Sally",
    • "The same person cannot be in two places",
    • "Each seat can only be used once",
    • et c.
  • Steps means that we can improve on a potential solution one piece at a time (as long as it remains 'consistent')
    • "Select one square in the Sudoku grid and put a number there"
  • Target means that we can recognise when we have a complete solution

Recursive problem solving

"consistently arrange \(x\) 'pieces' in \(y\) 'locations/steps' to reach a target"

We can solve these problems by searching for a solution. Recursion is a natural way to implement exhaustive search, that considers every possible configuration one step at a time until it finds a solution (or every combination has been tried, and there are no solution).

Note: These kind of problems can be very hard to solve in general. Potentially \(\mathcal{O}(x^y)\).

Backtracking search

Candidate solutions are built recursively and backtracking is done if we notice that the solution is not going to work.

General idea:

  • At each step we have a configuration
  • Is the current configuration consistent?
    • If it also complete, then we have reached our target and found a solution
    • If it is not complete (but still consistent) then update the configuration by taking another step, and start over
  • If the configuration is not consistent, then backtrack to the previous consistent configuration and try something else
    • (If there are no previous configurations, the problem does not have a solution)

Backtracking search

Algorithm pseudocode:

bsolve(config):
  if config is consistent then
     if config is complete then
        return Success(config)
     else:
        for every possible action given config do
           s = bsolve(config updated by action)
           if s is Success then return s
  else:
     return Fail
end bsolve

For example, in Sudoku:

  • config is a nxn Sudoku grid
  • A board is consistent if it obeys the rules of Sudoku
  • A board is complete if also has numbers in all squares
  • An action is to pick an empty square and a candidate number according to some criteria

Specific 4x4 Sudoku example:

sudoku-4x4-start-only.svg

In this example: let the order of actions be determined by picking the first unfilled square starting from the top left, and to try the numbers in order for each square.

Backtracking example – Sudoku 4x4

sudoku-4x4-backtrack-example-1.svg

sudoku-4x4-backtrack-example-2.svg

sudoku-4x4-backtrack-example-3.svg

sudoku-4x4-backtrack-example-4.svg

sudoku-4x4-backtrack-example-5.svg

sudoku-4x4-backtrack-example-6.svg

sudoku-4x4-backtrack-example-7.svg

sudoku-4x4-backtrack-example-8.svg

sudoku-4x4-backtrack-example-9.svg

sudoku-4x4-backtrack-example-10.svg

sudoku-4x4-backtrack-example-11.svg

sudoku-4x4-backtrack-example-12.svg

sudoku-4x4-backtrack-example-13.svg

sudoku-4x4-backtrack-example-14.svg

sudoku-4x4-backtrack-example-15.svg

sudoku-4x4-backtrack-example-16.svg

sudoku-4x4-backtrack-example-17.svg

In this example: let the order of actions be determined by picking the first unfilled square starting from the top left, and to try the numbers in order for each square.

Exercises

  1. Linked lists : implement recursive methods
  2. Expressions : implement recursive methods
  3. Group scheduling : implement recursive scheduling
  4. Sudoku : implement helper methods and recursive solver
  5. Solving subset sum with dynamic programming (challenge problem)

  • Some exercises have restrictions (no var, Array, et c)
    • Read the instructions
  • Remember tail recursion
  • Study the existing methods before attempting tasks
  • If some sub-task is too hard, submit to get partial points
  • For scheduling and Sudoku solver consider a backtracking search-like approach
  • Sudoku comes with a GUI which you can use to play