第8篇文章中我们介绍了下链式存储中链表中的一种--单链表,但是单链表有一个缺点,就是无法快速访问到前驱结点,当查找到某个元素时,如果想找前边的元素结点,需要再次从头开始遍历,这样就比较麻烦。
那么就有人会问,是否可以在结点中再增加一个指针来指向前驱结点呢?答案是肯定的,增加了指向前驱结点的指针的链表称为
双链表
什么是双链表?
双链表,顾名思义就是可以向两个方向走的链表。它的每一个结点都有两个指针,一个指向后继结点,另一个指向前驱结点,如下图所示
我们从上图中可以看到,第一个结点的前驱结点pre指向了NULL,当然在设计时我们也可以把它指向头结点,最后一个结点的后继结点next指向了NULL,这种和单链表是一样的。
在双链表中,通过一个结点可以找到他的后继结点,也就可以找到他的前驱结点。
双链表在结构和算法描述上和单链表相同,只是某些算法实现比单链表复杂一些。例如在插入元素时,需要同时修改两个方向的指针指向,根据上图所示双链表,我们在元素40和67中间插入新元素88,来看一下图示过程
从上面的步骤图中,我们看出,在插入元素时,需要同时修改两个方向的指针指向,同理,在删除元素时,也要修改这两个指针。这里就不画图展示了,大家“脑补一下就可以了~~~”
前面已经学过了单链表的实现过程,双链表的实现与单链表的实现大同小异,接下来我们看看双链表的实现过程。
双链表的实现
学习完双链表的定义与基本操作之后,我们还是用c语言来实现一个双链表,此双链表所具有的功能如下:
创建双链表
获取双链表的长度
判断双链表是否为空
插入、删除、查找元素
销毁双链表
打印双链表
在学习完单链表时,我们每一种操作算法都有详细的描述和实现代码,双链表与单链表有很多操作是相似的,这里就不在详细描述,但是会给出关键代码的解释。dlist.h(头文件)
#ifndef _DLIST_H_ #define _DLIST_H_ struct Node; typedef struct Head * pHead;//头结点类型 typedef struct Node * pNode;//数据结点类型 //定义头结点 struct Head{ int length; pNode next; }; //定义数据结点 struct Node{ int data; pNode pre;//指向前驱结点的指针 pNode next;//指向后继结点的指针 }; pHead DlistCreate();//创建双链表方法声明 int getLength(pHead ph);//获取链表长度方法声明 int IsEmpty(pHead ph);//判断链表是否为空方法声明 int DlistInsert(pHead ph,int pos,int val);//在链表的pos位置插入val元素方法声明 pNode DlistDelete(pHead ph,int val);//删除链表中元素val方法声明 pNode DlistFind(pHead ph,int val);//查找链表中元素val方法声明 void DlistDestory(pHead ph);//销毁链表方法声明 void printFront(pHead ph);//打印链表中的元素方法声明(从头开始打印) void printLast(pHead ph);(从尾部开始打印) #endif
dist.c(函数实现文件)
#include "dlist.h"
#include <stdio.h>
#include <stdlib.h>
//创建双链表
pHead DlistCreate(){
pHead ph=(pHead)malloc(sizeof(struct Head));//为头结点分配空间
if(ph==NULL){
printf("分配头结点失败!");
return NULL;
}
//创建好头结点后,初始化头结点中的数组
ph->length=0;
ph->next=NULL;
return ph;
}
//获取链表长度
int getLength(pHead ph){
if(ph==NULL){
printf("传入的双链表有误!");
}
return ph->length;
}
//判断链表是否为空
int IsEmpty(pHead ph){
if(ph==NULL){
printf("传入的双链表有误!");
}
if(ph->length==0){
return 1;
}
else{
return 0;
}
}
//在链表的pos位置插入元素val
int DlistInsert(pHead ph,int pos,int val){
pNode pval=NULL;
if(ph==NULL||pos<0||pos>ph->length){
printf("插入元素时,传入参数有误");
}
//如果参数无误,给元素分配结点空间
pval=(pNode)malloc(sizeof(struct Node));
pval->data=val;
//判断在那个位置插入元素,先判断链表是否为空
if(IsEmpty(ph)){
ph->next=pval;
pval->next=NULL;
pval->pre=Null;
}
else{
pNode pCur=ph->next;
if(pos==0){
ph->next=pval;//将头结点指向pval
pval->next=pCur;//pval的后继指针指向pCur
pval->pre=NULL;//pval的前驱结点指向空
pCur->pre=pval; //让pCur的前驱结点指向pval
}
else{
for(int i=1;i<pos;i++){
pCur=pCur->next;//遍历链表,找到要插入的位置,并将pCur指针后移
}
//循环结束,此时pCur指向的就是要插入的位置
//下方是将指针断开,再连接的过程
pval->next=pCur->next;
pCur->next->pre=pval;
pval->pre=pCur;
pCur->next=pval;
}
}
ph->length++;
return 1;
}
//删除链表ph中的元素val
pNode DlistDelete(pHead ph,int val){
if(ph==NULL||ph->length==0){
printf("参数传入有误!");
}
pNode pval=DlistFind(ph,val);//找到值所在的结点
if(pval==NULL){
return NULL;
}
printf("将其删除\n");
pNode pRe=pval->pre;
pNode pNext=pval->next;
pRe->next=pNext;
pNext->pre=pRe;
return pval;
}
//查找某个元素
pNode DlistFind(pHead ph,int val){
if(ph==NULL){
printf("参数传递有误!")
}
pNode pTmp=ph->next;
//此过程与单链表没有差别
do{
if(pTmp->data==val){
printf("有此元素!\n");
return pTmp;
}
pTmp=pTmp->next;//如果上方判断不成立,则继续进行下一个判断
}
//循环条件是直到链表的结尾
while(pTmp->next!=NULL);
//如果上方都不成立,说明链表中没有这个元素,直接返回NULL
return NULL;
}
//销毁链表
void DlistDestory(pHead ph){
pNode pCur=ph->next;
pNode=pTmp;
if(ph==NULL){
printf("参数传入有误!");
}
while(pCur->next!=NULL){
pTmp=pCur->next;
free(pCur);//将结点释放
pCur=pTmp;
}
//回到初始化状态
ph->length=0;
ph->next=NULL;
}
//打印链表中的元素,从前往后打印
void printFront(pHead ph){
if(ph==NULL){
printf("参数输入有误!");
}
pNode pTmp=ph->next;
while(pTmp->next!=NULL){
printf("%d",pTmp->data);
pTmp=pTmp->next;
}
printf("\n")
}
//倒序打印
void printLast(pHead ph){
if(ph==NULL){
printf("参数输入有误!");
}
//现将指针移动到最后的位置
pNode pTmp=ph->next;
while(pTmp->next!=NULL){
pTmp=pTmp->next;
}
for(int i=--ph->length;i>=0;i--){
printf("%d",pTmp->data);
pTmp=pTmp->pre;
}
}
ok,双向链表基本的操作diamante就写完了,接下来我们看一下测试文件
main.c
#define _CRT_SECURE_NO_WARNINGS
#include "dlist.h"
#include <stdio.h>
#include <stdlib.h>
int main(){
//创建一个双链表
pHead ph=NULL;
ph=DlistCreate();
//向链表中插入元素
int num;
printf("请输入要插入的元素,输入0结束:\n");
while(1){
scanf("%d",&num);
if(num==0){
break;
DlistInsert(ph,0,num);//从头添加元素
}
}
printf("双链表的长度是%d\n",getLength(ph));
printFront(ph);
DlistInsert(ph,3,99);//在3位置插入新元素99
printFront(ph);
printLast(ph);
//查找元素
int val;
printf("请输入要查找的元素:\n");
scanf("%d",&val);
DlistFind(ph,val);
//删除元素
int del;
printf("请输入要删除的元素:\n");
scanf("%d",&del);
DlistDelete(ph,val);
printFront(ph);
//销毁链表
DlistDestoty(ph);
printf("双链表销毁成功:\n 此时链表的长度为%d\n",ph->length);
system("pause");
return 0;
}
总结
我们上方的代码实现了双链表的增删改查功能几个功能。
双链表的插入、删除操作,实现起来比单链表稍微复杂一丢丢~,其他操作都无较大的改动。
双链表是可以倒序遍历的,为了测试倒序功能,我们在上方代码中实现了printLast()方法,实现从后往前打印链表的元素。
双链表中的每个结点都要记录两个指针,所以空间消耗要略多一些。不过由于它良好的对称性,是的对结点前后两个结点操作更加灵活,也使得算法的时间效率得到了提高,说到底,就是空间换时间。
多谢关注,大家一起努力~~