C 语言链表详解

一、链表的基本概念

1.1 链表的定义

链表是一种线性数据结构,与数组不同,它在内存中不是连续存储的。链表由一系列节点组成,每个节点包含数据和指向下一个节点的指针。这种链式存储方式使得链表在插入和删除操作上具有高效性,并且可以动态地分配内存,无需像数组那样预先确定大小。

1.2 链表的分类

链表主要分为单向链表、双向链表和循环链表:

  • 单向链表:每个节点只包含一个指向下一个节点的指针,只能从表头向表尾单向遍历。
  • 双向链表:每个节点除了存储数据外,还包含两个指针,一个指向前一个节点,一个指向后一个节点,支持双向遍历。
  • 循环链表:又可分为单向循环链表和双向循环链表。在单向循环链表中,表尾节点的指针指向表头节点;双向循环链表中,表头节点的前驱指针指向表尾节点,表尾节点的后继指针指向表头节点 ,形成一个闭环。

二、单向链表的实现

2.1 节点的定义

在 C 语言中,通过结构体来定义链表节点。每个节点包含两部分:数据域和指针域。数据域用于存储实际的数据,指针域用于指向下一个节点。示例代码如下:


// 定义链表节点结构体

typedef struct Node {

int data; // 数据域,这里以整数为例,可根据需求修改

struct Node* next; // 指针域,指向下一个节点

} Node;

2.2 创建新节点

创建新节点的过程实际上是动态分配内存并初始化节点数据的过程。使用malloc函数在堆内存中分配空间,然后初始化节点的数据域和指针域。示例代码如下:


// 创建新节点

Node* createNode(int data) {

Node* newNode = (Node*)malloc(sizeof(Node));

if (newNode == NULL) {

printf("内存分配失败!\n");

return NULL;

}

newNode->data = data;

newNode->next = NULL;

return newNode;

}

2.3 链表的初始化

链表的初始化通常是创建一个空链表,即头指针指向NULL。示例代码如下:


// 初始化链表,返回头指针

Node* initList() {

return NULL;

}

2.4 插入节点

插入节点是链表的重要操作之一,根据插入位置的不同,可分为头插法、尾插法和指定位置插入。

  • 头插法:将新节点插入到链表头部,使新节点成为新的头节点。示例代码如下:

// 头插法插入节点

Node* insertAtHead(Node* head, int data) {

Node* newNode = createNode(data);

newNode->next = head;

return newNode;

}
  • 尾插法:将新节点插入到链表尾部。需要遍历链表找到尾节点,然后将尾节点的指针指向新节点。示例代码如下:

// 尾插法插入节点

Node* insertAtTail(Node* head, int data) {

Node* newNode = createNode(data);

if (head == NULL) {

return newNode;

}

Node* current = head;

while (current->next != NULL) {

current = current->next;

}

current->next = newNode;

return head;

}
  • 指定位置插入:在链表的指定位置插入新节点。首先需要找到指定位置的前一个节点,然后修改指针关系。示例代码如下:

// 在指定位置插入节点

Node* insertAtPosition(Node* head, int data, int position) {

if (position < 1) {

printf("位置无效!\n");

return head;

}

if (position == 1) {

return insertAtHead(head, data);

}

Node* newNode = createNode(data);

Node* current = head;

int count = 1;

while (current != NULL && count < position - 1) {

current = current->next;

count++;

}

if (current == NULL) {

printf("位置超出链表长度!\n");

free(newNode);

return head;

}

newNode->next = current->next;

current->next = newNode;

return head;

}

2.5 删除节点

删除节点同样根据删除位置分为删除头节点、删除尾节点和删除指定位置节点。

  • 删除头节点:将头指针指向下一个节点,然后释放原来头节点的内存。示例代码如下:

// 删除头节点

Node* deleteHead(Node* head) {

if (head == NULL) {

printf("链表为空!\n");

return NULL;

}

Node* temp = head;

head = head->next;

free(temp);

return head;

}
  • 删除尾节点:需要遍历链表找到尾节点的前一个节点,将其指针置为NULL,然后释放尾节点的内存。示例代码如下:

// 删除尾节点

