双向链表分类
根据链表的分类组合方式,双向链表包含:不带头非循环双向链表;带头非循环双向链表;不带头循环双向链表;带头循环双向链表。
图例:
不带头双向非循环链表
带头双向非循环链表
不带头双向循环链表
带头双向循环链表
1. 带头双向循环链表概述
在单链表那篇文章中我们使用c语言实现了无头单向非循环链表,本文将实现带头双向循环链表,在诸多链表结构中,上述两种链表在实际生活与面试题中最为常见。
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单。
2. 剖析带头双向循环链表
无头单向非循环链表结构简单,但代码实现复杂,而带头双向循环链表结构复杂,但代码实现简单,这是为什么呢?
让我们一同寻求真相。
如图所示
无头单向非循环链表:
无头单向非循环链表 结构极为简单,但每个节点只存储了下一节点的地址,假设phead指针指向首元节点,那么phead就只能按照d1->d2->d3这样的路径前进,当到达尾节点,就要重新从首元节点开始。同时因为没有头节点,因此进行插入删除等操作时都需要判断链表是否为NULL。
带头双向循环链表:
带头双向循环链表结构虽然复杂,每个节点存储了下一节点与上一节点的地址,并且较之无头单向非循环链表多了一个头节点,但代码实现极为简单。首先,无论从哪一个节点,都能迅速进行操作,不必担心路径问题,比如从头结点只需要进行 head=head->prev,此时head就指向了d3尾节点。同时头节点的存在让对链表操作时更是少了很多麻烦,无论何时,链表都存在一个节点,因此不需要考虑链表为NULL的情况,但要注意,不能对头节点进行增删查改等操作。
3. 带头双向循环链表的代码实现
需要实现的接口函数
DoubleList.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
ListNode* ListCreate(LTDataType x);// 创建返回链表的头结点.
ListNode* InitList();//初始化链表
ListNode* ListDestory(ListNode* pHead);// 双向链表销毁
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
//ListNode* ListErase(ListNode* pHead,ListNode* pos);
void ListErase(ListNode* pHead, ListNode* pos);
DoubleList.c
#include"DoubleList.h"
ListNode* ListCreate(LTDataType x)
{
assert(x);
struct ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* InitList()
{
struct ListNode* pHead = ListCreate(-1);
pHead->next = pHead;
pHead->prev = pHead;
return pHead;
}
// 双向链表销毁
ListNode* ListDestory(ListNode* pHead)
{
assert(pHead);
struct ListNode* cur = pHead->next;
while (cur != pHead)
{
struct ListNode* tmp = cur->next;
free(cur);
cur = tmp;
}
free(pHead);
pHead = NULL;
return pHead;
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
struct ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead, x);
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListErase(pHead, pHead->prev);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead->next, x);
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListErase(pHead, pHead->next);
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
assert(x);
struct ListNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
struct ListNode* Prev = pos->prev;
struct ListNode* newnode = ListCreate(x);
Prev->next = newnode;
pos->prev = newnode;
newnode->next = pos;
newnode->prev = Prev;
}
//双向链表删除pos位置的节点
void ListErase(ListNode* pHead, ListNode* pos)
{
assert(pos);
assert(pos != pHead);
struct ListNode* Prev = pos->prev;
struct ListNode* Next = pos->next;
Prev->next = Next;
Next->prev = Prev;
free(pos);
}
以上是各接口函数的具体实现。
代码注意事项
<> 由于双向链表的在指定位置插入(删除),与头插尾插(头删尾删)实现功能重复且代码的书写没有区别,因此头插尾插(头删尾删)调用了在指定位置插入(删除)的函数。
<> 如果链表中只有一个头节点,那么他的prev与next都应该指向自己。
<>有关两个链表的实现,老铁们应该发现了函数传参的不同,这个涉及传参概念。
由于单链表并没有头节点,因此需要创建第一个节点,然后让头指针指向它,因此phead指针需要改变,而形式传参并不会改变函数外实际参数的值,因此我们需要地址传递,传入phead指针的地址,因此传入二级指针。
但本文的双链表有头节点,头指针始终指向头节点,并不需要改变phead的指向,也就是说不用改变函数外实际参数的值,因此值传递即可,传入参数本身。
感谢观看,期待大佬们的指正!