All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.blazebit.collection.TrieMap Maven / Gradle / Ivy

/**
 * Copyright 2012 Blazebit
 * 

* 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 com.blazebit.collection; import java.io.Serializable; import java.util.*; /** * A trie implementation that can be used as map. This map implementation is * especially thought to be used when having char sequence keys. Additionally to * the standard methods of the {@link Map} interface, this implementation offers * methods to check if a key prefix is contained, get the best matching key from * an example key and retrieve prefixed views on the map. * * This map does not support null keys, instead an empty string should be used. * * The implementation uses nested {@link HashMap}s with {@link Character} keys * internally to ensure O(k) performance where k is * the length of an existing key, keyLen is the key which is used * for retrieval or modification operations and k ≤ keyLen. * * @param * The value type that the trie holds. * * @author Christian Beikov * */ public class TrieMap extends AbstractMap implements Serializable, Map { private static final long serialVersionUID = 1L; private static final class TrieNode implements Serializable { private static final long serialVersionUID = 1L; private final Map> children; private V value; private boolean inUse; public TrieNode(final V value, final boolean inUse) { this.children = new HashMap>(); this.value = value; this.inUse = inUse; } private TrieNode(final V value, final boolean inUse, int childrenSize) { this.children = new HashMap>(childrenSize); this.value = value; this.inUse = inUse; } public TrieNode(final boolean inUse) { this(null, inUse); } public TrieNode unset() { inUse = false; value = null; return this; } public TrieNode cloneDeep() { final TrieNode node = new TrieNode(value, inUse, children.size()); final Map> nodeChildren = node.children; for (final Map.Entry> entry : children .entrySet()) { nodeChildren.put(entry.getKey(), entry.getValue().cloneDeep()); } return node; } } private final TrieNode root; int size; transient int modCount; /** * Constructs an empty TrieMap */ public TrieMap() { this(null, true); } /** * Constructs a new TrieMap with the values from the given map. * * @param map * The map from which to construct this TrieMap */ public TrieMap(final Map map) { this(map, false); putAll(map); } /** * Constructs a new TrieMap by deep cloning the internally used nodes. * * @param map * The map from which to construct this TrieMap */ public TrieMap(final TrieMap map) { this(map, false); } /** * Internally used to encapsulate all initializations. * * @param map * The map from which to construct this TrieMap or null * @param nullAllowed * Whether null is allowed or not */ @SuppressWarnings("unchecked") private TrieMap(final Map map, final boolean nullAllowed) { if ((nullAllowed && map == null) || !(map instanceof TrieMap)) { this.root = new TrieNode(false); } else { this.root = ((TrieMap) map).root.cloneDeep(); } this.size = 0; this.modCount = 0; } /** * This method returns the root element and mainly is for sub map to * override. * * @return The current root node. */ TrieNode getRoot() { return root; } /** * {@inheritDoc} */ @Override public V put(final CharSequence key, final V value) { final CharSequence checkedKey = keyCheck(key); final int keyLength = checkedKey.length(); final V replacedValue; TrieNode currentNode = getRoot(); TrieNode lastNode = null; int i = 0; while (i < keyLength && currentNode != null) { lastNode = currentNode; currentNode = currentNode.children.get(checkedKey.charAt(i)); ++i; } if (currentNode == null) { /* We could not find the node for the given key, so create it */ currentNode = lastNode; final TrieNode newNode = new TrieNode(true); addNode(currentNode, checkedKey, --i, newNode); modifyData(newNode, value); replacedValue = null; } else if (currentNode.inUse) { /* We found the node and it is in use, so replace the value */ replacedValue = currentNode.value; if (replacedValue != value && (replacedValue == null || !replacedValue.equals(value))) { currentNode.value = value; } } else { /* We found a node that is not in use, so just set the value */ modifyData(currentNode, value); currentNode.inUse = true; replacedValue = null; } return replacedValue; } /** * Sets the given value as the new value on the given node, increases size * and modCount. */ void modifyData(final TrieNode node, final V value) { node.value = value; ++modCount; ++size; } /** * Adds the given new node to the node with the given key beginning at * beginIndex. */ private void addNode(final TrieNode node, final CharSequence key, final int beginIndex, final TrieNode newNode) { final int lastKeyIndex = key.length() - 1; TrieNode currentNode = node; int i = beginIndex; for (; i < lastKeyIndex; i++) { final TrieNode nextNode = new TrieNode(false); currentNode.children.put(key.charAt(i), nextNode); currentNode = nextNode; } currentNode.children.put(key.charAt(i), newNode); } /** * {@inheritDoc} */ @Override public V get(final Object key) { final TrieNode node = findNode(keyCheck(key)); return node == null ? null : node.value; } private TrieNode findNode(final CharSequence key) { final int strLen = key.length(); TrieNode currentNode = getRoot(); for (int i = 0; i < strLen && currentNode != null; i++) { currentNode = currentNode.children.get(key.charAt(i)); } return currentNode; } public String getBestMatch(final CharSequence str) { final int strLen = str.length(); TrieNode curNode = getRoot(); int i = 0; for (; i < strLen && curNode != null; i++) { curNode = curNode.children.get(str.charAt(i)); } return new StringBuilder(i - 1).append(str, 0, i - 1).toString(); } /** * {@inheritDoc} */ @Override public boolean containsKey(final Object key) { final TrieNode node = findNode(keyCheck(key)); return node != null && node.inUse; } /** * Returns true when an entry exists that that has the given prefix. * * @param prefix * The prefix for which to check if an entry is contained. * @return True when an entry with the given prefix exists, otherwise false. */ public boolean containsKeyPrefix(final CharSequence prefix) { return findNode(keyCheck(prefix)) != null; } /** * {@inheritDoc} */ @Override public V remove(final Object o) { final CharSequence key = keyCheck(o); final TrieNode currentNode = findPreviousNode(key); if (currentNode == null) { /* Node not found for the given key */ return null; } final TrieNode node = currentNode.children.get(key.charAt(key .length() - 1)); if (node == null || !node.inUse) { /* Node not found for the given key or is not in use */ return null; } final V removed = node.value; node.unset(); --size; ++modCount; if (node.children.isEmpty()) { /* Since there are no children left, we can compact the trie */ compact(key); } return removed; } private TrieNode findPreviousNode(final CharSequence key) { final int lastKeyIndex = key.length() - 1; TrieNode currentNode = getRoot(); for (int i = 0; i < lastKeyIndex && currentNode != null; i++) { currentNode = currentNode.children.get(key.charAt(i)); } return currentNode; } /** * Special version of remove for EntrySet. */ V removeMapping(final Object o) { if (!(o instanceof Map.Entry)) { throw new IllegalArgumentException(); } @SuppressWarnings("unchecked") final Entry e = (Map.Entry) o; final CharSequence key = keyCheck(e.getKey()); final V value = e.getValue(); final TrieNode currentNode = findPreviousNode(key); if (currentNode == null) { /* Node not found for the given key */ return null; } final TrieNode node = currentNode.children.get(key.charAt(key .length() - 1)); if (node == null || !node.inUse) { /* Node not found for the given key or is not in use */ return null; } final V removed = node.value; if (removed != value && (removed == null || !removed.equals(value))) { /* * Value in the map differs from the value given in the entry so do * nothing and return null. */ return null; } node.unset(); --size; ++modCount; if (node.children.isEmpty()) { /* Since there are no children left, we can compact the trie */ compact(key); } return removed; } /** * {@inheritDoc} */ @Override public void clear() { final TrieNode rootNode = root; rootNode.children.clear(); rootNode.unset(); ++modCount; size = 0; } /** * Compact the trie by removing unused nodes on the path that is specified * by the given key. */ private void compact(final CharSequence key) { final int keyLength = key.length(); TrieNode currentNode = getRoot(); TrieNode lastInUseNode = currentNode; int lastInUseIndex = 0; for (int i = 0; i < keyLength && currentNode != null; i++) { if (currentNode.inUse) { lastInUseNode = currentNode; lastInUseIndex = i; } currentNode = currentNode.children.get(key.charAt(i)); } currentNode = lastInUseNode; for (int i = lastInUseIndex; i < keyLength; i++) { currentNode = currentNode.children.remove(key.charAt(i)).unset(); } } private static CharSequence keyCheck(final Object key) { if (key == null) { throw new IllegalArgumentException( "This map does not support null keys"); } else if (!(key instanceof CharSequence)) { throw new IllegalArgumentException( "Argument must be instance of CharSequence"); } return (CharSequence) key; } private static CharSequence keyCheck(final CharSequence key) { if (key == null) { throw new IllegalArgumentException( "This map does not support null keys"); } return key; } /** * {@inheritDoc} */ @Override public int size() { return size; } /* * Iterators */ /** * Entry implementation for TrieMap. */ private final class TrieEntry implements Entry { private final CharSequence key; private final TrieNode node; public TrieEntry(final CharSequence key, final TrieNode node) { this.key = key; this.node = node; } @Override public final CharSequence getKey() { return key; } @Override public final V getValue() { return node.value; } @Override public final V setValue(final V value) { final V oldValue = node.value; node.value = value; return oldValue; } @Override public int hashCode() { final Object k = this.key; final Object v = this.node.value; int hash = 7; hash = 37 * hash + (k != null ? k.hashCode() : 0); hash = 37 * hash + (v != null ? v.hashCode() : 0); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!(obj instanceof Map.Entry)) { return false; } final Map.Entry other = (Map.Entry) obj; final Object k1 = this.key; final Object k2 = other.getKey(); if (k1 != k2 && (k1 == null || !k1.equals(k2))) { return false; } final Object v1 = this.node.value; final Object v2 = other.getValue(); if (v1 != v2 && (v1 == null || !v1.equals(v2))) { return false; } return true; } } /** * Iterator implementation for TrieMap. * * @param * The type of the entry */ private abstract class TrieIterator implements Iterator { protected int expectedModCount; private final Deque deque; private Entry next; private Entry current; public TrieIterator() { this(getRoot(), ""); } public TrieIterator(final TrieNode startNode, final CharSequence key) { expectedModCount = modCount; deque = new ArrayDeque(); deque.add(new TrieEntry(key, startNode)); fetchEntry(); } private void fetchEntry() { final Deque localDeque = deque; TrieEntry localNext = null; while (localNext == null && !localDeque.isEmpty()) { final TrieEntry tempEntry = localDeque.removeFirst(); final CharSequence key = tempEntry.key; final TrieNode node = tempEntry.node; final StringBuilder sb = new StringBuilder(key.length() + 1); sb.append(key).append(' '); if (node.inUse) { localNext = tempEntry; } for (final Entry> entry : node.children .entrySet()) { sb.setCharAt(key.length(), entry.getKey()); localDeque.addFirst(new TrieEntry(sb.toString(), entry .getValue())); } } next = localNext; } @Override public boolean hasNext() { return next != null; } public Entry nextEntry() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } final Entry entry = next; current = entry; if (entry == null) { throw new NoSuchElementException(); } fetchEntry(); return entry; } @Override public void remove() { final Entry entry = current; if (entry == null) { throw new IllegalStateException(); } final int localModCount = modCount; if (localModCount != expectedModCount) { throw new ConcurrentModificationException(); } TrieMap.this.remove(entry.getKey()); expectedModCount = localModCount; } } private final class KeyIterator extends TrieIterator { @Override public CharSequence next() { return nextEntry().getKey(); } } private final class EntryIterator extends TrieIterator> { @Override public Entry next() { return nextEntry(); } } private final class ValueIterator extends TrieIterator { @Override public V next() { return nextEntry().getValue(); } } /* * Views */ private transient Set keySet; private transient Collection values; private transient Set> entrySet; /** * {@inheritDoc} */ @Override public Set keySet() { final Set ks = keySet; return ks != null ? ks : (keySet = new KeySet()); } private final class KeySet extends AbstractSet { @Override public Iterator iterator() { return new KeyIterator(); } @Override public int size() { return TrieMap.this.size(); } @Override public boolean contains(final Object o) { return containsKey((CharSequence) o); } @Override public boolean remove(final Object o) { return TrieMap.this.remove((CharSequence) o) != null; } @Override public void clear() { TrieMap.this.clear(); } } @Override public Set> entrySet() { final Set> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } /** * {@inheritDoc} */ private final class EntrySet extends AbstractSet> { @Override public void clear() { TrieMap.this.clear(); } @Override public boolean contains(final Object o) { if (!(o instanceof Map.Entry)) { return false; } @SuppressWarnings("unchecked") final Map.Entry e = (Map.Entry) (Map.Entry) o; final V value = get(e.getKey()); return value != null && value.equals(e.getValue()); } @Override public Iterator> iterator() { return new EntryIterator(); } @Override public boolean remove(final Object o) { return removeMapping(o) != null; } @Override public int size() { return TrieMap.this.size(); } } /** * {@inheritDoc} */ @Override public Collection values() { final Collection v = values; return v != null ? v : (values = new Values()); } private final class Values extends AbstractCollection { @Override public void clear() { TrieMap.this.clear(); } @SuppressWarnings("unchecked") @Override public boolean contains(final Object o) { return TrieMap.this.containsValue((V) o); } @Override public Iterator iterator() { return new ValueIterator(); } @Override public int size() { return TrieMap.this.size(); } } /** * A special implementation of TrieMap that gives a prefixed view on an * existing TrieMap. * * @param * The value type that the trie holds. */ private static class SubTrieMap extends TrieMap { private static final long serialVersionUID = 1; private TrieNode subRootNode; private TrieMap parent; private final CharSequence prefix; public SubTrieMap(final TrieMap parent, final CharSequence prefix) { this.parent = parent; this.prefix = prefix; this.modCount = -1; } private void ensureLatest() { final int parentModCount = parent.modCount; /* * The submap is maybe ahead of the parent, e.g. in remove(), so * just update if modCount is lower than parent modCount */ if (modCount < parentModCount) { modCount = parentModCount; this.size = size(subRootNode = parent.findNode(prefix)); } } private int size(final TrieNode node) { if (node == null) { return 0; } int newSize = 0; if (node.inUse) { ++newSize; } for (final Map.Entry> entry : node.children .entrySet()) { newSize += size(entry.getValue()); } return newSize; } @Override TrieNode getRoot() { ensureLatest(); return subRootNode; } @Override public V put(CharSequence key, V value) { ensureLatest(); if (subRootNode == null) { final CharSequence localPrefix = prefix; return parent.put( new StringBuilder(localPrefix.length() + key.length()) .append(localPrefix).append(key).toString(), value); } else { return super.put(key, value); } } @Override void modifyData(final TrieNode curNode, final V value) { /* * For replace and add we can skip to update subRootNode, so adjust * size and modCount */ parent.modifyData(curNode, value); ++modCount; ++size; } @Override public V remove(final Object o) { ensureLatest(); final CharSequence key = keyCheck(o); if (key.length() == 0) { final CharSequence localPrefix = prefix; /* * We delegate the remove to the parent when we would try to * remove the root of this sub map */ return parent.remove(new StringBuilder(localPrefix.length() + key.length()).append(localPrefix).append(key) .toString()); } else { final int capturedModCount = modCount; final V removed = super.remove(key); if (capturedModCount != modCount) { /* If we really removed something, adjust parent */ final TrieMap parentMap = parent; ++parentMap.modCount; --parentMap.size; } return removed; } } @Override V removeMapping(final Object o) { final CharSequence key = keyCheck(o); if (key.length() == 0) { final CharSequence localPrefix = prefix; /* * We delegate the remove to the parent when we would try to * remove the root of this sub map */ return parent.removeMapping(new StringBuilder(localPrefix .length() + key.length()).append(localPrefix) .append(key).toString()); } else { final int capturedModCount = modCount; final V removed = super.removeMapping(key); if (capturedModCount != modCount) { /* If we really removed something, adjust parent */ final TrieMap parentMap = parent; ++parentMap.modCount; --parentMap.size; } return removed; } } @Override public void clear() { ensureLatest(); final int oldSize = this.size; super.clear(); subRootNode.unset(); final TrieMap parentMap = parent; parentMap.size -= oldSize; ++parentMap.modCount; } @Override public int size() { ensureLatest(); return super.size(); } @Override public TrieMap subMap(final CharSequence prefix) { ensureLatest(); final CharSequence localPrefix = this.prefix; return parent.subMap(new StringBuilder(localPrefix.length() + prefix.length()).append(localPrefix).append(prefix) .toString()); } } /** * Returns a view on the current map that acts like if every method call to * the current map where a key is involved would be prefixed with the given * prefix. Although the implementation behaves like described, it makes the * calls in a more performant way. * * @param prefix * The prefix which to use for the sub map. * @return A prefixed view on the current map. */ public TrieMap subMap(final CharSequence prefix) { return new SubTrieMap(this, keyCheck(prefix)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy