From 13cae465dd934968f510f2394308839c58792d39 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 29 May 2024 09:34:04 +0800 Subject: [PATCH] Create ImmutableTreeSeq --- .github/FUNDING.yml | 2 +- .../immutable/ImmutableTreeSeq.java | 356 ++++++++++++++++++ .../collection/internal/tree/IndexedTree.java | 277 ++++++++++++++ .../internal/tree/RedBlackTree.java | 17 + .../immutable/ImmutableTreeSeqTest.java | 47 +++ 5 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 kala-collection/src/main/java/kala/collection/immutable/ImmutableTreeSeq.java create mode 100644 kala-collection/src/main/java/kala/collection/internal/tree/IndexedTree.java create mode 100644 src/test/java/kala/collection/immutable/ImmutableTreeSeqTest.java diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6387e7a1..b06e4e82 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://afdian.net/@Glavo", "https://donate.glavo.org/"] \ No newline at end of file +custom: ["https://afdian.net/@Glavo", "https://space.bilibili.com/20314891"] \ No newline at end of file diff --git a/kala-collection/src/main/java/kala/collection/immutable/ImmutableTreeSeq.java b/kala-collection/src/main/java/kala/collection/immutable/ImmutableTreeSeq.java new file mode 100644 index 00000000..7f8fabef --- /dev/null +++ b/kala-collection/src/main/java/kala/collection/immutable/ImmutableTreeSeq.java @@ -0,0 +1,356 @@ +/* + * Copyright 2024 Glavo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kala.collection.immutable; + +import kala.Conditions; +import kala.collection.base.Traversable; +import kala.collection.factory.CollectionFactory; +import kala.collection.internal.tree.IndexedTree; +import kala.value.Var; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.util.Iterator; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public final class ImmutableTreeSeq extends AbstractImmutableSeq implements Serializable { + + @Serial + private static final long serialVersionUID = 5783543850996772547L; + + private static final Factory FACTORY = new Factory<>(); + private static final ImmutableTreeSeq EMPTY = new ImmutableTreeSeq<>(IndexedTree.empty()); + + private final IndexedTree root; + + private ImmutableTreeSeq(IndexedTree root) { + this.root = root; + } + + //region Static Factories + + @SuppressWarnings("unchecked") + public static @NotNull CollectionFactory> factory() { + return (ImmutableTreeSeq.Factory) FACTORY; + } + + @SuppressWarnings("unchecked") + public static ImmutableTreeSeq empty() { + return (ImmutableTreeSeq) EMPTY; + } + + public static @NotNull ImmutableTreeSeq of() { + return empty(); + } + + public static @NotNull ImmutableTreeSeq of(E value1) { + return new ImmutableTreeSeq<>(IndexedTree.empty().plus(0, value1)); + } + + public static @NotNull ImmutableTreeSeq of(E value1, E value2) { + return new ImmutableTreeSeq<>(IndexedTree.empty() + .plus(0, value1) + .plus(1, value2) + ); + } + + public static @NotNull ImmutableTreeSeq of(E value1, E value2, E value3) { + return new ImmutableTreeSeq<>(IndexedTree.empty() + .plus(0, value1) + .plus(1, value2) + .plus(2, value3) + ); + } + + public static @NotNull ImmutableTreeSeq of(E value1, E value2, E value3, E value4) { + return new ImmutableTreeSeq<>(IndexedTree.empty() + .plus(0, value1) + .plus(1, value2) + .plus(2, value3) + .plus(3, value4) + ); + } + + public static @NotNull ImmutableTreeSeq of(E value1, E value2, E value3, E value4, E value5) { + return new ImmutableTreeSeq<>(IndexedTree.empty() + .plus(0, value1) + .plus(1, value2) + .plus(2, value3) + .plus(3, value4) + .plus(4, value5) + ); + } + + @SafeVarargs + public static @NotNull ImmutableTreeSeq of(E... values) { + return from(values); + } + + public static @NotNull ImmutableTreeSeq from(E @NotNull [] values) { + if (values.length == 0) { + return empty(); + } + + IndexedTree node = IndexedTree.empty(); + int i = 0; + for (E value : values) { + node = node.plus(i++, value); + } + + return new ImmutableTreeSeq<>(node); + } + + public static @NotNull ImmutableTreeSeq from(@NotNull java.util.Collection values) { + if (values.isEmpty()) { + return empty(); + } + IndexedTree node = IndexedTree.empty(); + int i = 0; + for (E value : values) { + node = node.plus(i++, value); + } + return node.size() != 0 ? new ImmutableTreeSeq<>(node) : empty(); + } + + @SuppressWarnings("unchecked") + public static @NotNull ImmutableTreeSeq from(@NotNull Traversable values) { + if (values instanceof ImmutableTreeSeq) { + return ((ImmutableTreeSeq) values); + } + if (values.knownSize() == 0) { + return empty(); + } + return from(values.iterator()); + } + + public static @NotNull ImmutableTreeSeq from(@NotNull Iterable values) { + return from(values.iterator()); + } + + public static @NotNull ImmutableTreeSeq from(@NotNull Iterator it) { + IndexedTree node = IndexedTree.empty(); + int i = 0; + while (it.hasNext()) { + node = node.plus(i++, it.next()); + } + return node.size() != 0 ? new ImmutableTreeSeq<>(node) : empty(); + } + + public static @NotNull ImmutableTreeSeq from(@NotNull Stream stream) { + return stream.collect(factory()); + } + + public static @NotNull ImmutableTreeSeq fill(int n, E value) { + if (n <= 0) { + return empty(); + } + IndexedTree node = IndexedTree.empty(); + for (int i = 0; i < n; i++) { + node = node.plus(i, value); + } + return new ImmutableTreeSeq<>(node); + } + + public static @NotNull ImmutableTreeSeq fill(int n, @NotNull Supplier supplier) { + if (n <= 0) { + return empty(); + } + IndexedTree node = IndexedTree.empty(); + for (int i = 0; i < n; i++) { + node = node.plus(i, supplier.get()); + } + return new ImmutableTreeSeq<>(node); + } + + public static @NotNull ImmutableTreeSeq fill(int n, @NotNull IntFunction init) { + if (n <= 0) { + return empty(); + } + IndexedTree node = IndexedTree.empty(); + for (int i = 0; i < n; i++) { + node = node.plus(i, init.apply(i)); + } + return new ImmutableTreeSeq<>(node); + } + + public static @NotNull ImmutableTreeSeq generateUntil(@NotNull Supplier supplier, @NotNull Predicate predicate) { + IndexedTree node = IndexedTree.empty(); + int i = 0; + while (true) { + E value = supplier.get(); + if (predicate.test(value)) + break; + + node = node.plus(i++, value); + } + return node.size() != 0 ? new ImmutableTreeSeq<>(node) : empty(); + } + + public static @NotNull ImmutableTreeSeq generateUntilNull(@NotNull Supplier supplier) { + IndexedTree node = IndexedTree.empty(); + int i = 0; + while (true) { + E value = supplier.get(); + if (value == null) + break; + node = node.plus(i++, value); + } + return node.size() != 0 ? new ImmutableTreeSeq<>(node) : empty(); + } + + + //endregion + + @Override + public @NotNull String className() { + return "ImmutableTreeSeq"; + } + + @Override + public boolean supportsFastRandomAccess() { + return true; + } + + @Override + public @NotNull Iterator iterator() { + return root.iterator(); + } + + @Override + public int size() { + return root.size(); + } + + @Override + public int knownSize() { + return size(); + } + + @Override + public E get(int index) { + Conditions.checkElementIndex(index, size()); + return root.get(index); + } + + @Override + public @NotNull ImmutableSeq prepended(E value) { + return new ImmutableTreeSeq<>(root.changeKeysAbove(0, 1).plus(0, value)); + } + + @Override + public @NotNull ImmutableSeq appended(E value) { + return new ImmutableTreeSeq<>(root.plus(size(), value)); + } + + @Override + public @NotNull ImmutableSeq updated(int index, E newValue) { + Conditions.checkElementIndex(index, size()); + IndexedTree newRoot = root.plus(index, newValue); + return newRoot != root ? new ImmutableTreeSeq<>(newRoot) : this; + } + + @Serial + private Object writeReplace() { + return new SerializationReplaced<>(this); + } + + private static final class Factory implements CollectionFactory>, ImmutableTreeSeq> { + + @Override + public ImmutableTreeSeq empty() { + return ImmutableTreeSeq.empty(); + } + + @Override + public Var> newBuilder() { + return new Var<>(IndexedTree.empty()); + } + + @Override + public ImmutableTreeSeq build(Var> builder) { + return builder.value.size() == 0 ? ImmutableTreeSeq.empty() : new ImmutableTreeSeq<>(builder.value); + } + + @Override + public void addToBuilder(@NotNull Var> builder, E value) { + builder.value = builder.value.plus(builder.value.size(), value); + } + + @Override + public Var> mergeBuilder(@NotNull Var> builder1, @NotNull Var> builder2) { + if (builder2.value.size() > 0) { + if (builder1.value.size() > 0) { + IndexedTree newValue = builder1.value; + for (E e : builder2.value) { + newValue = newValue.plus(newValue.size(), e); + } + builder1.value = newValue; + } else { + builder1.value = builder2.value; + } + } + + return builder1; + } + } + + private static final class SerializationReplaced implements Serializable, Externalizable { + @Serial + private static final long serialVersionUID = 0L; + + private ImmutableTreeSeq value; + + public SerializationReplaced() { + } + + public SerializationReplaced(ImmutableTreeSeq value) { + this.value = value; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(value.size()); + for (E v : value) { + out.writeObject(v); + } + } + + @Override + @SuppressWarnings("unchecked") + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + int size = in.readInt(); + if (size == 0) { + this.value = empty(); + return; + } + + IndexedTree node = IndexedTree.empty(); + for (int i = 0; i < size; i++) { + node = node.plus(i, (E) in.readObject()); + } + this.value = new ImmutableTreeSeq<>(node); + } + + @Serial + private Object readResolve() { + return value; + } + } +} diff --git a/kala-collection/src/main/java/kala/collection/internal/tree/IndexedTree.java b/kala-collection/src/main/java/kala/collection/internal/tree/IndexedTree.java new file mode 100644 index 00000000..273bf6a6 --- /dev/null +++ b/kala-collection/src/main/java/kala/collection/internal/tree/IndexedTree.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2008 Harold Cooper. All rights reserved. + * Licensed under the MIT License. + * See LICENSE file in the project root for full license information. + */ + +package kala.collection.internal.tree; + +import kala.collection.base.AbstractIterator; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +// https://github.com/hrldcpr/pcollections/blob/master/src/main/java/org/pcollections/IntTree.java +public final class IndexedTree implements Iterable { + + // marker value: + private static final IndexedTree EMPTY_NODE = new IndexedTree<>(); + + @SuppressWarnings("unchecked") + public static IndexedTree empty() { + return (IndexedTree) EMPTY_NODE; + } + + // we use longs so relative keys can express all ints + // (e.g. if this has key -10 and right has 'absolute' key MAXINT, + // then its relative key is MAXINT+10 which overflows) + // there might be some way to deal with this based on left-verse-right logic, + // but that sounds like a mess. + private final long key; + + private final V value; + private final IndexedTree left, right; + private final int size; + + private IndexedTree() { + size = 0; + + key = 0; + value = null; + left = this; + right = this; + } + + public IndexedTree(final long key, final V value, final IndexedTree left, final IndexedTree right) { + this.key = key; + this.value = value; + this.left = left; + this.right = right; + this.size = 1 + left.size + right.size; + } + + private IndexedTree withKey(final long newKey) { + if (size == 0 || newKey == key) return this; + return new IndexedTree<>(newKey, value, left, right); + } + + @Override + public @NotNull Iterator iterator() { + return new Itr<>(this); + } + + public int size() { + return size; + } + + public boolean containsKey(final long key) { + if (size == 0) return false; + + if (key < this.key) return left.containsKey(key - this.key); + if (key > this.key) return right.containsKey(key - this.key); + // otherwise key==this.key: + return true; + } + + public V get(final long key) { + if (size == 0) return null; + if (key < this.key) return left.get(key - this.key); + if (key > this.key) return right.get(key - this.key); + // otherwise key==this.key: + return value; + } + + public IndexedTree plus(final long key, final V value) { + if (size == 0) return new IndexedTree<>(key, value, this, this); + if (key < this.key) return rebalanced(left.plus(key - this.key, value), right); + if (key > this.key) return rebalanced(left, right.plus(key - this.key, value)); + // otherwise key==this.key, so we simply replace this, with no effect on balance: + if (value == this.value) return this; + return new IndexedTree<>(key, value, left, right); + } + + public IndexedTree minus(final long key) { + if (size == 0) return this; + if (key < this.key) return rebalanced(left.minus(key - this.key), right); + if (key > this.key) return rebalanced(left, right.minus(key - this.key)); + + // otherwise key==this.key, so we are killing this node: + + if (left.size == 0) // we can just become right node + // make key 'absolute': + return right.withKey(right.key + this.key); + if (right.size == 0) // we can just become left node + return left.withKey(left.key + this.key); + + // otherwise replace this with the next key (i.e. the smallest key to the right): + + long newKey = right.minKey() + this.key; + // (right.minKey() is relative to this; adding this.key makes it 'absolute' + // where 'absolute' really means relative to the parent of this) + + V newValue = right.get(newKey - this.key); + // now that we've got the new stuff, take it out of the right subtree: + IndexedTree newRight = right.minus(newKey - this.key); + + // lastly, make the subtree keys relative to newKey (currently they are relative to this.key): + newRight = newRight.withKey((newRight.key + this.key) - newKey); + // left is definitely not empty: + IndexedTree newLeft = left.withKey((left.key + this.key) - newKey); + + return rebalanced(newKey, newValue, newLeft, newRight); + } + + /** + * Changes every key k>=key to k+delta. + * + *

This method will create an _invalid_ tree if delta<0 and the distance between the smallest + * k>=key in this and the largest jIn other words, this method must not result in any change in the order of the keys in this, + * since the tree structure is not being changed at all. + */ + public IndexedTree changeKeysAbove(final long key, final int delta) { + if (size == 0 || delta == 0) return this; + + if (this.key >= key) + // adding delta to this.key changes the keys of _all_ children of this, + // so we now need to un-change the children of this smaller than key, + // all of which are to the left. note that we still use the 'old' relative key...: + return new IndexedTree<>( + this.key + delta, value, left.changeKeysBelow(key - this.key, -delta), right); + + // otherwise, doesn't apply yet, look to the right: + IndexedTree newRight = right.changeKeysAbove(key - this.key, delta); + if (newRight == right) return this; + return new IndexedTree<>(this.key, value, left, newRight); + } + + /** + * Changes every key kThis method will create an _invalid_ tree if delta>0 and the distance between the largest + * k=key in this is delta or less. + * + *

In other words, this method must not result in any overlap or change in the order of the + * keys in this, since the tree _structure_ is not being changed at all. + */ + public IndexedTree changeKeysBelow(final long key, final int delta) { + if (size == 0 || delta == 0) return this; + + if (this.key < key) + // adding delta to this.key changes the keys of _all_ children of this, + // so we now need to un-change the children of this larger than key, + // all of which are to the right. note that we still use the 'old' relative key...: + return new IndexedTree<>( + this.key + delta, value, left, right.changeKeysAbove(key - this.key, -delta)); + + // otherwise, doesn't apply yet, look to the left: + IndexedTree newLeft = left.changeKeysBelow(key - this.key, delta); + if (newLeft == left) return this; + return new IndexedTree<>(this.key, value, newLeft, right); + } + + // min key in this: + private long minKey() { + if (left.size == 0) return key; + // make key 'absolute' (i.e. relative to the parent of this): + return left.minKey() + this.key; + } + + private IndexedTree rebalanced(final IndexedTree newLeft, final IndexedTree newRight) { + if (newLeft == left && newRight == right) return this; // already balanced + return rebalanced(key, value, newLeft, newRight); + } + + private static final int OMEGA = 5; + private static final int ALPHA = 2; + + // rebalance a tree that is off-balance by at most 1: + private static IndexedTree rebalanced( + final long key, final V value, final IndexedTree left, final IndexedTree right) { + if (left.size + right.size > 1) { + if (left.size >= OMEGA * right.size) { // rotate to the right + IndexedTree ll = left.left, lr = left.right; + if (lr.size < ALPHA * ll.size) // single rotation + return new IndexedTree<>( + left.key + key, + left.value, + ll, + new IndexedTree<>(-left.key, value, lr.withKey(lr.key + left.key), right)); + else { // double rotation: + IndexedTree lrl = lr.left, lrr = lr.right; + return new IndexedTree<>( + lr.key + left.key + key, + lr.value, + new IndexedTree<>(-lr.key, left.value, ll, lrl.withKey(lrl.key + lr.key)), + new IndexedTree<>( + -left.key - lr.key, value, lrr.withKey(lrr.key + lr.key + left.key), right)); + } + } else if (right.size >= OMEGA * left.size) { // rotate to the left + IndexedTree rl = right.left, rr = right.right; + if (rl.size < ALPHA * rr.size) // single rotation + return new IndexedTree<>( + right.key + key, + right.value, + new IndexedTree<>(-right.key, value, left, rl.withKey(rl.key + right.key)), + rr); + else { // double rotation: + IndexedTree rll = rl.left, rlr = rl.right; + return new IndexedTree<>( + rl.key + right.key + key, + rl.value, + new IndexedTree<>( + -right.key - rl.key, value, left, rll.withKey(rll.key + rl.key + right.key)), + new IndexedTree<>(-rl.key, right.value, rlr.withKey(rlr.key + rl.key), rr)); + } + } + } + // otherwise already balanced enough: + return new IndexedTree<>(key, value, left, right); + } + + private static final class Itr extends AbstractIterator { + private final List> stack = new ArrayList<>(); // path of nonempty nodes + private int key = 0; // note we use _int_ here since this is a truly absolute key + + Itr(final IndexedTree root) { + gotoMinOf(root); + } + + public boolean hasNext() { + return !stack.isEmpty(); + } + + public V next() { + IndexedTree node = stack.getLast(); + final V result = node.value; + + // find next node. + // we've already done everything smaller, + // so try least larger node: + + if (node.right.size > 0) // we can descend to the right + gotoMinOf(node.right); + else // can't descend to the right -- try ascending to the right + while (true) { // find current node's least larger ancestor, if any + key -= node.key; // revert to parent's key + stack.removeLast(); // climb up to parent + // if parent was larger than child or there was no parent, we're done: + if (node.key < 0 || stack.isEmpty()) break; + // otherwise parent was smaller -- try its parent: + node = stack.getLast(); + } + + return result; + } + + // extend the stack to its least non-empty node: + private void gotoMinOf(IndexedTree node) { + while (node.size > 0) { + stack.add(node); + key += node.key; + node = node.left; + } + } + } +} diff --git a/kala-collection/src/main/java/kala/collection/internal/tree/RedBlackTree.java b/kala-collection/src/main/java/kala/collection/internal/tree/RedBlackTree.java index 3d4f57eb..7f56090b 100644 --- a/kala-collection/src/main/java/kala/collection/internal/tree/RedBlackTree.java +++ b/kala-collection/src/main/java/kala/collection/internal/tree/RedBlackTree.java @@ -1,14 +1,31 @@ +/* + * Copyright 2024 Glavo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package kala.collection.internal.tree; import kala.internal.ComparableUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.Serial; import java.io.Serializable; import java.util.Comparator; import java.util.function.Consumer; public abstract class RedBlackTree> implements Serializable { + @Serial private static final long serialVersionUID = 3036340578028981301L; protected static final boolean RED = true; diff --git a/src/test/java/kala/collection/immutable/ImmutableTreeSeqTest.java b/src/test/java/kala/collection/immutable/ImmutableTreeSeqTest.java new file mode 100644 index 00000000..f6faa8d3 --- /dev/null +++ b/src/test/java/kala/collection/immutable/ImmutableTreeSeqTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Glavo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kala.collection.immutable; + +import kala.collection.SeqView; +import kala.collection.SeqViewTestTemplate; +import kala.collection.factory.CollectionFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public final class ImmutableTreeSeqTest implements ImmutableSeqTestTemplate { + @Override + public CollectionFactory> factory() { + return ImmutableTreeSeq.factory(); + } + + static final class ViewTest implements SeqViewTestTemplate { + @Override + public SeqView of(E... elements) { + return ImmutableVector.from(elements).view(); + } + + @Override + public SeqView from(E[] elements) { + return ImmutableVector.from(elements).view(); + } + + @Override + public SeqView from(Iterable elements) { + return ImmutableVector.from(elements).view(); + } + } +}