Structure de données - double liste chaînée (langage C)

Table des matières

modifier

 Initialisation de la liste double chaînée :

 Impression de liste doublement chaînée :

Insertion de queue de liste à double lien :

Le bouchon de tête de la double liste chaînée : 

Suppression de la queue de la liste à double lien :

 

Supprimez l'en-tête de la liste doublement chaînée :

Insertion avant la position pos de la liste double chaînée :

Suppression de la position pos de la double liste chaînée :

À propos de la différence entre liste séquentielle et liste chaînée :


 

  1. L'article précédent vous expliquait la liste chaînée circulaire à sens unique sans tête Ses caractéristiques : structure simple, généralement pas utilisée pour stocker des données seules. En pratique, il est davantage utilisé comme sous-structure d'autres structures de données, telles que des compartiments de hachage, des listes de graphes adjacents, etc. Cependant, les listes chaînées simples sont encore très courantes dans les tests écrits.
  2. Aujourd'hui, je vais vous expliquer la principale liste chaînée bidirectionnelle, ses caractéristiques : structure complexe, généralement utilisée pour stocker des données séparément. La structure de données de liste chaînée utilisée dans la pratique est une liste chaînée circulaire bidirectionnelle avec le plomb. De plus, bien que cette structure soit compliquée, vous constaterez que la structure apportera de nombreux avantages après avoir utilisé le code pour l'implémenter.

cc840b67ee0c4b93a06677d959337c5e.png

Comme le montre l'image, il s'agit de la structure de la liste à double lien que je vais vous expliquer aujourd'hui. Suivons mon fil de pensée et espérons vous donner une nouvelle compréhension des listes liées.

 


 Initialisation de la liste double chaînée :

  • Aujourd'hui, je vais vous apporter une autre façon de changer la structure de la liste chaînée. Si vous voulez savoir comment changer la structure de la liste chaînée avec des pointeurs doubles, vous pouvez vous référer à mon blog précédent sur la liste chaînée unique.
  • Idée : j'ai créé un nœud, puis assigné le nœud à phead, puis laissé sa position précédente et sa position suivante pointer vers lui-même, et finalement je suis revenu à phead, qui est le nœud principal de la position sentinelle que nous voulons.
ListNode* ListCreate(ListDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->val = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}
ListNode* ListInit()
{
	ListNode* phead = ListCreate(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

 


 Impression de liste doublement chaînée :

  • Parce que je veux que le terminal ressemble à ce qui suit, j'utilise la fonction d'impression.
  • Tout d'abord, c'est encore une vieille routine, je vais d'abord l'affirmer pour éviter les problèmes avec les paramètres passés.
  • Parce que le phead ici est un bit sentinelle, qui stocke des données non valides, je définis donc un nœud cur, utilise une boucle pour imprimer toutes les valeurs de la liste liée et marque leurs directions.

34fadb9e31fc4dfe935653401f14cf48.png

void ListPrint(ListNode* phead)
{
	assert(phead);

	printf("phead<->");
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<->", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

 


Insertion de queue de liste à double lien :

  • Lors de l'insertion à la fin d'une liste à double liaison, ses avantages sont reflétés. S'il s'agit d'une liste à liaison simple, si vous souhaitez insérer à la fin, vous ne pouvez parcourir que pour trouver le nœud de queue. Toutefois, s'il s'agit d'une une liste doublement chaînée, le nœud précédent de phead est le nœud de queue. , il n'a pas besoin de trouver la queue, ni de traverser. C'est aussi l'un des avantages de la double liste chaînée.
  • Idée d'insertion de queue : affirmez d'abord, puis utilisez tail pour enregistrer le nœud de queue, créez un nouveau nœud, puis modifiez la relation de lien entre le nœud de queue et le nœud de tête, de sorte que newnode soit le nouveau nœud de queue.

462c58bf083848549b3bd955e5a0ac03.png

 

void ListPushBack(ListNode* phead,ListDateType x)
{
	assert(phead);
	ListNode* tail = phead->prev;
	ListNode* newnode = ListCreate(x);

	tail->next = newnode;
	newnode->prev = tail;

	newnode->next = phead;
	phead->prev = newnode;


}

 


Le bouchon de tête de la double liste chaînée : 

  • Idée : Si la tête est insérée, c'est pour enregistrer d'abord la position du nœud principal, puis créer un nouveau nœud, puis modifier leur relation de lien. Parce que j'ai d'abord enregistré leurs positions, peu importe qui se connecte en premier. Si vous ne les enregistrez pas, vous devez être logique et ne pas trouver la position où le nœud principal ne peut pas être trouvé.
void ListPushFront(ListNode* phead, ListDateType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);
	ListNode* first = phead->next;

	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;
	
}

 


Suppression de la queue de la liste à double lien :

  • Idée : si la queue est supprimée, il est nécessaire d'enregistrer le nœud précédent du nœud de queue, puis de modifier la relation de lien entre phead et le nœud précédent du nœud de queue.
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	ListNode* tail = phead->prev;
	ListNode* tailprev = phead->prev->prev;
	tailprev->next = phead;
	phead->prev = tailprev;

	free(tail);

}

 


Supprimez l'en-tête de la liste doublement chaînée :

  • Idée : L'idée de la suppression de la tête est en fait similaire à celle de la suppression de la queue, sauf que le deuxième nœud après phead est enregistré ici. Ensuite, il s'agit de changer la relation de lien.
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead != phead->next);

	ListNode* first = phead->next;
	ListNode* second = first->next;

	phead->next = second;
	second->prev = phead;

	free(first);

}

