11 链表

1 链表的概述

链表就是用锁链连接起来的,锁链指的是指针,表指的是存放数据的节点。链表是由若干个节点组成,各个节点的结构完全类似,都是由有效数据和指针两部分组成,有效数据区用来存储有效数据信息,而指针用来指向链表的前一个或者后一个节点,链表就是利用指针将各个节点进行串联起来的链式存储的线性表。
链表与数组的比较
链表的优点是操作灵活,插入删除效率很高,但是缺点是需要额外分配存放节点地址的空间,而且操作繁琐;
数组的优点是操作简单,易于理解,而且不需要开辟额外的空间,但是在数组中间进行插入和删除的操作效率低。

2 单链表

2.1 单链表的结构

单链表的创建到使用的步骤:
(1)创建空的单链表,比如可以定义一个creat_node()函数来创建第一个节点;
(2)操作单链表(增、删、改、查、排),比如插入操作,定义一个insert_tail()函数来向链表(或者节点)的后面追加新节点;
(3)销毁单链表,比如定义一个destory_list()函数,用于销毁链表。

2.2 单链表的节点构成

在C语言中的构建方法就是定义一个结构体

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

结构体中的两个元素分别是节点的有效数据和指针。将数据定义为int类型,结构体中的指针为struct node * 类型,pnext指针指向的是下一节点空间。定义的结构体类型,本身并没有变量生成,也不占用内存。
使用堆内存创建一个节点
形成链表的特点:必须需要多少就有多少,必须可以随意删除和释放。
(1)申请堆内存并检查申请内存是否成功,申请的空间大小为节点结构体类型规定的大小,刚申请的新内存就是一个新节点;
(2)清零刚申请的堆内存空间;
(3)向新节点写入有效数据;
(4)初始化指针为NULL。
伪代码的实现:

struct node * creat_node(int data)
{
    struct node *p =(struct node*)malloc(sizeof (struct node));//申请一个节点的空间大小
    if(NULL == p)//检查申请内存是否成功
    {
        printf ("malloc errror.\n");
        return NULL;

    }
    bzero(p, sizeof(struct node ));//将 sizeof(struct node )个字节清0
    p->data = data;
    p->pnext = NULL;
    return p;
}

链表的头指针
头指针并不是节点,而是一个普通指针变量,占4个字节,头指针的类型是struct node *类型,所以它能指向链表的节点。

2.3 构建一个简单的链表

(1)定义头指针
(2)创建一个节点,并将头指针指向一个节点
(3)接着创建节点,并将创建的节点从前一个节点的尾部插入进来
(4)以此类推,需要多少数据便创建多少个节点,最终形成链表

2.4 从单链表尾部插入节点

在这里插入图片描述
(1)找到链表的最后一个节点;
(2)将新的节点和原来的最后一个节点连接起来。

void insert_tail(struct node *ph, struct node *new)
{
    struct node *p = ph;
    while (NULL != p->pnext)
    {
        p = p->pnext;
    }
    p->pnext = new;
}

函数的参数为链表的头指针ph和要插入的新节点的首地址。第一步的while循环用来找到最后一个节点的首地址,第二步的作用是将新节点和原来的最后一个节点连接起来。
构建一个简单的链表

#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
struct node//构建单链表节点
{
    int data;
    struct node *pnext;
};
struct node *create_node(int data);
void insert_tail(struct node *ph, struct node *new);
int main(void)
{
    struct node *pheader = create_node(1);
    insert_tail(pheader, create_node(2));
    insert_tail(pheader, create_node(3));
    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;
}

头节点
头节点有两个特点:
(1)紧跟在头指针后面
(2)头节点的数据部分是空的(或者存链表的节点数),指针部分指向第一个有效节点

2.5 从单链表头部插入节点

(1)新节点的pnext指向原来原来的第一个节点的首地址,即新节点和原来的第一个节点相连;
(2)头节点的pnext指向新节点的首地址,即头节点和新节点相连。
简单地来讲就是先连接尾巴,再连接头部。
在这里插入图片描述
伪代码实现如下:

void insert_head(struct node *ph, struct node *new)
{
new -> pnext = ph -> pnext;
ph -> pnext = new;
}

这两个步骤的顺序不可交换,若将头节点pnext指针指向新节点的首地址,当我们想要执行第一步时原来的第一个有效节点的地址已经丢失了,第一步就做不下去了。
箭头非指向
在C语言中,箭头->是用指针的方式来访问结构体中的某个成员,链表中节点的连接过程和程序中的箭头没有关系,链表中的节点是通过指针指向来连接的,编程中表现为给指针变量赋值,实质是把后一个节点的首地址赋值给前一个节点的pnext元素。

2.6 遍历单链表

遍历的方法是,从头指针+头节点开始,顺着链表连接指针一次访问链表的各个节点,取出当前访问节点的数据,然后再访问洗衣歌节点,知道最后一节点结束返回。
遍历过程分析
(1)指针p访问第一个有效节点并判断此节点是否是尾节点,取出数据,指针p移动到下一个节点;
(2)判断当前节点是否是尾节点,取出数据,移动到下一个节点;
(3)判断当前节点是否是尾节点,若是,取出数据,停止遍历。
代码分析

