【问题标题】:recursion vs iteration time complexity递归与迭代时间复杂度
【发布时间】:2020-07-10 20:16:42
【问题描述】:

任何人都可以准确解释幕后发生的事情,以使以下问题中的递归方法在时间复杂度方面更快更有效吗?

问题:编写一个程序,将整数数组作为输入,并返回在数组中排序的最大三个数字,而不对原始(输入)数组进行排序。

例如:

  • 输入:[22, 5, 3, 1, 8, 2]

  • 输出:[5, 8, 22]

尽管我们可以简单地对原始数组进行排序并返回最后三个元素,但这至少需要O(nlog(n)) 时间,因为最快的排序算法可以做到这一点。所以挑战是更好地执行并在O(n)时间完成任务。

所以我能够想出一个递归解决方案:

def findThreeLargestNumbers(array, largest=[]):

    if len(largest) == 3:
        return largest

    max = array[0]
    for i in array:
        if i > max:
            max = i

    array.remove(max)
    largest.insert(0, max)
    return findThreeLargestNumbers(array, largest)

我一直在其中找到最大的数字,将其从原始数组中删除,将其附加到我的空数组中,然后再次递归调用该函数,直到我的数组中有三个元素。

但是,当我查看建议的迭代方法时,我编写了以下代码:

def findThreeLargestNumbers(array):
    sortedLargest = [None, None, None]
    for num in array:
        check(num, sortedLargest)
    return sortedLargest

def check(num, sortedLargest):
    for i in reversed(range(len(sortedLargest))):
        if sortedLargest[i] is None:
            sortedLargest[i] = num
            return
        if num > sortedLargest[i]:
            shift(sortedLargest, i, num)
            return

def shift(array, idx, element):
    if idx == 0:
        array[0] = element
        return array
    array[0] = array[1]
    array[idx-1] = array[idx]
    array[idx] = element
    return array

两个代码都成功通过了所有测试,我确信迭代方法更快(尽管不是那么干净......)。但是,我导入了时间模块并通过提供一百万个随机整数的数组并计算每个解决方案返回最大三个数字的排序数组需要多长时间来测试代码。

递归方法比迭代方法快得多(大约快 9 倍)!

这是为什么呢?即使递归方法遍历巨大的数组 3 次,除此之外,每次它删除一个元素(这需要 O(n) 时间,因为所有其他 999 元素都需要在内存中移动),而迭代方法是只遍历输入数组一次,是的,在每次迭代时都进行一些操作,但是使用一个非常微不足道的大小为 3 的数组,甚至根本不需要时间!

我真的希望能够针对任何给定问题判断和选择最有效的算法,因此任何解释都会有很大帮助。

