【问题标题】:How to achieve O(n) worst-case time complexity for this function?如何实现此函数的 O(n) 最坏情况时间复杂度?
【发布时间】:2020-01-19 20:19:58
【问题描述】:

我在执行某项任务时遇到问题。这不是家庭作业或其他任何事情,而是现在的个人问题。我想知道是否有解决方案...

关键是要实现一个函数的预期 O(n) 最坏情况时间复杂度,它需要 2 个字符串数组作为输入(让我们调用第一个数组 A,第二个数组 @ 987654322@) 并且应该返回一个整数数组,其中每个元素表示数组 A 中相应元素的索引。

所以,函数应该是这样的:

private static int[] GetExistingStrings(string[] A, string[] B) { ... }
  • 数组A 包含所有可能的名称
  • 数组B 包含应排除的名称(即,如果存储在B 数组中的某些名称也在A 数组中,则它们的索引不应包含在输出 int[] 数组中;它是该数组也可能包含一些随机字符串,这些字符串不一定存在于A 数组中,甚至可能为空。

例如,如果我们有这些数组:

string[] A = { "one", "two", "three", "four" }; // 0, 1, 2, 3
string[] B = { "two", "three" }; // Indices of "two" and "three" not taken into account

函数应该返回:

int[] result = { 0, 3 }; // Indices of "one" and "four"

一开始,我尝试了一种显而易见且简单的方法(使用嵌套的 for 循环):

private static int[] GetExistingStrings(string[] A, string[] B)
{
    LinkedList<int> aIndices = new LinkedList<int>();

    for (int n = 0; n < A.Length; n++)
    {
        bool isExcluded = false;
        for (int m = 0; m < B.Length; m++)
        {
            if (A[n].Equals(B[m]))
            {
                isExcluded = true;
                break;
            }
        }

        if (!isExcluded)
        {
            aIndices.AddLast(i);
        }
    }

    int[] resultArray = new int[aIndices.Count];
    aIndices.CopyTo(resultArray, 0);
    return resultArray;
}

我使用 LinkedList 是因为我们不可能知道输出的数组大小应该是多少,还因为向该列表添加新节点是一个常量 O(1) 操作。当然,这里的问题是这个函数(我假设)是 O(n*M) 时间复杂度。所以,我们需要另辟蹊径……

我的第二种方法是:

private static int[] GetExistingStrings(string[] A, string[] B)
{
    int n = A.Length;
    int m = B.Length;

    if (m == 0)
    {
        return GetDefaultOutputArray(n);
    }

    HashSet<string> bSet = new HashSet<string>(B);
    LinkedList<int> aIndices = new LinkedList<int>();

    for (int i = 0; i < n; i++)
    {
        if (!bSet.Contains(A[i]))
        {
            aIndices.AddLast(i);
        }
    }

    if (aIndices.Count > 0)
    {
        int[] result = new int[aIndices.Count];
        aIndices.CopyTo(result, 0);
        return result;
    }

    return GetDefaultOutputArray(n);
}

// Just an utility function that returns a default array
// with length "arrayLength", where first element is 0, next one is 1 and so on...
private static int[] GetDefaultOutputArray(int arrayLength)
{
    int[] array = new int[arrayLength];
    for (int i = 0; i < arrayLength; i++)
    {
        array[i] = i;
    }
    return array;
}

这里的想法是将B 数组的所有元素添加到一个HashSet,然后使用它的方法Contains() 在for 循环中检查是否相等。但是我不能完全计算这个函数的时间复杂度......我确定for循环中的代码将执行n次。但是最让我烦恼的是 HashSet 初始化 - 这里应该考虑到它吗?它如何影响时间复杂度?这个函数是 O(n) 吗?还是 O(n+m) 因为 HashSet 初始化?

有什么办法可以解决这个任务并实现O(n)

【问题讨论】:

  • 而不是链表,使用预先设定容量的链表。列表的构造函数允许您指定容量。像 A.Count 这样的东西应该是一个很好的上限。在真正遇到问题之前不要使用链表。与一个好的旧名单相比,他们的表现非常糟糕。最坏的情况是在最后添加一些东西,需要遍历整个现有列表(找到你需要添加的元素)。
  • "有什么办法可以解决这个任务并达到 O(n)?"。不,你能得到的最好结果可能是 O(m*log(n))。
  • 在什么情况下了解复杂性很重要?您是否正在运行这样的代码数百万/数十亿次并且遇到性能问题,或者您是否正在尝试预先优化某些内容?
  • HashSet 对于查找的摊销为 O(1),因此不考虑整体复杂性。 AddLast() 是 O(1)。你有一个 for 循环,所以它是 O(n)。填充哈希集并复制到数组也是 O(n),所以它保持 O(n)。代码不是最优的,但不是你问的。
  • @ErikPhilips 我试图理解这个时间复杂度(Big-O Notation),特别是对于有 2 个变量作为输入的函数。我记得我看到了一个类似于我的任务,其中一个条件是实现O(n) 最坏情况下的时间复杂度,用于具有 2 个字符串数组(作为输入)的函数,并且在一个函数中,您需要简单地检查两个数组的每个元素为了平等。我不相信O(n)在这里是可能的,所以出于好奇,我不得不问。

标签: c# arrays algorithm time-complexity big-o


【解决方案1】:

如果A 中有n 元素,B 中有m 元素,并且字符串的长度为k,则hashmap 方法的预期时间为O(k*(m + n))。不幸的是,如果散列算法不起作用,最糟糕的时间是O(km(m + n))。 (几率非常低。)我之前犯过这个错误,感谢@PaulHankin 的更正。

要获得O(k*(m + n)) 最糟糕的时间,我们必须采取一种非常不同的方法。你要做的是从 B 中构建一个 trie。现在你遍历 A 的每个元素并在 trie 中查找它。与哈希不同,trie 保证了最坏情况下的性能(更好的是,即使我们没有使用它,也允许前缀查找)。这种方法不仅为我们提供了预期的平均时间O(k*(m + n)),而且还为我们提供了相同的最差时间。

您不能做得比这更好,因为仅处理列表就需要处理 O(k*(m + n)) 数据。

【讨论】:

  • 我认为你打错了——我计算出散列的最坏情况是 O(km(n+m)) 假设你建立一个 B 的哈希表并在其中查找 A 的所有元素(以及你的哈希表使用具有相同哈希码的项目的链接列表)。
  • @btilly 您能否澄清一下您是如何为我的第二种方法计算 O(k*(m + n)) 的?你看,我是这方面的新手......所以,在我的理解中: 1. HashSet 的初始化并将数组复制到它是一个O(n) 操作(我相信空间复杂度???); 2.单个for循环,执行n次,里面我们检查A中的元素是否存储在创建的bSet中->它仍然是O(n),因为HashSetContains()方法实际上是一个常量O(1) 操作; 3. 复制到新创建的数组也是线性时间操作-O(n)。我说的对吗?
  • @Faize13 当人们谈论哈希算法时,他们通常隐含地假设计算哈希值是O(1)。但事实上并非如此。您要散列的字符串越长,计算散列值所需的时间就越长。
【解决方案2】:

以下是使用 LINQ 重写第二种方法的方法,同时还可以选择不区分大小写的字符串比较:

public static int[] GetExistingStrings(string[] first, string[] second)
{
    var secondSet = new HashSet<string>(second, StringComparer.OrdinalIgnoreCase);
    return first
        .Select((e, i) => (Element : e, Index : i))
        .Where(p => !secondSet.Contains(p.Element))
        .Select(p => p.Index)
        .ToArray();
}

时间和空间复杂度相同(O(n))。这只是做同样事情的一种更奇特的方式。

【讨论】:

    猜你喜欢
    • 2022-07-07
    • 1970-01-01
    • 2018-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多