什么是數據結構與算法?
數據結構
從廣義上講,數據結構
就是指一組數據的存儲結構。
數據結構按照邏輯結構大致可以分為兩類:線性數據結構和非線性數據結構。
線性結構
線性結構指的是數據之間存在着一對一
的線性關系,是一組數據的有序集合。線性結構有且僅有一個開始結點和一個結束結點
,並且每個結點最多只有一個前驅和一個后繼。類比如現實生活中的排隊。
線性結構常見的有:數組、隊列、鏈表和棧等。
非線性結構
非線性結構指的是數據間存在着一對多
的關系,一個結點可能有多個前驅和后繼
。如果一個結點至多只有一個前驅且可以有多個后繼,這種結構就是樹形結構。類比如公司的組織結構。如果對結點的前驅和后繼的個數都不作限制,這種結構就是圖形結構。類比如社交網絡的朋友關系。
非線性結構常見的有:廣義表,樹,圖等。
算法
從廣義上講,算法
就是操作數據的一組方法。
在我看來,算法就是基於某種數據結構為了達到某種目的的實現步驟。
常見的算法有哪些?
舉個例子:
圖書館儲藏書籍你肯定見過吧?
![]()
為了方便查找,圖書管理員一般會將書籍分門別類進行“存儲”並按照一定規律編號,這就是書籍這種“數據”的存儲結構。那我們如何來查找一本書呢?有很多種辦法,你當然可以一本一本地找,也可以先根據書籍類別的編號,是人文,還是科學、計算機,來定位書架,然后再依次查找。籠統地說,這些查找方法都是算法,算法有好壞之分,好的算法可以提高查找效率,節約查詢時間;壞的算法對我們的查詢沒有任何幫助,甚至走進死循環。
數據結構和算法的關系
數據結構和算法是相輔相成的。數據結構是為算法服務的,算法要作用在特定的數據結構之上。 因此,我們無法孤立數據結構來講算法,也無法孤立算法來講數據結構。比如,因為數組具有隨機訪問的特點,常用的二分查找算法需要用數組來存儲數據。但如果我們選擇鏈表這種數據結構,二分查找算法就無法工作了,因為鏈表並不支持隨機訪問。數據結構是靜態的,它只是組織數據的一種方式。如果不在它的基礎上操作、構建算法,孤立存在的算法就是沒用的。
再舉個例子,計算數字從1
到100
之和,使用循環我們可能會寫出這樣的程序:
public int count(int number){
int res = 0;
for (int i = 1; i <= number; i++) {
res += i;
}
return res;
}
如果這里的100
變成了十萬、百萬,那么這里計算量同樣也會隨之增加,但是如果使用這樣一個求和的公式:
100 * (100 + 1) / 2
無論數字是多大,都只需要三次運算即可,算法可真秒!同樣數據結構與算法是相互依存的,數據結構為什么這么存,就是為了讓算法能更快的計算。所以學習數據結構與算法首先需要了解每種數據結構的特性,算法的設計很多時候都需要基於當前業務最合適的數據結構。
為什么要學習數據結構與算法?
當代程序員為了完成學業,為了更好的工作,為了寫出更優秀的代碼等等。反正只要你想學,總能找到堅持下去的理由。

-
每年涌現出大量計算機開發人員,如何在這么多競爭者中突出重圍,獲取心儀的Offer,掌握數據結構與算法已經成為必殺利器之一。
-
不單單是為了面試,掌握數據結構和算法,不管對於閱讀框架源碼,還是理解其背后的設計思想,都是非常有用的,畢竟每個程序員都不想止步於 CRUD。
-
在平時的開發過程中,如果不知道這些類庫背后的原理,不懂得時間、空間復雜度分析,你如何能用好、用對它們?存儲某個業務數據的時候,你如何知道應該用 ArrayList,還是 Linked List 呢?調用了某個函數之后,你又該如何評估代碼的性能和資源的消耗呢?
如何系統高效地學習數據結構與算法?
很多人都感覺數據結構和算法很抽象,晦澀難懂,宛如天書。還因為看不懂數據結構與算法,而一度懷疑自己太笨?正是這些原因,讓我們對數據結構和算法望而卻步。
其實學習數據結構和算法並不是很難,只要找到好的學習方法,抓住學習的重點,並且堅持下去,終有一天我們會征服這座高山。
那么學習數據結構與算法哪些是重點呢?
-
掌握復雜度分析方法 - 首先要掌握數據結構與算法中最重要的概念—
復雜度分析
,復雜度分析方法是考量效率和資源消耗的方法。所以,如果你只掌握了數據結構和算法的特點、用法,但是沒有學會復雜度分析,那就相當於只知道操作口訣,而沒掌握心法。 -
學習數據結構與算法是一個長期的過程,並且內容有很多,掌握了這些基礎的數據結構和算法,再學更加復雜的數據結構和算法,就會非常容易、非常快。
-
數據結構與算法的誕生都是為了解決實際問題,無數先輩解決問題留下的寶貴財富,才有了我們我們今天看到的這么多數據結構與算法,如果你深入了解了,你也可以發明新的數據結構與算法。所以在學習的過程中一定要結合實際場景分析,才能抓住核心,記得更牢靠。
一些可以讓你事半功倍的學習技巧

