C#进阶学习(六)单向链表和双向链表,循环链表(下)循环链表

      

目录

📊 链表三剑客:特性全景对比表

一、循环链表节点类

二、循环链表的整体设计框架

三、循环列表中的重要方法:

(1)头插法,在头结点前面插入新的节点

(2)尾插法实现插入元素:

 (3)删除头结点:

(4)删除第一个指定数据的节点

 (5)检查链表中是否存在指定数据这个就简单了,直接遍历,找到了就返回true没找到就返回false

(6) 更新节点值

(7)实现一个迭代器,方便遍历链表元素

(8)在指定索引插入值 

四、测试

五、总结

循环链表核心解析

1. 结构特性

2. 操作逻辑与实现要点

节点插入

节点删除

3. 复杂度与性能

4. 应用场景

5. 边界处理与易错点

6. 对比与选型

7. 设计启示


   

        前面我们已经会晤了单向链表与双向链表,今天我们来会会循环链表,其实循环链表就是将单向链表的尾指针指向了头结点,那么如何保证不死循环呢,我们一起来看看吧。

        循环链表是一种特殊的链表结构,其尾节点的指针不再指向null,而是指向头节点形成闭环:

  • 单向循环链表:尾节点.next = 头节点

  • 双向循环链表:尾节点.next = 头节点,头节点.prev = 尾节点

        如果关于循环链表还有不了解的读者可以先去看下这篇文章:

       线性表的说明

三种链表的对比:

📊 链表三剑客:特性全景对比表

对比维度单向链表双向链表循环链表
结构示意图A → B → C → null←A ↔ B ↔ C→A → B → C → [HEAD]
指针方向单方向(Next)双方向(Prev/Next)单/双方向 + 闭环
头节点访问O(1)O(1)O(1)
尾节点访问O(n)O(1)(维护尾指针时)O(n)(可通过设计优化到O(1))
插入操作头插O(1),尾插O(n)头尾插入均可O(1)头尾插入均可O(n)(需维护闭环)
删除操作需要前驱节点(平均O(n))可直接删除(O(1))类似单向链表但需维护闭环
内存开销最低(每个节点1指针)较高(每个节点2指针)与单向相同,但需额外闭环指针
遍历方向单向双向单向/双向 + 循环
核心优势结构简单,内存高效快速反向遍历,删除高效天然支持循环操作
经典应用场景栈、简单队列LRU缓存、浏览器历史记录轮询调度、音乐循环播放
边界处理复杂度简单(只需判断null)中等(需处理双向指针)较高(闭环维护易出错)
代码示例特征while (current != null)node.Prev.Next = node.Nextdo {...} while (current != head)

一、循环链表节点类

    /// <summary>/// 循环链表节点类/// </summary>/// <typeparam name="T">节点数据类型</typeparam>public class CircularNode<T>{/// <summary>/// 节点存储的数据/// </summary>public T Data { get; set; }/// <summary>/// 指向下一个节点的指针/// </summary>public CircularNode<T> Next { get; set; }/// <summary>/// 节点构造函数/// </summary>/// <param name="data">节点初始数据</param>/// <remarks>/// 初始化时将Next指向自身,形成最小闭环/// 当链表只有一个节点时,构成自环结构/// </remarks>public CircularNode(T data){Data = data;Next = this; // 关键闭环操作}}

二、循环链表的整体设计框架

/// <summary>
/// 单向循环链表实现类
/// </summary>
/// <typeparam name="T">链表元素类型</typeparam>
public class CircularLinkedList<T>
{/// <summary>/// 链表头节点(关键指针)/// </summary>private CircularNode<T> head;/// <summary>/// 节点计数器(优化统计效率)/// </summary>private int count;/// <summary>/// 链表元素数量(O(1)时间复杂度)/// </summary>public int Count => count;/// <summary>/// 判断链表是否为空/// </summary>public bool IsEmpty => head == null;/// <summary>/// 打印链表内容(调试用方法)/// </summary>public void PrintAll(){if (head == null){Console.WriteLine("[Empty List]");return;}var current = head;do{Console.Write($"{current.Data} -> ");current = current.Next;} while (current != head);  // 循环终止条件判断Console.WriteLine("[HEAD]");  // 闭环标记}
}

三、循环列表中的重要方法:

(1)头插法,在头结点前面插入新的节点

假设我们有这样一个循环链表:

那么我们如何利用头插法进行插入新节点呢:

        ①先创建一个新节点

        ②判断当前链表是否为空,为空则将当前新节点置为头结点

        ③当前链表存在,则,首先找到尾节点,然后将新节点指向原先的头结点,接着将新节点覆盖原先头结点,最后将尾节点指向新的头结点即可:如下图所示

        ④计数器++

代码实现:

/// <summary>
/// 在链表头部插入新节点
/// </summary>
/// <param name="data">插入数据</param>
/// <remarks>
/// 时间复杂度:O(n)(需要遍历找到尾节点)
/// 特殊情况处理:
/// 1. 空链表插入
/// 2. 单节点链表插入
/// 3. 多节点链表插入
/// </remarks>
public void AddFirst(T data)
{var newNode = new CircularNode<T>(data);if (head == null){// 空链表情况处理head = newNode;}else{// 查找当前尾节点(关键步骤)var tail = head;while (tail.Next != head){tail = tail.Next;}// 新节点指向原头节点newNode.Next = head;// 更新头指针head = newNode;// 更新尾节点指向新头(维持闭环)tail.Next = head;}count++;  // 更新计数器
}

(2)尾插法实现插入元素:

        思想:

①创建一个新节点

②如果当前链表为空,则将当前头结点设置为新节点,然后指向自己,闭环

③当前节点不为空,首先找到尾节点,接着将尾节点指向新节点,然后将新节点指向头结点,完毕!

④计数器++

实现代码:

/// <summary>
/// 在链表尾部插入新节点
/// </summary>
/// <param name="data">插入数据</param>
/// <remarks>
/// 时间复杂度:O(n)(需要遍历到尾部)
/// 优化思路:可以维护尾指针变量将时间复杂度降为O(1)
/// </remarks>
public void AddLast(T data)
{var newNode = new CircularNode<T>(data);if (head == null){// 空链表处理head = newNode;head.Next = head;  // 自环处理}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 新节点指向头节点newNode.Next = head;// 当前尾节点指向新节点tail.Next = newNode;}count++;
}

 (3)删除头结点:

①:判断列表是否有值,没有删除就是错误手段,应抛出错误

②:判断是否只有一个节点,是的话直接置空

③:多节点时,先找到尾节点,将头结点更新为头结点的下一个,将尾节点指向新的头结点

代码实现:

④:计数器--

/// <summary>
/// 删除链表头节点
/// </summary>
/// <exception cref="InvalidOperationException">空链表删除时抛出异常</exception>
/// <remarks>
/// 重点处理:
/// 1. 空链表异常
/// 2. 单节点链表删除
/// 3. 多节点链表删除
/// </remarks>
public void RemoveFirst()
{if (head == null)throw new InvalidOperationException("Cannot remove from empty list");if (head.Next == head) // 单节点判断条件{// 清除头节点引用head = null;}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 移动头指针到下一节点head = head.Next;// 更新尾节点指向新头tail.Next = head;}count--;  // 更新计数器
}

(4)删除第一个指定数据的节点

①:先判断是否为空链表,为空直接抛出错误

②:然后准备两个临时节点,一个是current,一个是previous。还有一个标志位:found

        current用来记录当前节点,在链表中一直跑跑跑,如果找到了目标值,就直接退出循环;还有就是当current的下一个节点是头结点时,说明此时已经到了尾节点,如果此刻的值还不等于,说明就是没找到。

        previous是为了判断当前节点是否为头结点,那怎么判断是不是头结点呢,我们首先会将previous置空,current每次往后移动一个节点,然后将previouscurrent覆盖,如果在经历了current遍历链表之后,previous还是空, 说明什么?说明目标值就是头结点,那么此刻就是删除头结点的操作;

        删除操作:1)如果删除的头结点,要进行判断是否是单节点,是的话直接置空,不是的话要找到尾节点,然后将重复上面的删除头结点操作

                         2)删除的不是头结点,就直接将previous的下一个指向current的下一个就好,因为你的previous是当前目标节点的前一个,你想删除当前节点,那么是不是就是将前一个节点的下一个指向当前目标节点的下一个,这样自己就被删除了。这里我们还要加一个特殊判断,就是如果当前节点是历史头结点,那么需要更新这个头结点

代码如下:

