数据结构中双链表比单链表的优点更多,在单链表中,我们的Node结点有两个数据成员,分别是data数据,和next指针,这样我们可以通过后继的指针来不断获取下一个结点,从而实现链表的相关操作。而双链表的Node中,不仅有data和后继,还包括有一个last前驱,这样我们找到一个结点后,想要得到他的前驱后继都会方便很多。
关于双链表结点的相关定义
struct Node
{
T data;
Node<T>*next; // 后继结点
Node<T>*last; // 前驱结点
};
first头结点也包括前驱和后继
双链表的特点就是可以非常方便的寻找任一结点的前驱和后继
构造函数
下面是双链表的三个构造函数,分别是无参、头插法、尾插法,与但链表的定义非常类似,只需要处理前驱结点即可
Link() // 无参构造函数
{
first=new Node<T>;
first->next=NULL;
first->last=NULL;
}
Link(T a[],int n) // 头插法
{
first=new Node<T>;
first->next=NULL;
Node<T>*s;
for(int i=0;i<n;++i)
{
s=new Node<T>;
s->data=a[i];
if(first->next!=NULL)
{
first->next->last=s;
}
s->next=first->next;
first->next=s;
s->last=first;
}
}
Link(int n,T a[]) // 尾插法
{
first=new Node<T>;
Node<T>*r,*s;
r=first;
for(int i=0;i<n;++i)
{
s=new Node<T>;
s->data=a[i];
r->next=s;
s->last=r;
r=s;
}
r->next=NULL;
}
双链表的遍历与但链表相同
插入操作
下面是双链表的插入操作,插入需要处理前驱后继结点两部,过程比较容易出错,一共有两种方法:
处理原则:先处理每个方向的远端指针,再处理近端指针
伪代码如下:
q->next=p->next;
q->last=p;
p->next=q;
q->next->last=q;
还有一种方法是:
处理原则:先在正向链上插入,后在逆向链上插入
q->next=p->next;
p->next=q;
q->last=p;
q->next->last=q;
在插入操作中一定要注意的问题是判断下一结点是否为空,否则会抛出指针异常
通常加入if(q->next!=NULL){q->next->last=q;}
删除操作
删除操作与但链表相似,但是考虑的问题会有前驱指针的处理
伪代码如下:
p->llink->rlink=p->rlink;
if(p->rlink) // 同样这个地方也是防止指针异常!!
p->rlink->llink=p->rlink;
delete(p);
双链表特点
- 创建双链表时无需指定链表的长度。
- 比起单链表,双链表需要多一个指针用于指向前驱节点,所以需要存储空间比单链表多一点。
- 双链表的插入和删除需要同时维护 next 和 last 两个指针。
- 双链表中的元素访问需要通过顺序访问,即要通过遍历的方式来寻找元素。