private AbstractNode<V> optimize(MutableNode<V> n) {
    if (n.getChildren() == null) {
      // this is a leaf node
      return new LeafNode<V>(n.getPrefix(), n.getValue());
    }

    if (n.getChildren().length == 1) {
      // this is a single-child node
      return new SingleChildNode<V>(n.getPrefix(), n.getValue(), optimize(n.getChildren()[0]));
    }

    // it's a multi-child node
    AbstractNode<V>[] optimizedChildren = new AbstractNode[n.getChildren().length];
    for (int i = 0; i < optimizedChildren.length; i++) {
      optimizedChildren[i] = optimize(n.getChildren()[i]);
    }
    Arrays.sort(optimizedChildren, new Comparator<AbstractNode<V>>(){
      @Override
      public int compare(AbstractNode<V> arg0, AbstractNode<V> arg1) {
        char[] prefix0 = arg0.getPrefix();
        char[] prefix1 = arg1.getPrefix();
        for (int i = 0; i < Math.min(prefix0.length, prefix1.length); i++) {
          if (prefix0[i] < prefix1[i]) {
            return -1;
          } else if (prefix0[i] > prefix1[i]) {
            return 1;
          }
        }
        throw new IllegalStateException("Nodes " + arg0 + " and " + arg1 + " have matching prefixes!");
      }
    });
    if (n.getPrefix().length == 1) {
//      if (isContiguous(optimizedChildren)) {
//        return new ContiguousRangeNode<V>(n.getPrefixFirst(), n.getValue(), optimizedChildren);
//      }
      return new SingleLengthMultiChildNode<V>(n.getPrefix()[0], n.getValue(), optimizedChildren);
    }
    
//    // special case for the root
//    if (n.getPrefix().length == 0) {
//      if (isContiguous(optimizedChildren)) {
//        return new ContiguousRangeNode<V>('\0', n.getValue(), optimizedChildren);
//      }
//    }
    return new MultiChildNode<V>(n.getPrefix(), n.getValue(), optimizedChildren);
  }