如何在O(1)中清零数组?

时间:2012-05-29 10:25:23

标签: c++ time-complexity

有没有办法将时间复杂度为O(1)的数组归零?很明显,这可以通过for循环,memset来完成。但他们的时间复杂度不是O(1)。

9 个答案:

答案 0 :(得分:16)

但不是任何阵列。它需要一个为此工作的数组才能工作。

template <typename T, size_t N>
class Array {
public:
    Array(): generation(0) {}

    void clear() {
        // FIXME: deal with overflow
        ++generation;
    }

    T get(size_t i) const {
        if (i >= N) { throw std::runtime_error("out of range"); }

        TimedT const& t = data[i];
        return t.second == generation ? t.first : T{};
    }

    void set(size_t i, T t) {
        if (i >= N) { throw std::runtime_error("out of range"); }

        data[i] = std::make_pair(t, generation);
    }


private:
    typedef std::pair<T, unsigned> TimedT;

    TimedT data[N];
    unsigned generation;
};

原则很简单:

  • 我们使用generation属性
  • 定义一个纪元
  • 当设置项目时,记录已设置项目的时期
  • 只能看到当前时代的项目
  • 因此,
  • 清算相当于递增纪元计数器

该方法有两个问题:

  • 存储增加:对于我们存储纪元的每个项目
  • 生成计数器溢出:有一些内容作为最大历元数

后者可以使用真正的大整数(uint64_t以更多存储为代价)来阻止。

前者是一种自然结果,一种可能的解决方案是使用存储桶来淡化问题,例如,最多64个与单个计数器关联的项目和一个识别在此计数器内有效的位掩码。


编辑:只是想重新回到桶中的想法。

原始解决方案的每个元素8字节(64位)的开销(如果已经对齐8字节)。根据存储的元素,它可能会或可能不会有什么大不了的。

如果这是一个大问题,我的想法是使用水桶;当然,像所有的权衡一样,它会进一步降低访问速度。

template <typename T>
class BucketArray {
public:
     BucketArray(): generation(0), mask(0) {}

     T get(size_t index, size_t gen) const {
         assert(index < 64);

         return gen == generation and (mask & (1 << index)) ?
                data[index] : T{};
     }

     void set(size_t index, T t, size_t gen) {
         assert(index < 64);

         if (generation < gen) { mask = 0; generation = gen; }

         mask |= (1 << index);
         data[index] = t;
     }

private:
     uint64_t generation;
     uint64_t mask;
     T data[64];
};

请注意,这个固定数量的元素的小数组(我们实际上可以模拟它并静态地检查它是低于或等于64)只有16个字节的开销。这意味着我们有每个元素2位的开销

template <typename T, size_t N>
class Array {
    typedef BucketArray<T> Bucket;
public:
    Array(): generation(0) {}

    void clear() { ++generation; }

    T get(size_t i) const {
        if (i >= N) { throw ... }

        Bucket const& bucket = data[i / 64];
        return bucket.get(i % 64, generation);
    }

    void set(size_t i, T t) {
        if (i >= N) { throw ... }

        Bucket& bucket = data[i / 64];
        bucket.set(i % 64, t, generation);
    }

private:
    uint64_t generation;
    Bucket data[N / 64 + 1];
};

我们将空间开销降低了一倍...... 32.现在,数组甚至可以用于存储char,例如,之前它本来是禁止的。成本是访问速度变慢了,因为我们得到了一个除法模数(当我们得到一个标准化的操作,一次性返回两个结果?)。

答案 1 :(得分:12)

您无法在n内修改内存中的O(n)个位置(即使您的硬件足够小n,也许允许常量操作将某些操作确定为零 - 对齐的内存块,例如闪存等)。

但是,如果练习的对象有点横向思考,那么你可以写一个代表“稀疏”数组的类。稀疏数组的一般概念是你保留一个集合(可能是一个map,虽然取决于它可能不是全部的用法),当你查找一个索引时,如果它不在基础集合然后返回0

如果你可以在O(1)中清除底层集合,那么你可以在O(1)中清零你的稀疏数组。清除std::map通常不是映射大小的常量时间,因为需要释放所有这些节点。但是你可以通过将整个树从“我的地图内容”移动到“我保留以供将来使用的节点树”来设计可以在O(1)中清除的集合。缺点只是仍然分配了这个“保留”空间,有点像vector变小时的情况。

答案 2 :(得分:10)

只要您接受一个非常大的常数因子,就可以将O(1)中的数组清零:

