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.Next | do {...} 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每次往后移动一个节点,然后将previous用current覆盖,如果在经历了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. 操作逻辑与实现要点
节点插入
-
头插法
新节点成为链表的起点:- 若链表为空,新节点自环即为头节点。
- 若链表非空,需先找到尾节点(遍历至
Next
指向头节点的节点)。 - 新节点的
Next
指向原头节点,尾节点的Next
更新为新节点,头指针重置为新节点。
耗时:O(n)(查找尾节点),维护尾指针可优化至 O(1)。
-
尾插法
新节点成为链表的终点:- 若链表为空,处理逻辑同头插法。
- 若链表非空,遍历找到尾节点后,使其
Next
指向新节点,新节点的Next
指向头节点。
耗时:O(n),维护尾指针可优化。
节点删除
-
删除头节点
- 单节点链表:直接置空头指针。
- 多节点链表:查找尾节点,将其
Next
指向原头节点的下一节点,更新头指针。
关键:确保尾节点与新头节点的连接,避免闭环断裂。
-
删除指定节点
- 遍历链表匹配目标值,记录前驱节点。
- 若目标为头节点:按头节点删除逻辑处理。
- 若为中间节点:前驱节点的
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}");}}}
}
相关文章:
C#进阶学习(六)单向链表和双向链表,循环链表(下)循环链表
目录 📊 链表三剑客:特性全景对比表 一、循环链表节点类 二、循环链表的整体设计框架 三、循环列表中的重要方法: (1)头插法,在头结点前面插入新的节点 (2)尾插法实现插入元素…...
Spring Boot配置文件优先级全解析:如何优雅覆盖默认配置?
📚 一、为什么需要了解配置文件优先级? 想象一下,你正在玩一个游戏🎮,游戏里有默认设置,但你可以通过不同的方式修改这些设置: 游戏内置的默认设置(就像Spring Boot的默认配置&…...
【多目标进化算法】NSGA-II 算法(结合例子)
目录 一、NSGA-II 是干什么的? 二、通过一个简单例子来解释 例子:挑选手机 三、NSGA-II 解决步骤 1. 初始化种群 2. 非支配排序(Fast Non-dominated Sorting) 3. 拥挤度距离(Crowding Distance) 4. 选择 + 交叉 + 变异 5. 合并种群、排序、更新 四、最后结果(…...
【Spring Boot】把jar包导入本地系统
【Java】把jar包导入本地maven仓库 一、方法一:将 JAR 添加到项目本地的 libs/ 目录二、方法二:把 JAR 安装到本地 Maven 仓库(推荐)三、查看是否安装成功(1)直接用文件管理器/终端查看(2&#…...
钧瓷收藏防坑指南:如何科学评估与理性收藏
关注大禹智库及时接收干货报告和视频 大禹智库 第 8期〔总第462期〕2025-4-17 一、价格敏感背后的收藏心理 每次钧瓷估价速算表的更新都会引发收藏圈的热议,这反映出藏家最核心的关切:“买得值不值?” 即便对部分藏家而言价格并非首要因素…...
CrewAI Community Version(一)——初步了解以及QuickStart样例
目录 1. CrewAI简介1.1 CrewAI Crews1.2 CrewAI Flows1.3 Crews和Flows的使用情景 2. CrewAI安装2.1 安装uv2.2 安装CrewAI CLI 3. 官网QuickStart样例3.1 创建CrewAI Crews项目3.2 项目结构3.3 .env3.4 智能体角色及其任务3.4.1 agents.yaml3.4.2 tasks.yaml 3.5 crew.py3.6 m…...
Vue 3.0 Composition API 与 Vue 2.x Options API 的区别
引言 Vue 作为一款流行的 JavaScript 框架,经历了多个版本的迭代。Vue 2.x 时期,Options API 是主要的开发方式;而到了 Vue 3.0,引入了 Composition API。这两种 API 风格各有特点,理解它们的区别对于开发者来说至关重…...
江苏广电HC2910-创维代工-Hi3798cv200-2+8G-海美迪安卓7.0-强刷包
江苏广电HC2910-创维代工-Hi3798cv200-28G-海美迪安卓7.0-强刷包 说明 1、由于原机的融合网关路由不能设置,原网口无法使用,需要用usb2.0的RJ45usb网卡接入。 通过usb接口网卡联网可以实现百兆网口连接。原机usb3.0的接口可以以接入硬盘,播放…...
clickhouse数据导出导入
clickhouse数据导出导入 CSV格式导出为csv格式导入为csv格式 JSON格式导出为json格式导入为json格式 SQL格式导出为SQL CSV格式 导出为csv格式 # 不带表头 clickhouse-client -h 127.0.0.1 --database"db" --query"select * from db.test_table FORMAT CSV&qu…...
GPU 在机器学习中的应用优势:从技术特性到云端赋能
一、引言:当机器学习遇见算力革命 在人工智能浪潮席卷全球的今天,机器学习已从实验室走向商业落地的核心战场。随着深度神经网络模型复杂度呈指数级增长(如 GPT-4 参数量突破万亿级),以及数据规模迈向 ZB 级别&…...
C++: 类和对象(中)
📔个人主页📚:秋邱-CSDN博客 ☀️专属专栏✨:C 🏅往期回顾🏆:C: 类和对象(上) 🌟其他专栏🌟:C语言_秋邱 类的默认成员函数 构造…...
基于slimBOXtv 9.16 V2-晶晨S905L3A/ S905L3AB-Mod ATV-Android9.0-线刷通刷固件包
基于slimBOXtv 9.16 V2-晶晨S905L3A/ S905L3AB-Mod ATV-Android9.0-线刷通刷固件包,基于SlimBOXtv 9 修改而来,贴近于原生ATV,仅支持晶晨S905L3A/ S905L3AB芯片刷机。 适用型号:M401A、CM311-1a、CM311-1s…...
Rocky8 升级 Python 3.9.20 并部署 Airflow 2.10.5
Rocky8 升级 Python 3.9.20 并部署 Airflow 2.10.5 1.系统环境配置1.1Python安装1.2 Airflow 配置1.2.1 基础配置1.2.2 新建数据库1.2.3 配置文件 airflow.cfg 1.3 安装 Airflow 2.Airflow 测试1.启动定时器2.登录系统3.自定义流3.1测试流 1.系统环境配置 # 系统版本查看 cat …...
基础智能体的进展与挑战第 3 章【记忆】
目录 第三章记忆3.1 人类记忆概述3.1.1 人类记忆的类型3.1.2 人类记忆模型 3.2 从人类记忆到智能体记忆3.3 智能体记忆的表示3.3.1 感知记忆3.3.2 短期记忆3.3.3 长期记忆 3.4 记忆生命周期3.4.1 记忆获取3.4.2 记忆编码3.4.3 记忆衍生3.4.4 记忆检索与匹配3.4.5 神经记忆网络3…...
Docker 容器与镜像核心操作命令大全(实战指南)
Docker 容器与镜像核心操作命令大全(实战指南) 摘要:本文全面整理 Docker 容器与镜像管理的高频操作命令,涵盖容器生命周期管理、镜像构建技巧、网络配置、文件挂载等场景,并附赠企业级高级用法。适用于开发、测试及生…...
见多识广3:帕累托最优解与帕累托前沿
目录 前言定义特点应用场景求解算法总结 前言 这里的知识都是kimi告诉我的,我主要记录一下。 定义 帕累托最优解:在多目标优化问题中,如果一个解在某个目标上优于另一个解,而在其他目标上至少不比另一个解差,那么这…...
遥感技术赋能电力设施监控:应用案例篇
目前主流的电力巡检手段利用无人机能够通过设定灵活航线进行低空飞行、搭载不同的采集设备,能够从不同角度对输电线进行贴近拍摄,但缺陷是偏远山区无人机飞行技术要求高,成本高,且飞行的无人机也可能会对输电线产生破坏。 星图云开…...
Docker容器虚拟化存储架构
本文主要描述Docker容器引擎中运行的应用如何持久化地存储数据。 如上所示,Docker容器引擎的总体应用架构图,包括Docker客户端应用、Docker Host服务端应用以及Docker Registry镜像仓库端应用。其中,Docker Host服务端应用包括Docker daemon容…...
Silverlight发展历程(微软2021年已经停止支持Silverlight 5)
Microsoft Silverlight 发展历程 引言 Microsoft Silverlight 是微软在 Web 多媒体和富互联网应用 (RIA) 领域的一次重要尝试,它从诞生到消亡的过程折射出了 Web 技术发展的变迁和行业格局的演变。本文将详细回顾 Silverlight 的完整发展历程,探讨其技…...
“星睿O6” AI PC开发套件评测 - 部署PVE搭建All in One NAS服务器
Radxa O6平台上部署PVE搭建All in One NAS服务器 Radxa O6是一款性能卓越的单板计算机,其强劲的硬件配置和多样化的接口设计,使其成为家庭和小型企业理想的All in One服务器解决方案。值得一提的是,O6原生配备了两个5G网口,便于直…...
【路由交换方向IE认证】BGP选路原则之AS-Path属性
文章目录 一、路由器BGP路由的处理过程控制平面和转发平面选路工具 二、BGP的选路顺序选路的前提选路顺序 三、AS-Path属性选路原则AS-Path属性特性AS-Path管进还是管出呢?使用AS-Path对进本AS的路由进行选路验证AS-Path不接收带本AS号的路由 四、BGP邻居建立配置 一…...
《软件设计师》复习笔记(14.3)——设计模式
目录 一、设计模式分类 1. 创建型模式(Creational Patterns) 2. 结构型模式(Structural Patterns) 3. 行为型模式(Behavioral Patterns) 真题示例: 一、设计模式分类 架构模式 高层设计决…...
Windows10,11账户管理,修改密码,创建帐户...
在这里,我们使用微软操作系统的一款工具:netplwiz 它可以非常便捷的管理用户账户. 一:修改密码(无需现在密码) 01修改注册表 运行命令:regedit 在地址栏输入: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Passwor…...
数据类型相关问题导致的索引失效 | OceanBase SQL 优化实践
背景 针对在OceanBase 论坛中遇到的一些典型SQL调优问题,进行记录与总结,分享给大家。本文介绍的事3个场景:数据类型不匹配、字符集相关属性不匹配,和过滤/联接条件上包含系统函数。 场景一:数据类型不匹配 类型不匹…...
银行卡风险画像在社交行业网络安全的应用
据中国支付清算协会统计,2023年银行卡欺诈案件造成的经济损失同比增长21%,而社交平台中超过35%的诈骗行为涉及金融账户盗用。本文将讲述如何使用风险画像技术助力社交网络安全。 银行卡风险画像的核心逻辑 银行卡风险画像是通过多维度数据分析构建的用…...
C++程序设计基础实验:C++对C的扩展特性与应用
C程序设计基础实验:C对C的扩展特性与应用 🔥 本文详细讲解C基础实验,包含C对C语言的扩充与增强特性,从零开始掌握函数重载、引用、指针等核心概念,附详细代码分析与运行结果。适合C初学者和有C语言基础想学习C的同学&a…...
极狐GitLab 外部授权控制机制是怎样的?
极狐GitLab 是 GitLab 在中国的发行版,关于中文参考文档和资料有: 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 外部授权控制 (BASIC SELF) 在高度控制的环境中,访问策略可能需要由外部服务控制,该服务允许基于项目…...
告别Feign:基于Spring 6.1 RestClient构建高可用声明式HTTP客户端
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
极狐GitLab 项目和群组的导入导出速率限制如何设置?
极狐GitLab 是 GitLab 在中国的发行版,关于中文参考文档和资料有: 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 项目和群组的导入导出速率限制 (BASIC SELF) 您可以为项目和群组的导入和导出配置速率限制: 更改速率限制&#…...
中华传承-医山命相卜-铁板神数
铁板神数 子平法 子平法 徐子平 倪海夏 一月(公历2025年1月29日-2025年2月27日) 运势:事业开局不利,难以快速适应工作节奏,可能面临上级的质疑或竞争压力。财富方面容易财来财去,需留意理财陷阱。 原因&…...
C++学习:六个月从基础到就业——面向对象编程:接口设计
C学习:六个月从基础到就业——面向对象编程:接口设计 本文是我C学习之旅系列的第十五篇技术文章,重点讨论在C中进行接口设计的原则、技术和最佳实践。查看完整系列目录了解更多内容。 引言 在面向对象的软件开发中,良好的接口设计…...
工作总结(十二)——迁移svn单项目到gitlab上,保留历史提交记录
文章目录 前言一、目的二、操作步骤1.创建项目库2.复制历史提交者账号3.复制待迁移项目以及历史记录4.push到gitlab远程仓库 总结 前言 本系列文章主要记录工作中一些需要记录的内容 一、目的 因为一些原因,我需要将svn库上的某个项目迁移到公司的gitlab库管理平台…...
PS中制作一张扣洞贴图
要在PS制作如下一张贴图,如下图所示 步骤: 1.首先复制一张图层 2.将最底层图层的透明度调整为0 3.选择画笔的模式为清除 4.设置画笔大小 5.选中需要清除的图层,然后就可以将图层的像素点清除了 6.导出成PNG文件即可 注࿱…...
STM32 HAL库 Freertos创建多任务
1. 引言 STM32F407 是 ST 公司推出的一款高性能微控制器,具有丰富的外设资源和强大的处理能力。HAL(Hardware Abstraction Layer)库是 ST 为其微控制器提供的硬件抽象层,它简化了硬件操作,提高了开发效率。FreeRTOS 是…...
android测试硬件工具 安卓硬件测试命令
Android开发常用ADB命令大全 在Android开发过程中,ADB(Android Debug Bridge)是一个非常重要的调试工具。掌握这些命令可以大大提高开发效率。如果你正在使用克魔开发助手(Keymob)这样的开发工具,你会发现它已经集成了很多ADB功能,让调试变得…...
第12篇:Linux程序访问控制FPGA端Switch<一>
Q:如何写.c代码访问读取FPGA端的滑动开关SW的值? A:DE1-SoC开发板上有10个滑动开关连接到DE1_SoC_Computer系统的并行输入端口,该端口只有一个10位只读Data寄存器映射到地址0xFF200040,对Data寄存器进行读操作并将读出…...
硬盘变废为宝!西部数据携微软等启动稀土回收 效率可达90%
快科技4月18日消息,西部数据(Western Digital)宣布,与微软、Critical Materials Recycling及PedalPoint Recycling携手,在美国启动一项跨产业前导计划-稀土回收。 目前,西部数据已经成功从报废硬盘&#x…...
元宇宙概念兴起,B 端数字孪生迎来哪些新机遇?
在科技飞速发展的当下,元宇宙概念如同一颗璀璨新星,迅速吸引了全球的目光。随着元宇宙的兴起,与之紧密相关的 B 端数字孪生技术也迎来了前所未有的发展机遇。元宇宙与 B 端数字孪生的融合,正悄然改变着多个行业的运作模式…...
用 NLP + Streamlit,把问卷变成能说话的反馈
网罗开发 (小红书、快手、视频号同名) 大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等…...
stl 容器 – map
stl 容器 – map 1. map 和 multimap的使用文档 参考文档 参考文档点这里哟 🌈 😘 2. map 类的介绍 map的声明如下 template < class Key, // map::key_type class T, // map::mapped_type class Compare less<Key>, // map::key_…...
20250417-vue-动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名: <base-layout><template v-slot:[dynamicSlotName]>...</template><!-- 缩写为 --><template #[dynamicSlotName]>...</template> </base…...
010301-cdn_waf-web扩展1-基础入门-网络安全
文章目录 1 WAF1.1WAF 的核心功能1.2 WAF 的部署类型1.3 WAF 的应用场景1.4 主流 WAF 产品1.5 如何选择 WAF?1.6 注意事项1.7 waf总结和演示 2 CDN2.1 核心原理2.2 关键功能2.3 典型应用场景2.4 优势2.5 主流CDN服务商2.6 技术实现2.7 注意事项2.8cdn安全测试和演示…...
CentOS7执行yum命令报错 Could not retrieve mirrorlist http://mirrorlist.centos.org
CentOS7执行yum命令报错 引更新yum源备份原有源创建新的源文件清理并重建缓存 引 CentOS 7 系统无法连接到 CentOS 的官方镜像站点。这通常是由于网络问题或 CentOS 7 已停止维护导致的(2024年6月30日后 CentOS 7 已进入 EOL) 报错明细: 已…...
在阿里云虚拟主机上启用WordPress伪静态
在阿里云虚拟主机上启用WordPress伪静态,需要根据虚拟主机的Web服务器类型(Nginx或Apache)进行相应的设置。以下是具体步骤: 1. 确认虚拟主机的Web服务器类型 登录阿里云虚拟主机管理控制台。 查看主机的配置信息,确认是使用Nginx还是Apac…...
【java 13天进阶Day06】Map集合,HashMapTreeMap,斗地主、图书管理系统,排序算法
Map集合 Collection是单值集合体系。 Map集合是另一个集合体系,是一种双列集合,每个元素包含两个值。 Map集合的每个元素的格式:keyvalue(键值对元素)。 Map集合也被称为“键值对集合”。 Map集合的完整格式:{key1value1 , ke…...
从代码学习深度学习 - 小批量随机梯度下降 PyTorch 版
文章目录 前言一、数据准备与处理1.1 数据集简介1.2 数据加载与预处理二、训练工具与辅助类三、可视化工具四、模型训练五、执行训练总结前言 深度学习是人工智能领域的核心技术之一,而小批量随机梯度下降(Mini-Batch Stochastic Gradient Descent, SGD)是训练神经网络的基…...
03、GPIO外设(三):标准库代码示例
标准库代码示例 1、点亮LED2、LED闪烁3、LED流水灯4、按键控制LED5、蜂鸣器 本章源代码链接: 链接: link 1、点亮LED 实验要求:点亮LED ①LED.c文件的代码如下: #include "LED.h"/*** LED引脚初始化*//* 定义数组,想要添加引脚…...
PyTorch 深度学习实战(37):分布式训练(DP/DDP/Deepspeed)实战
在上一篇文章中,我们探讨了混合精度训练与梯度缩放技术。本文将深入介绍分布式训练的三种主流方法:Data Parallel (DP)、Distributed Data Parallel (DDP) 和 DeepSpeed,帮助您掌握大规模模型训练的关键技术。我们将使用PyTorch在CIFAR-10分类…...
MCP系列之架构篇:深入理解MCP的设计架构
前言 在上一篇《MCP系列之基础篇》中,我们初步了解了MCP(模型上下文协议)的基本概念和价值。本篇文章将深入探讨MCP的技术架构,帮助开发者和技术爱好者更全面地理解这一协议的内部工作机制。我们将剖析MCP的核心组件、通信模型和工作流程,解析Host、Client和Server三者之…...
RT-Thread RTThread studio 初使用
RT-Thread Studio 下载 https://www.rt-thread.org/studio.html 安装使用 https://bbs.elecfans.com/jishu_2425653_1_1.html 4 编译问题解决 问题一:error: unknown type name clock_t 具体的类型值是在sys/_types.h中定义的,需要包含sys/_types.h 这个…...