Implementación de tipo de datos para múltiples histogramas dinámicos
Esto es casi el ejercicio 3.2.14. del libro Computer Science An Interdisciplinary Approach de Sedgewick & Wayne (ya que estoy estudiando por mi cuenta, lo cambié un poco):
Desarrolle una versión de Histogram que use StdDraw , para que un cliente pueda crear múltiples histogramas. Utilice un cliente de prueba que cree histogramas para lanzar monedas (ensayos de Bernoulli) con una moneda sesgada que tenga cara con probabilidad p, para p = 0,2, 0,4, 0,6. y 0.8, tomando el número de pruebas de la línea de comando.
Aquí están mis programas:
public class Histogram {
private final double[] data;
private final double max;
public Histogram(double[] data, double max) {
this.data = data;
this.max = max;
StdDraw.setXscale(0, data.length);
StdDraw.setYscale(0, max * 3);
}
public double[] getData() {
return data;
}
public int findMax() {
double max = 0;
int dataLength = data.length;
for (int i = 0; i < dataLength; i++) {
max = Math.max(max, data[i]);
}
return (int) max;
}
public void addData(int index) {
data[index]++;
}
public void draw(double xIncrement, double yIncrement) {
StdDraw.enableDoubleBuffering();
StdDraw.setPenColor(StdDraw.BOOK_BLUE);
for (int i = 0; i < data.length; i++) {
StdDraw.filledRectangle(i + 0.5 + xIncrement * data.length, yIncrement * data.length + data[i] / 2, 0.45, data[i] / 2);
StdDraw.show();
}
StdDraw.setPenColor(StdDraw.RED);
StdDraw.line(data.length + xIncrement * data.length + 0.005, 0,
data.length + xIncrement * data.length + 0.025, max * 3);
}
public static void main(String[] args) {
int trials = Integer.parseInt(args[0]);
double[] diceData = new double[6];
Histogram histogram = new Histogram(diceData, (trials / 6) * 2);
StdDraw.setPenColor(StdDraw.BOOK_BLUE);
for (int t = 1; t <= trials; t++) {
double r = Math.random();
if (r < 1.0 / 6.0) histogram.addData(0);
else if (r < 2.0 / 6.0) histogram.addData(1);
else if (r < 3.0 / 6.0) histogram.addData(2);
else if (r < 4.0 / 6.0) histogram.addData(3);
else if (r < 5.0 / 6.0) histogram.addData(4);
else if (r < 6.0 / 6.0) histogram.addData(5);
histogram.draw(0, 0);
}
}
}
public class Histograms {
private final Histogram[] histograms;
private final double max;
public Histograms(Histogram[] histograms, double max) {
this.histograms = histograms;
this.max = max;
StdDraw.setXscale(0, histograms[0].getData().length * histograms.length);
StdDraw.setYscale(0, max);
}
public void draw() {
int rows = histograms.length;
int columns = histograms.length;
for (int i = 0; i < columns; i++) {
if (rows % columns == 0) {
rows = rows / columns;
break;
} else {
rows++;
}
}
int m = 0;
for (int c = 0; c < columns; c++) {
for (int r = 0; r < rows; r++) {
histograms[m].draw(c, r);
m++;
}
}
}
public static void main(String[] args) {
int trials = Integer.parseInt(args[0]);
double max = trials;
double[] probabilities = {
0.2,
0.4,
0.6,
0.8
};
double[][] diceData = new double[4][2];
Histogram[] histograms = new Histogram[4];
for (int i = 0; i < 4; i++) {
histograms[i] = new Histogram(diceData[i], max);
}
for (int t = 1; t <= trials; t++) {
if (Math.random() < probabilities[0]) histograms[0].addData(0);
else histograms[0].addData(1);
if (Math.random() < probabilities[1]) histograms[1].addData(0);
else histograms[1].addData(1);
if (Math.random() < probabilities[2]) histograms[2].addData(0);
else histograms[2].addData(1);
if (Math.random() < probabilities[3]) histograms[3].addData(0);
else histograms[3].addData(1);
Histograms multipleHistograms = new Histograms(histograms, max);
multipleHistograms.draw();
StdDraw.pause(20);
}
}
}
Quería Histogramtrabajar también de forma independiente Histogramsy, por lo tanto, me vi obligado a inyectar algo de redundancia en estos dos programas (por ejemplo, ambos usan escalado, pero el escalado interno Histogramsanula el escalado interno Histogram).
Aquí hay una instancia de Histogram:
Entrada: 100
Producción:
Aquí hay una instancia de Histograms:
Entrada: 100
Producción:
¿Hay alguna forma de mejorar mis programas?
Gracias por tu atención.
Respuestas
Algunas observaciones, principalmente centradas en la Histogramclase.
Separación de intereses
Su aplicación contiene diferentes partes: partes algorítmicas (recopilación de datos de histograma), partes de la interfaz de usuario ( draw()métodos) y main()métodos.
Ya separa estas preocupaciones en diferentes métodos, lo cual es bueno. Pero recomiendo ir un paso más allá y tener diferentes clases, por ejemplo:
- Histograma (para la parte algorítmica),
- StdDrawHistogram (para presentar un histograma usando
StdDraw), - HistogramApp (que contiene el
main()método, conectando las partes).
API de clase de histograma
Cambiaría un poco la API pública de Histogram:
Estás usando una double[]matriz para contar. Pero nunca haces recuentos fraccionarios, siempre solo agregas un número entero 1a los cubos. Por lo tanto, debe cambiar eso a int[](o, long[]en el caso poco probable, espera más de dos mil millones de cuentas).
Su constructor public Histogram(double[] data, double max)obliga a la persona que llama a preparar una double[]matriz de la dimensión adecuada. Esto sorprende a la persona que llama. Debería ser suficiente indicar el Histogramnúmero de depósitos, y configurar la estructura de recuento (su matriz) debería ser responsabilidad del constructor del histograma.
Encapsulamiento
Por lo general, desea ocultar las partes internas (que está utilizando una matriz en lugar de alguna otra estructura de datos elegante) de las personas que llaman. De esta manera, puede cambiar las partes internas más tarde sin afectar el código que usa su clase. Y hace que sea imposible que el código externo juegue con los internos, sin pasar por la API de su clase. Por ejemplo, actualmente, en algún lugar de su main()método puede escribir cosas como diceData[2] = 7;, y luego se sobrescriben los datos del histograma en ese depósito. La única forma de cambiar los datos de su histograma debería ser llamando a sus métodos.
Siguiendo este argumento, también reemplazaría el public double[] getData()método con dos métodos:
public int getNumberOfBuckets() {
return data.length;
}
public int getCountInBucket(int i) {
return data[i];
}
También obliga a la persona que llama al constructor a proporcionar un maxvalor que no se puede conocer en ese momento, mientras que el histograma aún está vacío. Tal vez el nombre sea engañoso y no se refiera al recuento máximo en un cubo, sino a la altura del dibujo. Entonces no pertenece a la parte algorítmica, sino a la parte del dibujo.
Legibilidad
Por lo general, el código se lee con mucha más frecuencia que se escribe, por lo que es importante que pueda comprender de inmediato lo que está sucediendo.
Con el ejemplo de este maxcampo, recomiendo encarecidamente documentar el significado de los elementos de su código. Al menos para mí, no está claro si max está destinado a ser el recuento máximo encontrado en el histograma, o un espacio a reservar al dibujar el histograma, o algo más. Escribir comentarios de Javadoc para campos y métodos le ayuda a evitar tales ambigüedades. Especialmente si lee su código algunos meses después de la creación inicial, se alegrará de haberlo documentado. Tener que expresar sus conceptos por escrito le obliga a tener los conceptos claramente establecidos en su mente.
Otro medio para ayudar a la legibilidad es nombrar los elementos del código. Ya hizo un buen trabajo allí, solo que el nombre maxes un poco confuso, como se discutió anteriormente.
El campo máximo
Tienes un campo llamado max. Solo se usa dentro del constructor, para establecer la escala para el dibujo, y no se accede a ningún otro lugar. Por lo tanto, mantenerlo como información en la instancia durante toda la vida útil de la instancia después de la construcción es inútil y puede eliminar el private final double max;campo.
En el findMax()método útil , tiene una variable local que maxrecopila la información algorítmica "recuento más alto en cualquier depósito". Tener el mismo nombre que el campo maxconfunde al lector. Primero no me di cuenta de que se trataba de dos cosas diferentes. Recomendaría no reutilizar los nombres de campo como variables locales.