数据结构 :: 双向链表的设计与实现
说明:本文属于读书笔记。笔者将以讲述的方式表达全片文章。故文中提到的某些字词是非正式术语,只是笔者本人的理解性词语。
前言:此前已有文章讲述了单链表的设计与实现。如需了解,直飞链接:(有头单链表)(无头单链表)
强调!!!双链表实际并不是特别重要,正如前面的文章中所说,数据结构的基础实现,已有前人栽树,后人乘凉就好了。设计与实现只是加强个人对逻辑的推演,加深对数据结构基础的认知。
并且说明:链表的重心应该在单链表。链表是数据结构中的链式存储结构的实现。顺序存储结构与链式存储结构是后面更“高级”的数据结构的实现基础!!!
目录
1. 单链表与双链表的区别
之前设计与实现两种形式的单链表,大家应该不难发现单链表就好比单向车道,只能顺着一个方向走。如果现在要求将链表中的数据,逆序输出!!!怎么办?显然单链表是无法实现的。因为,总是它从前一个结点拿到地址后再去找后一个结点。
单链表就是只能实现从前往后式的功能操作,现在就是要实现同时可以从后往前的操作。(实现逆序输出链表数据)
单链表与双链表的区别:
- 单链表是使用一个指针指向链表,实现了依照结点的结构特点对数据进行访问。
双链表就是同时使用两个指针指向链表,且一个指向第一个结点,另一个指向尾部结点。- 单链表结点的结构特点,致使了其对数据访问操作的方式。此前,单链表结点只含有一个指针域(用于存储下一个结点的地址信息)。现在使用两个指针域(同时存储前后结点的地址信息)。
2. 双链表的设计与实现
如前文所述,**链表结点的结构特点,致使了其对数据访问操作的方式。**故为实现双链表的功能,进行如下设计。
2.1 结点结构
仿照单链表结点,其中单链表使用一个指针域,实现了对下一个数据的存储。现在,我们给予双链表两个指针域,分别存储前一个结点的地址信息与后一个结点的地址信息。(代码如下)
struct Node*{
int data; /* 存储数据 */
struct Node* prev; /* 存储前一个结点的地址信息 */
struct Node* next; /* 存储后一个结点的地址信息 */
};
2.2 创建新结点
前文提及,双链表将会使用两个指针,分别指向首尾结点。同时,在新建结点过程中,笔者使用 memcpy() 函数。(实现注意事项具体见代码及注释!)
/*
两个指针使用全局变量,一般不推荐使用,此处为了方便而使用,
建议读者理解思路之后自行修改操作
*/
struct Node* pHead;
struct Node* pTail;
struct Node* createNode(int data){
// 1. 申请内存
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 2. 赋空
#if 0
// 原来的方式
pnew->prev = NULL;
pnew->next = NULL;
#else
// 使用 memcpy() 函数:使用与多个同时操作
memcpy(pnew,0,sizeof(struct Node)); // 内存统一置空
#endif
// 3. 数据赋值
pnew->data = data;
// 4. 返回结点
return pnew;
}
2.3 元素插入
在双链表中,笔者将不在使用原来的区分形式写多个函数的插入方式,现在统一写成一个函数直接操作。
方式:
- 指定一个选择参数,作为如同头插法、尾插法、指定插入方式的选择 参数 flag。(-1 表示尾插,0 表示头插,正数表示指定插入的位置)
- 特殊情况:链表为空,直接挂载。
(实现注意事项具体见代码及注释!)
链表的学习与设计实现一定要通过作图来加深影响。后文将不提供图片示意!!!请读者自行绘图分析。
void insertNode(int data,int flag){
// 1. 申请新结点
struct Node* pnew = createNode(data);
// 2. 判断链表是否为空
if(NULL == pHead){
// 链表为空直接挂载
pHead = pTail = pnew;
return;
}
// 3. 根据flag的值进行操作
if(0 == flag){
// 看图!!!!!
pnew->next = pHead;
pHead->prev = pnew;
pHead = pnew;
}
elseif (-1 == flag){
// 看图!!!!!
pTail->next = pnew;
pnew->prev = pTail;
pTail = pnew;
}
else{
// 创建游标结点
struct Node* ptemp = pHead;
for(int i = 0;i < flag-2 ;i++){
// 链表过短
if(pTail == ptemp){ // 等价于尾插法
ptemp->next = pnew;
pnew->prev = ptemp;
ptemp = pnew;
}
// 移动游标
ptemp = ptemp->next;
}
pnew->next = ptemp->next;
ptemp->next = pnew;
pnew->prev = ptemp;
ptemp->next = pnew;
}
}
2.4 遍历链表(顺序、逆序)
在双链表的遍历中,可以 实现顺序、逆序遍历 ,此时需要传入一个 flag(0 为顺序;1 为逆序) 指定遍历方式。由于链表首位都有指针,故遍历时,顺序遍历移动 pHead,逆序遍历移动 pTail。(实现注意事项具体见代码及注释!)
void travel(int flag){
// 1. 判断链表是否为空
if(NULL == pHead)
{
printf("链表为空!\n");
return;
}
// 2. 顺序遍历
// 设置游标
struct Noed* ptemp;
if(0 == flag){
ptemp = pHead;
printf("顺序遍历:");
while(ptemp ){
printf("%d ",ptemp ->data);
ptemp = ptemp -next;
}
printf("遍历完成!\n");
}
elseif(1 == flag){
ptemp = pTail;
printf("顺序遍历:");
while(ptemp ){
printf("%d ",ptemp ->data);
ptemp = ptemp -prev;
}
printf("遍历完成!\n");
}
else{
printf("输入错误!flag(0 为顺序;1 为逆序)\n");
}
}
2.5 统计结点数
简单计数。从头至尾。(实现注意事项具体见代码及注释!)
int getCount(){
// 1. 如果链表为空返回 0
if(NULL == pHead) return 0;
// 2. 一个结点
if(pHead == pTail) return 1;
// 3. 结点数大于1,设置游标
int count = 0;
struct Node* ptemp = pHead;
for (ptemp = pHead;ptemp;ptemp = ptemp->next)
count++;
}
2.6 删除结点
本篇中使用删除指定位置上的结点,即传入的是“索引+1”,由于时双向链表,首尾均可操作,本篇将实现对于非首尾删除时,二分区域进行删除(略微提升性能)。(实现注意事项具体见代码及注释!)
void deleteNode(int pos){
// 1. 指定位置不合法,小于等于 0,或大于链表长度。
if(pos <= 0 || pos > getCount()){
printf("传入的参数值不合法!操作失败!\n");
return;
}
// 2. 创建游标
struct Node* pDel;
// 3. 分情况
if (1 == pos){ // 删除第一个结点
if(1 == count) {// 排除可能只有一个结点
free(pHead);
pHead = pTail = NULL;
return;
}
// 暂存要删除的结点
pDel = pHead;
// 下一个结点的 prev 置空
pHead->next->prev = NULL;
// pHead 指向下一个结点
pHead = pHead->next;
// 释放目标结点
free(pDel);
return;
}
if (count == pos){ // 删除最后一个结点
// 暂存要删除的结点
pDel = pTail;
// 前一个结点的 next 置空
pTail->prev ->next = NULL;
// pHead 指向下一个结点
pTail= pTail->prev;
// 释放目标结点
free(pDel);
return;
}
if (pos < (count / 2)){ // 前半部分
pDel = pHead;
for(int i = 0;i<pos-1;i++){
pDel = pDel->next;
}
// 被删除结点的前一个结点的 next 指向被删除结点的后一个结点
pDel->prev->next = pDel->next;
// 被删除结点的后一个结点的 prev 指向被删除结点的前一个结点
pDel->next->prev = pDel->prev;
// 释放 被删除结点
free(pDel);
}else{ // 后半部分
for(int i = 0;i<pos-1;i++){
pDel = pDel->prev;
}
// 被删除结点的前一个结点的 next 指向被删除结点的后一个结点
pDel->next->prev = pDel->prev;
// 被删除结点的后一个结点的 prev 指向被删除结点的前一个结点
pDel->prev->next = pDel->next;
// 释放 被删除结点
free(pDel);
}
}
3. 结语
到此,笔者简单的与大家分享了有头单链表、无头单链表、双链表的设计与实现。链表的基础操作就分享到这!
链表的真正使用情形肯定远不止这些!!!例如,学生信息管理系统项目中我们还会涉及排序问题等,包括有时还会遇到循环链表。此后的内容用法将在实际应用中来具体实现。
感谢阅读,如对代码有疑问或出现错误,请评论或留言,笔者会及时更正。
实操系列与功能补充指引!(待更新)