文章目录
1. 结构
2. 链表的分类
1. 单链表
2. 双链表
3. 循环单链表
4. 循环双链表
3. 优缺点
4. 单链表函数
5. 单链表代码实现
1. 结构
逻辑结构
链表是一种线性结构,由一系列结点(Node)组成。每个结点包含一个数据元素和一个指向下一个结点的指针(Pointer)。所有结点通过指针相连,形成一个链式结构。通常,我们将指向链表中第一个结点的指针称为“头结点”。
- data:存放结点的数据值。
- next:存放结点的直接后继的地址(指针)。
物理结构
与数组不同,链表中的结点需要自行组织,向系统申请很多分散在内存各处的结点。每个结点都保存在当前结点的数据和下一个结点的地址(指针)中,通过指针将结点串成一链。
- 逻辑结构:展示了结点之间通过指针连接的链表结构。
- 物理结构:展示了结点在内存中的分布和通过指针相互连接的方式。
2. 链表的分类
链表分为单链表、双链表、循环单链表、循环双链表和静态链表。
1. 单链表
单链表是一种线性数据结构,每个结点包含一个数据元素和一个指向下一个结点的指针。单链表的特点是每个结点只有一个前驱结点和一个后继结点,头结点没有前驱结点,尾结点没有后继结点。
结构:
- 每个结点包含两个部分:数据域和指针域。
- 指针域存放着下一个结点的地址。
示例:
head -> [data|next] -> [data|next] -> [data|next] -> NULL
2. 双链表
双链表是一种每个结点包含两个指针的链表结构,一个指向前驱结点,另一个指向后继结点。双链表的特点是可以在链表中双向遍历,既可以从头结点遍历到尾结点,也可以从尾结点遍历到头结点。
结构:
- 每个结点包含三个部分:数据域、前驱指针域和后继指针域。
- 前驱指针域存放着前一个结点的地址,后继指针域存放着后一个结点的地址。
示例:
NULL <- [prev|data|next] <-> [prev|data|next] <-> [prev|data|next] -> NULL
3. 循环单链表
循环单链表是一种特殊的单链表,其中尾结点的指针指向头结点,形成一个循环。循环单链表的特点是链表中的任何一个结点都可以作为起点进行遍历,最终会回到该结点。
结构:
- 每个结点包含两个部分:数据域和指针域。
- 尾结点的指针域指向头结点。
示例:
head -> [data|next] -> [data|next] -> [data|next] --|
|--------------------------------------------------|
4. 循环双链表
循环双链表是一种特殊的双链表,其中尾结点的后继指针指向头结点,头结点的前驱指针指向尾结点,形成一个双向循环。循环双链表的特点是可以从链表中的任何一个结点进行双向遍历,最终会回到该结点。
结构:
- 每个结点包含三个部分:数据域、前驱指针域和后继指针域。
- 尾结点的后继指针指向头结点,头结点的前驱指针指向尾结点。
示例:
head <-> [prev|data|next] <-> [prev|data|next] <-> [prev|data|next] <-> head
结点离散分配与指针连接
链表中的结点是离散分配的,彼此通过指针相连。每个结点只有一个前驱结点和一个后继结点。确定一个链表只需要头指针,通过头指针可以遍历整个链表。
3. 优缺点
优点
-
插入和删除操作效率高
- 链表的插入和删除操作非常高效,因为只需要修改相关结点的指针,不需要像数组那样移动大量的元素。因此,链表在频繁的插入和删除操作场景中具有明显的优势。
-
动态扩展性能更好
- 链表不需要像数组那样预先指定固定的大小,而是可以随时动态的增加或缩小。链表是真正的动态数据结构,不需要处理固定容量的问题。当需要增加新的元素时,只需要分配新的结点并调整指针即可。
缺点
-
查找慢
- 由于链表中的结点不是连续存储的,无法像数组一样根据索引直接计算出每个结点的地址。必须从头结点开始遍历链表,直到找到目标结点,这导致了链表的随机访问效率较低。在链表中查找某个特定元素需要遍历链表,时间复杂度为 O(n)。
-
额外的存储空间
- 链表的每个结点都需要存储指向下一个结点的指针,这会占用额外的存储空间。因此,相比于数组,链表需要更多的内存空间来存储相同数量的数据元素。对于每个结点,需要额外存储指针的空间,这在内存使用方面可能会造成一定的开销。
4. 单链表函数
单链表的特性
- 节点的定义:每个节点包含数据部分和指向下一个节点的指针部分。
- 头节点:链表的头节点是链表的起点,通过头节点可以遍历整个链表。
- 末尾节点:链表的末尾节点指向
NULL
,表示链表的结束。 - 动态扩展:节点在内存中不必是连续的,可以动态分配和释放内存。
为了实现链表的操作,需要实现以下函数:
-
初始化链表
void initLinkedList(LinkedList *list);
-
返回链表的长度:返回链表中元素的个数。
size_t getLength(const LinkedList *list);
-
在指定位置插入元素:在链表的指定位置插入一个元素。
void insertAt(LinkedList *list, size_t index, int element);
-
在末尾插入元素
void insertEnd(LinkedList *list, int element);
-
删除指定位置的元素并返回被删除的元素
int deleteAt(LinkedList *list, size_t index);
-
删除末尾元素
int deleteEnd(LinkedList *list);
-
获取指定位置的元素:返回链表中指定位置的元素。
int getElementAt(const LinkedList *list, size_t index);
-
修改指定位置的元素
void modifyAt(LinkedList *list, size_t index, int newValue);
-
释放链表内存
void destroyLinkedList(LinkedList *list);
5. 单链表代码实现
代码定义并实现了单向链表的基本操作,包括初始化、插入、删除、获取和修改元素,以及释放链表的内存。
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构定义
typedef struct Node {
int data; // 节点存储的数据
struct Node *next; // 指向下一个节点的指针
} Node;
// 链表结构定义
typedef struct {
Node *head; // 链表头节点指针
size_t size; // 链表中的节点个数
} LinkedList;
// 初始化链表
void initLinkedList(LinkedList *list) {
list->head = NULL; // 初始化头节点为空
list->size = 0; // 初始化节点个数为0
}
// 返回链表的长度
size_t getLength(const LinkedList *list) {
return list->size; // 返回链表的节点个数
}
// 在指定位置插入元素
void insertAt(LinkedList *list, size_t index, int element) {
// 检查插入位置是否有效
if (index > list->size) {
return; // 忽略无效的插入位置
}
// 创建新节点
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = element;
// 如果插入位置是头节点
if (index == 0) {
newNode->next = list->head;
list->head = newNode; // 新节点成为头节点
} else {
// 找到插入位置的前一个节点
Node *prevNode = list->head;
for (size_t i = 0; i < index - 1; i++) {
prevNode = prevNode->next;
}
// 插入新节点
newNode->next = prevNode->next;
prevNode->next = newNode; // 新节点插入在前一节点之后
}
list->size++; // 更新节点个数
}
// 在末尾插入元素
void insertEnd(LinkedList *list, int element) {
insertAt(list, list->size, element); // 在链表末尾插入元素
}
// 删除指定位置的元素并返回被删除的元素
int deleteAt(LinkedList *list, size_t index) {
// 检查删除位置是否有效
if (index >= list->size) {
return -1; // 忽略无效的删除位置
}
int deletedElement;
// 如果删除位置是头节点
if (index == 0) {
Node *temp = list->head;
list->head = temp->next;
deletedElement = temp->data;
free(temp); // 释放被删除节点的内存
} else {
// 找到删除位置的前一个节点
Node *prevNode = list->head;
for (size_t i = 0; i < index - 1; i++) {
prevNode = prevNode->next;
}
// 删除节点
Node *temp = prevNode->next;
prevNode->next = temp->next;
deletedElement = temp->data;
free(temp); // 释放被删除节点的内存
}
list->size--; // 更新节点个数
return deletedElement;
}
// 删除末尾元素并返回被删除的元素
int deleteEnd(LinkedList *list) {
return deleteAt(list, list->size - 1); // 删除链表末尾的元素
}
// 获取指定位置的元素
int getElementAt(const LinkedList *list, size_t index) {
// 检查索引位置是否有效
if (index >= list->size) {
return -1; // 返回无效的索引
}
// 遍历找到指定位置的节点
Node *currentNode = list->head;
for (size_t i = 0; i < index; i++) {
currentNode = currentNode->next;
}
return currentNode->data; // 返回指定位置的元素
}
// 修改指定位置的元素
void modifyAt(LinkedList *list, size_t index, int newValue) {
// 检查修改位置是否有效
if (index >= list->size) {
return; // 忽略无效的修改位置
}
// 遍历找到指定位置的节点
Node *currentNode = list->head;
for (size_t i = 0; i < index; i++) {
currentNode = currentNode->next;
}
currentNode->data = newValue; // 修改节点的值
}
// 释放链表内存
void destroyLinkedList(LinkedList *list) {
// 遍历释放每个节点的内存
Node *currentNode = list->head;
while (currentNode != NULL) {
Node *temp = currentNode;
currentNode = currentNode->next;
free(temp); // 释放每个节点的内存
}
list->head = NULL; // 头节点置为空
list->size = 0; // 节点个数置为0
}
// 主函数,测试链表操作
int main() {
LinkedList myList; // 声明链表
initLinkedList(&myList); // 初始化链表
printf("初始化链表成功!\n");
insertEnd(&myList, 1); // 链表尾部插入元素1
insertEnd(&myList, 2); // 链表尾部插入元素2
printf("向链表插入了2个元素\n");
printf("链表长度为: %zu\n", getLength(&myList)); // 获取链表长度
insertAt(&myList, 1, 3); // 在索引1处插入元素3
printf("在索引1处插入元素3\n");
printf("链表长度为: %zu\n", getLength(&myList)); // 再次获取链表长度
printf("索引1处的元素为: %d\n", getElementAt(&myList, 1)); // 获取索引1处的元素
modifyAt(&myList, 0, 4); // 修改索引0处的元素
printf("把索引0处的元素修改为4\n");
printf("删除索引1处的元素,该元素值是: %d\n", deleteAt(&myList, 1)); // 删除索引1处的元素
destroyLinkedList(&myList); // 销毁链表
printf("链表销毁成功!\n");
return 0;
}