我目前正在研究普林斯顿大学的算法第一部分。其中一项任务是实施随机队列。这是关于使用不同数据结构的实现和权衡的问题。
问题:
随机队列类似于堆栈或队列,除了从数据结构中的项目中随机均匀地选择删除的项目。创建实现以下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
}
这里的catch是实现dequeue操作和迭代器,因为dequeue删除并返回随机元素,迭代器以随机顺序遍历队列。
1。阵列实施:
我正在考虑的主要实现是数组实现。这与除随机性之外的阵列队列的实现相同。
查询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;
}
查询1.2:为了使迭代器满足随机返回元素的要求,我创建了一个包含队列所有索引的新数组,然后使用Knuth shuffle操作对数组进行洗牌并返回队列中那些特定索引的元素。但是,这涉及创建一个等于队列长度的新数组。同样,我确信我错过了一种更有效的方法。
2。内部班级实施
第二个实现涉及内部节点类。
public class RandomizedQueue<Item> {
private static class Node<Item> {
Item item;
Node<Item> next;
Node<Item> previous;
}
}
查询2.1。在这种情况下,我了解如何有效地执行出队操作:返回一个随机节点并更改相邻节点的引用。
但是,我对如何返回以随机顺序返回节点的迭代器感到困惑,而不必创建一个以随机顺序连接节点的整个新队列。
此外,除了可读性和易于实现之外,在阵列上使用这种数据结构有什么好处?
这篇文章有点长。我感谢你们花时间阅读我的问题并帮助我。谢谢!
答案 0 :(得分:12)
在您的数组实现中,您的 Query 1.1 似乎是最好的方法。删除随机元素的唯一方法是将所有内容移动以填充其位置。因此,如果您有[1,2,3,4,5]
并且已移除2
,则您的代码会移动第3,4和5项,并且您会减少计数。每次移除平均需要n / 2项移动。所以删除是O(n)。坏。
如果您在迭代时不想添加和删除项目,那么只需在现有阵列上使用Fisher-Yates shuffle,然后开始从前到后返回项目。没有理由复制。这实际上取决于您的使用模式。如果您想要在重复过程中添加和删除队列中的项目,那么如果您不进行复制,事情会变得很糟糕。
使用链表方法,随机出列操作难以有效实现,因为为了获得随机项,您必须从前面遍历列表。因此,如果队列中有100个项目并且您想要删除第85个项目,那么在到达要删除的项目之前,您必须从前面开始并按照85个链接进行操作。由于您使用的是双链表,因此当您要删除的项目超出中途点时,您可以通过从结尾向后计数将该时间缩短一半,但是当它仍然非常低效时队列中的项目数量很大。想象一下,从一百万个项目的队列中删除第500,000个项目。
对于随机迭代器,您可以在开始迭代之前就地对链接列表进行随机播放。这需要O(n log n)时间,但只需要O(1)额外空间。同样,您在添加或删除的同时会遇到迭代问题。如果你想要那种能力,那么你需要复制一份。
答案 1 :(得分:4)
对于您的查询1.1:在这里您确实可以在恒定时间内删除随机元素。 这个想法简单如下:
通过这种方式,您可以保持连续阵列没有“漏洞”。
答案 2 :(得分:1)
在创建迭代器时,您不需要对整个数组副本进行随机播放,但在next()
方法
答案 3 :(得分:0)
使用数组实现(必须是动态的/可调整大小的),以便为除了构建迭代器之外的所有操作实现常量(分摊)最坏情况运行时(由于shuffle,这需要线性时间)。
这是我的实施:
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()方法。
答案 4 :(得分:0)
对于您的查询 1.1,保留随机队列顺序的意图是没有意义的,因为您将其随机出列。
另外,我有点得到你真正想要的东西,那就是我可以发明一个随机队列,它可以随机出队,也可以从头到尾出队(这就是你想要保留顺序的原因,对吧?)。而这两个操作,比如 dequeueRandom() 和 dequeue() 都有摊销固定时间。
很遗憾,这不能同时进行。