I have a code where the goal is to give score to strings based on how much score certain substrings are worth (including repeating substrings).
The way my code (which is based on the Aho-Corasick algorithm) works is:
- Creating a tree-like data structure where each node represents one prefix of a string (or the full string, along with the data needed to calculate score for this match) from the set of substrings being searched for, with some extra links pointing from some of the nodes
a
to a node that represents the largest suffix of the string thata
represents, to avoid backtracking. - The search function uses this structure to find all the substrings (not remembering the matched results, only adding score if a match is found) with near O(N) complexity in most cases.
The performance bottleneck of my code is interlink()
which creates these "extra links" in the tree, everything else works reasonably fast, any suggestions for how to improve this function's time complexity or merge it with the tree creation process (while reducing time complexity) are greatly appreciated.
The input strings for createTree()
and scan()
are expected to contain only lowercase a-z characters, and they can get as long as millions of chars.
import java.util.ArrayList;
import java.util.Arrays;
public class Tree{
private int[] scores;
private int[] scoreIndexes;
private Tree[] branches = new Tree[26];
private Tree link;
private Tree(){}
/**
* @param strs - The substrings this tree will search for.
* @param scores - An array of equal length to <b>strs</b> where each <i>scores[i]</i> represents how much score <i>strs[i]</i> is worth.
*/
public Tree(String[] strs, int[] scores){
this.scores = scores;
Tree currentNode;
String str;
int strLength;
for(int i = 0; i < strs.length; i++){
currentNode = this;
str = strs[i];
strLength = str.length();
for(int c = 0; c < strLength; c++){
int idx = str.charAt(c) - 'a';
if(currentNode.branches[idx] == null)
currentNode.branches[idx] = new Tree();
currentNode = currentNode.branches[idx];
if(strLength == c + 1)
currentNode.addScoreIndex(i);
}
}
interlink();
}
/**
* @param str - The string to be scanned.
* @param minIdx - The first index from which scores from the tree's <b>scores</b> array are counted towards the total value of <b>str</b>.
* @param maxIdx - The last index from which scores from the tree's <b>scores</b> array are counted towards the total value of <b>str</b>.
* @return - The score <b>str</b> is worth.
*/
public long scan(String str, int minIdx, int maxIdx){
long score = 0L;
ArrayList<Tree> nodes = new ArrayList<Tree>();
int branchIndex;
Tree node;
for(int index = 0; index < str.length(); index++){
branchIndex = str.charAt(index) - 'a';
for(int e = 0; e < nodes.size(); e++) // advance nodes to next char
nodes.set(e, nodes.get(e).branches[branchIndex]);
while(nodes.remove(null));
if(nodes.isEmpty()){ // if no nodes left, continue the search from first node
nodes.add(this.branches[branchIndex]);
nodes.remove(null);
}
for(int e = 0; e < nodes.size(); e++){
node = nodes.get(e);
if(node.link != null)
if(!nodes.contains(node.link))
nodes.add(node.link);
if(node.scoreIndexes != null)
for(int x = 0; x < node.scoreIndexes.length; x++)
if(node.scoreIndexes[x] >= minIdx && node.scoreIndexes[x] <= maxIdx)
score += scores[node.scoreIndexes[x]];
}
}
return score;
}
/** Creates links in the tree, which are used to avoid backtracking in <b>scan(...)</b>. */
private void interlink(){
int[] path = new int[1];
for(int i = 0; i < 26; i++)
if(branches[i] != null){
path[0] = i;
branches[i].interlinkRecursive(this, path);
}
}
private void interlinkRecursive(Tree mainTree, int[] path){
int pathLength = path.length;
int[] newPath = null;
for(int i = 0; i < 26; i++)
if(branches[i] != null){
if(newPath == null)
newPath = Arrays.copyOf(path, pathLength + 1);
newPath[pathLength] = i;
branches[i].interlinkRecursive(mainTree, newPath);
}
if(pathLength < 2) return;
Tree node;
for(int k = 1; k < pathLength; k++){
node = mainTree;
for(int c = k; c < pathLength; c++){
node = node.branches[path[c]];
if(node == null) break;
}
if(node != null){
link = node;
break;
}
}
}
private void addScoreIndex(int index){
if(scoreIndexes == null) scoreIndexes = new int[1];
else scoreIndexes = Arrays.copyOf(scoreIndexes, scoreIndexes.length + 1);
scoreIndexes[scoreIndexes.length - 1] = index;
}
}
ArrayIndexOutOfBoundsException
when I try to run it. It would also be good to know how the class should be used. \$\endgroup\$createTree()
's first input is the substrings to be found, and the second input must be of equal length - representing the score each substring is worth.scan()
's inputs are:str
is the string to search substrings in. the other two are the min and max index to count score for in the substrings array, you can just useminIdx = 0; maxIdx = strs.length;
for testing. @aventurin \$\endgroup\$path
parameter ofinterlink
can become long then the nested for-loop's O(n^2) time complexity might be the cause of the performance degradation. \$\endgroup\$