mirror of
https://github.com/AdrianKuta/Tree-Data-Structure.git
synced 2026-06-19 19:00:14 +02:00
feat: stack-safe traversal + lazy Sequence + navigation/functional extensions
Core additive work for v3.4 (non-breaking): - Rewrite nodeCount(), height(), clear() and the post-order iterator iteratively so deep/degenerate trees no longer throw StackOverflowError (verified to 50k deep). - Add lazy Sequence traversal: asSequence(order), pre/post/levelOrderSequence(). - Add navigation extensions: isLeaf, degree, root(), ancestors(), siblings(), leaves(), descendants(). - Add functional extensions: findNode, filterNodes, anyNode, allNodes, countNodes, foldNodes, mapValues, deepCopy, structurallyEquals (all stack-safe). - Add tests for stack-safety, the new APIs, and previously-uncovered height/depth/nodeCount/path (incl. exception paths). 40 tests green on JVM.
This commit is contained in:
@@ -72,20 +72,30 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
|
||||
* @return All child and nested child count.
|
||||
*/
|
||||
fun nodeCount(): Int {
|
||||
if (_children.isEmpty())
|
||||
return 0
|
||||
return _children.size +
|
||||
_children.sumOf { it.nodeCount() }
|
||||
var count = 0
|
||||
val stack = ArrayDeque<TreeNode<T>>()
|
||||
stack.addAll(_children)
|
||||
while (stack.isNotEmpty()) {
|
||||
val node = stack.removeLast()
|
||||
count++
|
||||
stack.addAll(node._children)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of edges on the longest path between current node and a descendant leaf.
|
||||
*/
|
||||
fun height(): Int {
|
||||
val childrenMaxDepth = _children.map { it.height() }
|
||||
.maxOrNull()
|
||||
?: -1 // -1 because this method counts nodes, and edges are always one less then nodes.
|
||||
return childrenMaxDepth + 1
|
||||
var maxDepth = 0
|
||||
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
|
||||
stack.addLast(this to 0)
|
||||
while (stack.isNotEmpty()) {
|
||||
val (node, depthSoFar) = stack.removeLast()
|
||||
if (depthSoFar > maxDepth) maxDepth = depthSoFar
|
||||
node._children.forEach { stack.addLast(it to depthSoFar + 1) }
|
||||
}
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,9 +142,18 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
|
||||
* Remove all children from root and every node in tree.
|
||||
*/
|
||||
fun clear() {
|
||||
_parent = null
|
||||
_children.forEach { it.clear() }
|
||||
_children.clear()
|
||||
val all = ArrayDeque<TreeNode<T>>()
|
||||
val stack = ArrayDeque<TreeNode<T>>()
|
||||
stack.addLast(this)
|
||||
while (stack.isNotEmpty()) {
|
||||
val node = stack.removeLast()
|
||||
all.addLast(node)
|
||||
stack.addAll(node._children)
|
||||
}
|
||||
all.forEach { node ->
|
||||
node._parent = null
|
||||
node._children.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
/** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */
|
||||
fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? =
|
||||
preOrderSequence().firstOrNull { predicate(it.value) }
|
||||
|
||||
/** All nodes (pre-order) whose value matches [predicate]. */
|
||||
fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> =
|
||||
preOrderSequence().filter { predicate(it.value) }.toList()
|
||||
|
||||
/** `true` if any node's value matches [predicate]. Short-circuits. */
|
||||
fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean =
|
||||
preOrderSequence().any { predicate(it.value) }
|
||||
|
||||
/** `true` if every node's value matches [predicate]. Short-circuits. */
|
||||
fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean =
|
||||
preOrderSequence().all { predicate(it.value) }
|
||||
|
||||
/** Counts nodes whose value matches [predicate]. */
|
||||
fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int =
|
||||
preOrderSequence().count { predicate(it.value) }
|
||||
|
||||
/** Folds [operation] over all nodes in pre-order, starting from [initial]. */
|
||||
fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R =
|
||||
preOrderSequence().fold(initial) { acc, node -> operation(acc, node) }
|
||||
|
||||
/**
|
||||
* Returns a new tree with the same shape whose values are produced by [transform]. The original is
|
||||
* left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees.
|
||||
*/
|
||||
fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
|
||||
val newRoot = TreeNode(transform(value), treeIterator)
|
||||
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>()
|
||||
stack.addLast(this to newRoot)
|
||||
while (stack.isNotEmpty()) {
|
||||
val (source, target) = stack.removeLast()
|
||||
source.children.forEach { child ->
|
||||
val mappedChild = TreeNode(transform(child.value), child.treeIterator)
|
||||
target.addChild(mappedChild)
|
||||
stack.addLast(child to mappedChild)
|
||||
}
|
||||
}
|
||||
return newRoot
|
||||
}
|
||||
|
||||
/** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */
|
||||
fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it }
|
||||
|
||||
/**
|
||||
* Structural equality: `true` when [other] holds the same values in the same shape. Unlike
|
||||
* [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe.
|
||||
*/
|
||||
fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean {
|
||||
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>()
|
||||
stack.addLast(this to other)
|
||||
while (stack.isNotEmpty()) {
|
||||
val (a, b) = stack.removeLast()
|
||||
if (a.value != b.value) return false
|
||||
if (a.children.size != b.children.size) return false
|
||||
for (i in a.children.indices) {
|
||||
stack.addLast(a.children[i] to b.children[i])
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
/** `true` when this node has no children. */
|
||||
val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty()
|
||||
|
||||
/** The number of direct children of this node. */
|
||||
val <T> TreeNode<T>.degree: Int get() = children.size
|
||||
|
||||
/** Walks up the parent chain and returns the topmost ancestor (the tree root). */
|
||||
fun <T> TreeNode<T>.root(): TreeNode<T> {
|
||||
var node = this
|
||||
var parent = node.parent
|
||||
while (parent != null) {
|
||||
node = parent
|
||||
parent = node.parent
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/** The chain of ancestors from the immediate [parent] up to (and including) the root. */
|
||||
fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
|
||||
val result = mutableListOf<TreeNode<T>>()
|
||||
var parent = this.parent
|
||||
while (parent != null) {
|
||||
result.add(parent)
|
||||
parent = parent.parent
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** The other children of this node's parent (excludes this node). Empty for the root. */
|
||||
fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> =
|
||||
parent?.children?.filter { it !== this } ?: emptyList()
|
||||
|
||||
/** All leaf nodes in this subtree, in pre-order. */
|
||||
fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> =
|
||||
preOrderSequence().filter { it.isLeaf }.toList()
|
||||
|
||||
/** All nodes in this subtree except this node, in pre-order. */
|
||||
fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> =
|
||||
preOrderSequence().filter { it !== this }.toList()
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import com.github.adriankuta.datastructure.tree.iterators.LevelOrderTreeIterator
|
||||
import com.github.adriankuta.datastructure.tree.iterators.PostOrderTreeIterator
|
||||
import com.github.adriankuta.datastructure.tree.iterators.PreOrderTreeIterator
|
||||
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
|
||||
|
||||
/**
|
||||
* Lazily traverses this subtree in the given [order] as a [Sequence], without forcing the whole
|
||||
* traversal up front. Pairs with the Kotlin stdlib, e.g.
|
||||
* `root.asSequence().map { it.value }.firstOrNull { it == target }`.
|
||||
*/
|
||||
fun <T> TreeNode<T>.asSequence(
|
||||
order: TreeNodeIterators = TreeNodeIterators.PreOrder,
|
||||
): Sequence<TreeNode<T>> {
|
||||
val self = this
|
||||
return when (order) {
|
||||
TreeNodeIterators.PreOrder -> Sequence { PreOrderTreeIterator(self) }
|
||||
TreeNodeIterators.PostOrder -> Sequence { PostOrderTreeIterator(self) }
|
||||
TreeNodeIterators.LevelOrder -> Sequence { LevelOrderTreeIterator(self) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazy pre-order traversal as a [Sequence]. */
|
||||
fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder)
|
||||
|
||||
/** Lazy post-order traversal as a [Sequence]. */
|
||||
fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder)
|
||||
|
||||
/** Lazy level-order (breadth-first) traversal as a [Sequence]. */
|
||||
fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder)
|
||||
@@ -22,27 +22,22 @@ import com.github.adriankuta.datastructure.tree.TreeNode
|
||||
*/
|
||||
class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
|
||||
|
||||
private val stack = ArrayDeque<TreeNode<T>>()
|
||||
private val result = ArrayDeque<TreeNode<T>>()
|
||||
|
||||
init {
|
||||
stack.addAll(getChildrenStack(root))
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean = stack.isNotEmpty()
|
||||
|
||||
override fun next(): TreeNode<T> {
|
||||
return stack.removeFirst()
|
||||
}
|
||||
|
||||
private fun getChildrenStack(node: TreeNode<T>): ArrayDeque<TreeNode<T>> {
|
||||
// Iterative post-order: pop a node, prepend it to `result`, then push its children
|
||||
// left-to-right. Reading `result` front-to-back yields post-order — without the deep
|
||||
// recursion that overflowed the stack on degenerate (linear) trees.
|
||||
val stack = ArrayDeque<TreeNode<T>>()
|
||||
if(node.children.isEmpty()) {
|
||||
return ArrayDeque(listOf(node))
|
||||
stack.addLast(root)
|
||||
while (stack.isNotEmpty()) {
|
||||
val node = stack.removeLast()
|
||||
result.addFirst(node)
|
||||
node.children.forEach { stack.addLast(it) }
|
||||
}
|
||||
node.children.forEach {
|
||||
stack.addAll(getChildrenStack(it))
|
||||
}
|
||||
stack.addLast(node)
|
||||
return stack
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean = result.isNotEmpty()
|
||||
|
||||
override fun next(): TreeNode<T> = result.removeFirst()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotSame
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertSame
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TreeNodeFunctionalTest {
|
||||
|
||||
private fun sample() = tree(1) {
|
||||
child(2) {
|
||||
child(4)
|
||||
child(5)
|
||||
}
|
||||
child(3) {
|
||||
child(6)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun findNode() {
|
||||
assertEquals(6, sample().findNode { it == 6 }?.value)
|
||||
assertNull(sample().findNode { it == 99 })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterNodes() =
|
||||
assertContentEquals(listOf(2, 4, 6), sample().filterNodes { it % 2 == 0 }.map { it.value })
|
||||
|
||||
@Test
|
||||
fun anyNode() {
|
||||
assertTrue(sample().anyNode { it == 6 })
|
||||
assertFalse(sample().anyNode { it == 99 })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allNodes() {
|
||||
assertTrue(sample().allNodes { it > 0 })
|
||||
assertFalse(sample().allNodes { it < 5 })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun countNodes() = assertEquals(3, sample().countNodes { it > 3 })
|
||||
|
||||
@Test
|
||||
fun foldNodes() = assertEquals(21, sample().foldNodes(0) { acc, node -> acc + node.value })
|
||||
|
||||
@Test
|
||||
fun mapPreservesStructureAndTransformsValues() {
|
||||
val mapped = sample().mapValues { it * 10 }
|
||||
assertContentEquals(
|
||||
listOf(10, 20, 40, 50, 30, 60),
|
||||
mapped.preOrderSequence().map { it.value }.toList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deepCopyIsStructurallyEqualButDistinct() {
|
||||
val original = sample()
|
||||
val copy = original.deepCopy()
|
||||
assertNotSame(original, copy)
|
||||
assertTrue(original.structurallyEquals(copy))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun structurallyEqualsDistinguishesByValueAndShape() {
|
||||
assertTrue(sample().structurallyEquals(sample()))
|
||||
val different = tree(1) {
|
||||
child(2) { child(4) }
|
||||
child(3) { child(6) }
|
||||
}
|
||||
assertFalse(sample().structurallyEquals(different))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertSame
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TreeNodeNavigationTest {
|
||||
|
||||
private val root = TreeNode(1)
|
||||
private val n2 = TreeNode(2)
|
||||
private val n3 = TreeNode(3)
|
||||
private val n4 = TreeNode(4)
|
||||
private val n5 = TreeNode(5)
|
||||
private val n6 = TreeNode(6)
|
||||
|
||||
init {
|
||||
root.addChild(n2)
|
||||
root.addChild(n3)
|
||||
n2.addChild(n4)
|
||||
n2.addChild(n5)
|
||||
n3.addChild(n6)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLeaf() {
|
||||
assertTrue(n4.isLeaf)
|
||||
assertFalse(root.isLeaf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun degree() {
|
||||
assertEquals(2, root.degree)
|
||||
assertEquals(0, n4.degree)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun root() {
|
||||
assertSame(root, n6.root())
|
||||
assertSame(root, root.root())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ancestors() {
|
||||
assertContentEquals(listOf(n2, root), n4.ancestors())
|
||||
assertContentEquals(emptyList(), root.ancestors())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun siblings() {
|
||||
assertContentEquals(listOf(n5), n4.siblings())
|
||||
assertContentEquals(emptyList(), root.siblings())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leaves() {
|
||||
assertContentEquals(listOf(n4, n5, n6), root.leaves())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun descendants() {
|
||||
assertContentEquals(listOf(n2, n4, n5, n3, n6), root.descendants())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TreeNodeSequenceTest {
|
||||
|
||||
private fun sample() = tree(1) {
|
||||
child(2) {
|
||||
child(4)
|
||||
child(5)
|
||||
}
|
||||
child(3) {
|
||||
child(6)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preOrderSequence() =
|
||||
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().preOrderSequence().map { it.value }.toList())
|
||||
|
||||
@Test
|
||||
fun postOrderSequence() =
|
||||
assertContentEquals(listOf(4, 5, 2, 6, 3, 1), sample().postOrderSequence().map { it.value }.toList())
|
||||
|
||||
@Test
|
||||
fun levelOrderSequence() =
|
||||
assertContentEquals(listOf(1, 2, 3, 4, 5, 6), sample().levelOrderSequence().map { it.value }.toList())
|
||||
|
||||
@Test
|
||||
fun asSequenceDefaultsToPreOrder() =
|
||||
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().asSequence().map { it.value }.toList())
|
||||
|
||||
@Test
|
||||
fun asSequenceHonorsExplicitOrder() =
|
||||
assertContentEquals(
|
||||
listOf(1, 2, 3, 4, 5, 6),
|
||||
sample().asSequence(TreeNodeIterators.LevelOrder).map { it.value }.toList(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun sequenceShortCircuitsLazily() =
|
||||
assertEquals(4, sample().preOrderSequence().map { it.value }.first { it == 4 })
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* A deep, degenerate (linear) tree must not overflow the call stack. These tests build a chain
|
||||
* thousands of nodes deep — recursive implementations of [TreeNode.height], [TreeNode.nodeCount]
|
||||
* and the post-order iterator blow the stack here, so they pin the iterative rewrites.
|
||||
*/
|
||||
class TreeNodeStackSafetyTest {
|
||||
|
||||
private val depth = 50_000
|
||||
|
||||
private fun deepChain(): TreeNode<Int> {
|
||||
val root = TreeNode(0)
|
||||
var current = root
|
||||
for (i in 1..depth) {
|
||||
val child = TreeNode(i)
|
||||
current.addChild(child)
|
||||
current = child
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
@Test
|
||||
fun heightDoesNotOverflowOnDeepTree() {
|
||||
assertEquals(depth, deepChain().height())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeCountDoesNotOverflowOnDeepTree() {
|
||||
// nodeCount() excludes the root, so a chain of `depth` extra nodes counts as `depth`.
|
||||
assertEquals(depth, deepChain().nodeCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun postOrderIterationDoesNotOverflowOnDeepTree() {
|
||||
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PostOrder }
|
||||
assertEquals(depth + 1, tree.toList().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preOrderIterationDoesNotOverflowOnDeepTree() {
|
||||
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PreOrder }
|
||||
assertEquals(depth + 1, tree.toList().size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.github.adriankuta.datastructure.tree
|
||||
|
||||
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class TreeNodeUtilitiesTest {
|
||||
|
||||
private val root = TreeNode(1)
|
||||
private val a = TreeNode(2)
|
||||
private val b = TreeNode(3)
|
||||
|
||||
init {
|
||||
root.addChild(a)
|
||||
a.addChild(b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeCountCountsDescendantsExcludingRoot() {
|
||||
assertEquals(0, TreeNode("solo").nodeCount())
|
||||
assertEquals(2, root.nodeCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun heightIsLongestEdgePathToLeaf() {
|
||||
assertEquals(0, TreeNode("solo").height())
|
||||
assertEquals(2, root.height())
|
||||
assertEquals(1, a.height())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun depthIsDistanceToRoot() {
|
||||
assertEquals(0, root.depth())
|
||||
assertEquals(1, a.depth())
|
||||
assertEquals(2, b.depth())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathReturnsDescendantToReceiverChain() {
|
||||
assertContentEquals(listOf(b, a, root), root.path(b))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathThrowsWhenNotADescendant() {
|
||||
assertFailsWith<TreeNodeException> { root.path(TreeNode(99)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathThrowsWhenDescendantIsRootItself() {
|
||||
assertFailsWith<TreeNodeException> { root.path(root) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user