數據結構與算法之美 - 王爭


1
基礎知識就像是一座大樓的地基,它決定了我們的技術高度。而要想快速做出點事情,前提條件一定是基礎能力過硬,“內功”要到位。(內功:操作系統、計算機網絡、編譯原理)

學習數據結構和算法,並不是為了死記硬背幾個知識點。我們的目的是建立時間復雜度、空間復雜度意識,寫出高質量的代碼,能夠設計基礎架構,提升編程技能,訓練邏輯思維,積攢人生經驗,以此獲得工作回報,實現你的價值,完善你的人生。

掌握了數據結構與算法,你看待問題的深度,解決問題的角度就會完全不一樣。

2
從廣義上講,數據結構就是一組數據的存儲結構。算法就是操作數據的一組方法
圖書館儲藏書籍你肯定見過吧?為了方便查找,圖書管理員一般會將書籍分門別類進行“存儲”。按照一定規律編號,就是書籍這種“數據”的存儲結構

從狹義上講,是指某些著名的數據結構和算法,比如隊列、棧、
堆、二分查找、動態規划等。我們可以直接拿來用,可以高效地幫助我們解決很多實際的開發問題

數據結構是為算法服務的,算法要作用在特定的數據結構之上。

數據結構是靜態的,它只是組織數據的一種方式。如果不在它的基礎上操作、構建算法,孤立存在的數據結構就是沒用的

要學習數據結構與算法,首先要掌握一個數據結構與算法中最重要的概念——復雜度分析

數據結構和算法解決的是如何更省、更快地存儲和處理數據的問題,因此,我們就需要一個考量效率和資源消耗的方法,這就是復雜度分析方法

20個最常用的、 最基礎數據結構與算法,不管是應付面試還是工作需要,只要集中精力逐一攻克這20個知識點就足夠了
這里面有:
10個數據結構:數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳表、圖、Trie 樹;
10個算法:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規划、字符串匹配算法
掌握了這些基礎的數據結構和算法,再學更加復雜的數據結構和算法,就會非常容易、非常快

在學習數據結構和算法的過程中,你也要注意,不要只是死記硬背,不要為了學習而學習, 而是要學習它的“來歷”“自身的特點”“適合解決的問題”以及“實際的應用場景”

學習數據結構和算法的過程,是非常好的思維訓練的過程,所以,千萬不要被動地記憶,要多辯證地思考,多問為什么。如果你一直這么堅持做,你會發現,等你學完之后,寫代碼的時候就會不由自主地考慮到很多性能方面的事情,時間復雜度、空間復雜度非常高的垃圾代碼出現的次數就會越來越少。你的編程內功就真正得到了修煉。

學習技巧

1.每周花 1~2 個小時的時間,集中把這周的三節內容 涉及的數據結構和算法,全都自己寫出來,用代碼實現一遍
2.學習的目的還是掌握,然后應用
3.找到幾個人一起學習,一塊兒討論切磋,有問題及時尋求老師答疑
4.我們在枯燥的學習過程中,也可以給自己設立一個切實可行的目標
5.知識需要沉淀,不要想試圖一下子掌握所有,學習知識的過程 是反復迭代、不斷沉淀的過程

3.如何分析、統計算法的執行效率和資源消耗?

執行效率是算法一個非常重要的考量指標

我們需要一個不用具體的測試數據來測試,就可以粗略地估計算法的執行效率的方 法。這就是我們今天要講的時間、空間復雜度分析方法

大 O 復雜度表示法

算法的執行效率,粗略地講,就是算法代碼執行的時間。

這里有段非常簡單的代碼,求 1,2,3...n 的累加和。現在,我就帶你一塊來估算一下這段代碼的執行時間,假設每行代碼執行的時間都一樣,為 unit_time

int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; ++i) {
         sum = sum + i;
    }
    return sum;
}

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

按照這個分析思路,我們再來看這段代碼。

int cal(int n) {
  int sum = 0;
  int i = 1;
  int j = 1;
  for (; i <= n; ++i) {
    j = 1;
    for (; j <= n; ++j) {
      sum = sum +  i * j;
    }
  }
}

