圖解數據結構(開篇)

圖解數據結構

  • 做者:天行

參加了 lucifer 的 91 天學算法活動,不知不覺中已經一月有餘。從盲目地作到有目的、有套路地去作。git

在 lucifer 的 91 課程中,從基礎到進階到專題,在這個月中,經歷了基礎篇的洗禮,無論在作題思路,仍是作題速度都有了很大的提高,這個課程,沒什麼好說的,點贊點贊再點贊。也意識到學習好數據結構有多重要,不只是思惟方式的改變,仍是在工程上的應用。github

對一個問題使用畫圖、舉例、分解這 3 種方法將其化繁爲簡,造成清晰思路再動手寫代碼,一張好的圖可以更好地幫助去理解一個算法。所以本次分享如何使用畫圖同時結合經典的題目的方法去闡述數據結構。面試

​<!-- more -->算法

數據結構與算法有用麼?

這裏我摘錄了一個知乎的高贊回答給你們作參考:編程

我的認爲數據結構是編程最重要的基本功沒有之一!學了順序表和鏈表,你就知道,在查詢操做更多的程序中,你應該用順序表;而修改操做更多的程序中,你要使用鏈表;而單向鏈表不方便怎麼辦,每次都從頭至尾好麻煩啊,怎麼辦?你這時就會想到雙向鏈表 or 循環鏈表。
學了棧以後,你就知道,不少涉及後入先出的問題,例如函數遞歸就是個棧模型、Android 的屏幕跳轉就用到棧,不少相似的東西,你就會第一時間想到:我會用這東西來去寫算法實現這個功能。
學了隊列以後,你就知道,對於先入先出要排隊的問題,你就要用到隊列,例如多個網絡下載任務,我該怎麼去調度它們去得到網絡資源呢?再例如操做系統的進程(or 線程)調度,我該怎麼去分配資源(像 CPU)給多個任務呢?確定不能所有一塊兒擁有的,資源只有一個,那就要排隊!那麼怎麼排隊呢?用普通的隊列?可是對於那些優先級高的線程怎麼辦?那也太共產主義了吧,這時,你就會想到了優先隊列,優先隊列怎麼實現?用堆,而後你就有疑問了,堆是啥玩意?本身查吧,敲累了。
總之好好學數據結構就對了。我以爲數據結構就至關於:我塞牙了,那麼就要用到牙籤這「數據結構」,固然你用指甲也行,只不過「性能」沒那麼好;我要擰螺母,確定用扳手這個「數據結構」,固然你用鉗子也行,只不過也沒那麼好用。學習數據結構,就是爲了瞭解之後在 IT 行業裏搬磚須要用到什麼工具,這些工具備什麼利弊,應用於什麼場景。之後用的過程當中,你會發現這些基礎的「工具」也存在着一些缺陷,你不知足於此工具,此時,你就開始本身在這些數據結構的基礎上加以改造,這就叫作自定義數據結構。並且,你之後還會造出不少其餘應用於實際場景的數據結構。。你用這些數據結構去造輪子,不知不覺,你成了又一個輪子哥。

既然這麼有用,那咱們怎麼學習呢?個人建議是先把常見的數據結構學個大概,而後開始安裝專題的形式突破算法。這篇文章就是給你們快速過一下一部分常見的數據結構。數組

從邏輯上分,數據結構分爲線性和非線性兩大類。網絡

  • 線性數據結構包括數組、鏈表、棧、隊列。
  • 非線性結構包括樹、哈希表、堆、圖。

而咱們經常使用的數據結構主要是數組、鏈表、棧、樹,這同時也是本文要講的內容。數據結構

數據結構一覽

數組

數組的定義爲存放在連續內存空間上的相同類型數據的集合。由於內存空間連續,因此能在 O(1)的時間進行存取。函數

劍指 offer03.數組中的重複的數字

題目描述:工具

在一個長度爲 n 的數組 nums 裏的全部數字都在 0 ~ n-1 的範圍內。數組中某些數字是重複的,但不知道有幾個數字重複了,也不知道每一個數字重複了幾回。請找出數組中任意一個重複的數字。

分析:

