6

編集 - 不必要なコンテキストの説明をすべて削除しました - 言葉が多すぎて、最終的に問題とは無関係です。要約すると、バランスの取れた KD ツリーを構築するプロセス中に座標の配列を分割しています (詳細については、ウィキペディアの記事、「構築」セクションを参照してください。実際には、それぞれが同じ比較によって分割されなければならない n 個の項目の k 個の並列配列があります)。

これは宿題ではありません。すべてのニュアンスが確実に伝わるように、質問を書きました。

並べ替えられた配列が与えられた場合:

 int[] ints =  { 0, 1, 2, 3, 4, 5, 6 };
 //this one is important - my current solution fails on this
 int[] ints2 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Note due to a clarification asked by a colleague, all that's guaranteed about these arrays is that element[n] will be less than or equal to element[n+1].

Successful operations on these will separate them into two sub arrays L and R (indicated below):

/*ints == */  { 1, 3, 5, 0, 2, 4, 6 }
              /*|> L <|  |>   R  <|*/

/*ints2 == */ { 1, 3, 5, 7, 9, 0, 2, 4, 6, 8 }
              /*|>    L    <|  |>    R    <|*/

L contains the integers that are odd and R contains those that are even, whilst retaining the original sort-order of those elements within those subarrays.

The function will ideally NOT resort to re-sorting the elements (a lengthy sort operation will already have been performed in advance) and it won't use a temporary array. I believe that means I'm looking for O(N) complexity and O(1) memory.

The function can be provided with the start and end elements of each sub array - i.e. the caller can know in advance how many items will fall on the left/right sides (possibly by scanning the array in advance for odd/even). Edit in reality it starts off just as an array; so a solution that can work without these values is good, because otherwise the complete solution can only ever be at best O(2n) complexity in reality if an initial pass is required.

This where my current attempt is at now - I've updated it and commented it from what was in the original post.

public void DivideSubArray(int[] array, int leftStart, int leftCount, 
  int rightStart, int rightCount)
{
  int currentLeft = leftStart, currentRight = rightStart;
  int leftCounter = leftCount;
  int temp;
  int readahead;
  while (leftCounter != 0) {
    if ((array[currentLeft] % 2) == 0)
    {
      //remember the element we swap out
      temp = array[currentRight];
      //Set as next item on the right. We know this is the next lowest-sorted 
      //right-hand item because we are iterating through an already-sorted array
      array[currentRight++] = array[currentLeft];
      // * read ahead to see if there are any further elements to be placed
      // * on the left - move them back one by one till there are no more.
      readahead = currentLeft + 1;
      while ((array[readahead] % 2) != 0)
      {
        array[currentLeft++] = array[readahead++];
        leftCounter--;
      }
      //Now write the swapped-out item in, but don't increment our currentLeft.  
      //The next loop will check if the item is in the correct place.
      array[currentLeft] = temp;
    }
    else //this item is already in the correct place
    {
      currentLeft++;
      leftCounter--;
    }
  }
}

When called as follows:

int numOdd = ints.Count(i => (i % 2) == 1);
DivideSubArray(ints, 0, numOdd, numOdd, ints.Length - numOdd);

It produces the expected array for ints (and many other arrays), but not ints2:

{ 1, 5, 3, 7, 9, 0, 2, 6, 4, 8 }

So it partitions correctly - but swaps 3,5 and 6,4. I understand why: because in the first loop 5 is swapped to the left, then 2 is propagated over because the algorithm says that 5 is odd and should stay. I have a decision tree written out that'll fix it, but having followed it a few loops it infers that the solution is recursive.

I'm struggling to see how to get around this without running more sort operations within the sub array or creating temporary lists/arrays as workspace. Of course, though, a sort might increase complexity but keep the memory requirement; and if it turns out to be the fastest solution then it would make sense to use it.

You can see my current fastest (in run-time) and best memory solution under my answer. As a yardstick - the above attempt not only produces an incorrect result, but it also takes 3 times as long as the code in my answer.

I feel there must be a simple way to exploit a single 'spare' variable to swap the items - I just can't see it - I'm hoping the SO collective brain will :)

Of course, if the answer is 'no' then so be it.

4

7 に答える 7

0

次の方法で作業を簡単にできると思います。まず、最初に奇数が配列の先頭に昇順で書き込まれ、次に偶数が最後に降順に書き込まれるように配列を変更します。たとえば、{0,1,...6} の場合、これは {1,3,5,6,4,2,0} のようになります。それを行った後、別の線形パスを実行して、配列の 2 番目の部分を逆にします (これは非常に簡単で簡単です)。

