C#实现泛型列表List

前言:入行快一年了第一次写博客。其实寻思了一下也没啥能写的,毕竟学艺不精,不过看了好多本数据结构的书,总结一下,也是很好的,顺便帮助一下比我更菜的人...如有问题请指出,不胜感激。

其实网上关于类似的博客很多,但是好多大神会、懂,但是不一定会讲,因为他们早已忘了菜是什么感觉了。 = =,而我不一样,我会把我学习过程中遇到问题都拿出了,希望对你们有帮助。


,什么是列表?

1,列表是什么意思?

英文含义(有道翻译)

List = 列表;清单;目录。

文字含义(百度百科)

列表 = 以表格为容器,装载着文字或图表的一种形式,叫列表。

从字面含义上可以知道,列表就是用来装东西的。而在编程语言中,这里的东西就是各种指的各种类型。

列表总体上来说就是一种容器。

当然人分亚洲人欧洲人,列表也分好多种,这里全文上下所指的都是List这个列表,概念不一定可以泛用,这里注意一下。

2,容器的概念

把一个球放在瓶子里,然后瓶子就是这个球的容器,这是理所当然的。

其实编程中的容器,我更趋向于这种例子,有一块黑布,中间有个球大小的洞,你通过洞把球放进去,你也可以通过这个洞把球取出来,但是这个黑布下算不算容器,是什么你却不清楚。但是你所知的是,你可以从这个洞放,和取。

正如我们使用时的Add和Remove

我所理解的是,洞是瓶子的入口,而瓶子在编程中抽象成了一种规则。

3,瓶子的规则

这个地方,或许就很懵逼了,其实我也很懵逼,因为这是我现想的。

但是我可以说一下什么是规则。

还是那个例子,假设黑布下面什么都木有,所以其空间很大,但是你扔进去了很多球,不可能一个不漏的把球再拿出来吧?

为了避免你拿球的时候漏球,黑布下面的“容器”就开始操作了。

第一种方法,一球一槽

“容器”偶然发现了你球的发票,购买物品:球,数量:5

原来你有5个球,啊,然后容器变出来一个装球的槽,大概是这样子的

接下来你每次从洞里放进一个球,它就把它放在这个槽里,这样你想要第几个,或者全部取出来,都是很容易的。

知识点:

知道球的确定数量才可以这样做。

槽是不可扩充的。

取球很方便,直接拿就可以,毕竟没有盖子...

无法添加球,理论不可移除球,除非换个槽

第二种方法,连串串

这一次你买球的时候并没有要发票,"容器就不知道你有多少球了",但是丝毫不慌,它又有新方法了

当你放第一个球的时候,这里假定第一个球为A,“容器”偷偷的虚拟出来另一个球,俗称“头球”!

“容器”对“头球”说,“阿头,去发展下线吧!”

然后头球给A电话,A一看手机“未知号码”?

接通电话,头球:“我是你的上级,我在监视你,在这里只有我知道你的身份,如果你想活下去,就替我监视下一个人,顺便传达这番话”

然后A对B说,B对C说.....

于是就成了这个样子

这样不管来了多少球,它们都是关联的,只需要找到头球,依次找下线就行了...

知识点:

每个球都不知道自己上级是谁。

每个球都有唯一一个下级

必须要有头球。

取球很麻烦,必须从头球开始依次访问,直到找到指定球

添加很方便,移除很方便,比如移除A,头球的下线换成B,就阔以了

4,总结

1,黑布之下的空间=内存

2,一球一槽=顺序存储

3,连串串=链式存储

4,数组=顺序储存;列表=链式储存(单向链表)

5,以上说的是内存存储数据的两种方式,并不代表其它的容器类型一定是这两种,或者不是这两种。

二,C#代码实现系统自带List的部分功能

1,意义

实用意义:自带的容器不一定有你想要的方法,效率不一定有你自己实现的高

