2018年第11周-大話數據結構的筆記(線性表)

在我看來,編程語言其實就是跟操做系統借用計算及空間資源去實現本身的業務。

數據結構緒論

數據結構歷史

計算機的誕生之初,是用於數值運算的工具。
但現實生活中,咱們不少狀況下不是處理數值計算的問題,因此須要一些更科學有效的手段(好比表、樹和圖等數據結構)的來處理問題。因此數據結構是一門研究非數值計算的程序設計問題中的操做對象,以及它們之間的關係和操做等相關問題的學科。
1968年,美國的高德納(Donald E. Knuth)教授在其寫的《計算機程序設計藝術》第一卷《基本算法》中,較系統的闡述了數據的邏輯結構和存儲結構及操做,開創了數據結構的課程體系。算法

程序設計 = 數據結構 +算法
而程序我認爲就是,編程語言(程序)其實就是跟操做系統借用計算及空間資源去實現本身的業務。

基本概念和術語

數據:是描述客觀事物的符號,是計算機中能夠操做的對象,是能被計算機識別,並輸入給計算機處理的符號集合。shell

數據不只包括整型,浮點等數值類型,還包括字符及聲音、圖像、視頻等非數值類型。
整型、浮點,咱們能夠進行數值計算。
對應字符數據類型,咱們能夠進行非數值的處理,而聲音、圖像、視頻等,咱們能夠經過編碼的手段變成字符數據來處理。

數據元素:是組成數據的、有必定意義的基本單位,在計算機中一般做爲總體處理。也被稱爲記錄編程

數據項:一個數據元素能夠由若干個數據項組成。數據項是數據不可分割的最小單位。設計模式

數據對象:是性質相同的數據元素的集合,是數據的子集。 數組

數據結構:是相互之間存在一種或多種特定關係的數據元素的集合。 數據結構

邏輯結構:是指數據對象中數據元素之間的相互關係。編程語言

包括:
集合結構:集合結構中的數據元素除了同輸一個集合外,它們之間沒有其餘關係。
線性結構:線性結構中的數據元素之間是一對一的關係。
樹形結構:樹形結構中的數據元素之間是一對多的層次關係。
圖形結構:圖形結構的數據元素是多對多的關係。

物理結構:是指數據在邏輯結構中在計算機中的存儲形式。存儲結構應正確翻譯數據元素之間的邏輯關係,這纔是最爲關鍵的。如何存儲數據元素之間的邏輯關係,是實現物理結構的重點和難點。模塊化

順序存儲結構:是把數據元素存放在地址連續的存儲單元裏,其數據間的邏輯關係和物理關係是一致的。
鏈式存儲結構:是把數據元素存放在任意的存儲單元裏,這組存儲單元能夠是聯繫的,也能夠不是連續的。該存儲並不能反應其邏輯關係,所以須要用一個指針存放數據元素的地址,這樣經過地址就能夠找到相關數據元素的位置。

數據類型:是指一組性質相同的值的集合及定義在此集合上的一些操做的總稱。函數

簡單的說: 數據類型 = 集合 + 操做
這個有點相似於Java的類的定義。

數據類型,這個概念的來源思路是:在計算機中,內存不是無限大的,你若是要計算一個如1+1=二、 3+5=8 這樣的整型數字的加減乘除運行,顯然不須要開闢很大的適合小數甚至字符運算的內存空間。因而計算機的研究者們就考慮,要對數據進行分類,分出多種數據類型。 工具

在C語言中,按照取值的不一樣,數據類型能夠分爲兩類:

原子類型:是不能夠再分解的基本類型,包括整型、實型、字符等。在Java裏,也叫基本類型。
結構類型:由若干個類型(能夠是基本類型,也能夠不是基本類型)組合而成,是能夠再分解的。例如,整型數組是由若干個整型數據組成的。

舉個栗子,在C語言中,變量聲明的int a,b,這就意味着,在給變量a和b賦值時不能超過int的取值範圍,變量a和b之間的運算只能是int類型所容許的運算。

抽象是指取出事務具備的廣泛性的本質。它是抽出問題的特徵而忽略本質的細節,是對具體事務的一個歸納。抽象是一種思考問題的方式,它隱藏了繁雜的細節,只保留實現目標所必須的信息。

抽象數據類型(Abstract Data Type, ADT):咱們對與已有的數據類型進行抽象,就有了抽象數據類型,正規的定義:是指一個數學模型及定義在該模型上的一組操做。

