C语言常见问题(七)——动态内存分配之单向链表

目录

一、链表

1.什么是链表

2.头指针head

3.链表的节点

二、链表的操作

1.知识准备

2.插入节点

3.链表的遍历

4.检索链表的某个节点

5.删除节点

6.交换链表的两个节点

三、易错点总结

1.头指针head

2.遍历整个链表与检索尾结点的区别

3.哨兵节点


一、链表

1.什么是链表

        链表是一种动态数据结构(如图是一个单向链表)。为什么说链表是动态的?是因为链表的节点存储于堆区中,并且在程序运行期间我们可以任意修改链表的长度。

2.头指针head

        链表的节点是存储于堆区中的。对于堆区中的数据,我们不能向数组那样直接访问,通常我们会使用指向堆区中某一块内存的指针来读写这块内存的数据。所以我们会使用一个指向链表第一个节点的指针head来表示链表,协助我们在堆区找到这个链表。如果链表为空(链表中没有节点),那么head应等于NULL。

3.链表的节点

        通常我们使用结构体来定义链表的节点,如下:

struct Node{
    int data;			//数据
    struct Node * next;	//指向链表的下一节点
};

        其中数据部分可以自由扩充,可以包含int、float、char等变量,也可以包含数组甚至是结构体。

        next指向了链表的下一个节点,其实就是说next存储了链表下一个节点的地址。如果一个节点是链表的最后一个节点,那么该节点的next应为NULL。


二、链表的操作

以单向链表为例:

1.知识准备

        1)由于链表的操作涉及到了动态内存分配(比如插入和删除某节点),所以我们需要添加stdlib.h头文件。此外,还需要定义链表节点的结构体类型。

#include <stdio.h>
#include <stdlib.h>
typedef struct Node{
    int data;
    struct Node * next;
}tnode,*pnode;

        typedef的作用:重命名。将struct Node结构体类型重命名为tnode,将struct Node*类型重命名为pnode。上面这段代码在声明结构体的同时进行了重命名操作。

        2)除此之外,我们要在链表的生存周期内先定义head指针,如果你想在程序的整个运行期间都能操作链表,那你最好在main函数的开头就定义head。头指针head存储的是链表首节点的地址,当链表为空时,head应等于NULL。其定义如下: 

pnode head=NULL;//等价于struct Node * head=NULL;

        head存储的仅仅是链表首节点的地址。如果head是某个函数的局部变量,当函数返回时head就被释放了,尽管链表的空间没有被释放(只要程序员不主动释放),但你很可能再也找不到这个链表了。

        3)在创建新节点的时候,会使用动态内存分配函数malloc,函数原型如下: 

void * malloc(size_t size);

        其中,参数size是期望分配内存的大小,以字节为单位,一般我们会使用关键字sizeof来计算链表单个节点占内存的大小,例如malloc(sizeof(tnode));

        如果分配成功,mallc函数的返回值是指向被分配内存的指针,这个指针是void *类型,但我们需要的是pnode类型,因此会在返回值这里使用强制转换,例如(pnode)malloc(sizeof(tnode));

        如果未分配成功,malloc会返回空指针NULL。

2.插入节点

        实际编程中没有创建链表这一步骤。只需要用一个头指针指向链表的首节点,然后不断地往链表里插入节点,这些以next关联的节点组成了一个单向链表。

1)在链表的头部插入节点

/* insertIntoHead:在链表的头部插入data为x的节点,需要将返回值赋给head*/
pnode insertIntoHead(pnode head,int x){
    pnode p=NULL;
    //创建节点:
    if( (p=(pnode)malloc(sizeof(tnode))) == NULL){
        printf("malloc error!\n");//动态内存分配失败
        exit(0);//退出程序
    }
    p->data=x;
    p->next=NULL;
    //将该节点与链表的其他节点连起来:
    if(head==NULL){
        //如果是空链表
        return p;//返回当前链表的首节点地址
    }else{
        //如果不是空链表
        p->next=head;//使p的next指向原链表的首节点
        return p;//返回当前链表的首节点地址
    }
    
}

2)在链表的尾部插入节点

  