1、邊學邊練,適度刷題
2、多問、多思考、多互動
3、打怪升級學習法
4、知識需要沉淀,不要想試圖一下子掌握所有
復雜度分析
我們都知道,數據結構和算法本身解決的是“快”和“省”的問題,即如何讓代碼運行得更快,如何讓代碼更省存儲空間。所以,執行效率是算法一個非常重要的考量指標。那如何來衡量你編寫的算法代碼的執行效率呢?這里就要用到我們今天要講的內容:時間、空間復雜度分析。

大 O 復雜度表示法
算法的執行效率,粗略地講,就是算法代碼執行的時間。但是,如何在不運行代碼的情況下,用“肉眼”得到一段代碼的執行時間呢?
這里有段非常簡單的代碼,求 1,2,3…n 的累加和。現在,我就帶你一塊來估算一下這段代碼的執行時間。
public int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; i++) {
sum += i;
}
return sum;
}
從 CPU 的角度來看,這段代碼的每一行都執行着類似的操作:讀數據-運算-寫數據。盡管每行代碼對應的 CPU 執行的個數、執行的時間都不一樣,但是,我們這里只是粗略估計,所以可以假設每行代碼執行的時間都一樣,為 unit_time。在這個假設的基礎之上,這段代碼的總執行時間是多少呢?
第 2、3 行代碼分別需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,所以需要 2n * unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2) * unit_time。可以看出來,所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比。
按照這個分析思路,我們再來看這段代碼。
public int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum += i * j;
}
}
return sum;
}
我們依舊假設每個語句的執行時間是 unit_time。那這段代碼的總執行時間 T(n) 是多少呢?
第 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) 與每行代碼的執行次數 n 成正比。我們可以把這個規律總結成一個公式。注意,大 O 就要登場了!
我來具體解釋一下這個公式。其中,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)。
時間復雜度分析
如何分析一段代碼的時間復雜度?
1、只關注循環執行次數最多的一段代碼
2、加法法則:總復雜度等於量級最大的那段代碼的復雜度
3、乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積
幾種常見時間復雜度實例分析
O(1)
: 常數級別,不會影響增長的趨勢,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)
。O(logn)
: 對數級別,執行效率僅次於O(1)
,例如從一個100萬
大小的數組里找到一個數,順序遍歷最壞需要100萬
次,而logn
級別的二分搜索樹平均只需要20
次。二分查找或者說分而治之的策略都是這個時間復雜度。O(n)
: 一層循環的量級,這個很好理解,1s
之內可以完成千萬級別的運算。O(nlogn)
: 歸並排序、快排的時間復雜度,O(n)
的循環里面再是一層O(logn)
,百萬數的排序能在1s
之內完成。O(n²)
: 循環里嵌套一層循環的復雜度,冒泡排序、插入排序等排序的復雜度,萬數級別的排序能在1s
內完成。O(2ⁿ)
: 指數級別,已經是很難接受的時間效率,如未優化的斐波拉契數列的求值。O(!n)
: 階乘級別,完全不能嘗試的時間復雜度。
空間復雜度分析
如果能理解時間復雜度的分析,那么空間度的分析就會顯示的格外的好理解。它指的是一段程序運行時,需要額外開辟的內存空間是多少,我們來看下這段程序:
function test(arr) {
const a = 1
const b = 2
let res = 0
for (let i = 0; i < arr.length; i++) {
res += arr[i]
}
return res
}
我們定義了三個變量,空間復雜度是O(3)
,又是常數級別的,所以這段程序的空間復雜度又可以表示為O(1)
。只用記住是另外開辟的額外空間,例如額外開辟了同等數組大小的空間,數組的長度可以表示為n
,所以空間復雜度就是O(n)
,如果開辟的是二維數組的矩陣,那就是O(n²)
,因為空間度基本也就是以上幾種情況,計算會相對容易。
常見的空間復雜度就是
O(1)
、O(n)
、O(n²)
,像O(logn)
、O(nlogn)
這樣的對數階復雜度平時基本用不到
總結
常見時間復雜度對比:

- 復雜度也叫漸進復雜度,包括時間復雜度和空間復雜度,用來分析算法執行效率與數據規模之間的增長關系
- 越高階復雜度的算法,執行效率越低
- 常見的復雜度並不多,從低階到高階有:
O(1)
、O(logn)
、O(n)
、O(nlogn)
、O(n^2)