前端該如何準備資料結構和演算法?

ConardLi發表於2019-08-20

一、導讀

據我瞭解,前端程式設計師有相當一部分對“資料結構”和“演算法”的基礎概念都不是很清晰,這直接導致很多人在看到有關這部分的內容就會望而卻步。

實際上,當你瞭解了“資料結構”和“演算法”存在的真正意義,以及一些實際的應用場景,對它有了一個整體的認知之後,你可能會對它產生強烈的興趣。當然,它帶將帶給你的收益也是相當可觀的。

很多前端同學在看到“資料結構”和“演算法”後會有一定的牴觸心理,或者嘗試去練習,但是被難倒,從而放棄。

這很大一部分原因是因為你還不夠了解學習他們的意義,或者沒有掌握合理的練習方法。

實際上,當你有了一定的目的性,並且有了合理的練習方法,再來學習這部分內容會變得得心應手。
å
在本文中,我就來分享一下我學習“資料結構”和“演算法”的一些經驗和方法。

後面我也會針對所有常見的資料結構和演算法分類,進行全方位的梳理。

1.1 類別說明

資料結構和演算法的種類非常之多,拿樹舉例,樹的種類包括:二叉樹、B樹、B+樹、Trie樹、紅黑樹等等,本文只選擇了二叉樹。

對前端來講,沒有必要對某些比較偏的型別和解法多做了解,一是浪費寶貴的時間,二是應用的不多。

本文選擇的資料結構和演算法的類別均是出現頻率最高,以及應用最廣的類別。

1.2 題目說明

另外,做題時找對典型題目非常重要,可以讓你更快速更高效的掌握知識,本文後面也會給出每種型別的典型題目供大家參考。

題目來源:

  • awesome-coding-js:我的前端演算法開源專案,包括我做過的題目以及詳細解析
  • leetcode
  • 劍指offer

另外,我會在後面長期更新一個前端演算法的專欄,對每類資料結構和演算法進行詳細的講解,敬請期待。

二、為什麼要學習資料結構和演算法

在學習某塊內容之前,我們一定要首先明確為什麼要學,而不是盲目的跟風。

這將更有利於你從學習的過程中獲得收益,而且會為你的學習帶來動力。

首先明確一點,學習資料結構和演算法不一定就是記住二叉樹、堆、棧、佇列等的解題方法也不是死記硬背一些題目,如果你僅僅停留在這樣的表面思想,那麼你學習起來會非常痛苦。

2.1 解決問題的思想

計算機只是一個很冰冷的機器,你給他下發什麼樣的指令,它就能作出什麼樣的反應。

而開發工程師要做的是如何把實際的問題轉化成計算機的指令,如何轉化,來看看《資料結構》的經典說法:

設計出資料結構, 在施加以演算法就行了。

所以,很重要的一點,資料結構和演算法對建立解決問題的思想非常重要。

如果說 Java 是自動檔轎車,C 就是手動檔吉普。資料結構呢?是變速箱的工作原理。你完全可以不知道變速箱怎樣工作,就把自動檔的車子從 A 開到 B,而且未必就比懂得的人慢。寫程式這件事,和開車一樣,經驗可以起到很大作用,但如果你不知道底層是怎麼工作的,就永遠只能開車,既不會修車,也不能造車。如果你對這兩件事都不感興趣也就罷了,資料結構懂得用就好。但若你此生在程式設計領域還有點更高的追求,資料結構是繞不開的課題。

2.2 面試

這是非常現實的一點,也是很多前端學習資料結構和演算法的原因。

一般對待演算法的態度會分為以下幾類:

GoogleMicrosoft等知名外企在面試工程師時,演算法是起決定性因素的,前端工程師也是一樣,基本是每一輪都會考察,即使你有非常強的背景,也有可能因為一兩道演算法答的不好而與這樣的企業失之交臂。

第二類,演算法佔重要因素的,國內的某些大廠在面試時,也會把資料結構和演算法作為重要的參考因素,基本是面試必考,如果你達不到一定的要求,會直接掛掉。

