处理完了单链表,这次处理双链表。
在一个双链表中,每个节点都包含两个指针,指向前一个节点的指针和指向后一个节点的指针。这可以使我们以任何方式遍历双链表,甚至可以忽略前后地在双链表中访问。下面的图示展示了一个双链表:
下面是节点类型的声明文件:
double_linked_list_node.h
#ifndef _DOUBLE_LINKED_LIST
#define _DOUBLE_LINKED_LIST
typedef struct NODE{
struct NODE * fwd;
struct NODE* bwd;
int value;
}Node;
#endif
现在,存在两个根指针:一个指向链表的第一个节点,另一个指向最后一个节点。这两个指针允许我们从链表的任何一端开始遍历链表。
我们可能想把两个指针分开来声明为两个变量。但这样一来,我们必须把两个指针都传递给插入函数。为根指针声明一个完整的节点更方便,只是它的值字段绝不会被使用。在我们的例子中,这个技巧只是浪费了一个整型值的内存空间。对于值字段非常大的链表,分开声明两个指针可能更好一些。另外,我们也可以在根节点的值字段中保存其他一些关于链表的信息。例如链表当前包含的节点的数量。
根节点的fwd字段指向链表的第一个节点,根节点的fwd字段指向链表的最后一个节点。如果链表为空,这两个字段都为NULL。链表第一个节点的bwd和最后一个节点的fwd字段都为NULL。在一个有序的链表中,各个节点将根据value字段的值升序排列。
当把一个节点后插入到一个链表中时,可能出现四种情况:
1.新值可能必须插入到链表的中间位置;
2.新值可能必须插入到链表的起始位置;
3.新值可能必须插入到链表的结束位置;
4.新值可能必须插入到链表的起始位置,又插入到链表的结束位置(即链表为空)
下面是实现代码:
dll_ins1.c
#include <stdio.h>
#include <stdlib.h>
#include "double_linked_list_node.h"
int dll_insert(Node *rootp,int value){
Node *this;
Node *next;
Node *newnode;
/*查看value是否已经存在于链表中,如果是就返回。否则
* 为新值创建一个新节点(newnode将指向它)。this将
* 指向应该在新节点之前的那个节点,next将指向在新节点
* 之后的那个节点*/
for(this=rootp;(next=this->fwd)!=NULL;this=next){
if(next->value==value)
return 0;
if(next->value>value)
break;
}
newnode=(Node *)malloc(sizeof(Node));
if(newnode==NULL)
return -1;
newnode->value=value;
/*把新值添加到链表中*/
if(next!=NULL){
if(this!=rootp)/*新值插入到了链表中间*/
{
newnode->fwd=next;
this->fwd=newnode;
newnode->bwd=this;
next->bwd=newnode;
}else{/*新值插入到链表的起始位置*/
newnode->fwd=next;
rootp->fwd=newnode;
newnode->bwd=NULL;
next->bwd=newnode;
}
}
else{
if(this!=rootp)/*新值插入到链表的结束位置*/
{
newnode->fwd=NULL;
this->fwd=newnode;
newnode->bwd=this;
rootp->bwd=newnode;
}else{/*原先链表为空*/
newnode->fwd=NULL;
rootp->fwd=newnode;
newnode->bwd=NULL;
rootp->bwd=newnode;
}
}
return 1;
}
int main(){
Node root;
Node first,second,third;
first.value=10;
second.value=15;
first.fwd=&second;
second.fwd=NULL;
second.bwd=&first;
first.bwd=NULL;
root.fwd=&first;
root.bwd=&second;
if(dll_insert(&root,12)){
printf("insert success!\n");
}
return 0;
}
一开始,函数是this指向根节点。next指针始终指向this之后的哪一个节点。它的思路是这两个指针同步前进,直到新节点应该插入到这两者之间。for循环检查next所指节点的值,判断是否到达需要插入的位置。
如果在链表中找到新值,函数就简单返回。否则,当到达链表尾部或找到适当的插入位置时循环终止。在任何一种情况下,新节点都应该插入到this所指的节点后面。注意,在我们决定新值是否应该实际插入到链表之前,并不为它分配内存。如果事先分配内存,如果发现新值原先已经存在于链表中,就有可能发生内存泄露。
上面4种情况是分开实现的。当插入12时。下面这张图显示了for循环终止之后几个变量的状态:
然后,函数为节点分配内存,下面几条语句执行之后
newnode->fwd=next;
this->fwd=newnode;
newnode->bwd=this;
next->bwd=newnode;
就可以将新创建的节点插入链表中。
最后分析一下双链表和单链表:
传递给单链表的函数的参数是指向节点的指针的指针类型(Node **),但传递给双链表的是指向节点指针的类型(Node *),但其实传递给双链表的指向节点指针类型里面还包含着一个指针,所以它们本质上都是使用的指向节点指针的指针类型。观察图可以很清晰地看出这一点。
双链表省去了单链表中第三种方法的讨论,这是由于双链表中的边界条件我们直接分开处理了,就好像只讨论到了单链表中的第二种情况为止,而单链表中我们想法设法想将插入到起始位置这种情况和插入到中间这种情况统一到一起,于是出现了单链表中第3种处理思路。