数据结构之双链表

数据结构之双链表文章目录前言为何要双链表 双链表的结构图示项目的建造双链表结点的定义双链表的各种方法实现双链表之新建结点双链表之初始化双链表之判空双链表之求具体元素数量双链表之打印链表内容双链表之尾插双链表之尾删双链表之头插双链表之头删双链表之查找值双链表之任意位置插入值双链表之任意位置删除双链表之销毁空间前言上一节 博主讲解了单链表 并且具体的实现了单链表的增删改查 而这次博主要讲解的是双向循环链表 简称双链表 为何要双链表 既然有了单链表 为何还搞一个双链表呢 答案就是解决了单链表的一些缺点

前言


上一节,博主讲解了单链表,并且具体的实现了单链表的增删改查,而这次博主要讲解的是双向循环链表,简称双链表.


为何要双链表?


既然有了单链表,为何还搞一个双链表呢? 答案就是解决了单链表的一些缺点. 这个和上一节博主讲解的为何需要单链表,而答案是单链表可以解决顺序表空间浪费严重的问题一样. 所以他们三者构成了一个简单关系,如下图:


image-20210804084332347

补充:,大家如果对单链表的增删查改比较熟悉,就知道单链表有一个致命的缺陷:

  • 如果要动尾结点,就必须挨个遍历,找到倒数第二个结点.
  • 如果要动中间结点,往往也是需要先找到中间结点的前一个结点.

也就是说,如果想要修改一些东西,就必须得遍历一下链表,这就导致很憋屈,特别是尾插和尾删,明明只需要修改最后一个,却必须全部遍历,而这也就是双链表的诞生原因.他完美的解决了单链表的问题.

双链表的结构图示

既然知道了双链表解决的是单链表的哪些问题,我们不妨猜猜双链表到底什么结构呢?

  • 解决了想要动尾结点不需要遍历的问题— — — — 那尾结点一定有被指向,被谁指向呢?两种可能:
    • 被尾结点前一个指向
    • 被头结点指向
  • 解决了修改中间结点不需要遍历的问题— — — — 那说明该结点的前一个结点一定有被指向,被谁指向呢?两种可能:
    • 被所需要动结点的前两个结点指向
    • 被所需要动结点指向

所以,他的结构大致应该是下面这样,注意哦~~~,博主说的是大致,因为有一些小细节需要单独拎出来说的:

image-20210804092344575

嗯~~~,这样完全符合我们的要求,但实际上 双向循环链表的真正结构,也是这样,只是比起这个结构多了一个无效数据头结点,这是为了方便头插头删.

真正的双向链表结构:

image-20210804095817548

项目的建造


还是老规矩,博主用的vs2019,就仍然用它进行演示.

我们的目的是要实现双链表,那我们现在就需要3个文件,分别是List.h ,List.c, test.c

作用分别是: 结构体的定义,头文件的引用,函数声明, 函数定义,函数测试


如图:

image-20210804144831112

双链表结点的定义

上图中,双链表的结构图示我们已经非常清楚了,现在我们就需要按照图示用代码进行实现了.

图示中,一个结点的内容有哪些?

  • 一个指向前面结点的指针
  • 一个存储数据的空间
  • 一个指向后面结点的指针

List.h文件中定义双链表结构

typedef int LTDataType; //我们并不知道要存储什么数据,便以int为例. 之所以用typedef改名是为了以后不用int,修改更方便

typedef struct ListNode
{ 
          
    struct ListNode* prev;
    LTDataType data;
    struct ListNode* next;
}LTNode; //修改个更短的名字

双链表的各种方法实现


顺序表有顺序表的各种操作函数,单链表有单链表的各种操作,而我们的双链表同理,也是有自己的各种操作.

那有什么样子的操作呢? 博主全部列举在下面,后面将会一一进行实现.


List.h文件中声明各种方法

