双两表的引入:
首先,我们应该分析有单链表之后为什么又出现双链表?看名字可以知道双链表就是在单链表的基础上延伸出来的,也决定了它的出现是为了完善单链表的某
些缺点。
通过上一篇我们对单链表的分析,我们可以清楚的知道,单链表是对数组的一个扩展,解决了数组的大小比较死板的不容易扩展的问题,但
同时也出现了新的问题。
在单链表中,单链表的各个节点之间只由一个指针单向链接,所以单链表只能经由指针单向移动,也就是说一旦指针移动经过某个节点就无法回来,如果在操
作这个节点,除非从头指针开始再遍历一次,其他的某些操作也是比较麻烦(比如说插入节点、删除节点等等),因为单链表的节点只能往后移动不能往前移
动,在某些复杂的算法中有很大的局限性。
所以我们今天的主角登场了,为了解决以上的问题,C语言中引入了双链表这个新的数据结构,那什么是双链表呢,它跟单链表有什么区别呢,下面我们进入
今天的主题。
什么是双链表:
下面我从这几点去分析
第一:双链表跟单链表有什么区别呢?
单链表 = 有效数据 + 1个指针(指向后面的节点)
双链表 = 有效数据 + 2个指针(一个pNext指向下一个节点,一个pPrev指向前一个节点)
创建单链表节点的代码:
//定义一个结构体用于构建单链表
struct node
{
int data; //数据成员
struct node* pNext; //指向下一个节点指针
};
//创建节点函数
struct node* cretern_node(int data) {
//定义一个单链表结构体指针的变量,并且获取堆内存
struct node* p = (struct node*)malloc(sizeof(struct node));
//判断是否获取堆内存成功
if (NULL == p)
{
printf("malloc err");
return NULL;
}
//清空堆内存
memset(p, 0, 0);
//填入所要存储的数据
p->data = data;
//指向空指针(因为后面没有其他的节点了)
p->pNext = NULL;
return p;
}
创建双链表节点的代码:
//创建双链表的结构体
struct node
{
int data; //数据成员
struct node *pNext; //指向下一个节点指针
struct node* pPrev; //指向前一个节点指针
};
//创建双链表节点代码
struct node* create_node(int data)
{
//定义一个双链表结构体指针的变量,并且获取堆内存
struct node* p = (struct node*)malloc(sizeof(struct node));
//判断是否获取堆内存成功
if (NULL == p)
{
printf("malloc err");
return -1;
}
//清空堆内存(因为堆内存是脏的)
memset(p, 0, 0);
//存储所要存储的数据
p->data = data;
//指向空指针
p->pNext = NULL;
//指向空指针
p->pPrev = NULL;
return p;
}
我们不能从名字去分析这个问题,不能了解为单链表是一条链把每个节点链接,双链表就是通过两条链把节点链接起来,其实我们通过分析知道,双链表的出现就
是为了解决单链表只能单向访问节点的这个缺陷,所以我们可以知道双链表其实就是在单链表的基础上每个节点增加一个指针,指向前一个节点,所以叫双向链表
可能更容易理解一点。
双链表的基础算法
尾部节点的插入:
我们可以分析一下这个图,我们这里分析是有头节点的双链表,通过图我们可以知道头节点的pPrev指向的是NULL,尾节点也是指向NULL,这里我们新增加一个节
点,重点看红色的连线和线的箭头,通过分析我们可以得出,新的节点想插入到链表的尾部,只需要两步:
第一:我们要通过遍历的方法找到尾节点,遍历的方法跟单链表的方法一模一样,这里就不多说了。
第二:通过图分析,可以清楚的知道把新节点(这里就是节点三)插入尾部,我们要把原链表的尾节点(这里就是指节点二)的pNext指向NULL断开,重新指向新节
点的pNext,而新节点pPrev指向NULL这条链也要断开,重新指向前一节点(也就是节点二)的pPrev。
尾部插入节点的代码:
//创建尾部节点
void inster_tail(struct node* pHeader, struct node* new)
{
struct node* p = pHeader; //头指针
//第一步:先找到尾部节点
while (NULL !=p->pNext)
{
p = p->pNext;
}
//结束上面的循环后,p就指向了原来链表的最后一个节点
//第二步,将新节点插入原来的尾节点的后面
p->pNext = new; //后向指针关联好,新节点的地址前节点Next
new->pPrev = p; //前向指针关联好,新节点的Prev和前节点地址
//特别说明从上图可以看出来:p->pPrev和new->pNext都不用变
}
头部插入节点的代码:
//前插新节点
//算法参照图示进行编写,注意处理的顺序
void inster_head(struct node* pHeader, struct node* new) //pHeader:头指针 new:新节点
{
//把新节点的pNext指向原来的第一个有效节点
new->pNext = pHeader->pNext;
if (NULL != pHeader->pNext)
{
//原来有效节点的pPrev指针指向新节点的地址
pHeader->pNext->pPrev = new;
}
//头节点的pNext指向新节点的地址
pHeader->pNext = new;
//新节点Prev的指针指向头节点的地址
new->pPrev = pHeader;
}
这里通过代码结合图示,跟尾部节点的分析方法是一样的,就不在多解析了。
这里在次特别申明我们分析的都是有头节点链表
双链表的遍历:
根据前面我们对单链表的遍历的分析和前面对单双链表的区分,我们可以知道其实双链的遍历就是在单链表的基础上多了一个向前遍历算法,向后遍历的算法
跟单链表是一模一样。
向后遍历算法:
//后向遍历算法,参数pHeader指向链表头指针
void Backward_Taversal(struct node* pHeader)
{
struct node* p = pHeader;
while (NULL != p->pNext)
{
p = p->pNext;
printf("data = %d\n", p->data);
}
}
向前遍历的算法:
//前向遍历算法,参数pTail要指向链表末尾,还要注意操作的顺序
void Forward_Traversal(struct node* pTail)
{
struct node* p = pTail;
while (NULL != p->pPrev)
{
printf("data = %d\n", p->data);
p = p->pPrev;
}
}
向后的遍历我们就不再多分析了,说一下向前的遍历,我们要特别注意while循环里面语句处理的顺序,前后的顺序不能调换。
删除节点:
删除节点的代码:
//删除节点函数(特别说明应用场景是含有头节点,只对有效节点处理)
void delete_node(struct node* pHeder, int data)
{
struct node* p = pHeder;
while (NULL != p->pNext)
{
p = p->pNext;
//找到删除的节点
if (p->data == data)
{
//通过分析可以分是否为尾部节点
//尾节点
if (NULL == p->pNext)
{
//p表示当前节点,p->pNext表是后一个节点地址,p->pPrev表示前一个节点地址
p->pPrev->pNext = NULL;
p->pPrev = NULL; //这一步可以省略,因为后面我们通过free函数彻底删除p
//
}
//不是尾节点
else
{
//前一个节点的Next指针指向后一个节点的首地址,
p->pPrev->pNext = p->pNext;
//后一个节点的pPrev指针指向前一个节点的首地址,
p->pNext->pPrev = p->pPrev;
//当前节点的pPrev和pNext不用处理,因为后面会通过free函数释放p的地址,彻底删除p
}
free(p);
return 0;
}
}
printf("未找到目标节点\n");
return -1;
}
结合图和代码分析,相比于单链表,双方链表的删除的算法的实现其实更加间,因为前后都可以访问的特性,在某些操作上我们不需要记录上一个节点地址,只要
知道任意一个节点的地址,其他节点的地址我们都可以知道。首先是删除尾节点,通过图可以知道,只要将前一节点的pNext指向NULL(p->pPrev->pNext = NULL)。
删除普通节点就跟插入头节点的方法结合图(图中删除的节一)和代码去分析就可以,在这里就不在多分析。
我们的双链表之旅到这里就结束了,有什么写的不对的地方欢迎各位大佬指出矫正,感激不尽。