C语言学习——用指针处理链表

news2024/11/18 15:25:38

目录

11.7用指针处理链表

链表概述

简单链表

处理动态链表所需的函数

malloc函数

calloc函数

free函数

建立动态链表

输出链表

对链表的删除操作

对链表的插入操作

对链表的综合操作


11.7用指针处理链表

链表概述

链表是一种常见的数据结构。它是动态地进行存储分配的一种结构。可以根据需要开辟内存

链表有一个“头指针”变量,它存放一个地址,该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应该包括两个部分:用户需要的实际数据和下一个结点的地址。头指针会指向第一个元素,第一个元素又会指向第二个元素... ...直到最后一个元素,它称为“表尾”,它的地址部分存放一个“NULL”(表示空地址),链表到此结束。

链表中各元素在内存中可以不是连续存放的。要找到某一元素,必须先找到上一个元素,根据它提供的下一个元素的地址才能找到下一个元素。如果不提供“头指针”(head),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。

所以,链表的这种数据结构,必须利用指针变量才能实现,即一个结点中应包含一个指针变量,用它放下一个结点的地址。

前面介绍了结构体变量,用它做链表中的结点是最合适的。一个结构体变量包含若干个成员,这些成员可以是数值类型、字符类型、数组类型。我们用这个指针类型存放下一个结点的地址。例如,可以设计这样一个结构体类型:

struct student{
    int num;
    float score;
    struct student *next;
}

其中成员num和score用来存放结点中的有用的数据(用户需要用到的数据)。next是指针类型的成员,它指向struct、student类型数据(这就是next所在的结构体类型)。

一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据

现在,next是struct student类型中的一个成员,它又指向struct student类型的数据。用这种方法可以建立链表。

上图中,每一个结点都属于struct student类型,它的成员next存放下一个结点的地址,程序设计人员可以不必知道具体各地址结点的地址,只要保证将下一个结点的地址放到前一个结点的成员next中即可。

注意,上面只是定义了一个struct student类型,但未实际分配存储空间。只有定义了变量才分配内存单元。

简单链表

这里通过一个例子来说明如何建立和输出一个简单的链表。

建立一个上图所示的简单链表,它由3个学生数据的结点组成。输出各结点中的数据。

#include<stdio.h>
#define NULL 0
struct student{
    long num;
    float score;
    struct student *next;
}; 
​
int main()
{
    struct student a,b,c,*head,*p;
    a.num = 10101;   //对结点num和score赋值 
    a.score = 89.5;
    b.num = 10103;
    b.score = 90;
    c.num = 10107;
    c.score = 85;
    head = &a;      //将结点a的起始地址赋给头指针head 
    a.next = &b;    //将结点b的起始地址赋给a节点的next成员 
    b.next = &c;    //将结点c的起始地址赋给b节点的next成员 
    c.next = NULL;  //c结点的next成员不存放其他结点的地址 
    p = head;       //使p也指向a结点 
    do{
        printf("%ld  %.2lf\n",p->num,p->score);  //输出p指向结点的数据 
        p = p->next;  //使p指向下一个结点 
    } while(p!=NULL);  //输出完c结点后p的值为NULL,循环结束 
    
    return 0;
}
​
//输出:
10101  89.50
10103  90.00
10107  85.00 

看完这个代码,我们可以思考一下这几个问题:

  • 各个结点是怎样构成链表的?

  • 没有头指针head行不行?

  • p起什么作用?没有它行不行?

开始时使head指向a结点,a.next指向b结点,b.next指向c结点,这就构成链表关系。“c.next=NULL”的作用是使c.next不指向任何有用的存储单元。在输出链表时要借助p,先使p指向a结点,然后输出a结点的数据,“p = p->next”是为输出下一个结点做准备。p->next的值是b结点的地址,因此执行“p = p->next”后p就指向了b结点,所以在下一次循环时输出的就是b结点中的数据。

本题是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”

处理动态链表所需的函数

