優秀的算法每每取決於你採用哪一種數據結構,除了常規數據結構,平常更多也會遇到高級的數據結構,實現要比那些經常使用的數據結構要複雜得多,這些高級的數據結構可以讓你在處理一些複雜問題的過程當中多擁有一把利器。同時,掌握好它們的性質以及所適用的場合,在分析問題的時候迴歸本質,不少題目都能迎刃而解了。面試
這篇文章將重點介紹幾種高級的數據結構,它們是:優先隊列、圖、前綴樹、分段樹以及樹狀數組。 算法
1、優先隊列後端
1.優先隊列的做用數組
優先隊列最大的做用是能保證每次取出的元素都是隊列中優先級別最高的,這個優先級別能夠是自定義的,例如,數據的數值越大,優先級越高,或者是數據的數值越小,優先級越高,優先級別甚至能夠經過各類複雜的計算獲得。數據結構
優先隊列最經常使用的場景是從一堆雜亂無章的數據當中按照必定的順序(或者優先級)逐步地篩選出部分的乃至所有的數據。oop
例如,任意給定一個數組,要求找出前k大的數。最直接的辦法就是先對這個數組進行排序,而後依次輸出前k大的數,這樣的複雜度將會是O(nlogn),其中,n是數組的元素個數。性能
若是咱們借用優先隊列,就能將複雜度優化成O(k + nlogk),當數據量很大(即n很大),而k相對較小的時候,顯然,利用優先隊列能有效地下降算法複雜度,其本質就在於,要找出前k大的數,並不須要對全部的數進行排序。優化
2.優先隊列的實現方法網站
優先隊列的本質是一個二叉堆結構,堆在英文裏叫Binary Heap,它是利用一個數組結構來實現的徹底二叉樹。換句話說,優先隊列的本質是一個數組,數組裏的每一個元素既有多是其餘元素的父節點,也有多是其餘元素的子節點,並且,每一個父節點只能有兩個子節點,這就很像一棵二叉樹的結構了。設計
這裏有三個重要的性質須要牢記:
a.數組裏的第一個元素array[0]擁有最高的優先級別。
b.給定一個下標 i,那麼對於元素 array[i] 而言:
c.數組裏每一個元素的優先級別都要高於它兩個孩子的優先級別。
3.優先隊列最基本的操做
a.向上篩選
當有新的數據加入到優先隊列中,新的數據首先被放置在二叉堆的底部,而後不斷地對它進行向上篩選的操做,即若是發現它的優先級別比父節點的優先級別還要高,那麼就和父節點的元素相互交換,再接着網上進行比較,直到沒法再繼續交換爲止。因爲二叉堆是一棵徹底二叉樹,並假設堆的大小爲k,所以整個過程其實就是沿着樹的高度網上爬,因此只須要O(logk)的時間。
b.向下篩選
當堆頂的元素被取出時,咱們要更新堆頂的元素來做爲下一次按照優先級順序被取出的對象,咱們所須要的是將堆底部的元素放置到堆頂,而後不斷地對它執行向下篩選的操做,在這個過程當中,該元素和它的兩個孩子節點對比,看看哪一個優先級最高,若是優先級最高的是其中一個孩子,就將該元素和那個孩子進行交換,而後反覆進行下去,直到沒法繼續交換爲止,整個過程就是沿着樹的高度往下爬,因此時間複雜度也是O(logk)。
所以,不管是添加新的數據仍是取出堆頂的元素,都須要O(logk)的時間。
另一個最重要的時間複雜度是優先隊列的初始化,這是分析運用優先隊列性能時必不可少的,也是常常容易弄錯的地方。
假設咱們有n個數據,咱們須要建立一個大小爲n的堆,乍一看,每當把一個數據加入到堆裏,咱們都要對其執行向上篩選的操做,這樣以來就是O(nlogn)。可是,在建立這個堆的過程當中,二叉樹的大小是從1逐漸增加到n的,因此整個算法的複雜度是:
通過進一步的推導,最終的結果是O(n)。算法面試中是不要求推導的,你只須要記住,初始化一個大小爲n的堆,所須要的時間是O(n)便可。
注:
向上篩選,能夠用這個靜態圖
01例題分析
力扣(LeetCode)第347題. Top K Frequent Words 從一系列單詞中找出使用頻率最高的前K個單詞
解題思路:這道題的輸入是一個字符串數組,數組裏的元素可能會重複一次甚至屢次,要求按順序輸出前K個出現次數最多的字符串。
當咱們拿到這個題目的時候,看到」前K個「這樣的字眼就應該很天然地聯想到運用優先隊列。優先級別能夠由字符串出現的次數來決定,出現的次數越多,優先級別越高,反之越低。
統計詞頻的最佳數據結構就是哈希表(Hash Map),利用一個哈希表,咱們就能快速的知道每一個單詞出現的次數。
而後將單詞和其出現的次數做爲一個新的對象來構建一個優先隊列,那麼這個問題就很垂手可得地解決了。
解這類求前K個的題目,關鍵是看如何定義優先級以及優先隊列中元素的數據結構。
Desk (3)
/ \
car(2) book(1)
2、圖
1.圖的基本知識點
圖是全部數據結構裏面知識點最豐富的一個,最基本的知識點就有以下這些:
階(Order)、度:出度(Out-Degree)、入度(In-Degree)
樹(Tree)、森林(Forest)、環(Loop)
有向圖(Directed Graph)、無向圖(Undirected Graph)、徹底有向圖、徹底無向圖
連通圖(Connected Graph)、連通份量(Connected Component)
存儲和表達方式:鄰接矩陣(Adjacency Matrix)、鄰接鏈表(Adjacency List)
2.圖的算法
圍繞圖的算法也是五花八門:
以上的知識點只是圖論裏的冰山一角,對於算法面試而言,徹底不須要對每一個知識點都一一掌握,而應該有的放矢地進行準備。
3.關於圖的熱門考題
力扣(LeeCode)裏邊有許多關於圖論的算法題,並且都是很是經典的題目,如下的知識點是必須充分掌握並反覆練習的:
其中,環的檢測、二部圖的檢測、樹的檢測以及拓撲排序都是基於圖的遍歷,尤爲是深度優先方式的遍歷。而遍歷能夠在鄰接矩陣或者鄰接鏈表上進行,因此掌握好圖的遍歷是重中之重!由於它是全部其餘圖論算法的基礎。
至於最短路徑算法,能區分它們的不一樣特色,知道在什麼狀況下用哪一種算法就很好了。對於有充足時間準備的面試者,能熟練掌握它們的寫法固然是最好的。
可經過一道例題來複習圖論的知識。
02例題分析
力扣(LeetCode) 第785題. Is Graph Bipartite? 檢測一個圖是否爲二部圖
二部圖就是在圖裏面,圖的全部頂點能夠分紅兩個子集U和V,子集裏的頂點互不直接相連,圖裏面全部的邊,一頭連着子集U裏的頂點,一頭連着子集V裏的頂點。
必需要對這個圖進行一次遍歷才能判斷它是否爲二部圖。遍歷的方法有深度優先以及廣度優先。
基本的思想是,給圖裏的頂點塗上顏色。子集U裏的頂點都塗上紅色,子集V裏的頂點都塗上藍色。接着開始遍歷這個圖的全部頂點了,想象手裏握有兩種顏色(紅色和藍色)的畫筆,每次都是交替地給遍歷當中遇到的頂點塗上顏色,若是這個頂點尚未顏色,那就給它塗上顏色,而後換成另一支畫筆,遇到下一個頂點的時候,若是發現這個頂點已經塗上了顏色,並且顏色跟我手裏畫筆的顏色不一樣,那麼表示這個頂點它既能在子集U裏,也能在子集V裏,因此,它不是一個二部圖。
3、前綴樹
1.前綴樹的用法
前綴樹也被稱爲字典樹,由於這種數據結構被普遍地運用在字典查找當中。什麼是字典查找?舉例:給定一系列字符串,這些字符串構成了一種字典,要求你在這個字典當中找出全部以「ABC」開頭的字符串。
對於這樣的問題,常規想法是直接遍歷一遍字典,而後逐個判斷每一個字符串是否由「ABC」開頭。假設字典很大,有N個單詞,須要對比的不是「ABC」,而是任意的,不妨假設所要對比的開頭平均長度爲M,那麼這種暴力的搜索算法,時間複雜度就是O(M*N)。
若是咱們用前綴樹頭幫助對字典的存儲進行優化,就能夠把搜索的時間複雜度降低爲O(M),其中M表示字典裏最長的那個單詞的字符個數,在不少狀況下,字典裏的單詞個數N是遠遠大於M的。所以,前綴樹在這種場合中是很是高效的。
2.前綴樹的經典應用
在網站上的搜索框中輸入要搜索的文字時,搜索框會羅列出以搜索文字做爲開頭的相關搜索信息,這裏就運用到了前綴樹,在後端進行快速地檢索。
另外,漢字拼音輸入法,它的聯想輸出功能也運用到了前綴樹。
3.前綴樹的結構和性質
這裏可經過一個例子來深刻理解它,假若有一個字典,字典裏面有以下單詞:"A","to","tea","ted","ten","i","in","inn",每一個單詞還能有本身的一些權重值,那麼用前綴樹來構建這個字典將會是以下的樣子:
前綴樹有3個重要的性質:
a.每一個節點至少包含兩個基本屬性:
b.前綴樹的根節點是空的,所謂空,也就是說咱們只利用到了這個節點的children屬性,即咱們只關心在這個字典裏,有哪些打頭的字符。
c.除了根節點,其餘全部節點都有多是單詞的結尾,葉子節點必定都是單詞的結尾。
4.前綴樹的基礎操做
前綴樹最基本的操做就是兩個:建立和搜索。
建立前綴樹的方法很直觀,遍歷一遍輸入的字符串,對每一個字符串的字符進行遍歷,在遍歷的過程當中,從前綴樹的根節點開始,將每一個字符加入到節點的children字符集當中,若是字符集已經包含了這個字符,就能夠跳過,若是當前字符是字符串的最後一個,那麼就把當前節點的isEnd標記爲真。
前綴樹真正強大的地方在於,每一個節點還能用來保存額外的信息,好比能夠用來記錄擁有相同前綴的全部字符串。這樣一來,當用戶輸入某個前綴時,就能在O(1)的時間內給出對應的推薦字符串。
建立好前綴樹以後,搜索就會容易,方法相似,能夠從前綴樹的根節點出發,逐個匹配輸入的前綴字符,若是遇到了就繼續往下一層搜索,若是沒遇到,就當即返回。
03例題分析
LeetCode(力扣) 第212題:Word Search II 單詞查找 II
這是一道出現較爲頻繁的難題,題目給出了一個二維的字符矩陣,而後給出了一個字典,如今要求在這個字符矩陣中找到出如今字典裏的單詞。若有以下的字符矩陣:
解題思路:因爲字符矩陣的每一個點都能做爲一個字符串的開頭,因此必須嘗試從矩陣中的全部字符出發,上下左右一步步地走,而後去和字典進行匹配,若是發現那些通過的字符能組成字典裏的單詞,就把它記錄下來。
分別從矩陣的每一個字符出發,上下左右一步步地走,能夠借用深度優先的算法。關於深度優先算法,若是你對它不熟悉,能夠把它想象成走迷宮。
基本的算法有了,如何和字典匹配?直觀的作法是每次都循環遍歷字典,看看是否存在字典裏面,若是把輸入的字典變爲哈希集合的話,彷佛只須要O(1)的時間就能完成匹配。
可是,這樣的對比並不能進行前綴的對比,也就是說,必須每次都要進行一次全面的深度優先搜索,或者搜索的長度爲字典裏最長的字符串長度,這樣仍是不夠高效。假如在矩陣裏遇到了一個字符」V」,而字典里根本就沒有以「V」開頭的字符串,那麼根本就不須要將深度優先搜索進行下去,這樣一來,能夠大大地提升搜索效率。
剛纔提到了對比字符串的前綴,這裏須要藉助前綴樹來從新構建字典。構建好了前綴樹以後,每次從矩陣裏的某個字符出發進行搜索的時候,同步地對前綴樹進行對比,若是發現字符一直能被找到,就繼續進行下去,一步一步地匹配,直到在前綴樹裏發現一個完整的字符串,把它輸出便可。
4、分段樹
所謂分段樹,就是一種按照二叉樹的形式存儲數據的結構,每一個節點保存的都是數組裏某一段的總和。例如,數組是[1, 3, 5, 7, 9, 11],那麼它的分段樹就是:
由圖能夠看出,根節點保存的是從下標0到下標5的全部元素的總和,即36,它的左右兩個子節點分別保存左右兩半元素的總和,按照這樣的邏輯不斷地切分下去,最終的葉子節點保存的就是每一個元素的數值。
當更新數組裏某個元素的數值時,須要從分段樹的根節點出發,更新節點的數值,由於它保存的是數組元素的總和。接下來,修改的元素有可能會落在分段樹裏一些區間裏,至少葉子節點是確定須要更新的,因此,須要從根節點往下,判斷元素的下標是否在左邊仍是右邊,而後更新分支裏的節點大小。所以,複雜度就是遍歷樹的高度,即O(logn)。
完成了元素數值的更新以後,要對數組某個區間段裏的元素進行求和了。方法和更新操做相似,首先從跟節點出發,判斷所求的區間是否落在節點所表明的區間中,若是所要求的區間徹底包含了節點所表明的區間,那麼就得加上該節點的數值,意味着該節點所記錄的區間總和只是咱們所要求解總和的一部分。接下來,不斷地往下尋找其餘的子區間,最終得出所要求的總和。
分段樹的實如今書寫起來有些繁瑣,須要不斷地練習才能加深印象。
04例題分析
LeetCode(力扣) 第315題:Count of Smaller Numbers After Self
題目看起來很是簡單,給定一個數組nums,裏面都是一些整數,如今要求打印輸出一個新的數組counts,counts數組的每一個元素counts[i]表示nums中第i個元素右邊有多少個數小於nums[i]。
例如:輸入數組是[5, 2, 6, 1],應該輸出的結果是[2, 1, 1, 0]。什麼意思呢?好比對於5來講,它的右邊有兩個數比它小,分別是2和1,因此輸出的結果中,第一個元素是2,對於2來講,右邊只有1比它小,因此第二個元素是1,以此類推。
理解了問題以後,能夠看下如何借用分段樹來解決問題。既然是分段樹,就須要想一想分段樹的每一個節點應該須要包含什麼樣的信息。
對於這道題來講,由於要統計的是比某個數還要小的數的總和,若是把分段的區間設計成按照數值的大小來劃分,並記錄下在這個區間中的數的總和的話,那麼我就能快速地知道比當前數還要小的數有多少個了。
具體實現方式以下:
首先,從分段樹的根節點開始,根節點記錄的是數組裏最小值到最大值之間的全部元素的總和,而後分割根節點成左區間和右區間,不斷地分割下去,最後分段樹成爲這樣的模樣:
初始化的時候,每一個節點記錄的在此區間內的元素數量是0,接下來從數組的最後一位開始往前遍歷,每次遍歷,判斷這個數落在哪一個區間,那麼那個區間的數量加一。
當遇到1的時候,把它加入到分段樹裏,此時分段樹裏各個節點所統計的數量會發生必定的變化:
同時可得當前所遇到的最小值就是1。
接下來看看6,當把6加入到分段樹裏時,分段樹會發生以下變化:
在這個時候,須要求一下比6小的數有多少個?實際上是查詢一下分段樹,從1到5之間有多少個數。
須要從根節點開始查詢。因爲所要查詢的區間是1到5,它沒法包含根節點的區間1到6,因此繼續往下查詢。看左邊,很明顯,區間1到3被徹底包含在1到5之間,把該節點所統計好的數返回。再看來右邊,區間1到5跟區間4到6有交叉,繼續往下看,區間4到5徹底被包含在1到5之間,因此能夠立刻返回,並把統計的數量相加。
最後我可得在當前位置,在6的右邊比6小的數只有一個。
經過這樣的方法,每次把當前的數用分段樹進行個數統計,而後再計算出比它小的數便可。算法複雜度是O(nlogn)。
5、樹狀數組
樹狀數組也被稱爲Binary Indexed Tree,一樣的,爲了瞭解樹狀數組,讓咱們經過一個問題來好好理解一下這個數據結構吧。
假設咱們有一個數組array[0 … n-1], 裏面有n個元素,如今咱們要常常對這個數組作兩件事:
乍一看,彷佛能夠運用分段樹來求解,的確,分段樹是能夠在O(logn)的時間裏更新和求解前k個元素的總和。可是,仔細看這個問題,問題只要求求解前k個元素的總和,並不要求任意一個區間,在這種狀況下,就能夠借用樹狀數組了,它可讓咱們一樣在O(logn)的時間裏完成上述的操做。並且,相對於分段樹的實現,樹狀數組顯得更簡單。
樹狀數組的數據結構有如下幾個重要的基本特徵:
a.它是利用數組來表示多叉樹的結構,在這一點上和優先隊列有些相似,只不過,優先隊列是用數組來表示徹底二叉樹,而樹狀數組是多叉樹。
b.樹狀數組的第一個元素是空節點。
c.若是節點tree[y]是tree[x]的父節點,那麼須要知足以下條件:
y = x - (x & (-x))
因爲樹狀數組所解決的問題跟分段樹有些相似,因此不贅述了,力扣(LeetCode)上有不少經典的題目能夠用樹狀數組來解決,好比力扣(LeetCode)308題,求一個動態變化的二維矩陣裏,任意子矩陣裏的數的總和。
內容摘取自《300分鐘搞定算法面試》
第02講:高級數據結構
主講人:蘇勇,谷歌資深技術工程師