其它:几个月前面试的时候,让我实现链表,写不出来,报仇雪恨。只想搬砖的码农不是好码农...

2,自定义节点类

我们知道列表其实是链表后,其实我们只需要实现链表就行了。

节点类

class Node<T>
{
    public T data;//储存的数据
    public Node<T> Next;//自己关注的对象
    public Node(T data)//赋值用的构造方法
    {
        this.data = data;
    }
    public Node()//头结点没值,所以专门准备了一个构造方法
    {
    }
}

3,自定义列表类

class MyList<T>
{
    public MyList()//初始化链表的时候创建一个头节点
    {
        firstNode = new Node<T>();
    }
    /// <summary>
    /// 头结点
    /// </summary>
    private Node<T> firstNode;
    /// <summary>
    /// 列表长度
    /// </summary>
    public int Count { private set; get; }
}

上边也说了,不管你有没有数据,只要是链表表,就要有头节点

以下代码,全在myList内添加,所以只贴片段。

4,实现Add方法

    /// <summary>
    /// 始终是最后一个
    /// </summary>
    private Node<T> lastOne;  
    /// <summary>
    /// 添加数据
    /// </summary>
    /// <param name="t"></param>
    public void Add(T t)
    {
        Node<T> node = new Node<T>(t);
        if (Count == 0)
        {
            firstNode.Next = node;
        }
        else
        {
            lastOne.Next = node;
        }
        lastOne = node;
        Count++;
    }

新增一个数据,就new一个节点,这个节点包含当前数据和下一个节点信息

这个地方多了一个变量,为了添加方便,我们缓存一下最后一个节点,因为添加都是在最后一个节点里添加的

为了方便,新数据直接放到最后一个节点里,再把新数据当做最后一个节点。

第一个判断就简单了,没数据,就把头结点指向第一个节点

5,自定义Find方法

这个方法是列表类自己用的

    /// <summary>
    /// 找到指定节点
    /// </summary>
    /// <param name="index">找到第几个节点</param>
    /// <returns></returns>
    private Node<T> Find(int index)
    {
        if (index > Count)
            return null;

        Node<T> tempNode = firstNode;

        int tempIndex = 0;
        while (tempNode.Next != null)
        {
            if (tempIndex != index)
            {
                tempNode = tempNode.Next;
                tempIndex++;
            }
            else
            {
                return tempNode.Next;
            }
        }
        return null;
    }

最后return 返回想要节点的下一个是为什么?
比如我们想找第0个数据,其实是头结点的下一个,类推,找第N个数,就是N.next才是它的数据
因为多了一个头节点,值得注意的是链表的每次查找时间复杂度都是O(n),横向对比线性表为O(1)

6,索引器

有添加了和查找了,我们就可以存,取了

    public T this[int index]
    {
        get
        {
            if (index > Count)
                throw new System.Exception("超出索引");
            return Find(index).data;
        }
     }

添加一个索引器。

7,移除数据

    /// <summary>
    /// 移除数据
    /// </summary>
    /// <param name="index"></param>
    public void RemoveAt(int index)
    {
        if (index>Count)
            throw new System.Exception("超出索引");
        var valueUp = index == 0 ? firstNode : Find(index - 1);
        var value = valueUp.Next;
        valueUp.Next = value.Next;
        value = null;
        Count--;
    }

假设移除B,那么我们先找到A,代码中的Find(index-1)

然后A的Next等于B的Next

B就没人关注了,B=null,手动释放一下。

长度减1。

8,foreach遍历

这里我们需要继承一个接口IEnumerable

foreach遍历的时候其实就是执行一下GetEnumerator()这个方法。

我们在方法里把需要的数据返回出来就可以了。

    int tempCurrent = -1;
    public IEnumerator GetEnumerator()
    {
        while (tempCurrent<Count-1)
        {
            tempCurrent++;
            yield return Find(tempCurrent).data;
        }
        tempCurrent = -1;
    }