第 2、3、4 行代碼,每行都需要 1 個 unit_time 的執行時間,第 5、6 行代碼循環執行了 n 遍,需要 2n * unit_time 的執行時間,第 7、8 行代碼循環執行了\(n^2\)遍,所以需要 \(2n^2\)

  • unit_time 的執行時間。所以,整段代碼總的執行時間 T(n) = (\(2n^2\)+2n+3)*unit_time

總結

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

T(n) = O(2n+2),T(n) = O(\(2n^2\)+2n+3) 這就是大O時間復雜度表示法。大O時間復雜度表示代碼執行時間隨數據規模增長的變化趨勢,也叫作漸進時間復雜度,簡稱時間復雜度
當 n 很大時,公式中的低階、常量、系數三部分都可以忽略。我們只需要記錄一個最大量級就可以了,如果用大 O 表示法表示剛講的那兩段代碼的時間復雜度,就可以記為:T(n) = O(n); T(n) = O(\(n^2\))。

時間復雜度分析

1. 只關注循環執行次數最多的一段代碼

大 O 這種復雜度表示方法只是表示一種變化趨勢,我們在分析一個算法、一段代碼的時間復雜度的時候,也只關注循環執行次數最多的那一段代碼就可以了。這段核心代碼執行次數的 n 的量級,就是整段要分析代碼的時間復雜度。
舉一個例子:

int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; ++i) {
         sum = sum + i;
    }
    return sum;
}

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

2. 加法法則:總復雜度等於量級最大的那段代碼的復雜度

舉例:

這個代碼分為三部分,分別是求 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))).

3. 乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積

如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)*g(n)).

我們單獨看 cal() 函數。假設 f() 只是一個普通的操作,那第 4~6 行的時間復雜度就是, T1(n) = O(n)。但 f() 函數本身不是一個簡單的操作,它的時間復雜度是 T2(n) = O(n),所 以,整個 cal() 函數的時間復雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(\(n^2\))。
我剛剛講了三種復雜度的分析技巧。不過,你並不用刻意去記憶。實際上,復雜度分析這個
東西關鍵在於“熟練”。你只要多看案例,多分析,就能做到“無招勝有招”。

幾種常見時間復雜度實例分析

雖然代碼千差萬別,但是常見的復雜度量級並不多。我稍微總結了一下,這些復雜度量級幾乎涵蓋了你今后可以接觸的所有代碼的復雜度量級。

對於剛羅列的復雜度量級,我們可以粗略地分為兩類,多項式量級和非多項式量級。其中, 非多項式量級只有兩個:O(2n) 和 O(n!),非多項式時間復雜度的算法其實是非常低效的算法。

我們主要來看幾種常見的多項式時間復雜度

1. O(1)

O(1) 只是常量級時間復雜度的一種表示方法,並不是指只執行 了一行代碼。比如這段代碼,即便有 3 行,它的時間復雜度也是 O(1),而不是 O(3)。

一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)

2. O(logn)、O(nlogn)

對數階時間復雜度非常常見,同時也是最難分析的一種時間復雜度。我通過一個例子來說明一下。

\(2^n = x\) 則 x = \(log2_n\) 所以,這段代碼的時間復雜度就 是 O(log2n)。


這段代碼的時間復雜度為 O(log_3n)
實際上,不管是以 2 為底、以 3 為底,還是以 10 為底,我們可以把所有對數階的時間復 雜度都記為 O(\(log_n\))。為什么呢?
我們知道,對數之間是可以互相轉換的,\(log_3n\) 就等於 \(log_32\) * \(log_2n\),所以 O(\(log_3n\)) = O(C * \(log_2n\)),其中 C=\(log_32\) 是一個常量。基於我們前面的一個理論:在采用大 O 標記復 雜度的時候,可以忽略系數,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等於 O(log3n)。 因此,在對數階時間復雜度的表示方法里,我們忽略對數的“底”,統一表示為 O(logn)

如果一段代碼的時間復雜度是 O(logn),我們循環執行 n 遍,時間復雜度就是 O(nlogn) 了。而且,O(nlogn) 也是一種非常常見的算法時間復雜度。比如,歸並排序、快速排序的時間復雜度都是 O(nlogn)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM