数据结构-随机化队列



我目前正在处理普林斯顿算法第一部分中的队列分配。其中一项任务是实现随机队列。这是一个关于使用不同数据结构的实现和权衡的问题。

问题:

随机化队列类似于堆栈或队列,不同之处在于移除的项是从数据结构中的项中随机均匀选择的。创建一个实现以下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.阵列实现:

我考虑的主要实现是数组实现。除了随机性之外,这将与数组队列的实现相同。

Query1.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;
}

Query1.2:为了让迭代器满足随机返回元素的要求,我用队列的所有索引创建了一个新数组,然后用Knuth shuffle操作对数组进行shuffle,并返回队列中特定索引处的元素。但是,这需要创建一个与队列长度相等的新数组。再说一次,我确信我错过了一个更有效的方法。

2.内部类实现

第二个实现涉及一个内部节点类。

public class RandomizedQueue<Item> {
    private static class Node<Item> {
        Item item;
        Node<Item> next;
        Node<Item> previous;
    }
}

查询2.1。在这种情况下,我了解如何有效地执行出列操作:返回一个随机节点并更改相邻节点的引用。

然而,我对如何返回一个迭代器感到困惑,该迭代器以随机顺序返回节点,而不必创建一个以随机顺序连接节点的全新队列。

此外,除了可读性和易于实现之外,在数组上使用这样的数据结构还有什么好处?

这篇文章有点长。我很感激你们花时间阅读我的问题并帮助我。谢谢

在您的数组实现中,Query1.1似乎是最好的方法。移除随机元素的唯一其他方法是将所有元素向上移动以填充其位置。因此,如果你有[1,2,3,4,5],而你删除了2,你的代码会向上移动项目3、4和5,你会减少计数。这将需要,平均每次移除n/2个项目。所以移除是O(n)。令人不快的

如果在迭代时不添加和删除项目,那么只需在现有数组上使用Fisher Yates shuffle,并开始从前到后返回项目。没有理由复制。这实际上取决于你的使用模式。如果您设想在迭代时添加项目和从队列中删除项目,那么如果不进行复制,事情就会变得不稳定。

使用链表方法,随机出列操作很难有效实现,因为为了获得随机项,您必须从前面遍历列表。因此,如果你的队列中有100个项目,并且你想删除第85个项目,你必须从前面开始,并遵循85个链接,然后才能找到你想要删除的项目。由于您使用的是双链接列表,当要删除的项目超过中点时,您可以通过从末尾向后计数来将时间减半,但当队列中的项目数量很大时,效率仍然非常低。想象一下,从一百万个项目的队列中删除第500000个项目。

对于随机迭代器,可以在开始迭代之前将链表打乱。这需要O(n-logn)时间,但只需要O(1)额外的空间。同样,在添加或删除的同时也存在迭代的问题。如果你想要这种能力,那么你需要复制。

对于查询1.1:在这里,您确实可以在恒定时间内删除随机元素。想法简单如下:

  • 选择要返回的随机元素
  • 将其与队列中的最后一个元素交换
  • 删除队列中的最后一个元素

通过这种方式,您可以保持一个没有"孔"的连续阵列

使用数组实现(必须是动态的/可调整大小的),以便为除构建迭代器之外的所有操作实现恒定(摊销)的最坏情况运行时间(由于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()方法。

创建迭代器时,您不需要对数组的整个副本进行混洗,而是在next()方法中访问每个元素时惰性地对其进行混洗。

此外,我得到了你真正想要的,那就是我可以发明一个随机队列吗?它可以随机出列,也可以从第一个到最后一个出列(这就是你想要保留顺序的原因,对吧?)。这两个操作,比如dequeueRandom(),dequeue(),都有固定的摊销时间。

不幸的是,不能同时做到这一点。

最新更新