主程手记 C#数据结构—— List

提示:个人学习总结

一、List是什么?

List<T> 是 C# 中使用非常频繁的一种数据结构,我们常用它来替代数组。

二、特性

1、顺序存储,改查快,增(插入)删慢;

  • 使用 Add 将新元素添加在末尾是很快的,但是使用 Insert 将新元素添加在其他位置就不同了。因为这样会导致后面元素的移动位置(后移),删除同样道理,会导致后面元素的位置前移。这样效率是很低的!

2、长度可变,容量不够会自动扩容;

  • 每次扩容,容量增加一倍。

3、需要指定数据类型,保证类型安全;

  • 同时也避免了 ArrayList 可能出现的装箱拆箱操作。

三、底层剖析

1.New - 构造

public List(int capacity) {
        if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        Contract.EndContractBlock();

        if (capacity == 0)
            _items = _emptyArray;
        else
            _items = new T[capacity];
    }

List内部是用数组实现,而非链表,并且没有指定capacity时,数组大小为0

2.Add 

        //将给定对象添加到列表末尾,列表大小+1
        //如果需要,在添加新元素前,列表容量增加一倍
        public void Add(T item) {
            if (_size == _items.Length) EnsureCapacity(_size + 1);
            _items[_size++] = item;
            _version++;
        }

每次添加新的元素都需要检查容量是否还够,不够就调用EnsureCapacity增加容量。

        //如果列表当前容量小于min,则容量将增加到当前容量的两倍或min,以较大者为准
        private void EnsureCapacity(int min) {
            if (_items.Length < min) {
                int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
                //在遇到溢出前,允许列表增长到最大可能的容量(大约2GB)
                //请注意,即使_items.Length由于Uint强制转换而溢出,此检查仍然有效
                if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
                if (newCapacity < min) newCapacity = min;
                Capacity = newCapacity;
            }
        }

在EnsureCapacity()中,每次容量不够,都会扩容一倍,_defaultCapacity表示容量的默认值为4,扩充容量默认为4->8->16->32…

总结:List使用数组形式作为底层数据结构,好处是使用索引方式提取元素很快,但在扩容的时候就会很糟糕,每次new数组都会造成内存垃圾,这给垃圾回收GC带来了很多负担。

3.Remove- 删除数据


Remove接口中包含了 IndexOf 和 RemoveAt,其中用 IndexOf 函数是位了找到元素的索引位置,用 RemoveAt 可以删除指定位置的元素。

简而言之,元素删除的原理其实就是用 Array.Copy 对数组进行覆盖。IndexOf 启用的是 Array.IndexOf 接口来查找元素的索引位置,这个接口本身内部实现是就是按索引顺序从0到n对每个位置的比较,复杂度为O(n)。

4.Insert- 插入数据

与Add接口一样,先检查容量是否足够,不足则扩容。Insert插入元素时,使用的用拷贝数组的形式,将数组里的指定元素后面的元素向后移动一个位置。

List的Add,Insert,IndexOf,Remove接口都是没有做过任何形式的优化,都使用的是顺序迭代的方式,如果过于频繁使用的话,会导致效率降低,也会造成不少内存的冗余,使得垃圾回收(GC)时承担了更多的压力。

其他相关接口比如 AddRange,RemoveRange的原理和Add与Remove一样,区别只是多了几个元素,把单个元素变成了以容器为单位的形式进行操作。都是先检查容量是否合适,不合适则扩容,或者当Remove时先得到索引位置再进行整体的覆盖掉后面的的元素,容器本身大小不会变化,只是做了重复覆盖的操作。

其他接口也同样基于数组,并使用了类似的方式来对数据做操作

5.其他接口

[]:[]的实现,直接使用了数组的索引方式获取元素。

Clear清除接口:Clear接口在调用时并不会删除数组,而只是将数组中的元素清零,并设置 _size 为 0 而已,用于虚拟地表明当前容量为0

Contains 接口:Contains 接口使用的是线性查找方式比较元素,对数组进行迭代,比较每个元素与参数的实例是否一致,如果一致则返回true,全部比较结束还没有找到,则认为查找失败。

ToArray 转化数组接口:ToArray接口中,重新new了一个指定大小的数组,再将本身数组上的内容考别到新数组上,再返回出来。

 Find 查找接口:Find接口使用的同样是线性查找,对每个元素都进行了比较,复杂度为O(n)。

Enumerator 枚举迭代:每次获取迭代器时,Enumerator 每次都是被new出来,如果大量使用迭代器的话,比如foreach就会造成大量的垃圾对象,这也是为什么我们常常告诫程序员们,尽量不要用foreach,因为 List 的 foreach 会增加有新的 Enumerator 实例,最后由GC垃圾回收掉。

Sort 排序接口:它使用了 Array.Sort接口进行排序,Array.Sort 使用的是快速排序方式进行排序,从而我们明白了 List 的 Sort 排序的效率为O(nlogn)。

总结

我们把大部分的接口都列了出来,我们可以看到 List 的效率并不高,只是通用性强而已,大部分的算法都使用的是线性复杂度的算法,这种线性算法当遇到规模比较大的计算量级时就会导致CPU的大量损耗。

List的内存分配方式也极为不合理,当List里的元素不断增加时,会多次重新new数组,导致原来的数组被抛弃,最后当GC被调用时造成回收的压力。

我们可以提前告知 List 对象最多会有多少元素在里面,这样的话 List 就不会因为空间不够而抛弃原有的数组,去重新申请数组了。

最后List 并不是高效的组件,真实情况是,他比数组的效率还要差的多,他只是个兼容性比较强得组件而已,好用,但效率差。

参考:

http://t.csdn.cn/jW3ep

主程手记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值