【问题标题】:What is the time complexity of this recursive solution for removing duplicates?这种用于删除重复项的递归解决方案的时间复杂度是多少?
【发布时间】:2021-02-13 08:52:19
【问题描述】:

在我完成一个 Leetcode 问题后,我总是尝试同时确定渐近时间复杂度,以进行练习。

我现在正在查看问题26. Remove Duplicates from Sorted Array

给定一个排序数组nums,就地删除重复项,使得 每个元素只出现一次并返回新的长度。

不要为另一个数组分配额外的空间,你必须这样做 使用 O(1) 额外内存就地修改输入数组

澄清:

困惑为什么返回的值是一个整数,但你的答案是 数组?

注意输入数组是通过reference传入的,这意味着一个 调用者也将知道对输入数组的修改。

在内部你可以这样想:

// nums is passed in by reference. (i.e., without making a copy) int
len = removeDuplicates(nums);

// any modification to nums in your function would be known by the caller. 
// using the length returned by your function, it prints the first len elements.
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入: nums = [1,1,2]
输出: 2, nums = [1,2]
解释: 你的 函数应返回长度 = 2,其中 nums 的前两个元素 分别为 1 和 2。你留下什么并不重要 返回的长度。

我的代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    nums.forEach((num,i) => {
       if(nums[i+1] !== null && nums[i+1] == nums[i] ){
           nums.splice(i, 1);
           console.log(nums)
           removeDuplicates(nums)
       }
    })
    return nums.length;
};

对于这个问题,我从我的研究中得到了 O(log n)。每次运行时执行时间减半。有人可以验证或确定我是否错了?

所有的递归函数都是天生的O(logn)吗?即使有多个循环?

【问题讨论】:

  • 您的数组中是否需要文字 null
  • 为什么是递归的?我没有看到这里的优势。
  • 这可能是 O(n),但是您的解决方案更糟糕,因为它会重新检查递归调用中的所有元素。
  • "所有的递归函数本质上都是 O(long) 吗?即使有多个循环"(假设您的意思是 log n)- 不是。不可能所有递归函数都具有这种性能特征。 len = (list, count = 0) =&gt; list.length === 0 ? count : len(count.slice(1), count + 1) 将只是 O(n) 并且不能是其他任何东西。 一些递归函数将是O(log n),如树遍历或二分搜索。但是,有些会具有更高的复杂性,例如查找所有排列或斐波那契数。
  • 认为你的算法是 O(n²) 最坏情况和 O(n) 最好情况,平均情况介于两者之间。 (实际回答您的问题)

标签: javascript algorithm recursion duplicates


【解决方案1】:

对于这个问题,我从我的研究中得到了 O(log n)。每次运行时执行时间减半。有人可以验证或确定我是否错了?

每次运行的执行时间不会减半:想象一个极端情况,输入有 100 个值并且它们都相同。然后在递归树的每一层 一个 将找到并删除这些重复项。然后进行更深层次的递归调用。因此,对于每个重复值,递归树中都有一个级别。所以在这种极端情况下,递归树的深度为 99。

即使你要修改算法,也不可能做到O(log n),因为数组中的所有值都需要至少读取一次,而且仅此而已给它一个O(n)的时间复杂度。

您的实现使用splice,它需要移动删除点之后的所有值,因此一个splice 已经O(n),使您的算法O(n² )(最坏的情况)。

由于递归,它在最坏的情况下(用于调用堆栈)也会使用 O(n) 额外空间。

所有的递归函数都是固有的O(logn)吗?

没有。使用递归并没有说明整体时间复杂度。它可以是任何东西。当您在进行递归调用时可以忽略当前数组的 O(n)(如一半)时,通常会得到 O(logn)。例如,二分搜索算法就是这种情况。

改进

您可以通过不使用递归,而是使用迭代方法来避免额外的空间。此外,您不需要实际更改给定数组的长度,只需返回它的新长度应该是什么。所以你可以避免使用splice。相反,在数组中使用两个索引:一个运行到下一个不同的字符,另一个运行较慢的索引,您将新字符复制到该索引。当较快的索引到达输入的末尾时,较慢的索引表示具有唯一值的部分的大小。

看起来是这样的:

var removeDuplicates = function(nums) {
    if (nums.length == 0) return 0;
    let len = 1;
    for (let j = 1; j < nums.length; j++) {
        if (nums[j-1] !== nums[j]) nums[len++] = nums[j];
    }
    return len;
};

【讨论】:

  • 如果你不想修改数组,你应该写len++而不是nums[len++] = nums[j];。如果您确实想要对数组进行就地过滤,则还需要在循环后将剩余的元素放在末尾nums.length = len;
  • OP 应该添加 Leetcode 问题的规范,我相信是this one。它需要就地修改,并返回新长度而不需要缩短输入数组。这个解决方案通过了 Leetcode 的测试。
  • "你在返回的长度之外留下什么并不重要。" - 哦,哇,这太奇怪了。但是,示例输出 do 缩短了数组,并且似乎没有排除这样做的要求。
  • 确实,这很奇怪。由于 Leetcode 测量执行时间并为您提供比较分数,Leetcode 有一个习惯是排除任何不需要的东西,比如这里:调整数组的长度。
  • @Tyrique,这回答了你的问题吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-09-23
  • 1970-01-01
  • 2017-01-25
  • 2020-06-12
  • 2020-09-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多