在# 01--单向链表和# 02--单向循环链表已经介绍了单向链表,这篇文章来开始介绍一下双向链表。
一、双向链表的设计
在单向链表中设计了一个next指针指向下一个节点,在双向链表中需要设计一个prior指针指向上一个节点,使得当前节点即可以通过next拿到后续,也能prior拿到前驱。节点设计如下:
通过这样的节点设计,可以得到双向链表的链式结构形如:
双向链表的两种设计思路:
- 1.第一种是有头节点的双向链表的设计;
- 2.第二种是没有头节点的双向链表的设计。
双向链表的特性:
- 1.有唯一的第一个节点和最后一个节点;
- 2.第一个节点的prior指向NULL,最后一个节点的next指向NULL;
- 3.其他节点的prior指向前驱,next指向后续。
在# 01--单向链表和# 02--单向循环链表中已经通过比较知道了头节点的好处,所以在设计双向链表时我们也设计一个头节点。
准备
为方便函数调用返回一些状态值,对一些数据类型已经重定义:
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
定义节点
//定义结点
typedef struct Node{
ElemType data; //数据域
struct Node *prior; //指向前驱
struct Node *next; //指向后续
}Node;
typedef struct Node * LinkList;//重定义类型名
二、创建
Status createLinkList(LinkList *L){
//创建一个头节点并将*L 指向头节点
*L = (LinkList)malloc(sizeof(Node));
if (*L == NULL) return ERROR;
//头节点的next和prior都指向NULL
(*L)->prior = NULL;
(*L)->next = NULL;
(*L)->data = -1;
//新增数据
LinkList p = *L;//p指向最后一个节点
for(int i=0; i < 10;i++){
//1.创建新节点
LinkList temp = (LinkList)malloc(sizeof(Node));
temp->prior = NULL;
temp->next = NULL;
temp->data = i;
//2.将新节点加入链表(尾插法)
p->next = temp;//插到链表的尾部
//新节点的prior指向原来的尾节点
temp->prior = p;
//3.p指向尾节点,方便下一次插入
p = p->next;
}
return OK;
}
- 创建双向链表时,需要创建一个
头节点,根据双向链表的特性,此时头节点的prior和next都要指向NULL;- 使用
尾插法向链表中插入数据时,新创建的节点放在链表的尾部,原来的尾节点的next指向新节点,新节点的prior指向原来的尾节点;记录尾节点的位置方便下一次插入数据。
三、遍历
void display(LinkList L){
LinkList temp = L->next;
if(temp == NULL){
printf("打印的双向链表为空!\n");
return;
}
while (temp) {
printf("%d ",temp->data);
temp = temp->next;
}
printf("\n");
}
遍历的关键是知道在哪里结束,根据双向链表的特性,当节点的
next指向NULL时即结束。
四、插入
Status ListInsert(LinkList *L, int i, ElemType data){
//1. 插入的位置不合法 为0或者为负数
if(i < 1) return ERROR;
//2. 新建结点
LinkList temp = (LinkList)malloc(sizeof(Node));
temp->data = data;
temp->prior = NULL;
temp->next = NULL;
//3.将p指向头结点!
LinkList p = *L;
//4. 找到插入位置i直接的结点
for(int j = 1; j < i && p;j++) {
p = p->next;
}
//5. 如果插入的位置超过链表本身的长度
if(p == NULL){
return ERROR;
}
//6. 判断插入位置是否为链表尾部;
if (p->next == NULL) {
p->next = temp;
temp->prior = p;
}else
{
//1️⃣ 将p->next 结点的前驱prior = temp
p->next->prior = temp;
//2️⃣ 将temp->next 指向原来的p->next
temp->next = p->next;
//3️⃣ p->next 更新成新创建的temp
p->next = temp;
//4️⃣ 新创建的temp前驱 = p
temp->prior = p;
}
return OK;
}
- 插入数据时,首先需要判断插入的
位置是否合法;- 找到目标位置的
前一个节点,判断最终是否找到了有效的插入位置;- 创建新节点,根据双向链表的特性进行操作:首先目标位置的
前一个节点的next指向新结点;其次有如下有两种情况:- 1.如果插入的位置是
尾节点的位置,新节点的next指向前面节点的next(此时指向的是NULL),新节点的prior指向前面的节点;- 2.如果插入的位置
不是尾节点,新节点的next指向后面的节点,prior指向前面的节点;前面节点的next指向新节点,后面节点的prior指向新节点。
如上图所示,在向链表中插入p时,p插入的位置可能是
首元节点的位置,可能是尾节点的位置,也可能是其它位置。因为有头节点的存在,结合双向链表的特性我们知道,除尾结点以外的其它任何节点的next和prior的指向都不会是NULL,所以需要针对尾节点做特殊处理。
五、删除指定位置
Status ListDelete(LinkList *L, int i, ElemType *e){
int k = 1;
LinkList p = (*L);
//1.判断双向链表是否为空,如果为空则返回ERROR;
if (*L == NULL) {
return ERROR;
}
//2. 找到目标位置的前一个节点
while (k < i && p != NULL) {
p = p->next;
k++;
}
//3.如果k>i 或者 p == NULL 则返回ERROR
if (k>i || p == NULL) {
return ERROR;
}
//4.创建临时指针delTemp 指向要删除的结点,并将要删除的结点的data 赋值给*e带回
LinkList delTemp = p->next;
*e = delTemp->data;
//5. p->next 等于要删除的结点的下一个结点
p->next = delTemp->next;
//6. 如果删除结点的下一个结点不为空,则将将要删除的下一个结点的前驱指针赋值p;
if (delTemp->next != NULL) {
delTemp->next->prior = p;
}
//7.删除delTemp结点
free(delTemp);
return OK;
}
- 删除同样要先判断当前删除的
位置是否合法;- 找到
删除节点和删除节点的前驱;前驱节点的next指向删除节点的next;- 和
插入一样,需要判断删除节点是否是尾节点;- 1.如果删除的是尾结点,什么都不做(此时前驱节点的next指向的是NULL,思考一下为什么?);
- 2.如果删除的
不是尾结点,需要将删除节点的后续的prior指向删除节点的前驱。
六、删除指定元素
Status LinkListDeletVAL(LinkList *L, int data){
LinkList p = *L;
//1.遍历双向循环链表
while (p) {
//2.判断当前结点的数据域和data是否相等,若相等则删除该结点
if (p->data == data) {
//前驱的next指向后续
p->prior->next = p->next;
//不是尾节点时,后续的prior指向前驱
if(p->next != NULL){
p->next->prior = p->prior;
}
//释放被删除结点p
free(p);
//退出循环
break;
}
//没有找到该结点,则继续移动指针p
p = p->next;
}
return OK;
}
链表中可能有各们数据域数据相同的数据,所以要遍历整个链表找到所有符合条件的数据进行删除。
七、查询
int selectElem(LinkList L,ElemType elem){
LinkList p = L->next;
int i = 1;
while (p) {
if (p->data == elem) {
return i;
}
i++;
p = p->next;
}
return -1;
}
查询分为查询
指定位置的元素和查询指定元素的位置,前者的结果是唯一的,后者的结果可能是多个的,上面的方法是查询指定元素的位置,在查询到第一个符合条件的位置后就返回了。
八、修改
Status replaceLinkList(LinkList *L,int index,ElemType newElem){
if (index < 1) return ERROR;
LinkList p = (*L)->next;
for (int i = 1; i < index; i++) {
p = p->next;
}
p->data = newElem;
return OK;
}
不管理是修改、查询、还是删除和插入,最先应该判断的就是操作的
位置是否合法。对于异常情况的处理是保证一个算法稳定健壮的关键。
九、总结
头结点的重要性在双向链表中也有明显体现,在包含头节点的双向链表的删除、插入中要对尾节点已经特殊的处理。