前面说过,链表结构是动态分配的,即在需要的时候才开辟一个结点的存储单元。怎样动态地开辟和释放内存呢?C语言编译系统的库函数提供了以下有关函数。

malloc函数

其函数原型为:

void *malloc(unsigned int size);

其作用是在内存的动态存储区分配一个长度为size的连续空间。此函数的值(即“返回值”)是一个分配域的起始地址(类型为void)。如果此函数未能成功地执行(例如内存空间不足),则返回空指针(NULL)。

calloc函数

其函数原型为;

void *calloc(unsigned n,unsigned size);

其作用是在内存的动态存储区中分配n个长度为size的连续空间。函数返回一个指向分配域起始位置的指针;如果分配不成功,返回NULL。

用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。

free函数

其函数原型为:

void free(void *p);

其作用是释放由p指向的动态存储区,使这部分内存区能被其他变量使用。p是最近一次调用calloc或malloc函数时返回的值。free无返回值。

以前的C版本提供的malloc和calloc函数得到的是指向字符型数据的指针。ANSIC提供的malloc和calloc函数规定为void*类型。

建立动态链表

所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各节点数据,并建立起前后相连的关系。

写一函数建立一个有3名学生数据的单向动态链表。

#include<stdio.h>
#include <stdlib.h>  // 包含 malloc 和 NULL 的标准库
//#include<malloc.h>
#define NULL 0
#define LEN sizeof(struct student)
struct student {
    long num;
    float score;
    struct student *next;
};
​
int n;  //n为全局变量,本文件模块中各函数均可调用它
​
//creat函数,是指针类型,即函数带回一个指针值,她指向一个 struct student 类型数据。
//实际上 creat函数带回一个链表的起始地址
struct student *creat(void) { //括号内写void 表示本函数没有形参,不需要进行数据传递
    struct student *head;
    struct student *p1,*p2;
    n = 0;
    p2 = (struct student*)malloc(LEN);   //开辟一个新的单元
    p1 = p2;
    scanf("%ld %f",&p1->num,&p1->score);
    head = NULL;
    while(p1->num!=0) {
        n = n+1;
        if(n==1) head=p1;
        else
        {
            p2->next=p1;
        }
            p2 = p1;
            p1 = (struct student*)malloc(LEN);
            scanf("%ld %f",&p1->num,&p1->score); 
            
    } 
    p2->next=NULL;
    return(head);  //head是已经定义的指针变量,指向struct student类型数据。
     //因此 函数返回的是head的值,也就是链表中第一个结点的起始地址
}

部分代码解释:

p1 = p2 = (struct student*)malloc(LEN); 

开辟一个长度为LEN的内存区,malloc带回的是不指向任何数据类型的指针(void *类型) 。而p1、p2是指向struct student类型数据的指针变量,因此必须使用强制转换的方法使指针的基类型变为struct student类型,在malloc(LEN)之前加了“( *struct student)”它的作用是使malloc返回的指针转换为指向struct student类型数据的指针。注意“ * ”不可以省略,否则就转换成struct student类型了,而不是指针类型了。

这个算法的思路是,让p1指向新开辟的结点,p2指向链表中的最后一个结点,把p1所指的结点连接在p2所指的结点后面 用“p2->next=p1"来实现。

输出链表

将链表中各结点的数据依次输出,这个问题比较容易处理。

编写一个输出链表的函数print。

void print(struct student *head)
{
    struct student *p;
    printf("\nNow,These %d records are :\n",n);
    p = head;
    if(head != NULL)
    {
        do{
            printf("%ld  %.2lf\n",p->num,p->score);  //输出p指向结点的数据
            p = p->next;  //使p指向下一个结点 
        } while(p!=NULL);  //输出完c结点后p的值为NULL,循环结束 
     
    }
}

head的值由实参传过来,也就是将已有的链表的头指针传给被调用的函数,在print函数中从head所指的第一个结点出发顺序输出各个结点。

对链表的删除操作

