在O(1)中维护一个排序数组?

时间:2013-11-13 15:29:05

标签: c++ arrays algorithm sorting

我们有一个排序数组,我们希望将一个索引的值增加1个单位(array [i] ++),这样生成的数组仍然是排序的。这可能在O(1)中吗? 可以在STL和C ++中使用任何可能的数据结构。

在一个更具体的情况下,如果数组是由所有0值初始化的,并且它总是通过将索引值增加1来递增构造,那么是否存在O(1)解?

9 个答案:

答案 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 - 1i + 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),并将ii - 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强烈的标准和/或比例,那么有更好的解决方案,请参阅其他回复。