/// <summary>
/// 删除第一个匹配的节点
/// </summary>
/// <param name="data">要删除的数据</param>
/// <returns>是否成功删除</returns>
/// <remarks>
/// 关键点:
/// 1. 循环遍历时的终止条件
/// 2. 头节点删除的特殊处理
/// 3. 单节点链表的处理
/// </remarks>
public bool Remove(T data)
{if (head == null) return false;CircularNode<T> current = head;CircularNode<T>? previous = null;bool found = false;// 使用do-while确保至少执行一次循环do{if (EqualityComparer<T>.Default.Equals(current.Data, data)){found = true;break;}previous = current;current = current.Next;} while (current != head);if (!found) return false;// 删除节点逻辑if (previous == null) // 删除的是头节点{if (head.Next == head) // 唯一节点情况{head = null;}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 移动头指针head = head.Next;// 更新尾节点指向新头tail.Next = head;}}else // 删除中间或尾部节点{previous.Next = current.Next;// 如果删除的是原头节点(current == head)if (current == head) // 防御性检查{head = previous.Next; // 强制更新头指针}}count--;return true;
}

 (5)检查链表中是否存在指定数据
这个就简单了,直接遍历,找到了就返回true没找到就返回false

代码如下:

/// <summary>
/// 检查链表中是否存在指定数据
/// </summary>
/// <param name="data">查找目标数据</param>
/// <returns>存在返回true</returns>
/// <remarks>
/// 使用值相等比较(EqualityComparer.Default)
/// 注意:对于引用类型需要正确实现Equals方法
/// </remarks>
public bool Contains(T data)
{if (head == null) return false;var current = head;do{if (EqualityComparer<T>.Default.Equals(current.Data, data)){return true;}current = current.Next;} while (current != head);  // 完整遍历一圈return false;
}

(6) 更新节点值

这个在上面查找的基础上直接修改值就行了:

