发现Lexicographically minimal string rotation是一个众所周知的问题,让·皮埃尔·杜瓦尔(Jean Pierre Duval)于1983年提出了一个linear time algorithm。博客This可能是唯一公开谈论的资源。详细的算法。但是,Duval的算法基于成对比较(“ duels”)的概念,并且博客方便地使用偶数长度的字符串作为示例。
该算法如何处理奇数长度的字符串,而最后一个字符将没有与之竞争的对决?
答案 0 :(得分:1)
一个人物可以赢得“ bye”,而无需参加“决斗”就可以获胜。算法的正确性不依赖于您执行的特定对决。给定 any 两个不同的索引 i 和 j ,您始终可以得出结论,其中之一是字典最小的起始索引旋转(除非都是是词典最小旋转的相同的起始索引,在这种情况下,您拒绝哪个旋转都没有关系)。按特定顺序执行决斗的原因是性能:通过确保一半的决斗只需要比较一个字符,其余的一半只需要比较两个字符来获得渐近线性时间,依此类推,直到最后一次决斗只需要比较字符串长度的一半即可。但是,这里和那里的单个奇数字符并没有改变渐近复杂性,它只是使数学(和实现)更加复杂。长度为2 n +1的字符串仍然需要比长度2 n +1 更少的“决斗”
答案 1 :(得分:0)
此处操作:我接受了ruakh的回答,因为它与我的问题有关,但我想为可能会偶然发现这篇文章的其他人提供自己的解释,以了解Duval的算法。
问题:
从字法上讲,最小圆形子串是查找的问题 具有最低词典顺序的字符串的旋转 所有这些旋转。例如,字典上最小的 旋转“ bbaaccaadd”将是“ aaccaaddbb”。
解决方案:
Jean Pierre Duval(1983)提出了一种O(n)时间算法。
给定两个索引i
和j
,Duval的算法比较了从j - i
和i
开始的长度为j
的字符串段(称为”决斗” )。如果index + j - i
大于字符串的长度,则该段通过环绕而形成。
例如,考虑s =“ baabbaba”,i = 5,j =7。由于j-i = 2,所以从i = 5开始的第一段是“ ab”。从j = 7开始的第二个段是通过环绕而构建的,并且也是“ ab”。 如果字符串在字典上是相等的,例如上面的示例,我们选择从i开始的那一个作为获胜者,即i = 5。
重复上述过程,直到我们有一个获胜者。如果输入字符串的长度为奇数,则在第一个迭代中无需比较就可以赢得最后一个字符。
时间复杂度:
第一次迭代比较长度为1的n个字符串(n / 2个比较),第二次迭代可以比较长度为2的n / 2个字符串(n / 2个比较),依此类推,直到第i次迭代比较2个长度为n / 2的字符串(n / 2个比较)。由于每次获胜者的数量减半,因此递归树的高度为log(n),因此我们得到了O(n log(n))算法。对于小n,大约为O(n)。
空间复杂度也是O(n),因为在第一次迭代中,我们必须存储n / 2个获胜者,第二次迭代中要存储n / 4个获胜者,依此类推。 (维基百科声称此算法使用恒定空间,我不知道如何使用。)
这是一个Scala实现;随时转换为您喜欢的编程语言。
def lexicographicallyMinRotation(s: String): String = {
@tailrec
def duel(winners: Seq[Int]): String = {
if (winners.size == 1) s"${s.slice(winners.head, s.length)}${s.take(winners.head)}"
else {
val newWinners: Seq[Int] = winners
.sliding(2, 2)
.map {
case Seq(x, y) =>
val range = y - x
Seq(x, y)
.map { i =>
val segment = if (s.isDefinedAt(i + range - 1)) s.slice(i, i + range)
else s"${s.slice(i, s.length)}${s.take(s.length - i)}"
(i, segment)
}
.reduce((a, b) => if (a._2 <= b._2) a else b)
._1
case xs => xs.head
}
.toSeq
duel(newWinners)
}
}
duel(s.indices)
}