From a0493b9732636879b4d06538787f74e2757cd92a Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 15 Sep 2012 06:13:33 +0000 Subject: [PATCH] New code that can split an AVL tree into two AVL trees. This is in preparation for a new feature where LinkedTreeMap will have multiple roots, each in its own hash bucket. --- .../google/gson/internal/LinkedTreeMap.java | 200 ++++++++++++++++++ .../gson/internal/LinkedTreeMapTest.java | 100 +++++++++ 2 files changed, 300 insertions(+) diff --git a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java index bc8b45d0..137d0cbd 100644 --- a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java +++ b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java @@ -21,6 +21,7 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.util.AbstractMap; import java.util.AbstractSet; +import java.util.ArrayDeque; import java.util.Comparator; import java.util.ConcurrentModificationException; import java.util.Iterator; @@ -424,6 +425,7 @@ public final class LinkedTreeMap extends AbstractMap implements Seri final K key; V value; int height; + int hash; /** Create the header entry */ Node() { @@ -501,6 +503,204 @@ public final class LinkedTreeMap extends AbstractMap implements Seri } } + /** + * Returns a new array containing the same nodes as {@code oldTable}, but with + * twice as many trees, each of (approximately) half the previous size. + */ + static Node[] doubleCapacity(Node[] oldTable) { + // TODO: don't do anything if we're already at MAX_CAPACITY + int oldCapacity = oldTable.length; + @SuppressWarnings("unchecked") // Arrays and generics don't get along. + Node[] newTable = new Node[oldCapacity * 2]; + AvlIterator iterator = new AvlIterator(); + AvlBuilder leftBuilder = new AvlBuilder(); + AvlBuilder rightBuilder = new AvlBuilder(); + + // Split each tree into two trees. + for (int i = 0; i < oldCapacity; i++) { + Node root = oldTable[i]; + if (root == null) { + continue; + } + + // Compute the sizes of the left and right trees. + iterator.reset(root); + int leftSize = 0; + int rightSize = 0; + while (iterator.hasNext()) { + Node node = iterator.next(); + if ((node.hash & oldCapacity) == 0) { + leftSize++; + } else { + rightSize++; + } + } + + // Split the tree into two. + Node leftRoot = null; + Node rightRoot = null; + if (leftSize > 0 && rightSize > 0) { + leftBuilder.reset(leftSize); + rightBuilder.reset(rightSize); + iterator.reset(root); + while (iterator.hasNext()) { + Node node = iterator.next(); + if ((node.hash & oldCapacity) == 0) { + leftBuilder.add(node); + } else { + rightBuilder.add(node); + } + } + leftRoot = leftBuilder.root(); + rightRoot = rightBuilder.root(); + } else if (leftSize > 0) { + leftRoot = root; + } else { + rightRoot = root; + } + + // Populate the enlarged array with these new roots. + newTable[i] = leftRoot; + newTable[i + oldCapacity] = rightRoot; + } + return newTable; + } + + /** + * Walks an AVL tree in iteration order. Once a node has been returned, its + * left, right and parent links are no longer used. For this + * reason it is safe to transform these links as you walk a tree. + */ + static class AvlIterator implements Iterator> { + private final ArrayDeque> stack = new ArrayDeque>(); + + void reset(Node root) { + stack.clear(); + for (Node n = root; n != null; n = n.left) { + stack.add(n); + } + } + + public boolean hasNext() { + return !stack.isEmpty(); + } + + public Node next() { + Node node = stack.removeLast(); + for (Node n = node.right; n != null; n = n.left) { + stack.add(n); + } + return node; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + + /** + * Builds AVL trees of a predetermined size by accepting nodes of increasing + * value. To use: + *
    + *
  1. Call {@link #reset} to initialize the target size size. + *
  2. Call {@link #add} size times with increasing values. + *
  3. Call {@link #root} to get the root of the balanced tree. + *
+ * + *

The returned tree will satisfy the AVL constraint: for every node + * N, the height of N.left and N.right is different by at + * most 1. It accomplishes this by omitting deepest-level leaf nodes when + * building trees whose size isn't a power of 2 minus 1. + * + *

Unlike rebuilding a tree from scratch, this approach requires no value + * comparisons. Using this class to create a tree of size S is + * {@code O(S)}. + */ + static class AvlBuilder { + private final ArrayDeque> stack = new ArrayDeque>(); + private int leavesToSkip; + private int leavesSkipped; + private int size; + + void reset(int targetSize) { + // compute the target tree size. This is a power of 2 minus one, like 15 or 31. + int treeCapacity = Integer.highestOneBit(targetSize) * 2 - 1; + leavesToSkip = treeCapacity - targetSize; + size = 0; + leavesSkipped = 0; + stack.clear(); + } + + void add(Node node) { + node.left = node.parent = node.right = null; + node.height = 1; + + // Skip a leaf if necessary. + if (leavesToSkip > 0 && (size & 1) == 0) { + size++; + leavesToSkip--; + leavesSkipped++; + } + + stack.addLast(node); + size++; + + // Skip a leaf if necessary. + if (leavesToSkip > 0 && (size & 1) == 0) { + size++; + leavesToSkip--; + leavesSkipped++; + } + + /* + * Combine 3 nodes into subtrees whenever the size is one less than a + * multiple of 4. For example we combine the nodes A, B, C into a + * 3-element tree with B as the root. + * + * Combine two subtrees and a spare single value whenever the size is one + * less than a multiple of 8. For example at 8 we may combine subtrees + * (A B C) and (E F G) with D as the root to form ((A B C) D (E F G)). + * + * Just as we combine single nodes when size nears a multiple of 4, and + * 3-element trees when size nears a multiple of 8, we combine subtrees of + * size (N-1) whenever the total size is 2N-1 whenever N is a power of 2. + */ + int centerHeight = 2; + for (int scale = 4; (size & scale - 1) == scale - 1; scale *= 2) { + if (leavesSkipped == 0) { + Node right = stack.removeLast(); + Node center = stack.removeLast(); + Node left = stack.removeLast(); + center.left = left; + left.parent = center; + center.right = right; + right.parent = center; + center.height = centerHeight; + stack.addLast(center); + } else if (leavesSkipped == 1) { + Node right = stack.removeLast(); + Node center = stack.removeLast(); + center.right = right; + center.height = centerHeight; + center.height++; + stack.addLast(center); + leavesSkipped = 0; + } else if (leavesSkipped == 2) { + leavesSkipped = 0; + } + centerHeight++; + } + } + + Node root() { + if (stack.size() != 1) { + throw new IllegalStateException(); + } + return stack.getLast(); + } + } + private abstract class LinkedTreeMapIterator implements Iterator { Node next = header.next; Node lastReturned = null; diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java index a559dc21..92ac9227 100644 --- a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java +++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java @@ -16,6 +16,8 @@ package com.google.gson.internal; +import com.google.gson.internal.LinkedTreeMap.AvlBuilder; +import com.google.gson.internal.LinkedTreeMap.Node; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -70,6 +72,104 @@ public final class LinkedTreeMapTest extends TestCase { } } + public void testAvlWalker() { + assertAvlWalker(node(node("a"), "b", node("c")), + "a", "b", "c"); + assertAvlWalker(node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))), + "a", "b", "c", "d", "e", "f", "g"); + assertAvlWalker(node(node(null, "a", node("b")), "c", node(node("d"), "e", null)), + "a", "b", "c", "d", "e"); + assertAvlWalker(node(null, "a", node(null, "b", node(null, "c", node("d")))), + "a", "b", "c", "d"); + assertAvlWalker(node(node(node(node("a"), "b", null), "c", null), "d", null), + "a", "b", "c", "d"); + } + + private void assertAvlWalker(Node root, String... values) { + LinkedTreeMap.AvlIterator iterator = new LinkedTreeMap.AvlIterator(); + iterator.reset(root); + for (String value : values) { + assertTrue(iterator.hasNext()); + assertEquals(value, iterator.next().getKey()); + } + assertFalse(iterator.hasNext()); + } + + public void testAvlBuilder() { + assertAvlBuilder(1, "a"); + assertAvlBuilder(2, "(. a b)"); + assertAvlBuilder(3, "(a b c)"); + assertAvlBuilder(4, "(a b (. c d))"); + assertAvlBuilder(5, "(a b (c d e))"); + assertAvlBuilder(6, "((. a b) c (d e f))"); + assertAvlBuilder(7, "((a b c) d (e f g))"); + assertAvlBuilder(8, "((a b c) d (e f (. g h)))"); + assertAvlBuilder(9, "((a b c) d (e f (g h i)))"); + assertAvlBuilder(10, "((a b c) d ((. e f) g (h i j)))"); + assertAvlBuilder(11, "((a b c) d ((e f g) h (i j k)))"); + assertAvlBuilder(12, "((a b (. c d)) e ((f g h) i (j k l)))"); + assertAvlBuilder(13, "((a b (c d e)) f ((g h i) j (k l m)))"); + assertAvlBuilder(14, "(((. a b) c (d e f)) g ((h i j) k (l m n)))"); + assertAvlBuilder(15, "(((a b c) d (e f g)) h ((i j k) l (m n o)))"); + assertAvlBuilder(16, "(((a b c) d (e f g)) h ((i j k) l (m n (. o p))))"); + assertAvlBuilder(30, "((((. a b) c (d e f)) g ((h i j) k (l m n))) o " + + "(((p q r) s (t u v)) w ((x y z) A (B C D))))"); + assertAvlBuilder(31, "((((a b c) d (e f g)) h ((i j k) l (m n o))) p " + + "(((q r s) t (u v w)) x ((y z A) B (C D E))))"); + } + + private void assertAvlBuilder(int size, String expected) { + char[] values = "abcdefghijklmnopqrstuvwxyzABCDE".toCharArray(); + AvlBuilder avlBuilder = new AvlBuilder(); + avlBuilder.reset(size); + for (int i = 0; i < size; i++) { + avlBuilder.add(node(Character.toString(values[i]))); + } + assertEquals(expected, toString(avlBuilder.root())); + } + + public void testDoubleCapacity() { + @SuppressWarnings("unchecked") // Arrays and generics don't get along. + Node[] oldTable = new Node[1]; + oldTable[0] = node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))); + + Node[] newTable = LinkedTreeMap.doubleCapacity(oldTable); + assertEquals("(b d f)", toString(newTable[0])); // Even hash codes! + assertEquals("(a c (. e g))", toString(newTable[1])); // Odd hash codes! + } + + private static final Node head = new Node(); + + private Node node(String value) { + Node result = new Node(null, value, head, head); + result.hash = value.hashCode(); + return result; + } + + private Node node(Node left, String value, + Node right) { + Node result = node(value); + if (left != null) { + result.left = left; + left.parent = result; + } + if (right != null) { + result.right = right; + right.parent = result; + } + return result; + } + + private String toString(Node root) { + if (root == null) { + return "."; + } else if (root.left == null && root.right == null) { + return String.valueOf(root.key); + } else { + return String.format("(%s %s %s)", toString(root.left), root.key, toString(root.right)); + } + } + private void assertIterationOrder(Iterable actual, T... expected) { ArrayList actualList = new ArrayList(); for (T t : actual) {