【问题标题】:Data Structures - Randomized Queues数据结构 - 随机队列
【发布时间】:2015-09-17 13:37:23
【问题描述】:

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

问题:

随机队列类似于堆栈或队列,只是删除的项目是从数据结构中的项目中均匀随机选择的。创建实现以下 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.数组实现:

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

查询 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。 在这种情况下,我了解如何有效地执行出队操作:返回一个随机节点并更改相邻节点的引用。

但是,我对如何返回一个以随机顺序返回节点的迭代器感到困惑,而不必创建一个以随机顺序附加节点的全​​新队列。

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

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

【问题讨论】:

    标签: java random data-structures queue


    【解决方案1】:

    对于您的查询 1.1,保留随机队列顺序的意图是无意义的,因为您将其随机出列。

    另外,我有点得到你真正想要的东西,那就是我可以发明一个随机队列,它可以随机出队,也可以从头到尾出队(这就是你想要保留顺序的原因,对吗?)。而这两个操作,比如 dequeueRandom()、dequeue() 都有摊销常数时间。

    很遗憾,这不能同时进行。

    【讨论】:

      【解决方案2】:

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

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

      这样你就可以保持一个没有“洞”的连续数组

      【讨论】:

      • 但它改变了元素的顺序。
      • @gstackoverflow 但是,这个问题并不重要。出队、样本和迭代器都以随机顺序返回项目,因此,更改项目的内部顺序应该不是问题。
      【解决方案3】:

      在您的数组实现中,您的 Query 1.1 似乎是做事的最佳方式。删除随机元素的唯一其他方法是将所有内容向上移动以填充其位置。因此,如果您有 [1,2,3,4,5] 并删除了 2,您的代码会将项目 3、4 和 5 向上移动,并且您会减少计数。每次移除平均需要 n/2 个项目移动。所以去除是O(n)。不好。

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

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

      对于随机迭代器,您可以在开始迭代之前就地打乱链表。这需要 O(n log n) 时间,但只需要 O(1) 额外空间。同样,您在添加或删除的同时存在迭代问题。如果你想要那个能力,那么你需要制作一个副本。

      【讨论】:

      • 感谢您抽出宝贵时间回复我的问题 Jim。我知道它很长。非常感谢!
      【解决方案4】:

      使用数组实现(必须是动态的/可调整大小的),以便为除构建迭代器之外的所有操作实现恒定(摊销)最坏情况运行时(由于随机播放,这需要线性时间)。

      这是我的实现:

      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() 方法。

      【讨论】:

        【解决方案5】:

        在创建迭代器时,您不需要打乱整个数组副本,而是在使用 next() 方法访问每个元素时懒惰地打乱每个元素

        【讨论】:

          猜你喜欢
          • 2016-12-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多