【问题标题】:Complexity of reverse sentence algorithm逆句算法的复杂度
【发布时间】:2017-05-31 08:19:45
【问题描述】:

我正在研究 Python 中的数据结构问题,我必须以最有效的方式反转数组中单词的顺序。我想出了以下问题的解决方案

def reverse(arr, st, end):
    while st < end:
        arr[st], arr[end] = arr[end], arr[st]
        end -= 1
        st += 1

def reverse_arr(arr):
    arr = arr[::-1]
    st_index = 0
    length = len(arr)

    for i, val in enumerate(arr):
        if val == ' ':
            end_index = i-1
            reverse(arr, st_index, end_index)
            st_index = end_index + 2

        if i == length - 1:
            reverse(arr, st_index, length-1)

    return arr

如果 arr 是:

arr = [ 'p', 'e', 'r', 'f', 'e', 'c', 't', ' ',
        'm', 'a', 'k', 'e', 's', ' ',
        'p', 'r', 'a', 'c', 't', 'i', 'c', 'e' ]

返回:

['p', 'r', 'a', 'c', 't', 'i', 'c', 'e', ' ', 
 'm', 'a', 'k', 'e', 's', ' ', 
 'p', 'e', 'r', 'f', 'e', 'c', 't']

该解决方案运行良好,但我不明白该算法的复杂度是 O(n)。它写到遍历数组两次,每个项目的动作数量恒定是线性的,即 O(n),其中 n 是数组的长度。

我认为它应该超过 O(n),因为根据我的说法,每个单词的长度不是固定的,反转每个单词的时间复杂度取决于单词的长度。有人可以更好地解释这一点吗?

【问题讨论】:

  • O(n) 不是常数时间,即 O(1),它是线性时间,因此所用时间确实取决于 list 的长度;我认为您需要阅读有关时间复杂度的更多信息
  • @Chris_Rands 我认为它应该不仅仅是线性的,因为我们还应该考虑反转每个单词。我不明白反转每个单词的复杂性是O(1)。
  • 您也可以将其视为O(n * k),其中n 是数组长度,k 是单词的平均长度。在科学文章中,您必须同时包含两者,甚至对k 进行分析,但在通常情况下,您可以假设k 在某种程度上是恒定的。对于具有较长单词的数据集,它会更高一些,对于普通文本,它可能会有点波动,但对于足够长的文本,它会保持不变。如果您输入的是实际单词,您将很少看到长度为 25+ 的内容(除非处理德语)。总而言之,它很容易被认为是不重要的。
  • O(n) 中的 n 是整个列表的长度 arr,这是所有单词的累积长度(单独反转每个单词也是 O(n),其中n 是单词的长度)
  • 我同意@JonClements。我什至会不使用 for 和:rev_arr = list(' '.join(''.join(arr).split()[::-1]))(快大约 3 倍)

标签: python algorithm data-structures


【解决方案1】:

reverse 每个单词都会被调用一次。在该调用期间,它会为每个角色做大量的工作。

您可以用字数和平均字长(即O(wordCount*averageWordLength))或数组中的字符总数来表示它。如果你做后者,很容易看出你仍然在为每个字符做恒定的工作量(因为reversereverse_arr 都在为每个字符做恒定的工作量,没有两个reverse 调用会包含相同的字符),导致O(characterCount) 复杂性。

我不会假设解释中的“数组的长度”是指单词的数量,而是字符的数量,或者他们假设单词长度具有固定的上限(其中复杂度确实是O(wordCount))。

TL;DR: n in O(n)characterCount,而不是 wordCount

【讨论】:

    【解决方案2】:
    def reverse(arr, st, end):
        while st < end:
            arr[st], arr[end] = arr[end], arr[st]
            end -= 1
            st += 1
    
    def reverse_Cha(arr):
        arr = arr[::-1]
        st_index = 0
        length = len(arr)
    
        for i, val in enumerate(arr):
            if val == ' ':
                end_index = i-1
                reverse(arr, st_index, end_index)
                st_index = end_index + 2
    
            if i == length - 1:
                reverse(arr, st_index, length-1)
    
        return arr
    
    def reverse_Jon(arr):
        r = [ch for word in ' '.join(''.join(arr).split()[::-1]) for ch in word]
        return r
    
    def reverse_Nua(arr):
        rev_arr = list(' '.join(''.join(arr).split()[::-1]))
        return rev_arr
    

    如果我们考虑 3 个建议的解决方案:您的为 reverse_Cha,Jon Clements 的为 reverse_Jon,我的为 reverse_Nua。 我们注意到,当我们使用[::-1] 时,我们有O(n),当我们检查列表的每个元素时(长度n)等等。

    reverse_Cha 使用[::-1],然后检查每个元素两次(读取然后交换),因此复杂性取决于元素的总数(O(3n+c) 我们写为O(n)+c 来来自O(1)操作))

    reverse_Jon 使用[::-1],然后检查每个元素两次(检查每个单词的每个字符),因此复杂性取决于元素总数和单词数(O(3n+m),我们写为O(n+m) (用m字数)

    reverse_Nua使用[::-1],然后坚持python列表函数,复杂度因此仍然取决于元素的总数(这次直接O(n)

    就性能而言(1e6 循环),我们得到了reverse_Cha: 2.785867s; reverse_Jon:4.11845s(由于for); reverse_Nua: 1.185973s。

    【讨论】:

      【解决方案3】:

      我认为这是一个纯粹的理论问题,因为在现实世界的应用程序中,您可能宁愿将列表拆分为一个单词的子列表,然后以相反的顺序重新加入子列表 - 这需要更多内存,但速度要快得多。

      话虽如此,我想指出您展示的算法确实是 O(n) - 它取决于您的单词的 total 长度,而不是个别的话。换句话说:20 个 3 个字母的单词、6 个 10 个字母的单词、10 个 6 个字母的单词都需要相同的时间……你总是只遍历每个字母两次:在单个单词的反转期间一次(这是第一次调用reversereverse_arr 中)和整个数组的反转期间一次(第二次调用reverse)。

      【讨论】:

      • 我同意 20 个 3 字母单词、6 个 10 字母单词、10 个 6 字母单词需要相同的时间,但单词的长度不固定。任何地方都没有提到所有单词都是固定长度的。
      • 哦,没关系。只要字长总和相同,执行时间就保持不变,例如“10个3字母词和5个6字母词”也与上述所有其他示例的时间相同。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-04-22
      • 1970-01-01
      • 2013-11-01
      • 2016-04-12
      • 2011-06-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多