【问题讨论】:

    标签: python arrays algorithm data-structures


    【解决方案1】:

    优化建议。

    避免函数调用。避免产生临时垃圾。避免额外的比较。具有尽可能少地查看元素的逻辑。手动浏览您的代码是如何工作的,并查看它需要多少步骤。

    您的递归代码仅进行 3 次函数调用,并且正如其他地方所指出的,每次调用平均进行 1.5 次比较。 (1 寻找最小值,0.5 找出移除元素的位置。)

    您的迭代代码对每个元素进行大量比较,调用多余的函数,并调用诸如 sorted 之类的东西来创建/销毁垃圾。

    现在与这个迭代解决方案进行比较:

    def find_largest(array, limit=3):
        if len(array) <= limit:
            # Special logic not needed.
            return sorted(array)
        else:
            # Initialize the answer to values that will be replaced.
            min_val = min(array[0:limit])
            answer = [min_val for _ in range(limit)]
    
            # Now scan for smallest.
            for i in array:
                if answer[0] < i:
                    # Sift elements down until we find the right spot.
                    j = 1
                    while j < limit and answer[j] < i:
                        answer[j-1] = answer[j]
                        j = j+1
                    # Now insert.
                    answer[j-1] = i
    
            return answer
    

    没有函数调用。每个元素最多可以进行 6 次比较(验证 answer[0] &lt; i,验证 (j=1) &lt; 3,验证 answer[1] &lt; i,验证 (j=2) &lt; 3,验证 answer[2] &lt; i,然后找到 (j=3) &lt; 3不是真的)。如果对array 进行排序,您将遇到最坏的情况。但大多数情况下,您只进行第一次比较,然后移动到下一个元素。没有麻烦,没有大惊小怪。

    它是如何进行基准测试的?

    请注意,如果您想要最小的 100 个元素,那么您会发现使用更智能的数据结构(例如 heap)来避免冒泡排序是值得的。

    【讨论】:

    • 对于性能测试,我真的很难对您的算法进行基准测试,因为它在递归方法的时间复杂度方面惊人地保持了相同的结果。但是,当为这两种算法提供从 0 到 1000000000 的 3000 万个随机整数数组时,我能够注意到您的算法确实(并且可能不准确)的微小优势。但是当提供排序数组时,递归方法显然更快(大约快 3.5 倍)。你的答案很有价值。感谢您传播您的知识。
    【解决方案2】:

    我对 python 不是很满意,但我有不同的方法来解决这个问题,因为它的价值。 据我所知,所有发布的解决方案都是O(N<em>M)</em>,其中 N 是数组的长度,M 是最大元素数组的长度。 由于您的具体情况N &gt;&gt; M,您可以说它是O(N),但输入的时间越长,它就会越多O(NM) 我同意@zvone 的观点,您似乎在迭代解决方案中有更多步骤,这听起来像是对您不同计算速度的有效解释。 回到我的提议,用递归实现二分查找O(N*logM):

    import math
    
    def binarySearch(arr, target, origin = 0):
        """ 
        Recursive binary search
        Args:
            arr (list): List of numbers to search in
            target (int): Number to search with
        Returns:
            int: index + 1 from inmmediate lower element to target in arr or -1 if already present or lower than the lowest in arr
        """
        half = math.floor((len(arr) - 1) / 2);
        if target > arr[-1]:
            return origin + len(arr)
        if len(arr) == 1 or target < arr[0]:
            return -1
        if arr[half] < target and arr[half+1] > target:
            return origin + half + 1
        if arr[half] == target or arr[half+1] == target:
            return -1
        if arr[half] < target:
            return binarySearch(arr[half:], target, origin + half)
        if arr[half] > target: 
            return binarySearch(arr[:half + 1], target, origin)
        
    def findLargestNumbers(array, limit = 3, result = []):
        """ 
        Recursive linear search of the largest values in an array
        Args:
            array (list): Array of numbers to search in
            limit (int): Length of array returned. Default: 3
        Returns:
            list: Array of max values with length as limit
        """    
        if len(result) == 0:
            result = [float('-inf')] * limit
        if len(array) < 1:
            return result
        val = array[-1]
        foundIndex = binarySearch(result, val)
        if foundIndex != -1:
            result.insert(foundIndex, val)
            return findLargestNumbers(array[:-1],limit, result[1:])
        return findLargestNumbers(array[:-1], limit,result)
    

    它非常灵活,可能会激发更详细的答案。

    【讨论】:

      【解决方案3】:

      递归解决方案

      递归函数遍历列表 3 次找出最大的数,并从列表中删除最大的数 3 次。

      for i in array:
          if i > max:
              ...
      

      和

      array.remove(max)
      

      所以,你有 3×N 的比较,加上 3x 的删除。我猜移除是在 C 中优化的,但再次进行大约 3×(N/2) 次比较才能找到要移除的项目。

      因此,总共进行了大约 4.5 × N 次比较。

      另一种解决方案

      另一种解决方案只遍历列表一次,但每次都与sortedLargest 中的三个元素进行比较:

      for i in reversed(range(len(sortedLargest))):
          ...
      

      几乎每次它都会使用这三个分配对sortedLargest 进行排序:

      array[0] = array[1]
      array[idx-1] = array[idx]
      array[idx] = element
      

      那么,你是N次:

      • 致电check
      • 创建和反转range(3)
      • 访问sortedLargest[i]
      • 比较num &gt; sortedLargest[i]
      • 致电shift
      • 比较idx == 0

      大约 2×N/3 次做:

      array[0] = array[1]
      array[idx-1] = array[idx]
      array[idx] = element
      

      N/3 次array[0] = element

      很难计算,但这远远超过 4.5×N 次比较。

      【讨论】:

        猜你喜欢
        • 2023-03-08
        • 2023-03-04
        • 2023-03-24
        • 1970-01-01
        • 2017-01-25
        • 2021-10-01
        • 2023-03-13
        • 2019-06-05
        • 2015-12-14
        相关资源
        最近更新 更多