数据结构与算法专题之线性表——链表(三)循环链表
来源:互联网 发布:tenga 知乎 编辑:程序博客网 时间:2024/06/05 14:58
本文是线性表之链表第三弹——循环链表。在学习本章节之前,应该首先学习并掌握链表的概念及单链表的原理和实现,还未学习的小伙伴请移步上两篇文章,循序渐进才可以哦,传送门:
数据结构与算法专题之线性表——链表(一)单链表
数据结构与算法专题之线性表——链表(二)双向链表
好的,假设你已经拥有前置技能,下面我们开始学习循环链表~
循环链表的概念及结构
基本概念
循环链表,也就是循环的链表(好像是废话),也就是说,它的遍历可以循环起来,它可以是单向的,也可以是双向的,为了简化问题,我们以下所有实现均为单向循环链表,本章内容结合上一章双向链表,也可以写出双向循环链表,看你的咯。
循环链表与单链表的区别是,循环链表的尾指针指向的结点的next域并不是NULL,而是首元素,也就是head->next。这样,当遍历进行至链表末尾时,指针后移不会出现NULL,而是移至了链表首位,就形成了循环。
结点
这里是单向循环链表,所以结构与单链表完全一致,结构如下:
template<class T>struct Node{ T data; Node<T> *next;};
类结构
基本操作也与单链表一致,部分需要边界处理的地方有所不同,会再遇到时介绍。这里我们简化了一些操作,我们平时使用时可以根据实际场景来灵活开发其功能。
我们为其设置了一个内置的游标指针ptr,用来指定一个结点的前置结点,我们将del方法和get方法修改功能,改成将ptr指针移动index次后所指的位置的后继结点删除或获取,这是为了方便研究后面的约瑟夫环问题。
类的结构代码如下:
template<class T>class CList{private: Node<T> *head, *tail, *ptr; // ptr当前内置指针 int cnt;public: CList() { head = new Node<T>; ptr = tail = head; head->next = NULL; cnt = 0; } void push_back(T elem); // 向尾部插入 void del(int index); // ptr后移index次后删除所指元素的后继元素 void reset(); // 重置ptr指针 Node<T>* get(int index); // ptr后移index次后所指后继元素位置的指针 int size(); // 获取链表的大小};我们这里只研究代码中声明的五种方法。
循环链表的实现
1. 向尾部插入(push_back)
我们容易想到,一个空的循环链表与一个空的单链表结构完全一致,区别就在于插入元素以后对于尾结点的处理,下面是单链表插入元素后的样子,如图所示:
上图展示的是生成一个新结点p,使tail结点的指针域指向p然后再使tail指针指向p后的结果,也就是单链表push_front后的结果,想了解过程请移步第一章单链表的讲解。循环链表的插入,前面的步骤与单链表完全一致,会走到上图这一步,然后因为是循环,所以需要修改新tail->next为head->next,如图:
上图虽然没有画成环……但是确实是环形回路,可以看出,我们只要把单链表插入的方法稍微修改一下,即可变成循环链表,需要注意的是,如果插入前链表为空,那么插入后tail的指针域置为head->next,其实就是新元素本身,代码如下:
template<class T>void CList<T>::push_back(T elem) // 向尾部插入{ Node<T> *p = new Node<T>; p->data = elem; tail->next = p; tail = p; p->next = head->next; cnt++;}可以看出,上述操作对我们的游标指针ptr不会产生任何影响,所以不需要考游标指针。
2. 游标移动index次后删除元素(del)
我们知道,删除元素需要先获取到其前置结点,所以我们在内部声明游标指针的时候,默认“向前一位”,即内部游标指在位置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<class T>void CList<T>::del(int index) // ptr后移index次后删除所指元素后继元素{ if(index < 0 || cnt == 0) // 非法位置,忽略 { return ; } int i = index; while(i--) { ptr = ptr->next; } Node<T> *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--;}
3. 重置游标指针(reset)
为了使del具有通用性,假设当前游标不在头部,而在中间的某个位置,但是我想删除链表第i个元素,怎么办呢?这时候我们就需要先将游标归位到起始位置,然后再调用del(i),使游标移动i次,删除结点即可。
代码如下:
template<class T>void CList<T>::reset() // 重置ptr指针{ ptr = head;}
4. 游标移动index次后获取指针(get)
与del一样,移动index次后获取后继结点指针即可,代码:
template<class T>Node<T>* CList<T>::get(int index) // 获取ptr元素后继指针{ if(index < 0 || cnt == 0) // 非法位置,忽略 { return NULL; } int i = index; while(i--) ptr = ptr->next; return ptr->next;}
5. 获取链表长度(size)
返回内部计数器即可
template<class T>int CList<T>::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 <bits/stdc++.h>using namespace std;template<class T>struct Node{ T data; Node<T> *next;};template<class T>class CList{private: Node<T> *head, *tail, *ptr; // ptr当前内置指针 int cnt;public: CList() { head = new Node<T>; ptr = tail = head; head->next = NULL; cnt = 0; } void push_back(T elem); // 向尾部插入 void del(int index); // ptr后移index次后删除所指元素的后继元素 void reset(); // 重置ptr指针 Node<T>* get(int index); // ptr后移index次后所指后继元素位置的指针 int size(); // 获取链表的大小};template<class T>void CList<T>::push_back(T elem) // 向尾部插入{ Node<T> *p = new Node<T>; p->data = elem; tail->next = p; tail = p; p->next = head->next; cnt++;}template<class T>void CList<T>::del(int index) // ptr后移index次后删除所指元素后继元素{ if(index < 0 || cnt == 0) // 非法位置,忽略 { return ; } int i = index; while(i--) { ptr = ptr->next; } Node<T> *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<class T>void CList<T>::reset() // 重置ptr指针{ ptr = head;}template<class T>Node<T>* CList<T>::get(int index) // 获取ptr元素后继指针{ if(index < 0 || cnt == 0) // 非法位置,忽略 { return NULL; } int i = index; while(i--) ptr = ptr->next; return ptr->next;}template<class T>int CList<T>::size() // 获取链表的大小{ return cnt;}int main(){ int n,m; while(~scanf("%d %d", &n, &m)) { CList<int> 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 不敢死队问题
以上就是本章循环链表所有内容,感觉不是很重要,感觉链表的重点内容还是前面两章的单链表和双向链表,欢迎大家学习与交流~
下集预告&传送门:数据结构与算法专题之线性表——栈及其应用
- 数据结构与算法专题之线性表——链表(三)循环链表
- 数据结构与算法专题之线性表——链表(二)双向链表
- 数据结构与算法专题之线性表——链表(一)单链表
- 数据结构与算法专题之线性表——栈及其应用
- 数据结构与算法专题之线性表——队列及其应用
- 数据结构之线性结构(循环链表)【三】
- 数据结构与算法——线性表链式存储(双向循环链表)
- 算法与数据结构之三----循环链表
- 数据结构与算法之循环链表 <三>
- 数据结构与算法—循环链表
- 算法与数据结构之循环链表
- 数据结构专题——线性表
- 数据结构与算法学习之(三):线性表(下)
- 数据结构与算法之----线性表
- 数据结构与算法分析之线性表
- 数据结构与算法之线性表
- 【数据结构专题】线性表之顺序表
- 【数据结构专题】线性表之单链表
- 关于动态链库与动态链接库的心得
- 欢迎使用CSDN-markdown编辑器
- spring bean配置之集合属性
- hdu3333(线段树求不同数字之和)
- 51nod 1013 3的幂的和
- 数据结构与算法专题之线性表——链表(三)循环链表
- vb.net 教程 12-2 HtmlDocument类 3
- Java反射机制(一)
- C# Winform 窗体美化(十、自定义窗体)
- Python积累
- 0-1背包和完全背包问题辨析
- Qt开源项目
- Mybatis之逆向工程
- SqlSessionFactory(二)