抽象數據類型的定義僅取決於它的一組邏輯特性,而與其在計算機內部如何表現和實現無關(這是說這概念與計算機細節無關,不是說是實現該抽象類型時無關),在計算機編程者的角度來看,「抽象」的意義在於數據類型的數學抽象特徵。

算法現廣泛承認的定義是:算法是解決特定問題求解步驟的描述,在計算機表現爲指令的有限序列,而且每條指令表示一個或多個操做。
既然剛剛說抽象這個概念,那就是試着抽象「算法」,它有五個基本特徵:輸入、輸出、有窮性,肯定性和可行性。

算法設計的要求:正確性、可讀性、健壯性、時間效率高和存儲量低

根據上面說的遊戲規則,科學家們在翻譯現實世界的需求和計算機虛擬過程時,就提煉出一些高效的、不斷被驗證過的標準流程,這些流程就是咱們所說的計算機算法。另外模塊化是計算機思惟中很重要的思想,而在軟件中,那些模塊就是一個個算法,所以算法構成了計算機科學的基礎

上面說算法都是抽象的,那咱們怎麼對比算法的好壞呢?
比較容易想到的方法就是對算法進行數據測試,利用計算機的計時功能,來計算不一樣算的效率是高仍是低,這種方法叫:過後統計方法。但缺點有,咱們不妨看這裏兩個場景下,A、B兩種算法的速度:

場景一:使用1萬個數據進行測試,算法A的運行時間是1毫秒,算法B則須要運行10毫秒。
場景二:使用100萬個數據測試,算法A的運行時間是10000毫秒,算法B運行6000毫秒。
這時我問你,哪個算法好?若是單純從第一個場景做判斷,顯然是算法A好,可是若是單純看場景二,彷佛算法B更好一點。按照人的思惟,可能會說,數量小的時候算法A好,數量大的時候算法B好,而後還津津樂道本身懂得辯證法。計算機則不一樣,它比較笨,比較直接,不會辯證法,它要求你最好制定一個明確的標準(也就是上面說的五個基本特徵的肯定性),不要一下子這樣,一下子那樣。因此總結上述缺點有:
1.編寫代碼和準備數據很耗時間。2.依賴具體的計算環境。3。依賴具體的數據量。

對於方法一來判斷算法的好壞好像不太客觀,那麼咱們應該用什麼做爲標準來評判呢?
在計算機科學發展的早起,其實科學家們對這件事情也不很清楚。1965年哈特馬尼斯(Juris Hartmanis)和斯坦恩斯(Richard Stearns)提出了算法複雜度的概念(二人後來所以所以得到了圖靈獎),計算機科學家們開始考慮一個公平的、一致的評判算法好壞的方法。不過最先將複雜度嚴格量化衡量的是著名計算機科學家、算法分析之父高德納(Don Knuth)。今天,全世界計算機領域都以高德納的思想爲準。

另外咱們先看一下函數的漸近增加:給定兩個函數f(n)和g(n),若是存在一個整數N,使得對於全部的n>N,f(n)老是比g(n)大,那麼,咱們說f(n)的增加漸近快於g(n)。

這個定義用來比較是時間複雜度O(n)。時間複雜度,咱們先看這個例子
第一種算法

int i,sum=0,n=100;                                //執行1次
for(i=1;i<=n;i++){                                //執行n+1次
    sum = sum + i;                                //執行了n次
}
prinf("%d",sum);                                //執行1從

則這種算法執行了1+(n+1)+n+1次=2n+3次;
再來看第二種算法

int sum = 0, n = 100;                            //執行1次
sum = (1+n)*n/2;                                //執行1次
printf("%d",sum);                                //執行1次

第二種算法執行了1+1+1=3次。
這兩種算法,都是計算1到100的和,去掉頭尾和循環判斷的開銷,那麼這兩個算法其實就是n次和1次的差距。

測定運行時間最可靠的方法就是計算對運行時間有消耗的基本操做的執行次數。運行時間與這個計數成正比。

咱們在分析一個算法的運行時間時,重要的是把(對運行時間有消耗)基本操做的數量與輸入規模關聯起來,即基本操做的數量表示爲輸入規模的函數。

