web-dev-qa-db-ja.com

O(1)でソートされた配列を維持しますか?

並べ替えられた配列があり、結果の配列が引き続き並べ替えられるように、1つのインデックスの値を1単位だけ増やします(array [i] ++)。これはO(1)で可能ですか? STLおよびC++で可能な任意のデータ構造を使用することは問題ありません。

より具体的なケースでは、配列がすべて0の値で初期化され、インデックスの値を1つ増やすだけで常に増分的に構築される場合、O(1)ソリューションがあります。 ?

28
Farshid

私はこれを完全には解決していませんが、一般的な考え方は少なくとも整数には役立つと思います。より多くのメモリを犠牲にして、繰り返し値の実行の終了インデックスを維持する別個のデータ構造を維持できます(増分値を繰り返し値の終了インデックスと交換するため)。これは、最悪の場合のO(n)ランタイムに遭遇するのは繰り返し値であるためです。たとえば、_[0, 0, 0, 0]_があり、位置_0_で値をインクリメントするとします。次に、最後の場所(_3_)を見つけるのはO(n)です。

しかし、私が言及したデータ構造を維持しているとしましょう(マップにはO(1)ルックアップがあるので機能します)。その場合、次のようなものになります。

_0 -> 3
_

したがって、位置_0_で終わる_3_値の実行があります。値をインクリメントするとき、たとえば場所iで、新しい値が_i + 1_の値よりも大きいかどうかを確認します。そうでない場合は、大丈夫です。ただし、そうである場合は、2次データ構造にこの値のエントリがあるかどうかを確認します。ない場合は、単に交換することができます。 isエントリがある場合は、終了インデックスを検索して、その場所の値と交換します。次に、アレイの新しい状態を反映するために、セカンダリデータ構造に必要な変更を加えます。

より徹底的な例:

_[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_の実行により、1つのインデックスが早期に終了するため、その値をデクリメントする必要があります。

_3 -> 3
4 -> 6
5 -> 9
_

あなたがする必要があるもう一つのチェックは、値がもう繰り返されているかどうかを確認することです。 _i - 1_ thと_i + 1_ thの場所を調べて、問題の値と同じであるかどうかを確認することで、それを確認できます。どちらも等しくない場合は、この値のエントリをマップから削除できます。

繰り返しますが、これは単なる一般的な考え方です。それが私が考えたように機能するかどうかを確認するために、コード化する必要があります。

お気軽に穴をあけてください。

[〜#〜]更新[〜#〜]

私はこのアルゴリズムの実装を持っています ここ JavaScriptで。 JavaScriptを使用したのは、すばやく実行できるようにするためです。また、私はそれをかなり迅速にコーディングしたので、おそらくクリーンアップすることができます。コメントはありますが。私も難解なことは何もしていないので、これはC++に簡単に移植できるはずです。

アルゴリズムには、基本的に2つの部分があります。インクリメントとスワッピング(必要な場合)と、繰り返される値の実行の終了インデックスを追跡するマップ上で行われる簿記です。

コードには、ゼロの配列で始まり、ランダムな位置をインクリメントするテストハーネスが含まれています。すべての反復の最後に、配列がソートされていることを確認するためのテストがあります。

_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] + ")";
        }
    }
}
_
22
Vivin Paliath

より具体的なケースでは、配列がすべて0の値で初期化され、インデックスの値を1つ増やすだけで常に増分的に構築される場合、O(1)ソリューションがあります。 ?

いいえ。すべて0の配列が与えられた場合:[0, 0, 0, 0, 0]。最初の値をインクリメントすると、[1, 0, 0, 0, 0]の場合、ソートされたままになるように4つのスワップを行う必要があります。

重複のないソートされた配列が与えられた場合、答えは「はい」です。ただし、最初の操作の後(つまり、最初にインクリメントするとき)、重複する可能性があります。増分を増やすほど、重複が発生する可能性が高くなり、その配列を並べ替えたままにするためにO(n)が必要になる可能性が高くなります。

配列だけの場合、増分あたりの時間はO(n))未満を保証することはできません。探しているのが、並べ替えられた順序とインデックスによるルックアップをサポートするデータ構造である場合、それならおそらく 素晴らしいツリーを注文する が必要です。

10
Jim Mischel

値が小さい場合は、カウントソートが機能します。配列を表す[0,0,0,0] なので {4}。ゼロをインクリメントすると{3,1}:3つのゼロと1つ。一般に、任意の値xをインクリメントするには、xのカウントから1を差し引き、{x +1}のカウントをインクリメントします。ただし、スペース効率はO(N)です。ここで、Nは最大値です。

3
MSalters

したがって、ソートされた配列とハッシュテーブルを使用します。配列を調べて、要素が同じ値である「フラット」領域を見つけます。すべての平坦な領域について、3つのことを理解する必要があります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;
}
1
Andrey

