【问题标题】:Time complexity calculation for my algorithm我的算法的时间复杂度计算
【发布时间】:2016-08-23 03:50:33
【问题描述】:

给定一个字符串,找出其中第一个不重复的字符并返回它的索引。如果不存在,则返回-1。您可以假设字符串仅包含小写字母。

我将定义一个散列来跟踪字符的出现。从左到右遍历字符串,检查当前字符是否在哈希中,如果是则继续,否则在另一个循环中遍历字符串的其余部分,查看当前字符是否存在。如果不存在则返回索引,如果存在则更新哈希。

def firstUniqChar(s):

    track = {}
    for index, i in enumerate(s):
        if i in track:
            continue
        elif i in s[index+1:]: # For the last element, i in [] holds False
            track[i] = 1
            continue
        else:
            return index
    return -1

firstUniqChar('timecomplexity')

我的算法的时间复杂度(平均和最差)是多少?

【问题讨论】:

  • 找到第一个不重复的字符 - 这应该意味着例如对于abbacdec,算法将返回a(它不重复)或d(它只存在一次)?
  • 非重复方式仅存在一次@miraculixx 在你的情况下,返回 5

标签: python algorithm


【解决方案1】:

您的算法的时间复杂度为O(kn),其中k 是字符串中唯一字符的数量。如果k 是一个常数,那么它就是O(n)。由于问题描述清楚地限制了元素的替代数量(“假设小写(ASCII)字母”),因此k 是恒定的,并且您的算法在O(n) 时间运行这个问题。即使 n 将增长到无穷大,您也只会对字符串进行O(1) 切片,而您的算法将保持为O(n)。如果您删除了track,那么它将是O(n²)

In [36]: s = 'abcdefghijklmnopqrstuvwxyz' * 10000

In [37]: %timeit firstUniqChar(s)
100 loops, best of 3: 18.2 ms per loop

In [38]: s = 'abcdefghijklmnopqrstuvwxyz' * 20000

In [37]: %timeit firstUniqChar(s)
10 loops, best of 3: 36.3 ms per loop

In [38]: s = 'timecomplexity' * 40000 + 'a'

In [39]: %timeit firstUniqChar(s)
10 loops, best of 3: 73.3 ms per loop

它几乎认为T(n) 仍然具有O(n) 复杂性-它与字符串中的字符数完全线性地缩放,即使这是您算法的最坏情况-没有单一的独特的性格。


我将在这里介绍一种效率不高但简单而聪明的方法;先用collections.Counter统计字符直方图;然后遍历找到那个的字符

from collections import Counter
def first_uniq_char_ultra_smart(s):
    counts = Counter(s)
    for i, c in enumerate(s):
        if counts[c] == 1:
            return i

    return -1

first_uniq_char('timecomplexity')

这具有O(n)的时间复杂度; CounterO(n) 时间计算直方图,我们需要再次枚举字符串中的 O(n) 字符。但是在实践中,我相信我的算法具有较低的常数,因为它使用Counter 的标准字典。

让我们做一个非常愚蠢的蛮力算法。由于您可以假设字符串仅包含小写字母,因此请使用该假设:

import string
def first_uniq_char_very_stupid(s):
    indexes = []
    for c in string.ascii_lowercase:
        if s.count(c) == 1:
            indexes.append(s.find(c))

    # default=-1 is Python 3 only
    return min(indexes, default=-1)

让我们在 Python 3.5 上测试我的算法和在其他答案中找到的一些算法。我选择了一个对 my 算法不利的案例:

In [30]: s = 'timecomplexity' * 10000 + 'a'

In [31]: %timeit first_uniq_char_ultra_smart(s)
10 loops, best of 3: 35 ms per loop

In [32]: %timeit karin(s)
100 loops, best of 3: 11.7 ms per loop

In [33]: %timeit john(s)
100 loops, best of 3: 9.92 ms per loop

In [34]: %timeit nicholas(s)
100 loops, best of 3: 10.4 ms per loop

In [35]: %timeit first_uniq_char_very_stupid(s)
1000 loops, best of 3: 1.55 ms per loop

所以,我的愚蠢算法是最快的,因为它在最后找到a 并退出。而且我的智能算法最慢,除了这种最坏的情况之外,我的算法性能不佳的另一个原因是OrderedDict 是在 Python 3.5 上用 C 编写的,而 Counter 是在 Python 中编写的。


让我们在这里做一个更好的测试:

In [60]: s = string.ascii_lowercase * 10000

In [61]: %timeit nicholas(s)
100 loops, best of 3: 18.3 ms per loop

In [62]: %timeit karin(s)
100 loops, best of 3: 19.6 ms per loop

In [63]: %timeit john(s)
100 loops, best of 3: 18.2 ms per loop

In [64]: %timeit first_uniq_char_very_stupid(s)
100 loops, best of 3: 2.89 ms per loop

