數據結構基礎溫故-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#語言版)》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM