c# 数据结构 链表篇 有关单链表的一切

        本人能力有限,本文仅作学习交流与参考,如有不足还请斧正

目录

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&&currentNode.nextNode != null){if (currentNode.nextNode.value != value)currentNode = currentNode.nextNode;elsecurrentNode.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));//truelinke.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));//truelinke.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&&currentNode.nextNode != null){if (currentNode.nextNode.value != value)currentNode = currentNode.nextNode;elsecurrentNode.nextNode = currentNode.nextNode.nextNode;}}#endregion#region Searchpublic 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;elsetailNode = 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;}elsecurrentNode = currentNode.nextNode;}}#endregion#region Searchpublic 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;}elsecurrentNode = 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));// truelinke.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 Searchpublic 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 的代码改的 核心在于:

  1. 尾节点的 nextNode 指向头节点(形成环)
  2. 遍历 / 搜索时通过头节点判断终止条件(避免死循环)
  3. 维护头尾指针的环结构一致性

        你要是问都循环了 还区分头尾节点有必要吗?

        有的兄弟,有的 这样头尾插都是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);}
}

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

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

相关文章

蓝桥杯C++组算法知识点整理 · 考前突击(上)【小白适用】

【背景说明】本文的作者是一名算法竞赛小白&#xff0c;在第一次参加蓝桥杯之前希望整理一下自己会了哪些算法&#xff0c;于是有了本文的诞生。分享在这里也希望与众多学子共勉。如果时间允许的话&#xff0c;这一系列会分为上中下三部分和大家见面&#xff0c;祝大家竞赛顺利…

pipe匿名管道实操(Linux)

管道相关函数 1 pipe 是 Unix/Linux 系统中的一个系统调用&#xff0c;用于创建一个匿名管道 #include <unistd.h> int pipe(int pipefd[2]); 参数说明&#xff1a; pipefd[2]&#xff1a;一个包含两个整数的数组&#xff0c;用于存储管道的文件描述符&#xff1a; pi…

centos-stream-9上安装nvidia驱动和cuda-toolkit

这里写目录标题 驱动安装1. 更新系统2. NVIDIA GPU安装检查系统是否安装了 NVIDIA GPU2.1 首先&#xff0c;使用以下命令更新 DNF 软件包存储库缓存&#xff1a;2.2 安装编译 NVIDIA 内核模块所需的依赖项和构建工具2.3 在 CentOS Stream 9 上添加官方 NVIDIA CUDA 软件包存储库…

LDAP高效数据同步:Syncrepl复制模式实战指南

#作者&#xff1a;朱雷 文章目录 一、Syncrepl 复制简介1.1. 什么是复制模式1.2. 什么是 syncrepl同步复制 二、Ldap环境部署三、配置复制类型3.1. 提供者端配置3.2. 消费者端配置3.3.启动服务3.4.测试同步是否生效 四、总结 一、Syncrepl 复制简介 1.1. 什么是复制模式 Ope…

Linux 内核网络协议栈中的 struct packet_type:以 ip_packet_type 为例

在 Linux 内核的网络协议栈中,struct packet_type 是一个核心数据结构,用于注册特定协议类型的数据包处理逻辑。它定义了如何处理特定协议的数据包,并通过协议类型匹配机制实现协议分发。本文将通过分析 ip_packet_type 的定义和作用,深入探讨其在网络协议栈中的重要性。 …

QT Sqlite数据库-教程001 创建数据库和表-下

【1】创建带名称的数据库 #include <QtSql/QSqlDatabase> #include <QtSql/QSqlQuery> #include <QtSql/QSqlRecord> QString path QDir::currentPath(); QApplication::addLibraryPath(pathQString("/release/plugins")); QPluginLoader loader…

Cannot find module ‘vue‘ or its corresponding type declarations

在使用vue3vite创建新的工程时&#xff0c;在新增.vue文件时会出现Cannot find module vue这个错误。 只需要我们在项目中的.d.ts文件中添加以下代码即可 declare module *.vue {import { defineComponent } from vue;const component: ReturnType<typeof defineComponent&…

SSRF打靶总结

文章目录 一. PortSwigger1、本地服务器的基本SSRF2、基本的目标不是漏洞机3、Referer标头的外带SSRF4、简单黑名单的SSRF黑名单绕过思路&#xff1a; 5、重定向的SSRF6. 简单的白名单SSRF白名单绕过思路&#xff1a; 二、BWAPP1. SSRF 文件包含漏洞 | 内网探测2. XXE -> S…

STL-函数对象

1.函数对象 1.1 概念 重载函数调用操作符的类&#xff0c;其对象被称为函数对象 函数对象使用重载的&#xff08;&#xff09;时&#xff0c;行为类似函数调用&#xff0c;也成为仿函数 本质&#xff1a;函数对象&#xff08;仿函数&#xff09;是一个类&#xff0c;不是一…