重複意味至少出現兩次,那麼找重複就變成了統計數字出現的頻率了。那如何統計數字頻率呢?(不使用哈希表),咱們能夠開闢一個長度爲 n 的數組 count_nums,而且初始化爲 0,遍歷數組 nums,使用 nums[i]爲 count_nums 賦值.

圖解:

image

(注意:數組下標從 0 開始)

劍指 offer21. 調整數組順序使奇數位於偶數前面

題目描述:

輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得全部奇數位於數組的前半部分,全部偶數位於數組的後半部分。

分析:

根據題目要求,須要咱們調整數組中奇偶數的順序,那這樣的話,咱們能夠從數組的兩端同時開始遍歷,右邊遇到奇數的時候停下,左邊遇到偶數的時候停下,而後進行交換。

image

1122.數組的相對排序

題目描述:

給你兩個數組,arr1 和  arr2,
arr2  中的元素各不相同
arr2 中的每一個元素都出如今  arr1  中
對 arr1  中的元素進行排序,使 arr1 中項的相對順序和  arr2  中的相對順序相同。未在  arr2  中出現過的元素須要按照升序放在  arr1  的末尾。

示例

輸入:arr1 = [2,3,1,3,2,4,6,7,9,2], arr2 = [2,1,4,3,9,6]

輸出:[2,2,2,1,4,3,3,9,6,7]

分析:

觀察輸出,發現數字,由於 arr1 老是根據 arr2 中元素的相對大小來排序,因此只至關於在 arr2 中進行填充,每一個地方該填充多少呢?這個時候就須要去統計 arr1 中每一個數字出現的頻率。

image

小結

在數組中,由於數組是一個有序的結構,這裏的有序是指在位置上的有序,因此大多數只須要考慮順序或者相對順序便可。

鏈表

鏈表是一種線性數據結構,其中的每一個元素其實是一個單獨的對象,每個節點裏存到下一個節點的指針(Pointer)。
就像咱們玩的尋寶遊戲同樣,當咱們找到一個寶箱的時候,裏面還存在尋找下一個寶箱的藏寶圖,依次類推,每個寶箱都是如此,一直到找到最終寶藏。

image

經過單鏈表,能夠衍生出循環鏈表,雙向鏈表等。

咱們來看下鏈表中比較經典的幾個題目。

面試題 02.02. 返回倒數第 k 個節點

題目描述:

實現一種算法,找出單向鏈表中倒數第 k 個節點。返回該節點的值。

示例:

輸入: 1->2->3->4->5 和 k = 2
輸出: 4

分析:

想要找到倒數第 k 個節點,若是此時在數組中,那咱們只須要用最後一個數組的索引減去 k 就能找到這個值,可是鏈表是不能直接經過索引獲得的。若是此時,咱們知道最後一個節點的位置,而後往前找 k 個不就找到咱們須要的節點了嗎?等價於咱們要找的節點和最後一個節點相隔 k 個位置。因此當有一個指針 front 出發 k 步後,咱們再出發,等 front 到達終點時,咱們恰好到達倒數第 k 個節點。

image

咱們把這種解法叫作雙指針,或者快慢指針,或者先後指針,這種方法能夠用於尋找鏈表中間節點,判斷是鏈表中是否存在環(循環鏈表)並尋找環入口。

61. 旋轉鏈表

題目描述:

給定一個鏈表,旋轉鏈表,將鏈表每一個節點向右移動 k 個位置,其中 k 是非負數。

示例:

輸入: 1->2->3->4->5->NULL, k = 2

輸出: 4->5->1->2->3->NULL

分析:

每一個數字向後移動 k 位,那麼最後 k 位就須要移動到前面,和找倒數第 k 位數字很類似,k 位後面的都移到開頭。惟一須要注意的地方就是,k 的值可能大於鏈表長度的 2 倍及以上,因此須要算出鏈表的長度,以保證儘快找到倒數 k 的位置。

解法 1

找到位置後,直接斷開
image

解法 2

製做循環鏈表,而後再找倒數第 k 個數,而後斷開循環鏈表
image

24. 兩兩交換鏈表中的節點

題目描述:

給定一個鏈表,兩兩交換其中相鄰的節點,並返回交換後的鏈表。你不能只是單純的改變節點內部的值,而是須要實際的進行節點交換。

示例:

輸入:head = [1,2,3,4]

輸出:[2,1,4,3]

分析:

原理很簡單,兩個指針,分別指向相鄰的兩個節點,而後再添加一個臨時指針作換交換的中介添加 dummy 節點,不用考慮頭節點的狀況,更加方便。直接上圖:

image

除了同時操做一個鏈表以外,有的題目也會給出兩個或者更多的鏈表,如兩數相加,如 leetcode 中 2.兩數相加、21.合併兩個有序鏈表、160.相交鏈表

21.相交節點

題目描述:

編寫一個程序,找到兩個單鏈表相交的起始節點。

以下面的兩個鏈表

image

分析:

咱們知道,對於任意兩個數 ab,必定有 a+b-c=b+a-c,

image

基於 a+b-c=b+a-c,咱們能夠設置兩個指針,分別指向 A 和 B,以相同的步長同時移動,並在第一次到達尾節點的時候,指向另外一個鏈表,若是存在相交節點,也就是說 c > 0,那麼兩個指針必定會相遇,相遇處也就是相交節點。而當不存在時,即 c=0,那麼兩個指針最終都會指向空節點。

小結

鏈表中的操做無非就是兩種,插入,刪除。解題方法無非就是添加 dummy 節點(解決頭節點的判斷問題)、快慢指針(快慢不必定是單次步長同樣,應該理解爲平均步長,即便用了相同的時間,走的路程的長度來定義快慢)。

棧是一種先進後出(FILO, First In Last Out)的數據結構
能夠把棧理解爲

<center>

image

</center>

沒錯,就是上圖的罐裝薯片,想要吃到最底下的那片,必須依次吃完上面的。而在裝薯片的時候,最底下的反而是最早裝進去的。

在 leetcode 裏面關於棧比較經典的題目有:20.有效的括號;150.逆波蘭表達式求值

20.有效的括號

題目描述:

給定一個只包括 '(',')','{','}','[',']' 的字符串,判斷字符串是否有效。

示例:
"{[()][]}()"

| 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| {   | [   | (   | )   | ]   | [   | ]   | }   | (   | )   |

分析:

  • 一個有效的括號爲,右邊必須和左邊對應,且存在至少一對有效括號的索引爲[i, i+1]。 那麼,咱們只要是括號左邊部分,就入棧,右邊部分,就和棧頂元素比較。

圖解:

image

150.逆波蘭表達式求值

題目描述:

根據 逆波蘭表示法,求表達式的值。
有效的運算符包括 +, -, \*, / 。每一個運算對象能夠是整數,也能夠是另外一個逆波蘭表達式。

示例:
["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+"]

分析:

  • 根據運算法則,咱們能夠知道,一個運算有 num1,operation,num2 三部分組成。在一個逆波蘭表達式中,運算符前面兩個 num 就是這個運算的組成。
  • 咱們要作事情就是,找到一個運算符的時候,同時找到他前面的兩個數,而棧的現金先去特性知足這個需求,使用棧來解決。

image

227.基本計算器 II

題目描述:

實現一個基本的計算器來計算一個簡單的字符串表達式的值。字符串表達式僅包含非負整數,+, - ,\*,/ 四種運算符和空格 。 整數除法僅保留整數部分。

示例:3+5\*2/2-3

分析:

  • 與逆波蘭表達式不一樣的地方是,這裏運算符兩邊是操做數。可是,這又有什麼問題呢?萬變不離核心,咱們只須要在找到運算符的同時,獲得運算符兩邊的操做數。問題來了,還須要考慮運算符的優先級,想到的一個方法就是,只進行乘除法運算,最後進行加法運算,不進行減法運算(減去一個數 ⟺ 加上這個數的負數)

image

  • 若是可以把這個字符串表達式類似地轉換位逆波蘭表達式,那就能直接套用逆波蘭表達式的代碼了,回顧一下,逆波蘭表達式是,每次有操做符的時候,就從棧頂出來兩個元素。能夠經過使用兩個棧來實現,一個棧用來存儲操做數,一個棧用來存儲操做符。若是比棧頂的操做符符優先級低或者相同,那麼就從操做符棧取出棧頂運算符號