已有一个链表,希望删除其中某个结点。怎样考虑此问题的算法呢?

如图,从动态链表中删去一个结点,并不是真正从内存中抹掉,而是把它从链表里分离出来,只要撤销原来的链接操作即可。

写出一函数以删除动态链表中指定的结点。

以指定的学号作为删除结点的标志。例如:输入10103表示要求删除学号为10103的结点。

解题思路:从p指向第一个结点开始,检查该节点中的num值是否等于输入的要求删除的那个学号。如果相等就将该节点删除,如不相等,就将p后移一个结点,再如此下去,直到遇到表尾为止。

注意考虑,链表是空表(无结点)和链表中找不到要删除的结点的情况。

struct student *del (struct student *head,long num)
{
    struct student *p1,*p2;
    if(head==NULL) 
    {
        printf("\nList null!\n");
        return head;
    }
    p1 = head;
    while(num!=p1->num&&p1->next!=NULL)   //p1指向的不是所要找的结点,并且后面还有结点 
    {
        p2 = p1;
        p1 = p1->next;  //p1后移了一个结点 
    }
    if(num==p1->num)  //找到了 
    {
        if(p1==head) head = p1->next;  //若p1指向的是首结点,把第二个结点地址赋予head 
        else 
        {
            p2->next=p1->next;       //否则 将下一结点地址赋值给前一结点地址 
            printf("delete:%ld\n",num);
            n = n-1;
        }
    }
    else
    {
        printf("%ld not been found!\n",num);  //找不到该结点
    }
    return head; 
}

函数的类型是指向struct student类型数据的指针,它的值是链表的头指针。函数参数为head和要删除的学号num。head的值可能在函数执行过程中被改变(当删除第一个结点时)。

对链表的插入操作

对链表的插入是指将一个结点插入到一个已有的链表中。

需要考虑的两个问题:

  • 怎样找到插入的位置?

  • 怎样实现插入?

如果有一群小学生,按身高排序(由低到高)手拉手排好队。现在来了一名新学生,要求按身高顺序插入到队伍中。首先要确定插到什么位置。可以将新同学与队中第1名小学生比身高,若新学生比第1名学生高,就使新同学后移一个位置,与第2名学生比,如果仍比第2名学生高,再往后移,与第3名学生比... ...直到出现比第i名学生高,比i+1名学生低的情况为止。显然,新同学的位置应该在第i名学生之后,在第i+1名学生之前。在确定了位置之后,让第i名学生与第i+1名学生的手脱开,然后让第i名学生去拉新同学的手 ,让新同学另外一只手去拉第i+1名学生的手。这样就完成了插入,形成了新的队列。

struct student *insert(struct student *head,struct student *stud)
{
    struct student *p0,*p1,*p2;
    p1 = head;            //使p1指向第一个结点 
    p0 = stud;            //p0指向要插入的结点 
    if(head == NULL)       //如果原来的链表是空表 
    {
        head = p0;        //使p0指向的结点作为头结点 
        p0->next=NULL; 
    }
    else
    {
        while((p0->num>p1->num)&&(p1->next!=NULL))
        {
            p2 = p1;         //使p2指向刚才p1指向的结点 
            p1 = p1->next;   //p1后移一个结点 
        }
        if(p0->num<=p1->num)
        {
            if(head==p1) head = p0;  //插入到原来第一个结点之前 
            else p2->next = p0;      //插入到p2指向的结点之后 
            p0->next = p1;
        }
        else  //插入到最后的结点之后 
        {
            p1->next=p0;
            p0->next=NULL;
        }
        n = n+1;   //结点数加1 
        return head;
    }
 } 

函数参数时head和stud。stud也是一个指针变量,从实参传来待插入结点的地址给stud。语句“head = p0;”的作用是使p0指向待插入的结点。函数类型是指针类型,函数值是链表起始地址head。

对链表的综合操作

将以上建立、输出、删除和插入的函数组织在以后C程序中,用main函数作为主调函数。则有:

