带头双向循环链表的概念
双向链表是一个结构体内有两个指针,一个指针指向头,一个指向尾。所以给双向链表一个位置,可以很轻松的访问前面的节点与后面的节点。双向链表的结构复杂,但是代码实现会有很有的优势,实现反而简单。
今天简单的实现一下带头双向循环链表的增删查改与初始化。
双向链表的初始化与结构体定义
结构体定义
因为叫双向链表所以需要两个指针,一个指向前面的节点,一个指向后面的节点。
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDateType data;
}ListNode;
初始化
带头双向循环列表需要一个哨兵位,所以进行初始化的时候需要范围链表哨兵位的地址。可以进行二级指针传参,直接修改地址,因为接下来的代码都采用一级指针,为了整体规范性,没有采用二级指针。链表结构为循环,进行初始化的时候要让他自己指向自己。图与代码如下:
ListNode* ListCreate()
{
ListNode* newnode = NewNode();
newnode->prev = newnode;
newnode->next = newnode;
return newnode;
}
双向链表的头插
对双向链表进行头插需要开辟一个新的节点:
让哨兵位head指向新节点newnode,让newnode指向next,next指向newnode。实现如下图的连接:
在这里为什么定义了一个next的变量呢?
因为定义了next的变量后,可以不用考虑顺序,直接指向newnode与head,如果不定义next的变量,就是用newnode与head的话,需要考虑指向的先后顺序,防止陷入自己指向自己。
代码如下:
单向链表与双向的区别
双向链表的头插与单向不带头不循环链表有一个蛮大的区别:
双向链表的头插不需要考虑空指针,可以直接指向下一个节点,单向不带头不循环链表需要考虑刚开始进来的指针是空指针的问题,而且需要用二级指针改变地址。单向链表的代码如下:
双向链表的尾插
尾插与头插差别不是很大,定义一个tail指针变量:
将next的指针变量等于tail的地址。就可以不用按照顺序的进行指向。代码如下:
双向链表的头删
对链表进行头部的删除,最方便快捷的方式就是定义两个变量,一个记录头部的位置(准备释放空间),一个记录新头部的位置。这样对链表进行头删的时候可以不考虑顺序,直接进行指向。另外,因为链表节点的地址都是动态开辟的,删除的时候需要释放。
链表头删与尾删都需要注意的一点是:链表只有哨兵位的情况下,是不用进行删除的,所以要进行一个判断,判断该链表是不是非空。非空就进行删除。链表空的时候heap也在其中。也就是链表为空不计算head。
图如下:
代码如下:
因为链表的头删与尾删实现的思想是差不多的,所以这里不再介绍尾删了。
双向链表的查找
对链表查找就需要对链表进行遍历。对链表进行遍历的话,他的结束条件是等于哨兵位。所以我们需要创建一个变量。如图:
令该变量等于head的下一个节点,当值等于所要查找的时候,就让他进入下一个节点进行查找。
当cur与head进行重合就说明里面没有要查找的值,结束循环。代码如下:
双向链表的插入
在某一个位置前进行插入数据。双向链表虽然结构复杂,但是很好进行代码编写。在某一个为之前,直接去查找节点的prev就可以记录之前得到位置。然后进行指向的更改即可。图如下:
first指针变量就是pos->prev中存放的地址。
更改后的图:
代码如下:
当我们写出这段代码后,可以对头插,尾插代码进行更改,在头插、尾插函数中调用这个函数一次性进行实现。
用该代码进行头插就是在pos=head->next位置前插入。图如下:
更改后如下:
代码如下:
尾插就是pos=head进行插入。
图如下:
因为是循环,尾的后面就是头,那么头的后面就是尾。
代码如下:
双向链表的打印
链表进行打印的结束条件就是不能等于哨兵位。等于哨兵位说明链表要么为空,要么打印完成。
代码如下:
双向链表的销毁
既然是动态开辟的空间,就需要进行空间销毁。不然会内存泄露。
对空间进行释放后 ,记得在函数外手动置空。