本人能力有限,本文仅作学习交流与参考,如有不足还请斧正
目录
0.单链表好处
0.5.单链表分类
1.无虚拟头节点情况
图示:
代码:
头插/尾插
删除
搜索
遍历全部
测试代码:
全部代码
2.有尾指针情况
尾插
全部代码
3.有虚拟头节点情况
全部代码
4.循环单链表
几个特别说明的点
增加时 更新环结构
删除时 删的头节点
非删头节点 注意遍历终止条件
全部代码
0.单链表好处
优点 | 说明 / 场景 |
---|---|
动态内存分配 | 节点按需创建,无需预先指定固定大小,避免数组的空间浪费或溢出(如数据量不确定时) |
高效增删操作 | 插入 / 删除只需修改指针(平均 O(1) 时间复杂度),尤其适合频繁增删场景(如栈、队列) |
内存分散存储 | 节点可存储在非连续内存中,适应碎片化内存,利用零散空间(对比数组需连续内存) |
灵活动态长度 | 长度可随时增减,无需扩容 / 缩容(如动态缓冲区、链表队列) |
实现简单轻量 | 节点结构简单(数据 + 指针),代码易理解,适合入门学习及快速实现基础数据结构 |
适合链式场景 | 支持链表特有操作(反转、合并、判环等),常用于哈希表拉链法、操作系统进程调度等 |
0.5.单链表分类
分类维度 | 类型 | 核心特点 | 典型场景 / 优势 |
---|---|---|---|
头节点 | 无头节点 | 头指针直接指向第一个数据节点,需处理头节点边界条件 | 简单场景,代码稍繁琐 |
有头节点 | 头节点为虚拟节点,简化插入 / 删除操作 | 通用场景,代码更简洁 | |
循环 | 非循环 | 尾节点 next 为 null ,遍历有终点 | 大多数基础场景 |
循环 | 尾节点 next 指向头节点,形成环 | 循环遍历、约瑟夫环等问题 | |
辅助指针 | 无尾指针 | 尾插需遍历链表(\(O(n)\)) | 尾插操作较少的场景 |
带尾指针 | 尾插直接通过尾指针操作(\(O(1)\)) | 频繁尾插的场景 |
下无特殊说明 皆为非循环单链表
1.无虚拟头节点情况
请注意 无头节点的意思是没有虚拟头节点
而下所说的headNdoe代表的是实际数据第一个节点
单链表 = 实际数据头节点 + (节点 1 + 节点 2 + … + 节点 n) 其中,每个节点的定义为: 节点 = 数据 + 指向下一个节点的指针
图示:
注意 因为写c#的时候使用指针需要注意下列问题:
所以指向下一个节点的指针 定义为Node类 也就是Node本身
代码:
class Node {
public int value;
public Node nextNode;
public Node(int value, Node nextNode) {
this.value = value;
this.nextNode = nextNode;
}
}
头插/尾插
头插的精髓:每一次插入新node的时候 就把旧headNode作为nextNode,然后改变head的指向即可
class LinkeList {
public Node headNode;
public LinkeList() {
headNode = null;
}
//头插: 新节点的next指向头节点,然后将头节点指向新节点
public void AddToHead(int value) {
//创建一个新节点 并把原来的头节点放到后面去 这就是头插法的精髓
Node newNode = new Node(value, headNode);
//将头节点指向新节点
headNode = newNode;
}
}
尾插精髓:遍历到最后一个节点 将该节点NextNode指向新Node
public void AddToTail(int value) {
//创建一个新节点
Node newNode = new Node(value, null);
//如果链表为空,则直接将新节点作为头节点
if (headNode == null)
headNode = newNode;
else {
//遍历到最后一个节点
Node currentNode = headNode;
while (currentNode.nextNode != null)
{ currentNode = currentNode.nextNode; }
currentNode.nextNode = newNode;
}
}
删除
按值删除:单值
精髓:删除不是让你真删掉,而是将Node的指针置null 这样gc的时候就自动回收了
找到需要删除的节点的上一个节点,将其nextNode = 要删除节点的下一个Node
//按值删除
public void RemoveForValue(int value) {
//如果链表为空,则直接返回
if (headNode == null)
return;
//如果头节点就是要删除的节点,则直接将头节点指向目标的下一个节点
//相当于断开了原来的头节点 使其无用
if (headNode.value == value) {
headNode =headNode.nextNode;
return;
}
//遍历链表
Node currentNode = headNode;
while (currentNode!=null&¤tNode.nextNode != null)
{
if (currentNode.nextNode.value != value)
currentNode = currentNode.nextNode;
else
currentNode.nextNode = currentNode.nextNode.nextNode;
}
}
按值删除:删除所有匹配到的重复值
public void RemoveForValue(int value)
{
// 1. 处理头节点的所有重复值(用while循环替代if)
while (headNode != null && headNode.value == value)
{
headNode = headNode.nextNode; // 连续删除头节点中的重复值
}
// 2. 遍历删除中间和尾节点的重复值
Node currentNode = headNode;
while (currentNode != null)
{
// 检查当前节点的下一个节点是否是目标值(避免漏判尾节点)
while (currentNode.nextNode != null && currentNode.nextNode.value == value)
{
currentNode.nextNode = currentNode.nextNode.nextNode; // 删除下一个节点(重复值)
}
currentNode = currentNode.nextNode; // 移动到下一个非重复值节点
}
}
按节点删除 略
这个你知道有这么回事就行了 一般不会用到 因为他在使用的时候需要声明要删除的Node 所以从用户角度来看就不太友好 不建议使用
搜索
按值遍历
精髓:没有精髓 遍历按值打印即可
public bool SerachValue(int value)
{
if (headNode == null) { Console.WriteLine("链表为空 无法找到指定值"); return false; }
Node currentNode = headNode;
while (currentNode != null&& currentNode.nextNode != null)
{
if (currentNode.value == value)
{
Console.WriteLine("包含指定值" + value);
return true;
}
else {
currentNode = currentNode.nextNode;
}
}
Console.WriteLine("链表内没有指定值" + value);
return false;
}
遍历全部
精髓:没有精髓 遍历按值打印即可
public void PrintAllValue() {
if (headNode == null) return;
Node currentNode = headNode;
while (currentNode!= null)
{
Console.WriteLine(currentNode.value);
currentNode = currentNode.nextNode;
}
}
测试代码:
LinkeList linke = new LinkeList();
linke.AddToHead(2);
linke.AddToHead(1);
linke.AddToTail(3);
//1 2 3
linke.RemoveForValue(2);
//1 3
Console.WriteLine(linke.SerachValue(2));//false
Console.WriteLine(linke.SerachValue(1));//true
linke.PrintAllValue(); // 1 3
全部代码
using System.Buffers;
LinkeList linke = new LinkeList();
linke.AddToHead(2);
linke.AddToHead(1);
linke.AddToTail(3);
//1 2 3
linke.RemoveForValue(2);
//1 3
Console.WriteLine(linke.SerachValue(2));//false
Console.WriteLine(linke.SerachValue(1));//true
linke.PrintAllValue(); // 1 3
/// <summary>
/// 链表节点应该包含 值 和 指针
/// </summary>
class Node {
public int value;
public Node nextNode;
public Node(int value, Node newNode) {
this.value = value;
this.nextNode = newNode;
}
}
class LinkeList {
public Node headNode;
public LinkeList() {
headNode = null;
}
#region Add
//头插: 新节点的next指向头节点,然后将头节点指向新节点
public void AddToHead(int value)
{
//创建一个新节点 并把原来的头节点放到后面去 这就是头插法的精髓
Node newNode = new Node(value, headNode);
//将头节点指向新节点
headNode = newNode;
}
//尾插
public void AddToTail(int value)
{
//创建一个新节点
Node newNode = new Node(value, null);
//如果链表为空,则直接将新节点作为头节点
if (headNode == null)
headNode = newNode;
else
{
//遍历到最后一个节点
Node currentNode = headNode;
while (currentNode.nextNode != null)
{ currentNode = currentNode.nextNode; }
currentNode.nextNode = newNode;
}
}
#endregion
#region Remove
//按值删除
public void RemoveForValue(int value) {
//如果链表为空,则直接返回
if (headNode == null)
return;
//如果头节点就是要删除的节点,则直接将头节点指向目标的下一个节点
//相当于断开了原来的头节点 使其无用
if (headNode.value == value) {
headNode =headNode.nextNode;
return;
}
//遍历链表
Node currentNode = headNode;
while (currentNode!=null&¤tNode.nextNode != null)
{
if (currentNode.nextNode.value != value)
currentNode = currentNode.nextNode;
else
currentNode.nextNode = currentNode.nextNode.nextNode;
}
}
#endregion
#region Search
public bool SerachValue(int value)
{
if (headNode == null) { Console.WriteLine("链表为空 无法找到指定值"); return false; }
Node currentNode = headNode;
while (currentNode != null&& currentNode.nextNode != null)
{
if (currentNode.value == value)
{
Console.WriteLine("包含指定值" + value);
return true;
}
else {
currentNode = currentNode.nextNode;
}
}
Console.WriteLine("链表内没有指定值" + value);
return false;
}
#endregion
#region 遍历打印
public void PrintAllValue() {
if (headNode == null) return;
Node currentNode = headNode;
while (currentNode!= null)
{
Console.WriteLine(currentNode.value);
currentNode = currentNode.nextNode;
}
}
#endregion
}
2.有尾指针情况
这个的特别之处在于尾巴辅助的话 尾插不用遍历到最后尾巴
初始化的时候需要注意一下
尾插
class LinkeList
{
public Node headNode;
public Node tailNode;
public LinkeList()
{
headNode = tailNode = null;
}
}
// 尾插
public void AddToTail(int value)
{
Node newNode = new Node(value);
if (headNode == null)
headNode = tailNode = newNode;
tailNode.nextNode = newNode;
tailNode = newNode;
}
其他的就没什么了和无虚拟头节点的代码和方法几乎是一样的
全部代码
using System.Diagnostics;
LinkeList linkeList = new LinkeList();
linkeList.AddToHead(2);
linkeList.AddToHead(1);
linkeList.AddToTail(3);
linkeList.RemoveForValue(3);
linkeList.SerachValue(2);
linkeList.SerachValue(3);
linkeList.PrintAllValue();
class Node
{
public int value;
public Node nextNode;
public Node(int value, Node newNode = null)
{
this.value = value;
this.nextNode = newNode;
}
}
class LinkeList
{
public Node headNode;
public Node tailNode;
public LinkeList()
{
headNode = tailNode = null;
}
#region Add
// 头插
public void AddToHead(int value)
{
Node newNode = new Node(value,headNode);
if (headNode == null)
headNode = tailNode = newNode;
headNode = newNode;
}
// 尾插
public void AddToTail(int value)
{
Node newNode = new Node(value);
if (headNode == null)
headNode = tailNode = newNode;
if(tailNode!= null)
tailNode.nextNode = newNode;
else
tailNode = newNode;
}
#endregion
#region Remove
// 按值删除:双向查找 删除第一个找到的值
public void RemoveForValue(int value)
{
//头空 直接返回
if (headNode == null)
return;
//只有一个头
if (headNode.value == value)
{
if (headNode.nextNode == null)
headNode = tailNode = null;
return;
}
Node currentNode = headNode;
while (currentNode!=null && currentNode.nextNode != null)
{
//如果下一个节点的值等于要删除的值
if (currentNode.nextNode.value == value) {
//在尾巴上 就更新尾巴
if (currentNode.nextNode == tailNode)
{
tailNode = currentNode;
}
//不在尾巴上 就干掉下一个节点
currentNode.nextNode = currentNode.nextNode.nextNode;
}else
currentNode = currentNode.nextNode;
}
}
#endregion
#region Search
public bool SerachValue(int value)
{
if (headNode == null)
return false;
Node currentNode = headNode;
while (currentNode != null && currentNode.nextNode != null)
{
//如果下一个节点的值等于要删除的值
if (currentNode.nextNode.value == value)
{
Console.WriteLine("找到了目标值"+value);
return true;
}
else
currentNode = currentNode.nextNode;
}
Console.WriteLine("没找到了目标值" + value);
return false;
}
#endregion
#region 遍历打印
public void PrintAllValue()
{
Node currentNode = headNode;
while (currentNode != null)
{
Console.WriteLine(currentNode.value);
currentNode = currentNode.nextNode;
}
}
#endregion
}
3.有虚拟头节点情况
我认为其没有什么特别的含义 只是省去了头节点为null的判断 我截图对比一下
左无头 右有头
全部代码
using System;
LinkeList linke = new LinkeList();
linke.AddToHead(2);
linke.AddToHead(1);
linke.AddToTail(3);
// 1 2 3
linke.RemoveForValue(2);
// 1 3
Console.WriteLine(linke.SerachValue(2));// false
Console.WriteLine(linke.SerachValue(1));// true
linke.PrintAllValue(); // 1 3
/// <summary>
/// 链表节点应该包含 值 和 指针
/// </summary>
class Node
{
public int value;
public Node nextNode;
public Node(int value, Node newNode = null)
{
this.value = value;
this.nextNode = newNode;
}
}
class LinkeList
{
// 虚拟头节点
private Node dummyHead;
public LinkeList()
{
// 初始化虚拟头节点
dummyHead = new Node(0);
}
#region Add
// 头插: 新节点的next指向虚拟头节点的下一个节点,然后将虚拟头节点的next指向新节点
public void AddToHead(int value)
{
Node newNode = new Node(value, dummyHead.nextNode);
dummyHead.nextNode = newNode;
}
// 尾插
public void AddToTail(int value)
{
Node newNode = new Node(value);
Node currentNode = dummyHead;
while (currentNode.nextNode != null)
{
currentNode = currentNode.nextNode;
}
currentNode.nextNode = newNode;
}
#endregion
#region Remove
// 按值删除
public void RemoveForValue(int value)
{
Node currentNode = dummyHead;
while (currentNode != null && currentNode.nextNode != null)
{
if (currentNode.nextNode.value == value)
{
currentNode.nextNode = currentNode.nextNode.nextNode;
}
else
{
currentNode = currentNode.nextNode;
}
}
}
#endregion
#region Search
public bool SerachValue(int value)
{
Node currentNode = dummyHead.nextNode;
while (currentNode != null)
{
if (currentNode.value == value)
{
Console.WriteLine("包含指定值" + value);
return true;
}
currentNode = currentNode.nextNode;
}
Console.WriteLine("链表内没有指定值" + value);
return false;
}
#endregion
#region 遍历打印
public void PrintAllValue()
{
Node currentNode = dummyHead.nextNode;
while (currentNode != null)
{
Console.WriteLine(currentNode.value);
currentNode = currentNode.nextNode;
}
}
#endregion
}
4.循环单链表
我直接用情况2 的代码改的 核心在于:
- 尾节点的
nextNode
指向头节点(形成环) - 遍历 / 搜索时通过头节点判断终止条件(避免死循环)
- 维护头尾指针的环结构一致性
你要是问都循环了 还区分头尾节点有必要吗?
有的兄弟,有的 这样头尾插都是O1
几个特别说明的点
增加时 更新环结构
// 尾插法:新节点的next指向头节点,原尾节点的next指向新节点,更新尾节点
public void AddToTail(int value)
{
Node newNode = new Node(value, headNode); // 新节点的next指向头节点(形成环)
if (tailNode == null)
{
// 空链表:头尾节点指向新节点,自环
headNode = tailNode = newNode;
newNode.nextNode = newNode;
}
else
{
tailNode.nextNode = newNode; // 原尾节点连接新节点
tailNode = newNode; // 尾节点更新为新节点
}
}
删除时 删的头节点
public void RemoveForValue(int value)
{
if (headNode == null) return;
// 情况1:删除头节点
if (headNode.value == value)
{
if (headNode == tailNode) // 只有一个节点
{
headNode = tailNode = null; // 环断开
}
else // 多个节点,头节点后移,尾节点的next指向新头节点
{
headNode = headNode.nextNode;
tailNode.nextNode = headNode; // 尾节点保持环结构
}
return;
}
.................
}
非删头节点 注意遍历终止条件
while (previous.nextNode != headNode)
{
if (previous.nextNode.value == value)
{
Node target = previous.nextNode;
if (target == tailNode)
{
tailNode = previous;
tailNode.nextNode = headNode;
}
else
{
previous.nextNode = target.nextNode;
}
}
else
{
previous = previous.nextNode;
}
}
全部代码
class Node
{
public int value;
public Node nextNode;
public Node(int value, Node nextNode = null)
{
this.value = value;
this.nextNode = nextNode;
}
}
class CircularLinkedList
{
public Node headNode;
public Node tailNode;
public CircularLinkedList()
{
headNode = tailNode = null;
}
// 头插法
public void AddToHead(int value)
{
Node newNode = new Node(value, headNode);
if (headNode == null)
{
headNode = tailNode = newNode;
newNode.nextNode = newNode;
}
else
{
tailNode.nextNode = newNode;
headNode = newNode;
}
}
// 按值删除节点
public void RemoveForValue(int value)
{
if (headNode == null) return;
// 处理头节点是要删除的值的情况
while (headNode != null && headNode.value == value)
{
if (headNode == tailNode)
{
headNode = tailNode = null;
return;
}
headNode = headNode.nextNode;
tailNode.nextNode = headNode;
}
Node previous = headNode;
while (previous.nextNode != headNode)
{
if (previous.nextNode.value == value)
{
Node target = previous.nextNode;
if (target == tailNode)
{
tailNode = previous;
tailNode.nextNode = headNode;
}
else
{
previous.nextNode = target.nextNode;
}
}
else
{
previous = previous.nextNode;
}
}
}
// 遍历打印链表
public void PrintAllValue()
{
if (headNode == null) return;
Node current = headNode;
do
{
Console.WriteLine(current.value);
current = current.nextNode;
} while (current != headNode);
}
}