所以看来我的“愚蠢”算法一点也不愚蠢,它利用了 C 的速度,同时最大限度地减少了 Python 代码运行的迭代次数,并且在这个问题上明显获胜。

【讨论】:

  • 哦,不错。到目前为止,我最喜欢这个。
  • @JohnZwinck 实际上这比 Karin 的慢,有趣的是,我认为 OrderedDict 会更慢。
  • 有趣,我现在仍然很惊讶我的算法比你的情况和我分配的其他一些 s 更快(假设 Counter 是用 Python 编写的,如你所说)。我建议您也可以在 Jupyter 中为我的代码计时。实际上,我很确定我的算法的最坏情况是 O(n^2) 但我更感兴趣的是平均时间复杂度(似乎没有人明确回答),因为我发现这里的问题非常区分大小写,即,它似乎很大程度上取决于 s。
  • 如果只像现在实现的那样不实现计数器,我的速度真的可以快 3-4 倍。
  • @NicholasLiu 编辑了我的答案,您的算法在 O(n) 时间内运行。
【解决方案2】:

正如其他人所指出的,由于嵌套线性搜索,您的算法是 O(n²) 正如 @Antti 所发现的,OP 的算法是线性的,并且受 O(kn) 的约束,对于 k 作为所有可能的小写字母的数量。

我对@9​​87654324@ 解决方案的建议:

from collections import OrderedDict

def first_unique_char(string):
    duplicated = OrderedDict()  # ordered dict of char to boolean indicating duplicate existence
    for s in string:
        duplicated[s] = s in duplicated

    for char, is_duplicate in duplicated.items():
        if not is_duplicate:
            return string.find(char)
    return -1

print(first_unique_char('timecomplexity'))  # 4

【讨论】:

  • Yours 返回索引,而不是字符:)
  • 哇,你说得对 - 我对函数名的理解太深了 -_- 哇,这会让它变得不那么好:(
  • 其实你的算法太快了,直接返回string.find(char)
  • 哈哈谢谢你的拯救!我真的很想保留duplicated[s] = s in duplicated 并且几乎伤心欲绝!
  • 你需要string.find吗?为什么不枚举duplicated的值并返回第一个False值的索引?
【解决方案3】:

您的算法是 O(n2),因为您在 s 的循环内对 s 的切片进行了“隐藏”迭代。

更快的算法是:

def first_unique_character(s):
    good = {} # char:idx
    bad = set() # char
    for index, ch in enumerate(s):
        if ch in bad:
            continue
        if ch in good: # new repeat
            bad.add(ch)
            del good[ch]
        else:
            good[ch] = index

    if not good:
        return -1

    return min(good.values())

这是 O(n),因为 in 查找使用哈希表,并且不同字符的数量应该比 len(s) 少得多。

【讨论】:

  • O(n) 更好但还不够,因为sorted 按照Python TimeComplexityO(n log n)
  • sorted(good.values())[0] 替换为min(good.values())(在Python 2.x 上使用good.viewvalues() 以避免临时的lists),它又回到了O(n)。请注意,就非重复值的数量而言,sorted 步骤是O(n log n),这可能远小于总字符串大小(这是其他大操作系统中的n),所以它可能是不是很大,但是,min 直接做你想要的工作,保证与大 O 成本无关(因为主要算法是 O(n) 在字符串长度上,min 步骤是 O(n)在较小的n)。
  • 由于in badin good 的搜索,它最终可能仍然是O(n^2)
  • @miraculixx:Python 的setdict 被实现为哈希表;是的,哈希表的最坏情况查找成本是O(n),但是对于 Python 中的单个字符来说,实际的哈希冲突是不可能的(现在完整的 Unicode 空间的大小小于 21 位,我相信 Python 的哈希码至少是所有构建都为 32 位,64 位构建为 64 位),因此冲突只会是散列不匹配的桶冲突,并且随着 dict 的增长,重新散列往往会使一种大小的冲突在更大的大小上消失。在实践中,查找将是O(1)
  • 真的,你说得对,O(n^2) 在技术上是最坏的情况,但如果不是不可能触发它,那将是非常困难的,而且通常不值得担心这种可能性Python,尤其是现代 Python 3(其中超过一定长度的字符串使用具有每个解释器启动密钥的加密安全哈希算法,这意味着在一个会话中发生冲突的密钥不会在另一个会话中发生冲突,并通过故意触发呈现 DoS哈希冲突可能不是问题)。只需将dictset 视为“O(1),但具有更高的常数因子”。
猜你喜欢
  • 2011-06-21
  • 2021-08-25
  • 1970-01-01
  • 2015-03-09
  • 2018-10-15
  • 1970-01-01
  • 1970-01-01
  • 2014-12-20
  • 2016-02-27
相关资源
最近更新 更多