目录
前言
链表是一种常见的数据结构,它可以用来存储一系列有序的元素(通常称为节点)。链表的主要作用如下:
-
实现动态分配内存:链表可以方便地实现动态分配内存的功能,因为可以在运行时将新的元素添加到链表的末尾或将现有的元素从链表中移除。这种灵活性使得链表成为了一种非常灵活且可扩展的数据结构。
-
实现线性表:链表可以用来模拟线性表的结构,例如栈、队列等。通过调整链表的操作方式,可以实现不同的功能。例如,可以使用单向链表来实现栈,而双向链表则可以同时支持正向和反向遍历。
-
实现图或其他高级数据结构:链表也可以被用作其他更复杂的抽象数据结构的组成部分,如树形结构和图结构。在这些情况下,链表可以被用来表示图的边或树的边。
总之,链表是一种非常有用的数据结构,可以帮助程序员轻松地管理一组有序的节点。由于其动态性和灵活性,链表成为了许多算法和数据结构的基础组件之一。
1. 指针基本操作
声明指针:
int *ptr; // 声明一个整型指针
char *charPtr; // 声明一个字符型指针
取地址运算符(&):
int x = 10;
int *ptr = &x; // ptr指向变量x的地址
访问指针所指向的值:
int x = 10;
int *ptr = &x;
printf("Value of x: %d\n", *ptr); // 输出变量x的值
空指针:
int *ptr = NULL; // 将指针初始化为空指针
指针算术运算:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 将指针指向数组的第一个元素
// 使用指针访问数组元素
printf("Value at index 2: %d\n", *(ptr + 2));
指针和数组:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 数组名是数组的第一个元素的地址
// 使用指针遍历数组
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i));
}
void modifyValue(int *x) {
*x = 20; // 修改指针所指向的变量的值
}
int main() {
int y = 10;
modifyValue(&y); // 传递变量地址给函数
printf("Modified value: %d\n", y);
return 0;
}
动态内存分配:
int *ptr = malloc(sizeof(int)); // 分配一个整型变量大小的内存空间
*ptr = 42;
free(ptr); // 释放动态分配的内存
指针数组和数组指针:
int arr1[3] = {1, 2, 3};
int arr2[3] = {4, 5, 6};
int *ptrArr[2] = {arr1, arr2}; // 指针数组
int (*ptrArr)[3] = &arr1; // 数组指针
2. 构建链表
在C语言中,链表是由一个个节点组成的,每个节点包含数据和指向下一个节点的指针。以下是使用C语言构造链表的基本步骤:
首先,你需要定义一个节点结构体,这个结构体通常包含两个成员:一个是存储数据的变量,另一个是指向下一个节点的指针。例如:
struct Node {
int data; // 数据
struct Node* next; // 下一个节点
};
然后,你可以创建节点并添加到链表中。例如:
/**
* 说明:struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
* 这行代码是在C语言中创建一个新的Node结构体实例,并使用malloc函数为其分配内存。
*
* struct Node是一个结构体类型,它可能包含多个不同类型的字段(例如,整数、浮点数、字符、其他结构体等)。
*
* malloc(sizeof(struct Node))会分配足够的内存来存储一个Node结构体实例。malloc函数返回一*个指向新
* 分配内存的指针,该指针被存储在newNode变量中。在C语言中,动态内存分配是必要的,因为在程序运*行时,
* 我们可能不知道需要多少内存。malloc函数从堆中分配内存,而不是从栈中分配。分配的内存是连续的,*并且
* 可以通过指针来访问和修改。
*/
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
01增加新节点
/**
* @brief 创建新节点
*
* 根据传入的数据创建一个新的节点,并返回该节点的指针。
*
* @param data 数据值
*
* @return 节点指针
*/
struct Node* create_node(int data) {
// 创建一个新节点
struct Node* new_node = (struct Node*) malloc(sizeof(struct Node));
// 将传入的数据赋值给新节点的data成员
new_node->data = data;
// 将新节点的next成员初始化为NULL
new_node->next = NULL;
return new_node;
}
/**
* @brief 在尾部添加新的节点
*
* 在给定的链表的尾部添加一个新的节点,并设置节点的数据为给定的数据。
*
* @param head 链表头指针的指针
* @param data 节点数据
*/
void clist_add_at_end(struct Node** head, int data) {
// 创建新节点并分配内存空间
struct Node* new_node = (struct Node*) malloc(sizeof(struct Node));
new_node->data = data;
new_node->next = NULL;
// 如果链表为空,则将新节点设置为链表头
if (*head == NULL) {
*head = new_node;
return;
}
// 遍历链表,找到最后一个节点
struct Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
// 将新节点添加到链表尾部
current->next = new_node;
}
/**
* @brief 在链表指定位置插入一个新节点
*
* 在给定的链表中,在指定位置插入一个新节点。
*
* @param head 链表头节点的指针
* @param index 要插入新节点的位置索引
* @param data 新节点的数据
*
* @return 成功返回0,失败返回-1
*/
int clist_insert_node_at(struct Node** head, int index, int data) {
// 判断索引是否有效且链表不为空
if (index <= 0 || *head == NULL) {
return -1;
}
struct Node* current = *head;
int i;
// 找到要插入位置的前一个节点
for (i = 0; i< index - 1; i++) {
if (current->next == NULL) {
break;
}
current = current->next;
}
// 如果遍历的节点数不够,则索引无效,返回失败
if (i >= index - 1) {
return -1;
}
// 创建新节点
struct Node* new_node = create_node(data);
// 将新节点的下一个节点设为当前节点的下一个节点,然后当前节点的下一个节点设为新节点
new_node->next = current->next;
current->next = new_node;
return 0;
}
02 删除节点
/**
* @brief 删除节点
*
* 根据给定的数据,在链表中删除对应的节点。
*
* @param head 链表头指针的指针
* @param data 要删除的数据
*/
void clist_delete_node(struct Node** head, int data) {
if (*head == NULL) {
return;
}
// 如果链表头节点就是要删除的节点
if ((*head)->data == data) {
struct Node* next = (*head)->next;
free(*head);
*head = next;
return;
}
struct Node* current = *head;
// 遍历链表,查找要删除的节点
while (current->next != NULL && current->next->data != data) {
current = current->next;
}
// 如果遍历完链表没有找到要删除的节点
if (current->next == NULL) {
return;
}
// 如果要删除的节点是尾节点
if (current->next->next == NULL) {
free(current->next);
current->next = NULL;
} else {
struct Node* next = current->next->next;
free(current->next);
current->next = next;
}
}
03 获取节点数据和长度
/**
* @brief 获取节点数据
*
* 根据给定的索引,从指定节点中获取数据。
*
* @param node 节点指针
* @param index 索引
*
* @return 成功获取数据时返回数据值,否则返回-1
*/
int clist_get_node_data(struct Node* node, int index) {
// 判断节点指针是否为空或索引是否小于0,若满足条件则返回-1
if (node == NULL || index < 0) {
return -1;
}
// 初始化计数变量为0
int count = 0;
// 循环遍历节点,直到遍历到下一个节点为空为止
while (node != NULL) {
// 若计数等于索引,则返回当前节点的数据值
if (count == index) {
return node->data;
}
// 计数加1
count++;
// 将当前节点的指针指向下一个节点
node = node->next;
}
// 若循环结束仍未找到索引对应的节点,则返回-1
return -1;
}
/**
* @brief 获取所有节点的数据
*
* 从给定的节点开始遍历,获取所有节点的数据,并返回一个指向动态分配的整型数组的指针。
*
* @param node 节点指针
*
* @return 指向动态分配的整型数组的指针,如果节点为空则返回NULL
*/
int* clist_get_all_node_data(struct Node* node) {
// 如果节点为空,则返回NULL
if (node == NULL) {
return NULL;
}
// 动态分配一个整型数组,长度为 length+1
int* data = (int*) malloc(sizeof(int) * (length + 1));
int count = 0;
// 遍历节点链表,直到当前节点为空
while (node != NULL) {
// 将当前节点的数据存入数组中
data[count] = node->data;
count++;
// 指向下一个节点
node = node->next;
}
// 返回动态分配的整型数组指针
return data;
}
/**
* @brief 获取链表的长度
*
* 根据给定的链表节点,计算并返回链表的长度。
*
* @param node 链表节点指针
*
* @return 链表的长度
*/
int clist_get_length(struct Node* node) {
// 如果链表节点为空,返回0
if (node == NULL) {
return 0;
}
// 初始化链表长度为0
int length = 0;
// 遍历链表,直到链表节点为空
while (node != NULL) {
// 链表长度加1
length++;
// 指向下一个链表节点
node = node->next;
}
// 返回链表长度
return length;
}
3 注意事项
01 插入
-
首部插入:当向链表头部插入一个新节点时,我们需要将新节点的next指针指向原头节点的next指针所指向的位置,然后将原头节点的next指针更新为新节点的地址。需要注意的是,如果链表为空,则需要创建一个新的头节点并将新节点的next指针设置为NULL。
-
尾部插入:当向链表尾部插入一个新节点时,首先找到链表的最后一个节点,然后将新节点的next指针设置为其后一个节点的地址(即NULL),再将当前最后一个节点的next指针更新为新节点的地址。如果没有找到下一个节点(即链表为空),则说明需要在空链表上插入首个节点,此时应将新节点的next指针设为NULL。
-
中部插入,可以通过遍历链表找到要插入的位置,然后进行类似的操作。具体来说,从链表的头节点开始遍历,直到找到要插入位置的节点的前一个节点。在该前一个节点的next指针处插入新节点,并更新该前一个节点的next指针和新节点的next指针。
02 删除
-
首部删除:若要从链表的首部删除一个节点,只需获取头节点的下一个节点,并将其地址赋值给头节点即可。需要注意的是,在删除首部节点之前,应检查链表是否为空以避免错误。
-
中间删除:为了从链表的中间删除一个节点,首先要找到待删除节点的前一个和后一个节点。然后,令前一个节点的next指针指向后一个节点的地址,从而跳过被删除的节点。注意,在进行删除操作之前,应该先判断是否存在这样的前后相邻节点。
-
尾部删除:若要从链表的尾部删除一个节点,需找到倒数第二个节点,将其next指针置为NULL以表示链表结束。与首部删除类似,在删除尾部节点之前,也应确保链表不为空。