C#集合本质之链表的用法详解

链表的由来和定义

在现实生活中,我们把不同的商品放在一个购物车中。而在面向对象的世界里,有时候,也需要把不同类型的数据放到一起,组成一个集合。集合中的元素并不是彼此孤立的,在C#中,如何表达集合元素间的关系呢?

借助"自引用类"可以确立集合元素间的关系。比如有这样一个自引用类:

public class Node
{
    public int Data{get;set;}
    public Node Next{get;set;}
    public Node(int dataValue)
    {}
}

Node类的最大特点是:存在一个Node类型的属性,这个属性指向Node的另一个实例,Next属性也称为"引用链"。放到集合的场景中来说就是:把多个Node实例放到一个集合中,每一个Node实例包含一个Next属性指向下一个Node实例。而该集合中的最后一个Node实例会指向null。用图表示就是:

链表就是自引用类对象的线性集合,即序列。

由于每个自引用对象是由引用链链接起来,所以叫链表。堆栈与队列是约束版的链表,而二叉查找数是非线性数据结构。

链表的节点或元素虽然在逻辑上是连续的、线性的,当其内存不是连续存储的;数组元素在内存中是连续的,所以我们才可以通过索引来访问数组元素。

创建一个单向链表

首先创建一个节点,是一个自引用类:

namespace LinkedListLibrary
{
    public class ListNode
    {
        //当前节点对象
        public object Data { get; private set; }
        //Next属性也称为链,指向另一个ListNode对象实例,这样就把2个ListNode对象实例链接起来了
        public ListNode Next { get; set; }
        public ListNode(object dataValue): this(dataValue, null)
        {

        }
        public ListNode(object dataValue, ListNode nextNode)
        {
            Data = dataValue;
            Next = nextNode;
        }
    }
}

再模拟一个链表,如下:

namespace LinkedListLibrary
{
    public class List
    {
        private ListNode firstNode;
        private ListNode lastNode;
        private string name;
        public List(string listName)
        {
            name = listName;
            firstNode = lastNode = null;
        }
        public List() : this("list"){}

         ......
        //如果第一个节点是null,那就说明集合为空
        public bool IsEmpty()
        {
            return firstNode == null;
        }
    }
}

以上,如果第一个节点为null,那就说明该链表为空。List类提供了IsEmpty方法用来判断链表是否为空。List还包含另外5个重要的方法,下面展开来说。

在链表的的第一个节点前插入。

        //在最前面插入元素、节点
        public void InsertAtFront(object insertItem)
        {
            if (IsEmpty())//如果集合为空,加进来一个元素,相当于第一个节点和第二个节点相同,都是新加的元素
            {
                firstNode = lastNode = new ListNode(insertItem);
            }
            else //如果集合不为空,第一个节点就是新加的元素,原先的第一个节点变为下一个节点
            {
                firstNode = new ListNode(insertItem, firstNode);
            }
        }

以上,当集合不为空的情况下,实际上是把新添加的节点设为第一个节点,并把新的第一个节点的引用链指向原先的第一个节点。

在链表的最后一个节点后插入。

        public void InsertAtBack(object insertItem)
        {
            if (IsEmpty())//如果原先集合为空,第一个节点和最后一个节点就是新加的节点
            {
                firstNode = lastNode = new ListNode(insertItem);
            }
            else//如果原先的集合不为空,最后一个节点的属性值就是新加的节点
            {
                lastNode = lastNode.Next = new ListNode(insertItem);
            }
        }

以上,当集合不为空的情况下,实际上是把新添加的节点设置成最后一个节点,并把新的最后一个节点的引用链指向null。

移除链表最前面的节点。

        //移除最前面的元素、节点
        //即重新设置第一个节点的Next属性
        public object RemoveFromFront()
        {
            if (IsEmpty())
                throw new EmptyListException(name);
            //从第一个节点中取出节点对象
            object removeItem = firstNode.Data;
            if (firstNode == lastNode) //如果集合中只有一个元素
            {
                firstNode = lastNode = null;
            }
            else //正常情况下,把firstNode的Next属性所指向的节点赋值给第一个节点
            {
                firstNode = firstNode.Next;
            }
            return removeItem;
        }

