使用JAVA8函数式编程生成字母序列
问题的描述是这样的:
我正在寻找一种生成下列字母序列的方式:
A,B,C,...Z,AA,AB,AC,...ZZ
问题的解答:
-
首先,我们用函数的方式分解这个算法。我们所需要的组件有:
- 一个(可重复)的字母表
- 一个上界,例如想生成多少个字母。如要求生成序列ZZ,那上界就是2。
- 一种将字母表中的字母与先前生成的字母联合成一个笛卡尔积的方法
让我们看看代码:
- 我们可以这样写入字母表,如:
List<String> alphabet = Arrays.asList("A","B",,,"Z");
但是这样并不优雅,我们用jOOλ代替:
List<String> alphabet = Seq.rangeClosed('A','Z').map(Object::toString).toList();
上面的代码生成从字符A到Z的封闭区间(Java-8-Stream-speak 是包含上边界的),然后将字符映射成字符串,最后将其转化为列表。
- 使用上边界:
要求的字符序列包括:
A,B...Z,AA,AB,ZZ
但是我们应该很容易想到扩展该需求,能生成如下字符序列,或者更多:
A,B,..AA,AB,,,AAA,...ZZZ,...
因此,我们将再次使用rangeClosed():
//1=A..Z,2=AA..ZZ,3=AAA..ZZZ
Seq.rangeClosed(1,2).flatMap(length->...).forEach(System.out::println);
这种方法是为范围[1..2]中每个长度生成一个单独的流,然后再将这些流合并到一个流中。flatMap()的本质与命令式编程(imperative programming)中的嵌套循环类似。
- 合并字母到一个笛卡尔积中
这是最棘手的部分:我们需要合并字符及出现的次数。因此,我们将使用如下的流:
Seq.rangeClosed(1, length - 1)
.foldLeft(Seq.seq(alphabet), (s, i) ->
s.crossJoin(Seq.seq(alphabet))
.map(t -> t.v1 + t.v2))
);
我们再次使用 rangeClosed() 来生成范围 [1 .. length-1] 的值。foldLeft() 与 reduce() 基本一致,区别在于 foldLeft() 保证在流中的顺序是从“左至右”的,不需要 fold 函数来关联。
另一方面,这是一个共容易懂的词汇:foldLeft() 仅代表一条循环的命令。循环的“起源”(即循环的初始化值)是一个完整的字母表(Seq.seq(alphabet))。现在,在范围[1..length-1] 中的值生成一个笛卡尔积(crossJoin()),产生一个新的字母表,然后我们将每个合并的字母再组成一个单独的字符串(t.v1 与 t.v2)。
这就是整个过程。
将上面的内容合并到一起
下面是一个简单的打印 A .. Z, AA .. ZZ, AAA .. ZZZ 到控制台的程序:
import org.jooq.lambda.Seq;
public class Test {
public static void main(String[] args) {
int max = 3;
List<String> alphabet = Seq
.rangeClosed('A', 'Z')
.map(Object::toString)
.toList();
Seq.rangeClosed(1, max)
.flatMap(length ->
Seq.rangeClosed(1, length - 1)
.foldLeft(Seq.seq(alphabet), (s, i) ->
s.crossJoin(Seq.seq(alphabet))
.map(t -> t.v1 + t.v2)))
.forEach(System.out::println);
}
}
声明:对于这个问题,这确实不是最优的算法。在Stack Overflow,有一个匿名用户给出了一种最好实现方法。
import static java.lang.Math.*;
private static String getString(int n) {
char[] buf = new char[(int) floor(log(25 * (n + 1)) / log(26))];
for (int i = buf.length - 1; i >= 0; i--) {
n--;
buf[i] = (char) ('A' + n % 26);
n /= 26;
}
return new String(buf);
}