16

私はいくつかのデータ構造を実装しており、使用したい 1 つのプリミティブは次のとおりです。メモリ チャンク A[N] があり (可変長ですが、例では 100 を使用します)、このチャンク内には小さな部分があります。追加のメモリを使用せずに移動したい長さ K (30 としましょう) の C。

追加の問題は、A が「ラップ」することです。つまり、C は A[80] で開始でき、C の最初の 20 要素は要素 A[80..100] であり、最後の 10 要素は要素 A[ です。 0..10]。さらに、ターゲット範囲は、「ラップ」して、可能な方法で C とオーバーラップすることもできます。さらに、一定量以上の追加メモリを使用したくありません。すべてが適切に行われる必要があります。また、ターゲット範囲にもソース範囲にもない A の部分には、何か重要なものが含まれている可能性があるため、これも使用できません。したがって、1つのケースは次のようになります。

A は次のようになります。

|456789ABCDEF0123456789AB|-----|0123|

そして、これに変換する必要があります:

|89AB|-----|0123456789ABCDEF01234567|

ライブラリに委任するか、ライブラリから別のデータ構造を使用するだけでは、ここではオプションではありません。問題を自分で理解したいと思います。一見、些細なことではないかと思いましたが、いくつかのケースを区別するとすぐに明らかになりますが、今は深刻な問題を抱えています。もちろん、重ならない、ラップしないという些細なケースもありますが、少なくとも両方が同時に発生すると、厄介になります。1 つの空いている場所から始めて、そこに属するパーツを移動することもできますが、別の場所に別の空きパーツを作成すると、まだ使用できるパーツを追跡するのが難しくなります。

多分私は何かを完全に見逃しているかもしれませんが、ターゲット範囲がラップされない場合の私の特別なケースでさえ、ほぼ100行あり(半分はアサーションとコメントですが)、いくつかの追加で一般的なケースも処理できるように更新できましたインデックスの計算ですが、誰かがエレガントで短い解決策を持っている場合は、助けていただければ幸いです。直感的には、これは些細なことだと思いますが、まだ最善の解決策がわかりません。

注: もちろん興味深いケースは、C が A とほぼ同じ大きさの場合です。< N/2、それは些細なことです。

編集:追加のフラグ/インデックスを一定量以上使用すると、追加のメモリとしてカウントされ、可能であればそれを避けたいです。

編集: 一部の人々は私のコードを見たいと思っていました。私の質問はかなり抽象的なので、投稿したくありませんでしたが、誰かがそれを改善する方法を見ているかもしれません. ひどいです。ターゲットが最初から開始され(ただし、簡単に変更できます)、非常に長い場合にのみ機能しますが、O(n)で追加のメモリなしでジョブを実行します。

#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

void move_part(int* A, size_t N, size_t target, size_t source, size_t size, int show_steps)
{
  assert(source + size <= N);
  assert(target + size <= N);
  if (show_steps) {
    printf("Moving size %d from %d to %d.\n", size, source, target);
  }
  memmove(A + target, A + source, size * sizeof(int));
}

void swap_parts(int* A, size_t N, size_t first_begin, size_t second_begin, size_t size, int show_steps)
{
  if (show_steps) {
    printf("Swapping size %d at %d and %d.\n", size, first_begin, second_begin);
  }
  assert(first_begin + size <= N);
  assert(second_begin + size <= N);
  size_t i;
  for (i = 0; i < size; ++i) {
    int x = A[first_begin + i];
    A[first_begin + i] = A[second_begin + i];
    A[second_begin + i] = x;
  }
}

void move_to_beginning(int* A, size_t N, size_t begin, size_t size, int show_steps)
{
  assert(begin <= N);
  assert(size <= N);
  // Denotes the start of our "working range". Increases during
  // the algorithm and becomes N
  size_t part_start = 0;
  // Note: Keeping the size is crucial since begin == end could
  // mean that the range is empty or full.
  size_t end = (begin + size) % N;
  while (part_start != N) {
    size_t i;
    if (show_steps) {
      for (i = 0; i < N; ++i) {
    printf("%d ", A[i]);
      }
      printf("\n");
      printf("part_start %d  begin %d  end %d  size %d\n", part_start, begin, end, size);
    }
    // loop invariants
    assert(part_start < N);
    // The two pointers are in our range
    assert(part_start <= begin && begin <= N);
    assert(part_start <= end && end <= N);
    // size is valid (wrapped case, non-empty, non-full case)
    assert(begin <= end || (N - begin) + (end - part_start) == size);
    // size is valid (non wrapped case, non-empty, non-full case)
    assert(begin >= end || end - begin == size);
    // size is valid (working range is full or empty case)
    assert(begin != end || size == 0 || part_start + size == N);
    if (size == 0 || begin == N || begin == part_start) {
      // ##|1234|# -> 1234### ||
      if (show_steps) {
    printf("Case 1:\nTerminating\n");
      }
      // #||# -> ## ||
      // 12|##| -> 12## ||
      // |12|## -> 12## ||
      break;
      /* Not necessary any more, but would be the correct transformation:
     part_start = N;
     begin = N;
     end = N;
     size = 0;*/
    } else if (end == part_start) {
      // |##|123 -> ##|123|
      if (show_steps) {
    printf("Case 2:\n");
    printf("Setting end to %d.\n", N);
      }
      end = N;
    } else if (begin < end) {
      // ##|1234|# -> 1234### ||
      if (show_steps) {
    printf("Case 3:\n");
      }
      move_part(A, N, part_start, begin, size, show_steps);
      break;
      /* Not necessary any more, but would be the correct transformation:
     part_start = N;
     begin = N;
     end = N;
     size = 0;*/
    } else {
      size_t end_size = end - part_start;
      size_t begin_size = N - begin;
      assert(begin_size + end_size == size);
      if (end_size >= begin_size) {
    // 345|#|12 -> 12 5|#|34
    if (show_steps) {
      printf("Case 4:\n");
    }
    swap_parts(A, N, part_start, begin, begin_size, show_steps);
    assert(begin_size > 0); // Necessary for progress
    part_start += begin_size;
    size = end_size;
    // begin, end remain unchanged
      } else if (begin - part_start <= begin_size) {
    // 56|#|1234 -> 123 56|#|4
    size_t size_moved = begin - part_start;
    assert(size_moved >= end_size); // else the next step would be more efficient
    if (show_steps) {
      printf("Case 5\n");
    }
    swap_parts(A, N, part_start, begin, end_size, show_steps);
    move_part(A, N, end, begin + end_size, begin - end, show_steps);
    assert(end_size + (begin - end) == size_moved);
    size -= size_moved;
    part_start = begin;
    begin += size_moved;
    end += size_moved;
      } else if (end_size <= begin_size) {
    // 45|##|123 -> 123 #|45|# 
    if (show_steps) {
      printf("Case 6\n");
    }
    swap_parts(A, N, part_start, begin, end_size, show_steps);
    move_part(A, N, end, begin + end_size, begin_size - end_size, show_steps);
    part_start += begin_size;
    size = end_size;
    end = begin + end_size;
    // begin remains unchanged
      } else {
    // No case applies, this should never happen
    assert(0);
      }
    }
  }
}


int main()
{
  int N = 20;
  int A[20];
  size_t size = 17;
  size_t begin = 15;
  size_t i;
  for (i = 0; i < size; ++i) {
    A[(begin + i) % N] = i;
  }
  move_to_beginning(A, N, begin, size, 0);
  for (i = 0; i < size; ++i) {
    printf("%d ", A[i]);
  }
  printf("\n");
  return 0;
}
4

6 に答える 6

5

ケース 1: ソースは、配列全体よりも小さい 1 つの連続した領域内で最大でも宛先と重複します。

このケースの詳細な説明は、R. による最初の回答に記載されています。ここで追加することはありません。

ケース 2: ソースが 2 つの連続する領域でデスティネーションとオーバーラップするか、配列全体を回転させます

最も簡単な方法は、常に配列全体を回転させることです。これにより、不要な要素が目的の範囲から移動されますが、この場合K > N/2、必要な操作の数が 2 倍以上になることはありません。

配列を回転するには、サイクル リーダー アルゴリズムを使用します。配列の最初の要素 (A[0]) を取得し、目的の位置にコピーします。この位置の以前の内容は、適切な位置に再び移動します。一部の要素が開始位置に移動するまで続行します。

