Some times you might need to iterate through multiple lists. This happens, for example, when implementing a tree traversal algorithm. Thanks to tail recursion and Kotlin, a trivial implementation can be as follows:
tailrec fun countAll(nodes: List<Node>, acc: Int): Int {
return when {
nodes.isEmpty() -> acc
else -> countAll(nodes.first().children + nodes.subList(1, nodes.size), acc + 1)
}
}
您从调用开始countAll在根节点上(例如countAll(listOf(root),1)),该函数将累计所有子项反复地直到所有的遍历和计数。
这种方法的问题在于,它将大部分时间都花在通过将操作数的所有元素复制到新列表中来产生连接结果的过程。
从火焰图可以看出,大部分时间都花在了数组列表实例和调用全部添加。
我们在此处串联列表的原因只是为了随着添加更多项而不断迭代节点集合。
Let's make it better
为了避免串联列表的开销,我们可以使用的另一种方法是利用可以跨越列表边界的迭代器。
如果我们有一个,我们可以重写我们的countAll功能如下:
tailrec fun countAll(nodes: Iterator<Node>, acc: Int): Int {
return when {
!nodes.hasNext() -> acc
else -> {
val node = nodes.next()
countAll(nodes + node.children.iterator(), acc + 1)
}
}
}
🏃♂️ Time to code!
在我们理想的实施中,加运算符将负责为子级列表存储迭代器,而不会抢先复制新列表中的所有项目。
让我们看一下迭代器的实现:
class ConcatIterator<T>(iterator: Iterator<T>) : Iterator<T> {
private val store = ArrayDeque<Iterator<T>>()
init {
if (iterator.hasNext())
store.add(iterator)
}
override fun hasNext(): Boolean = when {
store.isEmpty() -> false
else -> store.first.hasNext()
}
override fun next(): T {
val t = store.first.next()
if (!store.first.hasNext())
store.removeFirst()
return t
}
operator fun plus(iterator: Iterator<T>): ConcatIterator<T> {
if (iterator.hasNext())
store.add(iterator)
return this
}
}
As the iterators get concatenated they get enqueued in the store
. In this case, the store uses an 一种rrayDeque which is a lightweight non-concurrent implementation of a Queue that performs the majority of its operations in amortized constant time.
The last touch
最后的增加使迭代器的使用更加舒适,这是通过实现加运算符迭代器。
operator fun <T> Iterator<T>.plus(iterator: Iterator<T>): ConcatIterator<T> =
when {
this is ConcatIterator<T> -> this.plus(iterator)
iterator is ConcatIterator<T> -> iterator.plus(this)
else -> ConcatIterator(this).plus(iterator)
}
此扩展功能可帮助我们通过生成一个迭代器,轻松地将两个现有的迭代器连接成一个新的迭代器。ConcatIterator。 这个功能的好处是它可以重用现有的ConcatIterator实例(如果在两个实例中可用)。
Let the numbers speak
现在,我们已经实现了countAll函数,让我们看看它的性能。
I've tested my assumptions using a little Kotlin playground that I've created to experiment with trees. You can find it here.
以下结果来自针对具有65201277节点的树测试这两种实现的结果。
Total count (countAll without ConcatIterator): 65201277 nodes (9227 ms)
Total count (countAll with ConcatIterator): 65201277 nodes (1288 ms)
如您所见,ConcatIterator版本快了10倍。 我们不再承担连接列表的开销,因此大部分计算都花在了执行计数上。
Conclusion
希望您喜欢。 让我知道您如何解决此类问题,以及在Kotlin中是否有更好的方法来解决此问题。
干杯!