Free Handbook · Every example compiled & verified

Data Structures & Algorithms

Build stacks, queues, linked lists, trees, heaps and graphs by hand in Scala, then pick the right List, Vector, Map, Queue or PriorityQueue by its cost.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 13 · what you'll be able to do

  • Pick a Scala collection from its cost table instead of defaulting to List for everything
  • Build an immutable stack, a two-list queue, a linked list and a binary search tree with case classes and pattern matching
  • Use mutable.PriorityQueue, TreeMap and immutable Queue correctly, including the min-heap ordering trick
  • Traverse a graph with BFS and DFS written as tail-recursive functions
  • Write binary search, merge sort and bottom-up dynamic programming, and sort with sortBy, sortWith and Ordering
01

The cost table: pick a collection by what it costs

A data structure is a trade-off: it makes some operations cheap by making others expensive. Big-O describes how the work grows as the input grows: O(1) is "the same work no matter how big", O(n) is "work grows in step with the size", O(log n) is "doubling the input adds one step". Scala adds a second axis: immutable collections share structure between versions, so "adding" returns a new collection cheaply without copying everything.

The headline: List is great at the front and bad at the back and in the middle. When you need indexing or appends, reach for Vector (immutable) or ArrayBuffer (mutable).
CollectionIndex / lookupPrependAppendBuilt on
ListO(n)O(1) ::O(n) :+singly linked cons cells
Vectoreffectively O(1)effectively O(1)effectively O(1)wide tree (32-way)
ArrayO(1)— copy— copyJVM array, mutable, fixed size
mutable.ArrayBufferO(1)O(n)O(1) amortisedresizable array
Map / Set (hash)O(1) effectively—O(1) effectivelyhash trie (immutable) or hash table (mutable)
TreeMap / TreeSetO(log n)—O(log n)red-black tree, kept sorted
immutable.QueueO(n)—O(1) enqueuetwo lists, amortised O(1) dequeue
mutable.PriorityQueueO(1) head (max)—O(log n)binary heap
scalaMain.scala
@main def run(): Unit =
  val data = Vector.range(0, 1_000_000)
  val target = 999_999

  var linearSteps = 0
  val it = data.iterator
  var found = false
  while !found && it.hasNext do
    linearSteps += 1
    if it.next() == target then found = true

  var binarySteps = 0
  var lo = 0
  var hi = data.length - 1
  var done = false
  while !done && lo <= hi do
    binarySteps += 1
    val mid = lo + (hi - lo) / 2
    if data(mid) == target then done = true
    else if data(mid) < target then lo = mid + 1
    else hi = mid - 1

  println(s"Linear search: $linearSteps steps")
  println(s"Binary search: $binarySteps steps")
Outputcompiled & run with real Scala
Linear search: 1000000 steps
Binary search: 20 steps

The same question, "where is 999,999?", answered by an O(n) and an O(log n) algorithm over one million sorted numbers. Scala has no break, so the loops stop with a flag.

Your turn

Replace the hand-written binary search with the standard one: data.search(target) returns Found(index) or InsertionPoint(index). Print both cases.

02

List, Vector and ArrayBuffer

A List is a chain of cells; 0 :: xs makes one new cell pointing at the old list, so it is O(1) and the old list is shared, not copied. But xs(i) walks i cells and xs :+ x rebuilds the whole chain. Vector is a shallow tree with 32 children per node, so indexing and appending touch at most a handful of nodes even for millions of elements. ArrayBuffer is the mutable, resizable array, for local, performance-sensitive code.

scalaMain.scala
import scala.collection.mutable

@main def run(): Unit =
  val xs = List(2, 3)
  val ys = 1 :: xs            // new head cell, xs shared
  println(ys)
  println(ys.tail eq xs)      // the very same object

  val v = Vector(1, 2, 3)
  val v2 = v :+ 4             // cheap append
  println(v2(3))
  println(v.updated(0, 99))   // new Vector, v unchanged
  println(v)

  val buf = mutable.ArrayBuffer.empty[Int]
  for i <- 1 to 5 do buf += i * i
  buf.remove(0)
  println(buf)
Outputcompiled & run with real Scala
List(1, 2, 3)
true
4
Vector(99, 2, 3)
Vector(1, 2, 3)
ArrayBuffer(4, 9, 16, 25)