Insertion avant la position pos de la liste double chaînée :

  • Idée: S'il s'agit d'une insertion, est-ce similaire à l'insertion de la queue et à l'insertion de la tête?Ici, je sauvegarde le nœud avant pos, puis crée un nouveau nœud, laisse le nœud avant pos pointer vers le nouveau nœud, et crée le nouveau nœud et pos Ensuite, établissez une relation de connexion.
void ListInsert(ListNode* pos, ListDateType x)
{
	assert(pos);

	ListNode* posprev = pos->prev;
	ListNode* newnode = ListCreate(x);

	posprev->next = newnode;
	newnode->prev = posprev;

	newnode->next = pos;
	pos->prev = newnode;
}

 


Suppression de la position pos de la double liste chaînée :

  • Idée : si la position pos doit être supprimée, enregistrez le nœud précédent de pos et le nœud suivant de pos, puis libérez la position pos, puis modifiez leur relation de lien.
  1. void ListErase(ListNode* pos)
    {
    	assert(pos);
    	ListNode* posprev = pos->prev;
    	ListNode* posnext = pos->next;
    
    	posprev->next = posnext;
    	posnext->prev = posprev;
    	
    	free(pos);
    }

Tout le monde a vu la suppression de la position pos et l'insertion avant la pos. Pensez-vous que c'est similaire à la suppression précédente du bouchon arrière et de la suppression du bouchon principal ? En fait, les deux dernières fonctions peuvent être réutilisées.

  •  Insertion de queue : En fait, il suffit de passer phead dans la fonction ListInsert, pour que l'insertion de queue soit réalisée.
	ListInsert(phead, x);
  • Prise de tête : la prise de tête sert à changer la position après phead, il suffit donc de passer phead->next directement.
	ListInsert(phead->next, x);
  • Suppression de la tête et suppression de la queue : Parce que la fonction de suppression que j'ai écrite consiste à supprimer la position pos, il suffit donc de passer la position à supprimer.
	ListErase(phead->prev);
	ListErase(phead->next);

 

 


À propos de la différence entre liste séquentielle et liste chaînée :

  •  En termes d'espace de stockage : la table de séquence est physiquement continue, tandis que la liste chaînée est logiquement continue, mais pas nécessairement physiquement continue.
  • Sur accès aléatoire : la liste séquentielle supporte l'accès aléatoire, mais la liste chaînée ne le supporte pas. Pour accéder aux nœuds de la liste chaînée, il faut la traverser.
  • Insertion et suppression à n'importe quelle position : la liste liée présente ici un grand avantage. La liste liée peut être insérée et supprimée à n'importe quelle position uniquement en changeant de direction. Mais pour les tableaux séquentiels, il peut être nécessaire de déplacer des éléments, ce qui est trop inefficace.
  • Insertion : étant donné que la table de séquence est continue, il peut y avoir une expansion malloc au-dessus de l'insertion, mais malloc est consommé. S'il est développé deux fois à la fois, mais qu'il n'est pas beaucoup utilisé, cela entraînera une perte d'espace. Si l'espace pour une expansion est +1, et la situation peut être confrontée à plusieurs expansions, et la consommation de malloc n'est pas faible, donc ce n'est pas conseillé. La liste liée n'a pas le concept de conteneur, ce qui présente des avantages à cet égard.
  • Utilisation du cache : le taux de réussite du cache de la table séquentielle est élevé, tandis que celui de la liste chaînée est faible.

#define  _CRT_SECURE_NO_WARNINGS 1
#include "DoubleList.h"