以上,本质是把原先排在第二位置的节点设置成第一个节点。

移除链表最后面的节点。

        //移除最后面的元素、节点
        public object RemoveFromBack()
        {
            if (IsEmpty())
            {
                throw new EmptyListException();
            }
            //从最后一个节点中获取节点对象
            object removeItem = lastNode.Data;
            if (firstNode == lastNode)//如果当前集合只有一个节点
            {
                firstNode = lastNode = null;
            }
            else
            {
                //先把第一个节点作为当前节点
                ListNode current = firstNode;
                //改变除最后一个节点之外的节点的值
                while (current.Next != lastNode)
                {
                    current = current.Next;
                }
                //最后current变成倒数第二个节点
                lastNode = current;
                current.Next = null;//最后一个节点的Next属性为null,即没有指向另一个节点
            }
            return removeItem;
        }

以上,从第一个节点开始,一直循环到倒数第二个节点,current就像一个指针,每指到一个节点,就把该节点的下面一个节点设置为当前节点。最后,把倒数第二个节点设置为最后一个节点。 把Current的引用链设置为null,让其能被垃圾回收机制回收。

打印链表。

        //打印显示
        public void Display()
        {
            if (IsEmpty())
            {
                Console.WriteLine("集合" + name + "为空");
            }
            else
            {
                Console.WriteLine("集合的名称是:" + name);
                //先把第一个节点作为当前节点
                ListNode current = firstNode;
                while (current != null)
                {
                    //把当前节点对象打印出来
                    Console.Write(current.Data + " ");
                    //把下一个节点设置为当前节点
                    current = current.Next;
                }
                Console.WriteLine("\n");
            }
        }   

以上,从第一个节点开始,一直循环到最后一个节点,current就像一个指针,每打印一个节点,就把当前节点设置为下一个节点,一直循环下去。

EmptyListException用来抛出链表为空的异常。

namespace LinkedListLibrary
{
    public class EmptyListException : Exception
    {
        public EmptyListException() : base("当前集合为空"){}
        public EmptyListException(string name) : base("集合" + name + "为空"){}
        public EmptyListException(string exception, Exception inner) : base(exception, inner){}
    }
}

客户端调用:

using LinkedListLibrary;
namespace ListTest
{
    class Program
    {
        static void Main(string[] args)
        {
            List list = new List();
            bool aBoolean = true;
            char aChar = 'a';
            int anInt = 12;
            string aStr = "hi";
            list.InsertAtFront(aBoolean);
            list.Display();
            list.InsertAtFront(aChar);
            list.Display();
            list.InsertAtBack(anInt);
            list.Display();
            list.InsertAtBack(aStr);
            list.Display();
            object removeObject;
            try
            {
                removeObject = list.RemoveFromFront();
                Console.WriteLine(removeObject + "被删除了...");
                list.Display();
                removeObject = list.RemoveFromFront();
                Console.WriteLine(removeObject + "被删除了...");
                list.Display();
                removeObject = list.RemoveFromBack();
                Console.WriteLine(removeObject + "被删除了...");
                list.Display();
                removeObject = list.RemoveFromBack();
                Console.WriteLine(removeObject + "被删除了...");
                list.Display();
            }
            catch (EmptyListException emptyListException)
            {
                Console.Error.WriteLine("\n" + emptyListException);
            }
            Console.ReadKey();
        }
    }
}

其它链表

以上,创建的是单向链表,其特点是第一个节点开始包含引用链,每个节点的引用链指向下一个节点,最后一个节点的引用链为null。单向链表只能从一个方向遍历。

环形单向链表与单向链表的区别是:其最后一个节点的引用链指向第一个节点。环形单向链表也只能从一个方向遍历,只不过遍历到最后一个节点后,又回到第一个节点重新开始遍历。

