什么是字典序最小字符串旋转
字符串旋转是指将字符串的前若干个字符移到字符串末端,例如将abc旋转一位得到的字符串是bca。对于一个字符串集合,将其中每个字符串旋转任意次之后,可以得到许多新的字符串。然而,其中有些字符串与其他字符串相同,例如将abc旋转一位后得到的字符串bca和abc旋转两位后得到的字符串cab都与原字符串相同,因此这些字符串对于集合中的字符串而言是相同的。字典序最小字符串旋转的问题就是要找到对于一个给定字符串,将其旋转n次后所得到的字符串中字典序最小的那个。
举个例子,给出字符串s="bcdaba",其旋转的可能结果如下表所示,其中第一行和第二行对应的字符串相同,第三行和第四行对应的字符串相同:
bcdaba | cdabab | dababc | ababcd | babcda | abcdab |
cdabab | dababc | ababcd | babcda | bcdaba | bcdaba |
dababc | ababcd | babcda | abcdab | cdabab | cdabab |
ababcd | babcda | abcdab | bcdaba | cdabab | dababc |
bababc | abcdab | bcdaba | cdabab | dababc | abcdba |
abcdba | bcdaba | cdabab | cdabab | abcdab | bababc |
我们可以发现,将s旋转3次得到的字符串"ababcd"是字典序最小的。因此,将s旋转3次即可得到字典序最小的趋势。
如何在JavaScript中实现字典序最小字符串旋转
方法1:暴力枚举
最简单的方法是直接枚举所有的旋转结果,然后取其中字典序最小的一个。具体实现如下:
function minLexRotation(s) {
var n = s.length;
for (var i = 0; i < n; i++) {
var tmp = s.substr(i) + s.substr(0, i);
if (tmp < s) {
s = tmp;
}
}
return s;
}
var s = "bcdaba";
console.log(minLexRotation(s)); // 输出:ababcd
由于要枚举所有的旋转结果,因此该算法的时间复杂度为O(n^2),使用起来的效率非常低。
方法2:Manber和Myers算法
Manber和Myers提出了一种时间复杂度为O(n)的算法,其核心思想是建立字符串的后缀数组。在此基础上,找到所有后缀的最长公共前缀长度,然后取其中的最小值,接着对这个长度对应的后缀进行旋转得到最终答案。
实现这个算法需要用到后缀数组和最长公共前缀数组,后缀数组指的是一个能快速查找字符串中所有后缀的数组,最长公共前缀数组是指一个数组,记录了相邻的两个后缀的最长公共前缀长度。下面是这个算法的详细实现:
function suffixArray(s) {
var n = s.length,
sa = new Array(n),
rank = new Array(n);
// 初始化数组
for (var i = 0; i < n; i++) {
sa[i] = i; // 后缀数组,存储的是后缀的下标
rank[i] = s.charCodeAt(i); // 名次数组,将字符转换为ascii码
}
for (var k = 1; k < n; k *= 2) {
// 按照第k个字符进行排序,产生新的名次数组
sa.sort(function(a, b) {
if (rank[a] !== rank[b]) {
return rank[a] - rank[b];
} else {
var ra = (a + k < n) ? rank[a + k] : -1,
rb = (b + k < n) ? rank[b + k] : -1;
return ra - rb;
}
});
// 产生新的名次数组
var newRank = new Array(n),
r = 0;
for (var i = 0; i < n; i++) {
if (i === 0) {
newRank[sa[i]] = 0;
} else if (rank[sa[i]] === rank[sa[i - 1]] && (sa[i] + k < n ? rank[sa[i] + k] : -1) === (sa[i - 1] + k < n ? rank[sa[i - 1] + k] : -1)) {
newRank[sa[i]] = r;
} else {
newRank[sa[i]] = ++r;
}
}
rank = newRank;
}
return sa;
}
function lcp(s, sa) {
var n = s.length,
h = new Array(n),
height = 0;
for (var i = 0; i < n; i++) {
var k = rank[i],
j = sa[k - 1];
if (k === 0) {
h[k] = 0;
} else {
while (i + height < n && j + height < n && s.charAt(i + height) === s.charAt(j + height)) {
height++;
}
h[k] = height;
if (height > 0) {
height--;
}
}
}
return h;
}
function minLexRotation(s) {
var n = s.length,
t = s + s,
sa = suffixArray(t),
h = lcp(t, sa),
res = n,
k = 0;
for (var i = 1; i < n * 2; i++) {
if (sa[i] < n && sa[i - 1] >= n || sa[i - 1] < n && sa[i] >= n) { // 筛选出第一个旋转后为字典序最小的后缀
if (h[i] < res) {
res = h[i];
k = sa[i];
} else if (h[i] === res && t.substr(sa[i], res) < t.substr(k, res)) {
k = sa[i];
}
}
}
return t.substr(k, n);
}
var s = "bcdaba";
console.log(minLexRotation(s)); // 输出:ababcd
该算法的时间复杂度为O(n),其中n为字符串长度。
总结
本文介绍了JavaScript中实现字典序最小字符串旋转的两种方法,暴力枚举与Manber和Myers算法。暴力枚举虽然实现简单,但时间复杂度较高,适合解决小规模的问题。而Manber和Myers算法虽然涉及到了一些较为复杂的数据结构,但时间复杂度较低,适合解决大规模的问题。