なぜ私はこれがより簡単であるべきだと思いますか? 最初のステップですべきことは、通常のqsortアルゴリズムが行うことです(少し奇妙な比較演算子を使用)。インターネットを検索して、qsort パーティションがどのように作成されているかを確認できます (たとえば、ここに 1 つの例があります)。この 2 つのステップで問題を分割すると、ソリューションの実装がより簡単になると心から信じています。また、全体的な複雑さは変わらないことに注意してください。

これがあなたを助けることを願っています。

編集:これが私の提案の最初の部分を行うことができると私が信じている方法です:

public void DivideSubArray(int[] array, int leftStart, 
              int leftCount, int rightStart, int rightCount)
{
    int currentRight = rightStart + rightCount - 1;

    int current = leftStart;
    while (current < currentRight) {
        if ((array[current] % 2) == 0)
        {
            int temp = array[current];
            array[current] = array[currentRight];
            array[currentRight] = temp;
            currentRight--;
        } else {
            current++;
        }
    } 
}

偶数部分を逆にするコードは提供していません。これは非常に単純であると信じているためです。また、このアプローチがコードをどれだけ簡素化するかという事実を強調したかったのです。

于 2012-04-26T11:27:26.080 に答える
0

このスレッドで試してみてもいいですか。C# について話していることがわかります。私は言語を知りませんが、これはタスクにとって重要ではないと思います。

問題の説明に欠けているものがあります-ソートされた配列がどこから来ているか。おそらく、説明を求めるコメントを投稿するべきだったのですが、考えられるすべての可能性をカバーする回答を書きに行くことにしました。うまくいけば、その答えは将来より多くの人々に役立つでしょう.

