用 C 语言实现单向链表

在这篇文章中,我们将通过 C 语言实现一个简单的单向链表,并实现几个常用的链表操作,包括链表的创建、节点插入、删除、查找、更新以及打印等功能。链表是一个非常重要的线性数据结构,它与数组相比具有动态存储优势,因此在实际开发中广泛应用。

版本:

链表基础知识

链表是一种数据结构,它由一系列节点(Node)构成,每个节点包含两个部分:

数据域(Data):存储实际的数据。

指针域(Next):指向下一个节点的指针。

单向链表

单向链表是最简单的链表形式,每个节点只有一个指针域,指向下一个节点。最后一个节点的指针域为空(NULL),表示链表的结尾。

实现单向链表

我们将实现以下几个功能:

链表创建:创建一个空的链表。

节点插入:支持头部插入、尾部插入和指定位置插入。

节点删除:删除链表中的指定节点。

查找节点:查找链表中是否包含指定数据的节点。

更新节点:修改链表中指定节点的数据。

遍历链表:遍历并打印链表中的所有节点。

/**

* @file name : LinkedList.c

* @brief : 实现链表的增删改查

* @author : qrshxc@163.com

* @date : 2025/04/09

* @version : 1.0

* @note :

* CopyRight (c) 2025-2026 qrshxc@163.com All Right Reseverd

*/

1. 链表创建

链表的创建首先需要创建一个头节点。头节点不存储实际数据,但它是链表的起点,所有的操作都需要从头节点开始。我们使用 calloc 动态分配内存来创建头节点。

/**

* @name LinkedList_Create

* @brief 创建一个空链表,并对空链表进行初始化

* @param

* @return

* @retval head 头结点地址

* @date 2025/04/09

* @version 1.0

* @note

*/

LinkedList_t *LinkedList_Create(void)

{

//给头结点申请一片内存

LinkedList_t * head = (LinkedList_t *)calloc(1,sizeof(LinkedList_t));

//错误处理

if (head == NULL)

{

perror("Colloc memory for head is failed!");

exit(-1);

}

//对头结点进行初始化,注意:头结点没有数据域

head->next = NULL;

//返回头结点地址

return head;

}

2. 创建新节点

为了插入新节点,我们需要一个函数来创建一个新的节点并初始化数据和指针域。

/**

* @name LinkedList_NewNode

* @brief 创建新节点,并进行初始化(数据域+指针域)

* @param data 数据域

* @return

* @retval NewNode 新结点地址

* @date 2025/04/09

* @version 1.0

* @note

*/

LinkedList_t *LinkedList_NewNode(DataType_t data)

{

//给新结点申请一片内存

LinkedList_t *NewNode = (LinkedList_t *)calloc(1,sizeof(LinkedList_t));

//错误处理

if (NewNode == NULL)

{

perror("Colloc memory for NewNode is failed!");

return NULL;

}

//对新结点进行初始化

NewNode->data = data;

NewNode->next = NULL;

//返回新结点地址

return NewNode;

}

3.插入节点

如图:

头部插入

头部插入是将新节点插入链表的最前面。我们需要将新节点的指针域指向当前的第一个节点,然后将头节点的指针指向新节点。

/**

* @name LinkedList_HeadInsert

* @brief 头部插入数据

* @param head 头节点地址

* @param data 数据域

* @return

* @retval true 插入成功

* @retval true 插入失败

* @date 2025/04/09

* @version 1.0

* @note

*/

bool LinkedList_HeadInsert(LinkedList_t * head,DataType_t data)

{

//创建新节点,并对新节点进行初始化

LinkedList_t *NewNode = LinkedList_NewNode(data);

if (NewNode == NULL)

{

printf("Can not insert NewNode!\n");

return false;

}

//判断链表是否为空,如果为空,则直接插入

if (head->next == NULL)

{

//如果为空,则直接把新节点地址赋值给首节点

head->next = NewNode;

return true;

}

//如果不为空,则直接把新节点放在链表头部

NewNode->next = head->next;

head->next = NewNode;

return true;

}

尾部插入

尾部插入是将新节点插入链表的末尾。我们需要遍历链表找到最后一个节点,将其指向新节点。

/**

* @name LinkedList_TailInsert

* @brief 尾部插入数据

* @param head 头节点地址

* @param data 数据域

* @return

* @retval true 插入成功

* @retval true 插入失败

* @date 2025/04/09

* @version 1.0

* @note

*/