我把所有的数据在这里遍历了一遍,其实这里我感觉自己错了,但是说不上来哪不对...

但是有一个好处是可见的,就是继承这个接口后,可以在初始化的时候赋值了。

比如这样

        MyList<string> myList = new MyList<string>()
        {
            "哟","和","啊"
        };

我一开始并不懂这是怎么做到的,直到我把Add这个方法注释之后,你在初始化时赋值就会报错。

原来它把初始化时赋的值都调用了Add这个方法,当参数依次传了进去。

然后功能基本齐全了。

三,完整代码,和使用方式

class MyList<T>:IEnumerable
{
    public MyList()
    {
        firstNode = new Node<T>();
    }
    /// <summary>
    /// 头结点
    /// </summary>
    private Node<T> firstNode;
    /// <summary>
    /// 始终是最后一个
    /// </summary>
    private Node<T> lastOne;
    /// <summary>
    /// 长度
    /// </summary>
    public int Count { private set; get; }

    public T this[int index]
    {
        get
        {
            if (index>Count)
            {
                throw new System.Exception("超出索引");
            }
            return Find(index).data;
        }
    }
    /// <summary>
    /// 添加数据
    /// </summary>
    /// <param name="t"></param>
    public void Add(T t)
    {
        Node<T> node = new Node<T>(t);
        if (Count == 0)
        {
            firstNode.Next = node;
        }
        else
        {
            lastOne.Next = node;
        }
        lastOne = node;
        Count++;
    }
    /// <summary>
    /// 找到指定节点
    /// </summary>
    /// <param name="index">找到第几个节点</param>
    /// <returns></returns>
    private Node<T> Find(int index)
    {
        if (index > Count)//超出索引,返回
            return null;

        Node<T> tempNode = firstNode;

        int tempIndex = 0;
        while (tempNode.Next != null)
        {
            if (tempIndex != index)
            {
                tempNode = tempNode.Next;
                tempIndex++;
            }
            else
            {
                return tempNode.Next;
            }
        }
        return null;
    }
    /// <summary>
    /// 移除数据
    /// </summary>
    /// <param name="index"></param>
    public void RemoveAt(int index)
    {
        if (index > Count)
            throw new System.Exception("超出索引");
        var valueUp = index == 0 ? firstNode : Find(index - 1);
        var value = valueUp.Next;
        valueUp.Next = value.Next;
        value = null;
        Count--;
    }
    int tempCurrent = -1;
    public IEnumerator GetEnumerator()
    {
        while (tempCurrent<Count-1)
        {
            tempCurrent++;
            yield return Find(tempCurrent).data;
        }
        tempCurrent = -1;
    }
}
class Node<T>
{
    public T data;//储存的数据
    public Node<T> Next;//自己关注的对象
    public Node(T data)//赋值用的构造方法
    {
        this.data = data;
    }
    public Node()//头结点没值,所以专门准备了一个构造方法
    {
    }
}

测试了一下,和C#List对比

        List<string> List = new List<string>()
        {
            "哟","和","啊"
        };
        MyList<string> myList = new MyList<string>()
        {
            "哟","和","啊"
        };
        List.Add("o");
        myList.Add("o");
        List.RemoveAt(0);
        myList.RemoveAt(0);
        print(List.Count);
        print(myList.Count);
        foreach (var item in List)
        {
            print(item);
        }
        foreach (var item in myList)
        {
            print(item);
        }

运行结果一模一样,我就不放出来了

总结

第一次写博客,发现耗时比较长。

过程中又解决了几个之前没注意过的问题,收益立竿见影。

因为没有参照什么文章,或者书籍,实现过程都是自己猜测的,和C#List具体的实现无关,如果有什么不对的地方,请指出,不胜感激。

猜你喜欢

转载自blog.csdn.net/qq_41886896/article/details/89945238
今日推荐