我们有一个排序数组,我们希望将一个索引的值增加1个单位(array [i] ++),这样生成的数组仍然是排序的。这可能在O(1)中吗? 可以在STL和C ++中使用任何可能的数据结构。
在一个更具体的情况下,如果数组是由所有0值初始化的,并且它总是通过将索引值增加1来递增构造,那么是否存在O(1)解?
答案 0 :(得分:22)
我没有完全解决这个问题,但我认为总体思路至少可能对整数有所帮助。以更多内存为代价,您可以维护一个单独的数据结构,该结构维护一系列重复值的结束索引(因为您希望将递增的值与重复值的结束索引进行交换)。这是因为重复的值会导致您遇到最糟糕的O(n)
运行时:假设您有[0, 0, 0, 0]
并且您在位置0
处增加了值。然后O(n)
找出最后一个位置(3
)。
但是,让我们说你维护我提到的数据结构(地图可以工作,因为它有O(1)
查找)。在这种情况下,你会有这样的事情:
0 -> 3
因此,您有一系列以0
位置结束的3
值。当您增加一个值时,假设在位置i
处,您检查新值是否大于i + 1
处的值。如果不是,你很好。但如果是,则查看辅助数据结构中是否存在此值的条目。如果没有,你可以简单地交换。如果是条目,则查找结束索引,然后与该位置的值交换。然后,您需要对辅助数据结构进行任何更改,以反映阵列的新状态。
更全面的例子:
[0, 2, 3, 3, 3, 4, 4, 5, 5, 5, 7]
辅助数据结构是:
3 -> 4
4 -> 6
5 -> 9
假设您在位置2
处增加值。因此,您将3
增加到4
。该阵列现在看起来像这样:
[0, 2, 4, 3, 3, 4, 4, 5, 5, 5, 7]
你看下一个元素,即3
。然后,在辅助数据结构中查找该元素的条目。该条目为4
,这意味着有3
一段以4
结尾。这意味着您可以将当前位置的值与索引4
处的值进行交换:
[0, 2, 3, 3, 4, 4, 4, 5, 5, 5, 7]
现在您还需要更新辅助数据结构。具体来说,3
的运行会提前结束一个索引,因此您需要递减该值:
3 -> 3
4 -> 6
5 -> 9
您需要做的另一项检查是查看该值是否再次重复。您可以查看i - 1
和i + 1
个位置来查看它们是否与相关值相同。如果两者都不相等,则可以从地图中删除该值的条目。
同样,这只是一个普遍的想法。我将不得不编写它以查看它是否按照我的想法运行。
请随意挖洞。
<强>更新强>
我在JavaScript中实现了这个算法here。我只使用JavaScript,所以我可以快速完成。此外,因为我很快编码它可能会被清理干净。我确实有评论。我也没有做任何深奥的事情,所以这应该很容易移植到C ++。
算法基本上有两个部分:递增和交换(如果需要),以及在地图上完成的簿记,用于跟踪重复值运行的结束索引。
代码包含一个测试工具,它以零数组开头并递增随机位置。在每次迭代结束时,都会进行测试以确保对数组进行排序。
var array = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
var endingIndices = {0: 9};
var increments = 10000;
for(var i = 0; i < increments; i++) {
var index = Math.floor(Math.random() * array.length);
var oldValue = array[index];
var newValue = ++array[index];
if(index == (array.length - 1)) {
//Incremented element is the last element.
//We don't need to swap, but we need to see if we modified a run (if one exists)
if(endingIndices[oldValue]) {
endingIndices[oldValue]--;
}
} else if(index >= 0) {
//Incremented element is not the last element; it is in the middle of
//the array, possibly even the first element
var nextIndexValue = array[index + 1];
if(newValue === nextIndexValue) {
//If the new value is the same as the next value, we don't need to swap anything. But
//we are doing some book-keeping later with the endingIndices map. That code requires
//the ending index (i.e., where we moved the incremented value to). Since we didn't
//move it anywhere, the endingIndex is simply the index of the incremented element.
endingIndex = index;
} else if(newValue > nextIndexValue) {
//If the new value is greater than the next value, we will have to swap it
var swapIndex = -1;
if(!endingIndices[nextIndexValue]) {
//If the next value doesn't have a run, then location we have to swap with
//is just the next index
swapIndex = index + 1;
} else {
//If the next value has a run, we get the swap index from the map
swapIndex = endingIndices[nextIndexValue];
}
array[index] = nextIndexValue;
array[swapIndex] = newValue;
endingIndex = swapIndex;
} else {
//If the next value is already greater, there is nothing we need to swap but we do
//need to do some book-keeping with the endingIndices map later, because it is
//possible that we modified a run (the value might be the same as the value that
//came before it). Since we don't have anything to swap, the endingIndex is
//effectively the index that we are incrementing.
endingIndex = index;
}
//Moving the new value to its new position may have created a new run, so we need to
//check for that. This will only happen if the new position is not at the end of
//the array, and the new value does not have an entry in the map, and the value
//at the position after the new position is the same as the new value
if(endingIndex < (array.length - 1) &&
!endingIndices[newValue] &&
array[endingIndex + 1] == newValue) {
endingIndices[newValue] = endingIndex + 1;
}
//We also need to check to see if the old value had an entry in the
//map because now that run has been shortened by one.
if(endingIndices[oldValue]) {
var newEndingIndex = --endingIndices[oldValue];
if(newEndingIndex == 0 ||
(newEndingIndex > 0 && array[newEndingIndex - 1] != oldValue)) {
//In this case we check to see if the old value only has one entry, in
//which case there is no run of values and so we will need to remove
//its entry from the map. This happens when the new ending-index for this
//value is the first location (0) or if the location before the new
//ending-index doesn't contain the old value.
delete endingIndices[oldValue];
}
}
}
//Make sure that the array is sorted
for(var j = 0; j < array.length - 1; j++) {
if(array[j] > array[j + 1]) {
throw "Array not sorted; Value at location " + j + "(" + array[j] + ") is greater than value at location " + (j + 1) + "(" + array[j + 1] + ")";
}
}
}
答案 1 :(得分:10)
在一个更具体的情况下,如果数组是由所有0值初始化的,并且它总是通过将索引值增加1来递增构造,那么是否存在O(1)解?
没有。给定一个全0的数组:[0, 0, 0, 0, 0]
。如果你增加第一个值,给出[1, 0, 0, 0, 0]
,那么你必须进行4次交换以确保它保持排序。
给定一个没有重复的排序数组,答案是肯定的。但是在第一次操作之后(即第一次增加),你可能会有重复。你做的越多,你重复的可能性就越大,O(n)就越有可能保持那个数组的排序。
如果您只拥有数组,则无法保证每个增量的时间少于O(n)。如果您要查找的是支持排序顺序和按索引查找的数据结构,那么您可能需要order stastic tree。
答案 2 :(得分:3)
如果值很小,则计数排序将起作用。将数组[0,0,0,0]
表示为{4}
。递增任何零给出{3,1}
:3个零和一个零。通常,要增加任何值x,从x的计数中减去一个并增加{x + 1}的计数。空间效率是O(N),其中N是最高值。
答案 3 :(得分:1)
这取决于有多少项可以具有相同的值。如果更多的项可以具有相同的值,则不可能使用普通数组的O(1)。
让我们举个例子:假设array [5] = 21,你想做数组[5] ++:
增加项目:
array[5]++
(因为它是一个数组,所以是O(1))。
所以,现在数组[5] = 22。
检查下一项(即数组[6]):
如果array [6] == 21,那么你必须继续检查新项目(即数组[7]等等),直到找到一个高于21的值。此时,您可以交换值。此搜索不 O(1),因为您可能需要扫描整个数组。
相反,如果项目不能具有相同的值,那么您有:
增加项目:
array[5]++
(因为它是一个数组,所以是O(1))。
所以,现在数组[5] = 22。
下一个项目不能是21(因为两个项目不能具有相同的值),因此它必须具有值&gt; 21,数组已经排序。
答案 4 :(得分:1)
所以你采用排序数组和哈希表。你越过数组来找出'平坦'区域 - 元素具有相同的值。对于每个平坦的区域,你必须弄清楚三件事情1)它开始的地方(第一个元素的索引)2)它的值是什么3)下一个元素的值是什么(下一个更大的元素)。然后将此元组放入哈希表中,其中键将是元素值。这是先决条件,它的复杂性并不重要。
然后当你增加一些元素(索引i)时,你会查找一个表以获取下一个更大元素的索引(称之为j),并将i
与i - 1
交换。然后1)向哈希表添加新条目2)更新现有条目的前一个值。
使用完美的哈希表(或可能值的有限范围),它几乎是O(1)。缺点:它不会稳定。
以下是一些代码:
#include <iostream>
#include <unordered_map>
#include <vector>
struct Range {
int start, value, next;
};
void print_ht(std::unordered_map<int, Range>& ht)
{
for (auto i = ht.begin(); i != ht.end(); i++) {
Range& r = (*i).second;
std::cout << '(' << r.start << ", "<< r.value << ", "<< r.next << ") ";
}
std::cout << std::endl;
}
void increment_el(int i, std::vector<int>& array, std::unordered_map<int, Range>& ht)
{
int val = array[i];
array[i]++;
//Pick next bigger element
Range& r = ht[val];
//Do the swapping, so last element of that range will be first
std::swap(array[i], array[ht[r.next].start - 1]);
//Update hashtable
ht[r.next].start--;
}
int main(int argc, const char * argv[])
{
std::vector<int> array = {1, 1, 1, 2, 2, 3};
std::unordered_map<int, Range> ht;
int start = 0;
int value = array[0];
//Build indexing hashtable
for (int i = 0; i <= array.size(); i++) {
int cur_value = i < array.size() ? array[i] : -1;
if (cur_value > value || i == array.size()) {
ht[value] = {start, value, cur_value};
start = i;
value = cur_value;
}
}
print_ht(ht);
//Now let's increment first element
increment_el(0, array, ht);
print_ht(ht);
increment_el(3, array, ht);
print_ht(ht);
for (auto i = array.begin(); i != array.end(); i++)
std::cout << *i << " ";
return 0;
}
答案 5 :(得分:0)
是和否。
如果列表仅包含唯一的整数,则为是,因为这意味着您只需要检查下一个值。在任何其他情况下都没有。如果值不是唯一的,则递增N个重复值中的第一个意味着它必须移动N个位置。如果值是浮点值,则x和x + 1之间可能有数千个值
答案 6 :(得分:0)
非常清楚要求是非常重要的;最简单的方法是将问题表达为ADT(抽象数据类型),列出所需的操作和复杂性。
以下是我认为您正在寻找的内容:提供以下操作的数据类型:
Construct(n)
:创建一个大小为n
的新对象,其值均为0
。
Value(i)
:返回索引i
的值。
Increment(i)
:增加索引i
的值。
Least()
:返回值最小的元素的索引(如果有多个,则返回一个这样的元素)。
Next(i)
:在从i
开始的排序遍历中返回元素Least()
之后的下一个元素的索引,这样遍历将返回每个元素。
除了构造函数之外,我们希望上述每个操作都具有复杂性O(1)
。我们还希望对象占用O(n)
空间。
该实现使用桶列表;每个桶都有一个value
和一个元素列表。每个元素都有一个索引,指向它所属的桶的指针。最后,我们有一个指向元素的指针数组。 (在C ++中,我可能使用迭代器而不是指针;在另一种语言中,我可能会使用侵入式列表。)不变量是没有桶是空的,并且桶的value
是严格单调的增加。
我们从一个值为0的存储桶开始,其中包含n
个元素的列表。
Value(i)
是通过返回迭代器在数组的元素i
引用的元素的桶的值来实现的。 Least()
是第一个存储桶中第一个元素的索引。 Next(i)
是元素i
上迭代器引用的元素之后的下一个元素的索引,除非迭代器已经指向列表的末尾,在这种情况下它是第一个元素。下一个桶,除非元素的桶是最后一个桶,在这种情况下我们就在元素列表的末尾。
感兴趣的唯一界面是Increment(i)
,如下所示:
如果元素i
是其存储桶中唯一的元素(即存储桶列表中没有下一个元素,而元素i
是存储桶列表中的第一个元素):< / p>
O(1)
,无论列表的大小如何,因为它只是一个指针交换),然后删除下一个桶。如果元素i
不是其存储桶中的唯一元素,则:
i
推送到下一个存储桶列表中。i
的新存储桶,并将其插入此存储桶和下一个存储桶之间。答案 7 :(得分:0)
我认为可以不使用哈希表。我在这里有一个实现:
#include <cstdio>
#include <vector>
#include <cassert>
// This code is a solution for http://stackoverflow.com/questions/19957753/maintain-a-sorted-array-in-o1
//
// """We have a sorted array and we would like to increase the value of one index by only 1 unit
// (array[i]++), such that the resulting array is still sorted. Is this possible in O(1)?"""
// The obvious implementation, which has O(n) worst case increment.
class LinearIncrementor
{
public:
LinearIncrementor(int numElems);
int valueAt(int index) const;
void incrementAt(int index);
private:
std::vector<int> m_values;
};
// Free list to store runs of same values
class RunList
{
public:
struct Run
{
int m_end; // end index of run, inclusive, or next object in free list
int m_value; // value at this run
};
RunList();
int allocateRun(int endIndex, int value);
void freeRun(int index);
Run& runAt(int index);
const Run& runAt(int index) const;
private:
std::vector<Run> m_runs;
int m_firstFree;
};
// More optimal implementation, which increments in O(1) time
class ConstantIncrementor
{
public:
ConstantIncrementor(int numElems);
int valueAt(int index) const;
void incrementAt(int index);
private:
std::vector<int> m_runIndices;
RunList m_runs;
};
LinearIncrementor::LinearIncrementor(int numElems)
: m_values(numElems, 0)
{
}
int LinearIncrementor::valueAt(int index) const
{
return m_values[index];
}
void LinearIncrementor::incrementAt(int index)
{
const int n = static_cast<int>(m_values.size());
const int value = m_values[index];
while (index+1 < n && value == m_values[index+1])
++index;
++m_values[index];
}
RunList::RunList() : m_firstFree(-1)
{
}
int RunList::allocateRun(int endIndex, int value)
{
int runIndex = -1;
if (m_firstFree == -1)
{
runIndex = static_cast<int>(m_runs.size());
m_runs.resize(runIndex + 1);
}
else
{
runIndex = m_firstFree;
m_firstFree = m_runs[runIndex].m_end;
}
Run& run = m_runs[runIndex];
run.m_end = endIndex;
run.m_value = value;
return runIndex;
}
void RunList::freeRun(int index)
{
m_runs[index].m_end = m_firstFree;
m_firstFree = index;
}
RunList::Run& RunList::runAt(int index)
{
return m_runs[index];
}
const RunList::Run& RunList::runAt(int index) const
{
return m_runs[index];
}
ConstantIncrementor::ConstantIncrementor(int numElems) : m_runIndices(numElems, 0)
{
const int runIndex = m_runs.allocateRun(numElems-1, 0);
assert(runIndex == 0);
}
int ConstantIncrementor::valueAt(int index) const
{
return m_runs.runAt(m_runIndices[index]).m_value;
}
void ConstantIncrementor::incrementAt(int index)
{
const int numElems = static_cast<int>(m_runIndices.size());
const int curRunIndex = m_runIndices[index];
RunList::Run& curRun = m_runs.runAt(curRunIndex);
index = curRun.m_end;
const bool freeCurRun = index == 0 || m_runIndices[index-1] != curRunIndex;
RunList::Run* runToMerge = NULL;
int runToMergeIndex = -1;
if (curRun.m_end+1 < numElems)
{
const int nextRunIndex = m_runIndices[curRun.m_end+1];
RunList::Run& nextRun = m_runs.runAt(nextRunIndex);
if (curRun.m_value+1 == nextRun.m_value)
{
runToMerge = &nextRun;
runToMergeIndex = nextRunIndex;
}
}
if (freeCurRun && !runToMerge) // then free and allocate at the same time
{
++curRun.m_value;
}
else
{
if (freeCurRun)
{
m_runs.freeRun(curRunIndex);
}
else
{
--curRun.m_end;
}
if (runToMerge)
{
m_runIndices[index] = runToMergeIndex;
}
else
{
m_runIndices[index] = m_runs.allocateRun(index, curRun.m_value+1);
}
}
}
int main(int argc, char* argv[])
{
const int numElems = 100;
const int numInc = 1000000;
LinearIncrementor linearInc(numElems);
ConstantIncrementor constInc(numElems);
srand(1);
for (int i = 0; i < numInc; ++i)
{
const int index = rand() % numElems;
linearInc.incrementAt(index);
constInc.incrementAt(index);
for (int j = 0; j < numElems; ++j)
{
if (linearInc.valueAt(j) != constInc.valueAt(j))
{
printf("Error: differing values at increment step %d, value at index %d\n", i, j);
}
}
}
return 0;
}
答案 8 :(得分:0)
从修改后的元素遍历数组,直到找到正确的位置,然后交换。平均案例复杂度为O(N),其中N是重复的平均数。最坏的情况是O(n),其中n是数组的长度。只要N不大并且不能与n一起严重缩放,你就可以了,并且可能出于实际目的假装它是O(1)。
如果重复是n和n强烈的标准和/或比例,那么有更好的解决方案,请参阅其他回复。