/* insertIntoRear:在链表的尾部插入data为x的节点,需要将返回值赋给head*/
pnode insertIntoRear(pnode head,int x){
     pnode p=NULL,rear=NULL;
    //创建节点:
    if( (p=(pnode)malloc(sizeof(tnode))) == NULL){
        printf("malloc error!\n");//动态内存分配失败
        exit(0);//退出程序
    }
    p->data=x;
    p->next=NULL;
    //将该节点与链表的其他节点连起来:
    if(head==NULL){
        //如果是空链表
        return p;//返回当前链表的首节点地址
    }else{
        //如果不是空链表
        //寻找链表的最后一个节点:
        rear=head;
        while(rear->next!=NULL){
            rear=rear->next;
        }
        rear->next=p;//使原链表尾结点的next指向p
        return head;//返回当前链表的首节点地址
    }
}

3)如果要在链表的其他位置插入节点,需要和链表的检索联系起来

3.链表的遍历

pnode through(pnode head){
    pnode p0=NULL,p=head;
    while(p!=NULL){
        /*对该节点的加工*/
        printf("%d ",p->data);
        /*对该节点的加工end*/
        p0=p;//保存前驱节点的位置,方便加工
        p=p->next;
    }
    return head;
}

(1)其中,对节点的加工部分可以与其他操作相结合;

(2)p0指针不是必要的,只是为了方便一些复杂的加工操作才使用其保留前驱节点的位置;

(3)返回值不是必要的,根据需要来定。

4.检索链表的某个节点

假设链表节点的定义如下,key能唯一标识链表的节点。

typedef struct Node{
int data;
int key;
    struct Node * next;
}tnode,*pnode;

 检索key值为key0的节点,并返回该节点的地址:

pnode searchNode(pnode head,int key0){
    pnode p=head;
    while(p!=NULL){
        if((p->key)==key0){
            return p;
        }
        p=p->next;
    }
    return NULL;
}

5.删除节点

1)删除某个节点

例如,删除data值为data0的节点:

  

/*deleteNode:删除所有data值为data0的节点,需要将返回值赋给head*/
pnode deleteNode(pnode head,int data0){
    pnode p0=NULL,p=head;
    //链表为空:
    if(head==NULL){
        return head;
    }
    //链表只有一个节点:
    if(p->next==NULL){
        if((p->data)==data0){
            free(p);
            return NULL;
        }else{
            return head;
        }
    }
    //链表节点数大于1时:
    while(p!=NULL){
        if((p->data)==data0){
            if(p==head){
                //如果删除的是首节点:
                p0=p;
                p=p->next;  //p指向链表的第二个节点
                head=p;     //首节点变为链表的第二个节点
                free(p0);
            }else{
                //如果删除的不是首节点:
                p0->next=p->next;//p前驱节点的next指向p的后继节点
                free(p);
                p=p0->next;

            }
        }else{
            //如果不需要删除:
            p0=p;
            p=p->next;
        }
    }
    return head;
}

 2)删除整个链表

/*删除整个链表,需要将返回值NULL赋给head*/
pnode delectList(pnode head){
    pnode p=head;
    while(p!=NULL){
        head=p->next;
        free(p);
        p=head;
    }
    return NULL;
}

6.交换链表的两个节点

a.如果p1和p2均不为首节点:

b.使p1的next指向p2的下一节点,p2的next指向p1的下一节点:

p=p2->next;

p2->next=p1->next;

p1->next=p;

等价于

  

c.让p01的next指向p2,p02的next指向p1

p01->next=p2;

p02->next=p1;

注意:如果p1、p2中有一个为首节点,则需要修改head的值。

#include <stdio.h>
#include <stdlib.h>
typedef struct Node{
    int data;
    struct Node * next;
}tnode,*pnode;

pnode swapNode(pnode head,int data1,int data2);
pnode insertIntoRear(pnode head,int x);
void through(pnode head);