第三類,起加分作用,很多公司不會把資料結構和演算法作為硬性要求,但是也會象徵性的出一些題目,當你把一道演算法題答的很漂亮,這絕對是加分項。

可見,學好資料結構和演算法對你跳槽更好的公司或者拿到更高的薪水,是非常重要的。

三、如何準備

瞭解了資料結構和演算法的重要性,那麼究竟該用什麼樣的方法去準備呢?

3.1 全方位瞭解

在學習和練習之前,你一定要對資料結構和演算法做一個全方位的瞭解,對資料結構和演算法的定義、分類做一個全面的理解,如果這部分做的不好,你在做題時將完全不知道你在做什麼,從而陷入盲目尋找答案的過程,這個過程非常痛苦,而且往往收益甚微。

本文後面的章節,我會對常見的資料結構和演算法做一個全方位的梳理。

3.2 分類練習

當你對資料結構和演算法有了一個整體的認知之後,就可以開始練習了。

注意,一定是分類練習!分類練習!分類練習!重要的事情說三遍。

我曾見過非常多的同學帶著一腔熱血就開始刷題了,從leetcode第一題開始,剛開始往往非常有動力,可能還會發個朋友圈或者沸點什麼的?,然後就沒有然後了。

因為前幾題非常簡單,可能會給你一定的自信,但是,按序號來的話,很快就會遇到hard。或者有的人,乾脆只刷簡單,先把所有的簡單刷完。

但是,這樣盲目的刷題,效果是非常差的,有可能你堅持下來,刷了幾百道,也能有點效果,但是整個過程可能非常慢,而且效果遠遠沒有分類練習要好。

所謂分類練習,即按每種類別練習,例如:這段時間只練習二叉樹的題目,後面開始練習回溯演算法的題目。

在開始練習之前,你往往還需要對這種具體的類別進行一個詳細的瞭解,對其具體的定義、相關的概念和應用、可能出現的題目型別進行梳理,然後再開始。

3.3 定期回顧和總結

在對一個型別針對練習一些題目之後,你就可以發現一定的規律,某一些題目是這樣解,另一些題目是那樣解...這是一個很正常的現象,每種型別的題目肯定是存在一定規律的。

這時候就可以開始對此類題目進行總結了,針對此類問題,以及其典型的題目,發現的解題方法,進行總結。當下次你再遇到這種型別的題目,你就能很快想到解題思路,從而很快的解答。

所以,當你看到一個題目,首先你要想到它屬於哪種資料結構或演算法,然後要想到這是一個什麼型別的問題,然後是此類問題的解決方法。

如果你看到一個新的問題還不能做到上面這樣,那說明你對此類題目的掌握程度還不夠,你還要多花一些經歷來進行練習。

當然,後面我會把我在這部分的總結分享出來,幫助大家少走一些彎路。

3.4 題目的選擇

關於題目來源,這裡我推薦先看《劍指offer》,然後是leetcode,《劍指offer》上能找到非常多的典型題目,這對你發現和總結規律非常重要。看完再去刷leetcode你會發現更加輕鬆。

關於難度的選擇, 這裡我建議leetcode簡單、中等難度即可,因為我們要做的是尋找規律,即掌握典型題目即可,當你掌握了這些規律,再去解一些hard的問題,也是可以的,只是多花些時間的問題。切忌不要一開始就在很多刁鑽古怪的問題上耗費太多時間。

經過上面的方法,我在練習一段時間後,基本leetcode中等難度的問題可以在20minAC,另外在最近跳槽的過程中,基本所有的演算法問題我都能很快的手寫出來,或者很快的想到解題思路。希望大家在看到我的經驗和方法後也能達到這樣的效果,或者做的比我更好。

四、時間複雜度和空間複雜度

在開始學習之前,我們首先要搞懂時間複雜度和空間複雜度的概念,它們的高低共同決定著一段程式碼質量的好壞:

4.1 時間複雜度

