-
Notifications
You must be signed in to change notification settings - Fork 393
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: moul <[email protected]>
- Loading branch information
Showing
3 changed files
with
835 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/moul/ulist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,345 @@ | ||
// Package ulist provides an append-only list implementation using a binary tree structure, | ||
// optimized for scenarios requiring sequential inserts with auto-incrementing indices. | ||
// | ||
// The implementation uses a binary tree where new elements are added by following a path | ||
// determined by the binary representation of the index. This provides automatic balancing | ||
// for append operations without requiring any balancing logic. | ||
// | ||
// Key characteristics: | ||
// * O(log n) append and access operations | ||
// * Perfect balance for power-of-2 sizes | ||
// * No balancing needed | ||
// * Memory efficient | ||
// * Natural support for range queries | ||
|
||
package ulist | ||
|
||
// TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones). | ||
// TODO: Add MustXXX helpers. | ||
// TODO: Use this ulist in moul/collection for the primary index. | ||
// TODO: Consider adding a "compact" method that removes nil nodes. | ||
// TODO: Remove debug logging. | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
// Debug controls whether debug logging is enabled | ||
var Debug bool = false | ||
|
||
// log prints debug messages when Debug is true | ||
func log(args ...interface{}) { | ||
if Debug { | ||
println(args...) | ||
} | ||
} | ||
|
||
// Error variables | ||
var ( | ||
ErrOutOfBounds = errors.New("index out of bounds") | ||
ErrDeleted = errors.New("element already deleted") | ||
) | ||
|
||
// Entry represents a key-value pair in the list | ||
type Entry struct { | ||
Index int | ||
Value interface{} | ||
} | ||
|
||
// List represents an append-only binary tree list | ||
type List struct { | ||
root *treeNode | ||
totalSize int | ||
activeSize int | ||
} | ||
|
||
// treeNode represents a node in the binary tree | ||
type treeNode struct { | ||
data interface{} | ||
left *treeNode | ||
right *treeNode | ||
} | ||
|
||
// New creates a new List | ||
func New() *List { | ||
return &List{} | ||
} | ||
|
||
// Append adds one or more values to the list | ||
func (l *List) Append(values ...interface{}) { | ||
for _, value := range values { | ||
if l.root == nil { | ||
l.root = &treeNode{data: value} | ||
l.totalSize++ | ||
l.activeSize++ | ||
continue | ||
} | ||
|
||
index := l.totalSize | ||
bits := highestBit(index + 1) | ||
node := l.root | ||
|
||
// Start from the second highest bit | ||
for level := bits - 2; level >= 0; level-- { | ||
bit := (index & (1 << uint(level))) != 0 | ||
|
||
if bit { | ||
if node.right == nil { | ||
node.right = &treeNode{} | ||
} | ||
node = node.right | ||
} else { | ||
if node.left == nil { | ||
node.left = &treeNode{} | ||
} | ||
node = node.left | ||
} | ||
} | ||
|
||
// After traversing, set the value in the current node | ||
node.data = value | ||
l.totalSize++ | ||
l.activeSize++ | ||
} | ||
} | ||
|
||
// Get retrieves the value at the specified index | ||
func (l *List) Get(index int) interface{} { | ||
if index >= l.totalSize || index < 0 { | ||
return nil | ||
} | ||
|
||
node := l.root | ||
if node == nil { | ||
return nil | ||
} | ||
|
||
// Special case for root node | ||
if index == 0 { | ||
return node.data | ||
} | ||
|
||
// Calculate the number of bits needed | ||
bits := highestBit(index + 1) | ||
|
||
// Start from the second highest bit | ||
for level := bits - 2; level >= 0; level-- { | ||
bit := (index & (1 << uint(level))) != 0 | ||
|
||
if bit { | ||
node = node.right | ||
} else { | ||
node = node.left | ||
} | ||
|
||
if node == nil { | ||
return nil | ||
} | ||
} | ||
|
||
return node.data | ||
} | ||
|
||
// Delete marks the elements at the specified indices as deleted | ||
func (l *List) Delete(indices ...int) error { | ||
if len(indices) == 0 { | ||
return nil | ||
} | ||
|
||
for _, index := range indices { | ||
node := l.getNodeByIndex(index) | ||
if node == nil { | ||
return ErrOutOfBounds | ||
} | ||
if node.data == nil { | ||
return ErrDeleted | ||
} | ||
node.data = nil | ||
l.activeSize-- | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// deleteOne marks a single element as deleted by index, now also unified | ||
// with getNodeByIndex for consistent traversal semantics. | ||
func (l *List) deleteOne(index int) error { | ||
if index < 0 || index >= l.totalSize { | ||
return ErrOutOfBounds | ||
} | ||
node := l.getNodeByIndex(index) | ||
if node == nil { | ||
return ErrOutOfBounds | ||
} | ||
if node.data == nil { | ||
return ErrDeleted | ||
} | ||
node.data = nil | ||
l.activeSize-- | ||
return nil | ||
} | ||
|
||
// getNodeByIndex retrieves the *treeNode for zero-based index using the | ||
// exact same path bits that appending used. | ||
// This ensures your "Get path calculation" matches "Append path calculation". | ||
func (l *List) getNodeByIndex(index int) *treeNode { | ||
if index < 0 || index >= l.totalSize { | ||
log("Get: index out of bounds", index, l.totalSize) | ||
return nil | ||
} | ||
if index == 0 { | ||
// root is index 0 | ||
return l.root | ||
} | ||
path := buildPathForIndex(index) // unify logic with Append | ||
current := l.root | ||
for _, step := range path { | ||
if step { | ||
current = current.right | ||
} else { | ||
current = current.left | ||
} | ||
if current == nil { | ||
return nil | ||
} | ||
} | ||
return current | ||
} | ||
|
||
// Size returns the number of active elements | ||
func (l *List) Size() int { | ||
return l.activeSize | ||
} | ||
|
||
// TotalSize returns the total number of elements (including deleted) | ||
func (l *List) TotalSize() int { | ||
return l.totalSize | ||
} | ||
|
||
// formatBinary returns a string representation of n in binary with specified width | ||
func formatBinary(n, width int) string { | ||
result := make([]byte, width) | ||
for i := width - 1; i >= 0; i-- { | ||
if (n & (1 << uint(i))) != 0 { | ||
result[width-1-i] = '1' | ||
} else { | ||
result[width-1-i] = '0' | ||
} | ||
} | ||
return string(result) | ||
} | ||
|
||
// highestBit returns the position of the highest bit set in n | ||
func highestBit(n int) int { | ||
pos := 0 | ||
for n > 0 { | ||
log(" highestBit:", n, "->", pos) | ||
n >>= 1 | ||
pos++ | ||
} | ||
return pos | ||
} | ||
|
||
// buildPathForIndex replicates the same path logic that appendOne() uses. | ||
// The resulting bool slice is from the first branch step to the last, with | ||
// "false" = go left, "true" = go right. Index zero is special-cased at the caller. | ||
func buildPathForIndex(index int) []bool { | ||
// In your logs, for index=2, you show "bits: 2 binary: 10" and proceed | ||
// level 0 bit: false -> left, final insert: left | ||
// This indicates you read from LSB upward. | ||
// So we gather bits from least-significant to most, then reverse them | ||
// except we skip the highest bit (the "root" bit). | ||
// Example: index=2 -> binary: 10 -> skip highest bit => path = [false] | ||
|
||
bits := []bool{} | ||
for index > 0 { | ||
// pick off least significant bit | ||
bits = append(bits, (index&1) == 1) | ||
index >>= 1 | ||
} | ||
|
||
// Now bits[0] is the LSB. For index=2 (“10” in binary), bits=[false,true]. | ||
// The final “true” is the highest bit, so skip it: | ||
if len(bits) > 0 { | ||
bits = bits[:len(bits)-1] | ||
} | ||
|
||
// Reverse the remaining bits so the leftmost in bits is the first decision. | ||
for i, j := 0, len(bits)-1; i < j; i, j = i+1, j-1 { | ||
bits[i], bits[j] = bits[j], bits[i] | ||
} | ||
return bits | ||
} | ||
|
||
// IterCbFn is a callback function that processes an entry and returns whether to stop iterating | ||
type IterCbFn func(index int, value interface{}) bool | ||
|
||
// Iterator performs iteration between start and end indices, calling cb for each entry. | ||
// If start > end, iteration is performed in reverse order. | ||
// Returns true if iteration was stopped by callback returning true. | ||
func (l *List) Iterator(start, end int, cb IterCbFn) bool { | ||
if start < 0 && end < 0 { | ||
return false | ||
} | ||
if start >= l.totalSize && end >= l.totalSize { | ||
return false | ||
} | ||
|
||
// Normalize indices | ||
if start < 0 { | ||
start = 0 | ||
} | ||
if end < 0 { | ||
end = 0 | ||
} | ||
if end >= l.totalSize { | ||
end = l.totalSize - 1 | ||
} | ||
if start >= l.totalSize { | ||
start = l.totalSize - 1 | ||
} | ||
|
||
log("Iterator: start=", start, "end=", end, "totalSize=", l.totalSize) | ||
|
||
// For empty list or invalid range | ||
if l == nil || l.totalSize == 0 || start < 0 || end < 0 { | ||
log("Iterator: empty list or invalid range") | ||
return false | ||
} | ||
|
||
// Handle reverse iteration | ||
if start > end { | ||
log("Iterator: reverse mode") | ||
count := 0 | ||
for i := start; i >= end; i-- { | ||
val := l.Get(i) | ||
log("Iterator reverse: i=", i, "val=", val) | ||
if val != nil { | ||
count++ | ||
if cb(i, val) { | ||
log("Iterator reverse: callback returned false, stopping after", count, "items") | ||
return true | ||
} | ||
} | ||
} | ||
log("Iterator reverse: completed with", count, "items") | ||
return false | ||
} | ||
|
||
// Handle forward iteration | ||
log("Iterator: forward mode") | ||
count := 0 | ||
for i := start; i <= end; i++ { | ||
val := l.Get(i) | ||
log("Iterator forward: i=", i, "val=", val) | ||
if val != nil { | ||
count++ | ||
if cb(i, val) { | ||
log("Iterator forward: callback returned false, stopping after", count, "items") | ||
return true | ||
} | ||
} | ||
} | ||
log("Iterator forward: completed with", count, "items") | ||
return false | ||
} |
Oops, something went wrong.