推断字符串是否包含所有唯一字符(并且不使用任何其他数据结构)的通用算法表示要遍历字符串,针对搜索匹配的整个字符串迭代每个字母。这种方法是 O(n ^ 2)。
下面的方法(用C语言编写)使用偏移来迭代字符串部分,因为例如在短字符串中没有理由用第一个字符作为第一个字符测试最后一个字符。
我的问题是:算法的运行时间是 O(n!)还是类似 O(nlogn)?
#include <stdio.h>
int strunique(const char *str)
{
size_t offset = 1;
char *scout = (char *)str, *start;
for (; *scout != '\0'; ++scout, ++offset)
for (start = (char *)str + offset; *start != '\0'; ++start)
if (*start == *scout)
return 0;
return 1;
}
int main(void)
{
printf("%d\n", strunique("uniq"));
printf("%d\n", strunique("repatee"));
return 0;
}
答案 0 :(得分:22)
除了其他答案之外,我还想指出问题可以在没有额外内存的O(1)
中解决,也不需要修改输入字符串的内容。
首先,做strnlen(str, 256)
。如果超过255,return 0
。根据鸽子原则,某些角色必须不止一次出现。此操作仅需O(1)
,因为我们只检查字符串的有界前缀。
现在,字符串比常量(256)短,所以使用任何朴素算法仅在O(1)
个额外时间内完成。
答案 1 :(得分:19)
不,它仍然是O(n ^ 2)。你只是稍微提高了常量。你仍然需要做两个循环 - 基本上天真的计数循环测量大O时间应该告诉你这个。
此外,没有O(n + 1 / 2n)这样的东西。 Big O表示法是为了让您了解应该采取的数量级。 n + 1 / 2n = 1.5n。由于大O掉落所有常数因子,那就是n。
你可以在没有额外记忆的情况下击败O(n ^ 2)。如果没有别的,您可以通过ascii值(nlog(n)时间)对字符串进行排序,然后遍历数组,寻找du(n次)对于O(n + nlogn)= O(nlogn)时间。可能还有其他技巧。
请注意,排序方法可能无法提供更好的运行时间 - 天真的方式具有1的最佳案例运行时,而排序第一算法必须排序,因此它具有nlogn的最佳情况。所以最好的大时间可能不是最好的选择。
答案 2 :(得分:11)
如果 可能的字符数(不要与字符串的长度相混淆)未修复(不是这里的情况) )算法的时间复杂度为 O(n ^ 2)。 如果我们假设只有固定数量的有效字符(在这种情况下为255
/ 4G
),那么您的算法会在最坏情况下运行 O(n)的 即可。如果条件成立,则可以轻松地改进算法以在 O(1) 中运行。
注意渐近行为和大哦:这些是理论结果。这不是因为算法在 O(1)中运行,而是在合理的时间内运行。这意味着它在恒定的时间内运行。所以 - 渐近地说 - 无论你输入长度为10 1000 的字符串还是长度为10 10&#39; 的字符串,都不会有任何区别。这些长度足够大)。它所花费的时间也可能超过宇宙年龄的一百倍。
你可以在for循环上做一个简单的比最坏情况分析:
for (; *scout != '\0'; ++scout, ++offset)
for (start = (char *)str + offset; *start != '\0'; ++start)
//single statement
现在我们想知道单个语句(它包含固定数量的语句)将重复多少次。由于您从未修改字符串的内容。我们知道有一个索引 n ,其值为\0
。
所以我们可以把它重写为:
for (scout = 0,offset = 0; scout < n; ++scout, ++offset)
for (start = offset; *start < n; ++start)
//single statement
(我假设字符串从内存地址0
开始),但由于这只是一个允许的转移,因此只能简单地推理它。
现在我们要计算内部for
循环中的语句数(参数化)。这等于:
o 的偏移量和 n 字符串的长度。
现在我们可以使用这个公式计算外for
- 循环级别的指令数。这里 o 以0
开头,并遍历(不包括)n
。所以说明总数是:
O(n ^ 2)。
但现在必须要问:是否有可能构建这样的字符串?答案是不!只有255
个有效字符(NUL
字符不被视为字符);如果我们不能做出这个假设,则上述成立。假设第一个字符是a
(带有任意字符),然后它与字符串中的另一个a
匹配,可以在 O(n)中解析time(循环遍历字符串的其余部分);或者它表示所有其他字符与a
不同。在第一种情况下,算法终止于O(n);在第二种情况下,这意味着第二个字符是不同的。
我们说第二个字符是b
。然后我们再次遍历 O(n)中的字符串,如果它找到另一个b
,我们终止 2n 或 O(n)< / em>步骤。如果没有,我们需要尝试找到下一个字符c
的匹配项。
关键是我们只需要最多255
次:因为只有255个有效字符。因此,时间复杂度 255n 或 O(n)。
此解释的另一个变体是&#34;如果外部for
循环正在寻找第i个字符,我们知道i左边的所有字符都与该字符不同(否则我们早就已经拒绝了。&#34; 现在因为只有255
个字符而左边的所有字符都彼此不同,而且当前字符不同,我们知道{ {1}} - 字符串的字符,我们再也找不到新的不同字符了,因为没有这样的字符。
假设您有一个包含256
个字符(3
,a
和b
)的字母 - 这只是为了让您更容易理解此事。现在说我们有一个字符串:
c
很明显,您的算法将使用 O(n)步骤:scout
v
b a a a a a b
^
start
只会迭代整个字符串一次,到达start
并返回。
现在说字符串中没有b
的重复内容。在这种情况下,算法在迭代字符串一次后不会停止。但是这意味着所有其他字符应该与 a 不同(毕竟我们在字符串上迭代,并且没有找到重复)。所以现在考虑一个具有该条件的字符串:
b
现在很明显,在字符串的其余部分中首次尝试查找字符scout
v
b a a a a a a
^
start
将会失败。现在你的算法增加了侦察兵:
b
开始搜索 scout
v
b a a a a a a
^
start
。在这里,我们非常幸运:第一个字符是a
。但如果有重复的a
;它最多需要花费两次迭代,所以 O(2n)才能找到副本。
现在我们已经达到了约束的情况:也没有a
。在这种情况下,我们知道字符串必须以
a
我们进一步知道字符串的其余部分不能包含 scout
v
b a c ...
和b
,因为否则a
永远不会移动那么远。唯一剩下的可能性是字符串的其余部分包含scout
&#39; s。所以字符串写着:
c
这意味着在遍历字符串最多3次之后,无论字符串的大小如何,我们都会发现这样的重复,或者字符在该字符串中的分布情况。
您可以轻松修改此算法,让它在O(1)时间内运行:只需在索引上放置其他边界:
scout
v
b a c c c c c c c c c c
^
start
在这种情况下,我们限制了第一个循环,使其最多访问前255个字符,内循环仅访问第一个 256 (通知int strunique(const char *str)
{
size_t offset = 1;
char *scout = (char *)str, *start, *stop = scout+255;
for (; scout < stop && *scout != '\0'; ++scout, ++offset)
for (start = (char *)str + offset; start <= stop && *start != '\0'; ++start)
if (*start == *scout)
return 0;
return 1;
}
代替<=
)。因此,总步数受 255 x 256 或 O(1)的限制。上面的解释已经证明了为什么这就足够了。
注意:如果这是
<
,您需要将C
替换为255
,这使得它理论上确实 O(n)< / em>,但实际上是 O(n ^ 2),因为 n 之前的常数因子对于 O(n) O(n ^ 2)将胜过它。
由于我们将字符串终止检查与4'294'967'296
结合起来 - 检查此算法的运行速度几乎总是比上面提出的算法快。可能额外周期的唯一来源是附带修改后的256
- 循环的附加测试。但由于这些会导致更快的终止,因此在许多情况下不会产生额外的时间。
可以说:&#34; 是的,对于长度大于或等于256个字符的字符串,这是真的。&#34;,&#34;如何使用字符串大小小于for
?&#34;。关键是大哦分析处理渐近行为。即使某些字符串的行为是超指数小于或等于某个长度,您也不必考虑这些因素。
更多地强调渐近行为的(有问题的)方面。可以说,以下算法渐近地说是正确的:
256
它总是返回false;因为&#34;存在长度n 0 ,使得对于每个输入长度n> n 0 这个算法将返回正确的结果。&#34; 这与big-oh本身没什么关系,它更多地说明一个人必须要小心在 O(1)中运行的算法将胜过 O(n ^ 6)中的算法以获得任何合理的输入。有时,常数因素可能是巨大的。
答案 3 :(得分:5)
您的算法为O(N^2)
。通过简单地注意到在最坏的情况下,一个包含所有唯一字符的字符串,每个字符必须与其后面的每个字符进行检查,这很容易。也就是说,最坏情况下,N*(N-1)/2 = O(N^2)
比较。
请注意by definition:
f(x) = O(g(x))
如果存在某个常数,|f(x)| <= M|g(x)|
对于所有足够大的x
。{因此,当您说f(x) = O(n + 1/2n)
(对您的算法不正确)时,请遵循:
f(x) = O(n + 1/2n)
f(x) <= M * (n + 1/2n) for some M, n_0 for n >= n_0, by definition
f(x) <= (3/2 * M) n, n >= n_0
f(x) <= M' n, setting M' = 3/2 M, n >= n_0
f(x) = O(n), by definition
也就是说,常量总是会丢失,这就是为什么你可能会听到常量不重要的表达式(至少就计算运行时复杂性而言 - 显然它们对实际性能很重要)
答案 4 :(得分:3)
包含所有唯一字符的字符串的长度最多为255.在这种情况下,您的算法将在O(1)时间内运行。
如果字符串包含重复字符,则字符串的前255个元素中会出现其中一个重复字符。然后最坏的情况是字符串的前254个字符是唯一的,并且第255个字符重复直到字符串的结尾。然后你的算法在O(N)时间内运行。
您可以通过首先检查字符串的长度是否大于255并且如果是的话立即失败来保证算法的O(1)时间。
所有这些假设char
采用256个值中的一个。如果将char
中的字符数视为变量C,则在字符串仅包含唯一字符的情况下,算法的复杂度为O(C ^ 2),在字符串的情况下为O(NC)包含重复项,您可以通过首先检查字符串的长度是否大于C来保证O(C ^ 2)时间。
最优算法是O(min(N,C)),首先检查字符串是否长于C,然后使用任何线性时间重复检测算法。