zoukankan      html  css  js  c++  java
  • 数据结构基础温故-1.线性表(上)

    开篇:线性表是最简单也是在编程当中使用最多的一种数据结构。例如,英文字母表(A,B,C,D...,Z)就是一个线性表,表中的每一个英文字母都是一个数据元素;又如,成绩单也是一个线性表,表中的每一行是一个数据元素,每个数据元素又由学号、姓名、成绩等数据项组成。顺序表链表作为线性表的两种重要的存在形式,它们是堆栈、队列、树、图等数据结构的实现基础。

    一、线性表基础

    1.1 线性表的基本定义

      线性表:零个或多个数据元素的有限序列。线性表中的元素在位置上是有序的,类似于储户去银行排队取钱,人们依次排着队,排在前面的先取,排在后面的则后取。这种位置上的有序性就是一种线性关系。由此可以看出:线性表的前后两个元素存在一一对应关系

    PS:需要注意的是,这种前后关系是逻辑意义上而非物理意义上的,就好比如果银行做了改革,使用排队机进行排队,所有储户分散在银行的各个角落,他们取钱的顺序是根据储户从排队机获取的纸条上的号码来决定的。

    1.2 线性表的存储结构

      (1)顺序表

      线性表的顺序存储结构是指【用一块地址连续的存储空间依次存储线性表中的数据元素】。就好像我们刚刚提到的改革之前的银行,需要在业务窗口前排队等候办理。由此可以看出:在顺序表中,逻辑上相邻的元素在物理上也是相邻的

      (2)链表

      相比顺序表需要预先占用一块事先分配好的存储空间,链表就灵活一些。链表中逻辑上相邻的元素在物理上可以不相邻。这就好像改革之后的银行,人们办理业务的顺序是由手上的小纸条的号码来决定。在某些特定场合,链表的使用优先于顺序表。

    二、顺序表基础

    2.1 静态顺序表之数组

      在日常编程中,在处理一组数据时,最常使用的数据类型就是数组。它是线性表的顺序存储结构在程序语言中最直接的表现形式

      数组是最基础也是存取速度最快的一种集合类型,在.NET中它是引用类型,也就是说它所需的内存空间会在托管堆上分配,一旦数组被创建,其中的所有元素会被初始化为它们的默认值。

    PS:另外需要注意的是,当数组元素为值类型时,数组对象存放的是值类型对象本身。而当元素为引用类型时,数组对象存放的则是对象的引用(指针)。

      (1)数组元素为值类型时:

    int[] arrInt = new int[5];
    arrInt[2] = 5;
    arrInt[4] = 3;

      下图展示了上面的数组arrInt在内存(这里如未说明都指在.NET中的内存分配)中的分配形式,可以看到值类型数组在被创建的同时就拥有了默认值0。

      (2)数组元素为引用类型时:

    // System.Windows.Forms.Control
    Control[] arrCtrl = new Control[5];
    arrCtrl[0] = new Button();
    arrCtrl[3] = new Label();

      下图则展示了上面的数组arrCtrl在内存中的分配,可以看到在托管堆中划分了一块能够存放5个指针的内存区域,并且每个元素都被初始化为null。如果某个元素被赋值,那么会存放一个指向实际对象存储区域的指针。

    总结:数组优点很多,缺点也很明显:在实际编程中,无法动态改变集合的大小

    2.2 动态顺序表之ArrayList与List<T>

      如果需要动态地改变数组所占用的内存空间的大小,则需要以数组为基础做进一步的抽象以实现这个功能。在C#中,ArrayList被称为动态数组,它的存储空间可以被动态地改变,同时还有添加、删除元素的功能。

      (1)简单好用但不是类型安全的ArrayList

      ①Add-添加新元素

            // 在数组末尾顺序添加指定元素
            public virtual int Add(object value)
            {
                // 当容量达到最大值时
                if (this._size == this._items.Length)
                {
                    // 调整存储空间大小
                    this.EnsureCapacity(this._size + 1);
                }
    
                this._items[this._size] = value;
                return this._size++;
            }    

      可以看到,在添加新元素时会进行数组容量的判断,如果达到最大值则会调用方法动态调整数组大小。

      ②RemoveAt-移除指定元素

            // 移除指定索引的元素
            public virtual void RemoveAt(int index)
            {
                if (index < 0 || index > this._size)
                {
                    throw new ArgumentOutOfRangeException("index", "索引超过范围");
                }
                // 插入位置后的元素向前移动一位
                for (int i = index + 1; i < this._size; i++)
                {
                    this._items[i - 1] = this._items[i];
                }
    
                this._size--;
                this._items[this._size] = null; // 最后一位空出的元素置为空
            }

      可以看到,在移除老元素时会进行大量的元素移动操作。这里的ArrayList采用的元素类型是object,所以最后将空出的元素置为null。

      ③EnsureCapacity-动态调整数组大小

            // 动态调整数组空间大小
            private void EnsureCapacity(int min)
            {
                if (this._items.Length < min)
                {
                    // 新空间大小=原空间大小*2
                    int num = (this._items.Length == 0) ?
                        _defaultCapacity : (this._items.Length * 2);
                    if (num < min)
                    {
                        num = min;
                    }
                    // 调整数组空间大小
                    this.Capacity = num;
                }
            }

      事实上,内存空间一旦分配是没有办法更改大小的。ArrayList其实使用“搬家”的方法来实现这个功能的,即当房子住不下这么多人的时候,那么换一个更大的新房子就行了。这里,ArrayList需要扩容时,会在内存空间中开辟一块新区域,容量为原来的2倍,并把所有元素都复制到新内存空间中。

      (2).NET2.0出现的泛型版本:List<T>

      由于ArrayList实际存放的是object对象(在.NET中object是万物之宗,即所有类型的父类),在进行存取操作时需要进行大量的装箱和拆箱操作(如果你不知道装箱和拆箱,那么请阅读.NET中六个重要的概念),降低程序性能。于是,从.NET 2.0开始出现了泛型版本的List<T>,它完美取代了ArrayList。

    List<int> intNumList = new List<int>();
    intNumList.Add(1);
    intNumList.Add(2);
    
    int num1 = intNumList[0];
    int num2 = intNumList[1];

      可以看到,在集合创建的时候就把元素类型限定为int类型,它是安全的,并且还避免了装箱和拆箱操作。因此,在实际编程中一般都会使用List<T>。

    三、.NET中的ArrayList与List<T>

      在.NET中已经为我们提供了两个强有力的顺序表结构类型,我们可以通过Reflector来查看其具体实现。

    3.1 ArrayList的实现

      通过查看源码,其关键就在于EnsureCapacity方法动态地调整数组大小。

    3.2 List<T>的实现

      通过查看源码,其关键就在于使用了泛型,而其他的方法如Add、Remove以及EnsureCapacity都和ArrayList没有多大区别。

    参考资料

    (1)程杰,《大话数据结构》

    (2)陈广,《数据结构(C#语言描述)》

    (3)段恩泽,《数据结构(C#语言版)》

  • 相关阅读:
    cf1100 F. Ivan and Burgers
    cf 1033 D. Divisors
    LeetCode 17. 电话号码的字母组合
    LeetCode 491. 递增的子序列
    LeetCode 459.重复的子字符串
    LeetCode 504. 七进制数
    LeetCode 3.无重复字符的最长子串
    LeetCode 16.06. 最小差
    LeetCode 77. 组合
    LeetCode 611. 有效三角形个数
  • 原文地址:https://www.cnblogs.com/edisonchou/p/4593972.html
Copyright © 2011-2022 走看看