之前我们讲过单链表的定义和实现,这里就从这开始往后讲,如果不知道的话可以看看前一篇博客。
链表的分类
链表可分为单链表和双链表,单链表和双链表又可以分为带头结点的和不带头结点的链表,还有循环和不循环的链表,根据使用情况不同,衍生出了八种链表。

单链表和双链表
双链表与单链表相比,多了一个指向前驱结点的指针prev。


带头结点和不带头结点
带头结点和不带头结点就是带不带哨兵位的链表。




循环和不循环
循环链表和不循环链表的区别在于,循环链表的尾结点的next指针指向的不为NULL,指向的是第一个结点,如果是带哨兵位的链表则指向哨兵结点。

我们知道,单链表实现插入操作时要存储前一个结点,循环双链表不用,通过prev指针就能找到前一个结点,不论是头插还是尾插效率都很高。

因为这八种链表都是类似的,我们之前实现过带哨兵位的单链表的接口函数,这里实现一下带哨兵位的循环双链表的接口函数,其他链表就会变得很容易上手。
带哨兵位的循环双链表结点的定义
循环双链表的英语为Circular doubly linked list。这里我们就这样命名:
c
typedef int DCLDataType;//数据类型重命名
typedef struct DCListNode
{
DCLDataType data;//存放结点数据
struct DCListNode* prev;//指向前驱的指针
struct DCListNode* next;//指向后继的指针
}DCLNode;
带哨兵位的循环双链表
和之前一样,我们定义一个源文件test.c用来对接口函数功能进行简单测试,一个源文件DCList.c用来实现接口函数,一个头文件DCList.h来声明结构体和函数。
接口函数的定义
我们主要实现这些接口函数:
c
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int DCLDataType;//数据类型重命名
typedef struct DCListNode
{
DCLDataType data;//存放结点数据
struct DCListNode* prev;//指向前驱的指针
struct DCListNode* next;//指向后继的指针
}DCLNode;
//申请结点
DCLNode* BuyNewDCLNode(DCLDataType x);
//初始化链表
DCLNode* DCListInit();
//打印链表
void DCListPrint(DCLNode* L);
//销毁链表
void DCListDestroy(DCLNode* L);
//查找下标为i的结点
DCLNode* DCListGetElem(DCLNode* L,int i);
//查找值为x的结点,没有则返回NULL
DCLNode* DCListLocateElem(DCLNode* L, DCLDataType x);
//在指针pos后的位置插入新结点
void DCListInsert(DCLNode* pos, DCLDataType x);
//删除pos位置上的结点
void DCListDelete(DCLNode* pos);
//头插和尾插
void DCListInsertFront(DCLNode* L, DCLDataType x);
void DCListInsertBack(DCLNode* L, DCLDataType x);
//头删和尾删
void DCListDeleteFront(DCLNode* L);
void DCListDeleteBack(DCLNode* L);
初始化
初始化链表和单链表一样,就是申请创建一个头结点,然后返回该结点。
所以这里我们还是要定义一个函数来申请创建一个结点。

这里我们让该结点指向自己是保证这是一个循环双链表的结构。

然后就可以初始化链表了。

-1标记这是哨兵结点,把该结点返回给链表,链表就初始化完成了。
打印
打印就是从第一个有效结点开始打印,我们用cur来存当前结点,因为链表用来不会指向空,所以判断条件是当cur等于哨兵结点L的时候,打印就结束了。

销毁
销毁链表也就是释放该链表的每个结点。最后记得释放掉头结点L。

查找
查找也是分为按值查找和按下标查找。
按值x查找,如果找到则返回该结点,找不到则返回NULL。

按下标查找,返回该结点。所以我们就要确保寻找的下标位置的可行性。

插入
在指针pos后插入一个新结点,关键就在于处理好这个位置前后结点和新结点的prev指针和next指针。


这里我们用newnode表示新结点,先改新结点的next,newnode->next=pos->next。然后让后一个结点的prev指向新结点,pos->next->prev=newnode。
再让新结点的prev指向pos,pos指向新结点就好了。newnode->prev=pos.
pos->next=newnode.

复用该函数就能实现头插和尾插了。

头结点L的prev指向的就是尾结点。
我们简单测试一下头插和尾插的功能。

函数功能正常。
删除
删除pos位置上的结点,删除也很简单,就让pos的前一个结点的next指向pos的后一个结点,pos的后一个结点的prev指向pos的前一个结点,再释放掉pos就好了。

头删和尾删就可以复用该函数了。

我们这里还要确保链表中不是只有头结点,还存在其他结点,不然没有删除的必要,然后头删是L指向的第一个节点。
我们简单测试一下(链表中已经存了1 2 3 4 5 6 7 8 )。

函数功能正常。
以上就是带哨兵位的循环双链表接口函数的实现了。至于为什么不是第i个位置插入删除,而是指针pos,实际上插入或删除函数和查找第i个位置的函数这两个接口函数一起用就可以实现该功能了。