bool LinkedList_TailInsert(LinkedList_t *head, DataType_t data)

{

// 创建新节点,并对新节点进行初始化

LinkedList_t *NewNode = LinkedList_NewNode(data);

if (NewNode == NULL)

{

printf("Can not insert NewNode!\n");

return false; // 插入失败返回false

}

// 判断链表是否为空,如果为空,则直接插入

if (head->next == NULL)

{

// 如果为空,则直接把新节点地址赋值给首节点

head->next = NewNode;

return true;

}

// 如果不为空,则找到链表尾部插入新节点

LinkedList_t *phead = head->next; // 从头节点的下一个节点开始

while (phead->next != NULL)

{

phead = phead->next;

}

// 找到尾节点后插入新节点

phead->next = NewNode;

return true;

}

指定位置插入

我们还可以根据指定的目标值(destval)来插入新节点。我们遍历链表,找到目标节点后,将新节点插入到目标节点之后。

/**

* @name LinkedList_InsertByValue

* @brief 指定位置插入数据

* @param head 头节点地址

* @param data 数据域

* @param destval 目标值

* @return

* @retval true 插入成功

* @retval true 插入失败

* @date 2025/04/09

* @version 1.0

* @note

*/

bool LinkedList_InsertByValue(LinkedList_t *head,DataType_t data, DataType_t destval)

{

// 1.创建新节点,并对新节点进行初始化

LinkedList_t *NewNode = LinkedList_NewNode(data);

if (NewNode == NULL)

{

printf("Can not insert NewNode!\n");

return false; // 插入失败返回false

}

// 2.判断链表是否为空,如果为空,则直接插入

if (head->next == NULL)

{

// 如果为空,则直接把新节点地址赋值给首节点

head->next = NewNode;

return true;

}

//3.遍历链表,目的是找的目标节点,比较节点数据域

LinkedList_t *phead = head->next;//用首节点当成头结点

while(phead != NULL && phead->data != destval)

{

phead = phead->next;

}

if (phead == NULL)

{

return false;

}

//找到目标节点,把目标节点放在头结点后面

NewNode->next = phead->next;

phead->next = NewNode;

return true;

}

4. 删除节点

如图:

删除节点时,我们需要遍历链表,找到目标节点,并将前一个节点的 next 指向目标节点的下一个节点,然后释放目标节点的内存。

/**

* @name LinkedList_DellInsert

* @brief 删除

* @param head 头节点地址

* @param data 数据域

* @return

* @retval true 删除成功

* @retval true 删除失败

* @date 2025/04/09

* @version 1.0

* @note

*/

bool LinkedList_Del(LinkedList_t *head, DataType_t dest)

{

//判断链表是否为空

if (head->next == NULL)

{

return false;

}

//备份头结点和当前节点的地址

LinkedList_t *phead = head;

LinkedList_t *current = head->next;

while(current != NULL)

{

//如果当前节点的值等于目标值,则删除

if (current->data == dest)

{

//让头节点跳过当前节点

phead->next = current->next;

free(current);

return true;//删除成功

}

//头结点后移

phead = current;

//当前节点后移

current = current->next;

}

return false;

}

5. 查找节点

我们可以遍历链表来查找目标数据的节点,并返回找到的节点的指针。如果找不到,返回 NULL。

/**

* @name LinkedList_Search

* @brief 查找链表中指定数据的节点

* @param head 链表头指针

* @param dest 要查找的数据

* @return

* @retval LinkedList_t* 成功返回节点指针

* @retval NULL 失败返回NULL

* @date 2025/04/09

* @version 1.0

* @note

*/

LinkedList_t *LinkedList_Search(LinkedList_t *head, DataType_t dest) {

// 参数有效性检查

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

return NULL; // 空链表直接返回NULL

}

// 从头结点的第一个有效节点开始遍历

LinkedList_t *current = head->next;

// 顺序遍历链表

while (current != NULL) {

// 找到数据匹配的节点

if (current->data == dest) {

return current; // 返回找到的节点指针

}

current = current->next; // 继续检查下一个节点

}

// 遍历完仍未找到目标值

return NULL;

}

6. 修改节点

更新节点的操作是查找目标节点后,直接修改其数据域。

