xo-ordinaltree: custom allocator support in RB tree

This commit is contained in:
Roland Conybeare 2025-11-30 17:02:48 -05:00
commit 792df8b297
2 changed files with 156 additions and 68 deletions

View file

@ -72,7 +72,10 @@ namespace xo {
* - Reduce.operator() :: (Accumulator x Key) -> Accumulator
* - Reduce.operator() :: (Accumulator x Accumulator) -> Accumulator
*/
template <typename Key, typename Value, typename Reduce = NullReduce<Key>>
template <typename Key,
typename Value,
typename Reduce = NullReduce<Key>,
typename Allocator = std::allocator<std::pair<const Key, Value>>>
class RedBlackTree;
namespace detail {
@ -114,10 +117,24 @@ namespace xo {
contents_{std::move(kv_pair)},
reduced_{std::move(r)} {}
static Node * make_leaf(value_type const & kv_pair,
template <typename NodeAllocator>
static Node * make_leaf(NodeAllocator& alloc,
value_type const & kv_pair,
ReducedValue const & leaf_rv) {
return new Node(kv_pair,
std::pair<ReducedValue, ReducedValue>(leaf_rv, leaf_rv));
using traits = std::allocator_traits<NodeAllocator>;
// get memory
Node * node = traits::allocate(alloc, 1);
try {
// placemenent new
traits::construct(alloc, node, kv_pair,
std::pair<ReducedValue, ReducedValue>(leaf_rv, leaf_rv));
return node;
} catch(...) {
traits::deallocate(alloc, node, 1);
throw;
}
} /*make_leaf*/
static Node * make_leaf(value_type && kv_pair,
@ -260,7 +277,7 @@ namespace xo {
size_t size() const { return size_; }
/* const access */
ContentsType const & contents() const { return contents_; }
value_type const & contents() const { return contents_; }
/* non-const value access.
*
* editorial: would prefer to return
@ -270,7 +287,7 @@ namespace xo {
* is considered unrelated to std::pair<Key, Value>,
* so l-value conversion not allowed
*/
ContentsType & contents() { return contents_; }
value_type & contents() { return contents_; }
Node * parent() const { return parent_; }
Node * child(Direction d) const { return child_v_[d]; }
@ -403,7 +420,7 @@ namespace xo {
* .second = value associated with this node
* .third = reduced value
*/
ContentsType contents_;
value_type contents_;
/* accumulator for some binary function of Values.
* must be associative, since value will be produced
* by any ordering of calls to Reduce::combine().
@ -1230,8 +1247,10 @@ namespace xo {
* - f=false for existing node (k already in tree before this call)
* - n=node containing key k
*/
template<typename NodeAllocator>
static std::pair<bool, RbNode *>
insert_aux(value_type const & kv_pair,
insert_aux(NodeAllocator & alloc,
value_type const & kv_pair,
bool allow_replace_flag,
Reduce const & reduce_fn,
RbNode ** pp_root)
@ -1281,7 +1300,9 @@ namespace xo {
/* invariant: N->child(d) is nil */
if (N) {
RbNode * new_node = RbNode::make_leaf(kv_pair,
RbNode * new_node = RbNode::make_leaf(alloc,
kv_pair,
reduce_fn.leaf(kv_pair.second));
N->assign_child_reparent(d, new_node);
@ -1300,7 +1321,8 @@ namespace xo {
*/
return std::make_pair(true, new_node);
} else {
*pp_root = RbNode::make_leaf(kv_pair,
*pp_root = RbNode::make_leaf(alloc,
kv_pair,
reduce_fn.leaf(kv_pair.second));
/* tree with a single node might as well be black */
@ -1331,7 +1353,9 @@ namespace xo {
* - N has no child nodes
* - N->parent() != nullptr
*/
static void remove_black_leaf(RbNode *N,
template <typename NodeAllocator>
static void remove_black_leaf(NodeAllocator & alloc,
RbNode *N,
Reduce const & reduce_fn,
bool debug_flag,
RbNode **pp_root)
@ -1340,6 +1364,8 @@ namespace xo {
using xo::xtag;
using xo::print::ccs;
using traits = std::allocator_traits<NodeAllocator>;
//constexpr char const *c_self = "RbTreeUtil::remove_black_leaf";
scope log(XO_DEBUG(debug_flag));
@ -1351,7 +1377,7 @@ namespace xo {
if (!P) {
/* N was the root node, tree now empty */
*pp_root = nullptr;
delete N;
traits::deallocate(alloc, N, 1);
return;
}
@ -1360,7 +1386,7 @@ namespace xo {
*/
Direction d = P->replace_child_reparent(N, nullptr);
delete N;
traits::deallocate(alloc, N, 1);
/* need to delay this assignment until
* we've determined d
@ -1784,16 +1810,18 @@ namespace xo {
*
* return true if a node was removed; false otherwise.
*/
static bool erase_aux(Key const &k,
template<typename NodeAllocator>
static bool erase_aux(NodeAllocator & alloc,
Key const & k,
Reduce const & reduce_fn,
bool debug_flag,
RbNode **pp_root) {
RbNode ** pp_root) {
using xo::scope;
using xo::xtag;
scope log(XO_DEBUG(debug_flag));
RbNode *N = *pp_root;
RbNode * N = *pp_root;
log && log("enter", xtag("root", N));
@ -1843,7 +1871,7 @@ namespace xo {
if (X == nullptr) {
/* N has 0 or 1 children */
erase_1child_aux(N, reduce_fn, debug_flag, pp_root);
erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root);
} else {
/* R->right_child() is nil by definition */
@ -1934,7 +1962,7 @@ namespace xo {
RbTreeUtil::display_aux(D_Invalid, R, 0 /*depth*/, &log);
}
erase_1child_aux(N, reduce_fn, debug_flag, pp_root);
erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root);
} else {
/*
* here the triangle ascii art indicates a tree structure,
@ -1954,7 +1982,8 @@ namespace xo {
*/
/* will be swapping info in {R, N}:
* everything except RbNode.contents_
* everything except RbNode.contents_.
* Annoying but necessary to have stable Node memory locations
*/
RbNode::swap_locations(R, N, debug_flag);
@ -1978,48 +2007,34 @@ namespace xo {
* / .
* W
*/
erase_1child_aux(N, reduce_fn, debug_flag, pp_root);
erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root);
}
#ifdef GOING_AWAY
#ifdef OBSOLETE
/* would be convenient to just make this assignment,
* but several disadvantages:
* 1. invalidates an iterator pointing to R
* when nearby-in-key-space N gets deleted
* 2. gives up key constness
*/
N->contents_ = R->contents_;
/* (preserving
* N->color_,
* N->size_,
* N->parent_,
* N->reduced_,
* N->child_v_[])
*/
/* now relabel N as new R (R'),
* and relabel R as new N (N').
* Then go to work on reduced problem of deleting N'.
* Problem is redueced since now N' has 0 or 1 child.
*
* (Doesn't matter that N' contains key,values of R,
* since we're going to delete it anyway)
*/
N = R;
/* (preserving R->parent_, R->child_v_[]) */
N->contents_ = R->contents_; N = R;
#endif
}
return true;
} /*erase_aux*/
static void erase_1child_aux(RbNode * N,
template <typename NodeAllocator>
static void erase_1child_aux(NodeAllocator & alloc,
RbNode * N,
Reduce const & reduce_fn,
bool debug_flag,
RbNode ** pp_root) {
scope log(XO_DEBUG(debug_flag));
using traits = std::allocator_traits<NodeAllocator>;
RbNode * P = N->parent();
/* N has 0 or 1 children
@ -2054,7 +2069,7 @@ namespace xo {
}
log && log("delete red root node", xtag("addr", N));
delete N;
traits::deallocate(alloc, N, 1);
} else {
assert(false);
@ -2088,24 +2103,24 @@ namespace xo {
}
log && log("delete node", xtag("addr", N));
delete N;
traits::deallocate(alloc, N, 1);
} else {
/* N is black with no children,
* may need rebalance here
*/
if (P) {
RbTreeUtil::remove_black_leaf(N, reduce_fn, debug_flag, pp_root);
RbTreeUtil::remove_black_leaf(alloc, N, reduce_fn, debug_flag, pp_root);
} else {
/* N was root node */
*pp_root = nullptr;
log && log("delete black root node", xtag("addr", N));
delete N;
traits::deallocate(alloc, N, 1);
}
}
}
}
} /*erase_1child_aux*/
/* verify that subtree at N is in RB-shape.
* will cover subset of RedBlackTree class invariants:
@ -2120,6 +2135,7 @@ namespace xo {
* f(f(L, Node::value), R)
* where: L is reduced-value for left child,
* R is reduced-value for right child
* RB8. inorder traversal visits all the keys in subtree
*
* returns the #of nodes in subtree rooted at N.
*/
@ -2271,6 +2287,14 @@ namespace xo {
if (p_black_height)
*p_black_height = black_height;
/* RB8. inorder traversal visits all the nodes */
std::size_t subtree_z = N ? N->size() : 0ul;
XO_EXPECT(i_node == subtree_z,
tostr(c_self, ": expect visit count = node.size",
xtag("visit_count", i_node),
xtag("node.size", 0)));
return i_node;
} /*verify_subtree_ok*/
@ -2477,7 +2501,7 @@ namespace xo {
struct NodeTypeTraits<Key, Value, Reduce, false> {
using NativeNodeType = Node<Key, Value, Reduce>;
using NodeType = NativeNodeType;
using ContentsType = typename NodeType::ContentsType;
using ContentsType = typename NodeType::value_type;
using NodePtrType = NodeType *;
};
@ -2487,7 +2511,7 @@ namespace xo {
struct NodeTypeTraits<Key, Value, Reduce, true> {
using NativeNodeType = Node<Key, Value, Reduce>;
using NodeType = NativeNodeType const;
using ContentsType = typename NodeType::ContentsType const;
using ContentsType = typename NodeType::value_type const;
using NodePtrType = NodeType const *;
};
@ -2905,9 +2929,29 @@ namespace xo {
bool is_equal(value_type const & x, value_type const & y) const { return x == y; }
}; /*SumReduce*/
/* red-black tree with order statistics
*/
template <typename Key, typename Value, typename Reduce>
/** @class RedBlackTree
* @brief red-black tree with order statistics
*
* Lazily balanced. Longest path to a leaf is at most 2x the length of shortest path.
*
* Maintains order statistics. Accumulates some associative relation on key,value pairs.
*
* Can obtain iterator to k'th element of a tree with n nodes in log(n) time.
* Allows behaving as a weak random-access iterator with log(n) cost per query
*
* Missing Features:
* 1. efficient iterator arithmetic
* 2. pretty printing
* 3. reflection support
* 4. custom allocation support [WIP]
* 5. custom key compare
* 6. garbage collector integration
* 7. std library integration
**/
template <typename Key,
typename Value,
typename Reduce,
typename Allocator>
class RedBlackTree {
static_assert(ReduceConcept<Reduce, Value>);
//static_assert(requires(Reduce r) { r.nil(); }, "missing .nil() method");
@ -2916,11 +2960,20 @@ namespace xo {
using key_type = Key;
using mapped_type = Value;
using value_type = std::pair<Key const, Value>;
// using key_compare = Compare // not yet
using allocator_type = Allocator;
using allocator_traits = std::allocator_traits<Allocator>;
using ReducedValue = typename Reduce::value_type;
using RbTreeLhs = detail::RedBlackTreeLhs<RedBlackTree<Key, Value, Reduce>>;
using RbTreeConstLhs = detail::RedBlackTreeConstLhs<RedBlackTree<Key, Value, Reduce>>;
using RbUtil = detail::RbTreeUtil<Key, Value, Reduce>;
using RbNode = detail::Node<Key, Value, Reduce>;
using node_type = RbNode;
using node_allocator_type = typename std::allocator_traits<Allocator>::template rebind_alloc<node_type>;
using node_allocator_traits = std::allocator_traits<node_allocator_type>;
using Direction = detail::Direction;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
@ -2928,7 +2981,32 @@ namespace xo {
using const_iterator = detail::ConstIterator<Key, Value, Reduce>;
public:
explicit RedBlackTree(bool debug_flag = false) : debug_flag_{debug_flag} {}
explicit RedBlackTree(const allocator_type & alloc = allocator_type{},
bool debug_flag = false) :
node_alloc_{alloc},
debug_flag_{debug_flag} {}
#ifdef NOT_YET
RedBlackTree(const RedBlackTree & other) :
node_alloc_{node_allocator_traits::select_on_container_copy_construction(other.node_alloc_)}
//, compare_{other.compare_}
{
copy_from(other);
}
// similar: copy constructor with explicit alloc
RedBlackTree(const RedBlackTree & other, const allocator_type & alloc) :
node_alloc_{alloc}
{
copy_from(other);
}
// move ctor
RedBlackTree(RedBlackTree && other) noexcept :
node_alloc_{std::move(other.node_alloc_)},
root_{other.root_}, size_{other.size}
// , compare_{other.compare_)}
{}
#endif
bool empty() const { return size_ == 0; }
size_type size() const { return size_; }
@ -3145,10 +3223,11 @@ namespace xo {
*/
RbTreeLhs operator[](Key const & k) {
std::pair<bool, RbNode *> insert_result
= RbUtil::insert_aux(value_type(k, Value() /*used iff creating new node*/),
false /*allow_replace_flag*/,
this->reduce_fn_,
&(this->root_));
= RbUtil::template insert_aux<node_allocator_type>(this->node_alloc_,
value_type(k, Value() /*used iff creating new node*/),
false /*allow_replace_flag*/,
this->reduce_fn_,
&(this->root_));
return RbTreeLhs(this, insert_result.second, k);
} /*operator[]*/
@ -3260,7 +3339,8 @@ namespace xo {
scope log(XO_DEBUG(c_logging_enabled));
std::pair<bool, RbNode *> insert_result
= RbUtil::insert_aux(std::move(kv_pair),
= RbUtil::insert_aux(this->node_alloc_,
std::move(kv_pair),
true /*allow_replace_flag*/,
this->reduce_fn_,
&(this->root_));
@ -3275,13 +3355,14 @@ namespace xo {
insert_result.first));
} /*insert*/
bool erase(Key const & k) {
bool erase(Key const & key) {
scope log(XO_DEBUG(debug_flag_), xtag("size", size_));
if (log) {
log("pre", xtag("tree", *this));
log("pre", xtag("key", key), xtag("tree", *this));
}
bool retval = RbUtil::erase_aux(k,
bool retval = RbUtil::erase_aux(this->node_alloc_,
key,
this->reduce_fn_,
debug_flag_,
&(this->root_));
@ -3321,9 +3402,8 @@ namespace xo {
using xo::xtag;
constexpr const char *c_self = "RedBlackTree::verify_ok";
constexpr bool c_logging_enabled = false;
scope log(XO_DEBUG(c_logging_enabled));
scope log(XO_DEBUG(debug_flag_));
/* RB0. */
if (root_ == nullptr) {
@ -3356,7 +3436,7 @@ namespace xo {
xtag("self.size", size_),
xtag("n", n_node)));
if (c_logging_enabled)
if (debug_flag_)
log && log(xtag("size", this->size_),
xtag("blackheight", black_height));
@ -3366,13 +3446,21 @@ namespace xo {
void display() const { RbUtil::display(this->root_, 0); } /*display*/
private:
/* #of key/value pairs in this tree */
private:
/** allocator state **/
node_allocator_type node_alloc_;
/** number of nodes in this tree. Each node holds one (key,value) pair **/
size_t size_ = 0;
/* root of red/black tree */
/** root of red/black tree. Empty tree has null root. **/
RbNode * root_ = nullptr;
/* .reduce_fn :: (Accumulator x Key) -> Accumulator */
/** accumulates custom order statistics;
* for example partial sums of @tparam Values
* reduce_fn_:: (Accumulator x Key) -> Accumulator
**/
Reduce reduce_fn_;
/* true to enable debug logging */
/** true to enable debug logging **/
bool debug_flag_ = false;
}; /*RedBlackTree*/

View file

@ -154,7 +154,7 @@ namespace {
TEST_CASE("rbtree", "[redblacktree]") {
constexpr bool c_debug_flag = false;
RbTree rbtree{c_debug_flag};
RbTree rbtree{RbTree::allocator_type{}, c_debug_flag};
std::uint64_t seed = 14950349842636922572UL;
/* can reseed from /dev/urandom with: */