This article describes the author's algorithm learning experience. Since the author is not familiar with the algorithm, please feel free to correct me if there is any error.
Jiugongge keyboard problem
Given a mapping from numbers to letters: 1 => [a, b, c], 2 => [d, e, f], 3=> [g, h, i], ... , implement a function List<String> calculate(String input), if the user inputs any number string, convert each number according to the mapping table, and return all possible conversion results. For example, if you enter "12", you will get 9 results of ad, ae, af, bd, be, bf, cd, ce, cf; if you enter "123", you will get 27 results.
The above is the basic version of this question, and there is an enhanced version: the keys of the mapping table can also be combinations of numbers, such as 12 => [x], 23 => [y]. If you enter "12", there will be 10 results, and there will be 1 result x (a total of 10 results); if you enter "123", not only the previous 27 results, but also xg, xh, xi, ay , by, cy these 6 results (33 results in total).
Basic solution
The recursive method or the iterative method can be used, and the two methods are equivalent. The following is the algorithm idea and implementation code.
recursion
The idea of recursion is to continuously degrade the problem into sub-problems until the sub-problems are small enough. For example, calculate("123") is equivalent to calculate("1") * calculate("23"), and calculate("23") is equivalent to calculate("2") * calculate("3"). A special set multiplication is introduced here, which can multiply two sets such as [a, b, c] and [d, e, f] to get 9 results (that is, the result corresponding to the input "12"), This multiplication can be implemented with a function. In this way, the algorithm framework of the recursive method is clearly expressed.
The implementation code is
static List<String> calculate(String input) {
if (input.isEmpty()) {
return Collections.emptyList();
}
String key = String.valueOf(input.charAt(0));
List<String> values = mappings.get(key);
String substring = input.substring(1);
if (substring.isEmpty()) {
return values;
} else {
return product(values, calculate(substring));
}
}
// 乘法
static List<String> product(List<String> lefts, List<String> rights) {
List<String> results = new ArrayList<>();
for (String left : lefts) {
for (String right : rights) {
results.add(left + right);
}
}
return results;
}
Iterative method
The idea of the iterative method is to process each bit of the input in turn, use the obtained result set to represent the current state, and remember and update the current state. For example, the processing process of calculate("123") is, the first step is to process "1" to get the initial state [a, b, c], and the second step to process "2", let the current state [a, b, c] Multiply [d, e, f] corresponding to "2" to get a new state [ad, ae, af, bd, be, bf, cd, ce, cf], the third step processes "3", multiply the current state With [g, h, i] corresponding to "3", a new state is obtained, at which time all input values have been processed.
The implementation code is
static List<String> calculate(String input) {
List<String> results = Collections.emptyList();
for (int i = 0; i < input.length(); i++) {
String key = String.valueOf(input.charAt(i));
List<String> values = mappings.get(key);
if (results.isEmpty()) {
results = values;
} else {
results = product(results, values);
}
}
return results;
}
static List<String> product(List<String> lefts, List<String> rights) {
List<String> results = new ArrayList<>();
for (String left : lefts) {
for (String right : rights) {
results.add(left + right);
}
}
return results;
}
Difficulty-enhanced solution
The basic version directly uses each digit as the key, and uses mappings.get(key) to obtain the mapping value. For keys with unequal lengths, the difficulty-enhanced version needs to change the way to traverse each key in the mappings to determine whether the current input starts with this key. If so, use the mapping value of this key and remove this key from the current input. key, and then continue processing the remaining input.
The basic version of the recursive method can be slightly modified to get the solution of the difficulty-enhanced version. The iterative method cannot be modified to obtain a new solution, because each step may generate multiple branches with different step sizes, and the remaining inputs to be processed by these branches are different and cannot be iterated monotonically. In terms of algorithm theory, since the recursive method here is not a tail recursive, so there is no equivalent iterative version. Iterative calculations can be implemented by converting recursive calculations into continuations using stacks or queues, which are actually equivalent to simulating recursion, which is left to the reader to complete.
In fact, iterative method can be implemented more efficiently, but use dynamic programming method.
recursion
The implementation code is
static List<String> calculate(String input) {
if (input.isEmpty()) {
return Collections.emptyList();
}
List<String> results = new ArrayList<>();
for (String key : mappings.keySet()) {
if (input.startsWith(key)) {
List<String> values = mappings.get(key);
if (values == null) {
throw new IllegalArgumentException("Unrecognized input");
}
String substring = input.substring(key.length());
if (substring.isEmpty()) {
results.addAll(values);
} else {
results.addAll(product(values, calculate(substring)));
}
}
}
return results;
}
// 乘法
static List<String> product(List<String> lefts, List<String> rights) {
List<String> results = new ArrayList<>();
for (String left : lefts) {
for (String right : rights) {
results.add(left + right);
}
}
return results;
}
Performance optimization of recursive method
Let's look for optimization points. The recursive process has a lot of repeated calculations. You can use the memoization technology to build a cache to speed up. According to the test, the speed can be doubled. The data structure of the cache can be implemented by a hash table or an array. The hash table uses the input parameter of calculate() as the key and the result of calculate() as the value; the array uses input.length() as the index value and the result of calculate() as the element value. If the input size is n, the space complexity of the cache is O(n). This is left to the reader to implement.
results.addAll(product(values, calculate(substring)));
This line can be optimized, product() returns a new list, this list is copied to results, there is some memory allocation and copying overhead. If product() could output directly to results instead of returning a list, there would be no need to create and copy a list. According to the test, the speed can be increased by about 10%.
The code is modified to
// 调用处这么改
product(values, calculate(substring), results);
// 定义处这么改
static List<String> product(List<String> lefts, List<String> rights, List<String> results) {
for (String left : lefts) {
for (String right : rights) {
results.add(left + right);
}
}
}
String substring = input.substring(key.length());
This line also seems to be optimized, substring() returns a new substring, there is some memory allocation and copying overhead. Actually, on older JVMs there was an optimization, substring() was a view object like subList(), referencing the original string directly instead of creating a copy, but newer JVMs remove this optimization to prevent memory leaks (large raw characters The string is no longer in use, but is still referenced by the substring and cannot free space). We could manually implement a StringView class to do the same optimization, but substring() is called less often, so there is no real performance gain. This technique is quite interesting. The implementation of StringView is provided in the appendix at the end of the article.
dynamic programming
The dynamic programming method can achieve the same or even faster speed than memoization, because it also avoids repeated calculations, and the space complexity is only O(1).
The two algorithm textbooks "Introduction to Algorithms" and "Algorithm Design" both mention that dynamic programming cannot be implemented with recursion. The idea of dynamic programming is to start from the initial state and generate new states step by step. Each new state is obtained from one or more previous states. It is implemented like a state machine, so it is impossible to express it by recursion. For this question, you can continue to scan the input string, scan one bit forward at each step, and use the currently scanned character sequence to represent the current state. If the current state ends with a key, multiply the mapping value of this key by On the result set of the "previous step" corresponding to it, save the new result set to the cache. What is this "previous step"? Instead of simply rolling back one bit, you can remove the key you just scanned from the current state, and you can roll back to the previous state (equivalent to rolling back key.length bits), because each step saves the result set To the cache, so just query the cache to get the result set of the previous step. The cache uses the number of rollback bits as the key. Since the number of rollback bits cannot exceed the length of the longest key at most, the cache is a sliding window whose size does not exceed this number. When new values come in, the oldest values are squeezed out. Just constant space.
The implementation code is
static List<String> calculate(String input) {
String state = "";
List<String> results = new ArrayList<>();
Cache cache = new Cache();
for (int i = 0; i < input.length(); i++) {
state += input.charAt(i);
List<String> newResults = new ArrayList<>();
for (String key : mappings.keySet()) {
if (state.endsWith(key)) {
List<String> prevResult = cache.get(key.length());
List<String> values = mappings.get(key);
if (prevResult == null) {
newResults.addAll(values);
} else {
newResults.addAll(product(prevResult, values));
}
}
}
results = newResults;
cache.put(results);
}
return results;
}
static List<String> product(List<String> lefts, List<String> rights) {
List<String> results = new ArrayList<>();
for (String left : lefts) {
for (String right : rights) {
results.add(left + right);
}
}
return results;
}
// Sliding window
private static class Cache {
private int maxLength = mappings.keySet().stream().map(String::length).max(Integer::compareTo).get();
private LinkedList<List<String>> queue = new LinkedList<>();
List<String> get(int lookBackLength) {
if (queue.size() < lookBackLength) {
return null;
}
return queue.get(lookBackLength - 1);
}
void put(List<String> solutions) {
if (queue.size() == maxLength) {
queue.removeLast();
}
queue.offerFirst(solutions);
}
}
appendix
Implementation code of StringView
class StringView {
private final String string;
private final int offset;
StringView(String string, int offset) {
if (offset > string.length()) {
throw new IllegalArgumentException("offset should be within string length");
}
this.string = string;
this.offset = offset;
}
StringView subview(int additionalOffset) {
return new StringView(string, offset + additionalOffset);
}
boolean isEmpty() {
return offset == string.length();
}
boolean startsWith(String key) {
int keyLength = key.length();
if (string.length() < offset + keyLength) {
return false;
}
for (int i = 0; i < keyLength; i++) {
if (key.charAt(i) != string.charAt(i + offset)) {
return false;
}
}
return true;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。