Node* deleteTail(Node* head) {

if (head == NULL) {

printf("链表为空!\n");

return NULL;

}

if (head->next == NULL) {

free(head);

return NULL;

}

Node* current = head;

while (current->next->next != NULL) {

current = current->next;

}

free(current->next);

current->next = NULL;

return head;

}
  • 删除指定位置节点:找到指定位置的前一个节点,修改指针关系,然后释放要删除节点的内存。示例代码如下:

// 删除指定位置节点

Node* deleteAtPosition(Node* head, int position) {

if (position < 1) {

printf("位置无效!\n");

return head;

}

if (position == 1) {

return deleteHead(head);

}

Node* current = head;

int count = 1;

while (current != NULL && count < position - 1) {

current = current->next;

count++;

}

if (current == NULL || current->next == NULL) {

printf("位置超出链表长度!\n");

return head;

}

Node* temp = current->next;

current->next = current->next->next;

free(temp);

return head;

}

2.6 遍历链表

遍历链表是从表头开始,依次访问每个节点的数据。示例代码如下:


// 遍历链表

void traverseList(Node* head) {

Node* current = head;

while (current != NULL) {

printf("%d ", current->data);

current = current->next;

}

printf("\n");

}

三、双向链表的实现

3.1 双向链表节点的定义

双向链表的节点比单向链表多一个指向前一个节点的指针。示例代码如下:


// 定义双向链表节点结构体

typedef struct DoublyNode {

int data;

struct DoublyNode* prev;

struct DoublyNode* next;

} DoublyNode;

3.2 创建双向链表节点

与单向链表类似,使用malloc函数分配内存并初始化节点。示例代码如下:


// 创建双向链表新节点

DoublyNode* createDoublyNode(int data) {

DoublyNode* newNode = (DoublyNode*)malloc(sizeof(DoublyNode));

if (newNode == NULL) {

printf("内存分配失败!\n");

return NULL;

}

newNode->data = data;

newNode->prev = NULL;

newNode->next = NULL;

return newNode;

}

3.3 双向链表的插入操作

双向链表的插入操作同样有头插法、尾插法和指定位置插入,不过需要同时处理前驱和后继指针。

  • 头插法:示例代码如下:

// 双向链表头插法插入节点

DoublyNode* insertDoublyAtHead(DoublyNode* head, int data) {

DoublyNode* newNode = createDoublyNode(data);

if (head == NULL) {

return newNode;

}

newNode->next = head;

head->prev = newNode;

return newNode;

}
  • 尾插法:示例代码如下:

// 双向链表尾插法插入节点

DoublyNode* insertDoublyAtTail(DoublyNode* head, int data) {

DoublyNode* newNode = createDoublyNode(data);

if (head == NULL) {

return newNode;

}

DoublyNode* current = head;

while (current->next != NULL) {

current = current->next;

}

current->next = newNode;

newNode->prev = current;

return head;

}

3.4 双向链表的删除操作

删除操作也需要同时更新前驱和后继节点的指针。

  • 删除头节点:示例代码如下:

// 双向链表删除头节点

DoublyNode* deleteDoublyHead(DoublyNode* head) {

if (head == NULL) {

printf("链表为空!\n");

return NULL;

}

if (head->next == NULL) {

free(head);

return NULL;

}

DoublyNode* temp = head;

head = head->next;

head->prev = NULL;

free(temp);

return head;

}
  • 删除尾节点:示例代码如下:

// 双向链表删除尾节点

DoublyNode* deleteDoublyTail(DoublyNode* head) {

if (head == NULL) {

printf("链表为空!\n");

return NULL;

}

if (head->next == NULL) {

free(head);

return NULL;

}

DoublyNode* current = head;

while (current->next->next != NULL) {

current = current->next;

}

free(current->next);

current->next = NULL;

return head;

}

3.5 双向链表的遍历

双向链表支持正向和反向遍历。正向遍历与单向链表类似,反向遍历则从尾节点开始,通过前驱指针向前访问。示例代码如下:


// 正向遍历双向链表

void traverseDoublyListForward(DoublyNode* head) {

DoublyNode* current = head;

while (current != NULL) {

printf("%d ", current->data);

current = current->next;

}

printf("\n");

}

// 反向遍历双向链表

