Je travaille actuellement sur la partie I des algorithmes de Princeton. L'une des tâches consiste à implémenter une file d'attente aléatoire. Cette question concerne la mise en œuvre et les compromis d'utilisation de différentes structures de données.
Question:
Une file d'attente aléatoire est similaire à une pile ou à une file d'attente, à la différence que l'élément supprimé est choisi uniformément de manière aléatoire parmi les éléments de la structure de données. Créez un type de données générique RandomizedQueue qui implémente l'API suivante:
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
}
Le problème ici est d’implémenter l’opération de retrait de la file et l’itérateur, car celle-ci supprime et renvoie un élément random et l’itérateur parcourt la file d’attente dans un ordre random .
1. Implémentation de la matrice:
L'implémentation principale que je considérais est une implémentation de tableau. Ce sera identique à la mise en œuvre d'une file d'attente de matrice sauf le caractère aléatoire.
Query 1.1: Pour l'opération de retrait de la file d'attente, je sélectionne simplement un nombre de manière aléatoire à partir de la taille du tableau, puis retourne cet élément, puis déplace le dernier élément du tableau à la position de l'élément renvoyé.
Cependant, cette approche modifie l'ordre de la file d'attente. Dans ce cas, cela n’a pas d’importance puisque je retire de la file d'attente dans un ordre aléatoire. Cependant, je me demandais s'il existait un moyen efficace en termes de temps et de mémoire pour retirer de la file un élément aléatoire du tableau tout en préservant l'ordre de la file sans avoir à créer un nouveau tableau et à lui transférer toutes les données.
// 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: Pour que l'itérateur réponde à l'exigence de retourner des éléments de manière aléatoire, je crée un nouveau tableau avec tous les indices de la file d'attente, puis mélange le tableau avec une opération de mélange aléatoire de Knuth et renvoie les éléments correspondant à ces index particuliers dans La queue. Cependant, cela implique la création d'un nouveau tableau égal à la longueur de la file d'attente. Encore une fois, je suis sûr qu'il me manque une méthode plus efficace.
2. Mise en œuvre de la classe interne
La deuxième implémentation implique une classe de nœud interne.
public class RandomizedQueue<Item> {
private static class Node<Item> {
Item item;
Node<Item> next;
Node<Item> previous;
}
}
Requête 2.1. Dans ce cas, je comprends comment effectuer efficacement l'opération de retrait de la file d'attente: retournez un nœud aléatoire et modifiez les références des nœuds adjacents.
Cependant, je ne sais pas comment renvoyer un itérateur qui renvoie les nœuds dans un ordre aléatoire sans devoir créer une toute nouvelle file d'attente avec des nœuds attachés dans un ordre aléatoire.
En outre, quels sont les avantages de l’utilisation d’une telle structure de données par rapport à un tableau, autres que la lisibilité et la facilité de mise en œuvre?
Ce post est un peu long. J'apprécie que vous ayez pris le temps de lire ma question et de m'aider. Merci!
Dans votre implémentation de tableau, votre Requête 1.1 semble être la meilleure façon de faire les choses. La seule autre façon de supprimer un élément aléatoire serait de tout déplacer pour occuper sa place. Ainsi, si vous aviez [1,2,3,4,5]
et que vous avez supprimé 2
, votre code déplacerait les éléments 3, 4 et 5 vers le haut et vous réduiriez le nombre. Cela prendra, en moyenne, n/2 mouvements d’articles pour chaque déménagement. Donc, le retrait est O (n). Mal.
Si vous n'allez ni ajouter ni supprimer des éléments lors de l'itération, utilisez simplement un shuffle Fisher-Yates sur le tableau existant et commencez à renvoyer les éléments de l'avant à l'arrière. Il n'y a aucune raison de faire une copie. Cela dépend vraiment de votre modèle d'utilisation. Si vous envisagez d'ajouter et de supprimer des éléments de la file d'attente pendant que vous effectuez une itération, les choses risquent de mal tourner si vous n'en faites pas de copie.
Avec l'approche par liste chaînée, l'opération de retrait aléatoire de la file d'attente est difficile à mettre en œuvre efficacement car, pour accéder à un élément aléatoire, vous devez parcourir la liste de l'avant. Donc, si vous avez 100 éléments dans la file d'attente et que vous souhaitez supprimer le 85ème élément, vous devrez commencer par le devant et suivre 85 liens avant de vous rendre à celui que vous souhaitez supprimer. Étant donné que vous utilisez une liste à double liaison, vous pouvez potentiellement réduire ce temps de moitié en comptant à rebours depuis la fin lorsque l'élément à supprimer dépasse la moitié de la moitié, mais il est tout à fait inefficace que est large. Imaginez que vous retiriez le 500 000e élément d'une file d'attente d'un million d'éléments.
Pour l'itérateur aléatoire, vous pouvez mélanger la liste chaînée sur place avant de commencer l'itération. Cela prend O (n log n), mais seulement O(1) espace supplémentaire. Encore une fois, vous avez le problème d'itérer en même temps que vous ajoutez ou supprimez. Si vous voulez cette capacité, alors vous devez en faire une copie.
Pour votre requête 1.1: Ici, vous pouvez en effet supprimer un élément aléatoire en temps constant .
De cette façon, vous continuez à avoir un tableau continu sans "trous"
Vous n'avez pas besoin de mélanger toute la copie du tableau lorsque vous créez l'itérateur, mais paresseusement, Fisher-Yate mélange chaque élément en y accédant avec la méthode next()
Utilisez l'implémentation de la matrice (doit être dynamique/redimensionnable) afin d'obtenir un temps d'exécution constant dans le pire des cas (amorti) pour toutes les opérations, à l'exception de la construction de l'itérateur (cette opération prend du temps de manière linéaire en raison du brassage).
Voici ma mise en œuvre:
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");
}
}
}
Remarque: mon implémentation est basée sur l’affectation suivante: http://coursera.cs.princeton.edu/algs4/assignments/queues.html
Bonus challenge: essayez d'implémenter une méthode toString ().