基于单链表的一些缺点,这里对双链表进行一个实现。
一、带头双向循环链表
结构图大概如下,每个结点有两个指针域,一个数据域。指针分别指向前一个和后一个结点。尾结点的下一个是头结点,头结点的前一个是尾结点。
双链表可以往前找也可以往后找,虽然它结构复杂,但是代码实现确实很简单,这也源于它复杂结构。
头结点是不存储有效数据的。它只起到一个链接首尾结点的作用。
二、常用接口
双链表的结构体如下
typedef int SLTDataType;
typedef struct SLTNode
{
struct SLTNode* prev;//前驱指针
struct SLTNode* next;//后继指针
SLTDataType val;
}Node;
1、初始化和销毁
初始化双链表就是开辟一个头结点并返回,只有一个结点时,链表的前一个和后一个都是自己。
销毁时,遍历链表释放其余结点后,在释放头结点。
//初始化
Node* SLTInit()
{
//创建头结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("malloc,failed!");
return NULL;
}
newNode->prev = newNode;
newNode->next = newNode;
return newNode;
}
//销毁链表
void SLTDestroy(Node* phead)
{
assert(phead);
//phead的下一个不是自己,说明还有结点没释放
while (phead->next != phead)
{
Node* front = phead->next;
front->next->prev = phead;
phead->next = front->next;
free(front);
}
free(phead);//最后释放头结点
phead = NULL;
}
销毁链表的过程
2、头插和尾插
对于头插,可以看成是在头结点后插入一个数据。插入只需要改变四个指针的指向即可,但需要注意顺序。
//头插
void SLTPushFront(Node* phead, SLTDataType x)
{
assert(phead);
//创建结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("malloc,failed!");
return;
}
newNode->val = x;
//修改指针指向
Node* front = phead->next;
newNode->next = front;
newNode->prev = phead;
front->prev = newNode;
phead->next = newNode;
}
对于尾插,可以看成在尾结点后插入,也可以看成在头结点(head)前插入,因为head的前驱结点就是尾。
代码与修改指向和头插类似,这里就不画图带大家理解了。
//尾插
void SLTPushBack(Node* phead, SLTDataType x)
{
assert(phead);
//创建结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("malloc,failed!");
return;
}
newNode->val = x;
//修改指针指向
Node* tail = phead->prev;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
tail->next = newNode;
}
3、头删和尾删
头删,即删除第一个结点,也就是头结点的下一个,但是不包括头结点。
但不能直接释放,因为第一个结点存储第二个结点的地址。直接释放会导致找不到第二个结点,所以需要保存一下第二个结点的地址。
可以删除的前提必然是链表不为空。
//头删
void SLTPopFront(Node* phead)
{
assert(phead);
//链表不为空
if (!SLTEmpty(phead))
{
Node* front = phead->next;
phead->next = front->next;
front->next->prev = phead;
free(front);
}
}
next是一个地址,=是赋值,改变指针的指向就是修改结构体内指针的值,也就是修改地址。
同样,尾删可以看成是删除尾结点或者删除头结点的前一个。删除方法和头删类似。
//尾删
void SLTPopBack(Node* phead)
{
assert(phead);
//链表不为空
if (!SLTEmpty(phead))
{
Node* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
tail = NULL;
}
}
4、pos位置插入和删除
pos是一个结构体指针,插入是在pos前插入,删除就直接删除pos位置的结点。
插入:和头插尾插类似,只需要修改四个指针的指向即可。
//pos位置前插入
void SLTInsert(Node* phead, SLTDataType x, Node* pos)
{
assert(phead);
assert(pos);//插入位置不能为NULL
//创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("malloc,failed!");
return;
}
newNode->val = x;
//修改指针指向
Node* prev = pos->prev;
newNode->next = pos;
newNode->prev = prev;
pos->prev = newNode;
prev->next = newNode;
}
删除:同头删和尾删,先改变指针指向,在释放pos位置的结点。
有了头删和尾删的基础,pos位置的删除也会更容易理解。
//删除pos位置数据
void SLTErase(Node* phead, Node* pos)
{
assert(phead);
//删除位置要合理,不能删除头结点
assert(pos&&pos!=phead);
Node* prev = pos->prev;
Node* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
4、查找和判空
查找:给一个值,查找链表内有无该数据,有就返回该结点的地址,没有就返回NULL;
遍历链表并做判断即可。
//查找数据
Node* SLTFind(Node* phead, SLTDataType x)
{
Node* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
return cur;
cur = cur->next;
}
printf("数据不存在!\n");
return NULL;
}
判空:链表内只有一个头结点时,为空链表。只需要判断头结点的下一个或前一个 是不是自己即可。
//判断空链表
bool SLTEmpty(Node* phead)
{
assert(phead);
if (phead->next == phead)
return true;
return false;
}
三、链表和顺序表的优缺和区别
对于链表和顺序表,都有一定的优略。
它们的不同大致有以下这些。
关于链表的介绍,就到这里啦。