void traverseDoublyListBackward(DoublyNode* head) {

if (head == NULL) {

return;

}

DoublyNode* current = head;

while (current->next != NULL) {

current = current->next;

}

while (current != NULL) {

printf("%d ", current->data);

current = current->prev;

}

printf("\n");

}

四、循环链表的实现

4.1 单向循环链表

  • 节点定义:与单向链表相同,但表尾节点的指针指向表头节点。
  • 初始化:头指针指向NULL,创建第一个节点后,使其指针指向自身。示例代码如下:

// 初始化单向循环链表

Node* initCircularList() {

return NULL;

}

Node* createFirstNode(int data) {

Node* newNode = createNode(data);

newNode->next = newNode;

return newNode;

}
  • 插入操作:头插法和尾插法需要在插入后调整指针,使其形成闭环。示例代码如下:
// 单向循环链表头插法

Node* insertCircularAtHead(Node* head, int data) {

Node* newNode = createNode(data);

if (head == NULL) {

newNode->next = newNode;

return newNode;

}

Node* current = head;

while (current->next != head) {

current = current->next;

}

newNode->next = head;

current->next = newNode;

return newNode;

}

// 单向循环链表尾插法

Node* insertCircularAtTail(Node* head, int data) {

Node* newNode = createNode(data);

if (head == NULL) {

newNode->next = newNode;

return newNode;

}

Node* current = head;

while (current->next != head) {

current = current->next;

}

current->next = newNode;

newNode->next = head;

return head;

}
  • 删除操作:删除节点后需要重新调整指针形成闭环。示例代码如下:

// 单向循环链表删除头节点

Node* deleteCircularHead(Node* head) {

if (head == NULL) {

printf("链表为空!\n");

return NULL;

}

if (head->next == head) {

free(head);

return NULL;

}

Node* current = head;

while (current->next != head) {

current = current->next;

}

Node* temp = head;

head = head->next;

current->next = head;

free(temp);

return head;

}

4.2 双向循环链表

双向循环链表结合了双向链表和循环链表的特点,节点有前驱和后继指针,且表头和表尾节点形成闭环。其实现与双向链表和单向循环链表类似,只是在初始化、插入和删除操作时需要同时处理前驱和后继指针,确保形成闭环 。

五、链表的应用场景

5.1 动态数据存储

链表可以根据实际需求动态地分配和释放内存,适合存储数据量不确定的情况。例如,在处理用户输入的字符串序列时,使用链表可以方便地添加或删除字符串节点,而不需要预先分配固定大小的数组。

5.2 实现栈和队列

栈和队列是常用的数据结构,链表可以很好地实现它们。通过链表的头插法和删除头节点操作可以实现栈的 “后进先出” 特性;通过尾插法和删除头节点操作可以实现队列的 “先进先出” 特性 。

5.3 操作系统中的进程管理

在操作系统中,进程的管理通常使用链表来实现。每个进程可以看作是链表中的一个节点,通过链表可以方便地对进程进行创建、删除、调度等操作。

5.4 哈希表的冲突解决

哈希表在处理冲突时,常用的方法之一是链地址法。即将哈希值相同的元素存储在同一个链表中,通过链表来解决哈希冲突 。

六、链表的优缺点

6.1 优点

  • 动态内存分配:链表可以根据需要动态地分配和释放内存,不需要预先确定数据的大小。
  • 插入和删除高效:在链表中插入和删除节点时,只需修改指针,不需要像数组那样移动大量元素,时间复杂度为 O (1)(在已知插入或删除位置的情况下)。
  • 灵活的数据结构:链表可以方便地实现多种数据结构,如栈、队列、树等。

6.2 缺点

  • 随机访问效率低:链表不能像数组那样通过下标直接访问元素,需要从表头开始遍历,时间复杂度为 O (n)。
  • 内存开销大:每个节点除了存储数据外,还需要存储指针,相比数组占用更多的内存空间。
  • 指针操作复杂:链表的操作涉及大量的指针操作,容易出现指针错误,如悬空指针、野指针等,调试难度较大。

以上详细介绍了 C 语言链表的各类知识。如果你还想深入了解链表的某部分内容,比如性能优化或具体应用案例,欢迎和我说说。

你可能感兴趣的:(C语言开发入门,c语言,链表,开发语言)