/// <summary>
/// 修改第一个匹配的节点值
/// </summary>
/// <param name="oldValue">旧值</param>
/// <param name="newValue">新值</param>
/// <returns>修改成功返回true</returns>
/// <remarks>
/// 注意:此方法直接修改节点数据引用
/// 如果节点存储的是引用类型,需要注意副作用
/// </remarks>
public bool Update(T oldValue, T newValue)
{if (head == null) return false;var current = head;do{if (EqualityComparer<T>.Default.Equals(current.Data, oldValue)){current.Data = newValue;  // 直接修改数据引用return true;}current = current.Next;} while (current != head);return false;
}

(7)实现一个迭代器,方便遍历链表元素

/// <summary>
/// 实现迭代器
/// </summary>
/// <returns></returns>
public IEnumerator<T> GetEnumerator()
{if (head == null) yield break;var current = head;do{yield return current.Data;current = current.Next;} while (current != head);
}

(8)在指定索引插入值 

①:先判断索引值是否合理,不合理直接抛出错误
②:判断是否在第一个位置插入,是的话,直接调用AddFirst();

③:判断是否在最后一个位置插入,是的话直接调用AddLast();

④:for循环,找到索引位置的前一个,将当前节点的下一个节点值存起来,然后指向新结点,最后将新节点指向下一个节点

代码如下:

⑤:计数器++

 /// <summary>/// 在指定索引位置插入节点/// </summary>/// <param name="index">插入位置(0-based)</param>/// <param name="data">插入数据</param>/// <exception cref="ArgumentOutOfRangeException">索引越界时抛出</exception>/// <remarks>/// 索引有效性检查:/// - index < 0 或 index > count 时抛出异常/// 当index=0时等价于AddFirst/// 当index=count时等价于AddLast/// </remarks>public void InsertAt(int index, T data){if (index < 0 || index > count)throw new ArgumentOutOfRangeException(nameof(index));if (index == 0){AddFirst(data);return;}if (index == count){AddLast(data);return;}var newNode = new CircularNode<T>(data);var current = head;// 移动到插入位置前驱节点for (int i = 0; i < index - 1; i++){current = current.Next;}// 插入新节点newNode.Next = current.Next;current.Next = newNode;count++;}

四、测试

    internal class Program{static void Main(string[] args){// 初始化循环链表var playlist = new CircularLinkedList<string>();Console.WriteLine($"新建播放列表,是否为空:{playlist.IsEmpty}");// 添加歌曲(混合使用头插和尾插)playlist.AddFirst("晴天 - 周杰伦");playlist.AddLast("七里香 - 周杰伦");playlist.AddFirst("夜曲 - 周杰伦");playlist.PrintAll();  // 输出:夜曲 -> 晴天 -> 七里香 -> [HEAD]// 插入操作playlist.InsertAt(1, "稻香 - 周杰伦");Console.WriteLine("\n插入新歌曲后:");playlist.PrintAll();  // 输出:夜曲 -> 稻香 -> 晴天 -> 七里香 -> [HEAD]// 删除操作playlist.RemoveFirst();Console.WriteLine("\n删除首曲后:");playlist.PrintAll();  // 输出:稻香 -> 晴天 -> 七里香 -> [HEAD]bool removed = playlist.Remove("晴天 - 周杰伦");Console.WriteLine($"\n删除晴天结果:{removed}");playlist.PrintAll();  // 输出:稻香 -> 七里香 -> [HEAD]// 查找测试bool exists = playlist.Contains("七里香 - 周杰伦");Console.WriteLine($"\n是否包含七里香:{exists}");  // 输出:True// 更新操作bool updated = playlist.Update("稻香 - 周杰伦", "稻香(Remix版) - 周杰伦");Console.WriteLine($"\n更新稻香结果:{updated}");playlist.PrintAll();  // 输出:稻香(Remix版) -> 七里香 -> [HEAD]// 边界测试:删除最后一个节点playlist.Remove("七里香 - 周杰伦");Console.WriteLine("\n删除七里香后:");playlist.PrintAll();  // 输出:稻香(Remix版) -> [HEAD]// 异常处理测试try{var emptyList = new CircularLinkedList<int>();emptyList.RemoveFirst();  // 触发异常}catch (InvalidOperationException ex){Console.WriteLine($"\n异常捕获:{ex.Message}");}// 使用迭代器遍历Console.WriteLine("\n当前播放列表循环播放:");foreach (var song in playlist){Console.WriteLine($"正在播放:{song}");}}}

测试结果:

 

五、总结

循环链表核心解析

1. 结构特性

循环链表是一种首尾相连的链式结构,其核心特征为:

  • 闭环设计​:尾节点的指针不再指向空值,而是指向头节点,形成环形链路。
  • 自洽节点​:每个新节点创建时默认指向自身,即使链表仅有一个节点也能维持闭合回路。
  • 遍历特性​:从任意节点出发均可遍历整个链表,没有传统链表的“终点”概念。
2. 操作逻辑与实现要点
节点插入
  • 头插法
    新节点成为链表的起点:

    1. 若链表为空,新节点自环即为头节点。
    2. 若链表非空,需先找到尾节点(遍历至 Next 指向头节点的节点)。
    3. 新节点的 Next 指向原头节点,尾节点的 Next 更新为新节点,头指针重置为新节点。
      ​耗时​:O(n)(查找尾节点),维护尾指针可优化至 O(1)。
  • 尾插法
    新节点成为链表的终点:

    1. 若链表为空,处理逻辑同头插法。
    2. 若链表非空,遍历找到尾节点后,使其 Next 指向新节点,新节点的 Next 指向头节点。
      ​耗时​:O(n),维护尾指针可优化。
节点删除
  • 删除头节点

    1. 单节点链表:直接置空头指针。
    2. 多节点链表:查找尾节点,将其 Next 指向原头节点的下一节点,更新头指针。
      ​关键​:确保尾节点与新头节点的连接,避免闭环断裂。
  • 删除指定节点

    1. 遍历链表匹配目标值,记录前驱节点。
    2. 若目标为头节点:按头节点删除逻辑处理。
    3. 若为中间节点:前驱节点的 Next 跳过目标节点,直接指向其后继节点。
      ​注意​:删除后需校验头指针是否失效,防止逻辑错误。
3. 复杂度与性能
  • 时间复杂度

    • 基础操作(头插、尾插、删除):默认 O(n),因需查找尾节点或遍历匹配。
    • 优化策略:维护尾指针变量,可将头尾操作降至 O(1)。
    • 查询与修改:O(n),需遍历至目标位置。
  • 空间复杂度
    与单向链表一致,每个节点仅需存储数据和单个指针,无额外内存负担。

4. 应用场景
  • 循环任务调度
    如操作系统的轮询机制,循环链表可自然支持任务队列的循环执行。

  • 多媒体播放控制
    音乐播放器的“循环播放”模式,通过链表闭环实现歌曲无缝衔接。

  • 游戏逻辑设计
    多玩家回合制游戏中,循环链表可管理玩家顺序,实现循环回合。

  • 资源池管理
    数据库连接池等场景,循环分配资源时可通过链表快速定位下一个可用资源。

5. 边界处理与易错点
  • 空链表操作
    插入首个节点时需维护自环,删除操作前必须检查链表是否为空,避免空指针异常。

  • 单节点维护
    删除仅有的节点后,需及时置空头指针,防止遗留无效引用。

  • 循环终止条件
    遍历时使用 do-while 结构,确保至少访问头节点一次,终止条件为回到起始点。

  • 闭环完整性
    任何操作后需验证尾节点的 Next 是否指向头节点,防止闭环断裂导致死循环。

6. 对比与选型
  • VS 单向链表

    • 优势:天然支持循环访问,尾节点操作更易优化。
    • 劣势:删除非头节点时仍需遍历,代码复杂度稍高。
  • VS 双向链表

    • 优势:内存占用更低,适合单向循环足够使用的场景。
    • 劣势:无法快速反向遍历,中间节点删除效率较低。
7. 设计启示
  • 扩展性考量
    可增加 Tail 指针变量,将尾节点访问从 O(n) 优化至 O(1),提升高频尾插场景性能。

  • 迭代器安全
    实现自定义迭代器时,需处理链表在遍历过程中被修改的情况,避免并发冲突。

  • 数据一致性
    节点删除后应及时更新计数器(count),确保 Count 属性准确反映实际长度。

        好的呀。我们终于结束了关于链表的知识了!继续前进!

附本文所有代码:

namespace 循环链表
{/// <summary>/// 循环链表节点类/// </summary>/// <typeparam name="T">节点数据类型</typeparam>public class CircularNode<T>{/// <summary>/// 节点存储的数据/// </summary>public T Data { get; set; }/// <summary>/// 指向下一个节点的指针/// </summary>public CircularNode<T> Next { get; set; }/// <summary>/// 节点构造函数/// </summary>/// <param name="data">节点初始数据</param>/// <remarks>/// 初始化时将Next指向自身,形成最小闭环/// 当链表只有一个节点时,构成自环结构/// </remarks>public CircularNode(T data){Data = data;Next = this; // 关键闭环操作}}/// <summary>/// 单向循环链表实现类/// </summary>/// <typeparam name="T">链表元素类型</typeparam>public class CircularLinkedList<T>{/// <summary>/// 链表头节点(关键指针)/// </summary>private CircularNode<T>? head;/// <summary>/// 节点计数器(优化统计效率)/// </summary>private int count;/// <summary>/// 链表元素数量(O(1)时间复杂度)/// </summary>public int Count => count;/// <summary>/// 判断链表是否为空/// </summary>public bool IsEmpty => head == null;/// <summary>/// 打印链表内容(调试用方法)/// </summary>public void PrintAll(){if (head == null){Console.WriteLine("[Empty List]");return;}var current = head;do{Console.Write($"{current.Data} -> ");current = current.Next;} while (current != head);  // 循环终止条件判断Console.WriteLine("[HEAD]");  // 闭环标记}/// <summary>/// 在链表头部插入新节点/// </summary>/// <param name="data">插入数据</param>/// <remarks>/// 时间复杂度:O(n)(需要遍历找到尾节点)/// 特殊情况处理:/// 1. 空链表插入/// 2. 单节点链表插入/// 3. 多节点链表插入/// </remarks>public void AddFirst(T data){var newNode = new CircularNode<T>(data);if (head == null){// 空链表情况处理head = newNode;}else{// 查找当前尾节点(关键步骤)var tail = head;while (tail.Next != head){tail = tail.Next;}// 新节点指向原头节点newNode.Next = head;// 更新头指针head = newNode;// 更新尾节点指向新头(维持闭环)tail.Next = head;}count++;  // 更新计数器}/// <summary>/// 在链表尾部插入新节点/// </summary>/// <param name="data">插入数据</param>/// <remarks>/// 时间复杂度:O(n)(需要遍历到尾部)/// 优化思路:可以维护尾指针变量将时间复杂度降为O(1)/// </remarks>public void AddLast(T data){var newNode = new CircularNode<T>(data);if (head == null){// 空链表处理head = newNode;head.Next = head;  // 自环处理}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 新节点指向头节点newNode.Next = head;// 当前尾节点指向新节点tail.Next = newNode;}count++;}/// <summary>/// 删除链表头节点/// </summary>/// <exception cref="InvalidOperationException">空链表删除时抛出异常</exception>/// <remarks>/// 重点处理:/// 1. 空链表异常/// 2. 单节点链表删除/// 3. 多节点链表删除/// </remarks>public void RemoveFirst(){if (head == null)throw new InvalidOperationException("Cannot remove from empty list");if (head.Next == head) // 单节点判断条件{// 清除头节点引用head = null;}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 移动头指针到下一节点head = head.Next;// 更新尾节点指向新头tail.Next = head;}count--;  // 更新计数器}/// <summary>/// 删除第一个匹配的节点/// </summary>/// <param name="data">要删除的数据</param>/// <returns>是否成功删除</returns>/// <remarks>/// 关键点:/// 1. 循环遍历时的终止条件/// 2. 头节点删除的特殊处理/// 3. 单节点链表的处理/// </remarks>public bool Remove(T data){if (head == null) return false;CircularNode<T> current = head;CircularNode<T>? previous = null;bool found = false;// 使用do-while确保至少执行一次循环do{if (EqualityComparer<T>.Default.Equals(current.Data, data)){found = true;break;}previous = current;current = current.Next;} while (current != head);if (!found) return false;// 删除节点逻辑if (previous == null) // 删除的是头节点{if (head.Next == head) // 唯一节点情况{head = null;}else{// 查找当前尾节点var tail = head;while (tail.Next != head){tail = tail.Next;}// 移动头指针head = head.Next;// 更新尾节点指向新头tail.Next = head;}}else // 删除中间或尾部节点{previous.Next = current.Next;// 如果删除的是原头节点(current == head)if (current == head){head = previous.Next;  // 更新头指针}}count--;return true;}/// <summary>/// 检查链表中是否存在指定数据/// </summary>/// <param name="data">查找目标数据</param>/// <returns>存在返回true</returns>/// <remarks>/// 使用值相等比较(EqualityComparer.Default)/// 注意:对于引用类型需要正确实现Equals方法/// </remarks>public bool Contains(T data){if (head == null) return false;var current = head;do{if (EqualityComparer<T>.Default.Equals(current.Data, data)){return true;}current = current.Next;} while (current != head);  // 完整遍历一圈return false;}/// <summary>/// 修改第一个匹配的节点值/// </summary>/// <param name="oldValue">旧值</param>/// <param name="newValue">新值</param>/// <returns>修改成功返回true</returns>/// <remarks>/// 注意:此方法直接修改节点数据引用/// 如果节点存储的是引用类型,需要注意副作用/// </remarks>public bool Update(T oldValue, T newValue){if (head == null) return false;var current = head;do{if (EqualityComparer<T>.Default.Equals(current.Data, oldValue)){current.Data = newValue;  // 直接修改数据引用return true;}current = current.Next;} while (current != head);return false;}/// <summary>/// 在指定索引位置插入节点/// </summary>/// <param name="index">插入位置(0-based)</param>/// <param name="data">插入数据</param>/// <exception cref="ArgumentOutOfRangeException">索引越界时抛出</exception>/// <remarks>/// 索引有效性检查:/// - index < 0 或 index > count 时抛出异常/// 当index=0时等价于AddFirst/// 当index=count时等价于AddLast/// </remarks>public void InsertAt(int index, T data){if (index < 0 || index > count)throw new ArgumentOutOfRangeException(nameof(index));if (index == 0){AddFirst(data);return;}if (index == count){AddLast(data);return;}var newNode = new CircularNode<T>(data);var current = head;// 移动到插入位置前驱节点for (int i = 0; i < index - 1; i++){current = current.Next;}// 插入新节点newNode.Next = current.Next;current.Next = newNode;count++;}/// <summary>/// 实现迭代器/// </summary>/// <returns></returns>public IEnumerator<T> GetEnumerator(){if (head == null) yield break;var current = head;do{yield return current.Data;current = current.Next;} while (current != head);}}internal class Program{static void Main(string[] args){// 初始化循环链表var playlist = new CircularLinkedList<string>();Console.WriteLine($"新建播放列表,是否为空:{playlist.IsEmpty}");// 添加歌曲(混合使用头插和尾插)playlist.AddFirst("晴天 - 周杰伦");playlist.AddLast("七里香 - 周杰伦");playlist.AddFirst("夜曲 - 周杰伦");playlist.PrintAll();  // 输出:夜曲 -> 晴天 -> 七里香 -> [HEAD]// 插入操作playlist.InsertAt(1, "稻香 - 周杰伦");Console.WriteLine("\n插入新歌曲后:");playlist.PrintAll();  // 输出:夜曲 -> 稻香 -> 晴天 -> 七里香 -> [HEAD]// 删除操作playlist.RemoveFirst();Console.WriteLine("\n删除首曲后:");playlist.PrintAll();  // 输出:稻香 -> 晴天 -> 七里香 -> [HEAD]bool removed = playlist.Remove("晴天 - 周杰伦");Console.WriteLine($"\n删除晴天结果:{removed}");playlist.PrintAll();  // 输出:稻香 -> 七里香 -> [HEAD]// 查找测试bool exists = playlist.Contains("七里香 - 周杰伦");Console.WriteLine($"\n是否包含七里香:{exists}");  // 输出:True// 更新操作bool updated = playlist.Update("稻香 - 周杰伦", "稻香(Remix版) - 周杰伦");Console.WriteLine($"\n更新稻香结果:{updated}");playlist.PrintAll();  // 输出:稻香(Remix版) -> 七里香 -> [HEAD]// 边界测试:删除最后一个节点playlist.Remove("七里香 - 周杰伦");Console.WriteLine("\n删除七里香后:");playlist.PrintAll();  // 输出:稻香(Remix版) -> [HEAD]// 异常处理测试try{var emptyList = new CircularLinkedList<int>();emptyList.RemoveFirst();  // 触发异常}catch (InvalidOperationException ex){Console.WriteLine($"\n异常捕获:{ex.Message}");}// 使用迭代器遍历Console.WriteLine("\n当前播放列表循环播放:");foreach (var song in playlist){Console.WriteLine($"正在播放:{song}");}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/76484.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

交换网络基础

学习目标 掌握交换机的基本工作原理 掌握交换机的基本配置 交换机的基本工作原理 交换机是局域网&#xff08;LAN&#xff09;中实现数据高效转发的核心设备&#xff0c;工作在 数据链路层&#xff08;OSI 模型第二层&#xff09;&#xff0c;其基本工作原理可概括为 “学习…

科学研究:怎么做

科研&#xff08;科学研究&#xff09;​​ 是指通过系统化的方法&#xff0c;探索自然、社会或人文领域的未知问题&#xff0c;以发现新知识、验证理论或解决实际问题的活动。它的核心是​​基于证据的探索与创新​​&#xff0c;旨在推动人类认知和技术的进步。 科研的核心要…

算法题(128):费解的开关

审题&#xff1a; 本题需要我们将多组测试用例中拉灯数小于等于6的最小拉灯数输出&#xff0c;若拉灯数最小值仍大于6&#xff0c;则输出-1 思路&#xff1a; 方法一&#xff1a;二进制枚举 首先我们先分析一下基本特性&#xff1a; 1.所有的灯不可能重复拉&#xff1a;若拉的数…

MFC文件-屏幕录像

下载本文件 本文件将获取屏幕图像数据的所有代码整合到两个文件中&#xff08;ScreenRecorder.h和ScreenRecorder.cpp&#xff09;&#xff0c;使获取屏幕图像数据变得简单。输出IYUV视频流。还可以获取系统播放的声音&#xff0c;输出PCM音频流。由于使用了MFC类&#xff0c;本…

0801ajax_mock-网络ajax请求1-react-仿低代码平台项目

0 vite配置proxy代理 vite.config.ts代码如下图所示&#xff1a; import { defineConfig } from "vite"; import react from "vitejs/plugin-react";// https://vite.dev/config/ export default defineConfig({plugins: [react()],server: {proxy: {&qu…

JVM笔记【一】java和Tomcat类加载机制

JVM笔记一java和Tomcat类加载机制 java和Tomcat类加载机制 Java类加载 * loadClass加载步骤类加载机制类加载器初始化过程双亲委派机制全盘负责委托机制类关系图自定义类加载器打破双亲委派机制 Tomcat类加载器 * 为了解决以上问题&#xff0c;tomcat是如何实现类加载机制的…

IP编址(来自YESLAB新网工的笔记)

上层协议类型 概念&#xff1a;通常指的是位于网络层&#xff08;如 IP 层&#xff09;以上的协议类型&#xff0c;这些协议在数据传输时需要由网络层&#xff08;或更低层&#xff09;协议承载。以 IP 协议为例&#xff0c;IP 报文头部中的 协议字段&#xff08;Protocol Fie…

SpringBoot学习(过滤器Filter。拦截器Interceptor。全局异常捕获处理器GlobalExceptionHandler)(详细使用教程)

目录 一、过滤器Filter。 1.1定义与规范。 1.2工作原理与范围。 1.3使用场景。 1.4 SpringBoot实现过滤器。&#xff08;Filter配置2种方式&#xff09; <1>注解配置(WebFilter、Order、ServletComponentScan)。 创建过滤器类。 启用 Servlet 组件扫描。 <2>配置类…

c++题目_P1443 马的遍历

P1443 马的遍历 # P1443 马的遍历 ## 题目描述 有一个 $n \times m$ 的棋盘&#xff0c;在某个点 $(x, y)$ 上有一个马&#xff0c;要求你计算出马到达棋盘上任意一个点最少要走几步。 ## 输入格式 输入只有一行四个整数&#xff0c;分别为 $n, m, x, y$。 ## 输出格式 …

清华《数据挖掘算法与应用》K-means聚类算法

使用k均值聚类算法对表4.1中的数据进行聚类。代码参考P281。 创建一个名为 testSet.txt 的文本文件&#xff0c;将以下内容复制粘贴进去保存即可&#xff1a; 0 0 1 2 3 1 8 8 9 10 10 7 表4.1 # -*- coding: utf-8 -*- """ Created on Thu Apr 17 16:59:58 …

HarmonyOS-ArkUI V2工具类:AppStorageV2:应用全局UI状态存储

AppStorageV2是一个能够跨界面存储数据,管理数据的类。开发者可以使用AppStorageV2来存储全局UI状态变量数据。它提供的是应用级的全局共享能力,开发者可以通过connect绑定同一个key,进行跨ability数据共享。 概述 AppStorageV2是一个单例,创建时间是应用UI启动时。其目的…

打靶日记 zico2: 1

一、探测靶机IP&#xff08;进行信息收集&#xff09; 主机发现 arp-scan -lnmap -sS -sV -T5 -p- 192.168.10.20 -A二、进行目录枚举 发现dbadmin目录下有个test_db.php 进入后发现是一个登录界面&#xff0c;尝试弱口令&#xff0c;结果是admin&#xff0c;一试就出 得到加…

使用Java基于Geotools的SLD文件编程式创建与磁盘生成实战

前言 在地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;地图的可视化呈现至关重要&#xff0c;而样式定义语言&#xff08;SLD&#xff09;文件为地图元素的样式配置提供了强大的支持。SLD 能够精确地定义地图图层中各类要素&#xff08;如点、线、面、文本等&#x…

kubernetes》》k8s》》Service

Kubernetes 中的 Service 是用于暴露应用服务的核心抽象&#xff0c;为 Pod 提供稳定的访问入口、负载均衡和服务发现机制。Service在Kubernetes中代表了一组Pod的逻辑集合&#xff0c;通过创建一个Service&#xff0c;可以为一组具有相同功能的容器应用提供一个统一的入口地址…

【HDFS】EC重构过程中的校验功能:DecodingValidator

一、动机 DecodingValidator是在HDFS-15759中引入的一个用于校验EC数据重构正确性的组件。 先说下引入DecodingValidator的动机,据很多已知的ISSUE(如HDFS-14768, HDFS-15186, HDFS-15240,这些目前都已经fix了)反馈, EC在重构的时候可能会有各种各样的问题,导致数据错误…

现代c++获取linux系统架构

现代c获取linux系统架构 前言一、使用命令获取系统架构二、使用c代码获取系统架构三、验证四、总结 前言 本文介绍一种使用c获取linux系统架构的方法。 一、使用命令获取系统架构 linux系统中可以使用arch或者uname -m命令来获取当前系统架构&#xff0c;如下图所示 archuna…

didFinishLaunching 与「主线程首次 idle」, 哪个是更优的启动结束时间点 ?

结论先行 在这两个候选时间点里—— application:didFinishLaunchingWithOptions: 执行结束主线程第一次进入 idle&#xff08;RunLoop kCFRunLoopBeforeWaiting&#xff09; 若你只能二选一&#xff0c;以「主线程首次 idle」作为 启动结束 更合理。它比 didFinishLaunchin…

Vue3 + TypeScript中defineEmits 类型定义解析

TypeScript 中 Vue 3 的 defineEmits 函数的类型定义&#xff0c;用于声明组件可以触发的事件。以下是分步解释&#xff1a; 1. 泛型定义 ts <"closeDialog" | "getApplySampleAndItemX"> 作用&#xff1a;定义允许的事件名称集合&#xff0c;即组…

树莓派超全系列教程文档--(34)树莓派配置GPIO

配置GPIO GPIO控制gpio 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 GPIO控制 gpio 通过 gpio 指令&#xff0c;可以在启动时将 GPIO 引脚设置为特定模式和值&#xff0c;而以前需要自定义 dt-blob.bin 文件。每一行都对一组引脚应用相同的设…

AladdinEdu(H卡GPU算力平台)使用教程: 1)注册与开通流程 2)插件使用流程

一、注册与开通流程 首先进入AladdinEdu官网&#xff1a;AladdinEdu-同学们用得起的H卡算力平台-高效做AI就上Aladdin 完成注册&#xff0c;并进行学生认证&#xff1a;学生认证账户&#xff0c;认证期间享受教育优惠价。 登录官网进入控制台 二、插件使用流程 VScode中…