作者:低调
作者宣言:写好每一篇博客
文章目录
前言
亲爱的读者们,今天我又来更新好文了,通过上一篇博客,我重点介绍了链表的相关知识link大家如果没看过点开链接去查看,今天我重点讲述带头双向循环链表,此结构虽然复杂,但实现起来特别的简单,那我们话不多说,进入正题。
以下是本篇文章正文内容,下面案例可供参考
一、带头双向循环链表的实现
我们看到他的结构,有一个哨兵位的头结点,他不存储有效数据,不需要传二级指针。
我们先来看看怎么定义一个双向链表:
我们以存入整型为例:
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* next;//指向下一个结点
struct ListNode* prev;//指向上一个结点
}ListNode;
注:这里还是老样子,创建两个原文件,一个头文件。相信大家都知道为什么要这么创建了,在讲解顺序表的时候我有解释过。
让我们来看看他要实现那些接口:
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
1.1创建返回链表的头结点
//代码一
void ListCreate(ListNode** pphead)//初始化
{
*pphead = BuyListNode(0);
(*pphead)->prev = *pphead;
(*pphead)->next = *pphead;
}
//代码二
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(0);//将其初始化为0;
phead->next = phead;
phead->prev = phead;
return phead;
}
创建头节点相当于初始化,这两种代码都可以实现初始化的效果
1.2开辟一个新的结点
ListNode*BuyListNode(LTDataType x)
{
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL)
{
printf("开辟内存失败\n");
}
newNode->data = x;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
这个代码可以说跟单链表开辟一个新结点几乎一样。这里不做过多的介绍了。
1.3双向链表的销毁
void ListDestory(ListNode** pphead)
{
assert(*pphead);
ListNode* cur =(*pphead)->next;
while (cur != *pphead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(*pphead);
*pphead = NULL;
}
销毁的前一个的时候,记得保存下一个的地址,防止释放后找不到下一个的地址,造成程序崩溃。头结点需要最后释放,他是作为循环停止的标志。
1.4双向链表的打印
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;
while (cur!= phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
这跟单链表的打印几乎一模一样。
1.5双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)//尾插
{
assert(phead);
ListNode* newNode = BuyListNode(x);
ListNode* tail = phead->prev;
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
我们进行尾插的时候需要找到最后一个结点,在单链表的时候,尾结点需要进行遍历才能找到,但得益于循环的好处,尾结点就是头结点的前一个。对照图来看看代码是怎么实现的
1.6双向链表的尾删
void ListPopBack(ListNode* phead)//尾删
{
assert(phead);
assert(phead->next != phead);//判断是否删除没了
ListNode* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail ->prev;
free(tail);
tail=NULL;
}
代码实现起来都很简单。
1.7双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)//头插
{
assert(phead);
ListNode* newNode = BuyListNode(x);
newNode->next = phead->next;
newNode->prev = phead;
phead->next->prev = newNode;
phead->next = newNode;
}
要注意第一步和第二步,反过来写也行,但要保存head的下一个地址,看这图去理解代码。
1.8双向链表的头删
void ListPopFront(ListNode* phead)//头删
{
assert(phead);
assert(phead->next != phead);
ListNode* next = phead->next;
phead->next = next->next;
next ->next->prev = phead;
free(next);
next = NULL;
}
1.9查找元素的位置
ListNode* ListFind(ListNode* phead,LTDataType x)//查找元素的位置
{
assert(phead);
ListNode* cur = phead->next;
while (cur!=phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return -1;}
1.10双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)//从查找位置的前一个插入
{
assert(pos);
ListNode* newNode = BuyListNode(x);
pos->prev->next = newNode;
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev = newNode;
}
1.11双向链表删除pos位置的节点
void ListErase(ListNode* pos)//把查找的位置删除删除
{
assert(pos);
ListNode* ppos = pos->prev;
ppos->next = pos->next;
pos->next->prev = ppos;
free(pos);
}
注:前面几个接口作者没有画图了,我相信读者如果理解了单链表的实现原理,可以自己看着代码去理解。你们也要锻炼画图的能力。数据结构这一块代码不是太难理解,主要就是画图梳理思路。
二、双向链表的运行结果
#include "List.h"
int main()
{
//ListNode* pList = NULL;
ListNode* pList = ListInit();//两种初始化的方式
ListInit(&pList);
ListPushBack(pList,1);
ListPushBack(pList,2);
ListPushBack(pList,3);
printf("尾插后的结果:");
ListPrint(pList);
ListPushFront(pList, 5);
ListPushFront(pList, 6);
ListPushFront(pList, 7);
printf("头插后的结果:");
ListPrint(pList);
ListPopBack(pList);
ListPopBack(pList);
printf("尾删后的结果:");
ListPrint(pList);
ListPopFront(pList);
ListPopFront(pList);
printf("头删后的结果:");
ListPrint(pList);
ListNode* pos = ListFind(pList, 5);
if (pos != -1)
{
printf("找到了,改成了10\n");//
pos->data = 10;//充当了修改;
}
else
{
printf("没有找到\n");
}
printf("修改后的结果:");
ListPrint(pList);
ListInsert(pos, 20);
printf("在pos之前插入的结果:");
ListPrint(pList);
ListErase(pos);
printf("删除pos位置处的结果:");
ListPrint(pList);
ListDestory(&pList);
return 0;
}
总结
双向链表在实际生活用的比较广泛,所以我们要熟练的掌握他的使用。链表相关的知识点我在单链表部分已经做了详细的解释,这里我就不过多的解释了,这里我给大家留几道选择题:
1.在一个长度为n的顺序表中删除第i个元素,要移动_______个元素。如果要在第i个元素前插入一个
元素,要后移_________个元素。
A n-i,n-i+1 B n-i+1,n-i C n-i,n-i D n-i+1,n-i+1
2.取顺序表的第i个元素的时间同i的大小有关()
A 对 B 错
3.在一个具有 n 个结点的有序单链表中插入一个新结点并仍然保持有序的时间复杂度是 。 A O(1) B O(n) C O(n2) D O(nlog2n)
4.下列关于线性链表的叙述中,正确的是( )。
A 各数据结点的存储空间可以不连续,但它们的存储顺序与逻辑顺序必须一致
B 各数据结点的存储顺序与逻辑顺序可以不一致,但它们的存储空间必须连续
C 进行插入与删除时,不需要移动表中的元素
D 以上说法均不正确
5.设一个链表最常用的操作是在末尾插入结点和删除尾结点,则选用()最节省时间。
A 单链表
B 单循环链表
C 带尾指针的单循环链表
D 带头结点的双循环链表
6.链表不具有的特点是()。
A 插入、删除不需要移动元素
B 不必事先估计存储空间
C 可随机访问任一元素
D 所需空间与线性表长度成正比
7.在一个单链表中,若删除 P 所指结点的后续结点,则执行? A p = p->next;p->next = p->next->next; B p->next = p->next; C p->next = p->next->next; D p = p->next->next
8.一个单向链表队列中有一个指针p,现要将指针r插入到p之后,该进行的操作是____。 A p->next=p->next->next
B r->next=p;p->next=r->next
C r->next=p->next;p->next=r D r=p->next;p->next=r->next
E r->next=p;p->next=r F p=p->next->next
答案:1.A
2.B
3.B
4.C
5.D
6.C
7.C
8.C