本文为学习数据结构过程中的笔记,内容较为浅显,有问题的评论区见~~
一、基本概念
链表是一种线性数据结构,与数组不同,链表在内存中并不是连续存储的,因此不能通过索引直接访问某个元素。链表由一系列结点组成,每个结点包含两个部分:
数据域(Data Field):存储实际数据。
指针域(Pointer Field):存储指向下一个结点的指针。
链表的首个结点被称为“头结点”,最后一个结点被称为“尾结点”。
二、链表的分类
链表可简单分为单向链表、双向链表和循环链表。
1.概念
单向链表(Singly Linked List)
单向链表由一系列结点组成,每个结点包含一个数据域和一个指针域,该指针指向下一个结点。尾结点指向空。
双向链表(Doubly Linked List)
双向链表的每个结点包含三个部分:数据域、指向下一个结点的指针和指向前一个结点的指针。
循环链表(Circular Linked List)
循环链表也分为单向循环链表和双向循环链表,其特点是最后一个结点的指针指向链表的头结点,形成一个环,任意结点都可以视为头结点。
2.三种链表的对比
特性 | 单向链表 | 双向链表 | 循环链表 |
内存占用 | 较低,只需额外存储一个指针。 | 较高,每个结点需要存储两个指针。 | 与相应的单向或双向链表相同。 |
插入/删除效率 | 高效,在链表头部插入/删除最为简单。 | 高效,在任意位置插入/删除都比较简单。 | 高效,尤其是尾部操作,不需特殊处理尾结点。 |
访问效率 | 访问较慢,需要顺序遍历。 | 比单向链表略快,可双向遍历。 | 相似于单向或双向链表,需要顺序遍历。 |
遍历方向 | 只能从头节点向后遍历。 | 可以从任意结点向前或向后遍历。 | 支持循环遍历,从任意节点开始均可遍历整个链表。 |
实现难度 | 较低,指针操作相对简单。 | 较高,需要处理更多指针。 | 较高,需处理循环指针,防止无限循环。 |
典型应用场景 | 实现队列、栈、临时数据存储。 | 浏览器前进/后退、应用程序的撤销/重做功能、LRU算法。 | 实时循环任务处理、循环缓冲区。 |
三、三种链表的代码实现
1.单向链表
(1)结构定义
数据类型为int型,定义一个指向结构体的指针next用于记录下一个结点的地址。此处为最基础的写法,可以在此基础上扩充其他功能。
// 定义节点结构
typedef struct Node {
int data;
struct Node* next;
} Node;
// 创建新节点
Node* createNode(int data);
// 在链表中插入新节点
void insert(Node** head, int data, int position);
// 删除链表中的节点
void deleteNode(Node** head, int position);
// 查找链表中的节点
Node* search(Node* head, int data);
// 打印链表
void printList(Node* head);
// 释放链表内存
void freeList(Node* head);
(2)创建结点
输入一个int类型数据,先进行动态内存分配来存储这个结点,进行赋值之后将这个结点的next指针置空,然后返回这个结点的地址。
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
(3)插入新结点
使用一个指向指针的指针Node**head来修改头指针的参数(传址调用),先创建一个新结点newNode,判断position的值,如果插入位置为链表的头部,则直接将新结点的next指向原先的头结点,再把新结点作为头结点。
否则从头开始遍历链表至position-1,即插入位置的上一个结点。如果插入位置有效,则修改各结点指针的值,将新结点插入。如下图:
void insert(Node** head, int data, int position) {
Node* newNode = createNode(data);
if (position == 0) {
// 插入头部
newNode->next = *head;
*head = newNode;
}
else {
Node* temp = *head;
for (int i = 0; temp != NULL && i < position - 1; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("插入位置无效\n");
free(newNode);
}
else {
// 插入中间或尾部
newNode->next = temp->next;
temp->next = newNode;
}
}
}
(4)删除链表中的结点
先判断链表是否为空,接着判断删除位置,如果删除头结点,则直接将头结点的地址改为下一个结点的地址,并释放原来的头结点。
如果不是头结点则遍历链表,如果删除位置合法,则将删除位置的上一个结点的next指向删除位置的下一个结点,并释放删除位置的内存,即可删除目标位置的结点。如下图:
void deleteNode(Node** head, int position) {
if (*head == NULL) {
printf("链表为空\n");
return;
}
Node* temp = *head;
if (position == 0) {
// 删除头部节点
*head = temp->next;
free(temp);
}
else {
Node* prev = NULL;
for (int i = 0; temp != NULL && i < position; i++) {
prev = temp;
temp = temp->next;
}
if (temp == NULL) {
printf("删除位置无效\n");
}
else {
// 删除中间或尾部节点
prev->next = temp->next;
free(temp);
}
}
}
(5)查找链表中的结点
此处采用按照data值的方式查找,遍历链表,找到相关值则返回该结点的地址,也可以采用其他方式,如返回链表中的某一个结点。
Node* search(Node* head, int data) {
Node* temp = head;
while (temp != NULL) {
if (temp->data == data) {
return temp;
}
temp = temp->next;
}
return NULL;
}
(6)打印链表
从头遍历并输出。
void printList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
(7)释放内存
从头遍历并释放每个结点。
void freeList(Node* head) {
Node* temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
}
(8)主函数及部分测试用例
int main() {
Node* head = NULL;
insert(&head, 1, 0); // 插入到头部
insert(&head, 2, 1); // 插入到尾部
insert(&head, 3, 1); // 插入到中间
printList(head); // 输出:1 -> 3 -> 2 -> NULL
deleteNode(&head, 1); // 删除中间节点
printList(head); // 输出:1 -> 2 -> NULL
Node* found = search(head, 2);
if (found) {
printf("找到节点:%d\n", found->data); // 输出:找到节点:2
}
else {
printf("节点未找到\n");
}
freeList(head);
return 0;
}
2.双向链表
(1)结构定义
双向链表在结构定义时比单向链表多了一个指向上一结点的指针,在遍历查找时比单向链表更为迅速。在插入与删除时需要修改更多的指针,此处主要介绍插入与删除时的操作。
// 定义节点结构
typedef struct DNode {
int data; // 节点数据
struct DNode* prev; // 指向前一个节点的指针
struct DNode* next; // 指向下一个节点的指针
} DNode;
(2)创建结点
DNode* createDNode(int data) {
DNode* newNode = (DNode*)malloc(sizeof(DNode));
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
(3)插入新结点
如果插入链表头部则将新结点的next指向目前的头结点,当目前的头结点不为空即链表中已经存在结点时,将目前的头结点的prev指向新结点,最后将插入的新结点设为新的头结点。
否则从头结点开始向后遍历链表(也可以提前记录尾结点从后向前遍历,有多种方案),遍历到插入位置的上一个结点,如果插入位置合法,则将新结点插入到目标位置。插入过程与单向链表基本一致,需要修改prev指针即可。
void insert(DNode** head, int data, int position) {
DNode* newNode = createDNode(data);
if (position == 0) {
// 插入头部
newNode->next = *head;
if (*head != NULL) {
(*head)->prev = newNode;
}
*head = newNode;
}
else {
DNode* temp = *head;
for (int i = 0; temp != NULL && i < position - 1; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("插入位置无效\n");
free(newNode);
}
else {
// 插入中间或尾部
newNode->next = temp->next;
if (temp->next != NULL) {
temp->next->prev = newNode;
}
temp->next = newNode;
newNode->prev = temp;
}
}
}
(4)删除结点
如果删除的是头结点,只需要将头结点的下一个结点设为新的头结点,赋值完成后若当前结点不为空,则将当前结点的prev指针指向空,释放原来的头结点内存。
否则与单向链表一致,只需要多修改一次prev指针即可。
void deleteNode(DNode** head, int position) {
if (*head == NULL) {
printf("链表为空\n");
return;
}
DNode* temp = *head;
if (position == 0) {
// 删除头部节点
*head = temp->next;
if (*head != NULL) {
(*head)->prev = NULL;
}
free(temp);
}
else {
for (int i = 0; temp != NULL && i < position; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("删除位置无效\n");
}
else {
// 删除中间或尾部节点
if (temp->prev != NULL) {
temp->prev->next = temp->next;
}
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
free(temp);
}
}
}
3.循环链表(单向)
循环链表的结构定义与普通链表相同,此处只介绍单向循环链表的插入与删除操作。
(1)插入新结点
插入原理与单向链表一致。如果插入链表头部,且当前头指针为空(插入第一个元素),则将该结点的next指向自己,即自成环。否则遍历链表找到尾结点,将尾结点的next指向新结点,新结点的next指向当前头结点,再设插入的新结点为新的头结点。
如果插入其他位置,与单向链表一致。
void insert(Node** head, int data, int position) {
Node* newNode = createNode(data);
if (position == 0) {
if (*head == NULL) {
newNode->next = newNode; // 自成环
*head = newNode;
}
else {
Node* temp = *head;
while (temp->next != *head) {
temp = temp->next;
}
temp->next = newNode;
newNode->next = *head;
*head = newNode;
}
}
else {
Node* temp = *head;
for (int i = 0; temp->next != *head && i < position - 1; i++) {
temp = temp->next;
}
if (temp->next == *head) {
printf("插入位置无效\n");
free(newNode);
}
else {
newNode->next = temp->next;
temp->next = newNode;
}
}
}
(2)删除结点
如果删除的是头结点,当头结点自成环时直接释放头结点内存即可。否则遍历到尾结点,将尾结点的next指向头结点的下一个结点,将头结点的下一个结点设为新的头结点。
删除其他位置的结点与单向链表一致。
void deleteNode(Node** head, int position) {
if (*head == NULL) {
printf("链表为空\n");
return;
}
Node* temp = *head;
if (position == 0) {
// 删除头部节点
if ((*head)->next == *head) {
free(*head);
*head = NULL;
}
else {
Node* last = *head;
while (last->next != *head) {
last = last->next;
}
last->next = (*head)->next;
free(*head);
*head = last->next;
}
}
else {
Node* prev = NULL;
for (int i = 0; temp->next != *head && i < position; i++) {
prev = temp;
temp = temp->next;
}
if (temp->next == *head) {
printf("删除位置无效\n");
}
else {
prev->next = temp->next;
free(temp);
}
}
}
全文完,赏个点赞收藏吧~~