/*
* Copyright (C) 2012 Jason Gedge <www.gedge.ca>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package io.github.xwz.base.trie;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* A radix tree. Radix trees are String -> Object mappings which allow quick
* lookups on the strings. Radix trees also make it easy to grab the objects
* with a common prefix.
*
* @param <V> the type of values stored in the tree
* @see <a href="http://en.wikipedia.org/wiki/Radix_tree">Wikipedia</a>
*/
public class RadixTree<V> implements Map<String, V>, Serializable {
/**
* The root node in this tree
*/
RadixTreeNode<V> root;
/**
* Default constructor.
*/
public RadixTree() {
this.root = new RadixTreeNode<>("");
}
/**
* Traverses this radix tree using the given visitor. Note that the tree
* will be traversed in lexicographical order.
*
* @param visitor the visitor
*/
private void visit(RadixTreeVisitor<V, ?> visitor) {
visit(root, "", "", visitor);
}
/**
* Traverses this radix tree using the given visitor. Only values with
* the given prefix will be visited. Note that the tree will be traversed
* in lexicographical order.
*
* @param visitor the visitor
* @param prefix the prefix used to restrict visitation
*/
private void visit(RadixTreeVisitor<V, ?> visitor, String prefix) {
visit(root, prefix, "", visitor);
}
/**
* Visits the given node of this tree with the given prefix and visitor. Also,
* recursively visits the left/right subtrees of this node.
*
* @param node the node
* @param prefix the prefix
* @param visitor the visitor
*/
private void visit(RadixTreeNode<V> node, String prefixAllowed, String prefix, RadixTreeVisitor<V, ?> visitor) {
if (node.hasValue() && prefix.startsWith(prefixAllowed))
visitor.visit(prefix, node.getValue());
for (RadixTreeNode<V> child : node) {
final int prefixLen = prefix.length();
final String newPrefix = prefix + child.getPrefix();
if (prefixAllowed.length() <= prefixLen
|| newPrefix.length() <= prefixLen
|| newPrefix.charAt(prefixLen) == prefixAllowed.charAt(prefixLen)) {
visit(child, prefixAllowed, newPrefix, visitor);
}
}
}
@Override
public void clear() {
root.getChildren().clear();
}
@Override
public boolean containsKey(final Object keyToCheck) {
if (keyToCheck == null)
throw new NullPointerException("key cannot be null");
if (!(keyToCheck instanceof String))
throw new ClassCastException("keys must be String instances");
RadixTreeVisitor<V, Boolean> visitor = new RadixTreeVisitor<V, Boolean>() {
boolean found = false;
@Override
public void visit(String key, V value) {
if (key.equals(keyToCheck))
found = true;
}
@Override
public Boolean getResult() {
return found;
}
};
visit(visitor, (String) keyToCheck);
return visitor.getResult();
}
@Override
public boolean containsValue(final Object val) {
RadixTreeVisitor<V, Boolean> visitor = new RadixTreeVisitor<V, Boolean>() {
boolean found = false;
@Override
public void visit(String key, V value) {
if (val == value || (value != null && value.equals(val)))
found = true;
}
@Override
public Boolean getResult() {
return found;
}
};
visit(visitor);
return visitor.getResult();
}
@Override
public V get(final Object keyToCheck) {
if (keyToCheck == null)
throw new NullPointerException("key cannot be null");
if (!(keyToCheck instanceof String))
throw new ClassCastException("keys must be String instances");
RadixTreeVisitor<V, V> visitor = new RadixTreeVisitor<V, V>() {
V result = null;
@Override
public void visit(String key, V value) {
if (key.equals(keyToCheck))
result = value;
}
@Override
public V getResult() {
return result;
}
};
visit(visitor, (String) keyToCheck);
return visitor.getResult();
}
/**
* Gets a list of entries whose associated keys have the given prefix.
*
* @param prefix the prefix to look for
* @return the list of values
* @throws NullPointerException if prefix is <code>null</code>
*/
public List<Entry<String, V>> getEntriesWithPrefix(String prefix) {
RadixTreeVisitor<V, List<Entry<String, V>>> visitor = new RadixTreeVisitor<V, List<Entry<String, V>>>() {
List<Entry<String, V>> result = new ArrayList<>();
@Override
public void visit(String key, V value) {
result.add(new AbstractMap.SimpleEntry<>(key, value));
}
@Override
public List<Entry<String, V>> getResult() {
return result;
}
};
visit(visitor, prefix);
return visitor.getResult();
}
/**
* Gets a list of values whose associated keys have the given prefix.
*
* @param prefix the prefix to look for
* @return the list of values
* @throws NullPointerException if prefix is <code>null</code>
*/
public List<V> getValuesWithPrefix(String prefix) {
if (prefix == null)
throw new NullPointerException("prefix cannot be null");
RadixTreeVisitor<V, List<V>> visitor = new RadixTreeVisitor<V, List<V>>() {
List<V> result = new ArrayList<>();
@Override
public void visit(String key, V value) {
result.add(value);
}
@Override
public List<V> getResult() {
return result;
}
};
visit(visitor, prefix);
return visitor.getResult();
}
/**
* Gets a list of keys with the given prefix.
*
* @param prefix the prefix to look for
* @return the list of prefixes
* @throws NullPointerException if prefix is <code>null</code>
*/
public List<String> getKeysWithPrefix(String prefix) {
if (prefix == null)
throw new NullPointerException("prefix cannot be null");
RadixTreeVisitor<V, List<String>> visitor = new RadixTreeVisitor<V, List<String>>() {
List<String> result = new ArrayList<>();
@Override
public void visit(String key, V value) {
result.add(key);
}
@Override
public List<String> getResult() {
return result;
}
};
visit(visitor, prefix);
return visitor.getResult();
}
@Override
public boolean isEmpty() {
return root.getChildren().isEmpty();
}
@Override
public void putAll(Map<? extends String, ? extends V> map) {
for (Entry<? extends String, ? extends V> entry : map.entrySet())
put(entry.getKey(), entry.getValue());
}
@Override
public int size() {
RadixTreeVisitor<V, Integer> visitor = new RadixTreeVisitor<V, Integer>() {
int count = 0;
@Override
public void visit(String key, V value) {
++count;
}
@Override
public Integer getResult() {
return count;
}
};
visit(visitor);
return visitor.getResult();
}
@Override
public Set<Entry<String, V>> entrySet() {
// TODO documentation Of Map.entrySet() specifies that this is a view of
// the entries, and modifications to this collection should be
// reflected in the parent structure
//
RadixTreeVisitor<V, Set<Entry<String, V>>> visitor = new RadixTreeVisitor<V, Set<Entry<String, V>>>() {
Set<Entry<String, V>> result = new HashSet<>();
@Override
public void visit(String key, V value) {
result.add(new AbstractMap.SimpleEntry<>(key, value));
}
@Override
public Set<Entry<String, V>> getResult() {
return result;
}
};
visit(visitor);
return visitor.getResult();
}
@Override
public Set<String> keySet() {
// TODO documentation Of Map.keySet() specifies that this is a view of
// the keys, and modifications to this collection should be
// reflected in the parent structure
//
RadixTreeVisitor<V, Set<String>> visitor = new RadixTreeVisitor<V, Set<String>>() {
Set<String> result = new TreeSet<>();
@Override
public void visit(String key, V value) {
result.add(key);
}
@Override
public Set<String> getResult() {
return result;
}
};
visit(visitor);
return visitor.getResult();
}
@Override
public Collection<V> values() {
// TODO documentation Of Map.values() specifies that this is a view of
// the values, and modifications to this collection should be
// reflected in the parent structure
//
RadixTreeVisitor<V, Collection<V>> visitor = new RadixTreeVisitor<V, Collection<V>>() {
Collection<V> result = new ArrayList<>();
@Override
public void visit(String key, V value) {
result.add(value);
}
@Override
public Collection<V> getResult() {
return result;
}
};
visit(visitor);
return visitor.getResult();
}
@Override
public V put(String key, V value) {
if (key == null)
throw new NullPointerException("key cannot be null");
return put(key, value, root);
}
/**
* Remove the value with the given key from the subtree rooted at the
* given node.
*
* @param key the key
* @param node the node to start searching from
* @return the old value associated with the given key, or <code>null</code>
* if there was no mapping for <code>key</code>
*/
private V put(String key, V value, RadixTreeNode<V> node) {
V ret = null;
final int largestPrefix = RadixTreeUtil.largestPrefixLength(key, node.getPrefix());
if (largestPrefix == node.getPrefix().length() && largestPrefix == key.length()) {
// Found a node with an exact match
ret = node.getValue();
node.setValue(value);
node.setHasValue(true);
} else if (largestPrefix == 0
|| (largestPrefix < key.length() && largestPrefix >= node.getPrefix().length())) {
// Key is bigger than the prefix located at this node, so we need to see if
// there's a child that can possibly share a prefix, and if not, we just add
// a new node to this node
final String leftoverKey = key.substring(largestPrefix);
boolean found = false;
for (RadixTreeNode<V> child : node) {
if (child.getPrefix().charAt(0) == leftoverKey.charAt(0)) {
found = true;
ret = put(leftoverKey, value, child);
break;
}
}
if (!found) {
// No child exists with any prefix of the given key, so add a new one
RadixTreeNode<V> n = new RadixTreeNode<>(leftoverKey, value);
node.getChildren().add(n);
}
} else if (largestPrefix < node.getPrefix().length()) {
// Key and node.getPrefix() share a prefix, so split node
final String leftoverPrefix = node.getPrefix().substring(largestPrefix);
final RadixTreeNode<V> n = new RadixTreeNode<>(leftoverPrefix, node.getValue());
n.setHasValue(node.hasValue());
n.getChildren().addAll(node.getChildren());
node.setPrefix(node.getPrefix().substring(0, largestPrefix));
node.getChildren().clear();
node.getChildren().add(n);
if (largestPrefix == key.length()) {
// The largest prefix is equal to the key, so set this node's value
ret = node.getValue();
node.setValue(value);
node.setHasValue(true);
} else {
// There's a leftover suffix on the key, so add another child
final String leftoverKey = key.substring(largestPrefix);
final RadixTreeNode<V> keyNode = new RadixTreeNode<>(leftoverKey, value);
node.getChildren().add(keyNode);
node.setHasValue(false);
}
} else {
// node.getPrefix() is a prefix of key, so add as child
final String leftoverKey = key.substring(largestPrefix);
final RadixTreeNode<V> n = new RadixTreeNode<>(leftoverKey, value);
node.getChildren().add(n);
}
return ret;
}
@Override
public V remove(Object key) {
if (key == null)
throw new NullPointerException("key cannot be null");
if (!(key instanceof String))
throw new ClassCastException("keys must be String instances");
// Special case for removing empty string (root node)
final String sKey = (String) key;
if (sKey.equals("")) {
final V value = root.getValue();
root.setHasValue(false);
return value;
}
return remove(sKey, root);
}
/**
* Remove the value with the given key from the subtree rooted at the
* given node.
*
* @param key the key
* @param node the node to start searching from
* @return the value associated with the given key, or <code>null</code>
* if there was no mapping for <code>key</code>
*/
private V remove(String key, RadixTreeNode<V> node) {
V ret = null;
final Iterator<RadixTreeNode<V>> iter = node.getChildren().iterator();
while (iter.hasNext()) {
final RadixTreeNode<V> child = iter.next();
final int largestPrefix = RadixTreeUtil.largestPrefixLength(key, child.getPrefix());
if (largestPrefix == child.getPrefix().length() && largestPrefix == key.length()) {
// Found our match, remove the value from this node
if (child.getChildren().size() == 0) {
// Leaf node, simply remove from parent
ret = child.getValue();
iter.remove();
break;
} else if (child.hasValue()) {
// Internal node
ret = child.getValue();
child.setHasValue(false);
if (child.getChildren().size() == 1) {
// The subchild's prefix can be reused, with a little modification
final RadixTreeNode<V> subchild = child.getChildren().iterator().next();
final String newPrefix = child.getPrefix() + subchild.getPrefix();
// Merge child node with its single child
child.setValue(subchild.getValue());
child.setHasValue(subchild.hasValue());
child.setPrefix(newPrefix);
child.getChildren().clear();
}
break;
}
} else if (largestPrefix > 0 && largestPrefix < key.length()) {
// Continue down subtree of child
final String leftoverKey = key.substring(largestPrefix);
ret = remove(leftoverKey, child);
break;
}
}
return ret;
}
}