現在、プリンストンのアルゴリズムパートIの キューの割り当て に取り組んでいます。割り当ての1つは、ランダム化されたキューを実装することです。これは、さまざまなデータ構造を使用する場合の実装とトレードオフに関する質問です。
質問:
ランダム化されたキューは、削除されたアイテムがデータ構造内のアイテムからランダムに均一に選択されることを除いて、スタックまたはキューに似ています。次のAPIを実装する汎用データ型RandomizedQueueを作成します。
public class RandomizedQueue<Item> implements Iterable<Item> {
public RandomizedQueue() // construct an empty randomized queue
public boolean isEmpty() // is the queue empty?
public int size() // return the number of items on the queue
public void enqueue(Item item) // add the item
public Item dequeue() // remove and return a random item
public Item sample() // return (but do not remove) a random item
public Iterator<Item> iterator() // return an independent iterator over items in random order
public static void main(String[] args) // unit testing
}
ここでのキャッチは、デキューがランダム要素を削除して返し、イテレータがランダムな順序。
1。配列の実装:
私が検討していた主要な実装は、配列の実装です。これは、ランダム性を除いて、配列キューの実装と同じです。
Query 1.1:デキュー操作の場合、配列のサイズからランダムに数値を選択してその項目を返し、配列の最後の項目を次の位置に移動します返されたアイテム。
ただし、このアプローチはキューの順序を変更します。この場合、私はランダムな順序でデキューしているので問題ではありません。しかし、新しい配列を作成してすべてのデータをそこに転送する必要なしにキューの順序を維持しながら、配列からランダムな要素をデキューする時間/メモリ効率の良い方法があるかどうか疑問に思いました。
// Current code for dequeue - changes the order of the array after dequeue
private int[] queue; // array queue
private int N; // number of items in the queue
public Item dequeue() {
if (isEmpty()) throw NoSuchElementException("Queue is empty");
int randomIndex = StdRandom.uniform(N);
Item temp = queue[randomIndex]
if (randomIndex == N - 1) {
queue[randomIndex] = null; // to avoid loitering
} else {
queue[randomIndex] = queue[N - 1];
queue[randomIndex] = null;
}
// code to resize array
N--;
return temp;
}
Query 1.2:イテレータが要素をランダムに返すという要件を満たすために、キューのすべてのインデックスを含む新しい配列を作成し、Knuthシャッフル操作で配列をシャッフルし、キュー内の特定のインデックスにある要素を返します。ただし、これには、キューの長さに等しい新しい配列の作成が含まれます。繰り返しますが、より効率的な方法が欠けていると確信しています。
2。内部クラスの実装
2番目の実装には、内部ノードクラスが含まれます。
public class RandomizedQueue<Item> {
private static class Node<Item> {
Item item;
Node<Item> next;
Node<Item> previous;
}
}
クエリ2.1。この場合、デキュー操作を効率的に実行する方法を理解しています。ランダムノードを返し、隣接ノードの参照を変更します。
ただし、ランダムな順序で接続されたノードでまったく新しいキューを作成する必要なく、ランダムな順序でノードを返すイテレータを返す方法に困惑しています。
さらに、読みやすさと実装の容易さ以外に、配列に対してこのようなデータ構造を使用する利点は何ですか?
この投稿は長いです。皆さんが私の質問を読んで助けてくれたことに感謝します。ありがとう!
配列の実装では、Query 1.1が最善の方法のようです。ランダムな要素を削除する唯一の他の方法は、すべてを上に移動してそのスポットを満たすことです。だからあなたが[1,2,3,4,5]
を削除しました2
、コードでアイテム3、4、および5を上に移動し、カウントを減らします。これには、すべての削除に対して平均n/2のアイテム移動が必要です。したがって、除去はO(n)です。悪い。
反復中にアイテムを追加および削除しない場合は、既存の配列でフィッシャーイェーツのシャッフルを使用し、アイテムを前から後ろに返し始めます。コピーを作成する理由はありません。それは本当にあなたの使用パターンに依存します。反復中にキューにアイテムを追加したり削除したりすることを想定している場合、コピーを作成しないと状況が不安定になります。
リンクリストアプローチでは、ランダムアイテムを取得するためにリストを前面からトラバースする必要があるため、ランダムデキュー操作を効率的に実装することは困難です。したがって、キューに100個のアイテムがあり、85番目のアイテムを削除する場合、削除するアイテムに到達する前に、先頭から開始して85のリンクをたどる必要があります。二重リンクリストを使用しているので、削除するアイテムが中間点を超えた場合、最後から逆に数えることでその時間を半分に短縮できる可能性がありますが、キュー内のアイテムの数が多い場合は、それでもひどく非効率的ですは大きい。 100万個のアイテムのキューから50万個目のアイテムを削除するとします。
ランダム反復子の場合、反復を開始する前に、リンクリストをインプレースでシャッフルできます。これにはO(n log n)の時間がかかりますが、O(1)余分なスペースが必要です。ここでも、追加または削除と同時に反復処理するという問題があります。必要に応じてその能力、あなたはコピーを作成する必要があります。
クエリ1.1の場合:ここでは確かに、ランダムな要素を一定の時間で削除できます。アイデアは次のとおりです。
このようにして、「穴」のない連続した配列を維持します
イテレータを作成するときに配列のコピー全体をシャッフルする必要はありませんが、next()
メソッドでアクセスしながら、Fisher-Yateが遅延して各要素をシャッフルします
配列の実装を使用して(動的/サイズ変更可能でなければなりません)、反復子の構築を除くすべての操作で一定の(償却された)最悪の場合の実行時間を達成します(シャッフルのために線形時間がかかります)。
これが私の実装です:
import Java.util.Arrays;
import Java.util.Iterator;
import Java.util.NoSuchElementException;
import Java.util.Random;
/* http://coursera.cs.princeton.edu/algs4/assignments/queues.html
*
* A randomized queue is similar to a stack or queue, except that the item
* removed is chosen uniformly at random from items in the data structure.
*/
public class RandomizedQueue<T> implements Iterable<T> {
private int queueEnd = 0; /* index of the end in the queue,
also the number of elements in the queue. */
@SuppressWarnings("unchecked")
private T[] queue = (T[]) new Object[1]; // array representing the queue
private Random rGen = new Random(); // used for generating uniformly random numbers
/**
* Changes the queue size to the specified size.
* @param newSize the new queue size.
*/
private void resize(int newSize) {
System.out.println("Resizing from " + queue.length + " to " + newSize);
T[] newArray = Arrays.copyOfRange(queue, 0, newSize);
queue = newArray;
}
public boolean isEmpty() {
return queueEnd == 0;
}
public int size() {
return queueEnd;
}
/**
* Adds an element to the queue.
* @param elem the new queue entry.
*/
public void enqueue(T elem) {
if (elem == null)
throw new NullPointerException();
if (queueEnd == queue.length)
resize(queue.length*2);
queue[queueEnd++] = elem;
}
/**
* Works in constant (amortized) time.
* @return uniformly random entry from the queue.
*/
public T dequeue() {
if (queueEnd == 0) // can't remove element from empty queue
throw new UnsupportedOperationException();
if (queueEnd <= queue.length/4) // adjusts the array size if less than a quarter of it is used
resize(queue.length/2);
int index = rGen.nextInt(queueEnd); // selects a random index
T returnValue = queue[index]; /* saves the element behind the randomly selected index
which will be returned later */
queue[index] = queue[--queueEnd]; /* fills the hole (randomly selected index is being deleted)
with the last element in the queue */
queue[queueEnd] = null; // avoids loitering
return returnValue;
}
/**
* Returns the value of a random element in the queue, doesn't modify the queue.
* @return random entry of the queue.
*/
public T sample() {
int index = rGen.nextInt(queueEnd); // selects a random index
return queue[index];
}
/*
* Every iteration will (should) return entries in a different order.
*/
private class RanQueueIterator implements Iterator<T> {
private T[] shuffledArray;
private int current = 0;
public RanQueueIterator() {
shuffledArray = queue.clone();
shuffle(shuffledArray);
}
@Override
public boolean hasNext() {
return current < queue.length;
}
@Override
public T next() {
if (!hasNext())
throw new NoSuchElementException();
return shuffledArray[current++];
}
/**
* Rearranges an array of objects in uniformly random order
* (under the assumption that {@code Math.random()} generates independent
* and uniformly distributed numbers between 0 and 1).
* @param array the array to be shuffled
*/
public void shuffle(T[] array) {
int n = array.length;
for (int i = 0; i < n; i++) {
// choose index uniformly in [i, n-1]
int r = i + (int) (Math.random() * (n - i));
T swap = array[r];
array[r] = array[i];
array[i] = swap;
}
}
}
@Override
public Iterator<T> iterator() {
return new RanQueueIterator();
}
public static void main(String[] args) {
RandomizedQueue<Integer> test = new RandomizedQueue<>();
// adding 10 elements
for (int i = 0; i < 10; i++) {
test.enqueue(i);
System.out.println("Added element: " + i);
System.out.println("Current number of elements in queue: " + test.size() + "\n");
}
System.out.print("\nIterator test:\n[");
for (Integer elem: test)
System.out.print(elem + " ");
System.out.println("]\n");
// removing 10 elements
for (int i = 0; i < 10; i++) {
System.out.println("Removed element: " + test.dequeue());
System.out.println("Current number of elements in queue: " + test.size() + "\n");
}
}
}
注:私の実装は次の割り当てに基づいています: http://coursera.cs.princeton.edu/algs4/assignments/queues.html
ボーナスチャレンジ:toString()メソッドを実装してみてください。