C语言:详解双向链表
1.双向链表的结构
2.双向链表的实现
1.双向链表的结构
双向链表是带头双向循环链表。
- 链表带头的意思是有头节点,且头节点的数据是无效的。
- 双向指两节点能通过指针互相访问,上一节点可通过next指针找到下一节点,下一节点可通过prev指针找到上一节点。
- 循环指尾节点能通过next指针找到头节点,双向链表由于具有“双向”的性质,所以头节点能通过prev指针直接找到尾节点。
双向链表的节点由3个部分组成,分别是数据、指向下一节点的指针next、指向上一节点指针prev。
对于单链表,指向第一个节点的指针phead为NULL时,链表为空;对于双向链表,链表只有一个头节点时,为空链表,此时phead->next指向phead,phead->prev指向phead。
双向链表的结构如图所示:
2.双向链表的实现
创建3个文件,在头文件List.h中定义节点结构体、进行函数声明、包含所需头文件;在文件List.c中编写函数;最后在test.c中进行测试。
在List.h中,需声明的函数有初始化链表、销毁链表、打印链表、尾/头插、尾/头删、在pos指向的节点后插入、删除pos指向的节点、查找节点。
//List.h
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode //定义双向链表节点结构
{
LTDataType data; //数据
struct ListNode* next; //指向下一个节点的指针
struct ListNode* prev; //指向上一个节点的指针
}LTNode;
LTNode* LTInit(); //初始化
void LTDesTroy(LTNode* phead);//销毁链表
void LTPrint(LTNode* phead);//打印
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
void LTInsert(LTNode* pos, LTDataType x);//在pos后插入
void LTErase(LTNode* pos);//删pos
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
打印链表
遍历双向链表,即可完成打印。但需要注意,与单链表不同,双向链表的头节点的数据是无效的,所以要从第一个有效节点开始遍历,即指针pcur应赋为phead->next;在循环中用pcur=pcur->next使指针pcur移动,当pcur指向尾节点时,pcur->next又指向头节点,所以循环条件是pcur不能为phead。
void LTPrint(LTNode* phead)//打印
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
申请节点
用malloc函数动态开辟内存空间,定义指向新节点的指针node指向这块空间,赋给所需数据,让新节点的next、prev指针都指向自己,最后返回新节点。
LTNode* LTBuyNode(LTDataType x)//申请节点
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node;
node->prev = node;
return node;
}
初始化
用LTBuyNode申请一个数据为-1的新节点,再定义一个指针phead指向这个节点,返回phead。使用时用LTNode类型指针接收phead。
LTNode* LTInit()//初始化
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
尾插
先让新节点newnode连上头节点和尾节点,即newnode->prev=phead->prev,newnode->next=phead。
再让尾节点连上newnode,头节点连上newnode,即phead->prev->next=newnode,phead->prev=newnode,两步的先后顺序不可调换,因为执行完phead->prev->next=newnode后,尾节点就是newnode了,phead->prev原来指向d3节点,应改为指向newnode。
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;//连上尾节点
newnode->next = phead; //连上头节点
phead->prev->next = newnode;//尾节点连上newnode
phead->prev = newnode; //头节点连上newnode
}
头插
新节点应插在头节点和第一个有效节点之间,先让newnode连上头节点和第一个有效节点。
再让头节点连上newnode,第一个有效节点连上newnode。
void LTPushFront(LTNode* phead, LTDataType x)//头插
{ //插在头节点和第一个有效节点之间
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead; //连上头节点
newnode->next = phead->next;//连上第一个有效节点
phead->next->prev = newnode;//第一个有效节点连上newnode
phead->next = newnode; //头节点连上newnode
}
尾删
删除尾节点前要保存尾节点,头节点连上尾节点的上一个节点,尾节点的上一个节点连上头节点。
void LTPopBack(LTNode* phead)//尾删
{
assert(phead && phead->next != phead);//链表有效且不只有头节点
LTNode* del = phead->prev;//保存尾节点
del->prev->next = phead; //尾节点的上一个节点连上头节点
phead->prev = del->prev; //头节点连上尾节点的上一个节点
free(del);
del = NULL;
}
头删
头删删的是第一个有效节点,先头节点连上第一个有效节点的下一节点,再第一个有效节点的下一节点连上头节点。
void LTPopFront(LTNode* phead)//头删
{ //删第一个有效节点
assert(phead && phead->next != phead);
LTNode* del = phead->next;//保存第一个有效节点
phead->next = del->next; //头节点连上第一个有效节点的下一节点
del->next->prev = phead; //第一个有效节点的下一节点连上头节点
free(del);
del = NULL;
}
查找
从第一个有效节点开始遍历链表,若pcur->data==x,即找到了。
LTNode* LTFind(LTNode* phead,LTDataType x)//查找
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
return pcur; //找到了
pcur = pcur->next;
}
return NULL; //没找到
}
在pos后插入数据
pos指向指定的节点,先让新节点newnode连上pos指向的节点,以及连上pos指向节点的下一节点。
接下来,先让pos->next指向节点连上新节点,再让pos连上新节点。
void LTInsert(LTNode* pos, LTDataType x)//在pos后插入数据
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
删除pos
指向pos指向节点的上一节点的指针为pos->prev,指向pos指向节点的下一节点的指针为pos->next,在删除pos指向节点前把上下两节点连起来,用free释放掉pos,即可删除pos。
void LTErase(LTNode* pos)//删除pos
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;// pos->prev pos pos->next
free(pos);
pos = NULL;
}
销毁链表
遍历链表,循环一次销毁一个节点,遍历链表时应及时保存下一节点,避免销毁节点后找不到下一节点。
void LTDesTroy(LTNode* phead)//销毁链表
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
List.c文件中代码如下:
//List.c
#include"List.h"
void LTPrint(LTNode* phead)//打印
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
LTNode* LTBuyNode(LTDataType x)//申请节点
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node;
node->prev = node;
return node;
}
LTNode* LTInit()//初始化
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;//连上尾节点
newnode->next = phead; //连上头节点
phead->prev->next = newnode;//尾节点连上newnode
phead->prev = newnode; //头节点连上newnode
}
void LTPushFront(LTNode* phead, LTDataType x)//头插
{ //插在头节点和第一个有效节点之间
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead; //连上头节点
newnode->next = phead->next;//连上第一个有效节点
phead->next->prev = newnode;//第一个有效节点连上newnode
phead->next = newnode; //头节点连上newnode
}
void LTPopBack(LTNode* phead)//尾删
{
assert(phead && phead->next != phead);//链表有效且不只有头节点
LTNode* del = phead->prev;//保存尾节点
del->prev->next = phead; //尾节点的上一个节点连上头节点
phead->prev = del->prev; //头节点连上尾节点的上一个节点
free(del);
del = NULL;
}
void LTPopFront(LTNode* phead)//头删
{ //删第一个有效节点
assert(phead && phead->next != phead);
LTNode* del = phead->next;//保存第一个有效节点
phead->next = del->next; //头节点连上第一个有效节点的下一节点
del->next->prev = phead; //第一个有效节点的下一节点连上头节点
free(del);
del = NULL;
}
LTNode* LTFind(LTNode* phead,LTDataType x)//查找
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
return pcur; //找到了
pcur = pcur->next;
}
return NULL; //没找到
}
void LTInsert(LTNode* pos, LTDataType x)//在pos后插入数据
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
void LTErase(LTNode* pos)//删除pos
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;// pos->prev pos pos->next
free(pos);
pos = NULL;
}
void LTDesTroy(LTNode* phead)//销毁链表
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
测试
在test.c文件中测试上述代码。
例如:创建节点的数据为1,2,4的双向链表。
//test.c
#include"List.h"
void test1()
{
LTNode* phead = LTInit();
LTPushBack(phead, 2);
LTPushFront(phead, 1);
LTPushBack(phead, 4);
LTPrint(phead);
}
int main()
{
test1();
return 0;
}
在此基础上创建节点为1,2,3,4的链表。
//test.c
#include"List.h"
void test1()
{
LTNode* phead = LTInit();
LTPushBack(phead, 2);
LTPushFront(phead, 1);
LTPushBack(phead, 4);
LTNode* ret1 = LTFind(phead, 2);
LTInsert(ret1, 3);
LTPrint(phead);
}
int main()
{
test1();
return 0;
}
拙作一篇,望诸位同道不吝斧正。