eq compares references. It proves that prepending did not copy xs: both lists point at the same cells.

Building a List in a loop
Appending with :+ inside a loop is O(n²). Either prepend with :: and reverse once at the end (the classic functional idiom), collect into a ListBuffer or Vector, or better, express the loop as map/filter/foldLeft and let the library build the result.
03

Map and Set: hash lookups

A hash-based Map or Set turns a key into a number with hashCode, uses it to find a slot, and confirms with equals, so lookups are effectively O(1). Case classes generate both methods from their fields, which makes them safe keys. Use getOrElse, get (an Option) or updatedWith instead of apply, which throws on a missing key. Iteration order of a hash map is not something to rely on: sort before printing.

scalaMain.scala
@main def run(): Unit =
  val text = "the cat and the hat and the bat"
  val counts = text.split(" ").foldLeft(Map.empty[String, Int]) { (m, w) =>
    m.updated(w, m.getOrElse(w, 0) + 1)
  }
  counts.toList.sortBy((w, n) => (-n, w)).foreach((w, n) => println(s"$w: $n"))

  println(counts.get("cat"))
  println(counts.get("dog"))
  println(text.split(" ").groupMapReduce(identity)(_ => 1)(_ + _) == counts)

  val backend = Set("scala", "sql", "docker", "aws")
  val data = Set("python", "sql", "spark", "aws")
  println((backend intersect data).toList.sorted)
  println((backend diff data).toList.sorted)
  println((backend union data).size)
Outputcompiled & run with real Scala
the: 3
and: 2
bat: 1
cat: 1
hat: 1
Some(1)
None
true
List(aws, sql)
List(docker, scala)
6

groupMapReduce does the whole count in one call: group by the word, map each to 1, reduce by adding.

Your turn

Use a case class Point(x: Int, y: Int) as a key in a Set, then check set.contains(Point(1, 2)). Try the same with a plain class. Why does one work and not the other?

04

Stacks and queues

An immutable List is a stack: push is x :: stack, pop is pattern matching on top :: rest, both O(1). A queue needs cheap access to both ends, and the classic immutable trick is two lists: enqueue onto the back list, dequeue from the front list, and when the front runs out, reverse the back list into it once. Each element is reversed at most once, so every operation is O(1) amortised. scala.collection.immutable.Queue is exactly this.

scalaMain.scala
final case class TwoListQueue[A](front: List[A], back: List[A]):
  def enqueue(a: A): TwoListQueue[A] = copy(back = a :: back)

  def dequeue: Option[(A, TwoListQueue[A])] = front match
    case head :: rest => Some((head, copy(front = rest)))
    case Nil if back.nonEmpty => TwoListQueue(back.reverse, Nil).dequeue
    case Nil => None

object TwoListQueue:
  def empty[A]: TwoListQueue[A] = TwoListQueue(Nil, Nil)

def balanced(s: String): Boolean =
  val pairs = Map(')' -> '(', ']' -> '[', '}' -> '{')
  val stack = s.foldLeft(Option(List.empty[Char])) {
    case (None, _) => None
    case (Some(st), c) if pairs.values.exists(_ == c) => Some(c :: st)
    case (Some(top :: rest), c) if pairs.get(c).contains(top) => Some(rest)
    case (Some(_), c) if pairs.contains(c) => None
    case (acc, _) => acc
  }
  stack.contains(Nil)

@main def run(): Unit =
  val q = TwoListQueue.empty[String].enqueue("a").enqueue("b").enqueue("c")
  val Some((first, q2)) = q.dequeue: @unchecked
  val Some((second, _)) = q2.enqueue("d").dequeue: @unchecked
  println(s"$first $second")

  for s <- List("{[()]}", "([)]", "((", "f(x[0])") do
    println(f"$s%-8s ${if balanced(s) then "yes" else "no"}")