一個演算法的時間複雜度反映了程式執行從開始到結束所需要的時間。把演算法中基本操作重複執行的次數(頻度)作為演算法的時間複雜度。

沒有迴圈語句,記作O(1),也稱為常數階。只有一重迴圈,則演算法的基本操作的執行頻度與問題規模n呈線性增大關係,記作O(n),也叫線性階。

常見的時間複雜度有:

  • O(1): Constant Complexity: Constant 常數複雜度
  • O(log n): Logarithmic Complexity: 對數複雜度
  • O(n): Linear Complexity: 線性時間複雜度
  • O(n^2): N square Complexity 平⽅方
  • O(n^3): N square Complexity ⽴立⽅方
  • O(2^n): Exponential Growth 指數
  • O(n!): Factorial 階乘

4.2 空間複雜度

一個程式的空間複雜度是指執行完一個程式所需記憶體的大小。利用程式的空間複雜度,可以對程式的執行所需要的記憶體多少有個預先估計。

一個程式執行時除了需要儲存空間和儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些為現實計算所需資訊的輔助空間。

五、資料結構

資料結構這個詞相信大家都不陌生,在很多場景下可能都聽過,但你有沒有考慮過“資料結構”究竟是一個什麼東西呢?

資料結構即資料元素相互之間存在的一種和多種特定的關係集合。

一般你可以從兩個維度來理解它,邏輯結構和儲存結構。

5.1 邏輯結構

簡單的來說邏輯結構就是資料之間的關係,邏輯結構大概統一的可以分成兩種:線性結構、非線性結構。

線性結構:是一個有序資料元素的集合。 其中資料元素之間的關係是一對一的關係,即除了第一個和最後一個資料元素之外,其它資料元素都是首尾相接的。

常用的線性結構有: 棧,佇列,連結串列,線性表。

—非線性結構:各個資料元素不再保持在一個線性序列中,每個資料元素可能與零個或者多個其他資料元素髮生聯絡。

常見的非線性結構有 二維陣列,樹等。

5.2 儲存結構

邏輯結構指的是資料間的關係,而儲存結構是邏輯結構用計算機語言的實現。常見的儲存結構有順序儲存、鏈式儲存、索引儲存以及雜湊儲存。

例如:陣列在記憶體中的位置是連續的,它就屬於順序儲存;連結串列是主動建立資料間的關聯關係的,在記憶體中卻不一定是連續的,它屬於鏈式儲存;還有順序和邏輯上都不存在順序關係,但是你可以通過一定的方式去放問它的雜湊表,資料雜湊儲存。

5.3 資料結構-二叉樹

樹是用來模擬具有樹狀結構性質的資料集合。根據它的特性可以分為非常多的種類,對於我們來講,掌握二叉樹這種結構就足夠了,它也是樹最簡單、應用最廣泛的種類。

二叉樹是一種典型的樹樹狀結構。如它名字所描述的那樣,二叉樹是每個節點最多有兩個子樹的樹結構,通常子樹被稱作“左子樹”和“右子樹”。

5.3.1 二叉樹遍歷

重點中的重點,最好同時掌握遞迴和非遞迴版本,遞迴版本很容易書寫,但是真正考察基本功的是非遞迴版本。
根據前序遍歷和中序遍歷的特點重建二叉樹,逆向思維,很有意思的題目

5.3.2 二叉樹的對稱性

5.3.3 二叉搜尋樹

二叉搜尋樹是特殊的二叉樹,考察二叉搜尋樹的題目一般都是考察二叉搜尋樹的特性,所以掌握好它的特性很重要。
  1. 若任意節點的左⼦子樹不不空,則左⼦子樹上所有結點的值均⼩小於它的 根結點的值;
  2. 若任意節點的右⼦子樹不不空,則右⼦子樹上所有結點的值均⼤大於它的 根結點的值;
  3. 任意節點的左、右⼦子樹也分別為⼆二叉查詢樹。

5.3.4 二叉樹的深度

二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。

平衡二叉樹:左右子樹深度之差大於1

