【数据结构】链表
链表是一种常见的数据结构,用于存储一系列的元素。链表是由一组节点组成,每个节点包含数据和一个指向下一个节点的指针。C语言中,链表可以用结构体和指针来实现。下面分别介绍单链表、循环单链表和双向链表的实现。
1、单链表
单链表是一种最简单的链表,它的每个节点只包含一个指向下一个节点的指针。单链表的最后一个节点指向NULL,表示链表的结尾。单链表只能从头节点开始遍历,无法双向遍历。因此,单链表的插入和删除操作相对简单,但查找操作需要遍历整个链表。
单链表的定义通常使用一个结构体来表示节点:
struct Node {
int data;
struct Node *next;
};
其中,data表示节点存储的数据,next是指向下一个节点的指针。
单链表的基本操作包括:创建、遍历、插入、删除和查找。
1.1 创建单链表
创建单链表需要动态分配内存,一个一个地创建节点,并将每个节点连接起来。下面是一个创建单链表的示例代码:
struct Node *createLinkedList(int data[], int n) {
struct Node *head = NULL, *tail = NULL;
for (int i = 0; i < n; i++) {
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->data = data[i];
node->next = NULL;
if (head == NULL) {
head = node;
tail = node;
} else {
tail->next = node;
tail = node;
}
}
return head;
}
其中,data是一个整型数组,n表示数组的长度。该函数返回一个指向单链表头节点的指针。
1.2 遍历单链表
遍历单链表就是依次访问链表中的每个节点,并处理节点中的数据。可以使用一个循环来遍历链表,每次将指针移动到下一个节点。遍历单链表的代码如下:
void traverseLinkedList(struct Node *head) {
struct Node *p = head;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
1.3 插入节点
在链表中插入一个节点通常需要两个步骤:先创建一个新节点,然后将它插入到链表中正确的位置。对于单链表,需要将新节点插入到前一个节点和后一个节点之间,而对于双向链表,还需要更新新节点的前驱指针。下面是在单链表中插入节点的代码:
void insertNode(struct Node *head, int index, int data) {
struct Node *p = head;
for (int i = 0; i < index - 1 && p != NULL; i++) {
p = p->next;
}
if (p == NULL) {
printf("Error: Invalid index\n");
return;
}
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->data = data;
node->next = p->next;
p->next = node;
}
其中,head是链表的头节点,index表示要插入的位置(从1开始),data表示要插入的数据。该函数会在链表的第index个节点之前插入一个新节点。
1.4 删除节点
删除链表中的一个节点通常也需要两个步骤:先找到要删除的节点,然后将它从链表中移除。对于单链表,需要将前一个节点的next指针指向后一个节点,而对于双向链表,还需要更新后一个节点的前驱指针。下面是在单链表中删除节点的代码:
void deleteNode(struct Node *head, int index) {
struct Node *p = head;
for (int i = 0; i < index - 1 && p != NULL; i++) {
p = p->next;
}
if (p == NULL || p->next == NULL) {
printf("Error: Invalid index\n");
return;
}
struct Node *node = p->next;
p->next = node->next;
free(node);
}
其中,head是链表的头节点,index表示要删除的节点位置(从1开始)。该函数会删除链表的第index个节点。
1.5 查找节点
查找链表中的一个节点需要从头节点开始遍历链表,比较每个节点的数据,直到找到目标节点或遍历完整个链表。下面是在单链表中查找节点的代码:
int searchNode(struct Node *head, int data) {
struct Node *p = head;
int index = 1;
while (p != NULL && p->data != data) {
p = p->next;
index++;
}
if (p == NULL) {
return -1;
} else {
return index;
}
}
其中,head是链表的头节点,data是要查找的数据。该函数会返回目标节点在链表中的位置(从1开始),如果未找到则返回-1。
2、循环单链表
循环单链表是一种特殊的单链表,它的最后一个节点指向头节点,形成一个循环。循环单链表可以用于模拟环形队列等场景。循环单链表的定义和单链表类似,只需要将最后一个节点的next指针指向头节点即可。
循环单链表的操作和单链表大致相同,只需要在遍历链表时判断是否到达了头节点即可。下面是创建循环单链表的代码:
struct Node *createCircularLinkedList(int data[], int n) {
struct Node *head = NULL, *tail = NULL;
for (int i = 0; i < n; i++) {
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->data = data[i];
node->next = NULL;
if (head == NULL) {
head = node;
tail = node;
} else {
tail->next = node;
tail = node;
}
}
tail->next = head; // 将最后一个节点指向头节点
return head;
}
3、双向链表
双向链表是一种比单链表更复杂的链表,它每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。双向链表可以实现更多的功能,例如双向遍历和删除指定元素等。双向链表的定义通常使用一个结构体来表示节点:
struct Node {
int data;
struct Node *prev;
struct Node *next;
};
其中,data表示节点存储的数据,prev是指向前一个节点的指针,next是指向后一个节点的指针。
双向链表相比单链表,插入和删除操作相对复杂,但查找操作可以双向遍历,效率更高。
3.1 创建双向链表
创建双向链表需要动态分配内存,一个一个地创建节点,并将每个节点连接起来。下面是一个创建双向链表的示例代码:
struct Node *createDoublyLinkedList(int data[], int n) {
struct Node *head = NULL, *tail = NULL;
for (int i = 0; i < n; i++) {
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->data = data[i];
node->prev = NULL;
node->next = NULL;
if (head == NULL) {
head = node;
tail = node;
} else {
tail->next = node;
node->prev = tail;
tail = node;
}
}
return head;
}
3.2 遍历双向链表
遍历双向链表可以从头节点或尾节点开始遍历,可以使用两个指针分别指向头节点和尾节点,依次访问每个节点,并处理节点中的数据。遍历双向链表的代码如下:
3.2 遍历双向链表
遍历双向链表可以从头节点或尾节点开始遍历,可以使用两个指针分别指向头节点和尾节点,依次访问每个节点,并处理节点中的数据。遍历双向链表的代码如下:
void traverseDoublyLinkedList(struct Node *head) {
struct Node *p = head;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
void traverseDoublyLinkedListReverse(struct Node *tail) {
struct Node *p = tail;
while (p != NULL) {
printf("%d ", p->data);
p = p->prev;
}
printf("\n");
}
其中,traverseDoublyLinkedList函数从头节点开始遍历链表,traverseDoublyLinkedListReverse函数从尾节点开始遍历链表。
3.3 插入节点
在双向链表中插入一个节点需要三个步骤:先创建一个新节点,然后将它插入到链表中正确的位置,最后更新相邻节点的指针。下面是在双向链表中插入节点的代码:
void insertNode(struct Node *head, int index, int data) {
struct Node *p = head;
for (int i = 0; i < index - 1 && p != NULL; i++) {
p = p->next;
}
if (p == NULL) {
printf("Error: Invalid index\n");
return;
}
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->data = data;
node->prev = p;
node->next = p->next;
if (p->next != NULL) {
p->next->prev = node;
}
p->next = node;
}
其中,head是链表的头节点,index表示要插入的位置(从1开始),data表示要插入的数据。该函数会在链表的第index个节点之前插入一个新节点。
3.4 删除节点
删除链表中的一个节点需要三个步骤:先找到要删除的节点,然后将它从链表中移除,最后更新相邻节点的指针。下面是在双向链表中删除节点的代码:
void deleteNode(struct Node *head, int index) {
struct Node *p = head;
for (int i = 0; i < index - 1 && p != NULL; i++) {
p = p->next;
}
if (p == NULL) {
printf("Error: Invalid index\n");
return;
}
if (p->prev != NULL) {
p->prev->next = p->next;
}
if (p->next != NULL) {
p->next->prev = p->prev;
}
free(p);
}
其中,head是链表的头节点,index表示要删除的节点位置(从1开始)。该函数会删除链表的第index个节点。
3.5 查找节点
查找链表中的一个节点可以双向遍历链表,比较每个节点的数据,直到找到目标节点或遍历完整个链表。下面是在双向链表中查找节点的代码:
int searchNode(struct Node *head, int data) {
struct Node *p = head;
int index = 1;
while (p != NULL && p->data != data) {
p = p->next;
index++;
}
if (p == NULL) {
return -1;
} else {
return index;
}
}
int searchNodeReverse(struct Node *tail, int data) {
struct Node *p = tail;
int index = 1;
while (p != NULL && p->data != data) {
p = p->prev;
index++;
}
if (p == NULL) {
return -1;
} else {
return index;
}
}
其中,searchNode函数从头节点开始遍历链表,searchNodeReverse函数从尾节点开始遍历链表。这两个函数都会返回目标节点在链表中的位置(从1开始),如果未找到则返回-1。
总结
在C语言中,单链表、循环单链表和双向链表都是常用的数据结构,它们各有优缺点,适用于不同的场景。在实现链表时,需要注意节点的定义、头节点和尾节点的处理、遍历、插入、删除和查找等操作。
对于单链表,节点定义为包含数据和指向下一个节点的指针的结构体。头节点是指向第一个节点的指针,尾节点为空指针。在插入和删除节点时,需要注意处理头节点和尾节点的变化。
对于循环单链表,节点定义和单链表相同,但尾节点指向第一个节点。在插入和删除节点时,需要注意尾节点的变化。
对于双向链表,节点定义为包含数据和指向前一个节点和后一个节点的指针的结构体。头节点和尾节点分别是第一个节点和最后一个节点。在插入和删除节点时,需要更新相邻节点的前驱和后继指针。
在实现链表时,需要注意内存管理,避免内存泄漏和悬挂指针等问题。同时,要注意边界条件的处理,避免越界访问或非法操作。
最后,链表是一种动态数据结构,能够高效地实现插入、删除和查找等操作。但是,链表的缺点是不支持随机访问,访问任意位置的时间复杂度是O(n),而且需要额外的空间存储指针。因此,在不同的场景下,需要根据实际需求选择合适的数据结构。