Compartir tecnología

Preguntas de la entrevista sobre desarrollo de juegos 7

2024-07-08

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

¿Cómo se ve la estructura de datos subyacente de ArrayList?

La capa inferior de ArrayList se implementa en función de matrices. Lo hace expandiendo o reduciendo dinámicamente el tamaño de la matriz. Cuando la capacidad no es suficiente, creará una matriz más grande, luego copiará los datos originales y finalmente le agregará los nuevos datos.

El mecanismo de expansión de ArrayList es: cuando se agrega el primer elemento, la capacidad de ArrayList es 10 cada vez que se agrega un nuevo elemento, si se excede la capacidad, se duplicará la capacidad original, es decir, la capacidad original * 2; , si la capacidad original es 0, entonces la nueva capacidad es 1.

Ventajas y desventajas del mecanismo de expansión ArrayList:

ventaja:
  1. El mecanismo de expansión de ArrayList es simple de entender y fácil de implementar.
  2. La capacidad ampliada debe ser mayor que la capacidad original, lo que puede reducir la cantidad de copias de la matriz al agregar nuevos elementos y mejorar la eficiencia de agregar elementos.
defecto:
  1. El mecanismo de expansión de ArrayList provoca un desperdicio de memoria y puede provocar fácilmente un desperdicio de espacio de memoria.
  2. Cuando la capacidad es grande, cada expansión requiere una gran cantidad de memoria y recursos de CPU, lo que afectará el rendimiento. Por lo tanto, cuando se utiliza ArrayList, la capacidad inicial debe configurarse adecuadamente.
ArrayList no es seguro para subprocesos

La implementación interna de ArrayList se basa en matrices. Cuando varios subprocesos acceden al mismo ArrayList al mismo tiempo, pueden producirse inconsistencias en los datos. Por ejemplo, cuando un subproceso lee los datos de ArrayList y otro subproceso agrega/elimina datos. ArrayList, puede haber Cambie los datos en ArrayList, de modo que el hilo que lee los datos de ArrayList pueda leer datos incorrectos, lo que provocará un error en el programa.

Los métodos comunes para resolver la inseguridad de los subprocesos de ArrayList incluyen:
  1. Utilice Vector en lugar de ArrayList: Vector está sincronizado y todos los métodos se modifican mediante sincronización, por lo que es seguro para subprocesos;
  2. Envuelva ArrayList en seguridad para subprocesos: utilice el método Collections.synchronizedList(list) para convertir ArrayList en seguridad para subprocesos;
  3. Utilice CopyOnWriteArrayList: es una colección eficiente y segura para subprocesos. Su operación de escritura se implementa copiando la matriz subyacente. El costo de esta implementación está dentro de un rango aceptable. Es adecuado para escenarios con más lectura y menos escritura.
  4. Utilice bloqueos para lograr la seguridad de los subprocesos: puede utilizar mecanismos de bloqueo como ReentrantLock para implementar un ArrayList seguro para subprocesos.

La diferencia entre pila y montón

  • La pila es una tabla lineal especial. Su característica es que los datos solo se pueden insertar y eliminar en un extremo, según el principio de primero en entrar, último en salir, último en entrar, primero en salir. Es una estructura de almacenamiento que se puede utilizar para almacenar valores de parámetros de funciones, variables locales, etc.

  • Un montón es una estructura de árbol especial que se caracteriza por el hecho de que los valores de todos los nodos son mayores o iguales que los valores de sus nodos secundarios, y el valor del nodo raíz es el mayor o el menor. El montón es una estructura de almacenamiento dinámico que se puede utilizar para almacenar grandes cantidades de datos, como clasificación, búsqueda, etc.

Capa inferior de corrutina

La esencia de una rutina es un hilo liviano. Cada rutina tiene una pila para almacenar funciones y sus parámetros, variables locales, etc. La rutina se puede suspender, reanudar y cambiar. Así es como se implementa una rutina.

