[C语言]小白讲链表(通俗易懂)

1 引入链表的原因–数组的缺点

  • 数组中所有元素必须相同-----------结构体解决
  • 数组定义时必须明确数组的个数-----变长数组或者链表解决
  • 某个元素移动可能会造成其他元素大面积移动----------链表解决

2 链表的感性认识

  • 链表是由若干个节点(C语言中为结构体)组成;
  • 链表是锁链连接起来的表,锁链是指指针,表是指存放数据的节点
  • 数组和链表之间的关系:互补

3 链表实现前的准备

一个链表的创建步骤:

  1. 创建空的链表;
  2. 操作链表,创建节点;
  3. 链表节点删除。

接下来我们一步一步操作:

3.1 创建空的链表

//创建一个节点结构体
struct node
{
    int data;	//节点的有效数据
    struct node *pNext;	//struct node*类型的指针
};

注:上述代码只是定义了一个结构体类型,并没有变量产生,不占用内存,相当于给链表节点定义了一个模板

3.2 使用堆创建节点create_node()

使用堆内存创建一个节点,因为链表的内存是需要多少要多少,随时删除释放。

struct node * create_node(int data //节点中要存的数据)
{
	//1. 申请一个节点大小的堆内存
    //1.1 使用malloc来分配内存
    struct node *p = (struct node *)malloc(sizeof(struct node));
    //2. 判断堆内存是否申请成功
    if(NULL == p){
        printf("malloc error!\n");
        return NULL;
    }
    //3. 清理申请到的空内存
    bzero(p, sizeof(struct node));
    //4. 填充节点中的数据
    p->data = data;
    //5. 节点中的指针初始化为NULL
    p->pNext = NULL;
    
    //6. 返回一个struct node* 类型的指针
    //指向刚创建出来的节点的首地址的指针
    return p;
}

注:

  • malloc是什么意思,怎么使用
  • bzero函数是什么意思,怎么使用

4 链表的头指针与头节点

头指针:普通的指针(可以理解,最开始并没有节点,但是需要一个指向第一个节点的首地址的指针),占4个字节;
类型:struct node *
一个典型的链表:头指针—指向—>第一个节点,第一个节点指针-----指向----->下一个节点

头节点:紧跟在头指针的后面,数据部分为空,指针部分指向第一个有效节点。
注:头节点
有无
让对链表的操作有所不同,头节点的作用

5 总结:构建一个简单的链表

  1. 定义一个头指针:pHeader
  2. 创建第一个节点,让头指针指向第一个节点
  3. 第一个节点指向下一个节点
int main(void){
    struct node *pHeader = NULL;	//定义一个头指针:pHeader
    struct node *pHeader = create_node(1);	//创建第一个节点,并让头指针指向第一个节点
    return 0;
}

5.1 尾部插入节点(尾插法)

//输入参数:头指针和新节点地址
void insert_tail(struct node *pH, struct node *new){
    
    //用p来代替pH操作,或者可以不用p直接用pH?
    struct node *p = pH;
    //要是没有指向空,说明后面还有节点连着,继续向后循环
    while(NULL != p->pNext){
        p = p->pNext;
    }
    //将当前的最后一个节点的地址指向新节点的首地址
    p->pNext = new;
}

5.2头部插入节点(头插法)

有头节点的情况:

//输入参数:头指针和新节点地址
void insert_head(struct node *pH, struct node *new){
    
    //注:pH指向头节点的地址,也就是头指针
    //代码简单,逻辑要理一理
    //1.首先将要新加入的节点*new中的指针new->pNext指向原来第一个节点	 //(非头节点)的地址pH->pNext
    new->pNext = pH->pNext;
    //2.这时头节点指针的指向还是为原来的第一个节点首地址,应该让头节点中     //的指针pH->pNext指向新加入的节点首地址,即new
    pH->pNext = new;
    //此时完成了新节点的头部插入
}

没有头节点的情况:
下面代码有错误,无法加入节点,不知道问题出在哪里

//输入参数:头指针和新节点地址
void insert_head(struct node *pH, struct node *new){
    
    //假设没有头节点的情况
    //注:pH指向第一个节点的地址,也就是头指针
    //代码简单,逻辑要理一理
    //1.首先将要新加入的节点*new中的指针new->pNext指向原来第一个节点	 //的地址pH
    new->pNext = pH;
    //2.这时头指针的指向还是为原来的第一个节点首地址,应该让头指针pH指向新加入的节点首地址,即new
    pH = new;
    //此时完成了新节点的头部插入
}

5.2 全部代码—链表的实现看这里

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

//创建一个节点结构体
struct node
{
    int data;	//节点的有效数据
    struct node *pNext;	//struct node*类型的指针
};

//创建节点的函数,返回值为指向创建的节点的首地址指针
struct node * create_node(int data)//节点中要存的数据
{
	//1. 申请一个节点大小的堆内存
    //1.1 使用malloc来分配内存
    struct node *p = (struct node *)malloc(sizeof(struct node));
    //2. 判断堆内存是否申请成功
    if(NULL == p){
        printf("malloc error!\n");
        return NULL;
    }
    //3. 清理申请到的空内存
    bzero(p, sizeof(struct node));
    //4. 填充节点中的数据
    p->data = data;
    //5. 节点中的指针初始化为NULL
    p->pNext = NULL;
    
    //6. 返回一个struct node* 类型的指针
    //指向刚创建出来的节点的首地址的指针
    return p;
}

//尾插法添加节点
void insert_tail(struct node *pH, struct node *new){
    
    struct node *p = pH;
    while(NULL != p->pNext){
        p = p->pNext;
    }
    p->pNext = new;
}

int main(void){	
    
	struct node *pHeader = create_node(1);
	insert_tail(pHeader, create_node(2));
	insert_tail(pHeader, create_node(3));
	insert_tail(pHeader, create_node(4));
	printf("node1 data: %d.\n", pHeader->data);
	printf("node2 data: %d.\n", pHeader->pNext->data);
	printf("node3 data: %d.\n", pHeader->pNext->pNext->data);
	
	return 0;
}                                 

6 读取数据:遍历链表

思考一下:
遍历就是要依次访问链表中的数据,而链表中的数据由节点中的数据提供,而节点之间通过每个节点的指针相连,所以当操作了当前节点后,再由当前节点的指针就可以操作下一个节点,直到后面没有节点。

void list_for_each(struct node *pH){
    
    struct node *p = pH;
    printf("-------------begin---------------");
    //非空就不是最后一个节点,继续循环
    while(NULL != p->pNext){
        
        p = p->pNext;//退出条件,假设这句话执行后,p节点为最后一个节点,下面语句也会打印出信息,不存在漏掉的风险
        printf("node data: %d.\n", p->data);
    }
    printf("---------------end---------------");
}

7 链表节点的删除

思考一下:
链表就是一个一个节点组成,每个节点中有数据(头节点没有数据)和指向下一个节点的指针,当我们要删除一个节点的时候,需要将上一个节点的指针指向待删除节点的下一个节点的首地址,然后对删除后的节点进行内存释放free。

删除分为两种情况:

  1. 删除的节点为尾节点(因为尾节点的指针指向NULL)
  2. 删除的节点不为尾节点
int delete_node(struct node *pH, int data){
    
    struct node *p = pH;	//指向当前节点
    struct node *pPrev = NULL;	//当前节点的上一个节点

	//遍历走到尾节点退出循环    
    while(NULL != p->pNext)
	{

        pPrev = p;
        p = p->pNext;
        if(p->data == data){
            //尾节点的情况
            if(NULL == p->pNext){
            	pPrev->pNext = NULL;
                free(p);
            }
            //普通节点的情况
            else{
                pPrev->pNext = p->pNext;
                free(p);
            }
            return 0;	//删除成功
        }
    	printf("没有要删除的节点.\n");
        return -1;
	}
}

最后测试代码

工程src下创建main.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

//创建一个节点结构体
struct node
{
    int data;	//节点的有效数据
    struct node *pNext;	//struct node*类型的指针
};

//创建节点的函数,返回值为指向创建的节点的首地址指针
struct node * create_node(int data)//节点中要存的数据
{
	//1. 申请一个节点大小的堆内存
    //1.1 使用malloc来分配内存
    struct node *p = (struct node *)malloc(sizeof(struct node));
    //2. 判断堆内存是否申请成功
    if(NULL == p){
        printf("malloc error!\n");
        return NULL;
    }
    //3. 清理申请到的空内存
    bzero(p, sizeof(struct node));
    //4. 填充节点中的数据
    p->data = data;
    //5. 节点中的指针初始化为NULL
    p->pNext = NULL;
    
    //6. 返回一个struct node* 类型的指针
    //指向刚创建出来的节点的首地址的指针
    return p;
}

//尾插法添加节点
void insert_tail(struct node *pH, struct node *new){
    
    struct node *p = pH;
    while(NULL != p->pNext){
        p = p->pNext;
    }
    p->pNext = new;
}

//1头插法插入在头节点之后
void insert_head1(struct node *pH, struct node *new)
{
	new->pNext = pH->pNext;
	pH->pNext = new;
}

//没有头节点的处理,代码有bug,先注释了
/*
void insert_head2(struct node *pH, struct node *new)
{
	new->pNext = pH;
	pH = new;
}
*/

//遍历循环链表,打印信息
void list_for_each(struct node *pH){
    
    struct node *p = pH;	
	int n = 1;	//记录打印了几次,链表中节点的个数
    printf("-------------begin---------------\n");
    //非空就不是最后一个节点,继续循环
    while(NULL != p->pNext){
        
        p = p->pNext;//退出条件,假设这句话执行后,p节点为最后一个节点,下面语句也会打印出信息,不存在漏掉的风险
        printf("node%d data: %d.\n", n, p->data);
		n++;
    }
    printf("---------------end---------------\n");
}

int delete_node(struct node *pH, int data){
    
    struct node *p = pH;	//指向当前节点
    struct node *pPrev = NULL;	//当前节点的上一个节点

	//遍历走到尾节点退出循环    
    while(NULL != p->pNext)
	{

        pPrev = p;
        p = p->pNext;
        if(p->data == data){
            //尾节点的情况
            if(NULL == p->pNext){
            	pPrev->pNext = NULL;
				printf("delete %d.\n", p->data);
                free(p);
            }
            //普通节点的情况
            else{
                pPrev->pNext = p->pNext;
				printf("delete data == %d node.\n", p->data);
                free(p);
            }
            return 0;	//删除成功
        }
    	printf("没有要删除的节点.\n");
        return -1;
	}
}



int main(void){	
    
	struct node *pHeader = create_node(1);	//头节点
	insert_tail(pHeader, create_node(2));	//尾插法,插在头节点之后
	insert_tail(pHeader, create_node(3));	//尾插法,插在2后面
	insert_head1(pHeader, create_node(95));	//头插法,插在头节点之后,2之前
	insert_tail(pHeader, create_node(4));	//尾插法,插在3后面
	insert_head1(pHeader, create_node(97));	//头插法,插在头节点之后,95之前
	insert_head1(pHeader, create_node(90));	//头插法,插在头节点之后,97之前
	list_for_each(pHeader);	//应该输出 90 97 95 2 3 4
	delete_node(pHeader, 90);
	list_for_each(pHeader);	//应该输出 97 95 2 3 4
	
	return 0;
}                   

工程目录下创建CMakeLists.txt

CMAKE_MINIMUM_REQUIRED( VERSION 2.8)
ADD_EXECUTABLE(main src/main.c)

操作

mkdir build;
cd build;
cmake .. && make
./main

在这里插入图片描述

参考:《C语言内核深度解析》–朱有鹏 张先凤著

链表是一种常见的数据结构,它由一系列的结点组成。每个结点包含两部分,即数据域和指针域。数据域用于存储具体的数据,而指针域用于指向下一个结点的地址。链表的基本操作包括插入、删除和查找。 1. 插入操作:链表的插入操作可以在链表的任意位置插入一个新的结点。首先,创建一个新的结点,并将要插入的数据存储在该结点的数据域中。然后,将该结点的指针域指向原来位置的结点,同时修改前一个结点的指针域,使其指向新插入的结点。 2. 删除操作:链表的删除操作可以删除链表中的一个结点。首先,找到要删除的结点,并记录下其前一个结点的地址。然后,将前一个结点的指针域指向要删除结点的下一个结点,从而跳过要删除的结点。 3. 查找操作:链表的查找操作可以在链表中搜索指定的数据。从链表的头结点开始,依次遍历链表的每个结点,直到找到目标数据或者到达链表的末尾。 这些基本操作可以帮助我们进行链表的增删改查操作。通过合理地运用这些操作,我们可以实现各种复杂的功能。需要注意的是,在进行链表操作时,我们需要确保指针的正确性,避免出现指针丢失或者指针指向错误的情况。 总之,链表是一种灵活且常用的数据结构,通过简单的插入、删除和查找操作,我们可以实现各种功能。希望以上解释对您有帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【C语言链表实现】学生成绩管理系统(功能全面,通俗易懂)](https://blog.csdn.net/weixin_72074975/article/details/130254484)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [C语言链表详解(通俗易懂,超详细)](https://blog.csdn.net/weixin_46145739/article/details/104590875)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [C语言链表详解(通俗易懂)](https://blog.csdn.net/weixin_61661271/article/details/125131994)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值