作為一個程式設計師,每天都在和不同的資料打交道。那麼你真的瞭解資料麼?為什麼我們總在說資料結構呢?資料結構是不是就是我們的基本資料型別、字串、陣列呢?下面我們就來聊聊資料結構的那些事。
概念
資料結構是一門研究非數值計算的程式設計問題中的操作物件,以及它們之間的關係和操作等相關問題的學科。
《大話資料結構》
這本書中對資料結構做了如此定義。其實早在1968年,美國一名叫Donald E. Knuth
教授寫了一本書《計算機程式設計藝術》
,在其中第一卷《基本演算法》
就比較系統的闡述了資料的邏輯結構和儲存結構及其操作,開創了資料結構的體系。而瑞士電腦科學家Niklaus Wirth
在1976年也寫了一本書,名為《演算法+資料結構=程式設計》
。人們早早的就意識到資料結構之於計算機程式設計的重要性。
我們可以通俗的認為,資料結構就是計算機儲存、組織資料的方式,是相互之間存在一種或多種特定關係的資料元素的集合。
為了弄清楚資料結構,我們必須先來認識資料結構的一些基本概念。
資料
資料是描述客觀事物的符號,是計算機中可以操作的物件,是能被計算機識別,並輸入給計算機處理的符號集合。資料不僅僅包括整型、實型等數值型別,還包括字元及聲音、影像、視訊等非數值型別。
資料有2個特點:
- 可以輸入到計算機中
- 能唄計算機程式處理
對於整型,浮點型資料,可以直接進行數值計算。對於字串型別的資料,我們就需要進行非數值的處理。而對於聲音、影像、視訊這樣的資料,我們需要通過編碼的手段將其變成二進位制資料進行處理。
資料元素
資料元素: 是組成資料的,且有一定意義的基本單位,在計算機中通常作為整體處理。也被稱為記錄。比如在人類中,人就是資料元素了。
資料項
資料項: 一個資料元素可以由若干資料項組成。比如人這樣的資料元素,可以有眼睛、耳朵、鼻子、嘴巴、手臂這些資料項。當然也可以拆解為別的資料項:姓名、年齡、性別等。資料項是資料不可分割的最小單位。
資料物件
資料物件: 是性質相同的資料元素的集合,是資料的子集。性質相同是指資料元素具有相同數量和型別的數項。
總結如圖:
邏輯結構與物理結構
按照視點的不同,我們把資料結構分為邏輯結構和物理結構。
邏輯結構
邏輯結構是指資料物件中資料元素之間的相互關係。邏輯結構可以分為以下4種:
1. 集合結構
集合結構中的資料元素除了同屬於一個集合外,它們之間沒有其他關係,它們之間唯一的相同點就是"同屬於一個集合"。
2. 線性結構
線性結構中的資料元素之間的關係是一對一的。常用的線性結構有:線性表、棧、佇列、雙佇列、陣列、串。
3. 樹形結構
樹形結構中的資料元素是一對多的層級關係。常見的樹形結構: 二叉樹、B樹、哈夫曼樹、紅黑樹等。
4. 圖形結構
圖形結構中的資料元素之間的關係是多對多的。常見的圖形結構:鄰近矩陣、鄰接表。
物理結構
物理結構也叫儲存結構,指的是資料的邏輯結構在計算機的儲存形式。資料元素的儲存結構形式有2種: 順序儲存結構和鏈式儲存結構。
1. 順序儲存結構
順序儲存結構是指把資料元素存放在地址連續的儲存單元裡,其資料間的邏輯關係和物理關係是一致的。比如一個陣列,它的元素是一個接一個,在記憶體空間中的地址也是連續的,我們可以通過陣列的下標訪問每一個元素,也可以使用地址遞增的方式訪問。
2. 鏈式儲存結構
鏈式儲存結構是把資料元素放在任意的儲存單元裡,這組儲存單元可以是連續的,也可以是不連續的。資料元素的儲存關係並不能反映邏輯關係,因此需要用一個指標存放資料元素的地址,這樣通過地址就可以找到相關聯資料元素的位置。
演算法
說到資料結構,就不得不提起演算法,我們設計各種各樣資料結構的最終目的也是進行運算。那麼什麼是演算法呢?現如今普通認可的演算法的定義如下:
演算法是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,並且每條指令表示一個或多個操作。
演算法的特性
演算法具有5個基本特性: 輸入、輸出、有窮性、確定性和可行性。
-
- 輸入輸出
演算法具有零個或者多個輸入,至少有一個或多個輸出。
-
- 有窮性
有窮性指的是演算法在執行有限的步驟之後,自動結束而不會出現無限迴圈,且每一個步驟都在可接受的時間內完成。
-
- 確定性
確定性是指演算法的每一個步驟都具有確定的含義,不能出現二義性。 演算法在一定條件下,只有一條執行路徑,相同的輸入只能有唯一的輸出結果。
-
- 可行性
可行性: 演算法的每一步都必須是可行的,即每一步都能通過執行有限次數完成。
演算法的設計要求
-
- 正確性
演算法的正確性是指演算法至少應該具有輸入,輸出和加工處理無歧義性,能正確反映問題的需求、能夠得到問題的正確答案。正確性分為4個層次: - 演算法程式沒有語法錯誤; - 演算法程式對於合法的輸入資料能夠產生滿足要求的輸出結果; - 演算法程式對於非法的輸入資料能夠得出滿足規格說明的結果; - 演算法程式對於精心選擇的,甚至刁鑽的測試資料都有滿足要求的輸出結果;
-
- 可讀性
可讀性: 演算法設計的另一個目的是為了便於閱讀,理解和交流。可讀性高有助於人們理解演算法,晦澀難懂的演算法往往隱含錯誤,不容易發現,並且難於除錯和修改。
可讀性是演算法好壞的很重要的標誌!
-
- 健壯性
一個好的演算法還應該能對輸入資料的不合法的情況做出合適的處理,考慮邊界性,也是在寫程式碼經常要做的一個處理。當輸入資料不合法時,演算法也能做出相關處理,而不是產生異常和莫名其妙的結果。
-
- 時間效率高和儲存量低
用最少的儲存空間和最少的時間,辦成同樣的事,就是好演算法!
演算法的時間複雜度
使用高階程式語言編寫的程式在計算機上執行時所消耗的時間取決於下列的因素:
- 演算法採用的策略、方法
- 編譯產生的程式碼質量
- 問題的輸入規模
- 機器執行指令的速度
演算法的時間複雜度的定義如下:
在進行演算法分析時,語句的總執行次數T(n)
是關於問題規模n
的函式,進而分析T(n)
隨著n
變化情況並確定T(n)
的數量級。演算法的時間複雜度,也就是演算法的時間量度,即為T(n) = O(f(n))
。它表示隨問題規模n
的增大,演算法執行時間的增長率和f(n)
的增長率相同,稱作演算法的漸近時間複雜度,簡稱為時間複雜度。其中f(n)
是問題規模n
的某個函式。
大寫O( )
來體現演算法時間複雜度的記法,我們稱之為大O
記法。
推導大O
階的方法:
- 用常數1取代執行時間中所有加法常數
- 在修改後的執行次數函式中,只保留最高階項
- 如果在最高階項存在且不是1,則去除與這個項相乘的常數
常數階
int sum = 0, n = 100;
sum = (1 + n) * n / 2;
printf("%d", sum);
複製程式碼
這個演算法的每行程式碼都會執行一次,執行次數函式是f(n) = 3
,依據推導大O
階的方法,第一步就是將常數替換為1,由於沒有最高階項,所以該演算法的時間複雜度為O(1)
。
需要注意的是,常數階的演算法不管常數是多少,我們都記做O(1)
,並沒有O(2)、O(3)
之類的複雜度。
線性階
for (int i = 0; i < n; i++) {
// 時間複雜度為O(1)的操作
}
複製程式碼
分析演算法的複雜度,就是要分析迴圈結構的執行情況。上述程式碼的時間複雜度為O(n)
,是因為迴圈體中的程式碼要執行n
次。
對數階
int i = 1;
while (i < n) {
i = i * 2;
}
複製程式碼
在迴圈體內,i
每次都乘以2倍,即為2^x = n
,可以得出次數x
為以2為低n
的對數x = log2n
。根據推導大O
階的方法,去除最高階項的常數,即為O(logn)
。
平方階
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 時間複雜度為O(1)的操作
}
}
複製程式碼
迴圈巢狀n*n
,時間複雜度為O(n^2)
。
常見的時間複雜度所耗時從小到大依次是:
對於演算法的分析,一種方法就是計算所有情況的平均值,這種時間複雜度的計算的方法稱為平均時間複雜度。另一種方法是計算最壞的情況下時間複雜度, 這種方法稱為最壞時間複雜度。一般沒有特殊情況下,都是指最壞時間複雜度。
演算法空間複雜度
演算法的空間複雜度通過計算演算法所需的儲存空間實現,演算法空間複雜度的計算公式記做: S(n) = n(f(n))
,其中n
為問題的規模,f(n)
為語句關於n
所佔儲存空間的函式。
一般情況下,一個程式在機器上執行時,除了需要寄存本身所用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的輔助儲存空間。其中,對於輸入資料所佔的具體儲存量取決於問題本身,與演算法無關。 這樣我們只需要分析該演算法在實現時所需要的輔助空間就可以了。
下面演算法僅僅通過開闢了一個臨時變數temp
,與問題規模n
大小無關,所以其空間複雜度為S(1)
。
int temp;
for(int i = 0; i < n/2 ; i++){
temp = a[i];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
複製程式碼
而這個演算法中,我們需要開闢一個大小為n
的輔助變數b
,所以空間複雜度為S(n)
。
int b[10] = {0};
for(int i = 0; i < n;i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[i];
}
複製程式碼
非廣告提示,文中大部分概念性東西參考自《大話資料結構》
這本書,感興趣的同學可以買來一讀。
參考文獻:
- 《大話資料結構》