/**

* @name LinkedList_Update

* @brief 修改链表中指定数据的节点

* @param head 链表头指针

* @param oldVal 旧数据

* @param newVal 新数据

* @return

* @retval true 修改成功

* @retval false 修改失败

* @date 2025/04/09

* @version 1.0

* @note

*/

bool LinkedList_Update(LinkedList_t *head, DataType_t oldVal, DataType_t newVal) {

// 参数有效性检查

if (head == NULL || head->next == NULL)

{

return false; // 空链表直接返回失败

}

// 从头结点的下一个节点开始遍历

LinkedList_t *current = head->next;

// 遍历链表查找目标值

while (current != NULL) {

// 找到匹配的节点

if (current->data == oldVal) {

current->data = newVal; // 修改数据域

return true; // 返回修改成功

}

current = current->next; // 继续检查下一个节点

}

// 遍历完仍未找到目标值

return false;

}

7. 遍历链表

为了查看链表的内容,我们实现了一个打印函数,遍历整个链表并输出每个节点的数据。

/**

* @name LinkedList_DellInsert

* @brief 遍历链表

* @param head 头节点地址

* @return

* @retval

* @date 2025/04/09

* @version 1.0

* @note

*/

void LinkedList_Print(LinkedList_t *head) {

if (head == NULL) {

printf("链表不存在\n");

return;

}

LinkedList_t *current = head->next; // 跳过头节点

printf("当前链表:");

while(current != NULL) {

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

current = current->next;

}

printf("NULL\n");

}

8.测试及结果

int main() {

// 1. 创建链表 ====================================

printf("1. 创建链表...\n");

LinkedList_t *list = LinkedList_Create();

if (list == NULL) {

printf("链表创建失败!\n");

return -1;

}

printf("创建成功\n");

// 2. 插入测试 ====================================

printf("2. 插入测试...\n");

// 头部插入

printf("头部插入10, 20, 30\n");

LinkedList_HeadInsert(list, 10);

LinkedList_HeadInsert(list, 20);

LinkedList_HeadInsert(list, 30);

// 尾部插入

printf("尾部插入40, 50\n");

LinkedList_TailInsert(list, 40);

LinkedList_TailInsert(list, 50);

// 打印当前链表

printf("当前链表:");

LinkedList_Print(list); // 预期:30 -> 20 -> 10 -> 40 -> 50 -> NULL

printf("\n");

// 3. 查找测试 ====================================

printf("3. 查找测试...\n");

DataType_t search_val = 10;

LinkedList_t *found = LinkedList_Search(list, search_val);

if (found) {

printf("找到值%d,节点地址:%p\n", search_val, (void*)found);

} else {

printf("未找到值%d\n", search_val);

}

printf("\n");

// 4. 修改测试 ====================================

printf("4. 修改测试...\n");

DataType_t old_val = 10, new_val = 100;

if (LinkedList_Update(list, old_val, new_val)) {

printf("修改成功:%d -> %d\n", old_val, new_val);

} else {

printf("修改失败:未找到%d\n", old_val);

}

printf("修改后链表:");

LinkedList_Print(list); // 预期:30 -> 20 -> 100 -> 40 -> 50 -> NULL

printf("\n");

// 5. 删除测试 ====================================

printf("5. 删除测试...\n");

DataType_t del_val = 20;

if (LinkedList_Del(list, del_val)) {

printf("删除成功:%d\n", del_val);

} else {

printf("删除失败:未找到%d\n", del_val);

}

printf("删除后链表:");

LinkedList_Print(list); // 预期:30 -> 100 -> 40 -> 50 -> NULL

printf("\n");

return 0;

}

结果

1. 创建链表...

创建成功2. 插入测试...

头部插入10, 20, 30

尾部插入40, 50

当前链表:当前链表:30 -> 20 -> 10 -> 40 -> 50 -> NULL

3. 查找测试...

找到值10,节点地址:0x5a73654456d0

4. 修改测试...

修改成功:10 -> 100

修改后链表:当前链表:30 -> 20 -> 100 -> 40 -> 50 -> NULL

5. 删除测试...

删除成功:20

删除后链表:当前链表:30 -> 100 -> 40 -> 50 -> NULL

在这篇文章中,我们实现了一个基础的单向链表,包括链表的创建、节点插入、删除、查找、更新和打印等操作。链表是一个动态数据结构,灵活性很强,适用于许多需要频繁插入和删除数据的场景。

希望这篇文章能帮助你理解和实现单向链表的基本操作!