前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >数据结构基础温故-1.线性表(下)

数据结构基础温故-1.线性表(下)

作者头像
Edison Zhou
发布于 2018-08-20 08:24:24
发布于 2018-08-20 08:24:24
44400
代码可运行
举报
文章被收录于专栏:EdisonTalkEdisonTalk
运行总次数:0
代码可运行

上一篇中,我们了解了单链表与双链表,本次将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)。

一、循环链表基础

1.1 循环链表节点结构

  循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p.next是否为空,现在则是p.next不等于头结点,则循环未结束。

1.2 循环链表的O(1)访问时间

  在单链表中,有了头结点,我们可以在O(1)时间访问到第一个节点,但如果要访问最后一个节点却需要O(n)的时间,因为我们需要对整个链表进行一次遍历。在循环链表中,我们可以借助尾节点来实现,即不用头指针,而是用指向终端结点的尾指针来表示循环链表,这时候无论是查找第一个节点还是最后一个节点都很方便,可以控制在O(1)的时间内,如下图所示。

  从上图中可以看到,终端结点用尾指针(tail)指示,则查找终端结点是O(1),而开始结点,其实就是tail.Next,其时间复杂也为O(1)。由此也可以联想到,在合并两个循环链表时,只需要修改两个链表的尾指针即可快速地进行合并。

二、循环链表实现

2.1 循环链表节点的定义实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class CirNode<T>
    {
        public T Item { get; set; }
        public CirNode<T> Next { get; set; }

        public CirNode()
        {
        }

        public CirNode(T item)
        {
            this.Item = item;
        }
    }

  这里跟单链表的节点定义实现并无区别。

2.2 循环链表新节点的插入实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        public void Add(T value)
        {
            CirNode<T> newNode = new CirNode<T>(value);
            if (this.tail == null)
            {
                // 如果链表当前为空则新元素既是尾头结点也是头结点
                this.tail = newNode;
                this.tail.Next = newNode;
                this.currentPrev = newNode;
            }
            else
            {
                // 插入到链表末尾处
                newNode.Next = this.tail.Next;
                this.tail.Next = newNode;
                // 改变当前节点
                if (this.currentPrev == this.tail)
                {
                    this.currentPrev = newNode;
                }
                // 重新指向新的尾节点
                this.tail = newNode;
            }

            this.count++;
        }

  首先,这里的currentPrev字段是使用了前驱节点来标识当前节点,如要获取当前节点的值可以通过currentPrev.Next.Item来获得。其次,在最后将尾节点指针指向新插入的节点。