這裏「函數」是指數學上的函數概念,如y=f(x)
假設算法A的函數是4n+8, 和算法B的函數是2n^2+1,和它們對應的階函數
次數 算法A(4n+8) 算法A'(n) 算法B(2n^2+1) 算法B'(n^2)
n=1 12 1 3 1
n=2 16 2 9 4
n=3 20 3 19 9
n=10 48 10 201 100
n=100 408 100 20001 10000
n=1000 4008 1000 2000001 1000000

根據上面的表格,咱們拿算法A與算法B的比較,發現其比較結果跟 算法A'和算法B'的比較結果是同樣。再根據函數的漸近增加的定義,咱們發現,與最高次項相乘的常數並不重要。

再來看一個例子
次數 算法C(2n^2+3n+1) 算法C'(n^2) 算法D(2n^3+3n+1) 算法D'(n^3)
n=1 6 1 6 1
n=2 15 4 23 8
n=3 28 9 64 27
n=10 231 100 2031 1000
n=100 20231 10000 2000301 1000000

根據上面的表格,咱們拿算法C與算法D的比較,發現其比較結果跟 算法C'和算法D'的比較結果是同樣。再根據函數的漸近增加的定義,咱們發現,最高次項的指數大的,函數隨着n的增加,結果也會變得增加特別快。

最後看一個例子
次數 算法E(2n^2) 算法F(3n+1) 算法G(2n^2+3n+1)
--- ---- ----- -----
n=1 2 4 6
n=2 8 7 15
n=5 50 16 66
n=10 200 31 231
n=100 20 000 301 20 301
n=1,000 2 000 000 3001 2003001
n=10,000 200 000 000 30 001 200030001
n=100,000 20 000 000 000 300 001 20000300001
n=1,000,000 2 000 000 000 000 3 000 001 200 000 3000 001

根據上面的表格,咱們發現當n值愈來愈大,3n+1已經無法和2n^2的結果比較,最終幾乎能夠忽略不計。並且算法E也越來月接近算法G。因此咱們能夠得出一個結論,判斷一個算法的效率時,函數中的常數和其餘次要項經常能夠忽略,而更應該關注主項(最高階項)的階數

綜上關於算法的函數描述,咱們能夠得出:某個算法,隨着n的增大,它會愈來愈優於另外一個算法,或者愈來愈差於另外一算法。

時間複雜度 在進行算法分析,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化狀況並肯定T(n)的數量級。算法的時間複雜度,也就是算法的時間亮度,記做:T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸近時間複雜度,簡稱時間複雜度。其中f(n)是問題規模n的某個函數。

分官方的名稱,O(1)叫常數階,O(n)叫線性階,O(n^2)叫平方階。

推導大O階方法:

  1. 用常數1取代運行花四濺中的全部加法常數。
  2. 在修改後的運行次數函數中,只保留最高階項。
  3. 若是最高階項存在且不是1,則去除與這個項相乘、相加的常數。

常見的時間複雜度
圖:

clipboard.png

最壞狀況與平均狀況

最壞狀況是一種保證,不可能比它更壞了,咱們能夠理解爲數學上的邊界值或極限。在應用中,這是一種最重要的需求,一般,除非特別指定,提到的時間複雜度都是最壞狀況的運行時間。
平均狀況是全部狀況中最有意義的,由於它是指望的運行時間。

算法的空間複雜度經過計算算法所需的存儲空間實現,算法空間複雜度的計算公式記做: S(n)=O(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數。
如:算法執行時所需的輔助空間對於輸入數據量而言是個常數,則稱爲算法爲原地工做,空間複雜度爲O(1)。

線性表

線性表(List):零個或多個數據元素的有限序列。
線性表的抽象數據類型定義:

ADT 線性表(List)
Data
    線性表的數據對象集合爲{a1,a2,...,an},每一個元素的類型均爲DataType.其中出第一個元素a1外,每一個元素有且只有一個直接前驅元素,除了最後一個元素an外,每一個元素有且只有一個直接後繼元素.數據元素之間的關係是一對一的關係.
Operation  
    InitList(*L): 初始化操做,創建一個空的線性表L, 若是變量定義爲L *l, 則調用該方法爲InitList(l);
    ListEmpty(L): 若線性表爲空, 返回true, 不然返回false. 若是變量定義爲L *l, 則調用該方法爲ListEmpty(*l);
    ClearList(*L): 將線性表清空.
    GetElem(L,i,*e): 將線性表L中的第i個位置元素值返回給e.
    LocateELem(L,e): 在線性表L中查找與給定值e相等的元素,若是查找成功,返回該元素在表中序號表示成功; 不然,返回0表示失敗.
    ListInsert(*L,i,e): 在線性表L中的第i個位置插入新元素e.
    ListDelete(*L,i,*e): 刪除線性表L中第i個位置元素,並用e返回其值.
    LIstLength(L): 返回線性表L的元素個數

endADT

以上方法是線性表的基本操做, 是最基本的,若是想要線性表的更爲複雜的操做,徹底能夠用這些基本操做的組合來實現.

線性表的順序存儲結構, 指的是用一段地址連續的存儲單元依次存儲線性表的數據元素.

線性表的鏈式存儲結構,爲了表示每一個數據元素ai與其直接後繼數據元素ai+1之間的邏輯關係,對數據元素ai來講,除了存儲其自己的信息以外,還需存儲一個指示其後繼的信息(即直接後繼的存儲位置).咱們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域. 指針域中存儲的信息稱作指針或鏈.這兩部分信息組成數據元素ai的存儲映像,稱爲結點(Node).

哇,這好多概念,數據域,指針域,指針,鏈,存儲映像,結點
n個結點(ai的存儲映像)鏈結成一個鏈表,即爲線性表(a1, a2,...,an)的鏈式存儲結構,所以此鏈表的每一個結點中只包含一個指針域,因此叫作單鏈表.

鏈表中的第一個結點的存儲位置叫作頭指針.
爲了方便地對鏈表操做,會在單鏈表的第一個結點前附設一個結點,稱爲頭結點,其包含的指針就叫作"頭指針",指向第一個結點的存儲位置.

單鏈表結構與順序存儲結構優缺點:

  1. 存儲分配方式:
    1.1. 順序存儲結構用一段連續的存儲單元一次存儲線性表的元素. 對空間可能會有點浪費,或者須要頻繁擴展.
    1.2. 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素. 需額外空間存儲指針信息.
  2. 時間性能:
    2.1. 查找,順序存儲結構是O(1),單鏈表O(n)
    2.1. 插入和刪除: 順序存儲結構須要平均移動表長一半的元素,時間爲O(n),單鏈表在找出某位置的指針後,插入和刪除時間爲O(1)
  3. 空間性能:
    3.1. 順序存儲結構預分配存儲空間,分大了,浪費,分小了易發生上溢(擴展)
    3.2. 單鏈表不須要分配存儲空間,只要有就能夠分配,元素個數也不受限制.

循環鏈表(circular linked list):將單鏈表中終端結點的指針端由空指針改成指向頭結點,就使整個單鏈表造成一個環,這種頭尾相接的單鏈表稱爲單循環鏈表.

雙向鏈表(double likned list):在單鏈表的每一個節點中,再設置一個指向其前驅結點的指針域.

靜態鏈表:用數組描述的鏈表叫作靜態鏈表,或者叫遊標實現法.

總結

線性表的鏈表存儲結構和順序存儲結構,是後續數據結構(如棧、隊列、樹、圖)的基礎。固然術語和概念很重要,要時刻回顧並理解。
這線性表對應於Java的ArraysList、Vector和LinkedList。據我瞭解,之因此有了ArrayList,還要Vector的存在,除了是歷史庫以外,緣由之一,那估計就是由於C++的vector對象,他們僅僅是數組存儲結構的線性表,而list也僅僅是鏈表存儲結構的。感受是爲了適應C++過來搞Java的人吧。這也引伸出語言可以火的緣由,如不少語言不是憑空誕生的,因此都是參考前人的經驗,而且稍微兼容已有的語言,方便這些人員過分過來,讓這些人員不太抗拒。 另一個就是語言的專一的層面,如Java是面向對象,高級語言,那它所涉及的機器知識、操做系統就較少,如內存、CPU(線程)等,可能體會沒那麼深,相反設計模式、軟件工程上,反而比較吃香。而對應的大數據、人工智能,這相對於機器知識要比較多,如何讓幾臺機器配合工做,如何新增機器時,自動加入控制計算機資源等。這就涉及到操做系統、shell腳步比較多,從而與shell腳步比較接近的Python,和c比較接近的go語言就比較火了。這個我想能夠專開一篇文章來講說。

參考:《大話數據結構》

相關文章
相關標籤/搜索