/*交换data值为data1和data2两个节点的位置,需要将返回值赋给head*/
pnode swapNode(pnode head,int data1,int data2){
    pnode p=head,p1=NULL,p2=NULL,p0=NULL,p01=NULL,p02=NULL;
    while(p!=NULL){
        //寻找data等于data1的节点p1:
        if(p->data==data1){
            p1=p;
            p01=p0;//p01为p1的前一项
        }
        //寻找data等于data1的节点p2:
        if(p->data==data2){
            p2=p;
            p02=p0;//p02为p2的前一项
        }
        p0=p;
        p=p->next;
    }
    if(p1==p2||p1==NULL||p2==NULL){
        return head;
    }else{
        p=p2->next;
        p2->next=p1->next;
        p1->next=p;
        if(p1==head){
            head=p2;
            p02->next=p1;
        }else if(p2==head){
            head=p1;
            p01->next=p2;
        }else{
            p01->next=p2;
            p02->next=p1;
        }
    }
    return head;
}

/* insertIntoRear:在链表的尾部插入data为x的节点,需要将返回值赋给head*/
pnode insertIntoRear(pnode head,int x){
     pnode p=NULL,rear=NULL;
    //创建节点:
    if( (p=(pnode)malloc(sizeof(tnode))) == NULL){
        printf("malloc error!\n");//动态内存分配失败
        exit(0);//退出程序
    }
    p->data=x;
    p->key=x;
    p->next=NULL;
    //将该节点与链表的其他节点连起来:
    if(head==NULL){
        //如果是空链表
        return p;//返回当前链表的首节点地址
    }else{
        //如果不是空链表
        //寻找链表的最后一个节点:
        rear=head;
        while(rear->next!=NULL){
            rear=rear->next;
        }
        rear->next=p;//使原链表尾结点的next指向p
        return head;//返回当前链表的首节点地址
    }
}

/*through遍历整个链表,输出data的值*/
void through(pnode head){
    pnode p=head;
    while(p!=NULL){
        /*对该节点的加工*/
        printf("%3d",p->data);
        /*对该节点的加工end*/
        p=p->next;
    }
    printf("\n");
    return;
}

int main()
{
pnode head=NULL;
//插入data取1~10的节点:
    for(int i=1;i<=10;i++)
        head=insertIntoRear(head,i);
    through(head);//输出链表
    head=swapNode(head,2,5);//交换data等于2、5的节点的位置
    through(head);//输出链表
    head=swapNode(head,1,10);//交换data等于1、10的节点的位置
    through(head);//输出链表
    return 0;
}

三、易错点总结

1.头指针head

        head头指针指向链表的首节点,是struct Node *类型,而不是struct Node类型,head的值为链表的首节点的地址。因此,如果首节点改变了(指链表的首节点不再是原先head指向的节点。比如插入、删除和交换节点的位置,都有可能改变链表的首节点),head的值也要相应变化。

        head是指针类型,如果在函数执行过程中链表的首节点改变了,要改变main函数中head的值有两种方式:

        一是让函数返回链表变化后首节点的地址,将其赋值给head,例如:函数调用可以写成head=insertIntoRear(head,i);

        二是通过指向head的二级指针访问head的存储空间,例如:函数调用可以写成insertIntoRear(&head,i);这时函数的声明应写成insertIntoRear(pnode * phead,int x);然后通过*phead去修改head的值。

2.遍历整个链表与检索尾结点的区别

        1)遍历整个链表,包括对尾结点的加工:

p=head;
while(p!=NULL){
    /*节点的加工*/
    p=p->next;
}

        2)检索链表的尾结点p,尾结点的特征是p->next==NULL

p=head;
while(p->next!=NULL){
    p=p->next;
}

3.哨兵节点

        通过插入节点、删除节点以及交换两节点位置的例子,我们发现,涉及首节点变换时,我们需要修改head的值,因此需要对这种情况单独讨论。为了简化代码的书写、统一对链表的操作,我们会使用哨兵节点

        哨兵节点也是节点,类型为struct Node,是链表首节点的前驱节点,在链表为空时就已经存在。哨兵节点的数据部分没有意义,它的next指针指向链表的首节点,当链表为空时,哨兵节点的next值为NULL。链表的头指针head指向哨兵节点。

  

  • 11
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

易水卷长空

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

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

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

打赏作者

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

抵扣说明:

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

余额充值