-
Notifications
You must be signed in to change notification settings - Fork 11
Draft: 48 extend persistent data structures with deque #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 28 commits
eda35d3
4f14869
4690c40
d591655
f186772
a28145e
b5a5810
afb6bca
cd9b78f
4cec676
75c8153
7d14d39
0a981a7
961b774
7234cb6
c4604d4
9be3445
e09ac80
30e0c25
7c55bf8
15f7b96
b7a8a7f
f5d4840
0e4dfd7
c668e2c
8d9b31d
ca2f3ff
153c25b
9bceebb
befc5a1
58be67a
191e30c
3317bc4
d11eaa8
25ae5c5
eb9ccbf
94dd4e9
5dce0db
f34e96d
8723ee4
31267c8
bb00c3d
8420b86
8934375
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,4 +37,3 @@ | |
| # /lib/ | ||
| /lib/java | ||
| /lib/java-contrib | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| // This file is part of SoSy-Lab Common, | ||
| // a library of useful utilities: | ||
| // https://github.com/sosy-lab/java-common-lib | ||
| // | ||
| // SPDX-FileCopyrightText: 2026 Dirk Beyer <https://www.sosy-lab.org> | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package org.sosy_lab.common.collect; | ||
|
|
||
| import com.google.errorprone.annotations.Immutable; | ||
| import com.google.errorprone.annotations.Var; | ||
| import java.util.Iterator; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** | ||
| * A persistent implementation of a deque on the basis of {@link PersistentLinkedList}. | ||
| * | ||
| * <p>To avoid O(n) runtime complexity when accessing the bottom of the deque, two separate {@link | ||
| * PersistentLinkedList}s are used. {@code top} represents the top part of the deque, while {@code | ||
| * bottom} forms the lower part of the deque. If one were to reverse {@code bottom} and then add it | ||
| * to the bottom of {@code top}, one would receive one list representing the whole deque in correct | ||
| * order. | ||
| * | ||
| * <p>It provides operations to show the top- and bottom-most elements of the deque, as well as ones | ||
| * to remove them or add new items to the deque in either places. In most cases, these will complete | ||
| * in O(1). Occasionally, these operations will require more time, as the deque might need to be | ||
| * rebalanced (i.e. when one of the lists becomes empty, the other list is split up into top and | ||
| * bottom to further guarantee access to both ends of the deque). | ||
| * | ||
| * <p>Currently, it is only possible to create an empty deque and then add new elements one at a | ||
| * time. | ||
| * | ||
| * @param <T> type of elements to be stored in deque | ||
| */ | ||
| @Immutable(containerOf = "T") | ||
| public final class PersistentBalancingDoubleListDeque<T> implements PersistentDeque<T> { | ||
| final PersistentLinkedList<T> top; | ||
| final PersistentLinkedList<T> bottom; | ||
|
|
||
| public PersistentBalancingDoubleListDeque() { | ||
| top = PersistentLinkedList.of(); | ||
| bottom = PersistentLinkedList.of(); | ||
| } | ||
|
|
||
| private PersistentBalancingDoubleListDeque(PersistentLinkedList<T> top, PersistentLinkedList<T> bottom) { | ||
| this.top = top; | ||
| this.bottom = bottom; | ||
| } | ||
|
|
||
| /** | ||
| * Checks both sublists and returns true if both are empty, false if at least one is not. | ||
| * | ||
| * @return true if {@code top} and {@code bottom} are both empty, false if at least one is not | ||
| */ | ||
| @Override | ||
| public boolean isEmpty() { | ||
| return top.isEmpty() && bottom.isEmpty(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns element at the top of the deque. | ||
| * | ||
| * @return element at top of deque; null if deque empty | ||
| */ | ||
| @Nullable | ||
| @Override | ||
| public T getTop() { | ||
| // If deque contains only one element, one of the lists will be empty. Calling head() on an | ||
| // empty list throws an exception, so this needs to be caught. Further, the one element in | ||
| // the non-empty list should be returned, as it is both head and tail of the deque. | ||
| try { | ||
| return top.head(); | ||
| } catch (IllegalStateException e1) { | ||
| try { | ||
| return bottom.head(); | ||
| } catch (IllegalStateException e2) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns element at the bottom of the deque. | ||
| * | ||
| * @return element at bottom of deque; null if deque empty | ||
| */ | ||
| @Nullable | ||
| @Override | ||
| public T getBottom() { | ||
| // If deque contains only one element, one of the lists will be empty. Calling head() on an | ||
| // empty list throws an exception, so this needs to be caught. Further, the one element in | ||
| // the non-empty list should be returned, as it is both head and tail of the deque. | ||
| try { | ||
| return bottom.head(); | ||
| } catch (IllegalStateException e1) { | ||
| try { | ||
| return top.head(); | ||
| } catch (IllegalStateException e2) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Places new element on top of the deque. | ||
| * | ||
| * @param value element to be added to deque | ||
| * @return deque instance with new element added on top | ||
| */ | ||
| @Override | ||
| public PersistentBalancingDoubleListDeque<T> insertTop(T value) { | ||
| return new PersistentBalancingDoubleListDeque<>(top.with(value), bottom); | ||
| } | ||
|
|
||
| /** | ||
| * Places new element at the bottom of the deque. | ||
| * | ||
| * @param value element to be added to deque | ||
| * @return deque instance with new element added at the bottom | ||
| */ | ||
| @Override | ||
| public PersistentBalancingDoubleListDeque<T> insertBottom(T value) { | ||
| return new PersistentBalancingDoubleListDeque<>(top, bottom.with(value)); | ||
| } | ||
|
|
||
| /** | ||
| * Removes element at the top of the deque from deque. | ||
| * | ||
| * @return deque instance after top element has been removed | ||
| */ | ||
| @Override | ||
| public PersistentBalancingDoubleListDeque<T> deleteTop() { | ||
| // top should only ever be empty if only one element in bottom or deque completely empty | ||
| try { | ||
| if (top.isEmpty()) { | ||
| return new PersistentBalancingDoubleListDeque<>(top, bottom.tail()).rebalanceDeque(); | ||
| } | ||
| return new PersistentBalancingDoubleListDeque<>(top.tail(), bottom).rebalanceDeque(); | ||
| } catch (IllegalStateException e) { | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Removes element at the bottom of the deque from deque. | ||
| * | ||
| * @return deque instance after bottom element has been removed | ||
| */ | ||
| @Override | ||
| public PersistentBalancingDoubleListDeque<T> deleteBottom() { | ||
| // bottom should only ever be empty if only one element in top or deque completely empty | ||
| try { | ||
| if (bottom.isEmpty()) { | ||
| return new PersistentBalancingDoubleListDeque<>(top.tail(), bottom).rebalanceDeque(); | ||
| } | ||
| return new PersistentBalancingDoubleListDeque<>(top, bottom.tail()).rebalanceDeque(); | ||
| } catch (IllegalStateException e) { | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| private PersistentBalancingDoubleListDeque<T> rebalanceDeque() { | ||
| boolean topEmpty = top.isEmpty(); | ||
| boolean bottomEmpty = bottom.isEmpty(); | ||
|
|
||
| if (topEmpty && bottomEmpty) { | ||
| return this; | ||
| } else if (topEmpty && !bottomEmpty) { | ||
| return split(bottom.reversed()); | ||
| } else if (!topEmpty && bottomEmpty) { | ||
| return split(top); | ||
| } | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| private PersistentBalancingDoubleListDeque<T> split(PersistentLinkedList<T> list) { | ||
| int size = list.size(); | ||
| int halfSize = size / 2; | ||
|
|
||
| if (size <= 0) { | ||
| throw new IllegalArgumentException("Cannot split empty list!"); | ||
| } else if (size == 1) { | ||
| return this; | ||
| } | ||
|
|
||
| @Var PersistentLinkedList<T> newTop = PersistentLinkedList.of(); | ||
| @Var PersistentLinkedList<T> newBottom = PersistentLinkedList.of(); | ||
| Iterator<T> iterator = list.iterator(); | ||
|
|
||
| for (int i = 0; i < size; i++) { | ||
| T element = iterator.next(); | ||
| if (i < halfSize) { | ||
| newTop = newTop.with(element); | ||
| } else { | ||
| newBottom = newBottom.with(element); | ||
| } | ||
| } | ||
| newTop = newTop.reversed(); | ||
| return new PersistentBalancingDoubleListDeque<>(newTop, newBottom); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // This file is part of SoSy-Lab Common, | ||
| // a library of useful utilities: | ||
| // https://github.com/sosy-lab/java-common-lib | ||
| // | ||
| // SPDX-FileCopyrightText: 2026 Dirk Beyer <https://www.sosy-lab.org> | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package org.sosy_lab.common.collect; | ||
|
|
||
| import static com.google.common.truth.Truth.assertThat; | ||
|
|
||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
|
|
||
| public class PersistentBalancingDoubleListDequeTest { | ||
|
|
||
| PersistentBalancingDoubleListDeque<Object> emptyDeque; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am questioning whether plain empty This can be simplified by removing all of the class variables, as they seem like they can be set up quickly and targeted in actual tests. Also, using proper, established types instead of plain |
||
| PersistentBalancingDoubleListDeque<Object> size1Deque; | ||
| PersistentBalancingDoubleListDeque<Object> fullDeque; | ||
| Object o1; | ||
| Object o2; | ||
| Object o3; | ||
| Object o4; | ||
| Object o5; | ||
|
|
||
| @Before | ||
| public void setup() { | ||
|
|
||
| emptyDeque = new PersistentBalancingDoubleListDeque<>(); | ||
|
|
||
| size1Deque = new PersistentBalancingDoubleListDeque<>(); | ||
| o5 = new Object(); | ||
| size1Deque = size1Deque.insertTop(o5); | ||
|
|
||
| fullDeque = new PersistentBalancingDoubleListDeque<>(); | ||
| o1 = new Object(); | ||
| o2 = new Object(); | ||
| o3 = new Object(); | ||
| o4 = new Object(); | ||
| fullDeque = fullDeque.insertTop(o2); | ||
| fullDeque = fullDeque.insertTop(o1); | ||
| fullDeque = fullDeque.insertBottom(o3); | ||
| fullDeque = fullDeque.insertBottom(o4); | ||
| } | ||
|
|
||
| @Test | ||
| public void testEmptyDeque() { | ||
| assertThat(emptyDeque.isEmpty()).isTrue(); | ||
|
|
||
| assertThat(emptyDeque.getTop()).isNull(); | ||
| assertThat(emptyDeque.getBottom()).isNull(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testInsert() { | ||
| assertThat(emptyDeque.isEmpty()).isTrue(); | ||
|
|
||
| emptyDeque = emptyDeque.insertTop(o2); | ||
| emptyDeque = emptyDeque.insertTop(o1); | ||
| emptyDeque = emptyDeque.insertBottom(o3); | ||
| emptyDeque = emptyDeque.insertBottom(o4); | ||
|
|
||
| assertThat(emptyDeque.top).containsExactly(o1, o2).inOrder(); | ||
| assertThat(emptyDeque.bottom).containsExactly(o4, o3).inOrder(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testFullDeque() { | ||
| assertThat(fullDeque.isEmpty()).isFalse(); | ||
|
|
||
| assertThat(fullDeque.getTop()).isEqualTo(o1); | ||
| assertThat(fullDeque.getBottom()).isEqualTo(o4); | ||
| } | ||
|
|
||
| @Test | ||
| public void testRemove() { | ||
| assertThat(fullDeque.isEmpty()).isFalse(); | ||
|
|
||
| fullDeque = fullDeque.deleteTop(); | ||
| assertThat(fullDeque.top).containsExactly(o2); | ||
| assertThat(fullDeque.bottom).containsExactly(o4, o3).inOrder(); | ||
|
|
||
| fullDeque = fullDeque.deleteTop(); | ||
| assertThat(fullDeque.top).containsExactly(o3); | ||
| assertThat(fullDeque.bottom).containsExactly(o4); | ||
|
|
||
| fullDeque = fullDeque.deleteBottom(); | ||
| assertThat(fullDeque.top).containsExactly(o3); | ||
|
|
||
| fullDeque = fullDeque.deleteBottom(); | ||
| assertThat(fullDeque.isEmpty()).isTrue(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDequeOfSize1() { | ||
| assertThat(size1Deque.isEmpty()).isFalse(); | ||
|
|
||
| assertThat(size1Deque.getTop()).isEqualTo(o5); | ||
| assertThat(size1Deque.getBottom()).isEqualTo(o5); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // This file is part of SoSy-Lab Common, | ||
| // a library of useful utilities: | ||
| // https://github.com/sosy-lab/java-common-lib | ||
| // | ||
| // SPDX-FileCopyrightText: 2026 Dirk Beyer <https://www.sosy-lab.org> | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package org.sosy_lab.common.collect; | ||
|
|
||
| import com.google.errorprone.annotations.Immutable; | ||
|
|
||
| /** | ||
| * Interface for persistent deques. A persistent data structure is immutable, but provides cheap * | ||
| * copy-and-write operations. Thus, all write operations ({@link #insertTop(Object)}, {@link | ||
| * #insertBottom(Object)}, {@link #deleteTop()}, {@link #deleteBottom()}) will not modify the | ||
| * current instance, but return a new instance instead. | ||
| * | ||
| * @param <T> type of elements stored in deque | ||
| */ | ||
| @Immutable(containerOf = "T") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The class is declared to be immutable, is actually immutable, but it is still using/allowing mutable methods.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to list methods that are just handed through, like Overall this looks much better! Thank you! |
||
| public interface PersistentDeque<T> { | ||
|
|
||
| /** | ||
| * Returns true if deque is empty, false if not. | ||
| * | ||
| * @return true if deque empty, else false | ||
| */ | ||
| boolean isEmpty(); | ||
|
|
||
| /** | ||
| * Retrieves element at top of deque. | ||
| * | ||
| * @return element at top of deque | ||
| */ | ||
| T getTop(); | ||
|
|
||
| /** | ||
| * Retrieves element at bottom of deque. | ||
| * | ||
| * @return element at bottom of deque | ||
| */ | ||
| T getBottom(); | ||
|
|
||
| /** | ||
| * Inserts element at top of deque. | ||
| * | ||
| * @param value element to be inserted | ||
| * @return deque instance with new element at top of deque | ||
| */ | ||
| PersistentBalancingDoubleListDeque<T> insertTop(T value); | ||
|
|
||
| /** | ||
| * Inserts element at bottom of deque. | ||
| * | ||
| * @param value element to be inserted | ||
| * @return deque instance with new element at bottom of deque | ||
| */ | ||
| PersistentBalancingDoubleListDeque<T> insertBottom(T value); | ||
|
|
||
| /** | ||
| * Deletes element currently at top of deque. | ||
| * | ||
| * @return deque instance after top element has been removed | ||
| */ | ||
| PersistentBalancingDoubleListDeque<T> deleteTop(); | ||
|
|
||
| /** | ||
| * Deletes element currently at bottom of deque. | ||
| * | ||
| * @return deque instance after bottom element has been removed | ||
| */ | ||
| PersistentBalancingDoubleListDeque<T> deleteBottom(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as this test class only uses methods defined by the
PersistentDequeinterface, and works purely in a black-box fashion, it can be namedPersistentDequeTest.