有没有办法将时间复杂度为O(1)的数组归零?很明显,这可以通过for循环,memset来完成。但他们的时间复杂度不是O(1)。
答案 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)
我解释了David在另一个answer中提到的O(1)时间解,但是它使用了2n的额外内存。
存在更好的算法,该算法仅需要1位额外的内存。
请参阅我刚刚写的关于该主题的Article。
它还说明了David提到的算法以及其他一些信息,以及当今的最新算法。它还具有后者的implementation。
在简短的解释中(正如我将要重复的文章所述),它巧妙地采用了David的答案中提出的算法并将其全部就位,同时仅使用了(很少)额外的内存。