Skip to content

Commit

Permalink
Complete Multiway Trees
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Jan 16, 2025
1 parent 8d6d635 commit 8620c78
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 6 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ P66 (***) Layout a binary tree (3).

P70B Omitted; we can only create well-formed trees.

P70C (*) Count the nodes of a multiway tree.
[P70C](mtree/src/P70C.scala) (*) Count the nodes of a multiway tree.

P70 (**) Tree construction from a node string.
[P70](mtree/src/P70.scala) (**) Tree construction from a node string.

P71 (*) Determine the internal path length of a tree.
[P71](mtree/src/P71.scala) (*) Determine the internal path length of a tree.

P72 (*) Construct the postorder sequence of the tree nodes.
[P72](mtree/src/P72.scala) (*) Construct the postorder sequence of the tree nodes.

P73 (**) Lisp-like tree representation.
[P73](mtree/src/P73.scala) (**) Lisp-like tree representation.

## Graphs

Expand Down
2 changes: 1 addition & 1 deletion bintree/src/DList.scala → mtree/src/DList.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package bintree
package mtree

final case class DList[A](run: List[A] => List[A]):
// O(1)
Expand Down
5 changes: 5 additions & 0 deletions mtree/src/MTree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mtree

final case class MTree[+T](value: T, children: Seq[MTree[T]] = Nil):
override def toString: String =
s"M(${value.toString} {${children.map(_.toString).mkString(",")}})"
51 changes: 51 additions & 0 deletions mtree/src/P70.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package mtree

// P70 (**) Tree construction from a node string.
//
// We suppose that the nodes of a multiway tree contain single characters.
// In the depth-first order sequence of its nodes, a special character ^
// has been inserted whenever, during the tree traversal, the move is a
// backtrack to the previous level.
//
// By this rule, the tree in the figure opposite is represented as:
//
// afg^^c^bd^e^^^
// Define the syntax of the string and write a function string2MTree to construct
// an MTree from a String.  Make the function an implicit conversion from String.
// Write the reverse function, and make it the toString method of MTree.
//
// scala> MTree('a', List(MTree('f', List(MTree('g'))), MTree('c'), MTree('b', List(MTree('d'), MTree('e'))))).toString
// res0: String = afg^^c^bd^e^^^
object P70:
def string2MTree(s: String): MTree[Char] =
/*
First recursive dfs call collects the children,
second recursive dfs call collects the siblings.
x=g, xs=^^c^bd^e^^^, ys=^c^bd^e^^^, zs=c^bd^e^^^
x=e, xs=^^^, ys=^^, zs=^
x=d, xs=^e^^^, ys=e^^^, zs=^
x=b, xs=d^e^^^, ys=^, zs=
x=c, xs=^bd^e^^^, ys=bd^e^^^, zs=
x=f, xs=g^^c^bd^e^^^, ys=c^bd^e^^^, zs=
x=a, xs=fg^^c^bd^e^^^, ys=, zs=
*/
def loop(s: String): (List[MTree[Char]], String) =
s match
case "" => (Nil, "")
case s"^$xs" => (Nil, xs)
case _ =>
val (x, xs) = (s.head, s.tail)
val (children, ys) = loop(xs)
val (siblings, zs) = loop(ys)
(MTree(x, children) :: siblings, zs)

loop(s)._1.head

def tree2String(t: MTree[Char]): String =
def loop(acc: DList[Char], t: MTree[Char]): DList[Char] =
val xs = t.children.foldLeft(DList.empty[Char])(loop)
val ys = DList.singleton(t.value) ++ xs ++ DList.singleton('^')
acc ++ ys

loop(DList.empty[Char], t).toList.mkString
10 changes: 10 additions & 0 deletions mtree/src/P70C.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package mtree

// P70C (*) Count the nodes of a multiway tree.
// Write a method nodeCount which counts the nodes of a given multiway tree.
//
// scala> MTree('a', List(MTree('f'))).nodeCount
// res0: Int = 2
object P70C:
extension [A](t: MTree[A])
def nodeCount: Int = 1 + t.children.foldLeft(0)((acc, c) => acc + c.nodeCount)
25 changes: 25 additions & 0 deletions mtree/src/P71.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mtree

// P71 (*) Determine the internal path length of a tree.
// We define the internal path length of a multiway tree as the
// total sum of the path lengths from the root to all nodes of the tree.
// By this definition, the tree in the figure of problem P70 has an
// internal path length of 9.  Write a method internalPathLength to return that sum.
//
// scala> "afg^^c^bd^e^^^".internalPathLength
// res0: Int = 9
/*
ANSWER: We observe that the path length is equal to the number of
nodes in a path from the root to a leaf, with a node counted only
once. So, path length of abd = 3, but abd + abe is 4, not 6.
The catch is to pass the _same_ accoumulated value to all the
children of a node.
*/
object P71:
private def loop[A](acc: Int, t: MTree[A]): Int =
acc + t.children.map(loop(acc + 1, _)).sum

extension [A](t: MTree[A])
def internalPathLength: Int =
loop(0, t)
16 changes: 16 additions & 0 deletions mtree/src/P72.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mtree

