数据结构与算法专题之线性表——链表(三)循环链表

  本文是线性表之链表第三弹——循环链表。在学习本章节之前,应该首先学习并掌握链表的概念及单链表的原理和实现,还未学习的小伙伴请移步上两篇文章,循序渐进才可以哦,传送门:
  数据结构与算法专题之线性表——链表(一)单链表

  数据结构与算法专题之线性表——链表(二)双向链表




  好的,假设你已经拥有前置技能,下面我们开始学习循环链表~

循环链表的概念及结构

基本概念

  循环链表,也就是循环的链表(好像是废话),也就是说,它的遍历可以循环起来,它可以是单向的,也可以是双向的,为了简化问题,我们以下所有实现均为单向循环链表,本章内容结合上一章双向链表,也可以写出双向循环链表,看你的咯。

  循环链表与单链表的区别是,循环链表的尾指针指向的结点的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 不敢死队问题


  以上就是本章循环链表所有内容,感觉不是很重要,感觉链表的重点内容还是前面两章的单链表和双向链表,欢迎大家学习与交流~


  下集预告&传送门:数据结构与算法专题之线性表——栈及其应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值