2.2 循环链表当前节点的移除实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        public void Remove()
        {
            if (this.tail == null)
            {
                throw new NullReferenceException("链表中没有任何元素");
            }
            else if (this.count == 1)
            {
                // 只有一个元素时将两个指针置为空
                this.tail = null;
                this.currentPrev = null;
            }
            else
            {
                if (this.currentPrev.Next == this.tail)
                {
                    this.tail = this.currentPrev;
                }
                // 移除当前节点
                this.currentPrev.Next = this.currentPrev.Next.Next;
            }

            this.count--;
        }

  这里考虑到删除节点时必须寻找其前驱节点会导致链表进行遍历,故使用了当前节点的前驱节点来标识这个当前节点。移除当前节点只需要currentPrev.Next = currentPrev.Next.Next即可。

  以下是单循环链表的完整模拟实现代码,需要注意的是该CircularLinkedList主要是为下面的约瑟夫问题而设计,故只实现了一些很简单的功能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 单向循环链表的模拟实现
    /// </summary>
    public class MyCircularLinkedList<T>
    {
        private int count; // 字段:记录数据元素个数
        private CirNode<T> tail; // 字段:记录尾节点的指针
        private CirNode<T> currentPrev; // 字段:使用前驱节点标识当前节点

        // 属性:指示链表中元素的个数
        public int Count
        {
            get
            {
                return this.count;
            }
        }

        // 属性:指示当前节点中的元素值
        public T CurrentItem
        {
            get
            {
                return this.currentPrev.Next.Item;
            }
        }

        public MyCircularLinkedList()
        {
            this.count = 0;
            this.tail = null;
        }

        public bool IsEmpty()
        {
            return this.tail == null;
        }

        // Method01:根据索引获取节点
        private CirNode<T> GetNodeByIndex(int index)
        {
            if (index < 0 || index >= this.count)
            {
                throw new ArgumentOutOfRangeException("index", "索引超出范围");
            }

            CirNode<T> tempNode = this.tail.Next;
            for (int i = 0; i < index; i++)
            {
                tempNode = tempNode.Next;
            }

            return tempNode;
        }

        // Method02:在尾节点后插入新节点
        public void Add(T value)
        {
            CirNode<T> newNode = new CirNode<T>(value);
            if (this.tail == null)
            {
                // 如果链表当前为空则新元素既是尾头结点也是头结点
                this.tail = newNode;
                this.tail.Next = newNode;
                this.currentPrev = newNode;
            }
            else
            {
                // 插入到链表末尾处
                newNode.Next = this.tail.Next;
                this.tail.Next = newNode;
                // 改变当前节点
                if (this.currentPrev == this.tail)
                {
                    this.currentPrev = newNode;
                }
                // 重新指向新的尾节点
                this.tail = newNode;
            }

            this.count++;
        }

        // Method03:移除当前所在节点
        public void Remove()
        {
            if (this.tail == null)
            {
                throw new NullReferenceException("链表中没有任何元素");
            }
            else if (this.count == 1)
            {
                // 只有一个元素时将两个指针置为空
                this.tail = null;
                this.currentPrev = null;
            }
            else
            {
                if (this.currentPrev.Next == this.tail)
                {
                    // 当删除的是尾指针所指向的节点时
                    this.tail = this.currentPrev;
                }
                // 移除当前节点
                this.currentPrev.Next = this.currentPrev.Next.Next;
            }

            this.count--;
        }

        // Method04:获取所有节点信息
        public string GetAllNodes()
        {
            if (this.count == 0)
            {
                throw new NullReferenceException("链表中没有任何元素");
            }
            else
            {
                CirNode<T> tempNode = this.tail.Next;
                string result = string.Empty;
                for (int i = 0; i < this.count; i++)
                {
                    result += tempNode.Item + " ";
                    tempNode = tempNode.Next;
                }

                return result;
            }
        }
    }

2.3 单循环链表的简单测试

  这里的简单测试主要涉及:1.顺序插入5个节点,看节点元素是否正确;2.查看当前节点是否正确;3.移除某个元素,查看当前节点是否正确;测试代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        static void MyCircularLinkedListTest()
        {
            MyCircularLinkedList<int> linkedList = new MyCircularLinkedList<int>();
            // 顺序插入5个节点
            linkedList.Add(1);
            linkedList.Add(2);
            linkedList.Add(3);
            linkedList.Add(4);
            linkedList.Add(5);

            Console.WriteLine("All nodes in the circular linked list:");
            Console.WriteLine(linkedList.GetAllNodes());
            Console.WriteLine("--------------------------------------");
            // 当前节点:第一个节点
            Console.WriteLine("Current node in the circular linked list:");
            Console.WriteLine(linkedList.CurrentItem);
            Console.WriteLine("--------------------------------------");
            // 移除当前节点(第一个节点)
            linkedList.Remove();
            Console.WriteLine("After remove the current node:");
            Console.WriteLine(linkedList.GetAllNodes());
            Console.WriteLine("Current node in the circular linked list:");
            Console.WriteLine(linkedList.CurrentItem);
            // 移除当前节点(第二个节点)
            linkedList.Remove();
            Console.WriteLine("After remove the current node:");
            Console.WriteLine(linkedList.GetAllNodes());
            Console.WriteLine("Current node in the circular linked list:");
            Console.WriteLine(linkedList.CurrentItem);
            Console.WriteLine("--------------------------------------");

            Console.WriteLine();
        }

  测试结果如下所示:

三、循环链表与约瑟夫问题

