1. 程式人生 > >【數據結構】 線性表的順序表

【數據結構】 線性表的順序表

width 不能 表現 rdquo 而在 替換 改變 如果 策略

  線性表是一種最為常用的數據結構,包括了一個數據的集合以及集合中各個數據之間的順序關系。線性表從數據結構的分類上來說是一種順序結構。在Python中的tuple,list等類型都屬於線性表的一種。

  從抽象數據類型的線性表來看,一個線性表應該具有以下這些操作(以偽代碼的形式寫出):

ADT List:
    List(self)    #表的構造操作,創建一個新表
    is_empty(self)    #判斷一個表是不是空表
    len(self)    #返回表的長度
    prepend(self,elem)    #在表的開頭加入一個元素elem作為新表頭
    append(self,elem)    #在表的末尾加上一個elem
    insert(self,elem,i)    #在指定位置i加上一個元素elem
    del_first(self)    #刪除表頭元素
    del_last(self)    #刪除表尾元素
    del(self,i)    #刪除位置i的元素
    search(self,elem)    #查找元素elem在表中出現的位置,如果沒找到返回-1
    forall(self,option)    #提供一個遍歷接口,對表中每一個元素實行操作option

  另外還可以考慮一些如sort,reserve等等的操作。同時,以上所有操作都是對表本身做出變化,也可以設計一些非變化的操作,即改變並返回的是表的一個副本而原表只作為一個數據源,本身保持不變。

  考慮到計算機內存本身的特點和線性表的各種操作的效率,主要可以考慮線性表的兩種基本模型或者說實現方式:

  1. 順序表,將表元素連續地存放在一片計算機內存中,元素之間的順序由它們的儲存順序自然表現出來。

  2. 鏈接表,通過每個元素的儲存單元中額外指出下一個元素的方法來實現元素在邏輯上的連續排列。

  下面將按照這兩種實現的方式分別說明

順序表

■  順序表的基本實現方式

  順序表的大小通常在創建之時就確定下來(但是不一定一直都是這個大小不能改變,見後動態順序表的實現方式)。

  順序表的特點是計算表中任何元素位置的過程都非常簡單,是O(1)操作。如果每個元素所需要的儲存單元大小都相同為c,那麽知道一個元素的下標之後想要計算它的物理地址訪問它只需要L = L0+c*i,這是一步簡單的單元操作,花費常量時間。如果表中每個元素所需要的儲存單元大小不一樣的話沒有辦法這麽簡單地算出物理地址,但是也不難處理。這牽扯到順序表的布局方式,一共有兩種布局:第一種“基本布局”是就如我們上面所說的,每個儲存單元之間都互相相鄰挨在一起,此時每個儲存單元裏都直接儲存著元素。第二種布局稱為“元素外置布局”,有點像linux中文件系統的架構,在順序表中儲存的都是儲存元素單元的物理地址,因為地址大小都是一樣的所以順序表本身可以做到和第一類布局一樣通過簡單計算得到某個下標的元素,然後再進行一步O(1)的“通過物理地址取內容”的操作來獲取到元素。雖然中間隔了一層,但是記錄元素物理地址的 這個順序表在時間上和空間上的開銷都不大,所以可以認為這種實現形式也是實現了一般元素組成的順序表。

  技術分享

  順序表的基本實現形式確定了之後還要根據表需要的操作來進行優化和改造。比如,作為線性表的一個特點,順序表必須要支持加入/刪除元素的操作。因為順序表的大小是確定的,這就帶來一個問題,我該如何確定我加入新元素不會超出順序表規定的空間。為了有效地解決這個問題和輔助其他很多的操作,在線性表裏再加上兩個儲存單元分別用於存儲表的長度和當前表內有多少元素非常有意義。所以,一個順序表在內存中申請得到的一片區域進一步被分成了三部分,分別是記錄表容量個數,記錄當前元素個數以及數據儲存塊。

  進一步思考,內存中這個表一旦創建出來,其大小就確定了,其前後的內存空間也可能被其他數據占據導致我沒有辦法對當前的順序表進行擴容或精簡。這對於一開始就聲明好表大小不再改變的數據結構比如Python中tuple這樣的而言可能還好一點,因為它的數據儲存部分不用改變,而對於list這樣的就很麻煩了。一種很容易想到的解決方案就是在創建之時把表創建的大一點,可能初始化的時候不把表填滿,留出一部分空間待以後有可能填進來的元素填充,當元素把這部分空間填滿了之後我就申請一塊更大的空間,把原來的數據復制過去然後繼續填充。這樣就可以讓更多元素加入進表中。但是重新申請一塊空間創建一個新表會使得原來舊表的地址失效,很多引用了舊表的操作也就失效了。這給人感覺是治標不治本的

  為了解決上面這個問題,另一種順序表的結構被提出,即分離式結構。相對的我們之前說的,腦補的順序表都是一體式結構。一體式結構中表信息(表長度和當前元素個數)與元素儲存區緊鄰在一起,這樣雖然方便管理,但是當我需要換個更大的空間時會有問題。而分離式結構的順序表將元素儲存區的地址存在表信息後面(和上面元素外置布局方式很有一個意思的感覺),使得元素儲存區成為一個可以替換的模塊。當當前元素儲存區存滿,我就可以新申請一片更大的區域,把當前內容復制過去,然後直接把順序表中指向原元素儲存區的指針指向新元素儲存區。這樣就可以做到在不改變表本身地址的情況下使順序表增容了。這實際上也是python中list的實現方式。這種儲存區滿了就換新的而不影響順序表本身的表是動態順序表。