ListNode* ListCreate(ListDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->val = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

ListNode* ListInit()
{
	ListNode* phead = ListCreate(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void ListPrint(ListNode* phead)
{
	assert(phead);

	printf("phead<->");
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<->", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

void ListPushBack(ListNode* phead,ListDateType x)
{
	assert(phead);
	//ListNode* tail = phead->prev;
	//ListNode* newnode = ListCreate(x);

	//tail->next = newnode;
	//newnode->prev = tail;

	//newnode->next = phead;
	//phead->prev = newnode;

	ListInsert(phead, x);

}

void ListPushFront(ListNode* phead, ListDateType x)
{
	assert(phead);
	//ListNode* newnode = ListCreate(x);
	//ListNode* first = phead->next;

	//phead->next = newnode;
	//newnode->prev = phead;

	//newnode->next = first;
	//first->prev = newnode;
	
	ListInsert(phead->next, x);
}

void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	//ListNode* tail = phead->prev;
	//ListNode* tailprev = phead->prev->prev;
	//tailprev->next = phead;
	//phead->prev = tailprev;

	//free(tail);
	
	ListErase(phead->prev);
}

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead != phead->next);

	//ListNode* first = phead->next;
	//ListNode* second = first->next;

	//phead->next = second;
	//second->prev = phead;

	//free(first);

	ListErase(phead->next);
}

int ListSize(ListNode* phead)
{
	assert(phead);

	int size = 0;
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

void ListInsert(ListNode* pos, ListDateType x)
{
	assert(pos);

	ListNode* posprev = pos->prev;
	ListNode* newnode = ListCreate(x);

	posprev->next = newnode;
	newnode->prev = posprev;

	newnode->next = pos;
	pos->prev = newnode;
}

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;
	
	free(pos);
}

 


 

Qu'est-ce que l'utilisation du cache :

En ce qui concerne "Cache Line", le cache consiste à charger des données dans un emplacement éloigné de lui-même.Pour le CPU, le CPU les stocke pièce par pièce. Et cela s'appelle "Chche Line".

Les programmes que nous écrivons forment en fait des instructions différentes et laissent le processeur les exécuter. Cependant, le processeur s'exécute rapidement et la mémoire ne peut pas suivre, de sorte que le processeur met généralement les données dans le cache. Pour les petits octets Par exemple, il est lu directement à partir de le registre, et le gros octet utilisera le cache de troisième niveau.

En bref, les données sont d'abord lues, puis calculées et enfin placées dans la mémoire principale, et s'il n'y a pas de commande, continuez.

En ce qui concerne la table de séquence, sa structure physique est continue. Il se peut qu'elle ne frappe pas au début, mais une fois que le cache est atteint, il peut être atteint en continu, c'est donc également la raison de l'utilisation élevée du cache de la table de séquence, et la La liste liée est également due au fait que sa structure physique entraîne une faible utilisation du cache.

 

4707fe47c7da4fdfa2c91bac6296089f.png

Voici le code source de la double liste chaînée :

#define  _CRT_SECURE_NO_WARNINGS 1
#include "DoubleList.h"

ListNode* ListCreate(ListDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->val = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

ListNode* ListInit()
{
	ListNode* phead = ListCreate(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void ListPrint(ListNode* phead)
{
	assert(phead);

	printf("phead<->");
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<->", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

void ListPushBack(ListNode* phead,ListDateType x)
{
	assert(phead);
	//ListNode* tail = phead->prev;
	//ListNode* newnode = ListCreate(x);

	//tail->next = newnode;
	//newnode->prev = tail;

	//newnode->next = phead;
	//phead->prev = newnode;

	ListInsert(phead, x);

}

void ListPushFront(ListNode* phead, ListDateType x)
{
	assert(phead);
	//ListNode* newnode = ListCreate(x);
	//ListNode* first = phead->next;

	//phead->next = newnode;
	//newnode->prev = phead;

	//newnode->next = first;
	//first->prev = newnode;
	
	ListInsert(phead->next, x);
}

void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	//ListNode* tail = phead->prev;
	//ListNode* tailprev = phead->prev->prev;
	//tailprev->next = phead;
	//phead->prev = tailprev;

	//free(tail);
	
	ListErase(phead->prev);
}

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead != phead->next);

	//ListNode* first = phead->next;
	//ListNode* second = first->next;

	//phead->next = second;
	//second->prev = phead;

	//free(first);

	ListErase(phead->next);
}

int ListSize(ListNode* phead)
{
	assert(phead);

	int size = 0;
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

void ListInsert(ListNode* pos, ListDateType x)
{
	assert(pos);

	ListNode* posprev = pos->prev;
	ListNode* newnode = ListCreate(x);

	posprev->next = newnode;
	newnode->prev = posprev;

	newnode->next = pos;
	pos->prev = newnode;
}

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;
	
	free(pos);
}
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int ListDateType;

typedef struct ListNode
{
	ListDateType val;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

ListNode* ListCreate(ListDateType x);
void ListPrint(ListNode* phead);
ListNode* ListInit();
void ListPushBack(ListNode* phead,ListDateType x);
void ListPushFront(ListNode* phead, ListDateType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
int ListSize(ListNode* phead);
void ListInsert(ListNode* pos, ListDateType x);
void ListErase(ListNode* pos);

 

 

 

 

 

 

 

 

 

 

 

Je suppose que tu aimes

Origine blog.csdn.net/m0_72165281/article/details/132074165
conseillé
Classement