前言
- 本部分围绕带头双循环链表进行,常规的数据结构与算法入门书实在是太忽视这部分的内容了,实际实现一遍才发现根本不是书里说的那么简单。
- 为了完整记录和展示自己的思维过程,在本篇最后的完整代码展示的是未经优化的版本,并且附加了大量注释,但是实测跑通并且具有一定的鲁棒性。
- IDE是vs2019,使用本篇代码是记得修改IDE的安全函数设置,老旧的IDE可能无法辨认安全函数。
- 基本操作不做具体解析,书上真的很详细,我只结合自己的理解记录一些关键部分或者是我想到但书上不一定有的
算法实现总结
双链表的顺序查找是关键
- 链表是无法随机查找的,因为其物理结构是离散的,也正因为如此,无论链表是否带头结点,我们一定需要一个头指针去帮我们找到链表。
- 因此,涉及顺序查找的基本操作,在函数中都会声明一个结构体指针(这里以
DuLinklist pMove
为例),让这个pMove
称为头指针的分身,去替动弹不得的本体完成任务。 - 很多书会把这个“分身”叫做移动指针或者功能指针,我们就统一称为移动指针好了。
为何要带头结点?
为了方便理解,我们把循环双链表中后继指针域指向头结点的结点称为末端结点。
这里以头插法为例,在带头双循环链表中的插入步骤图示如下:
根据算法描述,带头结点的头插法,新结点是插入在头结点的下一个结点位置。以图示为例,这一次插入都不会影响头结点和末端结点之间的关系,变化的是插入前的头结点和头结点的下一结点之间的关系。
那么,我们再看一下不带头的头插法操作步骤,如图:
可以看到,这次插入影响到了头指针、一号结点和末端结点之间的关系,比带头结点麻烦些。
头结点初始化方式及优劣
对于双循环链表,初始化头结点时不建议把头结点指针域都指向NULL,这样在使用头插法初始化链表的时候,第一个新插入结点需要单独进行操作。更推荐将头结点的指针域都指向自己,这样其本身也具有循环的特性,在插入新结点是接不需要对第一个新插入结点做特殊处理了。
新结点的插入删除与“外部结点”
这个外部结点是我结合算法原理和实现过程中的理解自己定义的。以插入新结点基本操作来说,新结点相对于被操作双循环链表就是外部结点,因为新结点还不是链表中的一员;而以删除结点基本操作来说,确定被删除的结点相对于被操作双循环链表是外部结点,因为是要被“剔除”到链表之外的。
利用“外部结点”统一逻辑
双链表结点的插入删除十分复杂,这换来方便查找前驱的优势。但有了外部结点的概念,插入删除结点的操作步骤就可以统一起来了,都可以抽象成以下步骤:
- 修改外部结点指针域
- 修改移动指针前驱/后继结点的后继/前驱指针域
- 修改移动指针的后继/前驱指针域
这部分可以结合后面完整代码中的insertNode()
函数本身及注释理解,简单来说,就是先修改最不影响链表结点关系的结点,再修改需要被结点指针域表示的结点,最后修移动指针本身。
完整代码展示(非最优,含注释)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OK 1
#define ERROR 0
#define MAXSIZE 100
/*
包含以下基本操作实现(带头结点双循环链表):
1. 初始化头指针 √
2. 初始化头结点(与1合二为一了) √
3. 尾插法建立双循环链表 √
4. 头插法建立双循环链表 √
5. 插入新结点——将数值为e的新结点插入到第pos个 √
6. 删除结点1——将第pos个结点删除,并用elem获取其数值 √
7. 删除结点2——将数值为e的结点删除,并返回其位置 √
8. 遍历获取结点个数 √
9. 打印所有结点数值 √
*/
typedef struct Node {
int data;//数值域
struct Node* prev;//前驱结点指针域
struct Node* next;//后继结点指针域
}DuNode,*DuLinklist;
int initHeadNode(DuLinklist &L) {//初始化头结点
//链表可以没有头结点,但必须有头指针
DuLinklist s;
L = (DuLinklist)malloc(sizeof(DuNode));
s = (DuLinklist)malloc(sizeof(DuNode));
s->data = -1;//初始化为-1表示头结点数值域没有用处也无需修改
s->next = NULL;//
s->prev = NULL;//
L->next = s;
return OK;
}//这里头结点初始化方法是让指针域指向NULL,但在头插法建立链表时会有很大的麻烦
//推荐建立头结点时让指针域指向自身
int printAll(DuLinklist L) {//打印全部结点的数值域
DuLinklist pMove = L->next;//移动指针,初始指向头结点,因为头结点无需打印
if (pMove->next == NULL) {
printf_s("链表未初始化。");
return ERROR;
}
printf_s("当前链表的全部元素为:");
pMove = pMove->next;
while (pMove != L->next) {
printf_s("%d ", pMove->data);
pMove = pMove->next;
}
printf_s("\n");
return OK;
}
int tailInsertInit(DuLinklist& L) {//尾插法初始化双循环链表
DuLinklist pMove = L->next;//移动指针,永远指向插入新结点前的末端结点
DuLinklist s;//用于创建新结点
int num=0;
if (L->next == NULL || L->next->next != NULL || L->next->prev != NULL) {
printf_s("链表已经初始化。\n");
return ERROR;
}
printf_s("请输入初始化结点个数:");
scanf_s("%d",&num);
if (num <= 0 || sizeof(num) > sizeof(int)) {
printf_s("输入不合法。\n");
return ERROR;
}
for (int i = 1; i <= num; i++) {
s = (DuLinklist)malloc(sizeof(DuNode));
s->data = i;//为新建立结点数值域赋值
pMove->next = s;//已有末端结点后继指针指向新结点
s->prev = pMove;//新结点前驱指针指向已有末端结点,此时s成为新的末端结点
L->next->prev = s;//L->next表示头结点,令头结点前驱指针指向新的末端结点s
s->next = L->next;//令新的末端结点s的后继指针指向头结点(L->next)
pMove = pMove->next;//更新pMove指针,使其指向末端结点
}
return OK;
}
int headInsertInit(DuLinklist& L) {//头插法初始化双循环链表
DuLinklist pHead = L->next;//指向头结点
DuLinklist s;//用于创建新的结点
int num = 0;//创建num个新结点
//由于头结点的存在,头插法是不需要变更末端指针的后继指针域的(因为后继指针域永远指向头结点)
//所以头插法只需要更改头结点和头结点的后继结点这两者的指针域就行了
if (L->next == NULL || L->next->next != NULL || L->next->prev != NULL) {
printf_s("链表已经初始化。\n");
return ERROR;
}
printf_s("请输入初始化结点个数:");
scanf_s("%d", &num);
if (num <= 0 || sizeof(num) > sizeof(int)) {
printf_s("输入不合法。\n");
return ERROR;
}
for (int i = 1; i <= num; i++) {
s = (DuLinklist)malloc(sizeof(DuNode));
s->data = i;//为新建立结点数值域赋值
if (pHead->next == NULL) {
s->prev = pHead;
s->next = pHead;
pHead->prev = s;
pHead->next = s;
}
else {
s->prev = pHead;
s->next = pHead->next;
//先去修改新创建结点的指针域,因为这样并不会破坏链表已有结点之间的关系
pHead->next->prev = s;//先修改头结点的下一结点,因为这个结点是通过头结点指针域找到的
pHead->next = s;//后修改头结点,因为头结点的下一结点不需要了
//当然也可以让一个DuLinklist变量指向头结点的下一结点,围绕着前驱结点和前驱指针域进行修改,思路是一样的。
}
}
return OK;
}
/*
这里是个大坑,由于头结点初始化时指针域指向NULL而不是自己,导致
第一个新结点s在插入时成了尾插法,因此针对第一个新结点s要单独分析。
所以推荐初始化头结点时就让头结点指向自身,不然麻烦。
*/
int insertNode(DuLinklist& L,int pos,int e) {//在pos位置插入数值域为e的新元素
if (pos <= 0 || e < 0 || sizeof(e)>sizeof(int)) {
printf_s("传值错误。\n");
return ERROR;
}
DuLinklist pMove = L->next;//移动指针
DuLinklist s;//用于新建结点
while (pos != 1) {
pMove = pMove->next;
if (pMove == L->next) {//pos过大,又跑回头结点了
printf_s("pos数值太大,新结点插入失败。\n");
return ERROR;
}
pos--;
}//while循环执行完毕后,pMove指向第pos-1个结点
s = (DuLinklist)malloc(sizeof(DuNode));
s->data = e;
s->prev = pMove;
s->next = pMove->next;
//同样的思想,先改变外部结点(也就是新建结点)的指针域,防止影响原有链表结点关系
pMove->next->prev = s;
//同样的思想,改完外部结点,再改变由已知结点(pMove)指针域确定结点(pMove->next)的指针域
pMove->next = s;
//最后改变已知结点(pMove)的指针域
return OK;
}
int deleteNode(DuLinklist& L, int pos, int& elem) {//删除第pos个结点,并用全局变量elem返回该结点的值
DuLinklist pMove = L->next;//移动指针,初始为链表头结点
if (pos <= 0) {
printf_s("传值错误。\n");
return ERROR;
}
while (pos != 0) {
/*
双链表的pMove可以直接赋值到第pos个结点,这样其上一个结点和下一个结点都可以被pMove指针域找到;
且这么操作都是针对外部结点的(也就是待删除结点)。
而单链表就不行,pMove找到第pos-1个结点才行,因为单链表结点只有后继指针域。
当然,双链表也可以按照单链表方式实现,但是要而外注意操作顺序
*/
pMove = pMove->next;
if (pMove == L->next) {//pos过大,又跑回头结点了
printf_s("pos数值太大,新结点插入失败。\n");
return ERROR;
}
pos--;
}
elem = pMove->data;
pMove->prev->next = pMove->next;
pMove->next->prev = pMove->prev;
free(pMove);
return OK;
}
int deleteValue(DuLinklist& L, int value, int &num) {//value是所需结点的数值,num是用来获取位置
if (value <= 0 || sizeof(value) > sizeof(int)) {
printf_s("输入数值不合法。\n");
return ERROR;
}
DuLinklist pMove = L->next->next;//从头结点的下一个结点开始计算
num = 1;//防止num有初始赋值,函数内初始化一次。
while (pMove->data != value) {
pMove = pMove->next;
if (pMove == L->next) {
return ERROR;
}
num++;
}
pMove->prev->next = pMove->next;
pMove->next->prev = pMove->prev;
free(pMove);
return OK;
}
int getLength(DuLinklist L,int &length) {// 用全局变量length获取长度
if (L->next == NULL) {
return ERROR;
}
else if (L->next->next == NULL) {
length = 0;
return OK;
}
else {
DuLinklist pMove = L->next->next;//头结点后第一个结点
length = 1;
while (pMove->next != L->next) {
pMove = pMove->next;
length++;
}
printf_s("链表长度为%d,即一共有%d个有效结点个数。\n",length,length);
return OK;
}
}
int main(void) {
DuLinklist L;
int elem = 0;
int num = 0;
int length = 0;
initHeadNode(L);
//tailInsertInit(L);
headInsertInit(L);
printAll(L);
insertNode(L,7,99);
printAll(L);
deleteNode(L, 2, elem);
printAll(L);
deleteValue(L, 3, num);
printf_s("被删除的结点是第%d个\n",num);
printAll(L);
getLength(L, length);
printf_s("程序全部运行结束。\n");
return 0;
}