1.链表初识

本文探讨了如何动态创建链表,包括节点的插入、删除以及访问,介绍了自引用结构的概念,重点讲解了如何通过链表结构存储和管理动态分配的学生信息。同时讨论了链表的优化方法,如尾插优化和环形链表,以及不同场景下的链表应用,如队列、栈和排序。
摘要由CSDN通过智能技术生成

命题的引出

读入若干个学生的信息并进行处理。由于学生个数未知,因此用动态分配的内存来存放学生信息

struct  student{   /*学生信息结构类型*/
       char  nu[7];         /*学号*/
       char  name[9];    /*姓名*/
} ; 

main()
{
      char nu[7];
      struct  student * ptr;
      
      printf("请输入学生学号");
      gets(nu);//读入学号
    
      while(strcmp(nu,"0000")!=0){//判断学号是否合法
          
          ptr = malloc(sizeof(struct  student)) ;
          strcpy(ptr->no,no);//学号复制到内存中 
          printf("请输入学生姓名");
          gets(ptr->name); //读取姓名到内存中
          
          printf("请输入学生学号");
          gets(nu);
      }
      ...
} 

涉及到的函数:gets(), strcmp(), strcpy.

存在问题:由于是动态分配内存来存放学生信息,因此存放每个学生信息的内存通常是不连续的,如何能访问到这些学生信息?

解决方法1:学生信息存放地址保存到指针数组中。

存在问题:该指针数组的长度还是定长的

image-20220321124140763

将碎片穿成链,将物理上不连续的碎片变成逻辑上连续

•如何将存储碎片穿成链呢?

•利用指针—指针的本质是存放地址的

•利用结构体作为存储分配的最小单位

•结构体中定义一个存放指针的分量(成员)

•利用动态存储分配机制,分配结构体内存

自引用结构

不能在struct book结构中再定义struct book类型的变量。

但可以定义指向struct book类型的指针:struct book*(称为自引用结构)。

image-20220321125802932

自引用结构包含一个指针成员,该指针指向与自身同一个类型的结构。如:

struct node{
    int data;
    struct node* pNext;
}

其中struct node*就是自引用结构。

pNext称为链节(link),用于把一个struct node类型的结构变量和另一个同类型的结构变量连接在一起。

struct node n1,n2;
n1.pNext=&n2;

image-20220321212239332

链表

链表结构

学生信息:

typedef struct student{
    //数据域
    char num[7];//学号
    char name[9];//姓名
    //指针域
    struct student* pNext;//记录下一个
}
image-20220321212731950

动态申请3个结构变量存储3个学生的信息,3个结构变量通过指针"链"在了一起,具有前驱和后继关系。第一个结构变量的地址单独记录在一个指针里

链表

  1. 定义:

    是用链节指针链在一起的**自引用结构变量(称为节点)**的线性集合,是线性表的一种存储结构。

  2. 链表结构

  • 节点:电路中联接三个或三个以上支路的点
  • 结点:直线或曲线的终点或交点
  • 头结点:pHead—指向首结点的指针变量
  • 每个结(节)点由2个域组成:
    • 数据域──存储结点本身的信息。
    • 指针域──存储指向后继结点的指针(针对单向链表)。
  • 尾结点的指针域置为NULL(用反斜杠表示),作为链表结束的标志。
image-20220321213735554

3.链表的特点:

  1. 链表是一种存储结构,用于存放线性表;
  2. 链表的结(节)点是根据需要调用动态内存分配函数进行分配的,因此链表可随需要伸长缩短,在要存储的数据个数未知的情况下节省内存;
  3. 链表的节点在逻辑上是连续的,但是各节点的内存通常是不连续的,因此不能立即被访问到,只能从头结点开始逐节点访问

链表的创建、节点的插入和删除

1.链表的创建

从无到有,节点逐个创建、加入(链接),步骤:

  • 声明表头指针
  • 生成结点(结构体)
  • 链接(插入头、或末尾追加)
  • 循环往复

节点的内存动态分配:

image-20220321215400711
pNew = malloc(sizeof(struct node));
pNew->data = 17;

链表结点的动态分配决定了结点在内存的位置不一定是连续的。

typedef struct ListNode {//ListNode:链表节点
       int data;    //定义data变量值,存储节点值
       struct ListNode *next;   //定义next指针,指向下一个节点,维持节点连接
} LISTNODE;
LISTNODE* creat_FIFO_List(){      //FIFO( First Input First Output)先进先出
    //step1:变量定义;
    //step2:变量初始化;
    //stept3:创建节点
    scanf("%d",&num);
    while(num != -1){
        pNew = malloc(sizeof(LISTNODE));
        if(pNew != NULL){
            pNew->data = num;
            //step4:细化:将新节点插入链表
        }
        scanf("%d",&num);
    }
    return pHead;
}    

