org.apache.commons.text.similarity.LongestCommonSubsequence Maven / Gradle / Ivy
Show all versions of commons-text Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.text.similarity;
/**
* A similarity algorithm indicating the length of the longest common subsequence between two strings.
*
*
* The Longest common subsequence algorithm returns the length of the longest subsequence that two strings have in
* common. Two strings that are entirely different, return a value of 0, and two strings that return a value
* of the commonly shared length implies that the strings are completely the same in value and position.
* Note. Generally this algorithm is fairly inefficient, as for length m, n of the input
* CharSequence
's left
and right
respectively, the runtime of the
* algorithm is O(m*n).
*
*
*
* This implementation is based on the Longest Commons Substring algorithm
* from
* https://en.wikipedia.org/wiki/Longest_common_subsequence_problem.
*
*
* For further reading see:
*
* Lothaire, M. Applied combinatorics on words. New York: Cambridge U Press, 2005. 12-13
*
* @since 1.0
*/
public class LongestCommonSubsequence implements SimilarityScore {
/**
* Calculates longestCommonSubsequence similarity score of two CharSequence
's passed as
* input.
*
* @param left first character sequence
* @param right second character sequence
* @return longestCommonSubsequenceLength
* @throws IllegalArgumentException
* if either String input {@code null}
*/
@Override
public Integer apply(final CharSequence left, final CharSequence right) {
// Quick return for invalid inputs
if (left == null || right == null) {
throw new IllegalArgumentException("Inputs must not be null");
}
return logestCommonSubsequence(left, right).length();
}
/**
*
* Computes the longestCommonSubsequence between the two CharSequence
's passed as
* input.
*
*
* Note, a substring and
* subsequence are not necessarily the same thing. Indeed, abcxyzqrs
and
* xyzghfm
have both the same common substring and subsequence, namely xyz
. However,
* axbyczqrs
and abcxyzqtv
have the longest common subsequence xyzq
because a
* subsequence need not have adjacent characters.
*
*
*
* For reference, we give the definition of a subsequence for the reader: a subsequence is a sequence that
* can be derived from another sequence by deleting some elements without changing the order of the remaining
* elements.
*
*
* @param left first character sequence
* @param right second character sequence
* @return lcsLengthArray
* @throws IllegalArgumentException
* if either String input {@code null}
*/
public CharSequence logestCommonSubsequence(final CharSequence left, final CharSequence right) {
// Quick return
if (left == null || right == null) {
throw new IllegalArgumentException("Inputs must not be null");
}
StringBuilder longestCommonSubstringArray = new StringBuilder(Math.max(left.length(), right.length()));
int[][] lcsLengthArray = longestCommonSubstringLengthArray(left, right);
int i = left.length() - 1;
int j = right.length() - 1;
int k = lcsLengthArray[left.length()][right.length()] - 1;
while (k >= 0) {
if (left.charAt(i) == right.charAt(j)) {
longestCommonSubstringArray.append(left.charAt(i));
i = i - 1;
j = j - 1;
k = k - 1;
} else if (lcsLengthArray[i + 1][j] < lcsLengthArray[i][j + 1]) {
i = i - 1;
} else {
j = j - 1;
}
}
return longestCommonSubstringArray.reverse().toString();
}
/**
*
* Computes the lcsLengthArray for the sake of doing the actual lcs calculation. This is the
* dynamic programming portion of the algorithm, and is the reason for the runtime complexity being
* O(m*n), where m=left.length() and n=right.length().
*
* @param left first character sequence
* @param right second character sequence
* @return lcsLengthArray
*/
public int[][] longestCommonSubstringLengthArray(final CharSequence left, final CharSequence right) {
int[][] lcsLengthArray = new int[left.length() + 1][right.length() + 1];
for (int i = 0; i < left.length(); i++) {
for (int j = 0; j < right.length(); j++) {
if (i == 0) {
lcsLengthArray[i][j] = 0;
}
if (j == 0) {
lcsLengthArray[i][j] = 0;
}
if (left.charAt(i) == right.charAt(j)) {
lcsLengthArray[i + 1][j + 1] = lcsLengthArray[i][j] + 1;
} else {
lcsLengthArray[i + 1][j + 1] = Math.max(lcsLengthArray[i + 1][j], lcsLengthArray[i][j + 1]);
}
}
}
return lcsLengthArray;
}
}