int main()
{
    struct student *head,stu;
    long del_num;
    printf("input records:\n");
    head = creat();                      //建立链表 返回头指针 
    print(head);                        //输出全部结点 
    printf("\ninput the delete number:");
    scanf("%ld",&del_num);              //输入要删除的学号 
    head = del(head,del_num);           //删除后链表的头地址 
    print(head);                        //输出全部结点 
    printf("\ninput the inserted record:");
    scanf("%ld %f",&stu.num,&stu.score);
    head = insert(head,&stu);           //插入一个结点 返回头结点地址 
    print(head);                        //输出全部结点 
    return 0;
 } 

上述代码只删除一个结点,插入一个结点。但如果想再插入一个结点,重复写上程序最后4行,共插入2个结点,结果确是错误的。

int main()
{
    struct student *head,stu;
    long del_num;
    printf("input records:\n");
    head = creat();                      //建立链表 返回头指针 
    print(head);                        //输出全部结点 
    printf("\ninput the delete number:");
    scanf("%ld",&del_num);              //输入要删除的学号 
    head = del(head,del_num);           //删除后链表的头地址 
    print(head);                        //输出全部结点 
    printf("\ninput the inserted record1:");
    scanf("%ld %f",&stu.num,&stu.score);
    head = insert(head,&stu);           //插入一个结点 返回头结点地址 
    print(head);                        //输出全部结点 
    printf("\ninput the inserted record2:");
    scanf("%ld %f",&stu.num,&stu.score);
    head = insert(head,&stu);           //插入一个结点 返回头结点地址 
    print(head);                        //输出全部结点 
    return 0;
 } 

(输出时,插入的数据会无限输出)

出现这个结果的原因是:stu是一个固定地址的结构体变量。第一次把stu结点插入到链表中,第二次若再用它来插入第二个结点,就把第一次结点的数据冲掉了,实际上并没有开辟两个结点。我们可以根据insert函数画出此时链表的情况。为了解决这个问题,必须在每插入一个结点时新开辟一个内存区。我们修改main函数,使之能删除多个结点(直到输入要删的学号为0),能插入多个结点(直到输入要删的学号为0)。

int main()
{
    struct student *head,*stu;
    long del_num;
    printf("input records:\n");
    head = creat();                      //建立链表 返回头指针 
    print(head);                        //输出全部结点 
    
//  printf("\ninput the delete number:");
//  scanf("%ld",&del_num);
//  while(del_num!=0)
//  {
//      head = del(head,del_num);           //删除后链表的头地址 
//      print(head);                        //输出全部结点 
//      printf("\ninput the delete number:");
//      scanf("%ld",&del_num);              //输入要删除的学号 
//  }
    printf("\ninput the inserted record:");
    stu = (struct student *)malloc(LEN);
    scanf("%ld %f",&stu->num,&stu->score);
    while(stu->num!=0)
    {
        head = insert(head,stu);            //插入一个结点 返回头结点地址 
        print(head);                        //输出全部结点 
        printf("\ninput the inserted record:");
        stu = (struct student *)malloc(LEN);
        scanf("%ld %f",&stu->num,&stu->score);
    }
    return 0;
 } 

stu定义为指针变量,在需要插入时先用malloc函数开辟一个内存区,将其起始地址经强制类型转换后赋给stu,然后输入此结构体变量中各成员的值。对不同的插入对象,stu的值是不同的,每次指向一个新的struct student变量。在调用insert函数时,实参为head和stu,将已建立的链表起始地址传给insert函数的形参,将stu(即新开辟的单元地址)传给形参stud,返回的函数值是经过插入之后的链表的头指针(地址)。

在这里,最后一个多次删除插入的代码,我这边同时运行,多次删除操作可以实现,但是到了多次插入就出了点问题,大家可以自行研究一下。

声明:本文章为个人学习笔记,资料整理参考谭浩强《C程序设计(第三版)》如有错误,欢迎大家指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2066082.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【架构设计】-- aarch(ARM) and X86