//遍历单链表,ph为指向单链表的头指针,将遍历的节点数据打印出来
void_list_for_each_1(struct node *ph)
{
    struct node *p = ph -> pnext;//p直接走到第一个节点
    printf("---------begin----------\n");
    while (NULL != p -> pnext)//判断是否为最后一个节点
    {
        printf ("node data:%d.\n",p -> data);
        p = p -> pnext;//走到下一个节点,也就是循环增量
    }
    printf ("node data:%d.\n",p -> data);
    printf("---------end----------\n");
}

结束while循环后还要打印一次p -> data是因为当p走到最后一个节点时,p -> pnext已经等于NULL,不会进入循环体,因此尾节点的data并不会打印出来。

void_list_for_each_2(struct node *ph)
{
    struct node *p = ph ;
    printf("---------begin----------\n");
    while (NULL != p -> pnext)
    {
        printf ("node data:%d.\n",p -> data);
    }
    printf("---------end----------\n");
}

若链表中没有头节点不能使用该遍历算法,因为会漏掉第一个节点的有效数据。

2.7 删除单链表的节点

情况一:
删除的节点不是尾节点
(1)把删除节点的前一个节点的pnext指针指向待删除节点的后一节点的首地址(这个节点从链表中摘除);
(2)对这个被摘除的节点进行free操作,释放内存
情况二:
删除的节点是尾节点
(1)把待删除尾节点的前一节点的pnext指针指向NULL(原来的尾节点前面的一个节点变成新的尾节点);
(2)对摘除的节点进行free操作,释放内存。
为什么要释放内存
程序在遍历链表后就结束返回,还没释放的内存会被自动释放。如果删除节点中没有free,整个程序中频繁的添加或者删除节点,会出现吃内存的现象。
代码实现
从链表中ph中删除节点,待删除的节点的特征是数据区等于data

