1. 概念
双向链表即:可以从前往后或者从后往前找,这就意味着链表的结点就不能只有一个指针域next了,还需要一个指向前期结点的指针域prior
头指针 指向 头结点,尾指针 指向 末个 有效结点。
- 逻辑结构:线性结构
- 存储结构:链式存储
2. 接口实现
2.1. 定义:结点结构体 和 操作链表的结构体
思路类似 链式队列,需要 头head 和 尾tail 两个指针。
头指针 指向 头结点,尾指针 指向 末个 有效结点。
typedef int DLLNDataType;
typedef struct DoubleLinkListNode
{
DLLNDataType data; //数据域
struct DoubleLinkListNode *next; //前驱指针域
struct DoubleLinkListNode *prior; //后驱指针域
} DLLN, *DLL;
将头尾指针放到一个操作双向链表的结构体里
typedef struct DoubleLinkList
{
DLL head; // 头指针
DLL tail; // 尾指针
int len; // 记录链表长度(比下标大1)
} ODLL;
2.2. 创建 空的双向链表
创建 头结点,首尾指针都指向其。
2.3. 插入 结点到 指定位置
PNew
|
post前驱====新结点====post结点
| |
PTemp->prior PTemp
2.4. 遍历 打印
可以从头或者尾进行遍历
但注意,结构体存放头尾指针的地址,不要直接操作,需要用伪头尾指针代替操作。
2.5. 删除 指定位置的 结点
思路:
- 经过伪指针位置移动, 指向删除结点
- 直接链接 被删除结点的 前驱后继 的指针域
- 释放被删除结点,长度len--
2.6. 查找 指定数据 首次出现的 下标位置
用 遍历 有头单向链表 的思想
用 post 记录下标
2.7. 修改 指定下标位置结点 的数据
思路:
- 经过伪指针位置移动, 指向目标结点
2.8. 删除 指定数据的 结点
思路:
- 用遍历无头单向链表的方式,依次匹配
- 数据相等时,再判断位置
-
- 如果是末尾位置 或 仅有头结点和一个数据节点时候。
-
-
- 真尾指针前移,伪指针执行删除操作
-
-
- 处于中间位置时候
-
-
- 直接将目标结点的 前驱后继 链接起来
- 释放被删除结点
-
-
- 长度len--
- 数据不相等,伪指针后移
3. 总结
- 指定位置插入、指定位置删除、修改指定位置;这三者移动指针的思路是相同的。
- 运用有头,无头单向链表思路。
4. 总体代码
#include <stdio.h>
#include <stdlib.h>
#define DEBUG(Str) printf("%s %s %s %d\n", Str, __func__, __FILE__, __LINE__)
// 双向链表 结点 结构体
typedef int DLLNDataType;
typedef struct DoubleLinkListNode
{
DLLNDataType data; //数据域
struct DoubleLinkListNode *next; //前驱指针域
struct DoubleLinkListNode *prior; //后驱指针域
} DLLN, *DLL;
// 操作双向链表的结构体
typedef struct DoubleLinkList
{
DLL head; // 头指针
DLL tail; // 尾指针
int len; // 记录链表长度(比下标大1)
} ODLL;
// 创建 空的双向链表
ODLL *DLLInit(void)
{
// 1.1 开辟空间 存放 操作双向链表 的结构体
ODLL *PD = (ODLL *)malloc(sizeof(ODLL));
if (NULL == PD)
{
DEBUG("ODLL malloc err");
return NULL;
}
// 1.2 对 操作双向链表 的结构体 初始化成员
// 同时开辟 头结点
PD->head = PD->tail = (DLL)malloc(sizeof(DLLN));
if (NULL == PD->head) //用头 或 尾 指针都行
{
DEBUG("DLL HeadNode malloc err");
return NULL;
}
PD->len = 0; //长度为0
// 2. 初始化 头结点 成员
// 头结点 指针域为NULL;用头 或 尾 指针都行
PD->head->prior = PD->head->next = NULL;
// 3. 返回 操作双向链表 的结构体 的地址
return PD;
}
// 遍历 打印
void DLLPrint(ODLL *PD)
{
// 正向遍历相当于遍历有头单向链表
printf("正向遍历\n");
DLL PTemp = PD->head;
while (PTemp->next != NULL)
{
PTemp = PTemp->next;
printf("%d\t", PTemp->data);
}
puts("");
// 反向遍历相当于遍历无头单向链表
printf("反向遍历\n");
DLL Ptemp = PD->tail;
while (Ptemp != PD->head)
{
printf("%d\t", Ptemp->data);
Ptemp = Ptemp->prior;
}
printf("\n");
}
// 插入 结点 到 指定位置
int DLLInsert(ODLL *PD, int post, DLLNDataType data)
{
// 容错 位置 判断
// 可以正好等于len,为新位置
if (post < 0 || post > PD->len)
{
DEBUG("err insert post! not at [0,len]!");
return -1;
}
// 创建新结点,保存数据
DLL Pnew = (DLL)malloc(sizeof(DLLN));
if (NULL == Pnew)
{
DEBUG("new node malloc err");
return -1;
}
// 初始化新结点
Pnew->data = data;
Pnew->prior = Pnew->next = NULL;
// 判断插入位置(三种情况)
// 1.表尾插,尾结点和新结点链接
// 长度len总比当前最大下标大1
if (post == PD->len)
{
// **直接在末尾插入,不需要移动**
Pnew->prior = PD->tail; //pnew指向旧尾本身
PD->tail->next = Pnew; //旧尾next指向pnew本身
// 尾指针后移动
PD->tail = Pnew;
// 长度++
PD->len++;
}
// 非末尾 下标len位置插入,即中间插
else
{
// **如下,移动后再插入**
// 定义伪指针,代替 头 或 尾 指针移动
DLL Ptemp = NULL;
// 1. 在中间的前半段插入
// len为奇数,正好是 [开头,正中间]***
// len为偶数,正好是 [开头,前半段末个]
if (post < PD->len / 2)
{
// 那就伪指针从头移动
Ptemp = PD->head;
// 移动伪头指针,指向post位置的结点
for (int i = 0; i <= post; i++)
Ptemp = Ptemp->next;
}
else
{
// PTemp代替尾指针移动
Ptemp = PD->tail;
// 移动伪尾指针
// 直接从有效下标开始,故i不可等于post
for (int i = PD->len - 1; i > post; i--)
Ptemp = Ptemp->prior;
}
// 进行插入
// 思路:先完成新结点的链接,再是其前后。否则断链
// Ptemp已经指向了post位置结点
// pnew先和post位置前驱连接,其前驱再链接pnew
// pnew再先和post链接,post再和pnew链接
// 最终,pnew成为新post位置的结点,原结点后移
Pnew->prior = Ptemp->prior;
Ptemp->prior->next = Pnew;
Pnew->next = Ptemp;
Ptemp->prior = Pnew;
// 插入完成,长度+1
PD->len++;
}
return 0;
}
// 删除 指定位置的 结点
int DLLDeletePost(ODLL *PD, int post)
{
// 容错判断post位置
if (post < 0 || post >= PD->len)
{
DEBUG("err del, need at [0,len)");
return -1;
}
// 位置判断
// 尾部删除
if (post == PD->len - 1)
{
PD->tail = PD->tail->prior; //尾指针前移
free(PD->tail->next); //释放
PD->tail->next = NULL; //置空
}
// 中间删除
else
{
DLL PTemp = NULL; //最终指向post位置
// 中间位置思路,同插入
if (post < PD->len / 2)
{
PTemp = PD->head;
// 指向post位置结点
for (int i = 0; i <= post; i++)
PTemp = PTemp->next;
}
else
{
PTemp = PD->tail;
// 这种for写法:效果同插入部分
// 是 post 和 末下标 相差几个空,就移动几次
for (int i = 0; i < PD->len - 1 - post; i++)
PTemp = PTemp->prior;
}
// 用post结点的前后指针,链接前驱后继,不用pdel
PTemp->next->prior = PTemp->prior;
PTemp->prior->next = PTemp->next;
free(PTemp);
PTemp = NULL;
}
//长度--
PD->len--;
return 0;
}
// 查找 指定数据 首次出现的 下标位置
int DLLFind(ODLL *PD, DLLNDataType data)
{
// 不可知,最优位置,只能遍历
DLL Ptemp = PD->head;
// 记录下标位置
int post = 0;
// 遍历有头单向链表
while (Ptemp->next != NULL)
{
Ptemp = Ptemp->next;
if (Ptemp->data == data)
return post;
post++;
}
return -1; //找不到
}
// 修改 指定下标位置结点 的数据
int DLLModify(ODLL *PD, int post, DLLNDataType data)
{
if (post < 0 || post >= PD->len)
{
DEBUG("err post to modify, not at [0,len-1]");
return -1;
}
// 去寻找这个位置,让PTemp指向post位置的结点
DLL PTemp = NULL;
if (post < PD->len / 2)
{
PTemp = PD->head;
// 有头链表,需要=post
for (size_t i = 0; i <= post; i++)
PTemp = PTemp->next;
}
else
{
PTemp = PD->tail;
for (size_t i = 0; i < PD->len - 1 - post; i++)
PTemp = PTemp->prior;
}
// 修改数据
PTemp->data = data;
return 0;
}
// 删除 指定数据 的所有结点
void DLLDeleteDataNode(ODLL *PD, DLLNDataType data)
{
// 用遍历 无头单向链表思路即可
// 暂不能优化
DLL Ph = PD->head->next; //无头链表的首个结点
while (Ph != NULL)
{
// 先判断 数据是否相等
if (Ph->data == data)
{
// 首先判断,位置是否是 末尾
// 是的话,优先特殊处理,以符合while判断 和 逻辑处理
// 应对:只有头结点和一个有效元素情况
// 和 遍历到最后个 是目标结点
if (Ph == PD->tail)
{
// 真表尾指针 前移
PD->tail = PD->tail->prior;
// 新被指向的尾结点,需要next置NULL
PD->tail->next = NULL;
// 释放
free(Ph);
Ph = NULL;
}
// 非末下标位置,是中间位置
else
{
// 这是 删除;区分插入
// 被删结点 的前后 链接起来 即可
Ph->prior->next = Ph->next;
Ph->next->prior = Ph->prior;
DLL PDel = Ph;
Ph = Ph->next;
free(PDel);
PDel = NULL;
}
// 成功删除了,需要长度-1
PD->len--;
}
// 不相等,则 指针 继续移动
else
Ph = Ph->next;
}
}
int main(int argc, char *argv[])
{
// 创建空的有头双向链表
// 包括:头结点,操作链表的结构体
ODLL *PD = DLLInit();
// 插入结点
for (int i = 0; i < 6; i++)
DLLInsert(PD, i, i * 11);
// 打印链表
DLLPrint(PD);
// 删除指定位置数据看看
puts("----");
DLLDeletePost(PD, 2);
DLLPrint(PD);
// 查看 数据 第一次出现的位置
printf("%d at post:%d\n", 55, DLLFind(PD, 55));
// 修改指定 下标位置 的数据
puts("----");
DLLModify(PD, 2, 999);
DLLPrint(PD);
printf("%d at post:%d\n", 999, DLLFind(PD, 999));
// 删除所有这个数据 的结点
puts("----");
DLLPrint(PD);
puts("--*************--");
DLLDeleteDataNode(PD, 55);
DLLPrint(PD);
}