3.1 何为约瑟夫问题

  据说著名犹太历史学家 Josephus 有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而 Josephus 和他的朋友并不想遵从,Josephus 要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

  以上就是著名的约瑟夫问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下Q个。从围成一圈这里就启发了我们可以使用循环链表来解决该问题。

3.2 使用循环链表解决约瑟夫问题

  (1)为CircularLinkedList添加Move()方法实现让当前节点向前移动N步

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        public void Move(int step = 1)
        {
            if (step < 1)
            {
                throw new ArgumentOutOfRangeException("step", "移动步数不能小于1");
            }

            for (int i = 1; i < step; i++)
            {
                currentPrev = currentPrev.Next;
            }
        }

  注意到这里循环是从1开始,因为currentPrev是当前节点的前驱节点,而不是真正的当前节点。

  (2)在Main()方法中添加测试代码验证是否能够正确让元素出列

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        static void JosephusTest()
        {
            MyCircularLinkedList<int> linkedList = new MyCircularLinkedList<int>();
            string result = string.Empty;

            Console.WriteLine("Step1:请输入人数N");
            int n = Convert.ToInt32(Console.ReadLine());
            Console.WriteLine("Step2:请输入数字M");
            int m = Convert.ToInt32(Console.ReadLine());
            Console.WriteLine("Step3:报数游戏开始");
            // 添加参与人员元素
            for (int i = 1; i <= n; i++)
            {
                linkedList.Add(i);
            }
            // 打印所有参与人员
            Console.Write("所有参与人员:{0}", linkedList.GetAllNodes());
            Console.WriteLine("\r\n" + "-------------------------------------");
            result = string.Empty;

            while (linkedList.Count > 1)
            {
                // 依次报数:移动
                linkedList.Move(m);
                // 记录出队人员
                result += linkedList.CurrentItem + " ";
                // 移除人员出队
                linkedList.Remove();
                Console.WriteLine();
                Console.Write("剩余报数人员:{0}", linkedList.GetAllNodes());
                Console.Write("  开始报数人员:{0}", linkedList.CurrentItem);
            }
            Console.WriteLine("\r\n" + "Step4:报数游戏结束");
            Console.WriteLine("出队人员顺序:{0}", result + linkedList.CurrentItem);
        }

  运行结果下图所示:

  ①N=10,M=4时:

  ②N=41,M=3时:

  从上图结果的出队人员顺序也可以看出,约瑟夫将自己和朋友安排在第16和第31个位置是在最后出队的,就只剩他俩好基友了,死不死就不是犹太人说了算了,又可以风骚地在一起“搞基”了。

3.3 使用LinkedList<T>解决约瑟夫问题

  在实际应用中,我们一般都会使用.NET中自带的数据结构类型来解决一般问题,这里我们就试着用LinkedList<T>来解决约瑟夫问题。

  (1)定义一个Person类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }
    }

  (2)初始化LinkedList集合

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        static LinkedList<Person> InitPersonList(int count)
        {
            LinkedList<Person> personList = new LinkedList<Person>();
            for (int i = 1; i <= count; i++)
            {
                Person person = new Person();
                person.Id = i;
                person.Name = "Counter-" + i.ToString();

                personList.AddLast(person);
            }

            return personList;
        }

  (3)由于LinkedList是双向链表,但不是循环链表,因此这里需要做一下判断

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        static void JosephusTestWithLinkedList()
        {
            Console.WriteLine("请输入人数N");
            int n = Convert.ToInt32(Console.ReadLine());
            Console.WriteLine("请输入数字M");
            int m = Convert.ToInt32(Console.ReadLine());
            Console.WriteLine("-------------------------------------");

            LinkedList<Person> linkedList = InitPersonList(n);

            LinkedListNode<Person> startNode = linkedList.First;
            LinkedListNode<Person> removeNode;

            while(linkedList.Count >= 1)
            {
                for (int i = 1; i < m; i++)
                {
                    if (startNode != linkedList.Last)
                    {
                        startNode = startNode.Next;
                    }
                    else
                    {
                        startNode = linkedList.First;
                    }
                }
                // 记录出队人员节点
                removeNode = startNode;
                // 打印出队人员ID号
                Console.Write(removeNode.Value.Id + " ");
                // 确定下一个开始报数人员
                if (startNode == linkedList.Last)
                {
                    startNode = linkedList.First;
                }
                else
                {
                    startNode = startNode.Next;
                }
                // 移除出队人员节点
                linkedList.Remove(removeNode);
            }
            Console.WriteLine();
        }

  这里使用startNode记录开始报数的人员节点,removeNode则记录要出队的人员节点。这里在确定下一个开始报数人员时通过手动判断LinkedList的当前节点是否已经达到了尾节点,如果是则转到头结点进行报数。最后将removeNode从LinkedList中移除即可。最终的运行结果如下图所示:

  ①N=10,M=4时:

  ②N=41,M=3时:

