文章目錄
Day1 緒論
1. 數據結構
1.1 數據結構是什麼
什麼是數據結構?
- 程序設計 = 數據結構 + 算法
- 數據結構: 數據元素相互之間存在的一種或多種特定關係的集合
傳統上:數據結構 = 邏輯結構 + 物理結構
- 邏輯結構:數據對象中數據元素中的相互關係
- 物理結構:數據的邏輯結構在計算機中的存儲形式。
1.2 邏輯結構
-
集合結構: 除了屬於一個集合外,沒有其它任何關係
-
線性結構:
-
樹型結構:
-
圖形關係:
1.3 物理結構
物理結構,研究的是,如何把數據元素存儲到計算機的存儲器中。
- 存儲器主要是針對內存而言,外部存儲器常用文件結構來描述。
- 數據元素存儲形式有兩種:順序存儲 和 鏈式存儲
-
順序存儲結構: 把元素存儲在地址連續的存儲單元裏,數據間的邏輯關係和物理單元是一致的。(下圖是物理結構,表現爲緊挨着)
-
鏈式存儲結構: 數據元素存放在任意的存儲單元裏,然後在這一個數據元素上放一個指針,來記錄下一個數據元素的地址。(因爲存放了指針,所以更費空間)
2. 算法
算法就是解決問題的技巧和方式。
- 如何計算 1 + 2 + … + 99 + 100 的值?有的人可能會一個個加過去,有的人會一個個加過去,有的人會使用等差數列(這個算法是高斯在小學發明的)。
- 一個問題可以由多個算法解決,一個算法不可能有通解所有問題的能力。
2.1 算法的五個基本特徵
- 輸入
- 輸出
- 有窮性
- 確定性
算法的每一條步驟都有確定的含義,不會出現二義性,不會有歧義。
算法在一定條件下,只有一條執行路徑。相同的輸入只能有一個輸出的結果。 - 可行性
每一步都可以在當前環境下執行有限次數完成(算法可以註明自己需要的環境)
2.2 算法設計的要求
1. 正確性 (大致分爲四層次)
- 算法程序沒有語法錯誤
- 算法程序對於合法輸入能夠產生滿足要求的輸出
- 算法程序對於非法輸入能夠產生滿足要求的說明
- 算法程序對於故意刁難的測試輸入都有滿足要求的輸出結果
2. 可讀性
- 算法設計的另一目的是爲了便於閱讀理解和交流
- 寫代碼的目的,一方面是爲了讓自己理解執行。另一方面是爲了便於自己或他人閱讀修改。
3. 時間效率高 & 存儲量低
Day2~3 時間複雜度&空間複雜度
1. 學習前準備
如何計算算法效率呢?
1.1 兩種計算方法
1.1.1 事後統計方法
需要事先編制好測試程序。利用計算機計時器對不同算法編制的運行時間做比較,從而確定算法效率的高低。
- 缺點:
- 編制測試程序需要花費時間和精力。
- 不同測試環境的差別還非常之大。
1.1.2 事前分析估算方法
在計算機重新編寫前,依據統計方法對算法進行估算。
- 影響因素:
- 算法的策略,方案
- 編譯參數的代碼質量
- 問題的輸入規模
- 機器執行指令的速度
由此可見: 除了軟硬件之外,就是算法的好壞和問題的輸入規模。
1.2 爲什麼只用高階階數
1.2.1 示例
計算1~100相加
- 算法1:
for(int i = 1, n = 100, sum = 0; i <= n; i++) { // 執行 1+n+1 次
sum = sum + i; // 執行 n 次
}
- 初始化方法執行 1 次
- 條件判斷執行 n+1 次(1次是判斷不成功時跳出循環)
- 函數體執行n次
總共 2n + 2 次
- 算法2:
int sum = 0, n = 100; // 執行1次
sum = (1 + n) * n / 2; // 執行1次
總共 2 次
1.2.2 說明
- 不關心編寫程序的語言是什麼,也不關心程序跑在什麼樣的計算機上,只關心實現的算法。
- 最重要的是,把一系列的步驟抽象出來。
- 把基本操作的數量和輸入模式關聯起來。
所以上面的算法1和算法2的關係,是n和1的關係。
1.2.3 函數的漸進增長
給定兩個函數f(n) 和 g(n),如果存在一個整數N,使得對於所有的n > N, f(n) 總是比g(n)大,那麼,我們說f(n)的漸進增長快於g(n)。
- f(n) = 3n + 1
- g(n) = 2n + 3
- N = 2
- 存在n > N時,f(n) > g(n),所以f(n)的漸進增長快於g(n)
可以使用相關工具(如Excel或matlab),進行繪圖。會發現,當n足夠大時,似乎只與最高階階數相關,其它因素可以忽略不計。
判斷一個算法效率時: 函數中的常數和次要項常常快於忽略,而更應該關注主項(最高項)的階數。
2. 時間複雜度
2.1 大O表示法
在進行算法分析時,語句總的執行次數 T(n)是關於問題規模 n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級 。算法的時間複雜度,也就是時間量度,記做:它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率 相同,稱作算法的漸進時間複雜度,簡稱爲時間複雜度 。其中f(n)是問題規模n的某個函數。(意思是,看重潛力,而不是當前。)
- 用來體現算法時間複雜度的記法,被稱爲大O表示法 。
- 隨着輸入規模n的增大,T(n)增長最慢的算法,一般爲最優算法。
- 1.2.2中,三條曲線算法的時間複雜度分別爲,,
2.2 萬能公式
關鍵是輸入n與輸出T(n)的關係
- 聚焦最高次項,其它全扔
- 最高次項常數變爲1
- 常數階:最高階爲常數,
- 線性階:最高階爲一次,。一般涉及非嵌套循環。
- 平方階:最高階爲兩次,。一般涉及一個嵌套的循環。
- 對數階:,這個寫法的時間複雜度爲
注: 推導大O並不難,關鍵是對數列的一些相關運算,更多地考慮數學知識與能力。
3. 函數調用的時間複雜度分析
3.1 Demo1
int i, j; // 1
for(i = 0; i < n; i++) { // n + 1
fuction(i); // 1
}
product static void function(int count) {
System.out.println(count); // 1
}
1 + n + 1 + n * 1 ⇒ 2n +2
所以,時間複雜度是:
3.2 Demo2
int i, j; // 1
for(i = 0; i < n; i++) { // n + 1
fuction(i); // n
}
product static void function(int count) {
int j; // 1
for(j = count; j < n; j++) { // O(n)
System.out.println(j);
}
}
裏面那個循環,一眼瞄上去,執行次數就是一個等差數列。不管它是從n還是n+1還是n-1開始,也不管它是1或2或0結束。總歸是等差數列,最高階是 ,也就是。(作爲程序員,需要的是抽象的概括能力。比如這裏,一眼瞄過去知道是等差,立馬就是一個甩過去。)
1 + n + 1 + n * O(n) ⇒
3.3 Demo3
n++; // 1
function(n); // n * O(n)
for(i = 0; i < n; i++) { // n+2
function(i); // O(n) * (n + 1)
}
for(i = 0; i < n; i++) { // O(n^2)
for(j = i; j < n; j++) {
System.out.println(j)
}
}
product static void function(int count) {
int j; // 1
for(j = count; j < n; j++) { // O(n)
System.out.println(j);
}
}
⇒
4. 常見時間複雜度
時間複雜度(升序) | 術語 | 舉例 |
---|---|---|
常數階 | 1 | |
對數階 | ||
線性階 | 2n + 1 | |
nlogn階 | ||
平方階 | ||
立方階 | ||
指數階 | ||
階乘 | n! | |
- | - |
由與太大了,我們沒有討論它們的意義。我們只探究前5個。
5. 最壞情況&平均情況
- 最壞情況: 窮盡到最後一次纔得到自己想要的結果
- 平均情況: 期望的結果
- 最壞運行時間: 是一種保證,在應用中,這是一種最重要的需求,如果沒特別指定,我們提到的運行時間都是最壞情況的運行時間。
- 平均運行時間: 期望的運行時間
6. 空間複雜度
6.1 什麼是空間複雜度
寫代碼時,可以用空間 換時間
- 判斷一個年份是否是閏年時,可以採取兩種方式:
- 寫一個判斷的函數,經過計算,返回結果(省空間,耗時間)
- 寫一個大型的數組,輸入年份,直接返回結果(省時間,耗空間)
6.2
空間複雜度計算公式:
n爲問題規模
f(n)爲語句關於n所佔存儲空間的函數
- 我們用時間複雜度 來指運行時間的需求,空間複雜度 來指空間需求。(一般指的複雜度是指時間複雜度)