这两种算法的比较?

17

我遇到一个问题,需要判断一个字符串是否包含所有独特的字符。

我写了一个解决方案,将每个字符添加到一个集合中,但是如果该字符已经存在,则返回false。

private static boolean allUniqueCharacters(String s) {

    Set<Character> charSet = new HashSet<Character>();
    for (int i = 0; i < s.length(); i++) {
        char currentChar = s.charAt(i);
        if (!charSet.contains(currentChar)) {
            charSet.add(currentChar);

        } else {
            return false;
        }

    }
    return true;

}

根据我正在阅读的书,这是“最优解”

public static boolean isUniqueChars2(String str) {
    if (str.length() > 128)
        return false;

    boolean[] char_set = new boolean[128];

    for (int i = 0; i < str.length(); i++) {
        int val = str.charAt(i);

        if (char_set[val]) {
            return false;
        }
        char_set[val] = true;
    }

    return true;
}

我的问题是,我的实现比提供的那个实现要慢吗?我认为是,但是如果散列查找是O(1),那么它们的复杂度不是相同的吗?

谢谢。


15
由于Java字符是16位的,数值范围从0到65535,因此在Java中,“solution”书中的方法行不通,所以一个128字节的数组是行不通的。 - Jim Garrison
22
它们具有相同的复杂性,但不一定速度相同。在楼梯上搬运10个花瓶的复杂程度与搬运10架钢琴的复杂程度相同,但所需付出的努力略有不同。哈希O(1)比数组O(1)要慢,因为哈希需要至少计算哈希值。此外,哈希是摊销的O(1),而不是真正的O(1);从复杂性的角度来看并不重要,但却影响速度。 - Amadan
5
苹果和橙子。您的实现更加通用,因为它不假设这是纯ASCII码。您可以进行一项性能改进,即摆脱contains操作,仅比较集合和字符串的大小。一旦它们开始有所不同,就返回false。 - Jilles van Gurp
7
如果作者认为只有128个字符,我建议把这本书扔掉。 - phuclv
3
公平起见,您的算法在 contains() 和 add() 方法中均计算了哈希两次。add() 方法返回元素是否已经存在,因此不需要额外进行 contains() 检查。 - Nathan Adams
显示剩余11条评论
6个回答

12

正如Amadan在评论中所说,这两种解决方案的时间复杂度都是O(n),因为你有一个循环遍历字符串,并且在循环中执行常量时间操作。这意味着运行方法所需的时间随字符串长度线性增加。

请注意,时间复杂度关乎当你改变输入大小时所需时间的变化。它不涉及在相同大小数据下速度有多快。

对于同一字符串,“最优”解决方案应该更快,因为集合对于数组存在一些额外开销。处理数组比处理集合要快。然而,要使“最优”解决方案实际起作用,您需要一个长度为2^16的数组。这就是有多少不同的char值。您还需要删除检查字符串是否超过128个字符的限制。

这是空间和时间之间折衷的众多例子之一。如果您想让它运行得更快,您需要更多的空间。如果您想节省空间,那么您必须慢一些。


3

这两种算法的时间复杂度均为O(N),但它们的空间复杂度不同。

书中的解决方案将始终需要存储128个字符的空间 - O(1),而您的解决方案的空间需求将根据输入线性变化 - O(N)

书中的空间需求基于假定的128个字符集。但考虑到可能需要不同的字符集,这可能会带来问题(并且不易扩展)。


@fsdff 这完全取决于面试官的期望,因此非常主观。但是你的解决方案对我来说似乎非常正常/常见。 - ernest_k
我不认为OP的解决方案是O(N)。当哈希表中输入了所有可能的键时,它将不再增长。所需空间与数组一样是O(1)。 - user1196549
@YvesDaoust 你假设了一些字符集,这是有道理的...除非复杂度分析的目的是了解最坏情况的想法。无论字符集是否有限,在这种情况下,事实是空间复杂度取决于输入,并且没有明确提到字符集的大小。 - ernest_k
@ernest_k:无论字母表大小是多少,假设为M,空间复杂度都没有区别:都是O(M)(或者如果你想要更精确一些,是O(M Log M))。与你所说的相反,它不依赖于输入的大小。 - user1196549
@YvesDaoust 更准确些。但是MN是由函数的相同输入定义的,我想这就是我试图表达的意思。 - ernest_k
显示剩余2条评论