同じ値を持つことができるアイテムの数によって異なります。より多くのアイテムが同じ値を持つことができる場合、通常の配列でO(1)を持つことはできません。

例を見てみましょう:array [5] = 21とし、array [5] ++を実行したいとします。

  • アイテムをインクリメントします。

    array[5]++
    

    (これは、配列であるため、O(1))です)。

    したがって、array [5] = 22になります。

  • 次の項目(つまり、array [6])を確認します。

    Array [6] == 21の場合、新しい項目をチェックし続ける必要があります(つまり、array [7]など)21より大きい値が見つかるまで。その時点で、値を交換できます。この検索はnot O(1)であるため、配列全体をスキャンする必要がある可能性があります。

代わりに、アイテムが同じ値を持つことができない場合は、次のようになります。

  • アイテムをインクリメントします。

    array[5]++
    

    (これは、配列であるため、O(1))です)。

    したがって、array [5] = 22になります。

  • 次の項目は21にすることはできません(2つの項目が同じ値を持つことはできないため)。したがって、値が21より大きい必要があり、配列はすでにソートされています。

1
Claudio

ハッシュテーブルを使わなくても可能だと思います。ここに実装があります:

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

はいといいえ。

リストに一意の整数のみが含まれている場合ははい。つまり、次の値を確認するだけで済みます。他の状況ではありません。値が一意でない場合、N個の重複する値の最初の値をインクリメントすると、N個の位置を移動する必要があります。値が浮動小数点の場合、xとx +1の間に数千の値がある可能性があります

0
MSalters

要件について非常に明確にすることが重要です。最も簡単な方法は、問題をADT(Abstract Datatype)として表現し、必要な操作と複雑さをリストすることです。

これがあなたが探していると思うものです:次の操作を提供するデータ型:

  • Construct(n):サイズnの新しいオブジェクトを作成します。その値はすべて_0_です。

  • Value(i):インデックスiの値を返します。

  • Increment(i):インデックスiの値をインクリメントします。

  • Least():値が最小の要素(または複数ある場合はそのような要素の1つ)のインデックスを返します。

  • Next(i):トラバーサルがすべての要素を返すように、Least()で始まるソートされたトラバーサルで要素iの次の要素のインデックスを返します。

コンストラクターを除いて、上記のすべての操作に複雑さO(1)を持たせたいと思います。また、オブジェクトがO(n)スペースを占めるようにします。

実装はバケットのリストを使用します。各バケットには、valueと要素のリストがあります。各要素にはインデックスがあり、その一部であるバケットへのポインタがあります。最後に、要素へのポインタの配列があります。 (C++では、おそらくポインターではなくイテレーターを使用します。別の言語では、おそらく侵入リストを使用します。)不変条件は、バケットが空になることはなく、バケットのvalueは厳密に単調に増加します。

n要素のリストを持つ値0の単一のバケットから始めます。

Value(i)は、配列の要素iにあるイテレータによって参照される要素のバケットの値を返すことによって実装されます。 Least()は、最初のバケットの最初の要素のインデックスです。 Next(i)は、要素iでイテレータによって参照された要素の次の要素のインデックスです。ただし、そのイテレータがすでにリストの最後を指している場合は、最初の要素です。要素のバケットが最後のバケットである場合を除き、次のバケットの要素。この場合、要素リストの最後になります。

対象となる唯一のインターフェースはIncrement(i)です。これは次のとおりです。

  • 要素iがバケット内の唯一の要素である場合(つまり、バケットリストに次の要素がなく、要素iがバケットリストの最初の要素である場合):

    • 関連するバケットの値をインクリメントします。
    • 次のバケットの値が同じである場合は、次のバケットの要素リストをこのバケットの要素リストに追加し(これは、単なるポインタースワップであるため、リストのサイズに関係なく、O(1)です)、削除します。次のバケツ。
  • 要素iがバケット内の唯一の要素ではない場合、次のようになります。

    • バケットリストから削除します。
    • 次のバケットに次の順次値がある場合は、要素iを次のバケットのリストにプッシュします。
    • それ以外の場合は、次のバケットの値が大きくなり、次の順次値と要素iのみを使用して新しいバケットを作成し、このバケットと次のバケットの間に挿入します。
0
rici

正しい場所が見つかるまで、変更された要素から配列に沿って繰り返し、次にスワップします。平均的なケースの複雑さはO(N)ここで、Nは重複の平均数です。最悪のケースはO(n)ここで、nは配列の長さです。 。Nが大きくなく、nとのスケーリングが悪くない限り、問題はなく、実用的な目的ではO(1)のふりをすることができます。

重複が標準であるか、nで強くスケーリングする場合は、より良い解決策があります。他の応答を参照してください。

0
John_C