void zero_out_array_in_constant_time(void* a, size_t n)
{
    char* p = (char*) a;
    for (size_t i = 0; i < std::numeric_limits<size_t>::max(); ++i)
    {
        p[i % n] = 0;
    }
}

无论数组的大小如何,这都将采用相同的步数,因此它是O(1)。

答案 3 :(得分:4)

没有

您不能在O(N)时间内访问N元素集合的每个成员。

正如Mike Kwan所观察到的那样,您可能会将成本从运行时转移到编译时,但这并不会改变操作的计算复杂性。

答案 4 :(得分:2)

显然不可能在固定的时间内初始化任意大小的数组。但是,完全可以创建一个类似于数组的ADT,它可以分摊在使用过程中初始化数组的成本。然而,通常的构造需要占存储量的3倍。致白:

template <typename T, size_t arr_size>
class NoInitArray
{
    std::vector<T> storage;

    // Note that 'lookup' and 'check' are not initialized, and may contain
    // arbitrary garbage.
    size_t lookup[arr_size];
    size_t check[arr_size];
public:
    T& operator[](size_t pos)
    {
        // find out where we actually stored the entry for (*this)[pos].
        // This could be garbage.
        size_t storage_loc=lookup[pos];

        // Check to see that the storage_loc we found is valid
        if (storage_loc < storage.size() && check[storage_loc] == pos)
        {
            // everything checks, return the reference.
            return storage[storage_loc];
        }
        else
        {
            // storage hasn't yet been allocated/initialized for (*this)[pos].
            // allocate storage:
            storage_loc=storage.size();
            storage.push_back(T());

            // put entries in lookup and check so we can find 
            // the proper spot later:
            lookup[pos]=storage_loc;
            check[storage_loc]=pos;

            // everything's set up, return appropriate reference:
            return storage.back();
        }
    }
};

如果clear()是某种不需要破坏的类型,至少在概念上可以添加一个T成员来清空这样一个数组的内容。

答案 5 :(得分:1)

在运行时无法将O(1)中的数组清零。这是直观的,因为没有语言机制允许在固定时间内设置任意大小的存储器块。你最接近的是:

int blah[100] = {0};

这将允许初始化在编译时发生。在运行时,memset通常是最快的,但O(N)。但是,problems与特定数组类型使用memset相关联。

答案 6 :(得分:0)

虽然仍为O(N),但映射到硬件辅助操作(如清除整个缓存行或内存页)的实现可以每个字的<1个周期运行。

实际上,对Steve Jessop的想法嗤之以鼻......

如果您有硬件支持可以同时清除任意大量的内存,则可以执行此操作。如果你假定一个任意大的数组,那么你也可以假定一个具有硬件并行性的任意大存储器,这样一个复位引脚就可以同时清除每个寄存器。该线路必须由一个任意大的逻辑门驱动(消耗任意大的功率),并且电路走线必须任意短(以克服R / C延迟)(或超导),但这些都很常见在超维空间。

答案 7 :(得分:0)

我喜欢Eli Bendersky的网页http://eli.thegreenplace.net/2008/08/23/initializing-an-array-in-constant-time,他的解决方案归功于Aho,Hopcroft和Ullman着名的书籍计算机算法设计和分析。这确实是初始化的O(1)时间复杂度,而不是O(N)。空间需求是O(N)额外存储,但是分配这个空间也是O(1),因为空间充满了垃圾。由于理论上的原因,我很喜欢这个,但我认为如果你需要重复初始化一个非常大的数组,并且每次只访问数组中相对较少的位置,那么实现某些算法也可能具有实际价值。 Bendersky提供了算法的C ++实现。

一个非常纯粹的理论家可能会开始担心N需要O(log(N))数字,但我忽略了这个细节,这可能需要仔细查看计算机的数学模型。 计算机编程艺术的第1卷可能给出了Knuth对此问题的看法。

答案 8 :(得分:0)

可以用O(1)的时间做到这一点,甚至可以用O(1)的额外空间来做到这一点。

我解释了David在另一个answer中提到的O(1)时间解,但是它使用了2n的额外内存。

存在更好的算法,该算法仅需要1位额外的内存。
请参阅我刚刚写的关于该主题的Article
它还说明了David提到的算法以及其他一些信息,以及当今的最新算法。它还具有后者的implementation

在简短的解释中(正如我将要重复的文章所述),它巧妙地采用了David的答案中提出的算法并将其全部就位,同时仅使用了(很少)额外的内存。