1、aarch(ARM) 架构 &#xff08;1&#xff09;操作系统支持&#xff1a;早期为 32 位操作系统&#xff0c;目前大部分都是 64 位操作系统 &#xff08;2&#xff09;全称&#xff1a;Advanced RISC Machine&#xff0c;由英国ARM Holdings plc公司开发 这种架构主要⽤于智能…

chatglm3-6b下载时,需要下载哪些文件

在huggingface或modelscope上下载chatglm3-6b时&#xff0c;会发现有两种可执行文件&#xff0c;一种是.bin&#xff0c;一种是.safetensors&#xff0c;在使用的时候你如果直接用git命令git clone https://www.modelscope.cn/ZhipuAI/chatglm3-6b.git直接下载&#xff0c;你会…

【中仕公考怎么样】事业编ABCDE类对应的专业

事业编考试分为ABCDE五个类别&#xff0c;对应的专业分别是&#xff1a; 综合应用能力(A类)&#xff1a; 招聘专业&#xff1a;汉语言与文秘类、法律类、新闻传播类、治安学、治安管理、社会工作、老年服务、青少年服务、思想政治教育、安全工程、公共事业管理、行政管理、人力…

前端打字效果

页面效果链接&#xff0c;点击查看https://live.csdn.net/v/419208?spm1001.2014.3001.5501 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, …

图像处理之:Video Processing Subsystem(一)

免责声明&#xff1a; 本文所提供的信息和内容仅供参考。作者对本文内容的准确性、完整性、及时性或适用性不作任何明示或暗示的保证。在任何情况下&#xff0c;作者不对因使用本文内容而导致的任何直接或间接损失承担责任&#xff0c;包括但不限于数据丢失、业务中断或其他经济…

deque容器的所有操作

1.deque原理 2.deque构造函数 只读迭代器这么写&#xff1a; 3.deque赋值操作 4.deque大小操作 5.deque插入和删除操作 6.deque数据存取 7.deque排序

Linux阿里云服务器,利用docker安装EMQX

第一步&#xff0c;给云服务器docker进行加速 阿里云搜索“镜像加速器”&#xff0c;找到下面这个菜单&#xff0c;点进去 然后找到镜像工具下的镜像加速器 把这个加速器地址复制 然后在自己的云服务器中&#xff0c;找到docker的文件夹 点击json配置文件 把地址修改为刚刚…

边坡监测预警摄像机

边坡是指山体或河岸等地表的斜坡部分&#xff0c;由于受到地质构造、气候变化等因素的影响&#xff0c;边坡可能存在塌方、滑坡等危险情况。为了及时监测和预警边坡的变化情况&#xff0c;可以使用边坡监测预警摄像机 。 边坡监测预警摄像机是一种结合了摄像技术和智能算法的设…

算法的学习笔记---按之字形顺序打印二叉树

&#x1f600;前言 在算法的学习中&#xff0c;二叉树是一种非常基础但又十分重要的数据结构。今天&#xff0c;我们将讨论一种特殊的二叉树遍历方法&#xff1a;之字形顺序打印。这个方法要求我们以“之”字形的顺序遍历并打印二叉树的节点值&#xff0c;也就是第一行从左到右…

开放式耳机别人能听到吗?现在开放式耳机用防漏音效果越来越好!

回答&#xff1a; 开放式耳机的通透的设计允许一部分声音泄露出来&#xff0c;因此站在您旁边的人确实有可能听到您耳机中的声音&#xff0c;尤其是当音量设置得比较高时。开放式耳机通常提供更为自然和宽敞的听感&#xff0c;但牺牲了一定的隔音效果和隐私性。如果您需要在公…

台球助教在线预约小程序源码开发:打造便捷高效的台球学习新体验

在当今快节奏的生活中&#xff0c;台球作为一项集休闲、竞技与社交于一体的运动&#xff0c;受到了越来越多人的喜爱。然而&#xff0c;对于初学者而言&#xff0c;想要快速提升技能&#xff0c;往往需要专业的指导和陪练。传统的台球教练预约方式往往存在信息不对称、预约流程…

