【问题标题】:Recursive base conversion time complexity analysis递归基转换时间复杂度分析
【发布时间】:2017-05-29 23:42:43
【问题描述】:

给定一个整数p 和目标基b,返回基bp 的字符串表示。字符串的末尾应该有最低有效位

^ 这是我给自己的问题。

我想出的朴素递归算法(C++)如下:

string convertIntToBaseRecursive(int number, int base) {
  // Base case
  if (!number) return "";

  // Adding least significant digit to "the rest" computed recursively
  // Could reverse these operations if we wanted the string backwards.
  return convertToBaseRecursive(number / base, base) + to_string(number % base);
}

虽然算法非常简单,但我想确保自己了解复杂性细分。我的想法在下面,我想知道它们是正确的还是错误的,如果它们是错误的,那么知道我在哪里偏离轨道会很好!

声明:

  • n = logb(p)是返回字符串的长度
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n)

推理:

为了在字符串末尾保留最低有效位,当它是我们在其他任何东西之前计算的值时,我们要么必须:

  1. 按原样递归计算字符串
  2. 每次我们计算一个位时都保持“移动”数组,这样我们就可以将最近的位添加到字符串的前面,而不是结尾
  3. 将字符串倒写,并在返回之前将其反转(最有效)

我们正在执行上述 C++ 算法中的第一种方法,+ 运算符在每个堆栈帧处创建一个新字符串。初始帧创建并返回一个长度为n 的字符串,下一帧创建一个长度为n-1n-2n-3 等的字符串。遵循这一趋势(无需证明为什么1 + 2 + 3 ... + n = O(n^2),很明显时间复杂度是O(n^2) = O(logb^2(p))。我们也只需要随时将O(n) 的东西存储在内存中。当原始堆栈帧解析(就在算法完成之前)我们将拥有原始字符串的内存,但在它解析之前它将是单个字符(O(1))+递归堆栈帧(O(n))。我们这样做this 在每个级别存储n 数量的单个字符,直到我们完成。因此空间复杂度为O(n)

当然,这个解决方案更有效的版本是

string convertIntToBaseIterative(int number, int base) {
  string retString = "";

  while (number) {
    retString += to_string(number % base);
    number /= base;
  }

  // Only needed if least significant
  reverse(retString.begin(), retString.end());
  return retString;
}

我相信上面的解决方案,n = logb(p) 有:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

这些分析是正确的还是我不在某个地方?

【问题讨论】:

    标签: c++ algorithm recursion numbers base


    【解决方案1】:

    由于返回值必须包含输出,因此您无法获得比O(n) 更好的空间复杂度。

    假设输出字符串按顺序由以下数字组成:a_1, a_2, a_3, ..., a_n。在递归方法中,我们创建一个字符串如下:"a_1" + "a_2" + .... + "a_n"。在迭代方法中,我们这样做:(((...(a_1) + a_2) + a_3) + ... + a_n)))...)。因此,两种情况下的时间复杂度在O(n^2) 应该相同(在 C++03 中。请参阅下面的 C++11 注释)。

    如您所见,这两种方法都深受实现细节的影响。 string 类型对于涉及重复连接的操作不是很有用。如果您有一个大小为n 的预分配数组,则可以将复杂度降低到O(n)

    注意 1: 有一些关于附加操作的细节。在 C++03 中,追加操作没有指定的复杂性,并且可能导致 Copy-On_write(如果字符串无法就地扩展并且需要重定位)。在 C++11 中,不允许使用 CoW 和绳索式实现,并且 append 应该导致每个字符摊销 O(1) 时间。因此,在 C++11 的情况下,我们应该能够获得两种实现的 O(n) 时间复杂度。

    注意2:要获得O(n) 时间复杂度与用户定义的字符串实现(包含长度),字符串需要在函数中通过引用传递。这将导致函数签名更改为:

    void convertToBaseRecursive(int number, int base, MyString& str)
    

    此实现将允许字符串在原地共享和更新,前提是字符串使用预先分配的数组。

    【讨论】:

    • 为什么是迭代方法O(n^2)?我的理解是,更高版本的 C++(也可能更早?)中的 += 运算符充当向量并且每次使每个 += 调用摊销 O(1) 时容量加倍,当完成 O(n) 次 = @987654338 @ 时间复杂度对吗?
    • 如果尺寸翻倍,你是对的。但是,在这两种方法中,构建字符串的数量和顺序应该相同。在递归方法中,在整个递归解开之前,我们不会开始构建字符串。当它完全展开时,我们将从左到右评估它,导致相同的操作集。
    • 我会根据用于连接的算法来更新答案。
    • 顺便说一句,+= 不是免费的,因为除非保持字符串的长度,否则会有一个循环到达字符串的末尾。这导致O(n^2)
    • 啊,好电话。因此,使用字符串,真的没有比二次(真的O(logb^2(p)))时间更好的方法了。堆栈会很好,然后我们可以获取堆栈的长度,并用它的内容填充一个预先分配的字符串(或其他东西)。谢谢!
    【解决方案2】:

    注意:

    鉴于与@user1952500 的聊天室对话,我根据我们讨论的内容对他的回答进行了一些修改。以下是他回答的编辑版本,反映了我们讨论的最新内容和我学到的内容:


    修改后的答案:

    由于返回值必须包含输出,因此您无法获得比 O(n) 更好的空间复杂度。

    假设输出字符串按顺序由以下数字组成:a_1, a_2, a_3, ..., a_n。在里面 递归方法(项目符号#1),我们创建一个字符串如下"a_1" + "a_2" + .... + "a_n",其中 递归产生O(n^2) 时间复杂度。在项目符号 #2 中,迭代方法不断推动角色 到字符串的前面,例如 (((...(a_1) + a_2) + a_3) + ... + a_n)))...),它会移动整个字符串 在每个字符添加上也会产生 O(n^2) 时间复杂度。关于您的书面迭代方法(项目符号 #3) 时间复杂度可以根据 C++ 的版本进行优化(见下面的注释)。

    字符串类型对于涉及重复连接的操作不是很有用。在旧版本的 C++ 中,您 可以通过预分配大小为 n 的字符串来实现O(n) 时间复杂度。在 C++11 中this 答案表明某些附加操作可以优化为对单个字符进行摊销O(1)。假设 确实如此,写出的迭代版本将具有 O(1) 时间复杂度,无需任何额外工作。

    注意:要使用此算法的递归版本获得 O(n) 时间复杂度,我们可以利用已摊销的 O(1) 字符附加并使用通过引用传递的单个字符串。这将需要递归版本的函数签名 改写如下:

    void convertToBaseRecursive(int number, int base, string& str)

    【讨论】:

    • 您可以点击第一个答案上的“编辑”按钮。 (然后她/他可以批准编辑。)
    • @Potatoswatter 我没有这样做的唯一原因是因为我有太多的编辑要做,因此我自己的措辞很多。我还应该这样做吗?我觉得编辑量太大了
    • 它可以去任何一种方式。乍一看,它们仍然很相似。您也可以要求 user195 在答案上启用“社区 wiki”模式,从而将其打开以进行免费编辑。
    猜你喜欢
    • 2020-09-19
    • 1970-01-01
    • 1970-01-01
    • 2022-06-22
    • 2022-10-25
    • 1970-01-01
    • 1970-01-01
    • 2015-12-17
    • 1970-01-01
    相关资源
    最近更新 更多