5.4 資料結構-連結串列

用一組任意儲存的單元來儲存線性表的資料元素。一個物件儲存著本身的值和下一個元素的地址。

  • 需要遍歷才能查詢到元素,查詢慢。
  • 插入元素只需斷開連線重新賦值,插入快。

連結串列在開發中也是經常用到的資料結構,React16Fiber Node連線起來形成的Fiber Tree, 就是個單連結串列結構。

5.4.1 基本應用

主要是對連結串列基本概念和特性的應用,如果基礎概念掌握牢靠,此類問題即可迎刃而解

5.4.2 環類題目

環類題目即從判斷一個單連結串列是否存在迴圈而擴充套件衍生的問題

5.4.3 雙指標

雙指標的思想在連結串列和陣列中的題目都經常會用到,主要是利用兩個或多個不同位置的指標,通過速度和方向的變換解決問題。
  • 兩個指標從不同位置出發:一個從始端開始,另一個從末端開始;
  • 兩個指標以不同速度移動:一個指標快一些,另一個指標慢一些。

對於單連結串列,因為我們只能在一個方向上遍歷連結串列,所以第一種情景可能無法工作。然而,第二種情景,也被稱為慢指標和快指標技巧,是非常有用的。

5.4.4 雙向連結串列

雙鏈還有一個引用欄位,稱為prev欄位。有了這個額外的欄位,您就能夠知道當前結點的前一個結點。

5.5 資料結構-陣列

陣列是我們在開發中最常見到的資料結構了,用於按順序儲存元素的集合。但是元素可以隨機存取,因為陣列中的每個元素都可以通過陣列索引來識別。插入和刪除時要移動後續元素,還要考慮擴容問題,插入慢。

陣列與日常的業務開發聯絡非常緊密,如何巧妙的用好陣列是我們能否開發出高質量程式碼的關鍵。

5.5.1 雙指標

上面連結串列中提到的一類題目,主要是利用兩個或多個不同位置的指標,通過速度和方向的變換解決問題。注意這種技巧經常在排序陣列中使用。

5.5.2 N數之和問題

非常常見的問題,基本上都是一個套路,主要考慮如何比暴利法降低時間複雜度,而且也會用到上面的雙指標技巧

5.5.3 二維陣列

建立一定的抽象建模能力,將實際中的很多問題進行抽象

5.5.4 資料統計

陣列少不了的就是統計和計算,此類問題考察如何用更高效的方法對陣列進行統計計算。

5.6 資料結構-棧和佇列

在上面的陣列中,我們可以通過索引隨機訪問元素,但是在某些情況下,我們可能要限制資料的訪問順序,於是有了兩種限制訪問順序的資料結構:棧(後進後出)、佇列(先進先出)

5.7 資料結構-雜湊表

雜湊的基本原理是將給定的鍵值轉換為偏移地址來檢索記錄。

鍵轉換為地址是通過一種關係(公式)來完成的,這就是雜湊(雜湊)函式。

雖然雜湊表是一種有效的搜尋技術,但是它還有些缺點。兩個不同的關鍵字,由於雜湊函式值相同,因而被對映到同一表位置上。該現象稱為衝突。發生衝突的兩個關鍵字稱為該雜湊函式的同義詞。

如何設計雜湊函式以及如何避免衝突就是雜湊表的常見問題。
好的雜湊函式的選擇有兩條標準:
  • 1.簡單並且能夠快速計算
  • 2.能夠在址空間中獲取鍵的均勻人分佈

例如下面的題目:

當用到雜湊表時我們通常是要開闢一個額外空間來記錄一些計算過的值,同時我們又要在下一次計算的過程中快速檢索到它們,例如上面提到的兩數之和、三數之和等都利用了這種思想。

5.8 資料結構-堆

堆的底層實際上是一棵完全二叉樹,可以用陣列實現

  • 每個的節點元素值不小於其子節點 - 最大堆
  • 每個的節點元素值不大於其子節點 - 最小堆
