Problem
A Fenwick Tree, or a Binary Indexed tree, is an interesting data structure that can efficiently update its elements. You can read more about it in this paper.
Here is a generic implementation of a Fenwick Tree, which could take any two reversible operations:
class FenwickTree<T> {
private let count : Int
private let neutral : T
private let forward : (T,T) -> T
private let reverse : (T,T) -> T
private var data : [T]
private var tree : [T]
init(
count: Int,
neutralElement: T,
forward: @escaping (T,T) -> T,
reverse: @escaping (T,T) -> T
) {
self.count = count
self.neutral = neutralElement
self.forward = forward
self.reverse = reverse
self.data = Array(repeating: neutralElement, count: count)
self.tree = Array(repeating: neutralElement, count: count + 1)
}
func update(index: Int, with newValue: T) {
let oldValue = data[index];
let delta = reverse(newValue, oldValue)
data[index] = newValue
var treeIndex = index + 1
while treeIndex <= count {
tree[treeIndex] = forward(tree[treeIndex], delta)
treeIndex += treeIndex & -treeIndex
}
}
func accumulated(at index: Int) -> T {
var sum = neutral
var treeIndex = index + 1
while 0 < treeIndex {
sum = forward(tree[treeIndex], sum)
treeIndex -= treeIndex & -treeIndex
}
return sum
}
func accumulated(in range: Range<Int>) -> T {
let low = range.lowerBound, high = range.upperBound - 1
let cumulatedLow = low == 0 ? neutral : accumulated(at: low - 1)
let cumulatedHigh = accumulated(at: high)
return low == high ? data[low] : reverse(cumulatedHigh,cumulatedLow)
}
}
Here are some example usecases:
let n = 10
let additiveTree = FenwickTree(count: n, neutralElement: 0, forward: +, reverse: -)
for i in 0..<n { additiveTree.update(index: i, with: i + 1) }
additiveTree.accumulated(at: 5)
additiveTree.accumulated(at: 9)
additiveTree.accumulated(in: 8..<10)
let multiplicativeTree = FenwickTree(count: n, neutralElement: 1, forward: *, reverse: /)
for i in 0..<n { multiplicativeTree.update(index: i, with: i + 1) }
multiplicativeTree.accumulated(at: 5)
multiplicativeTree.accumulated(at: 9)
multiplicativeTree.accumulated(in: 8..<10)
I am mainly concerned about the naming of functions and variables:
neutral
: This is the neutral element forforward
andreverse
forward
andreverse
should be transitive verbs since they have side effects. Preferably, the names should have the same number of letters.accumulated(at:)
is an unusual name, more familiar would befoldLeft
from Scala, orfoldl
from Haskell.accumulated(at:)
andaccumulated(in:)
have too similar names.
I can see some possible improvements:
- Performance:
accumulated(in:)
could have better time complexity at the expense of some auxiliary space; - Enforcing the reversibility of
forward
andreverse
in code;
Other areas of improvement are welcome.
Solution
Choosing the generic type (and parameters)
The Fenwick tree stores cumulative values of its elements, and is based on the ability to compute the sum and difference of those elements. You made a very general implementation, where “sum” and “difference” are provided as closure parameters. But it cannot be ensured that these are proper inverse operations of each other, and a slight variation of your multiplicativeTree
example demonstrates the problem:
let multiplicativeTree = FenwickTree(count: 4, neutralElement: 1,
forward: *, reverse: /)
multiplicativeTree.update(index: 0, with: 5)
print(multiplicativeTree.accumulated(at: 0)) // 5 OK
multiplicativeTree.update(index: 0, with: 10)
print(multiplicativeTree.accumulated(at: 0)) // 10 still OK
multiplicativeTree.update(index: 0, with: 13)
print(multiplicativeTree.accumulated(at: 0)) // 10 what?
multiplicativeTree.update(index: 0, with: 11)
print(multiplicativeTree.accumulated(at: 0)) // 0 what???
I would require the elements to conform to the AdditiveArithmetic
protocol instead. That protocol
- requires addition and subtraction, making the
forward
andreverse
parameter obsolete, - requires a
.zero
element, making theneutralElement
parameter obsolete, and - provides semantics: according to SE-0223, it “roughly corresponds to the mathematic notion of an additive group.”
AdditiveArithmetic
is adopted by all binary integer and floating point types, that probably covers most use-cases. If you really need a data structure where updating a value is done via multiplication then you can define a custom type which implements AdditiveArithmetic
accordingly.
The declaration and initialization then simplifies to
class FenwickTree<T: AdditiveArithmetic> {
private let count : Int
private var data : [T]
private var tree : [T]
init(count: Int) {
self.count = count
self.data = Array(repeating: .zero, count: count)
self.tree = Array(repeating: .zero, count: count + 1)
}
// ...
}
and sums/differences can just be computed with +
and -
, making the code even better readable.
Accessing the elements
There is a
func update(index: Int, with newValue: T) {
to set new value at an index, but no method to get a single value. I would suggest to implement the subscript
method instead, with both a getter and a setter:
subscript(index: Int) -> T {
get {
return data[index]
}
set {
// ... update `data` and `tree`
}
}
And not much more work is needed to make FenwickTree
a (mutable, random access) Collection
.
A wider range of ranges …
The accumulated(in:)
method becomes much more flexible if it takes a RangeExpression
parameter instead of a Range
:
func accumulated<R>(in range: R) -> T
where R: RangeExpression, R.Bound == Int
{
let realRange = range.relative(to: 0..<count)
let cumulatedLow = accumulated(at: realRange.lowerBound - 1)
let cumulatedHigh = accumulated(at: realRange.upperBound - 1)
return cumulatedHigh - cumulatedLow
}
Now you can call it with half-open, closed, or partial ranges:
additiveTree.accumulated(in: 1..<2))
additiveTree.accumulated(in: 1...3))
additiveTree.accumulated(in: 2...))
additiveTree.accumulated(in: ..<4))
additiveTree.accumulated(in: ...3))
The last line is equivalent to your
additiveTree.accumulated(at: 3))
which means that accumulated(at:)
is no longer needed as a public method (which solves the problem of the similar names of those methods.)
Further suggestions
The index operations
treeIndex += treeIndex & -treeIndex
treeIndex -= treeIndex & -treeIndex
look like magic to the first reader of your code. I would move that to helper methods (which the compiler inlines) and document it.
The tree[0]
element is never accessed. With slight modifications of the index calculations you can save one element in that array, i.e. initialize it as
self.tree = Array(repeating: .zero, count: count)
For example, the accumulation would be
func accumulated(at index: Int) -> T {
guard index >= 0 else { return .zero }
var sum = tree[0]
var treeIndex = index
while treeIndex > 0 {
sum += tree[treeIndex]
treeIndex -= treeIndex & -treeIndex
}
return sum
}
Finally, a single element can be retrieved from the tree
array alone, by computing the difference of two tree nodes (this is also described in the Fenwick article). Therefore, at the cost of slightly more computation, you can get rid of the self.data
array and save memory.