繁体   English   中英

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

[英]Maintain a sorted array in O(1)?

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

在更具体的情况下,如果数组由所有 0 值初始化,并且它总是仅通过将索引的值增加 1 来增量构造,是否有 O(1) 解决方案?

我没有完全解决这个问题,但我认为总体思路至少对整数有帮助。 以更多内存为代价,您可以维护一个单独的数据结构,该结构维护一系列重复值的结束索引(因为您希望将递增的值与重复值的结束索引进行交换)。 这是因为它是重复的值,你遇到最坏的情况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处递增值。 所以你增加了34 该数组现在看起来像这样:

[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个位置来检查它们,看它们是否与相关值相同。 如果两者不相等,则可以从地图中删除此值的条目。

同样,这只是一个普遍的想法。 我将不得不编写它以查看它是否按照我的想法运行。

请随意戳破洞。

UPDATE

在这里用JavaScript实现了这个算法。 我只使用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] + ")";
        }
    }
}

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

不。给定一个全0的数组: [0, 0, 0, 0, 0] 如果递增第一个值,给出[1, 0, 0, 0, 0] ,则必须进行4次交换以确保它保持排序。

给定一个没有重复的排序数组,答案是肯定的。 但是在第一次操作之后(即第一次增加),你可能会有重复。 您执行的增量越多,您重复的可能性就越高,O(n)保持该数组排序的可能性就越大。

如果您只拥有数组,则无法保证每个增量的时间少于O(n)。 如果您正在寻找的是支持排序顺序和按索引查找的数据结构,那么您可能需要一个订单结构树

如果值很小,则计数排序将起作用。 将数组[0,0,0,0]{4} 递增任何零会得到{3,1} :3个零和一个零。 通常,要增加任何值x,从x的计数中减去一个并增加{x + 1}的计数。 然而,空间效率是O(N),其中N是最高值。

这取决于有多少项可以具有相同的值。 如果更多项可以具有相同的值,则不可能使用普通数组的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(因为两个项目不能具有相同的值),因此它必须具有> 21的值并且数组已经排序。

所以你采取排序数组和哈希表。 你越过数组来找出'平坦'区域 - 元素具有相同的值。 对于每个平坦的区域,你必须弄清楚三件事情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;
}

是的,不是。

如果列表仅包含唯一的整数,则为是,因为这意味着您只需要检查下一个值。 在任何其他情况下都没有。 如果值不是唯一的,则递增N个重复值中的第一个意味着它必须移动N个位置。 如果值是浮点值,则x和x + 1之间可能有数千个值

非常清楚要求是非常重要的; 最简单的方法是将问题表达为ADT(抽象数据类型),列出所需的操作和复杂性。

以下是我认为您正在寻找的内容:提供以下操作的数据类型:

  • Construct(n) :创建一个大小为n的新对象,其值均为0

  • Value(i) :返回索引i处的值。

  • Increment(i) :增加索引i的值。

  • Least() :返回具有最小值的元素的索引(如果有多个则返回一个这样的元素)。

  • Next(i) :在从Least()开始的排序遍历中返回元素i之后的下一个元素的索引,这样遍历将返回每个元素。

除了构造函数之外,我们希望上述每个操作都具有复杂度O(1) 我们还希望对象占用O(n)空间。

该实现使用桶列表; 每个桶都有一个value和一个元素列表。 每个元素都有一个索引,指向它所属的桶的指针。 最后,我们有一个指向元素的指针数组。 (在C ++中,我可能使用迭代器而不是指针;在另一种语言中,我可能使用侵入式列表。)不变量是没有桶是空的,并且桶的value严格单调增加。

我们从一个值为0的桶中开始,该桶具有n元素的列表。

通过返回迭代器在数组的元素i处引用的元素的桶的值来实现Value(i) Least()是第一个存储桶中第一个元素的索引。 Next(i)是元素i处迭代器引用的元素之后的下一个元素的索引,除非迭代器已经指向列表的末尾,在这种情况下它是下一个桶中的第一个元素,除非element的存储桶是最后一个存储桶,在这种情况下,我们位于元素列表的末尾。

唯一感兴趣的界面是Increment(i) ,如下:

  • 如果元素i是其存储桶中的唯一元素(即存储桶列表中没有下一个元素,并且元素i是存储桶列表中的第一个元素):

    • 增加关联存储桶的值。
    • 如果下一个桶具有相同的值,则将下一个桶的元素列表追加到此桶的元素列表(这是O(1) ,无论列表的大小如何,因为它只是一个指针交换),然后删除下一个桶。
  • 如果元素i不是其桶中的唯一元素,则:

    • 将其从存储桶列表中删除。
    • 如果下一个桶具有下一个顺序值,则将元素i推送到下一个桶的列表中。
    • 否则,下一个存储桶的值会更大,然后创建一个具有下一个连续值的新存储桶,并且只创建元素i ,并将其插入此存储桶和下一个存储桶之间。

我认为可以不使用哈希表。 我在这里有一个实现:

#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;
}

只需从修改后的元素遍历数组,直到找到正确的位置,然后交换。 平均案例复杂度为O(N),其中N是重复的平均数。 最坏的情况是O(n),其中n是数组的长度。 只要N不大并且不能与n一起严重缩放,你很好并且可能出于实际目的假装它是O(1)。

如果重复是n和n强烈的规范和/或扩展,那么有更好的解决方案,请参阅其他响应。

作为其他答案的补充:如果您只能拥有数组,那么您确实无法保证操作是恒定时间的; 但由于数组已排序,您可以在log n操作中找到相同数字的运行结束,而不是在n操作中。 这只是一个二分查找。

如果我们希望大多数数字运行时间较短,我们应该使用 galloping search,这是一种变体,我们首先通过查看位置 +1、+2、+4、+8、+16 等来找到边界,然后在里面做二分查找。 您会得到一个通常恒定的时间(如果项目是唯一的,并且速度非常快),但可以增长到log n 除非由于某种原因,即使经过多次更新,长时间运行的相同数字仍然很常见,否则这可能胜过任何需要保留额外数据的解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM