/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.core.utils; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * A very basic binary search tree which is augmented to allow calculation of a node's index or retrieving by index in log2(n) time. Client code can * keep a track of node's index by retaining the reference to the node. * * The implementation of rebalancing is very primitive - it rebuilds the entire tree after a number of modifications, where the rebuild is done in * random ordering to enusre on-average balancing. * * @author Phil * * @param <E> */ final public class TreeList<E> implements Iterable<TreeList<E>.TreeListNode> { private TreeListNode root; private final boolean rebuildsAllowed; private int nbModificationsSinceRebuild; private int nbGetsWithUnbalancedTree; private boolean debugChecks = false; public TreeList() { this(true); } public TreeList(boolean rebuildsAllowed) { this.rebuildsAllowed = rebuildsAllowed; } public boolean isDebugChecks() { return debugChecks; } public void setDebugChecks(boolean debugChecks) { this.debugChecks = debugChecks; } public int size() { if (root == null) { return 0; } return root.size(); } public TreeListNode add(E value) { return insert(size(), value); } public boolean isValidReferences() { if (root != null) { return root.isValidReferences(true); } return true; } public TreeList<E> deepCopy(DeepCopier<E> valueCopier) { TreeList<E> ret = new TreeList<>(rebuildsAllowed); ret.debugChecks = debugChecks; ret.nbGetsWithUnbalancedTree = nbGetsWithUnbalancedTree; ret.nbModificationsSinceRebuild = nbModificationsSinceRebuild; if (root != null) { ret.root = root.deepCopy(valueCopier, null); } return ret; } /** * Append in bulk, rebuilding whole tree at same time to ensure optimality * * @param values * @return */ public List<TreeListNode> bulkAppend(List<E> values) { int n = values.size(); ArrayList<TreeListNode> ret = new ArrayList<>(n); if (values.size() > 0) { ArrayList<TreeListNode> nodes = rebuildStep1(); // add the new nodes nodes.ensureCapacity(nodes.size() + n); for (E val : values) { TreeListNode node = new TreeListNode(val); ret.add(node); nodes.add(node); } // do rebuild... buildOptimalFromEmpty(nodes); rebuildChecks(nodes); // int originalSize = size(); // // // insert in randomised order (should be close to optimal) // TIntArrayList tmpIndices = new TIntArrayList(n); // for(int i =0 ; i<n;i++){ // tmpIndices.add(i); // ret.add(null); // } // tmpIndices.shuffle(new Random(123)); // for(int i = 0 ; i<n ; i++){ // int srcIndx = tmpIndices.get(i); // int destIndx = originalSize + srcIndx; // TreeListNode node = insert(destIndx, values.get(srcIndx)); // ret.set(srcIndx, node); // } // } // return object has the same order as input object return ret; } public TreeListNode insert(int index, E value) { int initialSize = size(); TreeListNode ret = new TreeListNode(value); if (root == null) { root = ret; } else { TreeListNode current = root; int currentIndx = root.getNbLeftDescendents(); while (true) { if (debugChecks && currentIndx != current.getIndex()) { throw new RuntimeException(); } if (index <= currentIndx) { // goes before if (current.leftChild == null) { current.setLeftChild(ret); break; } else { // update currentIndx currentIndx -= 1 + current.leftChild.getNbRightDescendents(); // go to the left child current = current.leftChild; } } else { // goes after if (current.rightChild == null) { current.setRightChild(ret); break; } else { // update currentIndx currentIndx += 1 + current.rightChild.getNbLeftDescendents(); // go to right child current = current.rightChild; } } } } // debug checks are cpu intensive so only done if activated if (debugChecks && !isValidReferences()) { throw new RuntimeException(); } if (!rebuildIfNeeded()) { nbModificationsSinceRebuild++; } if (debugChecks && !isValidReferences()) { throw new RuntimeException(); } if (debugChecks && size() != initialSize + 1) { throw new RuntimeException(); } if (debugChecks && ret.getIndex() != index) { throw new RuntimeException(); } return ret; } public TreeListNode removeAt(int index) { int initialSize = size(); TreeListNode node = get(index); if (node == null) { throw new ArrayIndexOutOfBoundsException(index); } node.remove(); if (!rebuildIfNeeded()) { nbModificationsSinceRebuild++; } if (debugChecks && !isValidReferences()) { throw new RuntimeException(); } if (size() != initialSize - 1) { throw new RuntimeException(); } return node; } public TreeListNode get(int index) { if (nbModificationsSinceRebuild > 0 && !rebuildIfNeeded()) { nbGetsWithUnbalancedTree++; } if (root == null) { throw new ArrayIndexOutOfBoundsException(index); } else { TreeListNode node = root; while (true) { int leftSize = 0; if (node.leftChild != null) { leftSize = node.leftChild.size(); } // test to see if we should descend left or right or return this index if (index < leftSize) { node = node.leftChild; } else if (index == leftSize) { return node; } else { node = node.rightChild; index = index - leftSize - 1; } } } } private void setChild(TreeListNode newParent, TreeListNode child, boolean isLeftChild) { if (newParent == null) { if (root != null) { throw new RuntimeException(); } root = child; } else { newParent.setChild(child, isLeftChild); } } public ArrayList<E> toArrayList() { ArrayList<E> ret = new ArrayList<>(size()); for (TreeListNode node : this) { ret.add(node.getValue()); } return ret; } /** * Rebuild the binary search tree ensuring ordering of nodes is the same but nodes are inserted in random order, so the tree should be balanced on * average. */ private boolean rebuildIfNeeded() { if (!rebuildsAllowed) { return false; } if (nbModificationsSinceRebuild == 0) { return false; } // check if we should rebuild int halfSize = Math.max(size() / 2, 1); if (nbModificationsSinceRebuild < halfSize && nbGetsWithUnbalancedTree < halfSize) { return false; } rebuild(); return true; } /** * Rebuild the binary tree with (more-or-less) optimal balancing */ public void rebuild() { // get all children if (size() == 0) { return; } List<TreeListNode> nodes = rebuildStep1(); buildOptimalFromEmpty(nodes); rebuildChecks(nodes); // int finalHeight = 0; // if (root != null) { // finalHeight = root.getHeight(); // } // if (finalHeight > initialHeight) { // throw new RuntimeException(); // } return; } private void rebuildChecks(List<TreeListNode> nodes) { // do checks if (size() != nodes.size()) { throw new RuntimeException(); } int n = nodes.size(); for (int i = 0; i < n; i++) { if (get(i) != nodes.get(i)) { throw new RuntimeException(); } } } private ArrayList<TreeListNode> rebuildStep1() { nbModificationsSinceRebuild = 0; nbGetsWithUnbalancedTree = 0; // save all nodes in correct order ArrayList<TreeListNode> nodes = new ArrayList<>(size()); for (TreeListNode node : this) { nodes.add(node); } // clear tree root = null; for (TreeListNode node : nodes) { node.clearReferences(); } return nodes; } private void buildOptimalFromEmpty(List<TreeListNode> nodes) { if (root != null) { throw new UnsupportedOperationException(); } // optimally split class BuildHelper { TreeListNode split(List<TreeListNode> list) { // assign node for the centre int centre = list.size() / 2; TreeListNode centreNode = list.get(centre); List<TreeListNode> left = list.subList(0, centre); if (left.size() > 0) { centreNode.setLeftChild(split(left)); } if (centre + 1 < list.size()) { List<TreeListNode> right = list.subList(centre + 1, list.size()); if (right.size() > 0) { centreNode.setRightChild(split(right)); } } return centreNode; } } BuildHelper helper = new BuildHelper(); if (nodes.size() > 0) { root = helper.split(nodes); } } // private static int DEBUG_CALL_NB=0; public class TreeListNode { private final E value; private TreeListNode leftChild; private TreeListNode rightChild; private TreeListNode parent; private int nbChildren; public TreeListNode(E value) { this.value = value; } public TreeListNode deepCopy(DeepCopier<E> valueCopier, TreeListNode copiedParent) { TreeListNode ret = new TreeListNode(value != null ? valueCopier.deepCopy(value) : null); if (leftChild != null) { ret.leftChild = leftChild.deepCopy(valueCopier, ret); } if (rightChild != null) { ret.rightChild = rightChild.deepCopy(valueCopier, ret); } ret.parent = copiedParent; ret.nbChildren = nbChildren; return ret; } public boolean isDescendedFrom(TreeListNode node) { TreeListNode p = parent; while (p != null) { if (p == node) { return true; } p = p.parent; } return false; } public int getNbLeftDescendents() { return leftChild != null ? leftChild.size() : 0; } public int getNbRightDescendents() { return rightChild != null ? rightChild.size() : 0; } public boolean isValidReferences(boolean recurse) { // check either have a parent or I'm the root if (parent == null) { if (root != this) { return false; } } // check parent points to me if (parent != null && (parent.leftChild != this && parent.rightChild != this)) { return false; } // check left child points to me if (leftChild != null && leftChild.parent != this) { return false; } // check right child points to me if (rightChild != null && rightChild.parent != this) { return false; } // check children are different if (leftChild != null && leftChild == rightChild) { return false; } // recurse if (recurse && leftChild != null && leftChild.isValidReferences(true) == false) { return false; } if (recurse && rightChild != null && rightChild.isValidReferences(true) == false) { return false; } return true; } @Override public String toString() { return value != null ? value.toString() : ""; } public E getValue() { return value; } // private int getHeight() { // int ret = 1; // if (leftChild != null) { // ret = Math.max(ret, 1 + leftChild.getHeight()); // } // if (rightChild != null) { // ret = Math.max(ret, 1 + rightChild.getHeight()); // } // return ret; // } private void remove() { // DEBUG_CALL_NB++; int initialSize = TreeList.this.size(); // save references TreeListNode oldLeftChild = leftChild; TreeListNode oldRightChild = rightChild; TreeListNode oldParent = parent; TreeListNode oldSuccessor = getNext(); // find if I'm on left or right of parent boolean isLeft = false; if (parent != null) { isLeft = parent.leftChild == this; if (!isLeft && parent.rightChild != this) { throw new RuntimeException(); } } // test if I'm the root boolean isRoot = parent == null; if (isRoot && root != this) { throw new RuntimeException(); } // Remove all first, keeping the tree intact and numbers correct. // We remove in order of deepest down the tree first to keep everything intact. // Remove the successor if needed boolean removedSuccessor = false; boolean removedRightChildSuccessor = false; if (oldLeftChild != null && oldRightChild != null) { // As we have a right child we should: // (a) always have a successor // (b) the successor should be the right child or a child of the right child // (c) the successor cannot have a left child // (d) the successor cannot be the root // (e) the successor must have a parent if (oldSuccessor == null || oldSuccessor.leftChild != null || (oldSuccessor != oldRightChild && oldSuccessor.isDescendedFrom(oldRightChild) == false) || oldSuccessor == root || oldSuccessor.parent == null) { throw new RuntimeException(); } removedSuccessor = true; if (oldRightChild == oldSuccessor) { removedRightChildSuccessor = true; } // remove the successor boolean successorWasLeftChild = oldSuccessor == oldSuccessor.parent.leftChild; TreeListNode osparent = oldSuccessor.parent; if (successorWasLeftChild) { osparent.removeLeftChild(); } else { if (oldSuccessor != osparent.rightChild) { throw new RuntimeException(); } osparent.removeRightChild(); } // then if the successor is not the right child, splice it out so its right child // belongs to its parent if (oldSuccessor != oldRightChild && oldSuccessor.rightChild != null) { TreeListNode osrch = oldSuccessor.rightChild; oldSuccessor.removeRightChild(); osparent.setChild(osrch, successorWasLeftChild); } } // Remove left child if (oldLeftChild != null) { removeLeftChild(); } // Remove right child if (oldRightChild != null && removedRightChildSuccessor == false) { removeRightChild(); } // Finally remove myself. As we only remove the successor if we have both left // and right children, I must be higher than the successor. if (isRoot) { root = null; } else if (isLeft) { parent.removeLeftChild(); } else { parent.removeRightChild(); } // Now re-add if (oldLeftChild != null && oldRightChild == null) { // splice in left child TreeList.this.setChild(oldParent, oldLeftChild, isLeft); if (removedSuccessor) { throw new RuntimeException(); } } else if (oldLeftChild == null && oldRightChild != null) { // splice in right child TreeList.this.setChild(oldParent, oldRightChild, isLeft); if (removedSuccessor || removedRightChildSuccessor) { throw new RuntimeException(); } } else if (oldLeftChild != null && oldRightChild != null) { if (!removedSuccessor) { throw new RuntimeException(); } // successor takes my old position TreeList.this.setChild(oldParent, oldSuccessor, isLeft); // and gets old children oldSuccessor.setLeftChild(oldLeftChild); if (oldRightChild != oldSuccessor) { oldSuccessor.setRightChild(oldRightChild); } } // I should be empty now if (this.size() > 1) { throw new RuntimeException(); } // finally clear my references clearReferences(); // do checks if (oldLeftChild != null && oldLeftChild.isValidReferences(false) == false) { throw new RuntimeException(); } if (oldRightChild != null && oldRightChild.isValidReferences(false) == false) { throw new RuntimeException(); } if (TreeList.this.size() != initialSize - 1) { throw new RuntimeException(); } } private void clearReferences() { parent = null; leftChild = null; rightChild = null; nbChildren = 0; } /** * Get the index of this node * * @return */ public int getIndex() { // add my left children int ret = 0; if (leftChild != null) { ret += leftChild.size(); } // keep on going to parent and add left side + parent if came from the right TreeListNode node = this; while (node.parent != null) { if (node == node.parent.rightChild) { if (node.parent.leftChild != null) { ret += node.parent.leftChild.size(); } // include parent ret++; } else if (node != node.parent.leftChild) { throw new RuntimeException(); } node = node.parent; } return ret; } private TreeListNode getLeftMost() { TreeListNode ret = this; while (ret.leftChild != null) { ret = ret.leftChild; } return ret; } private TreeListNode getNext() { if (rightChild != null) { // get left-most node of right child return rightChild.getLeftMost(); } else { TreeListNode x = this; TreeListNode y = parent; while (y != null && x == y.rightChild) { x = y; y = y.parent; } return y; } } private void setChild(TreeListNode node, boolean isLeftNode) { if (isLeftNode) { setLeftChild(node); } else { setRightChild(node); } } private void setLeftChild(TreeListNode node) { if (leftChild != null) { throw new RuntimeException(); } if (node.parent != null) { throw new RuntimeException(); } leftChild = node; node.parent = this; addToChildCount(node.size()); } private void setRightChild(TreeListNode node) { if (rightChild != null) { throw new RuntimeException(); } if (node.parent != null) { throw new RuntimeException(); } rightChild = node; node.parent = this; addToChildCount(node.size()); } /** * Get the size of the subtree (number of children + 1). * * @return */ private int size() { return nbChildren + 1; } private TreeListNode removeLeftChild() { if (leftChild == null) { throw new RuntimeException(); } if (leftChild.parent != this) { throw new RuntimeException(); } addToChildCount(-leftChild.size()); leftChild.parent = null; TreeListNode ret = leftChild; leftChild = null; return ret; } private TreeListNode removeRightChild() { if (rightChild == null) { throw new RuntimeException(); } if (rightChild.parent != this) { throw new RuntimeException(); } addToChildCount(-rightChild.size()); rightChild.parent = null; TreeListNode ret = rightChild; rightChild = null; return ret; } private void addToChildCount(int nbNewChildren) { TreeListNode addTo = this; while (addTo != null) { addTo.nbChildren += nbNewChildren; addTo = addTo.parent; } } } @Override public Iterator<TreeListNode> iterator() { class It implements Iterator<TreeListNode> { private TreeListNode next; public It(TreeListNode next) { super(); this.next = next; } @Override public boolean hasNext() { return next != null; } @Override public TreeListNode next() { TreeListNode ret = next; next = next.getNext(); return ret; } @Override public void remove() { throw new UnsupportedOperationException(); } } if (root != null) { return new It(root.getLeftMost()); } return new It(null); } @Override public String toString() { int size = size(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < size; i++) { if (i > 0) { builder.append(", "); } builder.append("["); builder.append(get(i).toString()); builder.append("]"); } return builder.toString(); } }