C语言数据结构之双向链表
一级目录
二级目录
三级目录
一、基础知识
双向链表:双向链表和单链表的区别在于双向链表有了一个指向前面一个结点的指针域。
这样可以方便我们找到一个结点的时候,想知道这个结点的上一个结点的内容不需要再次从头开始遍历整个链表了,而是只需要使用当前结点的前驱指针就可以获得前一个结点的位置,从而访问他的数据。
上图就是一个双向链表的结点的结构。
二、手搓代码
1. 结构体以及宏定义
双向链表需要一个前驱指针
和一个后继指针
,所以我们在结点结构体中需要定义两个结构体指针,作用就是分别用来指向前驱结点和后继结点
同时定义好需要的宏定义,我们让正确为1
,错误为0
即可。
# define True 1
# define False 0
typedef struct DoubleLink
{
struct DoubleLink* prior;
int data;
struct DoubleLink* next;
}Dlink;
2. 创建双向链表
初始化一个头结点,让这个头结点里面数据域和指针域都为空。
有人会问为什么双向链表不让指针指向自身呢?
指向自身那不就成了双向循环链表了吗,我们不需要让他循环指向,只需要让前后两对结点互相指向就行了。
Dlink* CreateDlink()
{
Dlink* p = (Dlink*)malloc(sizeof(Dlink));
assert(p);
memset(p,0,sizeof(Dlink);
return p;
}
3. 判断是否为空链表
如果头结点的next域
为空,那就说明这个链表没任何存放数据的结点。
int IsEmpty(Dlink* p)
{
if(p->next == NULL)
{
return True;
}
return False;
}
4. 打印
没什么好说的,循环遍历打印。
void PrintDlink(Dlink* p)
{
if(IsEmpty(p) == True)
{
printf("空表无法打印\n");
return;
}
Dlink* tmp = p->next;
while(tmp!=NULL)
{
printf("%d ",tmp->data);
tmp = tmp->next;
}
putchat('\n');
}
5. 头插
头插双向链表分为两种情况:
1 链表为空
链表为空的情况下,只需要让新结点和头结点互相指向,新结点的后继指针指向空就好了。
2 链表不为空
链表不为空的情况下,我们需要先让新结点的后继指针和原本的首元结点进行互相指向,再让新结点和头结点进行互相指向。
- 为什么要先让新结点指向原本的首元结点呢?
因为如果我们先让头结点指向新结点就会丢失原本首元结点的位置。 - 为什么我们要分为空链表和非空链表两种情况进行头插呢?
我们可以看出,如果链表非空,我们需要让原本的首元结点的前驱指针指向新结点,而链表为空的情况下没有首元结点,所以需要分情况来头插
void InsertHead(Dlink* p,int num)
{
Dlink* node = (Dlink*)malloc(sizeof(Dlink));
assert(p);
memset(node,0,sizeof(Dlink));
node->data = num;
if(IsEmpty(p) == True)
{
p->next = node;
node->prior = p;
node->next = NULL;
return;
}
node->next = p->next;
p->next->prior = node;
node->prior = p;
p->next = node;
}
3. 尾插
尾插只需要遍历到链表的最后,然后让最后一个结点和新结点互相指向,最后一个结点指向空即可。
void InsertBack(Dlink* p,int num)
{
Dlink* node = (Dlink*)malloc(sizeof(Dlink));
assert(node);
memset(node,0,sizeof(Dlink));
node->data = num;
Dlink* tmp = p;
while(tmp->next!=NULL)
{
tmp = tmp->next;
}
tmp->next = node;
node->prior = tmp;
node->next = NULL;
}
4. 指定位置插入
首先遍历到需要插入的结点之前,比如说我想插入到下标为2的位置,那么我就先遍历到下标为1的结点,然后让新结点的next
指向2结点
,让node
的prior
指向1结点
,然后让2结点
的前驱结点
指向node,再让node
的next
指向2结点
。
如果要插入在下表为0
或者最后一个位置,可以直接调用头插尾插。
void InsertPosition(Dlink* p,int index,int num)
{
int i =0;
Dlink* tmp =p;
while(tmp->next!=NULL)
{
i++;
tmp = tmp->next;
}//这样得到的i是元素个数,而不是最大下标
if(index<0||index>i)
{
printf("下标错误\n");
return;
}
else if(index == 0)
{
InsertHead(p,num);
return;
}
else if(index == i)
{
InsertBack(p,num);
return;
}
else
{
i = 0;
tmp =p;
Dlink* node = (Dlink*)malloc(sizeof(Dlink));
assert(node);
memset(node,0,sizeof(Dlink));
node->data =num;
while(tmp->next!=NULL)
{
if(i == index)
{
node->next = tmp->next;
tmp->next->prior = node;
node->prior = tmp;
tmp->next = node;
return;
}
i++;
tmp = tmp->next;
}
}
}
5. 头删
头删也存在两种情况,头删就是删除首元结点
1 首元结点后没有结点了
这种情况,我们只需要让首元结点free
掉,让头结点的next
指向空即可
2 首元结点后还有结点
这种情况下,我们还需要考虑后一个结点的前驱指针的指向问题。
我们需要先让头结点连接上下标为1
的结点,然后再让1结点
的前驱指针指向头结点
void PopHead(Dlink* p)
{
if (IsEmpty(p) == True)
{
printf("空表怎么头删\n");
return;
}
Dlink* tmp = p->next;
if (tmp->next == NULL)
{
p->next = NULL;
free(tmp);
tmp = NULL;
return;
}
else
{
p->next = tmp->next;
tmp->next->prior = p;
free(tmp);
tmp = NULL;
return;
}
}
6. 尾删
尾删只需遍历到最后一个结点,然后让其前驱指针指向的结点的后继指向空(tmp->prior->next = NULL
),这样说很复杂,其实就是让倒数第二个结点指向空。这里和单链表不同的是,单链表需要定义一个指针来保留当前结点的上一个结点,而双向链表因为具有前驱指针的原因,不需要定义一个指针来专门指向当前结点的上一个结点。
void PopBack(Dlink* p)
{
if (IsEmpty(p) == True)
{
printf("空的怎么尾删\n");
return;
}
Dlink* tmp = p->next;
while(tmp->next!=NULL)
{
tmp=tmp->next;
}
tmp->prior->next = NULL;
free(tmp);
tmp = NULL;
}
7.指定位置删除
指定位置删除就是先遍历到要删除的结点,然后让这个结点的前驱结点和后继结点互相连接,然后删除掉当前结点即可。如果是下标为0
或者最后一个结点,那么就调用头删尾删函数即可。
void PopPosition(Dlink* p, int index)
{
if (IsEmpty(p) == True)
{
printf("空的怎么指定位置删除\n");
return;
}
int i = 0;
Dlink* tmp = p;
while (tmp->next != NULL)
{
i++;
tmp = tmp->next;
}
if (index<0 || index>i-1)
{
printf("下标错误\n");
return;
}
else
{
if (index == 0)
{
PopHead(p);
return;
}
else if (index == i-1 )
{
PopBack(p);
return;
}
else
{
i = 0;
tmp = p->next;
while (tmp->next != NULL)
{
if (i == index)
{
tmp->prior->next = tmp->next;
tmp->next->prior = tmp->prior;
free(tmp);
tmp = NULL;
return;
}
i++;
tmp = tmp->next;
}
}
}
}
三、总结
我这里没有写双向链表的更改和查询,因为这两种代码和单链表的更改和查询是一模一样的,大家可以查看我的上一篇博客【C语言数据结构】之单链表来互相学习和纠错查询和更改。
双向链表需要注意的是前驱指针和后继指针的指向问题,尤其是在头插和头删这两种操作下,我们一定要注意这个插入或者删除这个结点的时候,此结点后有无结点。有无结点的操作是不同的。没有结点,我们就不需要让后面的结点的前驱指针指向当前结点。有结点的情况下,我们就需要让当前结点和后面的结点之间互相连接。