双向链表是一种复杂类型的链表,它的节点包含指向序列中前一个节点和下一个节点的指针。 因此,在双向链表中,节点由三部分组成:节点数据,指向下一个节点的指针(next
指针),指向前一个节点的指针(prev
指针)。
双链表和单链表的区别本质上就是, 双链表的每个节点都增加了一个指向上一个节点的指针 ,这样就可以方便节点之间的互通了.
注: 我这里为了方便, 称节点 prior 指针为前驱指针, next 指针为后继指针
然后, 相关操作, 都增加了一步, 就是节点的 前驱指针需要指向上一个节点, 我们在覆盖指针的时候,注意相关顺序.
下面,开始我们的构思:
第一步: 我们想要创建一个双链表, 首先就要定义节点的类型,
typedef int ElemType;
第二步: 定义链表节点的结构体 ,包括自定义数据, 前驱结点和后继节点
typedef struct DNode
{
ElemType data; //节点数据
struct DNode *prior; //指向前驱节点的指针
struct DNode *next; //指向后继节点的指针
}DLinkList;
第三步: 我们已经把节点结构体定义了, 接下来, 就是通过构建各个结点之间的关系, 来创建一个我们所需要的单链表了 ,
基本操作,包括如下几步:
初始化链表
为头结点分配空间, 然后头节点后继指针置空 ,这样就初始化了双链表
L=(DLinkList *)malloc(sizeof(DLinkList)); //创建头结点L->prior=L->next=NULL;
双链表插入思路:
这样我们就构建了一个双链表 , 那如何往里面插入元素的或修改元素的 ,如何 进行相关的操作的?
我们看图示:
图上只标了 P 节点
插入前, 我们要让新节点插入到两个节点之间的话, 就要考虑指针了, 包括先后顺序
我们尝试性的把, S 的后继指针指向 后一个节点,
那后一个节点的位置在哪里 呢?
插入前, 后一个结点的位置指针在P->next
所以, 我们不能轻易的去覆盖指针 P->next
我们要用到 p->next ,所以先进行
s->next = p->next;
因为这时双链表 , 我们让新节点指向了后一个结点 ,当然,后一个节点的前驱指针也要指向s
后一个节点的位置在 P->next ,其前驱指针即为 p->next ->prior ,所以 把 s 的指针送给 后一个节点的前驱节点
p->next->prior = s;
那新节点和 后一个节点就已经链接上了 , 接下来把 s 和 p 链接起来
s -> prior = p; //新节点的前驱指针指向 p p->next =s ; //前一个结点的后继指针指向 s
这两个指针不冲突, 顺序不影响
s ->next = p->next;
p->next->prior = s;
s ->prior = p;
p ->next =s;
这几步就是,双链表插入节点的步骤, 还是那几个注意的点, 我们按照常规思维去覆盖指针前,要看那个指针对我们有没有用, 我们要先处理完, 安全后, 再覆盖,.
头插法建立双链表
思路: 我们是把一个数组里面的数据,插入到初始单链表 , 每次插入都是插入到头结点的后边 ,
数组 ElemType a[]
头结点 L
插入到 双链表 , 就是如上图所示:
至于插入的操作, 参考上面我们往双链表里插入元素的四步操作
下面开始我们的实操:
构建成员方法:
传入要建立的链表 ,传入要插入的数组数据 , 传入要操作的个数
void CreateListF(DLinkList *&L, ElemType a[] , int n){
先构造头结点;
分配空间,
L = (DLinkList *)malloc(sizeof(DLinkList)); // 分配空间
L->prior=L->next=NULL; // 头结点后继指针置空
然后, 我们要往里面插入新节点了, 新节点的数据就是数组里面的元素
那我们需要为新节点分配空间 ,定义新节点是 s
DLinkList *s; //定义新节点指针
因为每次插入的都是新节点, 所以每次插入都需要分配空间, 我们定义一个 for 循环来进行新节点的构造操作:
for(int i=0; i < n ; i++){
//n是要构建的双链表新节点的个数
s = (DLinkList *)malloc(sizeof(DLinkList)); //为每个新节点分配空间
s->data = a[i]; //为节点分配数据
//下面开始进行插入节点的操作 , 按照插入节点的操作, 考虑到只有头指针可以指引节点, 所以先把头结点的后继指针进行操作,
s ->next =L->next; //将新节点后继指针指向 头结点的后一个节点
//接下来,是如果插入前 , 头结点的后一个节点不为空, 就需要操作后面节点的前驱指针了,
//第一个插入的节点, 当然不用管后面的结点的前驱指针
if(L->next!NULL)
{
L->next->prior = s;
}
//后一个节点 , 已经操作完了, 下面开始操作新节点
L ->next = s; //头结点指向 新节点
s ->prior = L; //新节点的前驱指针指向头结点
}
尾插法建立双链表
思路: 尾插法, 就是在链表的尾结点上再连接一个结点 , 和 头插法的区别就是插入的新节点就是链表的尾结点, 尾结点最后置空 就行了.
所以我们要和单链表的尾插法一样 , 用一个指针指向尾结点, 然后进行相关操作就行了
首先
传入要建立的链表 ,传入要插入的数组数据 , 传入要操作的个数
void CreateListF(DLinkList *&L, ElemType a[] , int n){
然后 建立头结点
L = (DLinkLIst *)malloc(sizeof(DLinkList)); r = L;
然后接下来就是循环的建立新节点,分配新节点空间, 然后插入到双链表的尾部了
为构造新节点指针和尾结点指针
DLinkList *s , *r;
for(int i; i<n; i++) {s=(DLinkList *)malloc(sizeof(DLinkList)); //新节点分配空间s->data = a[i]; //为新节点送入输入里的数据r ->next = s; //把尾结点的后继指针指向 新节点 ss -> prior = r; //因为这是双链表, 新节点前驱指针指向链表尾结点r = s; //新节点链接完成,尾结点指针后移向新节点 }r->next = NULL; //全部新节点插入完成后,链表尾指针置为空
这样尾插法的操作就完成了
完整代码如下:
void CreateListR(DLinkList *&L, ElemType a[],int n)
{DLinkList *s, *r;int i;L = (DLinkLIst *)malloc(sizeof(DLinkList));r = L;for(i = 0; i<n; i++){s = (DLinkList *)malloc(sizeof(DLinkList));s -> data = a[i];r->next=s;s->prior = r;r = s;}r ->next = NULL; //尾插法,新节点没有后继节点,后继指针置为空就可以
}
在链表特定位置插入数据元素
首先传入要构建的链表 , 操作数据元素插入到第 i 个数据元素的位置 , 传入插入的元素
bool ListInsert(DLinkList *&L , int i , ElemType e){
假如我们在第 i 个 位置插入元素 , 我们首先要找到第i-1这个位置,然后插入到她后面就行了.
注: 我们所说的 第 i 个元素是生活中的第 i 个元素, 是从1 开始,到第 i 个 ,那双链表的 头结点一般不存数据元素 , 所以我们把头结点的后继节点称为第一个数据节点
我们要先找到第 i-1个元素 ,
第一个元素是 头结点 L ->next
寻找指针 p 先指向 L的后继节点
DLinkList *p = L->next; //寻找第 i 个元素的指针 ,初始指向第一个数据元素 (顾名思义,头结点不存数据)
DLinkList *s; //新节点指针
然后开始寻找第 i-1个节点
Iint j=1; //刚开始, p指向第一个元素 L
while(j<=i-1 && p!= NULL)
{
j++;
p=p->next;
}
如果 p 指向 NULL , 说明链表没有第 i 个元素, 链表已经到头了
if( p == NULL)
{
return false;
}
如果上述情况,都不发生的话,就说明我们找到了第 i -1个元素 p
else
{
s = (DLinkList *)malloc(sizeof(DLinkLIst )); //创建新节点
s->data = e; //为新节点赋值
s ->next = p->next; //在P后面插入新元素, 要考虑到其后面是否有
// 结点 ,有节点的话,还要安排它后面节点的前驱指针
//如果p 后面有节点,那就先安排其下一个节点的前驱指针,指向新节点
if(p->next !=NULL)
{
p->next->prior =s;
}
//然后再把新节点的前驱指针指向 第 i-1 个节点p , 第 i-1个节点的后继指针指向 新结点s
s->prior = p;
p ->next =s;
return true;
}
完整代码如下:
bool ListInsert(DLinkList *&L, int i, ElemType e)
{int j=0;DLinkList *p=L->next,*s;while(j<i-1 && p!=NULL){j++;p=p ->next;}if(P==NULL){return false;}else{s =(DLinkList *)malloc(sizeof(DLinkList));s ->data = e;s->next = p->next;if(p->next != NULL){p->next->prior =s;}s->prior = p;p->next =s;return true;}
}
在双链表中,删除第 i 个节点
构思: 删除第 i 个节点的意思就是, 把第 i -1 个元素 和 第 i+1 个元素 链接起来,
说白了就是
让第 i-1个元素的后继指针指向 第 i +1 个元素 ,第 i +1 个元素和前驱指针指向 第 i -1 个元素
,然后 第 i 个元素就脱离了链表的联系 , 然后我们就可以释放第 i 个节点的空间了.
下面开始实操:
传入链表 ,传入删除的节点位置, 设置保存删除元素的指针变量
bool ListDelete(DLinkList *&L , int i, ElemType &e){ //我们的函数操作代码都在大括号里面
我们接下来
我们要删除指定的节点 , 当然要找到 其 前一个节点 ,和后一个节点 ,
首先, 定义指针 *p 寻找 第 i-1 个节点 , 为了释放第 i 个结点 ,再定一个一个指针 *q ,来定位第 i 个节点
DLinkList *p;
DLinkList *q;
然后 ,开始遍历操作 , 头结点是第一个节点,我们要找第 i -1 个节点
int j=1; // 从第一个节点开始
p = L ; //p初始的时候指向 L
开始循环,找到 第 i 个节点,让 p依次向后遍历
while( j <= i-1 && p != NULL) //当 超过链表长度p 就变成空了,没必要再遍历下去 {j++;p = p->next; }
简单验证一下 , 在循环内 当 j 等于 2 时, p指向 L的后一个节点,就是第二个结点
当 i 超过链表的节点个数的时候, 当然 p 就会指向空, 我们也删除不了,就返回错误
if ( p == NULL)
{
return false;
}
当我们找到 第 i -1 个节点 *p 的时候 , 我们就进行上述我们的链接操作
else
{
//先定位 要删除的节点 q
q = p->next;
//找到第 i-1 个元素只能证明 , 第 i-1个 元素不是空,我们要时刻警惕空指针异常
if(q == NULL)
{
return false;
}
//如果通过上步的检验,我们就可以得知存在第 i 个元素 ,就把删除的数值传回
e = q->data;
//进行双链表链接操作
//第 i-1 个节点指向第 i+1 个元素, 然后第 i+1个元素的前驱指针指向第 i-1个元素
p->next = q-> next; / /我们已经进行了存储 p->next ,所以放心覆盖
// 然后第 i+1 个节点的前驱指针指向 第 i-1 个节点,
//再次警惕空指针异常, 存在第 i 个元素,就存在 第 i -1 个元素吗?
//如果存在第 i+1个元素,我们就要把第i+1 个节点的前驱指针指向第 i-1 个元素
if( p->next !=NULL)
{
p->next->prior = p; // p->next 此时已经指向第i+1个元素
}
free(q); //释放删除的节点空间
return true;
}
}
完整代码如下:
bool ListDelete(DLinkList *&L, int i, ElemType &e)
{int j = 0;DLinkList *p = L , *q;while(j<i-1 && p!NULL){j++;p = p->next;}if( p == NULL){return false;}else{q = p->next;if(q == NULL){return false;}e= q->data;p->next = q->next;if(p->next != NULL){p->next->prior = p;}free(q);return true;}
}
逆置双链表
问题:
有一个带头结点的双链表L,设计一个算法将其所有元素逆置 ,即第一个元素变为最后一个元素,第二个元素变成倒数第二个元素, ......, 最后一个元素变为第一个元素 ,
构思:
很简单,就是利用头插法,把链表的元素,重新插入到头结点的第一个节点上,
传入要修改的链表;
void reverse(DLinkList *&L){
然后定义一个指针节点 *p 指向要重新排列的元素
DLinkList *p;
再来一个元素存储重新排列元素的下一个元素,避免节点断开后失去联系
DLinkList *q;
第一个要重新排列的是头结点的后继节点
p = L->next;
往头节点插入前,链表先初始化,
L ->next = NULL;
下面开始遍历 ,插入头结点
whlie(p != NULL) {q = p->next;p->next = L ->next;if(L->next != NULL){L->next->prior = p;}L->next = p;p->prior = L;p = q; }
刚进入的时候, 当 p 所指向的节点不是 NULL的时候, 说明,L->next 不是空, 也就是我们所要逆置的
双链表不是空链表
我们要将第一个节点插入,自然要使用头插法,插入到链表头结点的后面
我们让 *p 指向要操作的节点,
p->next = L ->next; (直接操作指针错误)
我们能这样直接操作吗?
答案是,不能, 我们直接覆盖 p->next , 会造成 要操作节点的下一个节点失去联系,那下次我们就找不到要操作的节点了,所以要找指针*q ,先来存放 p->next,再来操作指针p->next
q = p->next; //存放 操作节点的下一个节点 ,直到 q = NULL,就表明没有可操作的节点了
然后,我们根据头插法的 操作, 先让操作的新节点指向头指针后一个节点
p ->next = L ->next;
新节点已经指向后一个节点了,那后一个节点指向 新节点吗?
如果插入前,单链表是空, 那头指针的后继指针 L->next 是空,我们上面已经让新节点指向 空指针 ,
如果插入前 ,单链表后面还有节点, 那头指针的后继指针L->next 不是空,我们上面的操作,是让新节点指向后一个节点
这两种结果,我们都让新节点指向了头结点的后继指针 ,
但是因为这是双链表 , 所以如果插入前, 头结点后有节点 *q 的话 ,我们就需要 *q 指向新节点 *p
if(L->next != NULL) //如果头指针后继节点不为空
{
L->next->prior = p; //那就让后继节点的前驱指针,指向新节点(头插法只在头结点 // 和第一个节点之间操作)
}
L->next = p; //然后让头结点的后继指针指向新节点 p ,
p->prior = L; //双链表让新节点指向头指针p = q; // p要继续向后遍历 ,这是我们存储的下一个节点的线索节点
所以逆置双链表的完整算法如下:
void reverse(DLinkList *&L)
{DLinkList *p = L->next,*q;L->next =NULL;while(p!=NULL){q = p ->next;p->next = L->next;if(L->next !=NULL){L->next->prior = p;}L->next = p;p->prior = L;p = q;}
}