链表的主要特征有是否带哨兵位头结点,是否双向,即是否有两个指针比变量存放该结点前一个结点和后一个结点,是否循环。实际上链表的组合不仅仅是图上的四种,而是一共有8种组合。前面介绍的单链表,即无头单向不循环链表,是一种最经典的链表结构。通常用于哈希桶和图的邻接表,由于结构简单通常不会用来单独存储数据。今天介绍的带头双向循环链表是一种复杂的链表结构,一般用于单独存储数据。实际使用链表单独存储数据都是用带头双向循环链表这种链表结构。下面,我就边实现边介绍这种结构的优势。
带头双向循环链表定义,首先需要定义一个存储的数据类型data,然后是两个指针,分别指向前一个结点(prev)和后一个结点(next)。
typedef int ListDataType;
//带头双向循环链表的定义
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
ListDataType data;
}LNode;
该接口实现思路如下,该接口用于创建创建哨兵位头结点,所以我们这个结点是不用来存储数据的。一开始哨兵位头结点
的next指针和prev指针都应该只想自己,这样才能够达成一个循环的效果。
// 创建返回链表的头结点.
LNode* ListCreate()
{
LNode* guard = (LNode*)malloc(sizeof(LNode));
if (NULL == guard)
{
perror("malloc fail\n");
return NULL;
}
//循环
guard->prev = guard->next = guard;
guard->data = -1;
return guard;
}
可能有些书籍上,哨兵位头结点可能会存储链表的长度。但是,设想一下这种情况可能只有当数据类型是整型是才有可能方便实现。如果链表存储的是浮点型,不就需要另外定义一个哨兵位头结点的类型。我这里实现的哨兵位头结点不存储数据,只有“站岗”的作用。
这里的销毁需要遍历一遍链表,然后依次释放结点,需要注意的是带头双向循环链表的结束条件和单链表的有所不同。单链表遍历的结束条件是当链表指向空,而带头双向循环链表遍历需要从第一个结点开始,当链表走回哨兵位头结点时,表示遍历结束。
// 双向链表销毁
void ListDestory(LNode* pHead)
{
assert(pHead);
//从第一个结点开始释放
LNode* cur = pHead->next;
while (cur != pHead)
{
LNode* next = cur->next;
free(cur);
cur = next;
}
//释放头结点
free(pHead);
}
打印无非就是遍历一遍链表,然后输出每个结点的数据。
// 双向链表打印
void ListPrint(LNode* pHead)
{
LNode* cur = pHead->next;
printf("<=head=>");
//cur==头结点结束打印
while (cur != pHead)
{
printf("<=%d=>",cur->data);
cur = cur->next;
}
printf("\n");
}
每一次插入数据都需要申请新的结点存放数据,所以我单独将申请结点这部分代码封装成接口,便于使用。
//开辟新结点
LNode* BuyNode(ListDataType x)
{
//动态开辟
LNode* newnode = (LNode*)malloc(sizeof(LNode));
if (NULL == newnode)
{
perror("malloc fail\n");
return NULL;
}
//初始化
newnode->prev = NULL;
newnode->next = NULL;
newnode->data = x;
return newnode;
}
带头双向循环链表尾插数据并不需要像单链表一样,每次都遍历链表找尾。因为,哨兵位头结点的prev指针存放着最后一个结点的地址。只需要通过prev指针就可以找到尾结点。将尾结点的next指针指向新节点,将新节点的prev指针指向原尾结点,将新节点的next指向哨兵位头结点,最后,将哨兵位头结点的prev指针指向新节点。
void ListPushBack(LNode* pHead, ListDataType x)
{
assert(pHead);
//申请结点
LNode* newnode = BuyNode(x);
//判空
if (NULL == newnode)
{
perror("malloc fail\n");
return;
}
//保存原尾结点
LNode* Headprev = pHead->prev;
//改变链表指向
newnode->prev = Headprev;
Headprev->next = newnode;
newnode->next = pHead;
pHead->prev = newnode;
}
bool ListEmpty(LNode* pHead)
{
assert(pHead);
return pHead == pHead->next;
}
尾删实现思路如下,想将尾结点的地址保存在一个指针变量中。使原尾结点的prev指针的next指针指向哨兵位头结点,使哨兵位头结点的prev指向原尾结点的prev(即新结点),释放原尾结点。
// 双向链表尾删
void ListPopBack(LNode* pHead)
{
assert(pHead);
//空表不可删除
assert(!ListEmpty(pHead));
//保存原尾结点
LNode* prevnode = pHead->prev;
//尾删
pHead->prev = prevnode->prev;
prevnode->prev->next = pHead;
//释放动态内存
free(prevnode);
prevnode = NULL;
}
带头双向循环链表的头插的实现思路如下,这里我定义一个临时结点来保存哨兵位头结点的下一个结点,然后开辟一个新节点,将新节点的next指向临时结点,让临时结点的prev指向新节点,让新节点的prev指向哨兵位的头结点,最后,让哨兵位头结点的next指向新结点即可。
// 双向链表头插
void ListPushFront(LNode* pHead, ListDataType x)
{
assert(pHead);
//开辟空间
LNode* newnode = BuyNode(x);
//临时结点存放哨兵位的下一个结点
LNode* first = pHead->next;
//头插
first->prev = newnode;
newnode->next = first;
newnode->prev = pHead;
pHead->next = newnode;
}
先定义一个临时结点保存第一个结点,然后让pHead的next指向临时结点的next,再让临时结点的next的prev指向pHead,最后释放掉第一个结点达成头删。
// 双向链表头删
void ListPopFront(LNode* pHead)
{
assert(pHead);
//空表不可删除
assert(!ListEmpty(pHead));
LNode* first = pHead->next;
//头删
pHead->next = first->next;
first->next->prev = pHead;
free(first);
first = NULL;
}
实现pos位置前插入可以先定义一个临时变量存放pos的前一个结点,然后改变pos和pos前一个结点的指向关系,达到pos前插入数据的效果。完成pos位置前插入这个接口便可以根据该接口复用达到头插尾插的效果
// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, ListDataType x)
{
assert(pos);
LNode* newnode = BuyNode(x);
LNode* posprev = pos->prev;
posprev->next = newnode;
newnode->prev = posprev;
newnode->next = pos;
pos->prev = newnode;
}
完成insert接口后,便可以对头插和尾插接口的实现用该接口进行复用,需要注意的是尾插其实就是在哨兵位头结点的prev指针前插入数据,头插就是在哨兵位头结点的next前插入数据。
定义两个临时结点保存pos位置前和pos位置后的结点,然后让pos前的结点的next指向pos后的结点,让pos后的结点的prev指向pos结点前的结点。
// 双向链表删除pos位置的节点
void ListErase(LNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
LNode* prev = pos->prev;
LNode* next = pos->next;
//删除
prev->next = next;
next->prev = prev;
//请手动置空
free(pos);
}
当然,完成本接口后,可以用本接口对实现头删尾删进行接口的复用。尾删其实就是删除在哨兵位头结点的prev指针指向的数据,头删就是删除哨兵位头结点的next处数据。