技術分享

  動態順序表也需要思考策略,比如說每次擴大儲存區的容量設置為多少比較合適。一種方案是每次擴大都是多擴大固定個元素,從復雜度的角度來看,每需要換一個儲存區復制原有數據需要花時間O(m),m為當時滿的儲存區中元素個數,計算可以得出對於最終長度為n的一個線性表,復制的總次數大概要在kn**2這個量級,k是個常量參數由每次擴大儲存區的大小決定。這看起來比較費時間的原因是更換儲存區太過頻繁了。有人提出了另一種策略,每次需要擴大儲存區時擴大數量為上一次擴大的兩倍,這樣可以計算證明其從0個元素增加至n個元素所花的時間是O(1)的。後一個策略在操作復雜度上帶來優化,但是同時它的空閑單元數最多的時候會占全部的一半,帶來了空間上的浪費。這是一個以空間換時間的案例。

■  順序表的基本操作

  ● 創建空表

  向內存申請一塊空間,在開頭記錄下表的容量max並將元素個數的計數num設置為0。

  ● 簡單判斷操作

  當num=max是判斷表滿,當num=0時判斷表空

  ● 訪問給定下標的元素

  訪問下標為i的元素時,先檢查i是否在[0,num)中,如果不是的話表明訪問越界。如果i合法的話,就計算出相關元素的地址(這個過程可能根據布局方式和結構的不同而不同)。這個操作顯然跟表一共有多少個元素沒多大關系,所以是個O(1)的操作。

  ● 遍歷操作

  按順序訪問表中元素,另外維護一個當前訪問元素的下標值,每訪問一次該值+1。因為每一次訪問都是通過計算地址得到目標元素的,所以每一次訪問都是O(1)的,一共n次訪問所以最終,遍歷操作是O(n)的。

  ● 查找給定元素的位置

  在沒有其他信息的情況下,檢索一個元素只能通過遍歷表來檢索。這稱為線性檢索。遍歷每一個元素,判斷元素的值是否和給出的值相同,否則繼續遍歷。

  ● 加入新元素

  加入元素分成好幾種情況。每成功加入一個新元素之後,表頭維護的num信息應該+1

  1,在尾部加入新元素,先判斷當前num是否等於max,如果是的話那就表明我們需要更換數據儲存區了。否則就在尾部加上相關元素。這個操作顯然是O(1)的,跟表當前有多少元素在沒有關系。

  2,把新數據存進儲存區的第i個單元,此時在判斷完表是否滿了之後還要考慮,把新數據寫入相關位置之後,原本在那個位置如果有老數據該怎麽辦。如果不要求插入後的表保持原來的順序(不保序)的話,那麽可以直接把原先那個位置的元素給放到整個表的最後面,這樣操作仍然是O(1)。如果要求保序,那麽在插入相關位置之後,從 此位置的原元素開始,之後每個元素都要向後移動一位,這麽一來使得操作和原表中一共有多少元素就掛鉤了。事實上,不論是平均還是最壞情況,保序的插入操作是O(n)的。

  ● 刪除元素

  刪除其實和插入是類似的,分成尾端刪除和定位刪除兩種情況。和插入也類似的,尾端刪除是一個O(1)操作,而定位刪除因為要考慮保序和不保序兩種情況,也分成了O(1)和O(n)兩種情形。

  除此之外,刪除還有一種比較特殊的情況,條件刪除。即看準某個符合條件的元素進行刪除。一般來說進行條件刪除也是先要遍歷表的,在遍歷中加上一個條件判斷,總體而言條件刪除仍然是一個O(n)的操作。

  總的來說,用順序表操作有優點也有缺點。一般而言其優點在於它直接按位置訪問元素,元素在表中儲存得十分緊湊,除了一些占O(1)的輔助信息以外其他的都是有效的元素信息。但是順序表的缺點也很明顯,為了能夠放下足夠多的元素,通常需要進行儲存區更換,而在更換之後又會有大量的空閑單元的出現。

■  Python中的List類型

  前面已經說過了,python中的tuple和list都是順序表的實現。此外list還是個采用了分離式結構的動態順序表,保證了在不斷加入元素的過程中,表對象的標識(id)不會改變。在python的官方實現中,為了讓list能夠更有效率地更換儲存區,采用了下面這樣的儲存區更換策略:建立空表或者很小的表的時候,系統默認給出一塊可以容納8個元素的儲存區,在元素增加的過程中,如果區滿了就換一塊4倍大的儲存區,以此類推直到表的大小達到5萬個元素左右的時候,當區再滿的時候就換一塊兩倍大的儲存區。之所以要在5萬這個節點上做出這種變化是為了避免出現過多的空閑儲存單元。

  對於list的一些主要的操作而言,有下面這幾點可以提一下:

  len函數的操作是一個O(1)的操作。因為它只是取了表頭信息中的num這個參數做了一些加工而已。

  元素訪問和賦值,尾端加入以及尾端刪除(包括尾端切片刪除)都是O(1)操作

  指定位置的元素加入,切片替換,切片刪除,表拼接(extend)都是O(n)操作

  reverse()操作是O(n),而sort()操作,python封裝的是最好的排序算法,其平均和最壞的時間復雜度都是O(nlogn)。

  另外,值得指出的一點是,python沒有提供接口讓程序員可以管理list中每個儲存單元的大小,雖然這樣設計的初衷是為了減輕編程負擔,避免人為的操作引起的錯誤,但這也無疑在一定程度上限制了使用表的自由。

由於篇幅過長,鏈接表的內容放在了另一篇筆記中。

【數據結構】 線性表的順序表