本文是线性表之链表第三弹——循环链表。在学习本章节之前,应该首先学习并掌握链表的概念及单链表的原理和实现,还未学习的小伙伴请移步上两篇文章,循序渐进才可以哦,传送门:
数据结构与算法专题之线性表——链表(一)单链表
数据结构与算法专题之线性表——链表(二)双向链表
好的,假设你已经拥有前置技能,下面我们开始学习循环链表~
循环链表,也就是循环的链表(好像是废话),也就是说,它的遍历可以循环起来,它可以是单向的,也可以是双向的,为了简化问题,我们以下所有实现均为单向循环链表,本章内容结合上一章双向链表,也可以写出双向循环链表,看你的咯。
循环链表与单链表的区别是,循环链表的尾指针指向的结点的next域并不是NULL,而是首元素,也就是head->next。这样,当遍历进行至链表末尾时,指针后移不会出现NULL,而是移至了链表首位,就形成了循环。
这里是单向循环链表,所以结构与单链表完全一致,结构如下:
template
struct Node
{
T data;
Node *next;
};
基本操作也与单链表一致,部分需要边界处理的地方有所不同,会再遇到时介绍。这里我们简化了一些操作,我们平时使用时可以根据实际场景来灵活开发其功能。
我们为其设置了一个内置的游标指针ptr,用来指定一个结点的前置结点,我们将del方法和get方法修改功能,改成将ptr指针移动index次后所指的位置的后继结点删除或获取,这是为了方便研究后面的约瑟夫环问题。
类的结构代码如下:
template
class CList
{
private:
Node *head, *tail, *ptr; // ptr当前内置指针
int cnt;
public:
CList()
{
head = new Node;
ptr = tail = head;
head->next = NULL;
cnt = 0;
}
void push_back(T elem); // 向尾部插入
void del(int index); // ptr后移index次后删除所指元素的后继元素
void reset(); // 重置ptr指针
Node* get(int index); // ptr后移index次后所指后继元素位置的指针
int size(); // 获取链表的大小
};
我们这里只研究代码中声明的五种方法。
我们容易想到,一个空的循环链表与一个空的单链表结构完全一致,区别就在于插入元素以后对于尾结点的处理,下面是单链表插入元素后的样子,如图所示:
上图展示的是生成一个新结点p,使tail结点的指针域指向p然后再使tail指针指向p后的结果,也就是单链表push_front后的结果,想了解过程请移步第一章单链表的讲解。循环链表的插入,前面的步骤与单链表完全一致,会走到上图这一步,然后因为是循环,所以需要修改新tail->next为head->next,如图:
上图虽然没有画成环……但是确实是环形回路,可以看出,我们只要把单链表插入的方法稍微修改一下,即可变成循环链表,需要注意的是,如果插入前链表为空,那么插入后tail的指针域置为head->next,其实就是新元素本身,代码如下:
template
void CList::push_back(T elem) // 向尾部插入
{
Node *p = new Node;
p->data = elem;
tail->next = p;
tail = p;
p->next = head->next;
cnt++;
}
可以看出,上述操作对我们的游标指针ptr
不会产生任何影响,所以不需要考游标指针。
我们知道,删除元素需要先获取到其前置结点,所以我们在内部声明游标指针的时候,默认“向前一位”,即内部游标指在位置i-1,宏观上我们认为游标指向的是i。所以从封装外看,我们是将游标移动index次删除所指结点,内部实现的时候其实是游标移动index次后删除它指的后继结点,这样做可以使代码更为简单且避免诸多特殊情况。
我们先不考虑删除的两种特例——即删除首元素和尾元素,如果删除的是中间的元素,那么完全与单链表删除一致,ptr目前指向待删元素的前置结点,构造一个p=ptr->next,p即是待删元素,使ptr->next=p,即可将p从链中剔除,然后释放p即可 。
现在我们考虑刚才的两种特殊情况:
(1) 如果删除的元素是尾元素
即tail所指的元素,那么我们的tail指针应该前移,指向ptr,因为尾元素删了,它的前置结点便成了尾元素。
(2) 如果删除的元素是首元素
即head->next,那么我们应该重置head结点的指针域为首元素的后置结点,即head->next=ptr->next
(3) 其实还有一种特殊情况:即链表只有一个元素
此时,删除的结点即是首节点又是尾结点,所以删除后就变成了空链表,应该将head指针域置空,并且tail指针和ptr游标指针全部归位。
有点不太好理解,我觉得纸上画图操作胜过一切,代码如下:
template
void CList::del(int index) // ptr后移index次后删除所指元素后继元素
{
if(index < 0 || cnt == 0) // 非法位置,忽略
{
return ;
}
int i = index;
while(i--)
{
ptr = ptr->next;
}
Node *p = ptr->next; // 获取待删元素指针
ptr->next = p->next;
if(p == tail) // 如果删除的元素是最后一个
{
tail = ptr; // 修改尾指针指向为ptr(删除元素的前置)
}
if(p == head->next) // 如果删除的是第一个
{
head->next = ptr->next;
}
if(cnt == 1) // 最后一个
{
head->next = NULL;
ptr = tail = head;
}
delete p;
cnt--;
}
为了使del具有通用性,假设当前游标不在头部,而在中间的某个位置,但是我想删除链表第i个元素,怎么办呢?这时候我们就需要先将游标归位到起始位置,然后再调用del(i),使游标移动i次,删除结点即可。
代码如下:
template
void CList::reset() // 重置ptr指针
{
ptr = head;
}
与del一样,移动index次后获取后继结点指针即可,代码:
template
Node* CList::get(int index) // 获取ptr元素后继指针
{
if(index < 0 || cnt == 0) // 非法位置,忽略
{
return NULL;
}
int i = index;
while(i--)
ptr = ptr->next;
return ptr->next;
}
返回内部计数器即可
template
int CList::size() // 获取链表的大小
{
return cnt;
}
这里就不贴完整代码了,下面的应用问题代码会完全使用刚才实现的循环链表。
这是一个比较经典的问题,说有n个人玩死亡游戏,n个人编号1~n并依次排列围成一个圈,从1号开始数,每数到第m个人,那个人就被杀死,然后接着刚才那个人的下一个人继续数,直到剩下一个人,问给定一个n和m,求最后活下来的人的编号。
分析,我们这里需要注意的一个问题是,从第一个人开始数m次,假设n>m,那么第m个人的编号应该是m。回到我们上面的代码,看del方法,是不是调用del(m)就可删除第m个结点呢?答案是否定的,因为我们的游标初始是在首元素,而题目中,“首元素”那个人是第1个人,但是对于游标来说,是游标后第0个元素,这也是计算机编号跟我们日常习惯的编号的差别之处,以后也会遇到很多关于“是从0开始还是从1开始?是n还是n-1?”之类的问题,我们只要举几个实际例子带入比较,就可以得出答案。
所以我们要利用上面实现过的循环链表来做此题的话,就应该先构造一个空链表,并把1~n这n个编号依次加入链表中,并循环调用del(m-1)来删除链表元素,直至size为1,输出首元素的值即可。
代码如下,留自己思考:
#include
using namespace std;
template
struct Node
{
T data;
Node *next;
};
template
class CList
{
private:
Node *head, *tail, *ptr; // ptr当前内置指针
int cnt;
public:
CList()
{
head = new Node;
ptr = tail = head;
head->next = NULL;
cnt = 0;
}
void push_back(T elem); // 向尾部插入
void del(int index); // ptr后移index次后删除所指元素的后继元素
void reset(); // 重置ptr指针
Node* get(int index); // ptr后移index次后所指后继元素位置的指针
int size(); // 获取链表的大小
};
template
void CList::push_back(T elem) // 向尾部插入
{
Node *p = new Node;
p->data = elem;
tail->next = p;
tail = p;
p->next = head->next;
cnt++;
}
template
void CList::del(int index) // ptr后移index次后删除所指元素后继元素
{
if(index < 0 || cnt == 0) // 非法位置,忽略
{
return ;
}
int i = index;
while(i--)
{
ptr = ptr->next;
}
Node *p = ptr->next; // 获取待删元素指针
ptr->next = p->next;
if(p == tail) // 如果删除的元素是最后一个
{
tail = ptr; // 修改尾指针指向为ptr(删除元素的前置)
}
if(p == head->next) // 如果删除的是第一个
{
head->next = ptr->next;
}
if(cnt == 1) // 最后一个
{
head->next = NULL;
ptr = tail = head;
}
delete p;
cnt--;
}
template
void CList::reset() // 重置ptr指针
{
ptr = head;
}
template
Node* CList::get(int index) // 获取ptr元素后继指针
{
if(index < 0 || cnt == 0) // 非法位置,忽略
{
return NULL;
}
int i = index;
while(i--)
ptr = ptr->next;
return ptr->next;
}
template
int CList::size() // 获取链表的大小
{
return cnt;
}
int main()
{
int n,m;
while(~scanf("%d %d", &n, &m))
{
CList lst;
for(int i = 1 ; i <= n ; i++)
lst.push_back(i);
while(lst.size() > 1)
{
lst.del(m - 1); // 考虑这里为什么是m-1
}
printf("%d\n", lst.get(0)->data);
}
return 0;
}
附上题目链接以及另一个练习的传送门:
SDUT OJ 1197 约瑟夫问题
SDUT OJ 2056 不敢死队问题
以上就是本章循环链表所有内容,感觉不是很重要,感觉链表的重点内容还是前面两章的单链表和双向链表,欢迎大家学习与交流~
下集预告&传送门:数据结构与算法专题之线性表——栈及其应用