image

496.下一個更大元素 I

題目描述:

給定兩個 沒有重複元素的數組  nums1 和  nums2 ,其中 nums1  是  nums2  的子集。找到  nums1  中每一個元素在  nums2  中的下一個比其大的值。nums1  中數字  x  的下一個更大元素是指  x  在  nums2  中對應位置的右邊的第一個比  x  大的元素。若是不存在,對應位置輸出 -1。

示例:

輸入: nums1 = [4,1,2], nums2 = [1,3,4,2].

輸出: [-1,3,-1]

分析:

題目要求咱們找出 nums1 中每一個元素在 nums2 中的下一個比這個元素大的值,又提到 nums1 是 nums2 的一個子集,咱們不妨找出 nums2 中每一個元素的下一個比他大的值,而後映射到 nums1 中。

那如何找出 nums2 中每一個元素的下一個比他大的值呢?對於當前元素,若下一個元素比他大,則找到了,不然的話,就把這個元素添加到棧中,一直到找到一個元素比棧頂元素大,這時候把棧裏面全部小於這個元素的元素都出棧,聽起來很繞,無妨,看圖---->
image

最後棧中依然有數據存在,這是爲何呢?由於這些元素後面找不到比他更大的值了。觀察示例數據,4 後面沒有比他更大的值了,5 和 1 也是。咱們還能觀察到棧中元素是從大到小的,能夠稱這個棧爲==單調遞減棧==(如 1019.尋找鏈表中的下一個更大節點,503.下一個更大元素 II、402.移掉 k 位數字,39.每日溫度,在 1673.找出最具備競爭力的子序列中,其實只須要構建一個單調遞增棧,而後截取前 k 個。)。

回到題目,須要找到 nums1 中元素在 nums2 中的下一個比其大的值,只須要在剛纔保存的信息中進行查找,找不到的則不存在,可使用哈比表保存每一個數對應的下一個較大值。

小結

棧因爲其隨時能夠出棧和進棧,產生很是多的組合,帶來了很是多的變化,因此讀懂題目很是重要,而後選擇方法,正所謂題目是有限的,方法是有限的。因此緊跟 lucifer 大佬學習套路,是一條值得堅持的道路,畢竟自古深情留不住,惟有套路得人心,這裏推薦 lucifer 大佬的《單調棧模板帶你秒殺八道題》,帶你亂殺。

樹雖相比於鏈表來講,至少有兩個節點(n 個節點就叫 n 叉樹),可是樹是一個抽象的概念,能夠理解爲一個不停作選擇的過程,給定一個起始條件,會產生多種結果,而這些結果又成爲新的條件,以此類推,直到再也不有新的條件。在樹種,起始條件就是根節點,再也不產生新的條件的就是葉子節點。在樹種,使用較多的是二叉樹。一顆二叉樹無論有多大,咱們均可以把他拆分爲五種形式,

image

無論是在樹上進行什麼操做,都須要進行遍歷,遍歷的方式有兩種:廣度優先遍歷(BFS)和深度優先遍歷(DFS)。
簡單來講,廣度就是先找到有多少種可能,而後找出這些可能有多少種可能;而深度就是每次只根據一個條件來找,直到最終沒有條件。
話很少說,上圖。

image

若是是試錯的話,廣度是一次把全部的結果都試一試,深度則是一條路走到黑。

這裏直接借用 lucifer 大佬的廣度、深度優先遍歷模板(套路)

function dfs(root) {
 if (知足特定條件){
  // 返回結果 or 退出搜索空間
 }
 for (const child of root.children) {
        dfs(child)
 }
}

深度優先遍歷根據邏輯處理(==敲黑板,很重要==)的前後分爲前序遍歷、中序遍歷、後序遍歷

// 前序遍歷
function dfs(root) {
 if (知足特定條件){
  // 返回結果 or 退出搜索空間
    }
    // 主要邏輯
    dfs(root.left)
    dfs(root.right)
//中序遍歷
function dfs(root) {
 if (知足特定條件){
  // 返回結果 or 退出搜索空間
    }
    dfs(root.left)
    // 主要邏輯
    dfs(root.right)
// 後序遍歷
function dfs(root) {
 if (知足特定條件){
  // 返回結果 or 退出搜索空間
    }
    dfs(root.left)
    dfs(root.right)
    // 主要邏輯
}
}

接下來就要實操了

199. 二叉樹的右視圖

題目描述:

給定一棵二叉樹,想象本身站在它的右側,按照從頂部到底部的順序,返回從右側所能看到的節點值。

示例:

輸入: [1,2,3,null,5,null,4]

輸出: [1, 3, 4]

解釋:

image

分析:

此題便可以使用廣度優先,也能夠深度優先。
使用廣度優先,只須要將每一層的節點用一個數組保存下來,而後輸出最後一個
使用深度優先,這裏我使用的是根右左的方式,這樣能保證在每進入到一個新的層時,第一個訪問到的就是最右邊的元素。

上圖:

image

112. 路徑總和

題目描述:

給定一個二叉樹和一個目標和,判斷該樹中是否存在根節點到葉子節點的路徑,這條路徑上全部節點值相加等於目標和。
說明: 葉子節點是指沒有子節點的節點。

示例:
image

分析:

求一條路徑(根節點到葉子節點),這不就一條路走到底嗎,沒什麼好猶豫的,選擇深度優先遍歷。由於須要得到路徑上的和,咱們須要把每一個節點的值(狀態)傳遞給下一個節點。

image

在 113. 路徑總和 II 中,和本題相似,只須要把節點加入到數組中傳遞給下一個節點;在 129. 求根到葉子節點數字之和,須要把當前值*10 傳遞給下一個節點。

662. 二叉樹最大寬度

題目描述:

給定一個二叉樹,編寫一個函數來獲取這個樹的最大寬度。樹的寬度是全部層中的最大寬度。這個二叉樹與滿二叉樹(full binary tree)結構相同,但一些節點爲空。
每一層的寬度被定義爲兩個端點(該層最左和最右的非空節點,兩端點間的 null 節點也計入長度)之間的長度。

示例:

image

分析:

最大寬度,不就是找出哪一層最長嗎?廣度優先搜索會更加方便,須要注意的是,非兩端節點的 null 節點也要算到長度中,因此如今每一層存儲的不只僅是有值節點。

上圖

image

513. 找樹左下角的值

題目描述:

給定一個二叉樹,在樹的最後一行找到最左邊的值。

示例:

<center>

image

</center>
分析:
兩個關鍵信息,一個最後一行,一個最左邊。好像廣度,深度均可以找到,在這裏以深度進行說明,最後一行就是depth最大的,因此在深度遍歷的時候,每次給一層傳遞depth信息。

image

與此題相似的還有 111. 二叉樹的最小深度,104.二叉樹的最大深度

感受,就這?好像也沒什麼難的啊,學完 lucifer 的課程,我就是這麼膨脹。

小結

無非就是,深度遍歷時,是否傳遞信息給下一層,給下一層傳遞什麼信息;廣度遍歷時,是否保存每一層,是否保存空節點。

總結

本次給你們介紹了四種比較常見的數據結構,分別是數組,鏈表,棧和樹。這四種只有樹是邏輯上的非線性數據結構,由於一個節點可能有多個孩子,而其餘數據結構只有一個前驅和一個後繼。

因爲先進後出的特性,咱們能夠用數組輕鬆地在 $O(1)$ 時間複雜度模擬棧的操做。可是隊列就沒那麼好命了,咱們必須使用鏈表來優化時間複雜度。

鏈表的考題相對比較單一,只要記住那幾個點就行了。

樹的題目比較豐富,和它的非線性數據結構有很大關係。因爲其是非線性的,所以有了各類遍歷方式,常見的是廣度優先和深度優先,不少題目都是靈活運用這兩種遍歷方式問題就迎刃而解。

關注公衆號力扣加加,學習算法不迷路。

參考:

相關文章
相關標籤/搜索