【问题标题】:increment an infinite binary counter [closed]增加一个无限二进制计数器[关闭]
【发布时间】:2013-02-07 08:36:24
【问题描述】:

取自编码面试:

您将如何以 O(1) 的时间复杂度实现具有 increment 的无限二进制计数器?

我想计算最右边0的第一个和第二个位置,但我不确定如何实现。

“无限计数器”表示您可以增加无限次(大于 MAX_INT)。

【问题讨论】:

  • 什么是无限二进制计数器?
  • sniff sniff...我发现了一些家庭作业...
  • 什么?增量总是 O(1),问题是什么?
  • @StenPetrov:真的吗?对于任意长度整数?我不这么认为。
  • @JonSkeet 啊哈我明白了……他说的是一个非常大的柜台。感谢您的澄清。那么如果目的是计数使其成为1的链表并且计数器的值是1的个数,递增/递减变为O(1),只是不要尝试其他操作:D

标签: c# binary bit-manipulation


【解决方案1】:

对于二进制计数器...

如果你想让计数器保持“正常”位模式,你不能,从根本上说 - 至少不是 always O(1) 而不是 amortized O(1)。

如果它是一个无限计数器,它可以有任意多个位。这意味着您可以拥有多个 N 位,所有位均为 1。递增该计数器意味着将所有这些位设置为 0,可以合理地假设为 O(N) 操作。

我们可以认为在“正常”计算中增量为 O(1) 的唯一原因是通常处理固定大小的类型,我们可以说(例如)“最多 32 位需要更改 -这是一个常数,因此可以想象它是一个 O(1) 运算。”

只是一个计数器......

另一方面,如果您只是希望能够在 O(1) 时间内递增,那么您将拥有无限的内存,并且您不在乎恢复值需要多长时间,您可以做到这一点,只需有效地使用长度为计数器大小的链表。

例如,在 C# 中:

public DodgySolution
{
    public static DodgySolution Zero = new DodgySolution(null);

    private DodgySolution tail;

    private DodgySolution(DodgySolution tail)
    {
        this.tail = tail;
    }

    // This bit is O(1)
    public DodgySolution Increment()
    {
        return new DodgySolution(this);
    }

    // This bit isn't...
    public BigInteger ToBigInteger()
    {
        return tail == null ? BigInteger.Zero
                            : BigInteger.One + tail.ToBigInteger();
    }
}

即使这假设引用分配是 O(1),但对于无限数量的对象,这可能会变得很棘手......

【讨论】:

  • 会不会更像 O(log N)?毕竟,数字越大,与数字的“价值”成比例要做的工作就越少。
  • 您可以保留最右边的1 和最右边的0 的位置并进行位操作而不是遍历位
  • @voithos:这取决于你的 N 是数字的还是它的大小(以位为单位)。
  • @EladBenda:“位操作”如何让您在 O(1) 中翻转任意数量的位?你跟踪什么并不重要——我怀疑你仍然需要能够更改任意数量的位。
  • @EladBenda:当位宽小于或等于您的指令集大小时,位操作只有 O(1)。
【解决方案2】:
  • 使用某种阵列存储和双倍策略。这意味着分配摊销 O(1)
    链表也应该可以。
  • 使用简单的教科书添加。对于更高的位,进位呈指数级罕见。平均成本为 1+0.5+0.25+... = 2 其中 O(1)

所以一个直接的实现已经摊销了 O(1) 的性能。唯一的问题是您需要可变存储。

查看数字n 的单个增量操作时,平均时间为 O(1),但最坏情况为 O(log(n))。内存使用量为 O(log(n))。

var counter=new List<bool>{false};

void Inc()
{
  while(counter[i])
  {
      counter[i]=false;
      i++;
  }
  if(i==counter.Length)
    counter.Add(true);
  else
    counter[i]=true;
}

【讨论】:

  • 您可以通过懒惰地执行操作将摊销转换为最坏情况的复杂性。在这里你可以保留一堆进位,如果你为每个增量做一两个就足够了。
【解决方案3】:

如果问题是要求增加 O(1) 计数器而没有任何其他限制,您的计数器可以实现为数字的链接列表,并且项目的总和是您的计数器的值.

如果之前的值大于 (Max-1),则递增将等同于在最后一项中添加 1 或添加新项=1。

由于您总是最多检查列表中的 2 个项目,因此增量将是 O(1)

只是不要尝试用你闪亮的新计数器做其他算术:D

【讨论】:

    【解决方案4】:

    我的尝试:

    我们对连续的10 保持聚合。

    意思是111000111是

    我可以用以下 DS 来表示:

    节点列表 { digit : bool, counter: long}

    1) 如果第一个块是 1。它变成大量的 0 并将下一个 0 变成 1。

    我们现在检查是否可以聚合大量的 1。

    2) 如果第一个块是 0,我们将第一个数字设为 1。看看我们是否可以聚合 1。

    示例 A:

    意思是111000111是

    读数:三个1数字,三个0数字,三个1数字,一个0数字

    增量()

    示例 B:

    增量()

    聚合:

    总是会有 const 数量的变化(直到最右边的 0 位)

    并且转动大部分1s 只是切换布尔成员。这是恒定的

    【讨论】:

    • “体积”有多大?如果随着时间的推移它变得更大,那么它不是一个恒定时间算法。 (老实说,我不确定我是否真的理解它的作用。如果你能显示 0-10 的位模式会很好。)
    • @JonSkeet 如果你是not sure you understand what this does,你怎么能预测If that gets bigger over time, it's not a constant time algorithm?我在答案中添加了一个示例。你能解释一下为什么大批量会是一个问题吗?因为它仅由 表示
    • 答案的先前版本有一个循环,看起来不像 O(1)。我会进一步考虑这一点 - 如果您可以将其编写为 实际代码,则可能更容易考虑它最终会变长(或简单地证明它不会变长)。我的怀疑是聚合部分需要您查看可能非常数的块。
    • @JonSkeet 聚合每一步最多涉及 3 个节点。最右边的 0 将变为 1,其左边的数字不会改变。我们只需要检查 0 变为 1 的节点是否聚合,它是 prev 和 next 节点。仅此而已。通过归纳,所有其他都已经聚合。
    • 好吧,我建议你尝试实现它。听起来至少很有趣,可以更详细地了解。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多