多线程(Java)

注&#xff1a;本文为本人学习过程中的笔记 1.导入 1.进程和线程 我们希望我们的程序可以并发执行以提升效率&#xff0c;此时引入了多进程编程。可是创建进程等操作开销太大&#xff0c;于是就将进程进一步拆分成线程&#xff0c;减少开销。进程与进程之间所涉及到的资源是…

在 Dev-C++中编译运行GUI 程序介绍(三)有趣示例一组

在 Dev-C中编译运行GUI程序介绍&#xff08;三&#xff09;有趣示例一组 前期见 在 Dev-C中编译运行GUI 程序介绍&#xff08;一&#xff09;基础 https://blog.csdn.net/cnds123/article/details/147019078 在 Dev-C中编译运行GUI 程序介绍&#xff08;二&#xff09;示例&a…

【高校主办】2025年第四届信息与通信工程国际会议(JCICE 2025)

重要信息 会议网址&#xff1a;www.jcice.org 会议时间&#xff1a;2025年7月25-27日 召开地点&#xff1a;哈尔滨 截稿时间&#xff1a;2025年6月15日 录用通知&#xff1a;投稿后2周内 收录检索&#xff1a;EI,Scopus 会议简介 JCICE 2022、JCICE 2023、JCICE 2…

【Linux】Linux 操作系统 - 03 ,初步指令结尾 + shell 理解

文章目录 前言一、打包和压缩二、有关体系结构 (考)面试题 三、重要的热键四、shell 命令及运行原理初步理解五、本节命令总结总结 前言 本篇文章 , 笔者记录的笔记内容包含 : 基础指令 、重要热键 、shell 初步理解 、权限用户的部分问题 。 内容皆是重要知识点 , 需要认真理…

Python: sqlite3.OperationalError: no such table: ***解析

出现该错误说明数据库中没有成功创建 reviews 表。以下是完整的解决方案: 步骤 1:创建数据库表 在插入数据前,必须先执行建表语句。请通过以下任一方式创建表: 方式一:使用 SQLite 命令行 bash 复制 # 进入 SQLite 命令行 sqlite3 reviews.db# 执行建表语句 CREATE T…

VSCode CLine 插件自定义配置使用 Claude 3.7 模型进行 AI 开发

一个互联网技术玩家&#xff0c;一个爱聊技术的家伙。在工作和学习中不断思考&#xff0c;把这些思考总结出来&#xff0c;并分享&#xff0c;和大家一起交流进步。 本文介绍如何在 Visual Studio Code (VSCode) 中安装和自定义配置 CLine 插件&#xff0c;并使用 Claude 3.7 模…

【VSCode配置】运行springboot项目和vue项目

目录 安装VSCode安装软件安装插件VSCode配置user的全局设置setting.jsonworkshop的项目自定义设置setting.jsonworkshop的项目启动配置launch.json 安装VSCode 官网下载 安装软件 git安装1.1.12版本&#xff0c;1.2.X高版本无法安装node14以下版本 nvm安装&#xff08;github…

linux shell编程之条件语句(二)

目录 一. 条件测试操作 1. 文件测试 2. 整数值比较 3. 字符串比较 4. 逻辑测试 二. if 条件语句 1. if 语句的结构 (1) 单分支 if 语句 (2) 双分支 if 语句 (3) 多分支 if 语句 2. if 语句应用示例 (1) 单分支 if 语句应用 (2) 双分支 if 语句应用 (3) 多分支 …

榕壹云在线商城系统:基于THinkPHP+ Mysql+UniApp全端适配、高效部署的电商解决方案

项目背景&#xff1a;解决多端电商开发的痛点 随着移动互联网的普及和用户购物习惯的碎片化&#xff0c;传统电商系统面临以下挑战&#xff1a; 1. 多平台适配成本高&#xff1a;需要同时开发App、小程序、H5等多端应用&#xff0c;重复开发导致资源浪费。 2. 技术依赖第三方…

神经动力学系统与计算及AI拓展

大脑&#xff0c;一个蕴藏在我们颅骨之内的宇宙&#xff0c;以活动脉动&#xff0c;如同由电信号和化学信号编织而成的交响乐&#xff0c;精巧地协调着思想、情感和行为。但是&#xff0c;这种复杂的神经元舞蹈是如何产生我们丰富多彩的精神生活的呢&#xff1f;这正是神经动力…

K8s常用基础管理命令(一)

基础管理命令 基础命令kubectl get命令kubectl create命令kubectl apply命令kubectl delete命令kubectl describe命令kubectl explain命令kubectl run命令kubectl cp命令kubectl edit命令kubectl logs命令kubectl exec命令kubectl port-forward命令kubectl patch命令 集群管理命…