// P72 (*) Construct the postorder sequence of the tree nodes.
//
// Write a method postorder which constructs the postorder sequence
// of the nodes of a multiway tree.  The result should be a List.
//
// scala> "afg^^c^bd^e^^^".postorder
// res0: List[Char] = List(g, f, c, d, e, b, a)
object P72:
private def loop[A](acc: List[A], t: MTree[A]): List[A] =
t.children.foldRight(t.value :: acc)((tree, xs) => loop(xs, tree))

extension [A](t: MTree[A])
def postorder: List[A] =
loop(Nil, t)
57 changes: 57 additions & 0 deletions mtree/src/P73.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package mtree

// P73 (**) Lisp-like tree representation.
// There is a particular notation for multiway trees in Lisp.
// Lisp is a prominent functional programming language.
// In Lisp almost everything is a list.
//
// Our example tree would be represented in Lisp as (a (f g) c (b d e)).
// The following pictures give some more examples.
//
// Note that in the "lispy" notation a node with successors (children) in
// the tree is always the first element in a list, followed by its children.
// The "lispy" representation of a multiway tree is a sequence of atoms and
// parentheses '(' and ')', with the atoms separated by spaces.
// We can represent this syntax as a Scala String.
// Write a method lispyTree which constructs a "lispy string" from an MTree.
//
// scala> MTree("a", List(MTree("b", List(MTree("c"))))).lispyTree
// res0: String = (a (b c))
//
// As a second, even more interesting, exercise try to write a method that
// takes a "lispy" string and turns it into a multiway tree.
object P73:
private def loop(s: String, i: Int): (MTree[Char], Int) =
if i >= s.length() then throw StringIndexOutOfBoundsException(s"String index out of range: $i")
else if s(i).isLetter then (MTree(s(i)), i + 1)
else if s(i) == '(' then
val x = s(i + 1)
val xs = IndexedSeq.unfold(i + 2)(j =>
Option.when(j < s.length() && s(j) != ')') {
val x = loop(s, j)
((x, x._2))
}
)
(MTree(x, xs.map(_._1)), xs.last._2 + 1)
else loop(s, i + 1)

def lispyString2Tree(s: String): MTree[Char] =
loop(s, 0)._1

extension [A](t: MTree[Char])
def lispyTree: String =
def loop(acc: DList[Char], t: MTree[Char]): DList[Char] =
if t.children.isEmpty then
acc
++ DList.singleton(' ')
++ DList.singleton(t.value)
else
val xs = t.children.foldLeft(DList.empty[Char])(loop)
acc
++ DList.singleton(' ')
++ DList.singleton('(')
++ DList.singleton(t.value)
++ xs
++ DList.singleton(')')

loop(DList.empty[Char], t).toList.tail.mkString
3 changes: 3 additions & 0 deletions mtree/src/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package mtree

given Conversion[String, MTree[Char]] = s => P70.string2MTree(s)
9 changes: 9 additions & 0 deletions mtree/test/src/P70CSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mtree

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import P70C.nodeCount

class P70CSpec extends AnyFunSpec:
it("count the nodes of a multiway tree"):
MTree('a', List(MTree('f'))).nodeCount shouldBe 2
15 changes: 15 additions & 0 deletions mtree/test/src/P70Spec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mtree

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class P70Spec extends AnyFunSpec:
val tree = MTree(
'a',
List(MTree('f', List(MTree('g'))), MTree('c'), MTree('b', List(MTree('d'), MTree('e'))))
)
it("tree construction from a node string"):
P70.string2MTree("afg^^c^bd^e^^^") shouldBe tree

it("tree to string"):
P70.tree2String(tree) shouldBe "afg^^c^bd^e^^^"
10 changes: 10 additions & 0 deletions mtree/test/src/P71Spec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package mtree

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import mtree.P71.internalPathLength
import scala.language.implicitConversions

class P71Spec extends AnyFunSpec:
it("internal path length of a tree"):
"afg^^c^bd^e^^^".internalPathLength shouldBe 9
10 changes: 10 additions & 0 deletions mtree/test/src/P72Spec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package mtree

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import mtree.P72.postorder
import scala.language.implicitConversions

class P72Spec extends AnyFunSpec:
it("postorder sequence of the tree nodes"):
"afg^^c^bd^e^^^".postorder shouldBe List('g', 'f', 'c', 'd', 'e', 'b', 'a')
21 changes: 21 additions & 0 deletions mtree/test/src/P73Spec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package mtree

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import mtree.P73.lispyTree
import scala.language.implicitConversions

class P73Spec extends AnyFunSpec:
it("lisp-like tree representation"):
val data = List(
("a^", "a"),
("ab^^", "(a b)"),
("abc^^^", "(a (b c))"),
("bd^e^^", "(b d e)"),
("afg^^c^bd^e^^^", "(a (f g) c (b d e))")
)

data.foreach { (s, expected) =>
s.lispyTree shouldBe expected
P73.lispyString2Tree(expected) shouldBe P70.string2MTree(s)
}

0 comments on commit 8620c78

Please sign in to comment.