次の開始位置にサイクル リーダー アルゴリズムを適用し続けます: A[1]、A[2]、...、A[GCD(N,d) - 1]。ここdで、 はソースと宛先の間の距離です。

ステップの後GCD(N,d)、すべての要素が適切な位置にあります。これは次の理由で機能します。

  1. 位置 0、1、...、GCD(N,d) - 1 は異なるサイクルに属します - これらの数値はすべて異なる (モジュロGCD(N,d)) ためです。
  2. 各サイクルには長さがあります。N / GCD(N,d)なぜならd / GCD(N,d)、 とN / GCD(N,d)は互いに素だからです。

このアルゴリズムは単純で、各要素を 1 回だけ移動します。スレッドセーフにすることができます (書き込み先の範囲内でない限り、書き込みステップをスキップする場合)。その他のマルチスレッド関連の利点は、各要素が「移動」前の値と「移動」後の値の 2 つの値しか持てないことです (一時的な中間値はあり得ません)。

しかし、常に最適なパフォーマンスが得られるとは限りません。element_size * GCD(N,d)がキャッシュ ライン サイズに匹敵する場合、すべてのGCD(N,d)開始位置を取得してまとめて処理することがあります。この値が大きすぎる場合は、開始位置をいくつかの連続したセグメントに分割して、必要なスペースを O(1) に戻すことができます。

問題は、element_size * GCD(N,d)がキャッシュ ライン サイズよりもはるかに小さい場合です。この場合、多くのキャッシュ ミスが発生し、パフォーマンスが低下します。配列の一部を一時的に「スワップ」領域 (サイズ ) と交換するという gusbro のアイデアはd、この場合のより効率的なアルゴリズムを示唆しています。キャッシュに収まる「スワップ」領域を使用し、重複していない領域を memcpy でコピーすると、より最適化される可能性があります。


もう1つのアルゴリズム。宛先範囲にない要素は上書きされません。そしてキャッシュフレンドリーです。唯一の欠点は、各要素を正確に 2 回移動することです。

アイデアは、2 つのポインターを反対方向に移動し、ポイントされた要素を交換することです。重なり合う領域が反転するだけなので、重なり合う領域に問題はありません。このアルゴリズムの最初のパスの後、すべてのソース要素が目的の範囲に移動されますが、順序が逆になります。したがって、2 番目のパスは宛先範囲を逆にする必要があります。

for (d = dst_start, s = src_end - 1;
     d != dst_end;
     d = (d + 1) % N, s = (s + N - 1) % N)
  swap(s, d);

for (d = dst_start, s = dst_end - 1;
     d != dst_end;
     d = (d + 1) % N, s = (s + N - 1) % N)
  swap(s, d);
于 2012-09-11T21:23:57.493 に答える
3

これはまだ完全な答えではありませんが、それは正しい考えかもしれないと思います。

ソース範囲の要素から開始し、それがマップされる宛先位置を検討します。その位置は、ソース範囲内またはソース範囲外のいずれかです。ソース範囲外の場合は、コピーするだけで、その要素を使用できます。一方、ソース範囲内の宛先位置にマップする場合は、それをコピーできますが、上書きする古い値を保存し、ソースのこの新しい要素を使用して上記のプロセスを繰り返し実行する必要があります。

基本的に、順列のサイクルで操作しています。

問題は、あなたが何を終えたか、そして何をしなければならないかを追跡することです。O(n)作業スペースなしでこれを行う方法があるかどうかは、すぐにはわかりません。

于 2012-09-10T02:45:16.663 に答える
3

このソリューションは O(N) であり、範囲が重複する場合に使用するスクラッチ スペースとして、既に処理されたソースの場所を使用します。コピー元とコピー先の内容をコピー先の先頭に到達するまでスワップし、以前に生成されたスクラッチ スペースからコピーを続行します。2 番目のループは、スクラッチ スペースの各文字が使用された後に、破壊された領域を復元します。

