2015-06-30 3 views
17

В настоящее время я работаю над алгоритмами Принстона, часть I. Одним из назначений является реализация рандомизированной очереди. Это вопрос, связанный с реализацией и компромиссом использования разных структур данных.Структуры данных - рандомизированные очереди

Вопрос:

Рандомизированная очередь похожа на стек или очередь, за исключением того, что элемент удален выбираются равномерно случайным образом из элементов в структуре данных. Создать общий тип данных RandomizedQueue, который реализует следующий API:

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 
} 

Подвох заключается в реализации операции DEQUEUE и итератор поскольку в DEQUEUE удаляет и возвращает случайного элемент и итераторы перебирают очереди в a .

1. Массив реализации:

Основная реализация Я рассматривал это реализация массива. Это будет идентично реализации очереди массивов, кроме случайности.

Query 1.1: Для операции dequeue я просто произвольно выбираю число из размера массива и возвращаю этот элемент, а затем перемещаю последний элемент в массиве в позицию возвращаемого элемента.

Однако этот подход изменяет порядок очереди. В этом случае это не имеет значения, поскольку я дежурирую в случайном порядке. Тем не менее, мне было интересно, есть ли время/память эффективный способ удаления из случайного элемента из массива при сохранении порядка очереди без необходимости создания нового массива и переноса всех данных на него.

// 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: Для итератора, чтобы удовлетворить требование о возвращении элементов в случайном порядке, я создаю новый массив со всеми индексами очереди, затем перетасовать массив операции Кнута в случайном порядке и возвращают элементы по тем конкретным индексам в очереди. Однако это связано с созданием нового массива, равного длине очереди. Опять же, я уверен, что мне не хватает более эффективного метода.

2. Внутренняя Реализация класса

Вторая реализация включает в себя класс внутреннего узла.

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

Query 2.1. В этом случае я понимаю, как эффективно выполнять операцию dequeue: Верните случайный узел и измените ссылки на соседние узлы.

Однако я смущен тем, как вернуть итератор, который возвращает узлы в случайном порядке, без необходимости создавать целую новую очередь с узлами, подключенными в случайном порядке.

Кроме того, в чем преимущества использования такой структуры данных над массивом, помимо удобства чтения и простоты реализации?

Этот пост длинный. Я ценю, что вы, ребята, нашли время, чтобы прочитать мой вопрос и помочь мне. Благодаря!

ответ

10

В вашей реализации массива ваш Query 1.1, кажется, лучший способ сделать что-то. Единственный способ удалить случайный элемент - это переместить все, чтобы заполнить его место. Так что если у вас было [1,2,3,4,5], и вы удалили 2, ваш код переместил бы предметы 3, 4 и 5 вверх, и вы уменьшили бы счет. Это займет в среднем n/2 элемента для каждого удаления. Таким образом, удаление O (n). Плохо.

Если вы не будете добавлять и удалять элементы во время итерации, просто используйте Shuffle Fisher-Yates в существующем массиве и начните возвращать элементы спереди назад. Нет причин делать копию. Это зависит от вашего шаблона использования. Если вы планируете добавлять и удалять элементы из очереди во время итерации, тогда все становится неустойчивым, если вы не делаете копию.

С помощью подхода, связанного с списком, операция случайного деактивации трудно реализовать эффективно, потому что для доступа к случайному элементу вам нужно пройти список спереди. Поэтому, если у вас есть 100 предметов в очереди, и вы хотите удалить 85-й предмет, вам нужно будет начать с фронта и следовать 85 ссылкам, прежде чем вы попадете в тот, который хотите удалить. Поскольку вы используете список с двойной связью, вы можете сократить это время пополам, посчитав назад с конца, когда элемент, который нужно удалить, находится за полпути, но он все еще ужасно неэффективен, когда количество элементов в очереди большой. Представьте, что вы удаляете 500 000-й элемент из очереди в один миллион элементов.

Для случайного итератора вы можете перетасовать связанный список на месте перед началом итерации. Это занимает время O (n log n), но просто O (1) дополнительное пространство. Опять же, у вас есть проблема с итерацией в то же время, когда вы добавляете или удаляете. Если вы хотите эту способность, вам нужно сделать копию.

+0

Спасибо, что нашли время ответить на мой вопрос Джим. Я знаю, что это было долго.Очень благодарен! – philosopher

-2

Правильный ответ - использовать массив. С удалением случайного элемента есть трюк, который нужно сделать для O (1)

1

Вам не нужно перетасовывать всю копию массива при создании итератора, но лениво Fisher-Yate перемещает каждый элемент при доступе к нему в метод next()

1

Использование реализации массива (должно быть динамическим/изменяемым по размеру) для достижения постоянной (амортизированной) наихудшего времени выполнения для всех операций, кроме создания итератора (это требует линейного времени из-за тасования).

Вот моя реализация:

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

Bonus задача: попробовать реализовать метод ToString().

0

Для вашего запроса 1.1: Здесь вы можете удалить случайный элемент в постоянное время. Идея просто следующим образом:

  • выбрать случайный элемент для возврата
  • поменять его с последним элементом в очереди
  • удалить последний элемент в очереди

Таким образом, вы продолжаете иметь непрерывный массив без «дыр»

Смежные вопросы