堆在處理某些特殊場景時可以大大降低程式碼的時間複雜度,例如在龐大的資料中找到最大的幾個數或者最小的幾個數,可以藉助堆來完成這個過程。

六、演算法

6.1 排序

排序或許是前端接觸最多的演算法了,很多人的演算法之路是從一個氣泡排序開始的,排序的方法有非常多中,它們各自有各自的應用場景和優缺點,這裡我推薦如下6種應用最多的排序方法,如果你有興趣也可以研究下其他幾種。

選擇一個目標值,比目標值小的放左邊,比目標值大的放右邊,目標值的位置已排好,將左右兩側再進行快排。
將大序列二分成小序列,將小序列排序後再將排序後的小序列歸併成大序列。
每次排序取一個最大或最小的數字放到前面的有序序列中。
將左側序列看成一個有序序列,每次將一個數字插入該有序序列。插入時,從有序序列最右側開始比較,若比較的數較大,後移一位。
迴圈陣列,比較當前元素和下一個元素,如果當前元素比下一個元素大,向上冒泡。下一次迴圈繼續上面的操作,不迴圈已經排序好的數。
建立一個大頂堆,大頂堆的堆頂一定是最大的元素。交換第一個元素和最後一個元素,讓剩餘的元素繼續調整為大頂堆。從後往前以此和第一個元素交換並重新構建,排序完成。

6.2 二分查詢

查詢是計算機中最基本也是最有用的演算法之一。 它描述了在有序集合中搜尋特定值的過程。

二分查詢維護查詢空間的左、右和中間指示符,並比較查詢目標或將查詢條件應用於集合的中間值;如果條件不滿足或值不相等,則清除目標不可能存在的那一半,並在剩下的一半上繼續查詢,直到成功為止。如果查以空的一半結束,則無法滿足條件,並且無法找到目標。

6.3 遞迴

遞迴是一種解決問題的有效方法,在遞迴過程中,函式將自身作為子例程呼叫。

你可能想知道如何實現呼叫自身的函式。訣竅在於,每當遞迴函式呼叫自身時,它都會將給定的問題拆解為子問題。遞迴呼叫繼續進行,直到到子問題無需進一步遞迴就可以解決的地步。

為了確保遞迴函式不會導致無限迴圈,它應具有以下屬性:

  • 一個簡單的基本案例 —— 能夠不使用遞迴來產生答案的終止方案。
  • 一組規則,也稱作遞推關係,可將所有其他情況拆分到基本案例。

6.3.1 重複計算

一些問題使用遞迴考慮,思路是非常清晰的,但是卻不推薦使用遞迴,例如下面的幾個問題:

這幾個問題使用遞迴都有一個共同的缺點,那就是包含大量的重複計算,如果遞迴層次比較深的話,直接會導致JS程式崩潰。

你可以使用記憶化的方法來避免重複計算,即開闢一個額外空間來儲存已經計算過的值,但是這樣又會浪費一定的記憶體空間。因此上面的問題一般會使用動態規劃求解。

所以,在使用遞迴之前,一定要判斷程式碼是否含有重複計算,如果有的話,不推薦使用遞迴。

遞迴是一種思想,而非一個型別,很多經典演算法都是以遞迴為基礎,因此這裡就不再給出更多問題。

6.4 廣度優先搜尋

廣度優先搜尋(BFS)是一種遍歷或搜尋資料結構(如樹或圖)的演算法,也可以在更抽象的場景中使用。

它的特點是越是接近根結點的結點將越早地遍歷。

例如,我們可以使用 BFS 找到從起始結點到目標結點的路徑,特別是最短路徑。

BFS中,結點的處理順序與它們新增到佇列的順序是完全相同的順序,即先進先出,所以廣度優先搜尋一般使用佇列實現。

6.5 深度優先搜尋

和廣度優先搜尋一樣,深度優先搜尋(DFS)是用於在樹/圖中遍歷/搜尋的一種重要演算法。

BFS 不同,更早訪問的結點可能不是更靠近根結點的結點。因此,你在DFS 中找到的第一條路徑可能不是最短路徑。

