mirror of
https://github.com/Brandon-Rozek/website.git
synced 2024-11-25 01:26:30 -05:00
New Post
This commit is contained in:
parent
710eeda698
commit
1f801d38ea
1 changed files with 113 additions and 0 deletions
113
content/blog/immutable-bfs-unfold.md
Normal file
113
content/blog/immutable-bfs-unfold.md
Normal file
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
title: "Immutable Traversals with Unfold"
|
||||
date: 2022-11-12T21:27:42-05:00
|
||||
draft: false
|
||||
tags: ["Functional Programming", "Scala"]
|
||||
math: false
|
||||
---
|
||||
|
||||
Let's consider the following binary tree:
|
||||
|
||||
```goat
|
||||
a
|
||||
/ \
|
||||
/ \
|
||||
b d
|
||||
\
|
||||
\
|
||||
c
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
We can encode this with the following Scala code:
|
||||
|
||||
```scala
|
||||
final case class BinNode(
|
||||
val label: String,
|
||||
val left: Option[BinNode],
|
||||
val right: Option[BinNode]
|
||||
)
|
||||
|
||||
// Leaf Nodes
|
||||
val c_node = BinNode("c", None, None)
|
||||
val d_node = BinNode("d", None, None)
|
||||
// Rest of nodes
|
||||
val b_node = BinNode("b", None, Some(c_node))
|
||||
val a_node = BinNode("a", Some(b_node), Some(d_node))
|
||||
```
|
||||
|
||||
For depth first search, an intuitive immutable implementation would be a recursive function.
|
||||
|
||||
```scala
|
||||
// Using Preorder traversal
|
||||
def DFS(node: BinNode): Iterator[BinNode] =
|
||||
lazy val left_side = node.left.fold(Iterator.empty[BinNode])(DFS)
|
||||
lazy val right_side = node.right.fold(Iterator.empty[BinNode])(DFS)
|
||||
Iterator(node) ++ left_side ++ right_side
|
||||
```
|
||||
|
||||
Let's evaluate this using our example above:
|
||||
|
||||
```scala
|
||||
DFS(a_node).toList.map(_.label)
|
||||
// List(a, b, c, d)
|
||||
```
|
||||
|
||||
The recursive implementation inherently uses the system stack to keep track of the nodes. This means that the last element gets evaluated in each step. Otherwise called last-in-first-out (LIFO). Breadth first search, however, uses a queue based approach where the first one added to the data structure is the first one considered (FIFO).
|
||||
|
||||
To preserve immutability in our code, we can use `unfold`. Here our state is the queue of nodes.
|
||||
|
||||
```scala
|
||||
def BFS(node: BinNode): Iterator[BinNode] =
|
||||
Iterator.unfold(List(node))(q =>
|
||||
if q.isEmpty then
|
||||
None
|
||||
else
|
||||
val crnt_node = q.head
|
||||
val next_q = q.tail ++ crnt_node.left ++ crnt_node.right
|
||||
Some(crnt_node, next_q)
|
||||
)
|
||||
```
|
||||
|
||||
Evaluating on our example:
|
||||
|
||||
```scala
|
||||
BFS(a_node).toList.map(_.label)
|
||||
// List(a, b, d, c)
|
||||
```
|
||||
|
||||
We can also use `unfold` for the depth first search approach as well. We can replace the list used with a stack.
|
||||
|
||||
```scala
|
||||
import scala.collection.mutable.Stack
|
||||
def DFS2(node: BinNode): Iterator[BinNode] =
|
||||
Iterator.unfold(Stack(node))(s =>
|
||||
if s.isEmpty then
|
||||
None
|
||||
else
|
||||
val crnt_node = s.pop()
|
||||
s.pushAll(crnt_node.right)
|
||||
s.pushAll(crnt_node.left)
|
||||
Some(crnt_node, s)
|
||||
)
|
||||
```
|
||||
|
||||
Using a stack introduces some mutability. We can use the immutable list data structure instead, as long as we satisfy the LIFO ordering.
|
||||
|
||||
```scala
|
||||
def DFS3(node: BinNode): Iterator[BinNode] =
|
||||
Iterator.unfold(List(node))(s =>
|
||||
if s.isEmpty then
|
||||
None
|
||||
else
|
||||
val crnt_node = s.last
|
||||
val next_s = s.init ++ crnt_node.right ++ crnt_node.left
|
||||
Some(crnt_node, next_s)
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue