數據結構和算法
如果沒有接觸過數據結構這門課程,或者說只是單單聽過這個名詞。那麼在含義方面,數據結構對於我們來說是非常陌生的。在瞭解一門課程之前,我們總是要知道這門課程要學習什麼。
一、什麼是數據結構?
在瞭解數據結構之前,我們需要知道什麼是數據。對於人類來說,一切可以讓我們獲取信息的東西都是數據。我們可以通過一個動物的叫聲判斷是什麼動物,我們可以通過一本書瞭解到作者想要表達的東西,我們也可以通過一張圖片瞭解到一個人的模樣…
我們現在研究數據結構是建立在計算機的基礎之前,所以對於計算機來講(拿C語言打比方),所以的基本數據類型的變量、派生類型變量、結構類型變量等…我們都可以稱爲數據。
數據結構是研究數據集合中各個元素之間關係的一門學科。我們必須注意兩個地方,第一個是集合,我們所研究的數據一般都是以集合的形式,這個集合可以爲空,也可以只有一個元素。第二個就是關係,我們研究的數據之間通常會有一定的關係,有些是雜亂無章的集合關係,有些是依次排列的線性關係,也有交錯混雜的樹形關係…但是他們必須要有一定的關係。
注意:在談到集合的時候,我們說可以有一個元素,但是不能是只能有一個元素。像我們使用到的int類型,我們不能將它稱爲數據結構。
二、 一些概念和術語
數據是對客觀事物的符號表示,在計算機科學中是指所有能輸入到計算機中並被計算機程序處理的符號的總稱。
數據元素是數據的基本單位,在計算機程序中通常作爲一個整體進行考慮和處理。一個數據元素可由若干個數據項組成,數據項是數據不可分割的最小單位。
數據對象是性質相同的數據元素的集合。
數據結構是互相之間存在一種或多種特定關係的數據元素的集合。
數據通常有四類結構:
- 集合:結構中數據元素同屬一個集合;
- 線性結構:結構中的數據元素之間存在一對一的關係;
- 樹形結構:結構中的數據元素之間存在一對多的關係;
- 網狀結構或圖狀結構:結構中的數據元素之間存在多對多的關係;
數據項作爲不可分割的最小單位,組成了一個數據元素。而多個數據元素組成的有一定關係的集合,又被我們稱之爲數據結構。按照集合中不同的關係,我們通常把數據的結構分爲四類:集合、線性結構、樹形結構、網狀或圖狀結構。而上述的一切,都被我們稱作數據。不過,數據遠不止這些。
注意:數據結構我們可以簡單理解爲數據關係,後面很多時候我們都可以直接用關係替代結構這個詞。
各個結構的圖如上。
三、邏輯結構和物理結構(存儲結構)
在上述的數據結構中,四類結構都是脫離計算機獨立存在的。即在我們現實生活中也通用,像我們平時用的名單、購物清單、賬單等…這個層面上的關係(結構),我們稱爲邏輯結構。簡單來說,就是數據之間對應的關係(一對一、一對多…)。而我們用計算機,通過實際的方式(主要是兩種)實現這種關係。這種通過具體方式,在計算機中實現的數據結構就稱爲物理結構,又稱爲存儲結構。
而根據具體實現方式不同,又分爲順序存儲結構和鏈式存儲結構。順序存儲結構使用數組存儲元素,所以數據元素在連續的內存地址上。而鏈式存儲結構通過一個個節點實現,在節點中存儲數據和下一個元素的指針。存儲數據的地方被稱爲數據域,存儲地址的地方被稱爲指針域。這樣通過指針域來實現數據之間的連接,所以鏈式存儲數據元素通常不在連續的內存地址上。
四、算法和算法效率的度量
4.1、算法
算法是一組指令,其目的就是針對確定的問題,用確定的指令解決該問題。算法本身是個非常容易理解的概念,而難的是具體某種算法。算法有以下幾個特性:
- 有窮性:即步驟是有窮的。
- 確定性:即每一條指令都有確定的含義,不會產生歧義。
- 可行性:這個我也就不解釋了。
- 有輸入:這裏的有輸入並不一定需要輸入,可以有0個輸入,也可以有多個輸入。
- 有輸出:一個算法需要有至少一個輸出。雖然輸出不會影響算法本身的正確性,但是對人來說輸出是最直觀的東西。
4.2、算法的設計
不同情況對算法的要求是不同的,通常情況下我們算法設計分爲以下幾個層次:
- 正確:這是對算法最基本的要求,即輸入合理的數據,可以得到預期的輸出
- 可讀:即在算法正確的基礎上,注意編碼的規範,讓程序易於解讀
- 健壯:有些算法在運行時,用戶並不會輸入程序員設想的數據。這個時候就需要程序員對這些極端數據,採取相應的措施。
- 效率和存儲:效率和存儲是算法中一個非常值得重視的問題,但是同時又是一大難題。有時候需要捨棄效率節約存儲,有時候需要捨棄存儲提升速度。這也就是我們爲什麼要學習數據結構的原因。學習數據結構後,我們可以針對不同的情況,採取相應的數據結構,從而達到效率的最大化。
4.3、算法效率的度量
算法的效率度量有兩個重要的數據,一個是運行時間的度量,一個是佔用存儲的度量,一個好的算法一定是要權衡兩者哪個更重要,將效率最大化。
(1)事後分析法
事後分析法我們可以從字面意思來了解,就是我們運行完程序,然後看看這個程序到底花了多少時間。我們可以在運行開始獲取一次時間,運行結束後再獲取一次時間,再把時間相減就可以了。我簡單演示一下,我們先編寫一個獲取時間的函數:
/**
* 獲取當前時間的秒數
*/
int getNowSec(){
time_t t;
struct tm *p;
time(&t);
p = gmtime(&t);
int sec = p->tm_sec;
return sec;
}
這個該死的寫法我也不解釋了,我們只需要知道可以獲取當前時間的秒數。然後我們測試一下:
int main(){
//獲取開始時間
int t1 = getNowTime();
//老版本C語言不支持這種寫法
for(int i = 0; i < 100000; i++){
printf("this is a test sentence\n");
}
//獲取結束時間
int t2 = getNowTime();
printf("it's spend %d\n", t2-t1);
return 0;
}
我們這裏輸出了十萬條語句,在我電腦上運行時間是9秒。
注意:如果是參加考試,不要將循環寫成**for(int i = 0)**這種形式。
我們很容易發現,這種方法和在不同設備、不同環境中測試結果是不一樣的,這種不確定的度量肯定是不可取的。
(2)漸進時間複雜度
漸進時間複雜度通常稱爲時間複雜度,使用這個方式度量時,我們通常將一條指令執行時間看成1,然後我們可以在執行算法之前對算法的執行時間有一個大概的估計。我們通常使用大O表示法來表示時間複雜度。大O表示法是一種通過問題規模表示一個算法的方式。
我們先看一個簡單的算法:
void dis(int n){
for(int i = 0; i < n; i++){
printf("test\n");
}
}
上述代碼我們執行了n條輸出語句,該算法的問題規模就是n,在實際執行時,這個算法一次循環應該會執行4條指令(for循環中三條),那他執行時間應該是4n,但是我們使用大O表示法通常不考慮係數,表示如下:
T=O(n)
我們再來看一個算法:
void dis(int n){
//part1
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
printf("test\n");
}
}
//part2
for(int i = 0; i < n; i++){
printf("test\n");
}
}
上面算法中我們有兩個部分,第一部分是一個雙重循環,該部分執行時間應該是n2(不考慮係數),第二部分是一個簡單的循環,執行時間爲n,按理我們的時間複雜度應該是:
T=O(n2 + n)
但是我們用大O表示法時,只考慮最高次項,那麼實際上應該寫成如下:
T=O(n2)
另外,大O表示法還有一個規則,即只考慮最壞的情況,我們看如下算法:
void dis(int n){
for(int i = 0; i < n; i++){
for(int j = n; j > 0; j--){
printf("test\n");
}
}
}
在這個算法中,內層循環是逐漸遞減的其算法的執行時間如下:
n*(n-1)*(n-2)*.....1
但是我們寫它的時間複雜度還是表示爲:
T=O(n2)
除了我們上面說的n、n2外,還有一些其它的時間複雜度,常數、指數、對數等…
我們總結大O表示法有如下幾個規則:
- 不考慮係數
- 只考慮最高次項
- 只考慮最壞的情況
另外,當一個算法執行時間是一個常數時,那麼他的時間複雜度爲O(1)。