03_Comprensión profunda de listas enlazadas

1. Lista con enlaces simples y lista con enlaces dobles

Desde un punto de vista estructural, una lista doblemente enlazada puede soportar la complejidad del tiempo O (1) para encontrar el nodo precursor. Es esta característica la que hace que las operaciones de inserción y eliminación de una lista doblemente enlazada en algunos casos sea mejor que una lista enlazada individualmente. . Simple y eficiente

Primero observe la operación de eliminación, dividida en dos situaciones

  • El primer tipo: eliminar el nodo con "valor igual a un valor dado" en el nodo
  • El segundo tipo: eliminar el nodo al que apunta el puntero dado

Primero veamos el primer tipo. Para ambos, la complejidad de tiempo es O (n).
El segundo tipo. Para una lista con un solo enlace, eliminar un nodo q requiere conocer su nodo predecesor, y una lista con un solo enlace no admite direct Obtiene el nodo predecesor, por lo que para encontrar el nodo predecesor, todavía tenemos que recorrer la lista vinculada desde el nodo inicial hasta p-> next = q, lo que indica que p es la lista doblemente vinculada del nodo predecesor de q. La lista doblemente enlazada tiene ventajas significativas, porque los nodos de la lista doblemente enlazada han guardado los punteros de los nodos predecesores.

De la misma manera, para la operación de inserción, la lista enlazada individualmente tiene que encontrar el nodo predecesor del nodo insertado desde el principio.
Una lista doblemente enlazada utiliza espacio para el tiempo.

2. Matriz y lista vinculada

Inserte la descripción de la imagen aquí
La matriz es simple y fácil de usar. Utiliza espacio de memoria contiguo en la implementación. Puede usar el mecanismo de caché de la CPU para leer previamente los datos en el grupo, por lo que la eficiencia de acceso es mayor. La lista enlazada no se almacena continuamente en la memoria, por lo que no es compatible con la memoria caché de la CPU y no se puede leer con eficacia.

PD: ¿Cuál es el mecanismo de caché de la CPU? ¿Por qué es mejor una matriz?
Cuando la CPU lee datos de la memoria, primero carga los datos leídos en la caché de la CPU. Y cada vez que la CPU lee datos de la memoria, no solo lee la dirección específica a la que se accede, sino que lee un bloque de datos (no estoy seguro de este tamaño ...) y lo guarda en la caché de la CPU, y luego accede a la memoria la próxima vez Cuando se encuentran datos, se buscará primero en la memoria caché de la CPU y, si se encuentran, no se recuperarán de la memoria. De esta manera, se realiza un mecanismo que es más rápido que la velocidad de acceso a la memoria, que es el significado de la caché de la CPU: se introduce para compensar la diferencia entre la velocidad de acceso a la memoria y la velocidad de ejecución de la CPU.
Para las matrices, el espacio de almacenamiento es continuo, por lo que cuando se carga un determinado subíndice, también se pueden cargar varios elementos subsiguientes del subíndice en la memoria caché de la CPU para que la velocidad de ejecución sea más rápida que el almacenamiento de listas vinculadas con espacio de almacenamiento discontinuo.

La desventaja de la matriz es que tiene un tamaño fijo y, una vez declarada, ocupará todo el espacio de memoria contiguo. Si la matriz declarada es demasiado pequeña, puede ser insuficiente. En este momento, solo puede solicitar un espacio de memoria más grande y copiar la matriz original en él, lo que requiere mucho tiempo. La lista vinculada en sí no tiene límite de tamaño y, naturalmente, admite la expansión dinámica.

3. Realice el algoritmo de eliminación no utilizado (LRU)

Mantenemos una lista ordenada enlazada individualmente, y se accede antes a los nodos más cercanos al final de la lista enlazada. Cuando se accede a nuevos datos, recorremos la lista enlazada secuencialmente desde el encabezado de la lista enlazada.
1. Si estos datos se han almacenado en caché en la lista vinculada anteriormente, recorremos para obtener el nodo correspondiente a estos datos, lo borramos de su posición original y luego lo insertamos en el encabezado de la lista vinculada.
2. Si los datos no están en la lista vinculada de caché, se pueden dividir en dos situaciones:
(1) Si la caché no está llena en este momento, inserte este nodo directamente en el encabezado de la lista vinculada
(2) Si el la caché está llena en este momento. El nodo final de la lista vinculada se elimina y el nuevo nodo de datos se inserta en el encabezado de la lista vinculada.

4. Utilice centinelas para simplificar la implementación

Recordando la operación de inserción de la lista enlazada individualmente, si insertamos un nuevo nodo después del nodo p, solo dos líneas de código

new_node->next = p->next;
p->next = new_node;

Sin embargo, cuando insertamos el primer nodo en la lista vacía, la lógica en este momento no es fácil de usar, necesitamos agregar el siguiente código

if (head == null) {
  head = new_node;
}

Veamos nuevamente la operación de eliminación.

p->next = p->next->next;

Si el eliminado es el último

if (head->next == null) {
   head = null;
}

ok, entonces un código tan pesado, podemos resolverlo
con un solo centinela. Este tipo de lista enlazada con nodos centinela se llama lista enlazada principal.
Inserte la descripción de la imagen aquí
Esta técnica se utiliza en ordenación por inserción, ordenación por fusión y programación dinámica.

Caso de uso de Sentinel:
código original:

// 在数组a中,查找key,返回key所在的位置
// 其中,n表示数组a的长度
int find(char* a, int n, char key) {
  // 边界条件处理,如果a为空,或者n<=0,说明数组中没有数据,就不用while循环比较了
  if(a == null || n <= 0) {
    return -1;
  }
  
  int i = 0;
  // 这里有两个比较操作:i<n和a[i]==key.
  while (i < n) {
    if (a[i] == key) {
      return i;
    }
    ++i;
  }
  
  return -1;
}

Código centinela:

// 在数组a中,查找key,返回key所在的位置
// 其中,n表示数组a的长度
// 我举2个例子,你可以拿例子走一下代码
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 7
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 6
int find(char* a, int n, char key) {
  if(a == null || n <= 0) {
    return -1;
  }
  
  // 这里因为要将a[n-1]的值替换成key,所以要特殊处理这个值
  if (a[n-1] == key) {
    return n-1;
  }
  
  // 把a[n-1]的值临时保存在变量tmp中,以便之后恢复。tmp=6。
  // 之所以这样做的目的是:希望find()代码不要改变a数组中的内容
  char tmp = a[n-1];
  // 把key的值放到a[n-1]中,此时a = {4, 2, 3, 5, 9, 7}
  a[n-1] = key;
  
  int i = 0;
  // while 循环比起代码一,少了i<n这个比较操作
  while (a[i] != key) {
    ++i;
  }
  
  // 恢复a[n-1]原来的值,此时a= {4, 2, 3, 5, 9, 6}
  a[n-1] = tmp;
  
  if (i == n-1) {
    // 如果i == n-1说明,在0...n-2之间都没有key,所以返回-1
    return -1;
  } else {
    // 否则,返回i,就是等于key值的元素的下标
    return i;
  }
}

A través del centinela, guardamos con éxito una declaración de comparación en <n. Cuando el tiempo de ejecución acumulativo es decenas de miles, cientos de miles de veces, el tiempo acumulativo es obvio.

5. Preste atención al manejo de las condiciones de contorno

Hay varias condiciones de contorno que se utilizan a menudo para comprobar si el código de la lista enlazada es correcto:

  • Si la lista vinculada está vacía, ¿funcionará correctamente el código?
  • Si la lista vinculada contiene solo un nodo, ¿el código funcionará correctamente?
  • Si la lista vinculada contiene solo dos nodos, ¿funcionará correctamente el código?
  • ¿La lógica del código funciona normalmente al procesar el nodo principal y el nodo final?

Ejercicios de lista enlazada:
Números correspondientes de LeetCode: 206, 141, 21, 19, 876

Curso de referencia: "La belleza de la estructura de datos y el algoritmo" por el Sr. Wang Zheng , un tiempo de geek

Supongo que te gusta

Origin blog.csdn.net/Yungang_Young/article/details/112604877
Recomendado
Clasificación