int delete_node (struct *ph, int data)
{
    struct node *p = ph;//指向当前节点
    struct node *pprev = NULL;//指向当前节点的前一节点
    while (NULL !=  p -> pnext)//遍历,走到尾节点退出循环
    {
        pprev = p;//跟随p移动,指向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;
}

2.8 单链表的逆序

首先遍历原链表,然后将原链表的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个一次取出,采用头插入法插入新的链表中即可。链表逆序=遍历+头插入。
代码实现
将ph指向的链表逆序

int reverse_node (struct node *ph)
{
    struct node *p = ph -> pnext;//p指向第一个有效节点
    struct node *pback ;//保存当前节点的后一节点地址
    //当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
    if ((NULL = p) || (NULL == p -> pnext))
        return;
    while (NULL !=  p -> pnext)//遍历
    {
        pback = p -> pnext;//保存p节点后面一个节点地址
        if (p == ph -> pnext)//原链表第一个有效节点
        {
            p -> pnext = NULL;//头插入之尾部连接
            else//原链表的非第一个有效节点
            {
               p -> pnext = ph -> pnext;//头插入之尾部连接
            }
               p -> pnext = p;//头插入之头部连接
               p = pback;//指针p走到下一个节点
    }
    insert_head(ph,p);
}

原链表中第一个有效节点逆序后变成了尾节点,它的pnext指针指向NULL。pback指针的作用是,在把当前遍历到的节点头插入到新的链表之前,保存下一个节点的地址,否则在当前节点插入新链表后,下一节点的地址就会丢失。
在遍历到最后一个节点时,尾节点不满足while循环条件,因此要在循环结束后手动将尾节点头插入到新链表中。

2 双链表

双链表是有两个遍历方向的链表。
单链表=有效数据+指针(指针指向后一个节点)
双链表=有效数据+两个指针(分别指向前一个节点和后一个节点)
双链表的结构

2.1 双链表插入节点

2.1.1 双链表尾部插入节点

在这里插入图片描述

//insert_tail(待插入的链表,新节点)
void insert_tail(struct node *ph, struct node *new)
{
//第一步:找到链表的尾节点
struct node *p = ph;
while ( NULL != p -> pnext)
{
p = p -> pnext;
}
//第二步:将新节点接到链表的尾节点后面成为新的尾节点
//(1)原来的尾节点的pnext指针指向新节点的首地址
//(2)新节点的pprev指针指向原来的尾节点的首地址
p -> pnext = new;
new ->pprev = p;
}
2.1.2 双链表头部插入节点

在这里插入图片描述

void insert_tail(struct node *ph, struct node *new)
{
new -> pnext = ph -> pnext;//新节点的next指针指向原来的节点1的地址
if (NULL != ph -> pnext)//节点1的prev指向新节点地址
ph -> pnext -> pprev = new;
ph -> pnext = new;//头节点的next指针指向新节点的地址
new -> pprev = ph;//新节点的prev指针指向头节点的地址
}

2.2 遍历双链表

正向遍历
struct node *list_for_each(struct node *ph)
{
    struct node *p = ph ;
    while (NULL == p )
    {
        return NULL;
    }
    while (NULL != p -> pnext)
    {
        p = p -> pnext;
        printf ("data=%d.\n",p -> data);
    }
   return p;
}

正向遍历和单链表遍历相同,此处不再赘述过程。

逆向遍历
#include<stdio.h>
#include<stdlib.h>
struct node
{
    int data;
    struct node *pprev;
    struct node *pnext;
};
void list_for_each_reverse(struct node *ptail)
{
    struct node *p = ptail;
    while (NULL != p -> pprev)
    {
        printf ("data=%d.\n",p -> data);
        p = p -> pprev
    }
}
int main (void)
{
    struct node *pheader = create_node(0);
    insert_tail(pheader,create_node(11));
    insert_tail(pheader,create_node(12));
    insert_tail(pheader,create_node(13));
    
    printf("正向遍历:\n");
    struct node *ptail = list_for_each(pheader)
    printf("反向遍历:\n");
    list_for_each_reverse(ptail);
    return 0;
}

list_for_each_reverse()函数接收一个尾节点的指针作为参数,进行逆向遍历,逻辑和正向遍历差不多,通过p = p -> pprev来向前移动,依次访问节点。main函数创建了一个包含三个有效节点的链表,然后正向、逆向遍历。

2.3 删除双链表的节点

情况一:
要删除尾节点
在这里插入图片描述
需要断开(1)和(2)这两条链接,然后释放free(p)就完成了尾节点的删除。用p->pprev->pnext=NULL;这条语句就断开了图中的链接(1)。用p->pprev=NULL断开图中链接(2),因为最终要释放尾节点,所以第二条语句可以省略。
情况二:
要删除的不是尾节点
在这里插入图片描述
要删除节点1就要断开(1)(2)(3)(4)这四条指针的链接,然后释放free(p)
p->pprev->pnext=p->pnext;前一个节点的pnext指向一个节点的首地址;
p->pprev=NULL;断开连接(2);
p->pnext=NULL;断开连接(3);
p->pnext->pprev=p->pprev后一个节点的pprev指向前一个节点的首地址;
同样因为释放free(p),所以第二步和第三步可以省略。
代码实现

int delete_node(struct node *ph, int data)
{
    struct node *p = ph;
    if(NULL == p)
        return -1;
    
    while (NULL != p -> pnext)
    {
        p = p -> pnext;
        if (p -> data == data)
        {
            if (NULL == p -> pnext)
            {
                p -> pnext ->pnext = NULL;
            }
            else
            {
                p -> pprev -> pnext = p -> pnext;
                p -> pnext-> pprev = p -> pprev;
            }
            free (p);
            
            return 0;
        }
    }
    printf ("未找到要删除的节点.\n")
    return -1;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
链表归并是指将两个有序链表合并成一个有序链表的过程。实验11_9的目的是通过链表归并的实现,加深对链表操作的理解和应用。 具体实现步骤如下: 1. 定义一个链表结构体Node,包含一个整型数据域和一个指向下一个节点的指针域。 2. 定义一个链表归并函数merge,输入参数为两个链表的头节点指针head1和head2,输出参数为归并后的链表头节点指针head3。 3. 在merge函数内部,定义一个临时节点指针p,将其指向头节点较小的那个链表的头节点,将头节点较大的那个链表的头节点赋值给head3。 4. 在while循环中,判断p的指针域是否为空,如果为空,则将p的指针域指向头节点较大的那个链表的头节点,并将该链表的头节点指针head3赋值给p。 5. 在while循环中,判断p所指向的节点的数据域与另一个链表头节点的数据域的大小关系,将p的指针域指向数据域较小的节点。 6. 在while循环结束后,将剩余的节点直接接到归并后的链表尾部。 完整代码如下: ``` #include<iostream> using namespace std; struct Node{ int data; Node* next; }; Node* merge(Node* head1,Node* head2){ if(head1==NULL) return head2; if(head2==NULL) return head1; Node* head3=NULL; Node* p=NULL; if(head1->data<head2->data){ p=head1; head1=head1->next; } else{ p=head2; head2=head2->next; } head3=p; while(head1!=NULL&&head2!=NULL){ if(head1->data<head2->data){ p->next=head1; head1=head1->next; } else{ p->next=head2; head2=head2->next; } p=p->next; } if(head1!=NULL) p->next=head1; if(head2!=NULL) p->next=head2; return head3; } int main(){ Node* head1=new Node; Node* head2=new Node; Node* p1=head1; Node* p2=head2; int n,m; cout<<"请输入第一个链表的长度:"; cin>>n; cout<<"请输入第一个链表的元素:"; for(int i=0;i<n;i++){ int x; cin>>x; Node* node=new Node; node->data=x; p1->next=node; p1=node; } cout<<"请输入第二个链表的长度:"; cin>>m; cout<<"请输入第二个链表的元素:"; for(int i=0;i<m;i++){ int x; cin>>x; Node* node=new Node; node->data=x; p2->next=node; p2=node; } Node* head3=merge(head1->next,head2->next); cout<<"合并后的链表为:"; while(head3!=NULL){ cout<<head3->data<<" "; head3=head3->next; } cout<<endl; return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值