线性表文档之循环双链表
Posted 二木成林
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线性表文档之循环双链表相关的知识,希望对你有一定的参考价值。
循环双链表
定义
概念
循环双链表就是在双链表的基础上,见链表的尾结点和链表的第一个结点连接起来,形成一个循环。
如果是带头结点的循环双链表,则将链表的尾结点的 next
指针指向链表的头结点,将链表头结点的 prior
指针指向链表的尾结点。
如果是不带头结点的循环双链表,则将链表的尾结点的 next
指针指向链表的开始结点,将链表开始结点的 prior
指针指向链表的尾结点。
结构体
循环双链表结点跟双链表结点一样,都是由一个数据域和两个指针域组成,其中数据域存储当前结点的数据值,而其中一个指针域存储当前结点的直接前驱的地址,另一个结点存储当前结点的直接后继的地址。如图所示:
循环双链表结点的结构体如下:
/**
* 循环双链表的节点
*/
typedef struct CDLNode
/**
* 节点的数据域
*/
int data;
/**
* 节点的指针域,指向前驱节点
*/
struct CDLNode *prior;
/**
* 节点的指针域,指向后继节点
*/
struct CDLNode *next;
CDLNode;
特点
双链表有的特点,循环双链表也有:
- 链式存储线性表时,不需要使用地址连续的存储元素,即不要求逻辑上相邻的元素在物理位置上也相邻。
- 链表由于是通过『链』建立起的数据元素之间的逻辑关系,插入和删除操作不需要移动元素,只需要修改链结点指针域的指向。
- 因为链表的元素是离散地分布在存储空间中,所以链表不能随机存取,如果要找到某个数据元素,最坏情况下需要遍历整个链表。
- 链表存储数据不需要大量连续存储空间,但链表结点除了存储数据值之外,还附加有指针域,就存在浪费存储空间的缺点。
- 由于有两个指针域,所以比单链表更占用存储空间。
- 双链表可以双向读取,即从任一结点开始既可以到表尾,也可以到表头。
循环双链表独有的特点:
- 最后一个结点的
next
指针域指向链表的第一个结点,第一个结点的prior
指针指向链表的最后一个结点。因此可以从任一结点的前驱或后继开始,找到链表的所有结点。
基本操作
注:如无特殊说明,下面关于链表的所有操作都是基于带头结点的链表。完整代码请参考:
概述
注:下面都是 C 语言代码,所以如果要对单链表进行删除或新增操作,链表参数都是双指针。如果要使用 C++ 的引用则改成
*&
。一般如果是考研建议使用&
引用,避免双指针。
循环双链表的常见操作如下:
void init(CDLNode **list)
:初始化循环双链表。其中list
是未初始化的循环双链表。CDLNode *createByHead(CDLNode **list, int nums[], int n)
:通过头插法创建循环双链表。其中list
是未初始化的循环双链表;nums
是待插入到链表的数据数组;n
是数组长度。返回创建成功的循环双链表。CDLNode *createByTail(CDLNode **list, int nums[], int n)
:通过尾插法创建循环双链表。其中list
是未初始化的循环双链表;nums
是待插入到链表的数据数组;n
是数组长度。返回创建成功的循环双链表。int insert(CDLNode **list, int i, int ele)
:在循环双链表第i
个位置插入值为ele
的新节点。其中list
是循环双链表;i
是指定结点序号,范围在[1, length
之间;ele
是新节点的数据值。如果插入成功则返回 1,否则返回 0 表示插入失败。void insertFirst(CDLNode **list, int ele)
:在循环双链表的头部插入新节点。其中list
是循环双链表;ele
是新节点的数据值。void insertLast(CDLNode **list, int ele)
:在循环双链表的尾部插入新节点。其中list
是循环双链表;ele
是新节点的数据值。int removeByNum(CDLNode **list, int i, int *ele)
:删除循环双链表第i
个结点,并用ele
保存被删结点的数据值。其中list
是循环双链表;i
是待删结点的序号,范围在[1, length]
之内;ele
用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。int removeByEle(CDLNode **list, int ele)
:删除循环双链表中第一个等于指定值ele
的结点。其中list
是循环双链表;ele
是指定值。如果删除成功则返回 1,否则返回 0 表示删除失败。int removeFirst(CDLNode **list, int *ele)
:删除循环双链表第一个结点。其中list
是循环双链表;ele
用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。int removeLast(CDLNode **list, int *ele)
:删除循环双链表最后一个结点。其中list
是循环双链表;ele
用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。CDLNode *getPrior(CDLNode *list, int ele)
:得到循环双链表中第一个等于指定值ele
结点的前驱结点。其中list
是循环双链表;ele
是指定值。如果查找成功则返回第一个值为ele
的结点的前驱结点。CDLNode *getNext(CDLNode *list, int ele)
:得到循环双链表中第一个等于指定值ele
结点的后继结点。其中list
是循环双链表;ele
是指定值。如果查找成功则返回第一个值为ele
的结点的后继结点。CDLNode *findByNum(CDLNode *list, int i)
:寻找循环单链表中第i
个结点。其中list
是循环双链表;i
是结点序号,从 1 开始。如果该结点存在则返回,否则返回NULL
。CDLNode *findByEle(CDLNode *list, int ele)
:寻找循环单链表中第一个值等于ele
的结点。其中list
是循环双链表;ele
是指定值。如果该结点存在则返回,否则返回NULL
。int size(CDLNode *list)
:获取循环双链表的长度,即结点个数。其中list
是循环双链表。返回循环双链表的长度。int isEmpty(CDLNode *list)
:判断循环双链表是否为空。其中list
是循环双链表。如果循环双链表为空则返回 1,否则返回 0。void clear(CDLNode **list)
:清空循环双链表。其中list
是循环双链表。void print(CDLNode *list)
:打印循环双链表所有结点。其中list
是循环双链表。
init
初始化循环双链表。如果是带头结点的循环双链表,则将头结点的 prior
和 next
指针指向头结点本身;如果是不带头结点的循环双链表,则将头指针指向 NULL
,不区分什么 prior
和 next
指针指向。
实现代码如下:
/**
* 循环双链表的初始化函数
* @param list 带初始化的循环双链表
*/
void init(CDLNode **list)
// 1.初始化循环双链表,即创建头结点。头结点的前驱节点指针和后继节点指针都指向自身,表示循环
// 1.1 为头结点分配空间
*list = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2 将头结点的 prior 指针指向自身
(*list)->prior = *list;
// 1.3 将头结点的 next 指针指向自身
(*list)->next = *list;
如果是使用 C++
中的引用,则代码如下:
/**
* 初始化单链表
* @param list 单链表
*/
void init(CDLNode *&list)
// 给双链表头结点分配空间
list = (CDLNode *) malloc(sizeof(CDLNode));
// 将双链表的指针域都指向 null,不需要理会数据域
list->prior = list;
list->next = list;
如果是不带头结点的初始化代码如下:
/**
* 初始化不带头节点的单链表
* @param list 待初始化的单链表
*/
void init(CDLNode *&list)
// 将头指针直接置为 NULL,表示空单链表
list = NULL;
createByHead
通过头插法创建循环双链表。所谓的头插法就是每次插入一个新元素都是插入在开始结点的位置,无论双链表是否带有头节点。
以 nums=[1, 2, 3, 4, 5]; n=5
为例如图所示:
实现步骤:
- 初始化循环双链表。
- 循环遍历数组
nums
,根据数组中的每个元素创建新结点,为其分配数据域和指针域(初始创建时指针域都指向NULL
),然后将新结点插入到链表头结点和原开始结点之间。
实现代码如下:
/**
* 通过头插法创建循环双链表
* @param list 循环双链表
* @param nums 待插入到双链表中的数据数组
* @param n 数组长度
* @return 创建成功的循环双链表
*/
CDLNode *createByHead(CDLNode **list, int nums[], int n)
// 1. 初始化循环双链表,即创建头结点,也可以直接调用 init 方法进行初始化
// 1.1 为头结点分配空间
*list = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2 将头结点的 prior 指针指向自身
(*list)->prior = *list;// 注意,将头结点的 prior 和 next 指针指向自身
// 1.3 将头结点的 next 指针指向自身
(*list)->next = *list;
/// 2.循环输入数组 nums,将每个元素插入到链表中
for (int i = 0; i < n; i++)
// 2.1 创建新节点
// 2.1.1 给新节点分配空间
CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
// 2.1.2 给新节点的数据域指定内容
newNode->data = nums[i];
// 2.1.3 将新节点的 prior 指针指向 null
newNode->prior = NULL;
// 2.1.4 将新节点的 next 指针指向 null
newNode->next = NULL;
// 2.2 将新节点插入到链表的头部,即头结点的后面
// 2.2.1 修改新节点的 next 指针,指向原链表的第一个节点
newNode->next = (*list)->next;
// 2.2.2 修改新节点的 prior 指针,指向原链表的头结点
newNode->prior = *list;
// 2.2.3 将原链表的第一个节点的 prior 指针指向新节点
(*list)->next->prior = newNode;
// 2.2.4 将头节点的 next 指针指向新节点
(*list)->next = newNode;
// 3.返回创建成功的循环双链表
return *list;
createByTail
通过尾插法创建循环双链表。
以 nums=[1, 2, 3, 4, 5]; n=5
为例如图所示:
实现步骤:
- 初始化循环双链表。
- 声明一个变量
tailNode
记录循环双链表的尾结点。 - 循环遍历数组
nums
中的每个元素,根据元素值创建新结点,为其分配存储空间,并指定数据域和指针域(初始创建时新结点的两个指针域prior
和next
都指向NULL
)。 - 然后将新结点插入到链表的尾部称为新的尾结点,并且将新尾结点的
next
指针域指向链表的头结点,将头结点的prior
指针域指向链表的尾结点。
实现代码如下:
/**
* 通过尾插法创建循环双链表
* @param list 循环双链表
* @param nums 待插入到双链表中的数据数组
* @param n 数组长度
* @return 创建成功的循环双链表
*/
CDLNode *createByTail(CDLNode **list, int nums[], int n)
// 1. 初始化循环双链表,即创建头结点,也可以直接调用 init 方法进行初始化
// 1.1 为头结点分配空间
*list = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2 将头结点的 prior 指针指向自身
(*list)->prior = *list;// 注意,将头结点的 prior 和 next 指针指向自身
// 1.3 将头结点的 next 指针指向自身
(*list)->next = *list;
// 保存链表的尾节点,其实可以利用循环双链表的特性,直接使用 list.prior 来获得链表的尾节点
CDLNode *tailNode = *list;
// 2.循环输入数组 nums,将每个元素插入到链表中
for (int i = 0; i < n; i++)
// 2.1 创建新节点
// 2.1.1 给新节点分配空间
CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
// 2.1.2 给新节点的数据域指定内容
newNode->data = nums[i];
// 2.1.3 将新节点的 prior 指针指向 null
newNode->prior = NULL;
// 2.1.4 将新节点的 next 指针指向 null
newNode->next = NULL;
// 2.2 将新节点插入到链表的尾部
// 2.2.1 将链表原尾节点的 next 指针指向新节点
tailNode->next = newNode;
// 2.2.2 将新节点的 prior 指针指向链表原尾节点
newNode->prior = tailNode;
// 2.2.3 将新节点的 next 指针指向链表的头结点
newNode->next = *list;
// 2.2.4 注意,将头结点的 prior 指针指向新节点(即新的尾节点),这样才能循环起来
(*list)->prior = newNode;
// 2.2.5 更新记录链表尾节点的变量 tailNode
tailNode = newNode;
// 3.返回创建成功的链表
return *list;
insert
在循环双链表的第 i
个位置插入值为 ele
的新结点。以 list=[1, 2, 3, 4, 5]; i=3; ele=66
为例如图所示:
实现步骤:
- 参数校验,结点序号
i
必须在范围[1, length]
之间,否则返回 0 表示插入失败。 - 遍历循环双链表,找到链表中第
i
个结点,并用变量node
进行记录。 - 创建新结点
newNode
,分配存储空间,并为其指定数据域和指针域(初始时prior
和next
指针都指向NULL
),然后将新结点newNode
插入到第i-1
个结点和第i
个结点之间。注意,双链表结点的链接需要处理prior
和next
两个指针域。 - 最后返回 1 表示插入成功。
实现代码如下:
/**
* 在第 i 个位置插入新节点
* @param list 循环双链表
* @param i 待插入新节点的序号位置,从 1 开始
* @param ele 新节点的数据值
* @return 如果插入成功则返回 1,否则返回 0
*/
int insert(CDLNode **list, int i, int ele)
// 0.参数校验
if (i < 1 || i > size(*list))
return 0;
// 变量,记录链表的每一个节点,从第一个节点开始
CDLNode *node = (*list)->next;
// 变量,计数器,记录已经迭代的节点个数
int count = 0;
// 1.循环遍历链表,寻找第 i 个节点
while (node != *list)
// 1.1 计数器加 1,表示已经迭代一个节点
count++;
// 1.2 找到第 i 个节点
if (count == i)
// 1.2.1 创建新节点
// 1.2.1.1 为新节点分配空间
CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2.1.2 为新节点指定数据域
newNode->data = ele;
// 1.2.1.3 将新节点的 prior 指针指向 null
newNode->prior = NULL;
// 1.2.1.4 将新节点的 next 指针指向 null
newNode->next = NULL;
// 1.2.2 将新节点插入到链表中
// 1.2.2.1 将第 i-1 个节点的 next 指针指向新节点
node->prior->next = newNode;
// 1.2.2.2 将新节点的 prior 指针指向第 i-1 个节点(即第 i 个节点的前驱节点)
newNode->prior = node->prior;
// 1.2.2.3 将新节点的 next 指针指向第 i 个节点
newNode->next = node;
// 1.2.2.4 将第 i 个节点的 prior 指针指向新节点,此时完成了新节点与第 i 个节点的链接
node->prior = newNode;
return 1;
// 1.3 继续链表的下一个节点
node = node->next;
return 0;
insertFirst
在循环双链表的头部插入新结点(如果是带头结点的循环双链表,则插入到头结点的后面,原开始结点的前面;如果说是不带头结点的循环双链表,则插入到原开始结点的前面,成为新的开始结点)。以 list=[1, 2, 3, 4, 5]; ele=66
为例如图所示:
实现步骤:
- 声明变量
firstNode
记录原循环双链表的开始结点。 - 创建新结点
newNode
,为其分配空间,并指定数据域和指针域(prior
和next
都指向NULL
)。 - 将新结点
newNode
插入到头结点和原链表开始结点firstNode
之间。
实现代码如下:
/**
* 插入新节点在链表中第一个节点的位置
* @param list 循环双链表
* @param ele 新节点的数据值
*/
void insertFirst(CDLNode **list, int ele)
// 变量,记录链表的第一个节点
CDLNode *firstNode = (*list)->next;
// 1.创建新节点
// 1.1 为新节点分配空间
CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2 为新节点的数据域指定内容
newNode->data = ele;
// 1.3 将新节点的 prior 指针指向 null
newNode->prior = NULL;
// 1.4 将新节点的 next 指针指向 null
newNode->next = NULL;
// 2.将新节点插入到链表的头部,即头结点的后面
// 2.1 将新节点的 next 指针指向原链表的第一个节点
newNode->next = firstNode;
// 2.2 将原链表的第一个节点的 prior 指针指向新节点,此时完成了新节点与原链表第一个节点的链接
firstNode->prior = newNode;
// 2.3 将头结点的 next 指针指向新节点,即让新节点成为链表的第一个节点
(*list)->next = newNode;
// 2.4 将新节点的 prior 指针指向链表头结点,此时完成了新节点与链表头结点的链接
newNode->prior = *list;
insertLast
在循环双链表的尾部插入新结点。可以通过 head->prior
直接获取到循环双链表的尾结点。以 list=[1, 2, 3, 4, 5]; ele=66
为例如图所示:
实现步骤:
- 创建新结点
newNode
,为其分配存储空间,并为其指定数据域和两个指针域。 - 通过
head->next
获取链表的尾结点lastNode
,然后将新结点newNode
插入到尾结点lastNode
和头结点head
之间,形成循环链接。
实现代码如下:
/**
* 插入新节点在链表中最后一个节点的位置
* @param list 循环双链表
* @param ele 新节点的数据值
*/
void insertLast(CDLNode **list, int ele)
// 1.创建新节点
// 1.1 为新节点分配空间
CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
// 1.2 为新节点的数据域指定内容
newNode->data = ele;
// 1.3 将新节点的 prior 指针指向 null
newNode->prior = NULL;
// 1.4 将新节点的 next 指针指向 null
newNode->next = NULL;
// 2.把新节点插入到链表的尾部
// 2.1 变量,记录循环双链表的尾节点,这里就是通过 list.prior 获得的链表尾节点
CDLNode *lastNode = (*list)->prior;
// 2.2 将链表尾节点的 next 指针指向新节点
lastNode->next = newNode;
// 2.3 将新节点的 prior 指针指向原尾节点,此时完成了原链表尾节点与新节点的链接(链接成功后,新节点就成为了链表的尾节点)
newNode->prior = lastNode;
// 2.4 将链表头结点的 prior 指针指向新节点,为了循环
(*list)->prior = newNode;
// 2.5 将新节点的 next 指针指向头结点,为了循环
newNode->next = *list;
removeByNum
删除循环双链表中第 i
个结点,并用 ele
保存被删结点的数据值。以 list=[1, 2, 3, 4, 5]; i=3
为例如图所示:
实现步骤:
- 参数校验,首先如果循环双链表为空则无法删除,其次序号
i
应该在[1, length]
范围内,否则返回 0 表示删除失败。 - 扫描循环双链表,找到第
i
个结点node
,并通过其前驱指针prior
和后继指针next
找到第i-1
个结点preNode
和第i+1
个结点nextNode
。 - 然后删除结点
node
,用ele
保存被删结点数据值,并释放结点存储空间。
实现代码如下:
/**
* 删除第 i 个位置的节点
* @param list 循环双链表
* @param i 待删除节点的序号,从 1 开始
* @param ele 如果删除成功则返回被删除的节点,否则返回 null
*/
int removeByNum(CDLNode **list, int i, int *ele)
// 0.参数校验
// 0.1 判断链表是否为空
if ((*list)->next == *list)
return 0;
// 0.2 判断序号是否在范围内
if (i < 1 || i > size(*list))
return 0;
// 变量,记录链表中每个节点,初始为链表第一个节点
CDLNode *node = (*list)->next;
// 变量,记录已经迭代的节点个数
int count = 0;
// 1.循环遍历双链表,注意循环结束的条件
while (node != *list)
// 1.1 计数器加1
count++;
// 1.2 找到第 i 个节点
if (count == i)
// 1.2.1 局部变量,记录第 i-1 个节点
CDLNode *preNode = node->prior;
// 1.2.2 局部变量,记录第 i+1 个节点
CDLNode *nextNode = node->next;
// 1.2.3 将第 i-1 个节点的 next 指针指向第 i+1 个节点
preNode->next = nextNode;
// 1.2.4 将第 i+1 个节点的 prior 指针指向第 i-1 个节点
nextNode->prior = preNode;
// 1.2.5 保存被删除节点的数据值
*ele = node->data;
// 1.2.6 释放节点空间
free(node);
// 1.2.7 返回,结束程序
return 1;
// 1.3 继续链表的下一个节点
node = node->next;
return 0;
removeByEle
删除循环双链表中第一个值为 ele
的结点。
实现步骤:
- 参数校验,如果循环双链表为空表,则无法删除。
- 声明变量
node
来记录循环双链表结点,直到遇到数据域值为ele
的结点。 - 再声明局部变量
preNode
和nextNode
分别记录node
结点的前驱结点和后继结点。 - 删除结点
node
,并释放结点存储空间。
实现代码如下:
/**
* 删除值为 ele 的节点
* @param list 循环双链表
* @param ele 指定值
* @return 如果删除成功则返回 1,否则返回 0
*/
int removeByEle(CDLNode **list, int ele)
// 0.参数校验
if ((*list)->next == *list)
return 0;
// 变量,记录链表中的每一个节点,从链表的第一个节点开始
CDLNode *node = (*list)->next;
// 1.循环遍历链表所有节点
while (node != *list)
// 1.1 找到值等于 ele 的节点
if (node->data == ele)
// 1.1.1 局部变量,记录第 i-1 个节点
CDLNode *preNode = node->prior;
// 1.1.2 局部变量,记录第 i+1 个节点
CDLNode *nextNode = node->next;
// 1.1.3 将第 i-1 个节点的 next 指针指向第 i+1 个节点
preNode->next = nextNode;
// 1.1.4 将第 i+1 个节点的 prior 指针指向第 i-1 个节点
nextNode->prior = preNode;
// 1.1.5 释放节点空间
free(node);
// 1.1.6 结束程序
return 1;
// 1.2 继续链表的下一个节点
node = node->next;
return 0;
removeFirst
删除循环双链表的开始结点。
实现步骤:
- 参数校验,如果循环双链表为空表,则无法删除。
- 声明变量
firstNode
记录循环双链表的开始结点。 - 删除结点
firstNode
,即让firstNode->next
成为链表新的开始结点。即修改头结点的next
指针指向firstNode->next
,将firstNode->next
结点的prior
指针指向头结点。 - 用
ele
保存被删结点firstNode
的值,并释放结点空间。
实现代码如下:
/**
* 删除链表的第一个节点
* @param list 循环双链表
* @param ele 保存被删除节点的数据域值
*/
int removeFirst(CDLNode **list, int *ele)
// 0.参数校验
if ((*list)->next == *list)
return 0;
// 1.删除链表第一个节点
数据结构--线性表的链式存储之循环双链表
(王道408考研数据结构)第二章线性表-第三节2:双链表的定义及其操作(插入和删除)
数据结构与算法之PHP实现链表类(单链表/双链表/循环链表)