PS:解决问题的思路和实现多种多样,这里给出的仅仅是最最普通的一种。

参考资料

(1)程杰,《大话数据结构》

(2)陈广,《数据结构(C#语言描述)》

(3)段恩泽,《数据结构(C#语言版)》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2015-07-03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数据结构基础温故-1.线性表(中)
在上一篇中,我们学习了线性表最基础的表现形式-顺序表,但是其存在一定缺点:必须占用一整块事先分配好的存储空间,在插入和删除操作上需要移动大量元素(即操作不方便),于是不受固定存储空间限制并且可以进行比较快捷地插入和删除操作的链表横空出世,所以我们就来复习一下链表。
Edison Zhou
2018/08/20
5030
数据结构基础温故-1.线性表(中)
前端学习 数据结构与算法 快速入门 系列 —— 链表(转载非原创)
转载来源:https://www.cnblogs.com/pengjiali/p/15320535.html
xlj
2021/09/23
8531
数据结构与算法(二)——线性表
所谓顺序存储,就是开辟一段连续的内存空间来存储。因此,线性表的顺序存储,其逻辑相邻,物理存储地址也相邻。
拉维
2022/03/28
3540
数据结构与算法(二)——线性表
【数据结构】线性表----链表详解
前面我们介绍的顺序表,在逻辑结构和物理结构上都是线性、连续的关系,那么我们在篇尾的时候也提到了是否有一种顺序表,无需在物理结构上连续,从而达到更好存取的目的呢?今天它来了。
Skrrapper
2024/06/18
1170
【数据结构】线性表----链表详解
【数据结构】链表(C++)
链表是线性表的链式存储方式,逻辑上相邻的数据在计算机中的内存位置不必须相邻,给每一个元素 加一个指针域,指向下一个元素的位置。
半生瓜的blog
2023/05/13
4610
【数据结构】链表(C++)
【愚公系列】2023年11月 数据结构(二)-链表
数据结构是计算机科学中的一个重要概念,它描述了数据之间的组织方式和关系,以及对这些数据的访问和操作。常见的数据结构有:数组、链表、栈、队列、哈希表、树、堆和图。
愚公搬代码
2023/11/01
3350
重学数据结构(一、线性表)
线性表是最常见也是最简单的一种数据结构。简言之, 线性表是n个数据元素的有限序列。 其一般描述为:
三分恶
2020/08/21
7360
重学数据结构(一、线性表)
怒肝 JavaScript 数据结构 — 循环链表篇
其实总结起来,我们讲到的链表也好,双向链表也好,组成每一个链表的元素就好比一个一个铁环,连接起来,就组成了一条长长的铁链。
杨成功
2022/09/22
3510
怒肝 JavaScript 数据结构 — 循环链表篇
《学习JavaScript数据结构与算法》-- 3.链表(笔记)
链表,是存储有序的元素集合。不同于数组,链表中的元素在内存中并不是连续放置的,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
爱学习的程序媛
2022/04/07
2090
《学习JavaScript数据结构与算法》-- 3.链表(笔记)
JS数据结构第三篇---双向链表和循环链表之约瑟夫问题
在上文《JS数据结构第二篇---链表》中描述的是单向链表。单向链表是指每个节点都存有指向下一个节点的地址,双向链表则是在单向链表的基础上,给每个节点增加一个指向上一个节点的地址。然后头结点的上一个节点,和尾结点的下一个节点都指向null。同时LinkedList类中再增加一个last内部属性,一直指向链表中最后一个节点。结构模拟如图:
tandaxia
2019/07/01
7290
JS数据结构第三篇---双向链表和循环链表之约瑟夫问题
Algorithms_基础数据结构(02)_线性表之链表_单向链表
N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。
小小工匠
2021/08/17
3610
数据结构-链表
链表结构五花八门,今天我重点给你介绍三种最常见的链表结构,它们分别是:单链表、双向链表和循环链表。我们首先来看最简单、最常用的单链表。
花落花相惜
2021/11/25
4090
剑指Offer面试题:4.从尾到头打印链表
  到解决这个问题肯定要遍历链表。遍历的顺序是从头到尾的顺序,可输出的顺序却是从尾到头。也就是说第一个遍历到的结点最后一个输出,而最后一个遍历到的结点第一个输出。这就是典型的“后进先出”,我们可以用栈实现这种顺序。
Edison Zhou
2018/08/20
4580
剑指Offer面试题:4.从尾到头打印链表
数据结构基础温故-4.树与二叉树(中)
在上一篇中,我们了解了树的基本概念以及二叉树的基本特点和代码实现,还用递归的方式对二叉树的三种遍历算法进行了代码实现。但是,由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多。而且,如果递归深度太大,可能系统撑不住。因此,我们使用非递归(这里主要是循环,循环方法比递归方法快, 因为循环避免了一系列函数调用和返回中所涉及到的参数传递和返回值的额外开销)来重新实现一遍各种遍历算法,再对二叉树的另外一种特殊的遍历—层次遍历进行实现,最后再了解一下特殊的二叉树—二叉查找树。
Edison Zhou
2018/08/20
5860
数据结构基础温故-4.树与二叉树(中)
数据结构与算法(四)-线性表之循环链表
前言:前面几篇介绍了线性表的顺序和链式存储结构,其中链式存储结构为单向链表(即一个方向的有限长度、不循环的链表),对于单链表,由于每个节点只存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说只能向后走,如果走过了,就回不去了,还得重头开始遍历,所以就衍生出了循环链表
2018/09/26
1.3K0
数据结构与算法(四)-线性表之循环链表
【数据结构】LinkedList与链表
ArrayList的底层是一段连续空间的数组,在ArrayList位置任意位置插入或者删除元素时,就需要将后续元素整体往前或者往后移动,时间复杂度为O(n),效率较低。所以ArrayList不适合做任意位置插入和删除比较多的场景。因此,java集合中又引入了LinkList,即链表结构。
xxxflower
2023/04/16
3560
【数据结构】LinkedList与链表
JS 循环链表
循环链表是一种链表的变体,其中链表中的最后一个节点指向链表的头节点,形成一个循环或环状结构。
肥晨
2023/06/27
1950
数据结构与算法(四)——双向链表&双向循环链表
此时,比如我已经获取到了C节点,那么我想要获取到C节点的前一个节点,就需要再次遍历该链表,且时间复杂度是O(n)。那么有没有一个好的方案可以便捷地获取到C的前一个节点呢,答案是使用双向链表。
拉维
2022/03/28
4920
数据结构与算法(四)——双向链表&双向循环链表
「中高级前端」窥探数据结构的世界- ES6版
数据结构是在计算机中组织和存储数据的一种特殊方式,使得数据可以高效地被访问和修改。更确切地说,数据结构是数据值的集合,表示数据之间的关系,也包括了作用在数据上的函数或操作。
coder_koala
2019/08/15
8640
《Java初阶数据结构》----3.<线性表---LinkedList与链表>
3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
用户11288958
2024/09/24
830
《Java初阶数据结构》----3.<线性表---LinkedList与链表>
推荐阅读
相关推荐
数据结构基础温故-1.线性表(中)
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文