目录
一.概念与结构
双链表的全称叫”双向带头循环链表“。
双链表的头节点和之前在单链表的头节点是两个概念,实际上前面在单链表阶段称呼不严谨,只是为了我们更好的理解就直接称之为单链表的头结点。
带头链表里的头节点实际为”哨兵位“,哨兵位结点不存储任何有效元素,只是站在这里“放哨”。
二.双向带头循环链表的创建
接下来我们来实现双链表的各个接口:
//创建双向链表结点的结构
typedef int LTDatetype;
typedef struct ListNode
{
LTDatetype data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
//初始化
LTNode* LTInit();
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead,LTDatetype x);
//头插
void LTPushFront(LTNode* phead, LTDatetype x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead,LTDatetype x);
//指定位置之后插入
void LTInsert(LTNode* pos, LTDatetype x);
//删除指定位置
void LTErase(LTNode* pos);
//销毁
void LTDestroy(LTNode** pphead);
//void LTDestroy2(LTNode* phead);
1.创建双向链表结点的结构
双向链表结点的结构和单链表结点的结构是有区别的,双向链表结点不仅要储存下一个结点的位置而且还要存放上一个结点的位置。因此我们需要定义两个指针分别为prev和next,然后就是给我们的data赋值。
示例如下:
//创建双向链表结点的结构
typedef int LTDatetype;
typedef struct ListNode
{
LTDatetype data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
2.初始化双链表
我们需要创建一个头结点作为整个链表的哨兵,也就是我们说的”哨兵位“。头结点的prev指针和next指针都是指向自己的。为了避免bug我们还需要将data赋值为-1。
代码如下:
LTNode* LTInit()
{
LTNode* phead = Buynode(-1);
return phead;
}
此时我们需要定义一个函数哟用来创建一个双链表结点。
代码如下:
//创建一个结点
LTNode* Buynode(LTDatetype x)
{
//开辟空间
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
//创建成功
newnode->data = x;
newnode->prev = newnode;
newnode->next = newnode;
//返回创建好的双链表结点
return newnode;
}
此时我们的头结点就创建好了,接下来我们来对双向链表进行操作。
3.对双向链表进行尾插
首先,我们需要创建一个我们需要插入的新结点,要插入到链表的尾部我们需要将之前的尾部结点的next指针指向新的结点,然后将新的尾部结点的prev指针指向之前的尾部结点,最后我们需要将头结点的prev指针指向新的尾结点,将新的尾结点的next指针指向头结点,这样尾插就完成了。
代码如下:
void LTPushBack(LTNode* phead, LTDatetype x)
{
assert(phead);
//创建一个新结点
LTNode* newnode = Buynode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
4.打印双向链表
我们没写完一个接口都需要打印检查一下看看是否实现成功了没,这是一个好的习惯。接下来我们来实现打印双向链表。我们需要定义一个pcur指针指向头结点的下一个结点也就是我们的第一个有效数据结点。由于双向链表是个环,所以我们的跳出循环条件不能和单向链表一样,双向链表的跳出循环条件为当pcur走到我们的头结点时就跳出。我们需要创建一个Next指针来保存pcur下一个结点的位置。然后释放当前的pcur指针然后往后走就这样一直循环直到pcur指针走到头结点为止。
实现代码如下:
//打印
void LTPrint(LTNode* phead)
{
assert(phead);
//指向第一个有效数据
LTNode* pcur = phead->next;
//到头结点就停止
while (pcur!=phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
5.对双向链表进行头插
和上面尾插一样我们需要创建一个新的结点,然后将头结点后面的第一个有效数据结点的prev指针指向新的结点,然后将新结点的next指针指向原来链表的第一个有效数据结点。然后将头结点的next指针指向新的结点,将新结点的prev指针指向头结点,这样我们的头插就完成了。
实现代码如下:
//头插
void LTPushFront(LTNode* phead, LTDatetype x)
{
assert(phead);
//创建一个新结点
LTNode* newnode = Buynode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
6.对双向链表进行尾删
首先我们需要判断一下双向链表是否为空。当我们的头结点的prev指针和next指针都指向头结点自己时,此时这个双向链表为空。我们需要定义一个函数来实现
代码如下:
LTNode* LTEmpt(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
之后我们需要定义一个del指针指向我们需要删除的那个结点,然后定义一个指针Prev指向我们要删除的结点的前一个结点。然后将头结点的pre指针指向最后一个结点的前一个结点,然后将倒数第二个结点的next指针指向头结点,然后释放del这个节点的空间,然后将del置空。
代码如下:
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//判断一下双链表是否为空
assert(!(LTEmpt(phead)));
//要被删除的尾指针
LTNode* del = phead->prev;
//尾指针前面的那个指针
LTNode* pre = phead->prev->prev;
phead->prev = pre;
pre->next = phead;
//释放
free(del);
del = NULL;
}
7.对双向链表进行头删
跟上面的尾删操作一样我们同样需要判断链表是否为空。然后定义一个del指针将头结点后面的第一个有效数据结点保存起来,然后将头结点的next指针指向del的后面的结点,将del后面的结点的prev指针指向头结点,最后释放del结点的内存然后置空。
代码如下:
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//判断一下双链表是否为空
assert(!(LTEmpt(phead)));
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
//释放空间
free(del);
del = NULL;
}
8.查找指定数据的结点
我们需要创建一个Find函数返回值为结构体指针类型,将链表头结点和我们要查找的数据传过去,之后我们创建一个结构体指针pcur指向头结点的下一个结点也就是我们的第一个有效数据。然后遍历链表创建一个Next指针保存pcur的下一个结点的位置。然后释放当前pcur的内存随后pcur移动到Next的位置,如果有结点里面的data等于我们要找的那个数据则返回当前结点的地址,直到pcur移动到头结点为止。如果遍历完整个数组都没有找到则返回NULL。
代码实现如下:
//查找
LTNode* LTFind(LTNode* phead,LTDatetype x)
{
assert(phead);
LTNode* pcur = phead->next;
//遍历链表
while (pcur!=phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没找到
return NULL;
}
9.在指定位置之后插入
首先我们需要用上面的find函数找到怕pos的位置也就是我们要在哪个结点后插入,之后我们需要创建一个新的结点,将新结点的next指针指向pos后面的那个结点然后将pos后面的那个结点的prev指向新结点,然后将新结点的next指针指向pos将pos的next指针指向新结点,这样就完成了。
代码如下:
//指定位置之后插入
void LTInsert(LTNode* pos, LTDatetype x)
{
assert(pos);
//创建结点
LTNode* newnode = Buynode(x);
pos->next->prev=newnode;
newnode->next = pos->next;
pos->next = newnode;
newnode->prev = pos;
}
10.删除指定位置
和上面的一样我们需要用find函数找到我们要删除的那个结点。然后将要被删除的那个结点的后面一个结点和被删除结点的前一个结点链接。
代码如下:
//删除指定位置
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* Next = pos->next;
LTNode* Prev = pos->prev;
Next->prev = Prev;
Prev->next = Next;
}
11.销毁链表
链表是我们开辟空间创建的程序运行完我们就要销毁它。由于会影响到头结点。此时我们需要传二级指针,创建一个结构体指针pcur指向头结点的下一个结点,跟打印链表一样我们需要循环遍历链表,我们要创建一个Next指针用于指向pcur的下一个结点,然后释放当前pcur的内存,然后pcur走向下一个结点直到走到头结点为止。跳出循环后,我们还需要将pcur和头结点的内存释放掉然后置空。
代码如下:
//销毁
void LTDestroy(LTNode** pphead)
{
assert(pphead && *pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//释放头节点
free(*pphead);
*pphead = NULL;
pcur = NULL;
}
三.总结
链表和顺序表之间的分析: