五十二、双向链表
52.1 双向链表的概念
- 在单向链表的基础上增加一个指向前驱结点的指针域
- 可以通过指向前驱结点的指针进行访问前一结点
52.2 双向链表与单向链表的区别
- 单向链表:
- 只有一个指针域next,指向后继结点
- 只能顺序向下访问,一去不回头
- 插入与删除时只需要进行操作即可,不需要判断是否与尾结点相关
- 双向链表:
- 有两个指针域,指向前驱结点的precursor指针,指向后继结点的next指针
- 可以通过指向前驱结点的指针域访问前面的结点,可以回头
- 插入时需要考虑是否插入到尾结点之后,如果插入到尾结点之后,则不能再让被插入结点的原next指针域指向的precursor指针域指向待插入结点,因为此时被插入结点的next指针域指向NULL,不能对NULL取precursor指针
- 删除结点是,需要考虑待删除结点是否是尾结点,如果不是尾结点,则将待删除结点的后一结点的pre指针指向待删除结点的前一结点
52.3 双向链表的组成
- 数据域: 由一项或多项数据构成,数据类型不定
- 指针域: 该结构体类型的指针,两个指针域,指向前驱结点的precursor指针,指向后继结点的next指针
52.4 双向链表的功能实现
52.4.1 双向链表的结构体
// 数据域是一个联合体,指针域指向前驱结点和后继结点
typedef struct double_linked_list
{
union
{
data num; // 表示数据内容
int len; // len表示当前有多少结点(头结点除外)
};
// 指向前驱结点的指针
struct double_linked_list *pre;
// 指向后继结点的指针
struct double_linked_list *next;
} dbLinkNode; // 结构体类型名重命名为dbLinkNode
52.4.2 双向链表功能图解
- 双向链表的头插
-
尾插
- 找到尾结点,把尾结点和新结点之间的关系连上
- 找到尾结点,把尾结点和新结点之间的关系连上
-
头删
-
尾删
-
按位置插入
-
按位置删除
- 找到pos-1位置的结点
- 找到pos位置的结点
- 找到pos-1位置的结点
52.5 双向链表的代码(接口,功能实现,测试用主函数,Makefile)
52.5.1 接口(head.h)
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 重命名数据域的数据类型名
typedef int data;
// 数据域是一个联合体,指针域指向前驱结点和后继结点
typedef struct double_linked_list
{
union
{
data num; // 表示数据内容
int len; // len表示当前有多少结点(头结点除外)
};
// 指向前驱结点的指针
struct double_linked_list *pre;
// 指向后继结点的指针
struct double_linked_list *next;
} dbLinkNode; // 结构体类型名重命名为dbLinkNode
// 创建单链表
dbLinkNode *init();
// 单链表的判空
int is_empty(dbLinkNode *l);
// 单链表的头插
int head_insert(dbLinkNode *l, data value);
// 单链表的尾插
int tail_insert(dbLinkNode *l, data value);
// 指定下标插入
int inset_by_index(dbLinkNode *l, int index, data value);
// 头删
int head_del(dbLinkNode *l);
// 尾删
int tail_del(dbLinkNode *l);
// 指定下标删除
int del_by_index(dbLinkNode *l, int index);
// 按值删除,返回删除的下标
int del_by_value(dbLinkNode *l, data value);
// 单链表的按值查找,返回下标
int find_by_value(dbLinkNode *l, data value);
// 单链表的按值修改,返回下标
int change_by_value(dbLinkNode *l, data old_value, data new_value);
// 单链表的按位置查找,返回对应的值
data find_by_index(dbLinkNode *l, int index);
// 输出单链表
int output(dbLinkNode *l);
// 释放单链表
int free_dbLinkNode(dbLinkNode **l);
#endif
52.5.2 功能实现(double_linked_list.c)
#include "head.h"
// 创建双向链表
dbLinkNode *init()
{
dbLinkNode *p = (dbLinkNode *)malloc(sizeof(dbLinkNode));
if (NULL == p)
{
/* code */
}
// 申请失败时返回NULL
if (NULL == p)
{
printf("创建双向链表失败\n");
return NULL;
}
// 初始结点数为0(头结点除外)
p->len = 0;
// 初始结点的pre指针域指向NULL
p->pre = NULL;
// 初始结点的next指针域指向NULL
p->next = NULL;
// 返回申请的堆区空间的首地址
return p;
}
// 双向链表的判空
int is_empty(dbLinkNode *l)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
// 头结点后无结点,或者结点数为0(头结点除外),此时链表为空,返回1
if (NULL == l->next || l->len == 0)
{
return 1;
}
return 0; // 链表不是空,返回0
}
// 双向链表的头插
int head_insert(dbLinkNode *l, data value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 新申请一个结点的空间,用ll指向
dbLinkNode *ll = (dbLinkNode *)malloc(sizeof(dbLinkNode));
// 新结点的值为传进来的值
ll->num = value;
// 新结点的next指针域指向头结点的原下一结点,即原第一结点
ll->next = p->next;
// 新结点的前驱结点指针域pre指向头结点
ll->pre = p;
// 让头结点next指针域指向这个新结点
p->next = ll;
// 如果这个双向链表不是空的,那么还有让原第一结点的pre指针域指向刚插入的结点
if (!is_empty(l))
ll->next->pre = ll;
// 结点计数变量+1
l->len++;
return 0;
}
// 双向链表的尾插
int tail_insert(dbLinkNode *l, data value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 循环向后指,直到p指向最后一个结点
while (NULL != p->next)
{
p = p->next;
}
// 新申请一个结点的空间,用ll指向
dbLinkNode *ll = (dbLinkNode *)malloc(sizeof(dbLinkNode));
// 新结点的值为传进来的值
ll->num = value;
// 新结点的next指针域指向NULL
ll->next = NULL;
// 新结点的pre指针域指向原最后一个结点
ll->pre = p;
// 新结点插到最后
p->next = ll;
// 结点计数变量+1
l->len++;
return 0;
}
// 指定下标插入
int inset_by_index(dbLinkNode *l, int index, data value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (index <= 0 || index > l->len)
{
printf("传入的下标有问题\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 记录当前下标
int i = 1;
while (1)
{
// 如果当前下标与传进来的下标相等,进行头插
// 因为此时指针p已经循环指向了此下标对应的结点的首地址
if (i == index)
{
// 新申请一个结点的空间,用ll指向
dbLinkNode *ll = (dbLinkNode *)malloc(sizeof(dbLinkNode));
// 新结点的值为传进来的值
ll->num = value;
// 新结点的next指针域指向被插入结点的下一结点
ll->next = p->next;
// 新结点的pre指针域指向被插入结点
ll->pre = p;
// 让头结点指向这个新结点
p->next = ll;
// 如果插进去的这个结点不在最后,
// 那么让刚插进去的这个结点的下一结点的pre指针域指向刚插进去的这个结点
if (NULL != ll->next)
ll->next->pre = ll;
// 插入后退出循环
break;
}
// 如果找到了最后还没找到,说明位置参数不对
if (NULL == p->next)
{
printf("传入位置参数不合法\n");
// 位置参数不对,return -1 表示函数异常结束
return -1;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p向后指,继续向后找
p = p->next;
}
// 找到并插入后,结点数+1
l->len++;
return 0;
}
// 头删
int head_del(dbLinkNode *l)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (is_empty(l))
{
printf("只有头结点,无法进行删除\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 保存待删除的结点,即第一结点
dbLinkNode *q = l->next;
// 将头结点的next指针域指向下下一结点,越过第一结点
p->next = q->next;
// 如果双向链表不是只有一个数据结点的链表
// 那么让待删除结点的后继结点的pre指针域指向头结点
if (NULL != q->next)
q->next->pre = p;
// 释放第一结点的空间
free(q);
q = NULL;
// 结点计数变量-1
l->len--;
return 0;
}
// 尾删
int tail_del(dbLinkNode *l)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (is_empty(l))
{
printf("只有头结点,无法进行删除\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 循环找到最后一个结点
while (1)
{
// 如果下一结点的next指针域指向NULL
// 说明当前结点是倒数第二个结点
if (NULL == p->next->next)
{
// q指向最后这个结点
dbLinkNode *q = p->next;
// 倒数第二个结点的next指针域指向NULL
p->next = NULL;
// 释放最后这个结点的空间
free(q);
q = NULL;
// 退出循环
break;
}
// 当前结点不是倒数第二个结点时继续向下找
p = p->next;
}
// 删除后,结点计数变量-1
l->len--;
return 0;
}
// 指定下标删除
int del_by_index(dbLinkNode *l, int index)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (index <= 0 || index > l->len)
{
printf("传入的下标有问题\n");
return -1;
}
if (is_empty(l))
{
printf("只有头结点\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 记录当前下标
int i = 1;
while (1)
{
// 如果当前下标与传进来的下标相等,进行删除
// 此时指针p的next指针域已经循环指向了要找的下标对应的结点
// 即p指向要删除的结点的前一结点
if (i == index)
{
// q指向这个要删除的结点
dbLinkNode *q = p->next;
// 让要删除的结点前一结点和后面一个结点连接
// 越过要删除的这个结点
p->next = q->next;
// 如果待删除结点不是尾结点
// 那么让待删除结点的后继结点的pre指针域指向前一结点
if (NULL != q->next)
q->next->pre = p;
// 释放要删除的这一结点的空间
free(q);
q = NULL;
// 删除后退出循环
break;
}
// 循环到了最后还没找到这个下标对应的结点
if (NULL == p->next)
{
// 提示位置参数不对,并返回-1表示函数异常结束
printf("传入位置参数不合法\n");
return -1;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p继续向后指,向后接着找
p = p->next;
}
// 删除后,结点计数变量-1
l->len--;
return 0;
}
// 按值删除,返回删除的下标
int del_by_value(dbLinkNode *l, data value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (NULL == l->next)
{
printf("只有头结点\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
dbLinkNode *p = l;
// 记录当前下标
int i = 1;
// 循环向后找
while (1)
{
// 如果找到了最后还没找到就退出循环
if (NULL == p->next)
{
break;
}
// 如果下一结点的值与传进来的值相同,则进行删除
if (value == p->next->num)
{
printf("准备删除\n");
// 指针q指向下一结点,即要删除的结点
dbLinkNode *q = p->next;
// 当前结点的next指针域指向要删除的结点的下一个结点
p->next = q->next;
// 如果待删除结点不是尾结点
// 那么让待删除结点的后继结点的pre指针域指向前一结点
if (NULL != q->next)
q->next->pre = q;
// 释放要删除的结点的空间
free(q);
l->len--;
printf("已删除\n");
// 返回当前所在下标
return i;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p继续向后指,向后接着找
p = p->next;
}
// 循环没找到,输出找不到这个值
printf("找不到这个值\n");
// 返回-1表示函数异常结束
return -1;
}
// 双向链表的按值查找,返回下标
int find_by_value(dbLinkNode *l, data value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (NULL == l->next)
{
printf("只有头结点\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点的下一结点
dbLinkNode *p = l->next;
// 记录当前位置
int i = 1;
// 循环向后找
while (1)
{
// 如果下一结点的值与传进来的值相同,则返回当前位置
if (value == p->num)
{
return i;
}
// 如果找到了最后还没找到就退出循环
if (NULL == p->next)
{
break;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p继续向后指,向后接着找
p = p->next;
}
// 循环没找到,输出找不到这个值
printf("链表中没有这个值\n");
// 返回-1表示函数异常结束
return -1;
}
// 双向链表的按值修改,返回下标
int change_by_value(dbLinkNode *l, data old_value, data new_value)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (is_empty(l))
{
printf("只有头结点\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点的下一结点
dbLinkNode *p = l->next;
// 记录当前位置
int i = 1;
// 循环向后找
while (1)
{
// 如果下一结点的值与传进来的旧值相同,则用新值进行修改并返回当前位置
if (old_value == p->num)
{
p->num = new_value;
return i;
}
// 如果找到了最后还没找到就退出循环
if (NULL == p->next)
{
break;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p继续向后指,向后接着找
p = p->next;
}
// 循环没找到,输出找不到这个值
printf("链表中没有这个值\n");
// 返回-1表示函数异常结束
return -1;
}
// 双向链表的按位置查找,返回对应的值
data find_by_index(dbLinkNode *l, int index)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
if (index < 0 || index > l->len)
{
printf("传入的下标有问题\n");
return -1;
}
if (is_empty(l))
{
printf("只有头结点\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点的下一结点
dbLinkNode *p = l->next;
// 记录当前位置
int i = 1;
// 循环向后找
while (1)
{
// 如果当前位置与传进来的下标相同,则返回这个位置的值
if (i == index)
{
return p->num;
}
// 如果找到了最后还没找到则提示位置参数不对
if (NULL == p->next)
{
printf("传入位置参数不合法\n");
// 返回-1表示函数异常结束
return -1;
}
// 这一圈没找到则当前下标+1向后找
++i;
// p继续向后指,向后接着找
p = p->next;
}
}
// 输出双向链表
int output(dbLinkNode *l)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
// 防止头结点指向改变,故新建一个指针指向头结点
// 用于指向待输出的结点
dbLinkNode *p = l;
// 记录当前到第几个结点
int i = 0;
while (1)
{
// 输出当前结点
// 是头结点时
if (0 == i)
{
printf("除头结点外,其余结点个数为:%d\n", p->len);
}
else // 非头结点时
{
printf("第%d位数据为:%d\n", i, p->num);
}
// 位置变量+1
++i;
// 如果当前结点最后一个结点,那么就退出(因为是先输出的,所以已经输出过了)
if (NULL == p->next)
break;
// 待输出的结点向后逐位移动
p = p->next;
}
return 0;
}
// 释放双向链表
int free_dbLinkNode(dbLinkNode **l)
{
if (NULL == l)
{
printf("传入参数为空\n");
return -1;
}
// 用于接收传来的头结点指针
// 传来的是头结点指针的地址,所以需要解引用获得其指向的地址
dbLinkNode *p = *l;
// 用于存放准备释放的结点的下一结点,免得找不到了
dbLinkNode *q = p->next;
// 进入循环,准备逐个释放
while (1)
{
// 释放当前结点
free(p);
// 让下一结点变成当前结点
p = q;
// 如果当前结点到了最后,指针已经指向NULL,表示空间都释放了
// 所以需要将头结点指针的指向更改,所以需要解引用指向NULL,然后退出
if (NULL == p)
{
*l = NULL;
return 0;
}
// 待释放结点的下一结点向后挪
// 因为上面已经把原来待释放结点的下一结点变成了当前准备释放的结点了
q = q->next;
}
}
52.5.3 测试用主函数(main.c)
#include "head.h"
int main(int argc, char const *argv[])
{
dbLinkNode *l = init();
printf("刚创建\n");
output(l);
printf("%d\n", is_empty(l));
printf("头插\n");
head_insert(l, 32);
output(l);
printf("%d\n", is_empty(l));
putchar(10);
printf("头插\n");
head_insert(l, 35);
output(l);
putchar(10);
printf("尾插\n");
tail_insert(l, 100);
output(l);
putchar(10);
printf("头插\n");
head_insert(l, 32);
output(l);
putchar(10);
printf("头插\n");
head_insert(l, 39);
output(l);
putchar(10);
printf("在第3个位置插入\n");
inset_by_index(l, 3, 99);
output(l);
putchar(10);
printf("值6所在位置为:%d\n",find_by_value(l, 6));
putchar(10);
printf("值100所在位置为:%d\n",find_by_value(l, 100));
putchar(10);
printf("将第一个32修改为55\n");
change_by_value(l,32,55);
output(l);
putchar(10);
printf("第三个位置上的值为:%d\n",find_by_index(l, 3));
putchar(10);
printf("第9个位置上的值为:%d\n",find_by_index(l, 9));
putchar(10);
printf("头删\n");
head_del(l);
output(l);
putchar(10);
printf("尾删\n");
tail_del(l);
output(l);
putchar(10);
printf("在第2个位置删除\n");
del_by_index(l, 2);
output(l);
putchar(10);
printf("删除32\n");
del_by_value(l, 32);
output(l);
putchar(10);
printf("删除33\n");
del_by_value(l, 33);
output(l);
putchar(10);
return 0;
}
52.5.1 Makefile
EXE=double_linked_list
CC=gcc
CFLAGs=-c
OBJs+=double_linked_list.o
OBJs+=main.o
all:$(EXE)
$(EXE):$(OBJs)
$(CC) $^ -o $@
%.o:%.c
$(CC) $(CFLAGs) $^ -o $@
clean:
rm *.o double_linked_list