作者最近做数据结构的习题时发现了好多关于双向链表的问题,诸如双向链表非空的判定条件,双向链表插入一个结点的四条基本语句,双向链表删除一个结点的两条基本语句,判断双向链表是否为回文表的算法之类的练习题。今天就不妨对双向链表的抽象数据结构进行一次深入的学习,具体内容参照本博文。
双向链表的优点
在单向链表中,查找直接后继的时间复杂度为O(1),查找直接前驱的时间复杂度为O(n)。为了克服这一缺点,引入了双向链表。顾名思义,双向链表有两个指针域,一个指向直接前驱,一个指向直接后继。这样无论从双向链表的任何一个结点出发都可以向前向后自由遍历。
双向链表的示意图
双向链表的存储结构
//双向链表的存储结构
typedef struct DuLNode
{
ElemType data;//数据域data
struct DuLNode *prior;//指向前驱结点
struct DuLNode *next;//指向后继结点
}DuLNode,*DuLinkList;
双向链表初始化
双向链表的初始化就是创建一个只带头结点的空的双向链表,头结点的prior和next指针都赋为NULL
//创建一个空的双向链表
Status InitList(DuLinkList &L)
{
L = new DuLNode;
L->prior = NULL;//赋为NULL
L->next = NULL;//赋为NULL
return OK;
}
求创双向链表的表长length
//求双向链表的长度length
//方法是对后继进行一次遍历
int GetLength(DuLinkList L)
{
DuLNode *p;
p=L->next;
int length=0;
while(p!=NULL)
{
length++;
p=p->next;
}
return length;
}
清空一个双向链表
清空双向链表就是只保留头结点,删除双向链表的其他结点并释放空间,函数的形参采用C++的引用传值,算法实现和单链表的清空算法相同。
//置空双向链表,恢复到初始化状态
//参照的是单链表的操作方法
Status ClearList(DuLinkList &L)
{
DuLNode *p,*q;
p=L->next;
while(p!=NULL)
{
q=p->next;
delete p;
p=q;
}
L->prior = NULL;
L->next = NULL;
return OK;
}
销毁一个双向链表
销毁算法只是在清空算法的基础上释放头结点即可。
//销毁双向链表
//方法:先清空双向链表,再delete头结点L
Status DestoryList(DuLinkList &L)
{
DuLNode *p,*q;
p=L->next;
while(p!=L)
{
q=p->next;
delete p;
p=q;
}
delete L;
return OK;
}
判断双向链表是否为空表
这个问题作者在填空选择题中遇到过,当时问的是双向循环链表为空表的判定条件是什么,嘿嘿,顺序表,单链表,单向循环链表,双向链表,双向循环链表,是不是很迷糊。。。
//判断双向链表是否为空
int ListEmpty(DuLinkList L)
{//如果是空表,则返回1,否则返回0
if(L->next==NULL)
return 1;
return 0;
}
头插法创建双向链表
相信这是很多数据结构初学者甚至包括C语言初学者遇到的链表基本问题,链表可以头插和尾插,顾名思义,头插法是指每次把新的结点都插在头结点的后面,使之成为头结点的后继结点;尾插法是指每次把新的结点都插在双向链表最后一个结点的后面,使之成为最后一个结点。
//头插法创建双向链表
void CreatList_H(DuLinkList &L,int n)
{
int i;
DuLNode *p;
p=new DuLNode;
cin>>p->data;
注意!!!注意!!!注意!!!重要的事情说三遍
//插入第一个结点时比较特殊,与第i(i>=2)个结点插入有所不同。
p->next = NULL;
L->next = p;
p->prior = L;
再从i=1开始进行for循环
for(i=1;i<n;i++)
{
//初始化结点p
DuLNode *p;
p=new DuLNode;
cin>>p->data;
///重建链,变动四个指针
p->next = L->next;
L->next->prior = p;
L->next = p;
p->prior = L;
}
}
尾插法创建双向链表
这里作者采用的是和尾插法创建单链表一模一样的算法,比较好理解。话不多说,代码附上。
//尾插法创建双向链表
//和单链表的尾插法相似,不同的是多了两个指针
void CreatList_R(DuLinkList &L,int n)
{
int i;
DuLNode *p,*r;
r = L;//r始终指向双向链表的最后一个结点
for(i=0;i<n;i++)
{ //初始化结点p
p = new DuLNode;
cin>>p->data;
//重建链
r->next = p;
p->prior = r;
p->next=NULL;
r=p;//指针r向后移动
}
r->next=NULL;//这一句受C语言启蒙老师影响,其实有无皆可
}
双向链表按照结点顺序插入结点
下图中是在结点p之后插入一个结点的示意图,与删除第i个结点r有所差别,但勉强能够帮助理解。
这个函数引用一个双向链表头结点,传入一个位置参数i,还有一个ElemType型参数e,当然,i最好合理。作用以e为结点值生成一个结点并插入双向链表的第i个位置。
//双向链表的按照结点顺序插入结点
Status InsertList(DuLinkList &L,int i,ElemType e)
{
int length=GetLength(L);
DuLNode *p,*r;
if(i<1||i>length+1)//插入位置不合理
return ERROR;
r=L;
//初始化结点p
p = new DuLNode;
p->data=e;
p->prior = p->next = NULL;
int sum=1;
//遍历找到前驱结点
while(r->next!=NULL&&sum<i)
{
r=r->next;
sum++;
}
//结点p要插在r之后
p->next = r->next;
p->prior = r;
r->next = p;
return OK;
}
双向链表递增的插入一个结点
作用以e为结点值生成一个结点并插入双向链表的合适位置,使得双向链表是非递减的,当然,要求在插入之前这个双向链表就是非递减的。
//双向链表递增插入某个结点
Status InsertSort_add(DuLinkList &L,ElemType e)
{
DuLNode *r,*pre,*p;
pre=L;//pre为要插入结点的前驱结点
p=pre->next;//p为要插入结点后继结点
//初始化结点r
r=new DuLNode;
r->data=e;
r->prior = r->next = NULL;
//如果L为空表就直接将r插在L的后面
if(GetLength(L)==0)
{
L->next=r;
r->prior = L;
return OK;
}
while(p!=NULL)
{
if(p->data>e)
{//找到该插入的位置为pre与p之间
pre->next = r;
r->prior = pre;
p->prior = r;
r->next = p;
break;//插入成功,跳出循环
}
pre=pre->next;
p=p->next;
}
if(p==NULL)
{//如果插入位置在链表末端
pre->next=r;
r->prior = pre;
r->next=NULL;
}
return OK;
}
双向链表按照结点位置取出结点
这是很基本的操作啦,这个函数返回的是DuLNode *的指针,下文还有返回ElemType型函数,函数传入结点的在双向链表中的顺序值i和引用型头结点。
//双向链表按照位置取出结点,返回的是指向该结点的指针
DuLNode *GetDuLNode(DuLinkList L,int i)
{
int n=GetLength(L),j=1;
if(i<1||i>n)//查找位置不合理
return NULL;
DuLNode *p;
p=L->next;
while(p!=NULL)
{
if(j==i)
return p;
p=p->next;
j++;
}
return NULL;//未找到返回NULL
}
双向链表的取值取出第i个元素值放入e中
//双向链表的取值取出第i个元素值放入e中
Status GetElem(DuLinkList L,int i,ElemType &e)
{
int n=GetLength(L),j=1;
if(i<1||i>n)//查找位置不合理
return ERROR;
DuLNode *p;
p=L->next;
while(p!=NULL)
{
if(j==i)//找到结点p
{
e=p->data;
break;
}
p=p->next;//p向后移动
j++;//j自增遍历
}
return OK;
}
双向链表删除的第i个结点r
下图为双向删除p结点的示意图
//删除双向链表的第i个结点r
Status DeleteList(DuLinkList &L,int i)
{
int n,j;
n=GetLength(L);
if(i<=0||i>n)//删除位置不合理
return ERROR;
DuLNode *r;
r=L;
for(j=0;j<i;j++)
{
r=r->next;//r为要删除的结点
}
//两条删除语句
r->prior->next = r->next;
r->next->prior = r->prior;
delete r;//释放r的空间
return OK;
}
删除双向链表中所有值为e的结点
注意啦,这个函数会删除双向链表中所有值为e的结点。把L变成空表也是有可能的。
//删除双向链表中所有值为e的结点
Status DeleteValue(DuLinkList &L,ElemType e)
{
if(GetLength(L)==0)//删除操作不合理
return ERROR;
DuLNode *p;
p=L->next;
while(p!=NULL)//对L进行一次彻头彻尾的遍历
{
if(p->data==e)
{//p为要删除的结点
p->prior->next = p->next;
p->next->prior = p->prior;
delete p;
}
p=p->next;//p指针后移
}
return OK;
}
双向链表的向后遍历输出
//双向链表的遍历输出
//和单链表遍历输出算法的一模一样
void DisplayList(DuLinkList L)
{
DuLNode *h;
h = L->next;
while(h!=NULL)
{
printf("%d ",h->data);
h = h->next;
}
printf("\n");
}
按照输入顺序创建双向链表,以0结束
例如输入1 2 3 4 5 0,创建的双向链表为1 2 3 4 5。
//按照输入顺序创建双向链表,以0结束
void CreatList_Zero(DuLinkList &L)
{
InitList(L);
int i=1;
ElemType x;
cin>>x;
while(x!=0)
{
InsertList(L,i++,x);//前面有哦
cin>>x;
}
}
完整代码
以上是双向链表的基本操作的相关函数,基本上对双向链表的操作都可以细化为初始化,清空,销毁,求表长,查找,删除,遍历这些基本操作,下面附上包含测试代码的完整代码,以C语言实现,有些冗长,莫见怪,采用的是codeblocks16.01创建的C++工程
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#define ElemType int
#define OK 1
#define FALSE -1
#define ERROR -2
#define Status int
#define MAXSIZE 100
//双向链表的存储结构
typedef struct DuLNode
{
ElemType data;//数据域data
struct DuLNode *prior;//指向前驱结点
struct DuLNode *next;//指向后继结点
}DuLNode,*DuLinkList;
//创建一个空的双向链表
Status InitList(DuLinkList &L)
{
L = new DuLNode;
L->prior = NULL;
L->next = NULL;
return OK;
}
//求双向链表的长度length
//方法是对后继进行一次遍历
int GetLength(DuLinkList L)
{
DuLNode *p;
p=L->next;
int length=0;
while(p!=NULL)
{
length++;
p=p->next;
}
return length;
}
//置空双向链表,恢复到初始化状态
//参照的是单链表的操作方法
Status ClearList(DuLinkList &L)
{
DuLNode *p,*q;
p=L->next;
while(p!=NULL)
{
q=p->next;
delete p;
p=q;
}
L->prior = NULL;
L->next = NULL;
return OK;
}
//销毁双向链表
//方法:先清空双向链表,再delete头结点L
Status DestoryList(DuLinkList &L)
{
DuLNode *p,*q;
p=L->next;
while(p!=L)
{
q=p->next;
delete p;
p=q;
}
delete L;
return OK;
}
//判断双向链表是否为空
int ListEmpty(DuLinkList L)
{//如果是空表,则返回1,否则返回0
if(L->next==NULL)
return 1;
return 0;
}
//头插法创建双向链表
void CreatList_H(DuLinkList &L,int n)
{
int i;
DuLNode *p;
p=new DuLNode;
cin>>p->data;
//第一个结点比较特殊,先插入
p->next = NULL;
L->next = p;
p->prior = L;
再从i=1开始进行for循环
for(i=1;i<n;i++)
{
DuLNode *p;
p=new DuLNode;
cin>>p->data;
///重建链
p->next = L->next;
L->next->prior = p;
L->next = p;
p->prior = L;
}
}
//尾插法创建双向链表
//和单链表的尾插法相似,不同的是多了两个指针
void CreatList_R(DuLinkList &L,int n)
{
int i;
DuLNode *p,*r;
r = L;//r始终指向双向链表的最后一个结点
for(i=0;i<n;i++)
{
p = new DuLNode;
cin>>p->data;
//重建链
r->next = p;
p->prior = r;
p->next=NULL;
r=p;//指针r向后移动
}
r->next=NULL;
}
//双向链表的按照节点顺序插入节点
Status InsertList(DuLinkList &L,int i,ElemType e)
{
int length=GetLength(L);
DuLNode *p,*r;
if(i<1||i>length+1)//插入位置不合理
return ERROR;
r=L;
//初始化结点p
p = new DuLNode;
p->data=e;
p->prior = p->next = NULL;
int sum=1;
//遍历找到前驱结点
while(r->next!=NULL&&sum<i)
{
r=r->next;
sum++;
}
//结点p要插在r之后
p->next = r->next;
p->prior = r;
r->next = p;
return OK;
}
//双向链表递增插入某个结点
Status InsertSort_add(DuLinkList &L,ElemType e)
{
DuLNode *r,*pre,*p;
pre=L;//pre为要插入结点的前驱结点
p=pre->next;//p为要插入结点后继结点
//初始化结点r
r=new DuLNode;
r->data=e;
r->prior = r->next = NULL;
//如果L为空表就直接将r插在L的后面
if(GetLength(L)==0)
{
L->next=r;
r->prior = L;
return OK;
}
while(p!=NULL)
{
if(p->data>e)
{//找到该插入的位置为pre与p之间
pre->next = r;
r->prior = pre;
p->prior = r;
r->next = p;
break;
}
pre=pre->next;
p=p->next;
}
if(p==NULL)
{//如果插入位置在链表末端
pre->next=r;
r->prior = pre;
r->next=NULL;
}
return OK;
}
//双向链表按照位置取出结点,返回的是结点指针
DuLNode *GetDuLNode(DuLinkList L,int i)
{
int n=GetLength(L),j=1;
if(i<1||i>n)
return NULL;
DuLNode *p;
p=L->next;
while(p!=NULL)
{
if(j==i)
return p;
p=p->next;
j++;
}
return NULL;
}
//双向链表的取值取出第i个元素值放入e中
Status GetElem(DuLinkList L,int i,ElemType &e)
{
int n=GetLength(L),j=1;
if(i<1||i>n)//查找位置不合理
return ERROR;
DuLNode *p;
p=L->next;
while(p!=NULL)
{
if(j==i)//找到结点p
{
e=p->data;
break;
}
p=p->next;//p向后移动
j++;//j自增遍历
}
return OK;
}
//删除双向链表中所有值为e的结点
Status DeleteValue(DuLinkList &L,ElemType e)
{
if(GetLength(L)==0)//删除操作不合理
return ERROR;
DuLNode *p;
p=L->next;
while(p!=NULL)
{
if(p->data==e)
{//p为要删除的结点
p->prior->next = p->next;
p->next->prior = p->prior;
delete p;
}
p=p->next;//p指针后移
}
return OK;
}
//删除双向链表的第i个结点r
Status DeleteList(DuLinkList &L,int i)
{
int n,j;
n=GetLength(L);
if(i<=0||i>n)
return ERROR;
DuLNode *r;
r=L;
for(j=0;j<i;j++)
{
r=r->next;//r为要删除的结点
}
//两条删除语句
r->prior->next = r->next;
r->next->prior = r->prior;
delete r;
return OK;
}
//双向链表的遍历输出
//和单链表遍历输出的一模一样
void DisplayList(DuLinkList L)
{
DuLNode *h;
h = L->next;
while(h!=NULL)
{
printf("%d ",h->data);
h = h->next;
}
printf("\n");
}
//按照输入顺序创建链表,以0结束
void CreatList_Zero(DuLinkList &L)
{
InitList(L);
int i=1;
ElemType x;
cin>>x;
while(x!=0)
{
InsertList(L,i++,x);
cin>>x;
}
}
//测试代码
int main()
{
DuLinkList L;
DuLNode *p;
int i,n;
ElemType e;
InitList(L);
printf("请输入创建双向链表的元素值为:");
CreatList_Zero(L);
printf("初始化链表输出为;");
DisplayList(L);
printf("此双向链表的长度为:%d\n",GetLength(L));
printf("此双向链表是否为空(1表示空,0表示非空):%d\n",ListEmpty(L));
printf("请输入你想取出的结点编号为:");
cin>>i;
p = GetDuLNode(L,i);
printf("该结点对应的elem值为:%d\n",p->data);
printf("请输入你想取出的结点编号为:");
cin>>i;
GetElem(L,i,e);
printf("该结点对应的elem值为:%d\n",e);
printf("请分别输入要插入元素的位置和结点值为:");
cin>>i>>e;
InsertList(L,i,e);
printf("新的双向链表输出为:");
DisplayList(L);
printf("请输入递增插入元素的值为:");
cin>>e;
InsertSort_add(L,e);
printf("新的双向链表输出为:");
DisplayList(L);
printf("请输入要删除的结点位置为:");
cin>>i;
DeleteList(L,i);
printf("新的双向链表输出为:");
DisplayList(L);
printf("请输入要删除结点的值为(将删除所有值为e的结点):");
cin>>e;
DeleteValue(L,e);
printf("新的双向链表输出为:");
DisplayList(L);
ClearList(L);
printf("清空后双向链表的长度为:%d\n",GetLength(L));
printf("此双向链表是否为空(1表示空,0表示非空):%d\n",ListEmpty(L));
printf("请输入头插法创建双向链表的长度为:");
cin>>n;
printf("请逐个输入元素:");
CreatList_H(L,n);
printf("头插法创建的双向链表输出为:");
DisplayList(L);
ClearList(L);
printf("请输入尾插法创建双向链表的长度为:");
cin>>n;
printf("请逐个输入元素:");
CreatList_R(L,n);
printf("尾插法创建的双向链表输出为:");
DisplayList(L);
}
双向链表注意事项总结
1.大多数操作与单向链表相似,插入结点和删除结点尤为重要,也是与单链表最大的区别所在。
2.双向链表的最大优势就是从任何一个结点都可以向前向后遍历,相应的时间复杂度比单链表小。
3.双向链表的缺点就是空间利用率不高,一个结点有两个指针域,并且在进行操作时比较复杂,稍微不注意就可能出错。
4.最后强调双向链表在插入结点操作时一定要注意那四条语言的顺序问题。这也是双向链表的重点所在。具体参照这篇博文双向链表的插入和删除操作具体实现
作者practical_sharp
作者也是数据结构的初学者,代码和理论有不正确不规范的地方还望见谅,不喜勿喷,欢迎评论交流