若创建的是首节点,则由pHead指向;

否则,新增的节点追加到链表尾节点后面;

为了让新增节点能方便追加到链表尾节点后面,需要设计指针pTrail来指向链表尾节点。

image-20220321223330360

追加过程:

pTrail->pNext = pNew;

pTtrail = pNew;

完整版:

LISTNODE* creat_FIFO_List(){
    //step1+step2:定义变量+初始化
    int num;
    LISTNODE* pHead, pTrail, pNew;
    pHead = NULL;
    pTrail = NULL;
    pNew = NULL;
    
    printf("Input positive numbers, -1 to end.\n");
    
    scanf("%d",&num);
    while(num != -1){
        //step3:创建节点,分配内存
        //不管是pHead,pTrail,pNew,都是一个厂子出来的
        pNew = malloc(sizeof(LISTNODE));
        
        //step4:新节点插入链表
        //得先判断内存申请成没成功
        if(pNew != NULL){
            pNew->data = num;
            if(pHead == NULL){//pHead从头到尾就只是占了个名字
                pHead = pNew;
                pTrail = pNew;
            }else
            {
                pTrail->pNext = pNew;//追加
                pTrail = pNew;
            }
        }
        scanf("%d",&num);
    }
    pTrail->pNext = NULL;//设置链表结束标志
    return pHead;
}
2.节点访问

•如何访问链表中的结点:由于链表中结点的内存是动态分配的,无法通过名称去访问,因此只能通过结点的地址去访问。

•某结点的地址记录在其前驱结点的地址域里,因此要想访问第n个结点,必须先得访问第n-1个结点,读取该结点的地址域;而要想访问第n-1个结点,必须先得访问第n-2个结点;以此类推,一直推到访问第1个结点。而第1个结点是由指针headPtr指向的,因此能访问第1个结点,从而也就能访问第2个结点,第3个结点……

void print_List(LISTNODE* pNew){
    if(pNew = NULL)
        printf("The list is empty\n");
    else{
        printf("The list is:\n");
        while(pNew != NULL){
            printf("%d-->",pNew->data);
            pNew = pNew->pNext;
        }
        printf("NULL\n\n");
    }
}

主函数:

int main ()
{
    LISTNODE* pHead;
    pHead = creat_FIFO_List();
}
3.链表节点的动态增加和删除
image-20220321235258975
增加
image-20220321235141499
if(pPre == NULL){
    //插在链表首结点
    pNew->pNext = pCurrent;
    pHead = pNew;
}
else if(pCurrent = NULL)
{
    //插在结尾
    pPre->pNext = pNew;
    pNew->pNext = NULL;
}
else
{
    //插在中间
    pPre->pNext = pNew;
    pNew->pNext = pCurrent;
}
删除
if(pCurrent != NULL){
    if(pPre == NULL)
        pHead = pCurrent->pNext;
    else
        pPre->pNext = pCurrent->pNext;
    free(pCurrent);
}

•链表插入删除效率高,达到O(1)。对于不需要搜索但变动频繁且无法预知数量上限的数据,比如内存池、操作系统的进程管理、网络通信协议栈的trunk管理等等等等,缺了它是绝对玩不转的。

•最显著的应用就是文件系统。你格式化硬盘时会让你选择fat32ntfs格式,其实就是让你选择存储链表空间规模及格式。为提高系统效率,你有时需要做文件碎片整理,这说明一个文件的数据不一定是连续存放的,那么操作系统是如何知道把不连续的数据合成一个文件提供给你的呢?

链表基本操作

typedef struct ListNode{
    int data;
    struct ListNode* pNext;
}LISTNODE,*pLISTNODE;

image-20220322205638998

对链表的基本操作:

创建链表,检索(查找)节点、插入、删除节点、修改节点等。

**(**1)链表的创建其实是一系列插入节点操作组成,时间跨度有长有短。所以我们可以设计适应各种情况的插入节点操作函数来实现创建功能。

(2)如果用链表实现一个先进先出的队列,我们需要将每个新节点插入到链表尾部,简称尾插

(3)如果用链表实现一个后进先出的栈,我们需要将每个新节点插入到链表头部,简称头插

