diff --git a/.github/run.sh b/.github/run.sh index 60062fa..4079db4 100755 --- a/.github/run.sh +++ b/.github/run.sh @@ -35,7 +35,7 @@ if (( no_test == 0 )); then else green='\033[1;32m' no_color='\033[0m' - printf "Running tests in the package: %b%s%b\n" "$green" "$1" "$no_color" + printf "Running tests in packages matching: %b%s*%b\n" "$green" "$1" "$no_color" sbt -warn "Test / testOnly $1*" fi fi diff --git a/README.md b/README.md index 60f8cb5..6996fd5 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,15 @@ P48 (**) Truth tables for logical expressions (3). P54 Omitted; our tree representation will only allow well-formed trees. -P55 (**) Construct completely balanced binary trees. +[P55](src/main/scala/bintree/P55.scala) (**) Construct completely balanced binary trees. -P56 (**) Symmetric binary trees. +[P56](src/main/scala/bintree/P56.scala) (**) Symmetric binary trees. -P57 (**) Binary search trees (dictionaries). +[P57](src/main/scala/bintree/P57.scala) (**) Binary search trees (dictionaries). -P58 (**) Generate-and-test paradigm. +[P58](src/main/scala/bintree/P58.scala) (**) Generate-and-test paradigm. -P59 (**) Construct height-balanced binary trees. +[P59](src/main/scala/bintree/P59.scala) (**) Construct height-balanced binary trees. P60 (**) Construct height-balanced binary trees with a given number of nodes. diff --git a/src/main/scala/bintree/P55.scala b/src/main/scala/bintree/P55.scala new file mode 100644 index 0000000..2d48b49 --- /dev/null +++ b/src/main/scala/bintree/P55.scala @@ -0,0 +1,45 @@ +package bintree + +import Tree.* + +// P55 (**) Construct completely balanced binary trees. +// In a completely balanced binary tree, the following property holds for +// every node: The number of nodes in its left subtree and the number of +// nodes in its right subtree are almost equal, which means their difference +// is not greater than one. +// +// Define an object named Tree. Write a function Tree.cBalanced to +// construct completely balanced binary trees for a given number of nodes. +// The function should generate all solutions. The function should take as +// parameters the number of nodes and a single value to put in all of them. +// +// scala> Tree.cBalanced(4, "x") +// res0: List(Node[String]) = List(T(x T(x . .) T(x . T(x . .))), T(x T(x . .) T(x T(x . .) .)), ... + +object P55: + private def build[A](n: Int, x: A): List[(Tree[A], Int)] = + if n == 0 + then Nil + else if n == 1 + then List((singleton(x), 1)) + else if n == 2 + then + List( + (Node(x, singleton(x), Empty), 2), + (Node(x, Empty, singleton(x)), 2) + ) + else + val k = (n - 1) / 2 + for + (l, i) <- build(k, x) + (r, j) <- build(n - k - 1, x) + h = i + j + 1 + xs = + if math.abs(i - j) == 1 + then List((Node(x, r, l), h)) + else Nil + y <- (Node(x, l, r), h) :: xs + yield y + + def cBalanced[A](n: Int, x: A): List[Tree[A]] = + build(n, x).map(_._1) diff --git a/src/main/scala/bintree/P56.scala b/src/main/scala/bintree/P56.scala new file mode 100644 index 0000000..2ede492 --- /dev/null +++ b/src/main/scala/bintree/P56.scala @@ -0,0 +1,29 @@ +package bintree + +import Tree.* + +// P56 (**) Symmetric binary trees. +// Let us call a binary tree symmetric if you can draw a vertical line +// through the root node and then the right subtree is the mirror image of +// the left subtree. Add an isSymmetric method to the Tree class to check +// whether a given binary tree is symmetric. Hint: Write an isMirrorOf +// method first to check whether one tree is the mirror image of another. +// We are only interested in the structure, not in the contents of the +// nodes. +// +// scala> Node('a', Node('b'), Node('c')).isSymmetric +// res0: Boolean = true + +object P56: + private def isMirror[A](t1: Tree[A], t2: Tree[A]): Boolean = + (t1, t2) match + case (Empty, Empty) => true + case (Empty, _) => false + case (_, Empty) => false + case (Node(_, l1, r1), Node(_, l2, r2)) => + isMirror(l1, r2) && isMirror(r1, l2) + + extension [A](t: Tree[A]) + def isSymmetric: Boolean = t match + case Empty => true + case Node(_, l, r) => isMirror(l, r) diff --git a/src/main/scala/bintree/P57.scala b/src/main/scala/bintree/P57.scala new file mode 100644 index 0000000..c03dfc2 --- /dev/null +++ b/src/main/scala/bintree/P57.scala @@ -0,0 +1,52 @@ +package bintree + +import Tree.* +import math.Ordered.orderingToOrdered + +// P57 (**) Binary search trees (dictionaries). +// Write a function to add an element to a binary search tree. +// +// scala> End.addValue(2) +// res0: Node[Int] = T(2 . .) +// +// scala> res0.addValue(3) +// res1: Node[Int] = T(2 . T(3 . .)) +// +// scala> res1.addValue(0) +// res2: Node[Int] = T(2 T(0 . .) T(3 . .)) +// +// Hint: The abstract definition of addValue in Tree should be +// `def addValue[U >: T <% Ordered[U]](x: U): Tree[U]`. The `>: T` is +// because addValue's parameters need to be _contravariant_ in T. +// (Conceptually, we're adding nodes above existing nodes. In order for the +// subnodes to be of type T or any subtype, the upper nodes must be of type +// T or any supertype.) The `<% Ordered[U]` allows us to use the < operator +// on the values in the tree. +// +// Use that function to construct a binary tree from a list of integers. +// +// scala> Tree.fromList(List(3, 2, 5, 7, 1)) +// res3: Node[Int] = T(3 T(2 T(1 . .) .) T(5 . T(7 . .))) +// +// Finally, use that function to test your solution to P56. +// +// scala> Tree.fromList(List(5, 3, 18, 1, 4, 12, 21)).isSymmetric +// res4: Boolean = true +// +// scala> Tree.fromList(List(3, 2, 5, 7, 4)).isSymmetric +// res5: Boolean = false + +object P57: + extension [A](t: Tree[A]) + // "covariant type A occurs in contravariant position". + // If we allow any subtype of A, then a list of cats + // may contain a dog! + // https://stackoverflow.com/a/43180701/839733 + def addValue[B >: A](x: B)(using Ordering[B]): Tree[B] = t match + case Empty => singleton(x) + case Node(y, l, r) if x < y => + val left = l.addValue(x) + Node(y, left, r) + case Node(y, l, r) => + val right = r.addValue(x) + Node(y, l, right) diff --git a/src/main/scala/bintree/P58.scala b/src/main/scala/bintree/P58.scala new file mode 100644 index 0000000..976be8e --- /dev/null +++ b/src/main/scala/bintree/P58.scala @@ -0,0 +1,14 @@ +package bintree + +import P56.* + +// P58 (**) Generate-and-test paradigm. +// Apply the generate-and-test paradigm to construct all symmetric, +// completely balanced binary trees with a given number of nodes. +// +// scala> Tree.symmetricBalancedTrees(5, "x") +// res0: List[Node[String]] = List(T(x T(x . T(x . .)) T(x T(x . .) .)), T(x T(x T(x . .) .) T(x . T(x . .)))) + +object P58: + def symmetricBalancedTrees[A](n: Int, x: A): List[Tree[A]] = + P55.cBalanced(n, x).filter(_.isSymmetric) diff --git a/src/main/scala/bintree/P59.scala b/src/main/scala/bintree/P59.scala new file mode 100644 index 0000000..42f6807 --- /dev/null +++ b/src/main/scala/bintree/P59.scala @@ -0,0 +1,39 @@ +package bintree + +import Tree.* + +// P59 (**) Construct height-balanced binary trees. +// In a height-balanced binary tree, the following property holds for every +// node: The height of its left subtree and the height of its right subtree +// are almost equal, which means their difference is not greater than one. +// +// Write a method Tree.hbalTrees to construct height-balanced binary trees +// for a given height with a supplied value for the nodes. The function +// should generate all solutions. +// +// scala> Tree.hbalTrees(3, "x") +// res0: List[Node[String]] = List(T(x T(x T(x . .) T(x . .)) T(x T(x . .) T(x . .))), T(x T(x T(x . .) T(x . .)) T(x T(x . .) .)), ... + +object P59: + def hbalTrees[A](h: Int, x: A): List[Tree[A]] = + if h == 0 + then Nil + else if h == 1 + then List(singleton(x)) + else if h == 2 + then + List( + Node(x, singleton(x), Empty), + Node(x, Empty, singleton(x)), + Node(x, singleton(x), singleton(x)) + ) + else + for + a <- hbalTrees(h - 1, x) + b <- hbalTrees(h - 2, x) + c <- List( + Node(x, a, b), + Node(x, b, a), + Node(x, a, a) + ) + yield c diff --git a/src/main/scala/bintree/Tree.scala b/src/main/scala/bintree/Tree.scala new file mode 100644 index 0000000..5fca7ac --- /dev/null +++ b/src/main/scala/bintree/Tree.scala @@ -0,0 +1,11 @@ +package bintree + +enum Tree[+A]: + case Empty + case Node(value: A, left: Tree[A], right: Tree[A]) + +object Tree: + def empty[A]: Tree[A] = Empty + + def singleton[A](x: A): Tree[A] = + Node(x, Empty, Empty) diff --git a/src/main/scala/list/P27.scala b/src/main/scala/list/P27.scala index 7301e7b..8b1c3f0 100644 --- a/src/main/scala/list/P27.scala +++ b/src/main/scala/list/P27.scala @@ -35,5 +35,5 @@ object P27: case n :: next => for xs <- P26.combinations(n, l) - xxs <- group(next, l.filterNot(x => xs.exists(_ == x))) + xxs <- group(next, l.filterNot(xs.contains)) yield xs :: xxs diff --git a/src/test/scala/bintree/P55Suite.scala b/src/test/scala/bintree/P55Suite.scala new file mode 100644 index 0000000..92bbabc --- /dev/null +++ b/src/test/scala/bintree/P55Suite.scala @@ -0,0 +1,29 @@ +package bintree + +import munit.FunSuite +import Tree.* + +class P55Suite extends FunSuite: + test("construct completely balanced binary trees"): + val data = List( + (0, Nil), + (1, List(singleton('x'))), + (2, List(Node('x', singleton('x'), Empty), Node('x', Empty, singleton('x')))), + (3, (List(Node('x', singleton('x'), singleton('x'))))), + ( + 4, + List( + Node('x', singleton('x'), Node('x', singleton('x'), Empty)), + Node('x', Node('x', singleton('x'), Empty), singleton('x')), + Node('x', singleton('x'), Node('x', Empty, singleton('x'))), + Node('x', Node('x', Empty, singleton('x')), singleton('x')) + ) + ) + ) + data.foreach { (n, expected) => + val obtained = P55.cBalanced(n, 'x') + assertEquals(obtained.size, expected.size) + obtained.foreach { tree => + assert(expected.contains(tree), s"Tree: $tree") + } + } diff --git a/src/test/scala/bintree/P56Suite.scala b/src/test/scala/bintree/P56Suite.scala new file mode 100644 index 0000000..a3715ad --- /dev/null +++ b/src/test/scala/bintree/P56Suite.scala @@ -0,0 +1,13 @@ +package bintree + +import munit.FunSuite +import Tree.* +import P56.* + +class P56Suite extends FunSuite: + test("check whether a given binary tree is symmetric"): + val obtained = Node('a', singleton('b'), Empty).isSymmetric + assertEquals(obtained, false) + + val obtained2 = Node('a', singleton('b'), singleton('c')).isSymmetric + assertEquals(obtained2, true) diff --git a/src/test/scala/bintree/P57Suite.scala b/src/test/scala/bintree/P57Suite.scala new file mode 100644 index 0000000..b717403 --- /dev/null +++ b/src/test/scala/bintree/P57Suite.scala @@ -0,0 +1,16 @@ +package bintree + +import munit.FunSuite +import Tree.* +import P56.* +import P57.* + +class P57Suite extends FunSuite: + test("add an element to a BST"): + val xs = List(3, 2, 5, 7, 1) + val obtained = xs.foldLeft(Tree.empty[Int])((t, x) => t.addValue(x)) + assertEquals(obtained.isSymmetric, true) + + val ys = List(3, 2, 5, 7, 4) + val obtained2 = ys.foldLeft(Tree.empty[Int])((t, x) => t.addValue(x)) + assertEquals(obtained2.isSymmetric, false) diff --git a/src/test/scala/bintree/P58Suite.scala b/src/test/scala/bintree/P58Suite.scala new file mode 100644 index 0000000..9cbb8c2 --- /dev/null +++ b/src/test/scala/bintree/P58Suite.scala @@ -0,0 +1,16 @@ +package bintree + +import munit.FunSuite +import Tree.* + +class P58Suite extends FunSuite: + test("construct all symmetric, completely balanced binary trees with a given number of nodes"): + val obtained = P58.symmetricBalancedTrees(5, 'x') + val expected = List( + Node('x', Node('x', Empty, singleton('x')), Node('x', singleton('x'), Empty)), + Node('x', Node('x', singleton('x'), Empty), Node('x', Empty, singleton('x'))) + ) + assertEquals(obtained.size, expected.size) + obtained.foreach { tree => + assert(expected.contains(tree), s"Tree: $tree") + } diff --git a/src/test/scala/bintree/P59Suite.scala b/src/test/scala/bintree/P59Suite.scala new file mode 100644 index 0000000..6171e6d --- /dev/null +++ b/src/test/scala/bintree/P59Suite.scala @@ -0,0 +1,17 @@ +package bintree + +import munit.FunSuite +import Tree.* + +class P59Suite extends FunSuite: + test("construct height-balanced binary trees"): + val obtained = P59.hbalTrees(3, 'x') + val expected = List( + Node('x', Node('x', Empty, Empty), Node('x', Empty, singleton('x'))), + Node('x', Node('x', Empty, Empty), Node('x', singleton('x'), Empty)), + Node('x', Node('x', Empty, Empty), Node('x', singleton('x'), singleton('x'))), + Node('x', Node('x', Empty, singleton('x')), Node('x', Empty, Empty)) + ) + expected.foreach { tree => + assert(obtained.contains(tree), s"Tree: $tree") + } diff --git a/src/test/scala/list/P23Suite.scala b/src/test/scala/list/P23Suite.scala index 8ec5ef5..faaa6cd 100644 --- a/src/test/scala/list/P23Suite.scala +++ b/src/test/scala/list/P23Suite.scala @@ -12,12 +12,12 @@ class P23Suite extends ScalaCheckSuite: val k = lst.size val n = Random.nextInt(k + 1) val obtained = P23.randomSelect(n, lst) - assert(obtained.size == n && obtained.forall(x => lst.exists(_ == x))) + assert(obtained.size == n && obtained.forall(lst.contains)) } property("extract n random elements from a list where n >>> list.size"): forAllNoShrink(Gen.listOf(ListGen.genInt)) { (lst: List[Int]) => val k = lst.size val obtained = P23.randomSelect(Int.MaxValue, lst) - assert(obtained.size == k && obtained.forall(x => lst.exists(_ == x))) + assert(obtained.size == k && obtained.forall(lst.contains)) }