Qt实现圆型控件的三种方法之子类化控件并重写paintEvent

前言 最近在研究绘制各种形状的控件&#xff0c;这里专门挑出圆形的控件进行记录&#xff0c;其它形状的也大差不差&#xff0c;会了圆形的之后其它的也类似。 正文 这里我挑出Label来进行举例。 子类化 QLabel 并重写 paintEvent 如果需要更复杂的自定义绘制&#xff0c;…

医疗器械维修技术——开启成功之门的钥匙

随着现代医疗科技的飞速发展&#xff0c;医疗器械的精密程度和复杂性与日俱增。这些高科技的医疗设备&#xff0c;不仅是医生诊断和治疗疾病的得力助手&#xff0c;更是患者重获健康的希望之光。然而&#xff0c;如同任何复杂的机器一样&#xff0c;医疗器械也难免会出现故障和…

DRF——视图类的继承关系功能梳理(APIView,GenericAPIView,GenericViewSet,五大mixin类,)

文章目录 视图APIViewGenericAPIView&#xff08;将数据库查询、序列化类的定义提取到类变量&#xff09;GenericViewSet&#xff08;继承ViewSetMixin和GenericAPIView&#xff09;五大类&#xff08;List,Create,Retrieve,Update,Destory&#xff09;ModelViewSet&#xff08…

【网络安全】15种常见网络攻击类型及防御措施_

随着攻击者效率和复杂性的提高&#xff0c;网络犯罪每年都在急剧增加。网络攻击的发生有多种不同的原因和多种不同的方式。但是&#xff0c;一个共同点是网络犯罪分子会寻求利用组织的安全策略、实践或技术中的漏洞。 什么是网络攻击&#xff1f; 网络攻击是指攻击者出于盗窃…

页面设计任务 个人网站页面

目录 成品: 任务描述 源码&#xff1a; 详细讲解&#xff1a; 1.导航栏部分 2.主页样式部分 3.关于我部分 4.作品集部分 5.联系我部分 6.页脚部分 成品: 任务描述 创建一个个人网站&#xff0c;包含以下部分&#xff1a; 顶部导航栏&#xff1a;包含多个链接&#…

数字人直播软件开发技术有哪些?一文教你摸透行情!

当前&#xff0c;开发数字人直播软件已经成为数字人赛道的重要入局方式&#xff0c;与之相关的数字人直播软件开发技术有哪些等话题的讨论的更是随着时间的推移不断攀升。毕竟&#xff0c;对于创业者而言&#xff0c;不管是打算自行开发&#xff0c;还是选择寻求数字人源码厂商…

微积分-定积分4.5(换元法则)

由于基本定理&#xff08;Fundamental Theorem of Calculus&#xff09;的缘故&#xff0c;能够找到反导函数&#xff08;即反微分&#xff09;是非常重要的。但我们现有的反导函数公式并没有告诉我们如何计算类似以下形式的积分&#xff1a; ( 1 ) ∫ 2 x 1 x 2 d x (1) \qua…

神经网络算法 - 一文搞懂BERT(基于Transformer的双向编码器)

本文将从BERT的本质、BERT的原理、BERT的应用三个方面&#xff0c;带您一文搞懂Bidirectional Encoder Representations from Transformers | BERT。 Google BERT BERT架构&#xff1a; 一种基于多层Transformer编码器的预训练语言模型&#xff0c;通过结合Tokenization、多种E…

贪心算法-最大容量问题

最大容量问题的贪心解法 目录 最大容量问题的贪心解法问题描述问题分析贪心策略代码实现&#xff08;C&#xff09;总结C学习资源 问题描述 给定一个数组ht&#xff0c;其中的每个元素代表一个垂直隔板的高度。我们可以通过任意两个隔板以及它们之间的空间来组成一个容器。容器…