LTNode* BuyListNode(LTDataType elem);                     //创建一个新链表结点
void ListInit(LTNode** pphead);   						  //初始化
bool ListEmpty(LTNode* phead);    						  //判断链表是否为空
size_t ListSize(LTNode* phead);   						  //返回链表元素数量
void ListPrint(LTNode* phead);    						  //打印链表内容

void ListPushBack(LTNode* phead, LTDataType* elem);       //尾插
void ListPushFront(LTNode* phead, LTDataType* elem);      //头插
void ListPopBack(LTNode* phead);                          //尾删
void ListPopFront(LTNode* phead);                         //头删

LTNode* ListFind(LTNode* phead,LTDataType elem);          //查找元素elem,并返回elem所在结点
void ListInsert(LTNode* pos,LTDataType elem);             //在结点pos之前插入elem,一般配合ListFind使用
void ListErease(LTNode* pos);                             //删除pos结点,一般配合ListFind使用
void ListDestroy(LTNode* phead);  						  //销毁双链表

双链表之新建结点

结点的作用是干什么呢? 没错, 那就是存储数据,所以新建结点首要目的就是把数据存储

LTNode* BuyListNode(LTDataType elem)
{ 
            
    LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    if(newnode == NULL)
    { 
            
        perror("错误原因:");
        exit(-1);
    }
    newnode->data = elem;
    newnode->prev = NULL;
    newnode->next = NULL;
    return newnode;
}

双链表之初始化

既然是初始化,那我们要初始化什么呢? 我们知道双链表是具有一个无效数据头结点的,所以我们初始化要做的事情就是

给无效数据头结点创立一个空间,并随机给个值,然后让其前后指针都指向着自己,这样就符合 双向循环链表的结构

//大家注意哦~,博主这里用的是二级指针,为什么呢?
//因为实参是一个一级指针
//而如果形参是一级指针,这就属于值传递了,函数内pphead的值改变并不会影响外面的实参,而初始化是需要修改实参的.
void ListInit(LTNode** pphead)
{ 
            
    assert(pphead); //pphead一定不能是空指针
    
    (*pphead) = BuyListNode(-1); //随机给一个值用来创建结点
    
    (*pphead)->prev = *pphead;
    (*pphead)->next = *pphead;
}

测试是否成功:

image-20210804153252046

通过调试可以发现,plist存储的数据是-1,前指针指向了plist自己,后指针plist也指向了自己.成功!!!


双链表之判空

双链表判空,判断的是哪一部分呢? 没错,判断的是头结点的后面部分,即排除了无效数据头结点的部分,如图:

image-20210804153759020

所以我们只需要判断就是头结点之后(phead->next)的结点值是不是phead,原因:

  • 当链表为空时候,就只剩下一个无效头结点.— — —即phead->next 等于 phead
  • 当链表不为空时候,就有有效结点— — —即phead->next 不等于 phead
bool ListEmpty(LTNode* phead)
{ 
              
    assert(phead); //phead不能为空
    
    return phead->next == phead; 
}

测试是否成功:

image-20210804155615159

1代表空,正确,因为这个时候除了头结点自己,就没有任何结点

双链表之求具体元素数量

同样,我们求取元素数量求取的还是头结点之后.

size_t ListSize(LTNode* phead)
{ 
              
    assert(phead);
    size_t sum = 0;
    LTNode* cur = phead->next; //从头结点之后开始遍历
    
    while(cur != phead) //当cur再次为phead时候,说明链表已经遍历完毕.
    { 
              
        sum++;
        cur = cur->next;
    }
    
    return sum;
}

测试是否成功:

image-20210804155545634

成功!!!

双链表之打印链表内容

一样,我们打印的还是有效数据,即头结点之后的数据

void ListPrint(LTNode* phead)
{ 
              
	assert(phead);
	LTNode* cur = phead->next;

	while (cur != phead)
	{ 
              
		printf("%d--->", cur->data);
		cur = cur->next;
	}
	printf("|完毕|\n");
}

双链表之尾插


终于到这里了,这就是我们的重头戏部分了.