Principio de C# GC (recolección de basura)

  1. Recuento de referencias: cuando se hace referencia a un objeto, su recuento de referencias aumenta en 1. Cuando la referencia deja de ser válida, su recuento de referencias disminuye en 1. Si el recuento de referencias de un objeto llega a 0, el recolector de basura reclamará el objeto.
  2. Marcar y borrar: cuando el recolector de basura se está ejecutando, atraviesa los objetos de acuerdo con la relación de referencia, marca los objetos accesibles como "vivos", marca los objetos inaccesibles como "muertos" y luego borra todos los objetos marcados como "muertos".
  3. Algoritmo de copia: el recolector de basura divide la memoria disponible en dos bloques y solo usa un bloque a la vez. Cuando el espacio es insuficiente, los objetos supervivientes se copian en otro bloque de espacio y luego los objetos copiados se borran del original. espacio.
  4. Algoritmo de complemento de marca: cuando el recolector de basura se está ejecutando, primero marcará todos los objetos supervivientes y luego moverá todos los objetos supervivientes a un extremo para limpiar los fragmentos de espacio no utilizados.

La diferencia entre sincronización de estado y sincronización de cuadros

Sincronización de estados Se refiere a transmitir el estado (como posición, velocidad, aceleración, etc.) de cada máquina en el sistema multimáquina a otras máquinas en cada ciclo de control, de modo que cada máquina permanezca sincronizada. La sincronización de estado puede lograr el rendimiento en tiempo real del control colaborativo de múltiples máquinas, pero dado que es necesario transmitir una gran cantidad de datos en cada ciclo de control, su precisión puede ser relativamente baja.

Sincronización de fotogramas Significa que en cada ciclo de control, los comandos de control de cada máquina en el sistema de múltiples máquinas se transmiten a otras máquinas para que cada máquina permanezca sincronizada. La sincronización de cuadros puede lograr la precisión del control colaborativo de múltiples máquinas, pero dado que solo se transmite una pequeña cantidad de comandos de control en cada ciclo de control, su rendimiento en tiempo real puede ser relativamente bajo.

Patrones de diseño comunes

Patrón singleton
Patrón de fábrica
Patrón compuesto
Patrón de proxy

Características de listas enlazadas, árboles binarios y tablas hash

1. Lista enlazada:
  • Es una estructura de lista lineal, que se caracteriza porque cada nodo tiene un puntero que apunta al siguiente nodo, formando así una lista enlazada;
  • Independientemente de insertar o eliminar un nodo, solo necesita cambiar la dirección del puntero y la complejidad del tiempo es O (1);
  • La complejidad temporal de encontrar un nodo es O (n), y la búsqueda debe realizarse en orden comenzando desde el nodo principal;
  • Las listas vinculadas no necesitan considerar problemas de asignación de espacio. Generalmente, la asignación dinámica de memoria se utiliza para lograr una administración de memoria flexible.
2. Árbol binario:
  • Un árbol binario es una estructura de árbol en la que cada nodo tiene como máximo dos hijos;
  • La complejidad temporal de las operaciones de búsqueda, inserción y eliminación del árbol binario es O (log2n), O (log2n) y O (log2n) respectivamente;
  • Dado que cada nodo del árbol binario no se almacena continuamente, sino jerárquicamente, y cada nodo solo puede tener dos hijos, el espacio de almacenamiento se puede utilizar de manera más eficiente.
3. Tabla hash:
  • Una tabla hash es una estructura de datos que asigna una clave a una ubicación en una tabla para acceder a registros y acelerar las búsquedas;
  • La complejidad del tiempo de búsqueda de la tabla hash es O (1), y la complejidad del tiempo de inserción y eliminación es O (n);
  • La implementación de una tabla hash requiere espacio adicional para almacenar la tabla hash en sí, y es necesario resolver el problema de las colisiones hash.

El principio subyacente de HashMap

La capa inferior de HashMap se implementa mediante una lista vinculada de matriz (árbol rojo-negro). Almacena datos de acuerdo con el valor hashCode de la clave. Puede calcular la posición de los datos en la matriz (conflicto hash) en función del hash. código y utiliza una lista vinculada (árbol rojo-negro) para almacenar conflictos Los datos. HashMap En Java 8, cuando la longitud de la lista vinculada excede el umbral (el valor predeterminado es 8), se convertirá en un árbol rojo-negro para mejorar la eficiencia de la consulta.Cuando la capacidad no sea suficiente, se expandirá automáticamente. El factor de carga predeterminado es 0,75 y el método de expansión es 2 veces la capacidad.

Cómo determinar si una lista enlazada tiene un ciclo

  1. Utilice una tabla hash para recorrer cada nodo en la lista vinculada y almacenar la dirección del nodo en la tabla hash. Si el nodo actual ya existe en la tabla hash, significa que la lista vinculada tiene un ciclo;
  2. Defina dos punteros, un puntero lento se mueve un paso a la vez y un puntero rápido se mueve dos pasos a la vez. Si el puntero rápido encuentra el puntero lento, significa que la lista vinculada tiene un ciclo.

¿Cuáles son los escenarios de uso de pilas y colas?

Las funciones de avance y retroceso del navegador: las páginas web visitadas por el navegador pueden realizar las funciones de avance y retroceso a través de la estructura de datos de la pila.

¿Sabes algo sobre el problema de la adherencia de TCP?

El problema de la adherencia de TCP se refiere al hecho de que el protocolo TCP no fragmenta los datos al transmitirlos, lo que hace que la cantidad de datos recibidos por el extremo receptor sea mayor que la cantidad de datos enviados por el extremo emisor.

Las formas de resolver el problema de la rigidez de TCP son:
  1. Agregue un delimitador en el extremo emisor: agregue un delimitador en el extremo emisor después de que el extremo receptor reciba el delimitador, puede dividir los datos según el delimitador.
  2. Agregue un encabezado en el extremo emisor: el extremo emisor agrega un encabezado antes de enviar los datos. El encabezado contiene la información de longitud de los datos actuales. El extremo receptor divide los datos según la información de longitud del encabezado.
  3. Agregue un búfer en el extremo emisor: antes de enviar datos, el extremo emisor primero coloca los datos en el búfer y solo envía una parte de los datos en el búfer cada vez. Después de recibir los datos, el extremo receptor determina si desea enviarlos. datos basados ​​en la longitud total de los datos completos.

Cómo implementar un TCP simple usando UDP

En primer lugar, los datagramas UDP pueden ayudar a implementar el proceso de protocolo de enlace de tres vías en el protocolo TCP/IP. En el primer protocolo de enlace, un cliente envía un datagrama UDP que contiene una solicitud de protocolo de enlace. Cuando el servidor reciba este mensaje, responderá con un mensaje de confirmación, indicando que el servidor ha recibido la solicitud de protocolo de enlace del cliente y está listo para brindar servicios. En el segundo protocolo de enlace, el cliente enviará nuevamente un datagrama UDP. Esta vez el mensaje contiene información útil, como la dirección IP del cliente, el número de puerto, etc., para que el servidor pueda identificar al cliente. En el tercer apretón de manos, el servidor enviará un datagrama UDP indicando que se ha establecido la conexión y el cliente puede comenzar a enviar datos.

En segundo lugar, los datagramas UDP también pueden ayudar a realizar el proceso de transmisión de datos en el protocolo TCP/IP. Cuando el cliente necesita enviar datos al servidor, los datos se encapsularán en un datagrama UDP y se enviarán al servidor después de que el servidor reciba el datagrama UDP, analizará los datos contenidos en el mensaje y realizará el procesamiento relacionado.

Finalmente, los datagramas UDP también pueden ayudar a implementar el proceso de terminación en el protocolo TCP/IP.Cuando el cliente ya no necesita comunicarse con el servidor, puede enviar un datagrama UDP para indicar que el cliente está finalizando la conexión. Después de que el servidor reciba este mensaje, liberará los recursos correspondientes, completando así todo el protocolo TCP/IP. proceso de conexión.

¿Has usado corrutinas? ¿Por qué utilizar corrutinas?Por qué las corrutinas son más rápidas

Las corrutinas permiten que los programas cambien entre diferentes tareas, mejorando así la eficiencia del programa y reduciendo el tiempo de ejecución del programa. Las corrutinas permiten que un programa cambie entre múltiples tareas en lugar de esperar a que se complete una tarea antes de comenzar otra. También puede compartir variables entre diferentes subprocesos, reduciendo así el tiempo de ejecución del programa. Para aplicaciones multitarea, el uso de corrutinas puede mejorar significativamente el rendimiento, lo que resulta en velocidades de ejecución más rápidas.

¿Es más rápido recorrer una matriz o una lista vinculada de la misma longitud? ¿Por qué?

Las matrices son más rápidas porque la dirección de cada elemento de la matriz es continua y fija, y la dirección del siguiente elemento se puede obtener rápidamente, mientras que la dirección de cada elemento de la lista vinculada es discontinua y es necesario atravesar el puntero. para obtener la dirección del siguiente elemento, por lo que el recorrido de la matriz es más rápido.

Hablemos de funciones virtuales. ¿Por qué necesitamos funciones virtuales?

Una función virtual es una función especial que se diferencia de las funciones ordinarias en que el compilador la define automáticamente y se puede llamar en tiempo de compilación. La característica de una función virtual es que su implementación se determina en tiempo de ejecución, no en tiempo de compilación.
El objetivo principal de las funciones virtuales es lograr polimorfismo. Una clase abstracta puede definir múltiples funciones virtuales y luego sus subclases pueden implementar estas funciones.

¿Puede el destructor ser una función virtual? ¿Tiene que ser una función virtual? ¿Por qué? ¿Cuál es el problema si no es una función virtual?

No es necesario que sea una función virtual, pero generalmente se recomienda utilizar una función virtual, porque una función virtual puede ser anulada por una clase derivada, de modo que el destructor de la clase derivada se pueda ejecutar correctamente si es una función virtual. no se utiliza, no se llamará al destructor de la clase derivada, lo que puede causar problemas como pérdidas de memoria.

Presentamos el proceso de renderizado

El proceso de renderizado es una serie de pasos que se utilizan para convertir datos de escenas de juegos a partir de información de entrada a imágenes que se muestran en la pantalla.

El proceso del proceso de renderizado se divide en tres etapas principales: etapa de preparación, etapa de geometría y etapa de iluminación.

En la fase de preparación, el motor del juego carga los modelos y texturas de la escena del juego en la unidad de procesamiento de gráficos (GPU) y organiza los datos para su uso en fases posteriores.

Durante la etapa de geometría, se utilizan transformaciones matriciales para colocar el modelo en un espacio tridimensional y convertir el modelo en una forma que pueda ser soportada por píxeles en la pantalla.

En la etapa de iluminación, la fuente de luz y el modelo de iluminación se utilizan para calcular el valor de color de cada píxel y la imagen resultante finalmente se muestra en la pantalla.

Hablemos de las operaciones de inserción, consulta y eliminación de árboles de búsqueda binarios y de cuál es la complejidad temporal.

  1. insertar:
  • Complejidad del tiempo: O (log n)
  • Pasos:
  1. Trate el nodo que se insertará como un nuevo nodo hoja, comenzando desde el nodo raíz;
  2. Si el valor del nodo que se va a insertar es menor que el valor del nodo actual, vaya al nodo secundario izquierdo del nodo actual;
  3. Si el valor del nodo que se insertará es mayor que el valor del nodo actual, vaya al nodo secundario derecho del nodo actual;
  4. Si el nodo actual no tiene nodos secundarios, el nodo que se insertará será el nodo secundario del nodo actual;
  5. De lo contrario, repita los pasos 2 a 4 hasta que encuentre un nodo sin nodos secundarios y el nodo que se va a insertar se utilice como nodo secundario de este nodo.

¿Cuáles son las condiciones para que el algoritmo codicioso obtenga la solución óptima?

Las condiciones para que el algoritmo codicioso obtenga la solución óptima son la "subestructura óptima" y la "propiedad de selección codiciosa":

  1. Subestructura óptima: La solución óptima a un problema contiene las soluciones óptimas a los subproblemas del problema;
  2. Propiedad de selección codiciosa: en cada paso, se realiza una elección óptima local y el resultado final es la solución óptima global.