2

哈希表在理论上是可接受的,但是会浪费资源。

哈希表建立在数组之上(因此它肯定比数组更昂贵),并且冲突解决需要额外的空间(至少是元素数量的两倍)。此外,任何访问都需要计算哈希值,可能还需要解决冲突。

与直接使用数组相比,这增加了很多开销。

另外要注意,哈希表具有O(1)的行为是一种流言。最坏情况下,访问一个大小为N的表可能需要花费O(N)的时间。


最后一句话,该算法的时间复杂度为O(1),因为当N>128时,最坏情况下你得出的结论是false。


尽管对哈希表进行单个访问的最坏情况比O(1)更糟糕,但这在这里完全不相关,因为算法执行Θ(n)访问,而不是单个访问。除非使用了烂哈希算法或冲突避免方案,否则算法的预期运行时间仍然为O(n)。我很好奇你打算如何改进渐近空间复杂度,在你的回答中你没有解释清楚。 - Konrad Rudolph
@KonradRudolph:例如,返回常量的哈希函数。在这个问题中,没有任何可以渐进地改进的东西。我只是说散列带来了实际上的无益处,相反地。 - user1196549
但是这不是正在使用的哈希函数,也不是通常(或有用地)计算哈希表操作复杂度的方式。但好吧,你说哈希没有实际好处;那么请告诉我,如果要针对Unicode字母表实现此算法,你会如何实现? - Konrad Rudolph
@KonradRudolph:这个问题是关于ASCII码的。 - user1196549
我的印象是,在问题下面的评论中已经确定这个问题不是关于ASCII的,相反,书中给出的答案是错误的。无论如何,ASCII包含127个有效字符,而不是128个,所以最多书中的答案仍然是不称职的。 - Konrad Rudolph

1

你的算法也是O(1)。可以将复杂度看作我的算法对处理元素数量变化的反应如何。因此,O(n)O(2n)实际上是相等的。

人们正在这里讨论O符号的增长率。


不是O(1)。绝对不是。除非输入大小有一个上限。 - user202729

1

你的解决方案可能比书上的解决方案慢。首先,哈希查找理想情况下具有常数时间查找。但是,如果存在多个哈希冲突,则对象的检索将不是常数时间。其次,即使它是常数时间查找,与通过索引在数组中查找元素相比,执行哈希代码函数通常涉及显着的开销。这就是为什么你可能想选择数组查找。然而,如果你开始处理非ASCII Unicode字符,那么由于空间开销的显着增加,你可能不想采用数组方法。


0

你的实现瓶颈在于,集合的查找(和插入)复杂度为O(log k),而数组的查找复杂度为O(1)

这听起来像是你的算法要差得多。但事实上并非如此,因为k受到128的限制(否则参考实现将出错并产生越界错误),可以视为常数。这使得集合查找的复杂度也为O(1),只不过比数组查找的常数稍大。

*假设实现合理,如树或哈希表。哈希表的时间复杂度通常不是常数,因为填充它需要log(n)次调整大小操作,以避免冲突增加导致线性查找时间,参见这里这里的stackoverflow答案。

这篇文章甚至解释了Java 8在查找时间退化为O(n)之前,会将哈希映射转换为二叉树(转换的时间复杂度为O(n log n),查找的时间复杂度为O(log n)),以避免因碰撞过多而导致性能下降。


我把这个讨论移到了聊天室里:https://chat.stackoverflow.com/rooms/178457/discussion-between-allo-and-konrad-rudolph。 - allo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接