还记得我们为什么有双链表吗?那就是尾插时候不需要从头遍历到尾,才能找到尾结点.

我们定义的这个双向链表的尾结点怎么找到呢? 没错 , phead->prev


那么我们尾插的步骤是什么呢? 请看图:

1

  • 第一步,创建新结点存储数据
  • 第二步,原尾结点和现在新结点互相链接
  • 第三步,新结点和头结点互相链接
void ListPushBack(LTNode* phead, LTDataType* elem)
{ 
                
    assert(phead);
    
    LTNode* tail = phead->prev;  //找到尾结点
    LTNode* newnode  = BuyListNode(elem); //创建新结点
    
    //原来尾结点和现在新结点链接
    tail->next = newnode;
    newnode->prev = tail;
    
    //新结点和头结点链接
    phead->prev = newnode;
    newnode->next = phead;
}

测试是否成功

image-20210804163905502

成功!!!

双链表之尾删


老规矩,到底怎样操作呢?,先上图!


1

  • 第一步,找到尾结点和倒数第二个结点
  • 第二步,释放尾结点
  • 第三步,头结点和倒数第二个结点连接
void ListPopBack(LTNode* phead)
{ 
                  
    assert(phead);
    assert(ListEmpty(phead)); //如果链表为空就不能删除.
    
    LTNode* tail = phead->prev;//尾结点
    LTNode* tail_prev = tail->prev;//倒数第二个结点
    
    free(tail);//释放尾结点
    tail = NULL;
    
    tail_prev -> next = phead; //头结点和倒数第二个结点连接
    phead->prev = tail_prev;
}

测试是否成功

image-20210804165944188

成功!!

双链表之头插


头插怎样操作?? 还是老规矩,先上图?


1

  • 第一步,创建新结点存储数据
  • 第二步,新结点与头结点后的首结点链接
  • 第三步,新结点与头结点链接
void ListPushFront(LTNode* phead, LTDataType* elem)
{ 
                    
    assert(phead);
    
    LTNode* newnode  = BuyListNode(elem); //创建新结点存储数据
    LTNode* Second_first = phead->next; //头结点后的首结点.
    
    //新结点 与 头结点后的首结点 链接
    Second_first ->prev = newnode;
    newnode->next = Second_first;
    
    //新结点与头结点链接
    phead->next = newnode;
    newnode->prev = phead;
}

测试是否成功:

image-20210804171903472

成功!!!

双链表之头删


怎样头删? img还是老规矩,先画图看清楚步骤!


1

  • 第一步,头结点与头删结点后的结点连接
  • 第二步,释放头删结点
void ListPopFront(LTNode* phead)
{ 
                      
    assert(phead);
    assert(!ListEmpty(phead)); //如果为空就无法删除
    LTNode* Front_second = phead->next->next;//头删结点后的结点
    LTNode* front = phead->next; //头删结点
    
    //连接
    phead->next = Front_second;
    Front_second->prev = phead;
    
    //释放头删结点
    free(front);
    front = NULL;
}

测试是否成功

image-20210804175009409

成功!!

双链表之查找值


这个题比较简单,直接遍历查找就行,然后返回该结点值