双向链表的第一个节点只包含指向下一个节点的引用链,最后一个节点只包含指向上一个节点的引用链,其它节点同时包含指向前一个节点和后一个节点的引用链。双向链表支持向前和向后遍历。

环形双向链表与双向链表的区别是:第一个节点向后引用链指向最后一个节点,而最后一个节点的向前引用链指向第一个节点。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • C#实现的简单链表类实例

    本文实例讲述了C#实现的简单链表类.分享给大家供大家参考.具体如下: 一.关于C#链表 C#链表可用类LinkedList来存放.本文中的类MyLinkedList只是实现了该类的最基本的功能.C#中没有指针,但因为C#中类在赋值时传递的是地址,因此仍然可以利用这点制作一个链表. 二.结点类Node和链表类MyLinkedList代码 /// <summary> /// 链表结点 /// </summary> class Node { //结点数据,前后结点 public obje

  • C#通过链表实现队列的方法

    本文实例讲述了C#通过链表实现队列的方法.分享给大家供大家参考.具体实现方法如下: public class Node { public int Data { get; set; } public Node Next { get; set; } public Node(int data) { this.Data = data; } } public class Queue { private Node _head; private Node _tail; private int _count =

  • C#模拟链表数据结构的实例解析

    写在前面 模块化编程是大多数初学者必经之路,然后可能你走向了结构化编程,链表是一种典型结构模式,它的出现克服了数组必须预先知道大小的缺陷,听不懂?你只需要记住,链表结构非常牛叉就可以了,学习这种结构对我们的逻辑思维有很大提升. 什么是链表结构呢? 链表是一种物理存储单元上非连续.非顺序的存储结构.比如A->B->C,这种结构,我们可以理解为A连接着B,B连接C,像这种结构我们就叫做链表结构.对了,火车的车厢,其实就是链表的结构的最好说明 为什么要有链表结构呢? 学过计算机的都知道数组(Arra

  • C#如何自定义线性节点链表集合

    本例子实现了如何自定义线性节点集合,具体代码如下: using System; using System.Collections; using System.Collections.Generic; namespace LineNodeDemo { class Program { static void Main(string[] args) { LineNodeCollection lineNodes = new LineNodeCollection(); lineNodes.Add(new

  • C#集合之链表的用法

    LinkedList<T>是一个双向链表,其元素会指向它前面和后面的元素.这样,通过移动到下一个元素可以正向遍历链表,通过移动到前一个元素可以反向遍历链表. 链表在存储元素时,不仅要存储元素的值,还必须存储每个元素的下一个元素和上一个元素的信息.这就是LinkedList<T>包含LinkedListNode<T>类型的元素的原因.使用LinkedListNode<T>,可以获得列表中的下一个和上一个元素.LinkedListNode<T>定义了

  • C#集合本质之链表的用法详解

    链表的由来和定义 在现实生活中,我们把不同的商品放在一个购物车中.而在面向对象的世界里,有时候,也需要把不同类型的数据放到一起,组成一个集合.集合中的元素并不是彼此孤立的,在C#中,如何表达集合元素间的关系呢? 借助"自引用类"可以确立集合元素间的关系.比如有这样一个自引用类: public class Node { public int Data{get;set;} public Node Next{get;set;} public Node(int dataValue) {} }

  • C#集合本质之堆栈的用法详解

    在"了解集合本质必须要知晓的概念-链表"中,我们了解了链表的概念和种类,并且模拟了一个单向链表.本篇体验的堆栈是约束版的链表,只能在栈顶接收新节点和释放节点. 堆栈的主要操作是压栈和出栈.压栈是将新节点放在栈顶,出栈是从栈顶取出一个节点,返回新弹出节点的数据项.堆栈也称为后进先出的数据结构. 接着上一篇,写一个派生于List的类来模拟堆栈的压栈和出栈. namespace LinkedListLibrary { public class StackInheritance : List

  • Java Collection集合用法详解

    目录 1.集合的主要体系及分支 1.1Collection集合及实现类 2.List集合(List是带有索引的,所以多注意索引越界等问题) 2.1 List的实现类 3.Set集合 3.1HashSet(Set的实现类) 3.2TreeSet集合(Set的实现类) 4.集合的高频面试题 4.1Arraylist 与 LinkedList 异同 4.2ArrayList 与 Vector 区别 集合框架底层数据结构总结 1.Collection 1.集合的主要体系及分支 1.1Collection

  • Java Map集合用法详解

    目录 Map集合的概述 常用方法: 遍历方式: Map的实现类: HashMap TreeMap 集合嵌套(补充知识): 高频面试题 1.Map 2.HashMap的底层实现 Map集合的概述 概述:interface Map<K,V> 其中K是键的类型,键是唯一的,不重复.V是值的类型,是可以重复.且每个键可以映射最多一个值.注意的是如果存在两个相同的键时,则会将现在的值替换之前的值. 创建方式:以多态的形式创建对象. 特点: 键值对映射关系 一个键对应一个值 键不能重复,值可以重复 元素存

  • Java集合TreeSet用法详解

    第1部分 TreeSet介绍 TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口. TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法. TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法.比如查找与指定目标最匹配项. TreeSet 实现了Cl

  • Java常用集合之Set和Map的用法详解

    目录 常用Set集合 Set集合的特点 HashSet 创建对象 常用方法 遍历 常用Map集合 Map集合的概述 HashMap 创建对象 常用方法 遍历 HashMap的key去重原理 常用Set集合 Set集合的特点 ​ Set接口下的集合都会有以下特点 不能存储重复元素 没有索引 HashSet HashSet集合的特点 底层数据结构是哈希表 存储元素的顺序和遍历获取出来的顺序可能不一致 没有索引 集合中不能存储重复元素 创建对象 HashSet<元素数据类型> set = new H

  • c++容器list、vector、map、set区别与用法详解

    c++容器list.vector.map.set区别 list 封装链表,以链表形式实现,不支持[]运算符. 对随机访问的速度很慢(需要遍历整个链表),插入数据很快(不需要拷贝和移动数据,只需改变指针的指向). 新添加的元素,list可以任意加入. vector 封装数组,使用连续内存存储,支持[]运算符. 对随机访问的速度很快,对头插元素速度很慢,尾插元素速度很快 新添加的元素,vector有一套算法. map 采用平衡检索二叉树:红黑树 存储结构为键值对<key,value> set 采用

  • OGNL表达式基本语法与用法详解

    一.OGNL中的#.%和$符号 #.%和$符号在OGNL表达式中经常出现,而这三种符号也是开发者不容易掌握和理解的部分.在这里我们简单介绍它们的相应用途. 1.#符号的三种用法 1)访问非根对象属性,例如示例中的#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀.实际上,#相当于ActionContext. getContext():#session.msg表达式相当于ActionContext.getContext().getSessi

  • Java List 用法详解及实例分析

    Java List 用法详解及实例分析 Java中可变数组的原理就是不断的创建新的数组,将原数组加到新的数组中,下文对Java List用法做了详解. List:元素是有序的(怎么存的就怎么取出来,顺序不会乱),元素可以重复(角标1上有个3,角标2上也可以有个3)因为该集合体系有索引 ArrayList:底层的数据结构使用的是数组结构(数组长度是可变的百分之五十延长)(特点是查询很快,但增删较慢)线程不同步 LinkedList:底层的数据结构是链表结构(特点是查询较慢,增删较快) Vector

  • Go语言学习之数组的用法详解

    目录 引言 一.数组的定义 1. 语法 2. 示例 二.数组的初始化 1. 未初始化的数组 2. 使用初始化列表 3. 省略数组长度 4. 指定索引值的方式来初始化 5. 访问数组元素 6. 根据数组长度遍历数组 三. 访问数组元素 1. 访问数组元素 2. 根据数组长度遍历数组 四.冒泡排序 五.多维数组 1. 二维数组 2. 初始化二维数组 3. 访问二维数组 六.向函数传递数组 1. 形参设定数组大小 2. 形参未设定数组大小 3. 示例 总结 引言 数组是相同数据类型的一组数据的集合,数

随机推荐