【初阶数据结构与算法】线性表之链表的分类以及双链表的定义与实现

在这里插入图片描述

文章目录

  • 一、链表的分类
  • 二、双链表的实现
    • 1.双链表结构的定义
    • 2.双链表的初始化和销毁
      • 初始化函数1
      • 初始化函数2
      • 销毁函数
    • 3.双链表的打印以及节点的申请
      • 打印函数
      • 节点的申请
    • 4.双链表的头插和尾插
      • 头插函数
      • 尾插函数
    • 5.双链表的查找和判空
      • 查找函数
      • 判空函数
    • 6.双链表的头删和尾删
      • 头删函数
      • 尾删函数
    • 7.双链表指定节点位置的操作
      • 删除指定的节点
      • 在指定节点前插入数据
  • 三、单链表和双链表的简单对比

一、链表的分类

   在上一篇中,我们简单了解了单链表,但是我们没有仔细的对链表的分类进行分析,因为我们是第一次接触到链表这种结构,所以我们先简单了解一下单链表,实现一下,现在才能对我们链表的分类有清晰的认知
   接下来我们来了解一下链表的具体分类,然后从分类中找出我们上节课实现的单链表,如下:
【初阶数据结构与算法】线性表之链表的分类以及双链表的定义与实现_第1张图片
   在上面的属性中,让它们进行组合一共就会有8种分类,比如带头单向循环链表,带头单向不循环链表,带头双向循环链表等等,这里就不一一列举了,我们主要来解释一下分类里面的每组名词是什么意思

  • 带头和不带头:带头和不带头不是我们之前说的头结点,之前我们的头结点是链表中存储数据的第一个节点,这里的带头是我们在创建链表时会申请一个头结点,这个头结点不存放数据,它只代表链表的头,无论我们进行删除还是增加都让它指向链表的头,它也叫哨兵位
  • 单向和双向:单向链表的意思就是它只有一个next指针指向后节点,前节点可以通过next指针找到后节点,但是后节点找不到前节点。双向链表的意思就是它不仅有一个next指针指向后节点,还有一个prev指针指向前节点,前后节点可以互相访问
  • 循环和不循环:不循环链表就是它的尾结点指向空指针,不会产生循环。循环链表就是它的尾结点指向头结点,这样的话整个链表就形成了循环

   根据上面的分类和各种名词的解释,我们现在来判断一下我们上节课讲的单链表属于哪个分类,首先我们没有创建一个不保存数据的头节点一直指向链表头不改变,之前说的头结点是要存放数据的,并且之前的头结点有可能被改变,所以并不属于带头的链表
   我们实现单链表的结构时,只有一个next指针指向下一个节点,没有prev指针指向上一个节点,所以我们可以判断出单链表属于单向的链表
   最后,由于我们实现的单链表的尾结点指向空,所以它是不循环链表,最后综合一下上面的分析,我们上一篇实现的单链表其实全称应该是单向不带头不循环链表
   所以我们现在就知道了,通常说的单链表虽然只说了单,但是其实它完整的名字是单向不带头不循环链表,那么我们今天要学习的双链表属于哪个类别呢?
   这里就不卖关子了,我们平常说的双链表跟单链表完全是两个反面,它属于双向带头循环链表,我们在实际应用中最常用的也是双向链表,因为它的每个方法的时间复杂度都基本达到了O(1),效率比较高,只是多了一点点空间的开销,接下来我们就来学习双链表的实现

二、双链表的实现

   在上面我们已经说过了,平常所说的双链表就是双向带头循环链表,它的特点我们上面已经介绍过了,接下来我们就来实现它,它的实现和单链表的实现的思路差不多,如果吃透了单链表,双链表的实现就不难了

1.双链表结构的定义

   我们在上面说过,双链表属于双向链表,不仅有一个指向下一个节点的next指针,还有一个指向上一个节点的prev指针,其余和单向链表的定义差不多,如下:

typedef int LTDateType;

typedef struct ListNode
{
   
	LTDateType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

2.双链表的初始化和销毁

   我们之前写的单链表没有初始化,但是双链表是有初始化的,因为我们说过,双链表的是带头的,初始化的目的就是为了给我们的双链表申请一个不保存数据的哨兵位

初始化函数1

   初始化函数就是为了帮我们申请一个哨兵位节点,所以它可以有两种方式,一种就是我们在主函数中创建好一个节点指针,默认置为空,然后我们通过传参的方式将这个指针传给初始化函数,由初始化函数给它申请哨兵位
   在初始化的时候我们要注意一点,就是我们的双向链表属于循环链表,不能把它的prev和next指针置为空,要把它们都指向哨兵位自己,否则就不循环了,至于哨兵位的数据部分是什么都不重要,可以不管,最后由于我们会改变这个指针,所以我们要传二级指针,如下:

void LTInit1(LTNode** pphead)
{
   
	assert(pphead);
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if(*pphead == NULL)
	{
   
	    perror("malloc");
	    return;
	}
	(*pphead)->next = (*pphead)->prev = *pphead;
}

初始化函数2

   还有另一种方式就是,不用接收任何参数,直接在初始化函数中创建新节点,初始化后将其返回即可,在主函数中直接接收即可,我们也推荐使用这种方式,具体原因后面再说,代码如下:

LTNode* LTInit2()
{
   
	LTNode* newnode = (LTNode*)malloc(

你可能感兴趣的:(链表,数据结构,算法,c++,c语言,java,python)