LTNode* ListFind(LTNode* phead,LTDataType elem)
{ 
                        
    assert(phead);
    LTNode* cur = phead->next;
    while(cur != phead)
    { 
                        
        if(cur->data == elem)
        { 
                        
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

双链表之任意位置插入值


任意位置插入值的步骤和其实和头插的步骤一模一样,只是头插是在头结点后面的首结点前插入.

而任意位置前插入是在给定的pos结点之前插入,所以这里博主就不画图了,大家可以看着头插的图进行理解.


void ListInsert(LTNode* pos,LTDataType elem) //还记得我们最开始声明时候说的,这个函数需要和ListFind配合吗?请看:
{ 
                          
    assert(pos);
    
    LTNode* pos_prev = pos->prev;  //保存pos结点之前的结点
    
    LTNode* newnode = BuyListNode(elem); //创建新结点
    
    //新结点与pos链接
    newnode->next = pos;
    pos->prev = newnode;
    
    //新结点与原来pos之前的结点连接
    pos_prev->next= newnode;
    newnode->prev = pos_prev;
    
}

测试是否成功

image-20210804181855458

成功!!!

双链表之任意位置删除


任意位置删除和尾删的步骤一模一样,只是尾删删除的是尾结点,连接的是原来尾结点前面结点和头结点

而任意位置删除的是我们给的结点pos,连接的是pos前后的结点,一样,大家可以去看尾删的动图,博主就不再画了


void ListErease(LTNode* pos)
{ 
                            
    assert(pos);
    
    LTNode* prev = pos-> prev; //pos 之前结点
    LTNode* next = pos->next;  //pos 之后结点
    
    free(pos);
    pos = NULL;
    
    prev->next = next;
    next->prev = prev;
}

测试是否成功:

数据结构之双链表

成功!!!

双链表之销毁空间

挨个释放即可

void ListDestroy(LTNode* phead)
{ 
                            
    assert(phead);
    
    LTNode* cur = phead->next;
    while(cur != phead)
    { 
                            
        LTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    
    free(cur);
    cur = NULL;
}

测试:

image-20210804185551853

成功~~~



版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/231094.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • linux命令hexdump,Linux系统中hexdump的命令汇总

    linux命令hexdump,Linux系统中hexdump的命令汇总Linux系统中hexdump的命令汇总hexdump是Linux系统中用来查看文件十六进制编码的命令,配合不同的参数其作用也有所不同,下面小编就给大家介绍下Linux中hexdump命令的用法,不了解的`朋友不妨来学习一下。查看一些二进制文件的内容,比如二进制文件中包含的某些字符串。可以将二进制文件转换为ASCII、10进制、16进制或8进制进行查看。-b每一字节以八进制显示,一行共16个字节…

    2025年11月27日
    5
  • 支持向量回归(SVR)的详细介绍以及推导算法

    支持向量回归(SVR)的详细介绍以及推导算法1SVR背景2SVR原理3SVR数学模型SVR的背景SVR做为SVM的分支从而被提出,一张图介绍SVR与SVM的关系这里两虚线之间的几何间隔r=d∣∣W∣∣\frac{d}{||W||}∣∣W∣∣d​,这里的d就为两虚线之间的函数间隔。(一图读懂函数间隔与几何间隔)这里的r就是根据两平行线之间的距离公式求解出来的SVR的原理SVR与一般线性回归的区别SVR一般线性回归1.数据在间隔带内则不计算损失,当且仅当f(x)与y之间的差距的绝对值大于ϵ\

    2022年6月6日
    46
  • tar命令的详解

    tar命令的详解

    2021年12月6日
    39
  • ActiveMQ

    ActiveMQ

    2021年7月8日
    107
  • mysql事务隔离级别可重复读_innodb默认隔离级别

    mysql事务隔离级别可重复读_innodb默认隔离级别一般的DBMS系统,默认都会使用读提交(Read-Comitted,RC)作为默认隔离级别,如Oracle、SQLServer等,而MySQL却使用可重复读(Read-Repeatable,RR)。要知道,越高的隔离级别,能解决的数据一致性问题越多,理论上性能损耗更大,可并发性越低。隔离级别依次为>:串行化>RR>RC>读未提交在SQL标准中,前三种隔离级别分别解决了幻象读、不可重复读和脏读的问题。那么,为什么MySQL使用可重复读作为默认隔离级别呢?这个是有历史.

    2025年9月16日
    7
  • maven的filters目录详解

    maven的filters目录详解maven的资源过滤maven的过滤资源需要结合maven的2个定义才能实现,分别是:profileresources下面分开来做介绍。profileprofile可以让我们定义一系列的配置信息,然后指定其激活条件。这样我们就可以定义多个profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果。需要掌握profile的定义以及激活条件。后面结合

    2022年5月22日
    44

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号