move(A,N, src_idx, dst_idx, len)
{
  first_dst_idx=dst_idx;
  first_src_idx=src_idx;
  mlen=0;
  while(src_idx != first_dst_idx && len > 0)
  {
    temp = A[dst_idx];
    A[dst_idx] = A[src_idx];
    A[src_idx] = temp;
    src_idx=(src_idx+1) mod N;
    dst_idx=(dst_idx+1) mod N;
    len--; mlen++;
  }
  src_idx = first_src_idx;
  while(len > 0)
  {
    A[dst_idx] = A[src_idx];
    A[src_idx] = A[first_dst_idx];
    src_idx=(src_idx+1) mod N;
    dst_idx=(dst_idx+1) mod N; 
    first_dst_idx=(first_dst_idx+1) mod N;
    len--;
  }
  while(mlen > 0)
  { // restore reamining scratch space
    A[src_idx] = A[first_dst_idx];
    src_idx=(src_idx+1) mod N;
    first_dst_idx=(first_dst_idx+1) mod N;
    mlen--;
  }
}
于 2012-09-11T21:20:20.250 に答える
2

**これは、Cの長さがAの長さの半分以下の場合にのみ機能します。ただし、修正するためにここに残しておきます。**

**このソリューションでは、ターゲット範囲の内容は保持されません。これは、元の質問の表現と一致すると私が信じる動作です。**

;; A function that wraps an out-of-bounds index to its proper location.
mod'(i):
  return (i + length(A)) mod length(A)

;; shifts the range A[i]..A[i + n] to A[i - delta]..A[i - delta + n]
move_backward (i,delta,n):
   A[mod'(i - delta)] = A[mod'(i)]
   if (n > 0):
     move_backward (i + 1, delta, n - 1)

;; shifts the range A[i - n]..A[i] to A[i - n + delta]..A[i + delta]
move_forward (i, delta, n):
   A[mod'(i + delta)] = A[mod'(i)]
   if (n > 0):
      move_forward (i - 1, delta, n - 1)

shift_range (source_first, source_last, target_first):
   n = mod'(source_last - source_first)
   delta = mod'(target_first - source_first)

   if (delta > length(A) / 2):
       move_backward (source_first, length(A) - delta, n)
   else
       move_forward (source_last, delta, n)
于 2012-09-09T16:32:27.877 に答える
2

OK、memmove循環バッファーを使用する場合は、次のようにします。

  • ケース 1: ソース/宛先が重複していません。を使用するだけmemcpyで、バッファがラップする場所で必要に応じて分割できます。

  • ケース 2: ソース/宛先が等しい。何もしない。

  • ケース 3: ソースの開始が正確に dest 領域内にある。単純なフォワード コピー ループを実行し、for (i=0; i<k; i++) A[(dest+i)%N] = A[(src+i)%N];

  • ケース 4: dest の開始が厳密にソース領域内にある。単純な後方コピー ループを実行し、for (i=K; i; i--) A[(dest+i-1)%N] = A[(src+i-1)%N];

編集:この回答は、K が最大で N/2 の場合にのみ機能します。そうしないと、source と dest の両方が互いの内部で始まる可能性があります。すぐに修正することはできませんが、問題を修正する開始オフセットと方向を選択できる可能性があります...

于 2012-09-09T18:35:07.333 に答える
0

O(n 2 ) アルゴリズムは非常に簡単です。バッファ全体を 1 バイト回転させてから、必要なステップ数だけ繰り返します。

void rotateBuffer(char *buffer, int size, int steps)
{
    char tmp;
    int i;

    for (i = 0; i < steps; i++)
    {
        tmp = buffer[size - 1];
        memmove(buffer + 1, buffer, size - 1);
        buffer[0] = tmp;
    }
}

高速ではありませんが、一定の一時ストレージのみで仕事を完了できます。

編集:

以下のコメントで説明されているように、静的な基になる「背景」に対してバッファーのサブ部分だけを回転させる必要がある場合は、次のようにすることができます。

void rotateBuffer(int count, int start, int length)
{
    int i;
    int j;
    int index;

    // rotate 'count' bytes
    for (i = 0; i < count; i++)
    {
        // rotate by a single byte
        for (j = length - 1; j >= 0; j--)
        {
            index = start + i + j;
            buf[(index + 1) % SIZE] = buf[index % SIZE];
        }
    }
}

バッファ全体をローテーションする必要がある場合は問題があると思いますが、その場合は上記のコードにフォールバックできます。

于 2012-09-09T17:27:29.853 に答える