用 C 语言实现单向链表

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

image-20250506185853472

版本:

链表基础知识

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

  • 数据域(Data):存储实际的数据。
  • 指针域(Next):指向下一个节点的指针。

单向链表

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

imgs20250418014714704.png

实现单向链表

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

  • 链表创建:创建一个空的链表。
  • 节点插入:支持头部插入、尾部插入和指定位置插入。
  • 节点删除:删除链表中的指定节点。
  • 查找节点:查找链表中是否包含指定数据的节点。
  • 更新节点:修改链表中指定节点的数据。
  • 遍历链表:遍历并打印链表中的所有节点。
/**
 * @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.插入节点

如图:

imgs20250418015133650.png

头部插入

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

/**
 * @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. 删除节点

如图:

imgs20250418015334710.png

删除节点时,我们需要遍历链表,找到目标节点,并将前一个节点的 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


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

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

posted @ 2025-04-21 01:19  九思0404  阅读(104)  评论(0)    收藏  举报