DFS中,結點的處理順序是完全相反的順序,就像它們被新增到棧中一樣,它是後進先出。所以深度優先搜尋一般使用棧實現。

6.6 回溯演算法

從解決問題每一步的所有可能選項裡系統選擇出一個可行的解決方案。

在某一步選擇一個選項後,進入下一步,然後面臨新的選項。重複選擇,直至達到最終狀態。

回溯法解決的問題的所有選項可以用樹狀結構表示。

  • 在某一步有n個可能的選項,該步驟可看作樹中一個節點。
  • 節點每個選項看成節點連線,到達它的n個子節點。
  • 葉節點對應終結狀態。
  • 葉節點滿足約束條件,則為一個可行的解決方案。
  • 葉節點不滿足約束條件,回溯到上一個節點,並嘗試其他葉子節點。
  • 節點所有子節點均不滿足條件,再回溯到上一個節點。
  • 所有狀態均不能滿足條件,問題無解。

回溯演算法適合由多個步驟組成的問題,並且每個步驟都有多個選項。

6.7 動態規劃

動態規劃往往是最能有效考察演算法和設計能力的題目型別,面對這類題目最重要的是抓住問題的階段,瞭解每個階段的狀態,從而分析階段之間的關係轉化。

適用於動態規劃的問題,需要滿足最優子結構和無後效性,動態規劃的求解過程,在於找到狀態轉移方程,進行自底向上的求解。

自底向上的求解,可以幫你省略大量的複雜計算,例如上面的斐波拉契數列,使用遞迴的話時間複雜度會呈指數型增長,而動態規劃則讓此演算法的時間複雜度保持在O(n)

6.7.1 路徑問題

6.7.2 買賣股票類問題

子序列問題

6.8 貪心演算法

貪心演算法:對問題求解的時候,總是做出在當前看來是最好的做法。

適用貪心演算法的場景:問題能夠分解成子問題來解決,子問題的最優解能遞推到最終問題的最優解。這種子問題最優解成為最優子結構

6.8.1 買賣股票類問題

6.8.2 貨幣選擇問題

6.9 貪心演算法、動態規劃、回溯的區別

貪心演算法與動態規劃的不同在於它對每個子問題的解決方案都作出選擇,不能回退,動態規劃則會儲存以前的運算結果,並根據以前的結果對當前進行選擇,有回退功能,而回溯演算法就是大量的重複計算來獲得最優解。

有很多演算法題目都是可以用這三種思想同時解答的,但是總有一種最適合的解法,這就需要不斷的練習和總結來進行深入的理解才能更好的選擇解決辦法。

七、前端編碼能力

這部分是與前端開發貼近最緊密的一部分了,在寫業務程式碼的同時,我們也應該關心一些類庫或框架的內部實現。

大多數情況下,我們在寫業務的時候不需要手動實現這些輪子,但是它們非常考察一個前端程式設計師的編碼功底,如果你有一定的演算法和資料結構基礎,很多原始碼看起來就非常簡單。

下面我揀選了一些問題:

八、小結

本文的部分圖片來源於網路,如有侵權,請聯絡我刪除,謝謝。

本文並沒有對每個點進行深入的分析,而是從為什麼、怎麼做、做什麼的角度對資料結構和演算法進行的全面分析(針對前端角度),希望看完本片文章能對你有如下幫助:

  • 對資料結構和演算法建立一個較全面的認知體系
  • 掌握快速學習資料結構和演算法的方法
  • 瞭解資料結構和演算法的重要分類和經典題型

如果你還想更深入的學習資料結構和演算法,請關注我的後續文章。

推薦我的演算法總結:awesome-coding-jshttps://github.com/ConardLi/a...

文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關注。

想閱讀更多優質文章、可關注我的github部落格,你的star✨、點贊和關注是我持續創作的動力!

推薦關注我的微信公眾號【code祕密花園】,每天推送高質量文章,我們一起交流成長。

相關文章