數據結構與算法-基本概念

什麼是數據結構與算法

從廣義上講數據結構就是指一組數據的存儲結構。算法就是操作數據的一組方法。

從狹義上講,是指某些著名的數據結構和算法,比如隊列、棧、堆、二分查找、動態規劃等。這些都是前人智慧的結晶,我們可以直接拿來用。

數據結構和算法是相輔相成的。數據結構是爲算法服務的,算法要作用在特定的數據結構之上。 因此,我們無法孤立數據結構來講算法,也無法孤立算法來講數據結構。

數據結構和算法是解決 "快"和"省"的問題

算法的執行效率

算法的執行效率,粗略地講,就是算法代碼執行的時間。但是,如何在不運行代碼的情況下,用“肉眼”得到一段代碼的執行時間呢?

複雜度分析

爲什麼需要複雜度分析

一個不用具體的測試數據來測試,就可以粗略地估計算法的執行效率的方法

因爲往往我們需要測試一段代碼或者接口的性能,都需要跑一遍才知道。往往受到環境和數據量影響,比如測試環境4核cpu生產環境8核,數據量測試環境處理幾千條,生產環境處理幾百萬。不同的環境處理結果不同。

時間複雜度

大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。

大 O 這種複雜度表示方法只是表示一種變化趨勢。我們通常會忽略掉公式中的常量、低階、係數,只需要記錄一個最大階的量級就可以了

例子1

如:求 1,2,3...n 的累加和

1  int cal(int n) {
2    int sum = 0;
3    int i = 1;
4    for (; i <= n; ++i) {
5      sum = sum + i;
6    }
7    return sum;
8  }

從 CPU 的角度來看,這段代碼的每一行都執行着類似的操作:讀數據-運算-寫數據。儘管每行代碼對應的 CPU 執行的個數、執行的時間都不一樣,但是,我們這裏只是粗略估計,所以可以假設每行代碼執行的時間都一樣,爲 unit_time。在這個假設的基礎之上,這段代碼的總執行時間是多少呢?

第 2、3 行代碼分別需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,所以需要 2n*unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2)*unit_time。可以看出來,所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比。

例子2

 1  int cal(int n) {
 2    int sum = 0;
 3    int i = 1;
 4    int j = 1;
 5    for (; i <= n; ++i) {
 6      j = 1;
 7      for (; j <= n; ++j) {
 8        sum = sum +  i * j;
 9      }
10    }
11  }

第 2、3、4 行代碼,每行都需要 1 個 unit_time 的執行時間,第 5、6 行代碼循環執行了 n 遍,需要 2n * unit_time 的執行時間,第 7、8 行代碼循環執行了 n2遍,所以需要 2n2 * unit_time 的執行時間。所以,整段代碼總的執行時間 T(n) = (2n2+2n+3)*unit_time。

儘管我們不知道 unit_time 的具體值,但是通過這兩段代碼執行時間的推導過程,我們可以得到一個非常重要的規律,那就是,所有代碼的執行時間 T(n) 與每行代碼的執行次數 f(n) 成正比。

總結

根據例子1和例子2得出總結代碼的執行時間 T(n) 與 f(n) 表達式成正比,總結公式就是:

T(n)=O(f(n))

T(n) 它表示代碼執行的時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和。因爲這是一個公式,所以用 f(n) 來表示。公式中的 O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比

所以,第一個例子中的 T(n) = O(2n+2),第二個例子中的 T(n) = O(2n2+2n+3)。這就是大 O 時間複雜度表示法。大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。

當 n 很大時,你可以把它想象成 10000、100000。而公式中的低階、常量、係數三部分並不左右增長趨勢,所以都可以忽略。我們只需要記錄一個最大量級就可以了,如果用大 O 表示法表示剛講的那兩段代碼的時間複雜度,就可以記爲:T(n) = O(n); T(n) = O(n2)。

時間複雜度分析方法

關注循環次數多的代碼

我們在分析一個算法、一段代碼的時間複雜度的時候,也只關注循環執行次數最多的那一段代碼就可以了。這段核心代碼執行次數的 n 的量級,就是整段要分析代碼的時間複雜度。

1  int cal(int n) {
2    int sum = 0;
3    int i = 1;
4    for (; i <= n; ++i) {
5      sum = sum + i;
6    }
7    return sum;
8  }

其中第 2、3 行代碼都是常量級的執行時間,與 n 的大小無關,所以對於複雜度並沒有影響。循環執行次數最多的是第 4、5 行代碼,所以這塊代碼要重點分析。前面我們也講過,這兩行代碼被執行了 n 次,所以總的時間複雜度就是 O(n)。

加法法則

總複雜度等於量級最大的那段代碼的複雜度

 1 int cal(int n) {
 2    int sum_1 = 0;
 3    int p = 1;
 4    for (; p < 100; ++p) {
 5      sum_1 = sum_1 + p;
 6    }
 7 
 8    int sum_2 = 0;
 9    int q = 1;
10    for (; q < n; ++q) {
11      sum_2 = sum_2 + q;
12    }
13  
14    int sum_3 = 0;
15    int i = 1;
16    int j = 1;
17    for (; i <= n; ++i) {
18      j = 1; 
19      for (; j <= n; ++j) {
20        sum_3 = sum_3 +  i * j;
21      }
22    }
23  
24    return sum_1 + sum_2 + sum_3;
25  }

這個代碼分爲三部分,分別是求 sum_1、sum_2、sum_3。我們可以分別分析每一部分的時間複雜度,然後把它們放到一塊兒,再取一個量級最大的作爲整段代碼的複雜度。

第一段的時間複雜度是多少呢?這段代碼循環執行了 100 次,所以是一個常量的執行時間,跟 n 的規模無關。這裏要再強調一下,即便這段代碼循環 10000 次、100000 次,只要是一個已知的數,跟 n 無關,照樣也是常量級的執行時間。

當 n 無限大的時候,就可以忽略。儘管對代碼的執行時間會有很大影響,但是回到時間複雜度的概念來說,它表示的是一個算法執行效率與數據規模增長的變化趨勢,所以不管常量的執行時間多大,我們都可以忽略掉。因爲它本身對增長趨勢並沒有影響。

那第二段代碼和第三段代碼的時間複雜度是多少呢?答案是 O(n) 和 O(n2),

綜合這三段代碼的時間複雜度,我們取其中最大的量級。所以,整個方法代碼的時間複雜度就爲 O(n2)。

也就是說:總的時間複雜度就等於量級最大的那段代碼的時間複雜度。那我們將這個規律抽象成公式就是:如果 T1(n)=O(f(n)),T2(n)=O(g(n));那麼 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).

乘法法則

嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

 1 int cal(int n) {
 2    int ret = 0; 
 3    int i = 1;
 4    for (; i < n; ++i) {
 5      ret = ret + f(i);
 6    } 
 7  } 
 8  
 9  int f(int n) {
10   int sum = 0;
11   int i = 1;
12   for (; i < n; ++i) {
13     sum = sum + i;
14   } 
15   return sum;
16  }

我們單獨看 cal() 函數。假設 f() 只是一個普通的操作,那第 4~6 行的時間複雜度就是,T1(n) = O(n)。但 f() 函數本身不是一個簡單的操作,它的時間複雜度是 T2(n) = O(n),所以,整個 cal() 函數的時間複雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章