(4)如果用链表实现一个按元素值排序的线性表,我们需要遍历链表,找到第一个大于新节点数据值的节点,插入到其和前驱节点之间,简称有序插

带有空节点的链表

第一个节点不实际存放数据,只存放下一节点地址。因此该链表永远不可能为空。从而可以简化程序的判断逻辑:插入节点或删除节点时不用判断前驱节点为空的情况。

image-20220322211121590

预备:头指针和空节点:
/*创建一个带空节点的链表头*/
void create_ListHead(pLISTNODE* ppHead){
    *ppHead = malloc(sizeof(LISTNODE));
    if((*ppHead) != NULL){
        (*ppHead)->pNext = NULL;
    }
}
主程序片段:
int main (){
    LISTNODE pHead = NULL;
    creat_ListHead(&pHead);
    if(pHead == NULL){
        printf("List is not created.no memory avaliable\n");
        exit(-1);
    }...
}
//尾插
void append1(pLISTNODE,int);
void append2(pLISTNODE,int);
//头插
void appfront(pLISTNODE,int);
//中间插
void insert(pLISTNODE,int);
尾插

一直遍历,直到尾结点

尾结点特征:pTrail->pNext == NULL

接尾巴:pTrail->pNext = pNew

image-20220322212823316

void append1(pLISTNODE p, int val){
    pLISTNODE pNew,pTrail;
    
    //创造新节点
    pNew = malloc(sizeof(pLISTNODE));
    if(pNew != NULL){//创造成功,,开始初始化
        pNew->data = val;
        pNew->pNext = NULL;
        
        /*找到尾结点,记为pTrail*/
        pTrail = p;
        while(pTrail->pNext != NULL){
            pTrail = pTrail->pNext;
        }
        /*接尾巴*/
        pTrail->pNext = pNew;
    }else
        printf("Not inserted. No memory avaliable.\n")
}
尾插优化

为了让新增节点能方便地追加到链表尾节点后面,可以设计一个尾指针pTrail来指向链表尾结点;

追加过程:

pTrail->pNext = pNew;

pTrail = pNew;

/*创建一个带空节点的表头*/
void create_ListHead(pLISTNODE* ppHead, pLISTNODE* ppTrail){
    *ppHead = malloc(sizeof(pLISTNODE));
    if((*ppHead) != NULL){
        (*ppHead)->pNext = NULL;
        *ppTrail = *ppHead;
    }
}
/*把一个新值插入到链表尾,利用尾指针*/
void append2( pLISTNODE* ppTrail,int val){
    pLISTNODE pNew;
    pNew = malloc(sizeof(pLISTNODE));
    
    if(pNew != NULL){
        pNew->data = val;
        pNew->pNext = NULL;
        
        (*ppTrail)->pNext = pNew;
        (*ppTrail) = pNew;
    }
    else
        printf("not inserted, no memory avaliable\n")
}

image-20220322230413341

下面这个是错误的,参数只传了尾结点的指针,为什么不可以,哪里不可以?

/*把一个新值插入到链表尾,利用尾指针*/
void append2( pLISTNODE pTrail,int val){
    pLISTNODE pNew;
    pNew = malloc(sizeof(pLISTNODE));
    
    if(pNew != NULL){
        pNew->data = val;
        pTrail = pNew;
}

image-20220322230333668

**总结:**想对谁进行赋值操作,在函数里就要用他的指针,同理想要改变主函数里的:

想要改变主函数中:就要向函数传递对应指向的
普通变量一级指针
一级指针二级指针
二级指针三级指针
扩展知识:如何只需一个指针,就能操作整个链表?
方案一:环形链表:

将链表尾结点指向链表头的空节点,尾指针变头指针,即:pTrail->pNext = pHead

image-20220322231450566

PS:其实就是相当于变量a = b,然后保留b扔了a.

要记住:范是看到函数的功能有将外面指针直接赋值的,如我要让pTrail = pNext这个操作就要用二级指针:(*ppTrail) = pNext;,这里我们强调的是外界变量(包括指针变量)名意义上的改变。

方案二:环形双链表

将链表节点的指针域扩展为包含前驱节点指针,并且使链表尾节点指向链表头的空节点,即双向环形链表;

这样,头指针指向节点的前驱节点即是尾节点,不再需要尾指针。

image-20220322232017950
头插:后进先出

新增的节点需要插入到链表头 / 空节点后面;

pcurrent = pHead->pNext;

pHead->pNext = pNew;

pNew->pNext = pCurrent;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绿駬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值