基本的に、タスクはそのままボックスに入れられます。「配列があり、それを所定の位置に分割します」。ただし、この配列の起源について少し説明したいと思います。

  • ケース 1:配列がどこかから読み取られ、コード内 (メモリ内) で並べ替えられます。この場合、オッズから偶数を分割すると、オーバーヘッドが発生しない洗練されたソリューションが得られます。
    1. 奇数と偶数の数を決定します (配列を 1 回通過することによりO(n))。
    2. 配列内の最大数と最小数を決定します。それらMAXMを と と呼びましょうMINM。これは、偶数と奇数を決定する最初のパスで実行できます。
    3. MAXM - MINM + 1奇数ごとに追加する配列をもう一度通過します。目標は、すべての奇数が偶数より大きくなるようにすることです。これは時間的に直線的ですO(n)
    4. kth_element アルゴリズムを使用して配列を分割します (基本的には、クイック ソートの重要な分離の 1 回のパス)。奇数から偶数を分割します。それぞれがいくつあるか、すべてのオッズがすべての偶数よりも大きいことを既に知っているという事実を利用します。アルゴリズムは線形時間O(n)で実行されますが、残念ながら、C++ ライブラリの実装(C# はありません) のみを参照しています。
    5. 奇数に対応するすべての配列スロットを通過しMAXM - MINM + 1、各数値から減算して元の奇数を取得します。これも時間的に線形ですO(n)
    6. 最後に、偶数と奇数の部分を別々に並べ替えます。これにより、並べ替え全体の複雑さが増すことはありませんが、to 部分が互いに分離されます。
  • ケース 2:固定ストレージから既に並べ替えられた配列を読み取ります。たとえば、ハードディスク上のファイルで、奇数と偶数の数が事前にわかっています。
    1. この場合、数字を入力する配列に配置する必要があるだけです。1 つは次の偶数に続くもので、もう 1 つは次の奇数に続くものです。この解決策は明らかであり、パフォーマンスにはまったく影響しません。
  • ケース 3:ハードディスク上のファイルなど、永続的なストレージから既に並べ替えられた配列を読み取りますが、奇数と偶数の数を事前に知りません。
    1. 配列の先頭から偶数を、配列の末尾からオッズを埋め始めます。そうすれば、最後に 2 つのシーケンスが途中で出会います。
    2. したがって、偶数はオッズから分割されますが、奇数は増加するのではなく減少します。奇数部分(これも線形です)のインプレース逆を行って行うだけで、目的の配列が得られます。

うまくいけば、説明されているシナリオの少なくとも 1 つがあなたに合っており、そこからのアイデアを使用して問題を解決できるでしょう。

于 2012-04-28T17:27:30.827 に答える
0

O(n) 時間と O(1) 空間アルゴリズムがあるかもしれないと思います。しかし、複雑すぎて私たちには理解できないかもしれません。

私はあなたにこれを納得させます:

  1. 元の問題の特殊なケースを示します。これを A と呼びます。
  2. 問題 A の逆を考えてみましょう。これを問題 B と呼びます。そして、どちらかについて O(n) 時間と O(1) 空間の解を得た場合、それを修正して別の問題を解けることを示します。
  3. 問題 B は O(n) 時間と O(1) 空間で解決できることを示しますが、解決策は非常に複雑で、多くの数学が必要です。

つまり、あなたの問題を簡単に解決できる可能性は低いということです。そうでなければ、問題 B を簡単に解決できます。

1.特別なケースを考えてみましょう:

この例では、A[] = {1,2,3,4,5,6,7,8}、1 ~ 2n、n = 4 とします。{1,3,5,7,2,4,6,8} に変更しますね。これを問題 A と呼びます。一般に、A[1] から A[2n] までのサイズ 2n の配列 A があり、A[1]、A[3]、A[ に範囲を変更したいことを意味します。 5]...、A[2n-1]、A[2]、A[4]、A[6]、A[2n]。これはあなたの問題の特殊なケースです。問題の解決策が得られれば、問題 A も簡単に解決できます。

2.問題Aの逆。

関連する問題を考えてみましょう。B = {1,2,3,4,5,6,7,8} とし、{1,5,2,6,3,7,4,8} に変更します。これはちょうどあなたがカードのデッキを持っていて、それらを 2 つの等しい部分に分割し、交互にマージする完璧なシャッフルをしたいようなものです. したがって、一般に、B[1] から B[2n] までのサイズ 2n の配列 B があります。B[1]、B[n+1]、B[2]、B[n+2]、....B[n]、B[2n] に範囲を変更します。

すると、問題Aと問題Bが逆の操作であることがわかります。つまり、サイズ 2n の配列の場合、操作 B を実行してから操作 A を実行すると、元の配列になり、最初に B を実行してから A を実行しても同じになります。

順列についてある程度の知識がある場合は、A のアルゴリズムを取得した場合、それを変更して B で機能するように変更できることがわかります。これに慣れていない場合は、後で詳しく説明します。

3. 問題 B は簡単に解決できません。

問題 B の O(n) 時間と O(1) 空間アルゴリズムは存在しますか。存在します。Computing the Cycles in the Perfect Shuffle Permutationで確認できます。これは 12 ページの論文であり、面接でこの解決策を思い付く可能性は低いことを意味します。私はそれを読みましたが、数論の数学が本当に必要です。そして、それはより理論的な解決策です。

結論:

問題Aの特別なケースであっても、あなたの問題に対する単純な(つまり、10ページの紙を必要としない)O(n)時間O(1)空間の解決策はないようです。それ以外の場合は、次のように変更できます問題 B を解決します。一般化された問題に対する O(n) 時間 O(1) 空間の解決策があるかどうかはわかりません。

あなたが本当にこの問題に興味があるなら。Knuth の The Art of Computer Programming を見ることができます。In Situ Permutation について説明する章があります。

私の考えを理解するのは簡単ではないかもしれませんので、質問があればコメントしてください。

于 2012-04-29T18:54:30.447 に答える
0

私は一時的な配列を使用しない解決策をまとめることができました.Nが大きいと信じられないほど遅くなりました。コードを投稿するつもりはありませんが、それはひどかったです!

編集 - これは私の元のソリューションを改善したものです。複雑さは技術的には O(2n) であり (フレームワークのドキュメントによると、List.CopyTo メソッドは O(n) である Array.Copy を使用するため)、メモリは O(n) です。

はい、ソリューションは、奇数/偶数分割の知識に事前に依存するのではなく、単純に配列を取り、その場で分割を行います。これは、(私の実際のコードに回帰すると) 初期パスが不要であることを意味します。

この解決策は簡単です: 配列をスキャンし、オッズを配列の先頭に戻し (または、既に正しい場所にある場合はそのままにしておきます)、偶数をリストに追加します。ループが完了すると、リストが配列の残りの部分にコピーされます。これは、メモリを犠牲にして複雑さの要件を満たします (最悪の場合は O(n))。とにかく、既に使用していたコードを大幅に改善します (2 つのリストのソリューションよりも 2 倍高速でした)。また、奇数/偶数の分割を取得するための初期パスも必要ありません。

public void DivideSubArray(int[] array)
{       
    int currentOdd=0;
    List<int> even = new List<int>(array.Length / 2);
    for (int i = 0; i < array.Length; i++)
    {
        if ((array[i] % 2) != 0)
        {
            even.Add(array[i]);
        }
        else
        {
            if (currentOdd != i)
                array[currentOdd++] = array[i];
            else
                currentOdd++;
        }
    }
    even.CopyTo(array, currentOdd);
}

リストの初期容量に注意してください - Mooing Duck が以下のコメントで言及しているように、いくつかの確率を利用して、わずかに高い値を選択することで、さらに改善できる可能性があります (平均してほぼ均等な分割が観察されると仮定します)。

そうは言っても、アルゴリズムは偶数分割で最も遅く実行されます。奇数のアイテムがさらにある場合、それは単なるスワップの束です。さらにイベントがある場合は、はい、さらに多くのAdd操作が必要ですが、パフォーマンスを低下させるのはリストのサイズ変更だけです。

私の最後の試みは、 izomorphius が提案したことを達成できるかどうかを確認することです-正しい順序でオッズを構築し、逆または追加の配列なしで任意の順序で偶数を構築します。それが可能であれば、その解決策は O(1) メモリになりますが、O(n + (並べ替えの複雑さ)) になります。実際にパフォーマンスが向上する場合、上記の解決策の半分の速度になります。

于 2012-04-26T13:08:59.243 に答える
0

リストを一方の端または他方の端がスクランブルされることなく分割する「直接的な」方法はないと思いますが、線形時間の定数空間ソリューションを使用することはできます。izomorphius によって提供されるパーティショニング アプローチは、最終的に右側が逆の順序になり (線形時間で簡単に修正可能)、もう一方の端はある程度予測可能な方法でスクランブルされ、右側から来た要素が混在します。左から来たものと逆の順序で。特定の要素が右側から来たかどうかを一定の時間で簡単に識別でき (左側の最後の項目と比較するだけです)、したがって、線形時間で、要素から移動された要素の順序を簡単に逆にすることができます。右側から左側へ。

これを行うと、元のサイズと非常によく似たパーティショニングの問題が残りますが、サイズは半分しかありません。唯一の違いは、ノードの値が偶数か奇数かではなく、ノードの値が「元の」最後の要素より大きいか小さいかに基づいて分割基準が決まることです。したがって、基本的に元のアルゴリズムをより小さなデータセットに適用できます。スプリットのどちら側にアイテムが多くなるかを事前に判断できるため、残りのサイドが元のサイズの半分以下になるようにスプリットを配置できます。最終的な効果は、サイズ 2N の配列を分割すると、サイズ N の配列を分割するのに比べて O(1) 倍の時間がかかることです。並べ替えられたデータの 2 つの任意に混合された実行から構成される任意サイズの配列を、並べ替えられたデータの 2 つの互いに素な実行に分割することは、定数スペースを使用して線形時間で実行できます

ちなみに、整数の場合は問題になりませんが、上記のアルゴリズムは 2 つの要素を比較し、最初の要素が 2 番目の要素の左または右に属するかどうかを知る機能に依存していることに注意することが重要です。したがって、安定したソートアルゴリズムの基礎として使用することはできません。

于 2012-04-27T20:46:45.240 に答える
0
// stable_partition.cpp
// example general inplace stable partition.

#include <algorithm>
#include <functional>
#include <iterator>
#include <iostream>
#include <vector>

template<typename Fwd, typename Pred>
  Fwd
  inplace_stable_partition(Fwd first, Fwd last, Pred pred)
  {
    ptrdiff_t nmemb = std::distance(first, last);

    if (nmemb == 1)
      return pred(*first) ? last : first;
    if (nmemb != 0)
      {
        Fwd split = first;
        std::advance(split, nmemb/2);

        first = inplace_stable_partition(first, split, pred);
        last = inplace_stable_partition(split, last, pred);

        std::rotate(first, split, last);
        std::advance(first, std::distance(split, last));
      }
    return first;
  }

int
main(int argc, char* argv[])
{
  using namespace std;

  vector<int> iv;
  for ( int i = 0; i < 10; i++ )
    iv.push_back(i);

  copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
  cout << endl;

  inplace_stable_partition(iv.begin(), iv.end(), bind2nd(modulus<int>(), 2));

  copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
  cout << endl;
  return 0;
}
于 2012-08-02T17:33:26.940 に答える
-1

要素の特別な関係順序を持つ安定したインプレース ソート アルゴリズムを探しているようです (奇数は偶数よりも小さい)。

それを考えると、あなたは O(n ln n) よりも優れているとは思えません。

インプレースマージソートを使用します。

同じ値を持つ要素の順序を保持する必要がない場合は、インプレース処理がはるかに簡単なクイック ソートを使用します (ただし、数十億の要素がある場合、これはあまり適していない可能性があります)。

于 2012-04-26T11:53:41.753 に答える