Outputcompiled & run with real Scala
a b
{[()]}   yes
([)]     no
((       no
f(x[0])  yes

Every operation returns a new queue; the old one is still valid. balanced uses List[Char] as the stack and None to mean "already broken".

Your turn

Rewrite balanced with scala.collection.mutable.Stack and a for loop. Which version is easier to read?

VisualizeDequeue after the front list runs outStep 1 / 4
def dequeue: Option[(A, TwoListQueue[A])] = front match
case head :: rest => Some((head, copy(front = rest)))
case Nil if back.nonEmpty => TwoListQueue(back.reverse, Nil).dequeue
case Nil => None
Line 1

Enqueued a, b, c: they sit in the back list, newest first.

Variables now
frontNil
backList(c, b, a)
All 4 steps as a table
StepLineWhat happenedVariables now
11Enqueued a, b, c: they sit in the back list, newest first.front = Nil back = List(c, b, a)
23Front is empty but back is not: reverse back once into a new front.front = List(a, b, c) back = Nil
32Now the front has a head: return it with the rest.head = a front = List(b, c)
42The next dequeue is O(1) straight from the front, with no reversing.head = b front = List(c)
05

A linked list built by hand

Scala's own List is a linked list, defined roughly as below: a sealed type with two cases, an empty list and a cell holding a head and a tail. Building it yourself shows why recursion and pattern matching are the natural way to process it, and interviewers still ask for "reverse a linked list".

scalaMain.scala
import scala.annotation.tailrec

enum MyList[+A]:
  case Empty
  case Cell(head: A, tail: MyList[A])

  def prepend[B >: A](b: B): MyList[B] = Cell(b, this)

  def reverse: MyList[A] =
    @tailrec def loop(rest: MyList[A], acc: MyList[A]): MyList[A] = rest match
      case Empty => acc
      case Cell(h, t) => loop(t, Cell(h, acc))
    loop(this, Empty)

  def map[B](f: A => B): MyList[B] = this match
    case Empty => Empty
    case Cell(h, t) => Cell(f(h), t.map(f))

  def mkString: String =
    @tailrec def loop(rest: MyList[A], acc: Vector[String]): String = rest match
      case Empty => acc.mkString(" -> ")
      case Cell(h, t) => loop(t, acc :+ h.toString)
    loop(this, Vector.empty)

@main def run(): Unit =
  val xs = MyList.Empty.prepend(3).prepend(2).prepend(1)
  println(xs.mkString)
  println(xs.reverse.mkString)
  println(xs.map(_ * 10).mkString)
Outputcompiled & run with real Scala
1 -> 2 -> 3
3 -> 2 -> 1
10 -> 20 -> 30

+A makes the list covariant (Module 09), which is why the single Empty case can be a MyList of anything.

Your turn

Add def length: Int as a tail-recursive loop, then def filter(p: A => Boolean): MyList[A].

Visualizereverse on 1 -> 2 -> 3Step 1 / 5
@tailrec def loop(rest: MyList[A], acc: MyList[A]): MyList[A] = rest match
case Empty => acc
case Cell(h, t) => loop(t, Cell(h, acc))
loop(this, Empty)
Line 4

Start with the whole list and an empty accumulator.

Variables now
rest1 -> 2 -> 3
accEmpty
All 5 steps as a table
StepLineWhat happenedVariables now
14Start with the whole list and an empty accumulator.rest = 1 -> 2 -> 3 acc = Empty
23Take 1 off the front and put it on the front of acc.rest = 2 -> 3 acc = 1
33Take 2: it goes in front of 1.rest = 3 acc = 2 -> 1
43Take 3.rest = Empty acc = 3 -> 2 -> 1
52Nothing left: acc is the reversed list. The recursive call is the last thing each case does, so @tailrec compiles it to a loop.
06

Recursion, @tailrec and memoisation

A recursive function solves a problem by calling itself on a smaller version until it hits a base case. Each call normally takes a JVM stack frame, and a deep recursion ends in StackOverflowError. If the recursive call is the last thing the function does, @tailrec makes the compiler turn it into a loop, and refuses to compile if it cannot. For overlapping sub-problems, cache results in a map (memoisation).

scalaMain.scala
import scala.annotation.tailrec
import scala.collection.mutable

@tailrec
def sumDigits(n: Long, acc: Long = 0): Long =
  if n == 0 then acc else sumDigits(n / 10, acc + n % 10)

val memo = mutable.Map.empty[Int, BigInt]
def fib(n: Int): BigInt =
  if n <= 1 then BigInt(n)
  else memo.get(n) match
    case Some(v) => v
    case None =>
      val v = fib(n - 1) + fib(n - 2)
      memo(n) = v
      v

def flatten(xs: List[Any]): List[Any] = xs.flatMap {
  case inner: List[?] => flatten(inner)
  case x => List(x)
}

@main def run(): Unit =
  println(sumDigits(98765))
  println(fib(10))
  println(fib(100))
  println(flatten(List(1, List(2, List(3, List(4)), 5), Nil, 6)))
Outputcompiled & run with real Scala
35
55
354224848179261915075
List(1, 2, 3, 4, 5, 6)

fib(100) overflows a Long, so the result is a BigInt. Without the memo, the call tree would have about 1021 nodes.

Error you will hit

@tailrec on a function that is not tail recursive

scala
import scala.annotation.tailrec

@tailrec
def factorial(n: Int): BigInt =
  if n <= 1 then 1 else n * factorial(n - 1)

@main def run(): Unit = println(factorial(20))
-- Error: Main.scala:5:37
5 |  if n <= 1 then 1 else n * factorial(n - 1)
  |                            ^^^^^^^^^^^^^^^^
  |                 Cannot rewrite recursive call: it is not in tail position
1 error found
Why the compiler said that

After factorial(n - 1) returns, there is still work to do: multiply by n. The call is not the last operation, so each call must keep its frame, and the compiler cannot turn it into a loop.

The fix

Carry the running product in an accumulator parameter, so the recursive call is the final expression.

scala
import scala.annotation.tailrec

@tailrec
def factorial(n: Int, acc: BigInt = 1): BigInt =
  if n <= 1 then acc else factorial(n - 1, acc * n)

@main def run(): Unit = println(factorial(20))
07

A binary search tree as an ADT

A binary search tree keeps smaller values in the left subtree and larger ones in the right, so search and insert follow one path: O(log n) when balanced, O(n) when sorted input turns it into a chain. In Scala a tree is a textbook algebraic data type: a sealed enum with a Leaf and a Node, and every operation is a pattern match. Insert returns a new tree that shares every untouched subtree with the old one. TreeSet and TreeMap are the balanced (red-black) versions in the library.

scalaMain.scala
enum Tree:
  case Leaf
  case Node(left: Tree, value: Int, right: Tree)

  def insert(v: Int): Tree = this match
    case Leaf => Node(Leaf, v, Leaf)
    case n @ Node(l, x, r) =>
      if v < x then Node(l.insert(v), x, r)
      else if v > x then Node(l, x, r.insert(v))
      else n

  def contains(v: Int): Boolean = this match
    case Leaf => false
    case Node(l, x, r) => if v == x then true else if v < x then l.contains(v) else r.contains(v)

  def inOrder: List[Int] = this match
    case Leaf => Nil
    case Node(l, x, r) => l.inOrder ++ (x :: r.inOrder)

  def height: Int = this match
    case Leaf => 0
    case Node(l, _, r) => 1 + math.max(l.height, r.height)

@main def run(): Unit =
  val tree = List(50, 30, 70, 20, 40, 60, 80).foldLeft(Tree.Leaf)(_.insert(_))
  println(tree.inOrder.mkString(" "))
  println(s"${tree.contains(60)} ${tree.contains(65)}")
  println(s"height: ${tree.height}")

  val chain = (1 to 7).foldLeft(Tree.Leaf)(_.insert(_))
  println(s"height after sorted inserts: ${chain.height}")

  val ts = collection.immutable.TreeSet(1 to 7*)
  println(s"TreeSet stays balanced: ${ts.rangeFrom(5).toList}")
Outputcompiled & run with real Scala
20 30 40 50 60 70 80
true false
height: 3
height after sorted inserts: 7
TreeSet stays balanced: List(5, 6, 7)

foldLeft(Tree.Leaf)(_.insert(_)) builds the tree from a list in one line. The same seven values give height 3 or 7 depending on insertion order, which is why the library uses a self-balancing tree.

08

Heaps and priority queues

A heap gives you the largest (or smallest) item in O(1) and inserts or removes in O(log n). Scala's mutable.PriorityQueue is a max-heap by the given Ordering. For a min-heap, pass the reversed ordering: PriorityQueue.empty(using Ordering[Int].reverse). Note that iterating or printing a PriorityQueue shows its internal array order, not sorted order; dequeue repeatedly (or dequeueAll) to get sorted output.

scalaMain.scala
import scala.collection.mutable

case class Task(name: String, priority: Int)

def topK(xs: Iterable[Int], k: Int): List[Int] =
  val heap = mutable.PriorityQueue.empty[Int](using Ordering[Int].reverse) // min-heap
  for x <- xs do
    if heap.size < k then heap.enqueue(x)
    else if x > heap.head then
      heap.dequeue()
      heap.enqueue(x)
  heap.dequeueAll.reverse.toList

@main def run(): Unit =
  val minHeap = mutable.PriorityQueue(42, 7, 19, 3, 25)(using Ordering[Int].reverse)
  println(s"smallest: ${minHeap.head}")
  println(minHeap.dequeueAll.mkString(" "))

  val tasks = mutable.PriorityQueue.empty[Task](using Ordering.by(_.priority))
  tasks ++= List(Task("write tests", 2), Task("fix production bug", 10), Task("update README", 1), Task("review pull request", 5))
  while tasks.nonEmpty do println(tasks.dequeue().name)

  println(topK(List(5, 91, 17, 64, 3, 88, 42, 70), 3))
Outputcompiled & run with real Scala
smallest: 3
3 7 19 25 42
fix production bug
review pull request
write tests
update README
List(91, 88, 70)

topK keeps a min-heap of size k: each new value only has to beat the smallest of the current top k. O(n log k) time, O(k) memory.

Your turn

Write the heap yourself on an ArrayBuffer[Int]: push appends and swaps with the parent at (i - 1) / 2 while smaller; pop moves the last item to index 0 and sifts it down.

09

Graphs: BFS and DFS

A graph is nodes joined by edges. In Scala the natural shape is an adjacency map: Map[String, List[String]]. Breadth-first search explores level by level with a queue and finds the shortest path when every edge counts as one step. Depth-first search goes deep first and answers "what can I reach?". Both are written below as tail-recursive loops over immutable state, with no mutation at all.

scalaMain.scala
import scala.annotation.tailrec
import scala.collection.immutable.Queue

val graph: Map[String, List[String]] = Map(
  "home"    -> List("blog", "tools"),
  "blog"    -> List("post-a", "post-b"),
  "tools"   -> List("ats"),
  "post-a"  -> List("ats"),
  "post-b"  -> Nil,
  "ats"     -> List("pricing"),
  "pricing" -> Nil,
)

def shortestPath(from: String, to: String): Option[List[String]] =
  @tailrec
  def loop(queue: Queue[String], cameFrom: Map[String, String]): Option[List[String]] =
    queue.dequeueOption match
      case None => None
      case Some((node, _)) if node == to =>
        Some(List.unfold(Option(to))(_.map(n => (n, cameFrom.get(n)))).reverse)
      case Some((node, rest)) =>
        val fresh = graph(node).filterNot(n => n == from || cameFrom.contains(n))
        loop(rest.enqueueAll(fresh), cameFrom ++ fresh.map(_ -> node))
  loop(Queue(from), Map.empty)

def dfs(node: String, seen: Vector[String] = Vector.empty): Vector[String] =
  graph(node).foldLeft(seen :+ node) { (acc, next) =>
    if acc.contains(next) then acc else dfs(next, acc)
  }

@main def run(): Unit =
  println(shortestPath("home", "pricing").map(_.mkString(" -> ")))
  println(dfs("home").mkString(", "))
  println(shortestPath("pricing", "home"))
Outputcompiled & run with real Scala
Some(home -> tools -> ats -> pricing)
home, blog, post-a, ats, pricing, post-b, tools
None

BFS found the 3-click route through tools, not the 4-click route through the blog. List.unfold walks cameFrom backwards from the target to rebuild the path.

VisualizeshortestPath(home → pricing): the queue level by levelStep 1 / 7
queue.dequeueOption match
case None => None
case Some((node, _)) if node == to => /* rebuild path */
case Some((node, rest)) =>
val fresh = graph(node).filterNot(n => n == from || cameFrom.contains(n))
loop(rest.enqueueAll(fresh), cameFrom ++ fresh.map(_ -> node))
Line 1

Start: only home is in the queue.

Variables now
queueQueue(home)
All 7 steps as a table
StepLineWhat happenedVariables now
11Start: only home is in the queue.queue = Queue(home)
26Visit home: enqueue blog and tools, both reached from home.queue = Queue(blog, tools)
36Visit blog: enqueue post-a and post-b.queue = Queue(tools, post-a, post-b)
46Visit tools: enqueue ats, remembering it came from tools.queue = Queue(post-a, post-b, ats) cameFrom(ats) = tools
55Visit post-a: ats is already known, so it is filtered out and the shorter route is kept.
66Visit ats: enqueue pricing.queue = Queue(pricing) cameFrom(pricing) = ats
73Dequeue pricing: it is the target. Walk cameFrom back to home.
10

Binary search and sorting

Binary search halves a sorted range each step. The standard library has it built in: sortedSeq.search(x) returns Found(i) or InsertionPoint(i). Merge sort is the sort to write by hand in an interview; with pattern matching on two lists it is almost a definition.

scalaMain.scala
import scala.collection.Searching.{Found, InsertionPoint}

def mergeSort(xs: List[Int]): List[Int] =
  def merge(a: List[Int], b: List[Int], acc: List[Int]): List[Int] = (a, b) match
    case (Nil, _) => acc.reverse ++ b
    case (_, Nil) => acc.reverse ++ a
    case (x :: xt, y :: yt) =>
      if x <= y then merge(xt, b, x :: acc) else merge(a, yt, y :: acc)
  if xs.lengthCompare(1) <= 0 then xs
  else
    val (left, right) = xs.splitAt(xs.length / 2)
    merge(mergeSort(left), mergeSort(right), Nil)

@main def run(): Unit =
  val prices = Vector(5, 9, 9, 14, 20, 31)
  for t <- List(9, 10, 1, 40) do
    prices.search(t) match
      case Found(i) => println(s"$t found at $i")
      case InsertionPoint(i) => println(s"$t not found, would go at $i")

  println(mergeSort(List(38, 27, 43, 3, 9, 82, 10)))
Outputcompiled & run with real Scala
9 found at 2
10 not found, would go at 3
1 not found, would go at 0
40 not found, would go at 6
List(3, 9, 10, 27, 38, 43, 82)

With duplicates, search reports a matching index, not necessarily the first one (here 2, not 1). If you need the first, write a lower-bound search yourself.

At work you call the library: sorted (by the element's Ordering), sortBy (by a key, and tuples sort field by field), and sortWith (a less-than function). All three are stable, so equal elements keep their original order. Ordering.by(...).reverse and orElseBy build multi-key orderings you can reuse.

scalaMain.scala
case class Person(name: String, dept: String, salary: Int)

@main def run(): Unit =
  val people = List(
    Person("Ravi", "data", 90), Person("Ana", "web", 75),
    Person("Chen", "data", 120), Person("Bola", "web", 75),
  )

  // dept ascending, then salary descending
  val sorted = people.sortBy(p => (p.dept, -p.salary))
  sorted.foreach(p => println(f"${p.name}%-5s ${p.dept}%-4s ${p.salary}"))

  val bySalary = Ordering.by[Person, Int](_.salary).reverse.orElseBy(_.name)
  println(people.sorted(using bySalary).map(_.name))
Outputcompiled & run with real Scala
Chen  data 120
Ravi  data 90
Ana   web  75
Bola  web  75
List(Chen, Ravi, Ana, Bola)

Ana and Bola tie on department and salary; the stable sort keeps them in their original order.

11

Dynamic programming

Dynamic programming is recursion that remembers: solve each overlapping sub-problem once and store the answer, top-down (the memo map above) or bottom-up (fill a table from the smallest case). The hard part is always saying, in one sentence, what dp(i) means. Bottom-up DP in Scala usually uses an Array for speed, or a foldLeft when only the previous row matters.

scalaMain.scala
// dp(a) = fewest coins that make amount a
def minCoins(coins: List[Int], amount: Int): Int =
  val inf = Int.MaxValue
  val dp = Array.fill(amount + 1)(inf)
  dp(0) = 0
  for a <- 1 to amount; c <- coins if c <= a && dp(a - c) != inf do
    dp(a) = math.min(dp(a), dp(a - c) + 1)
  if dp(amount) == inf then -1 else dp(amount)

// longest common subsequence, one row at a time
def lcs(a: String, b: String): Int =
  val last = a.foldLeft(Vector.fill(b.length + 1)(0)) { (prev, ca) =>
    b.indices.foldLeft(Vector(0)) { (row, j) =>
      row :+ (if ca == b(j) then prev(j) + 1 else math.max(prev(j + 1), row(j)))
    }
  }
  last.last

@main def run(): Unit =
  println(minCoins(List(1, 5, 10, 25), 63))
  println(minCoins(List(1, 3, 4), 6))
  println(minCoins(List(5, 10), 3))
  println(lcs("ABCBDAB", "BDCABA"))
  println(lcs("kitten", "sitting"))
Outputcompiled & run with real Scala
6
2
-1
4
4

For coins 1, 3, 4 and amount 6, "take the biggest coin" gives 4+1+1 (three coins); DP finds 3+3 (two). The LCS fold keeps only the previous row, so memory is O(len(b)) instead of a full table.

Your turn

Write climbStairs(n: Int): Long (1 or 2 steps at a time) with a fold over (Long, Long) pairs. climbStairs(10) is 89.

12

Cheat sheet: which Scala collection for which job

You need…UseCost
Process a sequence front to back, recursion, pattern matchingListO(1) prepend and head
Index, append, update, immutableVectoreffectively O(1)
Tight loops, local mutationArray or mutable.ArrayBufferO(1) index
Lookup by keyMapeffectively O(1)
"Have I seen this?"Seteffectively O(1)
Keep keys sorted, range queriesTreeMap, TreeSetO(log n)
A stackList (:: and pattern matching)O(1)
A queueimmutable.Queue or mutable.QueueO(1) amortised
Largest / smallest nextmutable.PriorityQueue (reverse the Ordering for min)O(log n)
Find in sorted datasearchO(log n) on an indexed seq
Shortest path, unweightedBFS with QueueO(V + E)
Overlapping sub-problemsDP: memo map, Array table or foldusually O(n) or O(n·m)
Big-O
How the work an algorithm does grows as the input grows, ignoring constant factors.
Structural sharing
How immutable collections make a "modified copy" cheaply: the new version reuses every part of the old one it did not change.
Algebraic data type (ADT)
A sealed type made of a fixed set of cases, such as Leaf and Node, processed with exhaustive pattern matching.
Tail recursion
Recursion where the recursive call is the last operation; @tailrec makes the compiler turn it into a loop.
Heap
A tree kept in an array where the largest (or smallest) item is always at the top.
BFS / DFS
Breadth-first search (level by level, a queue) and depth-first search (deep first, recursion or a stack).
Memoisation
Caching the result of a function call so the same sub-problem is solved only once.
Stable sort
A sort that keeps equal elements in their original order. sorted, sortBy and sortWith are stable.
Quick check

A function builds a result with var acc = List.empty[Int] and acc = acc :+ x inside a loop over 100,000 items. What is wrong?

Quick check

How do you get a min-heap from scala.collection.mutable.PriorityQueue?

Frequently asked questions

Should I use List or Vector in Scala?
Use List when you mostly work at the front: prepending, pattern matching on head and tail, and recursive processing. Use Vector when you need indexing, appending or updates, or you are not sure: its operations are effectively constant time. For tight local loops, an Array or ArrayBuffer is fastest.
Are data structures and algorithms asked in Scala interviews?
Yes. Scala coding rounds use the same problems as other languages, and interviewers also watch whether you write them idiomatically: immutable collections, pattern matching, recursion with @tailrec, and folds instead of mutable loops where it reads better. Data-engineering interviews add collection-heavy questions such as grouping and aggregation.
Does Scala have a priority queue?
Yes, scala.collection.mutable.PriorityQueue. It is a max-heap by the element's Ordering; pass a reversed Ordering for a min-heap. Printing it shows the internal heap order, so dequeue repeatedly (or use dequeueAll) to get sorted output.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.