從零開始的算法入門科普(二),你應該知道的數據結構類型·其二


壹 ❀ 引

我在從零開始的算法入門科普(一)這篇文章中,簡述了數據結構與算法的聯系,好的數據結構設計會讓算法工作事半功倍。那么在這篇文章中,我們接着以圖示的形式將其它數據結構一一說完,廢話不多說,本文開始。

貳 ❀ 數據類型

貳 ✿ 壹 棧Stack

棧也是數據結構的一種,需要注意的是棧只能在固定端進行數據插入與刪除,先插入的數據總是被壓入棧底,最后插入的數據在棧頂。

比如上圖中,最先插入的數據Green被壓入棧底,隨着數據的不斷插入,如果想取出Green,我們得先取出Red再取出Blue。

所以棧滿足后進先出(LIFO)的特點,雖然當我們想取棧中數據首先得先取出上層數據,但是站在如果我們總是要取最新數據的角度,棧無疑非常有優勢。

在JavaScript開發中,函數調用,執行上下文與遞歸等,都會使用棧。

貳 ✿ 壹 隊列Queue

隊列與前面介紹的幾種數據相同,都是排成一列的數據結構。隊列與棧尤為類似,但和棧不同的是,隊列有隊頭和隊尾,插入數據在隊尾,刪除數據在隊頭。

比如上面的例子中,先插入的Green在取出時也是第一位,因此隊列滿足先進先出(FIFO)的條件。

貳 ✿ 壹 哈希表Hash table

哈希表又稱為散列表,是一種借助哈希函數進行數據存儲與讀取的數據結構,哈希表一般用於存儲鍵值對(key-value)數據。

簡單解釋下概念,假設現在存在鍵key,我們將key帶入哈希函數f(),從而得到用於存放與key對應的value的地址信息。而下次我們要訪問value時,還是通過哈希函數f(key)得到存儲地址信息,以便快速訪問數據。

那么,什么是哈希函數呢?哈希函數其實就是把輸入的數據轉換成固定長度的不規則的值的函數,這個值也稱為哈希code。

當然,我們在這里不會介紹怎么實現哈希函數,現有的哈希函數算法有多種,比如代表性的MD5,SHA-1,SHA-2等等。

介紹完哈希函數,我們還是回過頭來介紹哈希表,我們來通過一個例子加深印象。

假設現在我們有多個姓名與性別的鍵值對,其中key為姓名,性別為value,我們要做的就是已知某個名字,能快速查找出該名字的用戶性別。

如果我們使用之前介紹的數組來存放數組,那么應該是下圖這樣:

現在我們要知道西西的性別,由於不知道在數組第幾位,所以只能進行線性搜索,一直找到第三位發現用戶為西西,從而得到性別為女。

利用數組存儲的問題是,隨着數據越來越大,我們查找耗時也會更大,如果剛好要找的用戶是最后一位,那就得將整個數組遍歷一遍,有沒有更好的做法呢,比如使用哈希表。

假設現在我們已實現了一個哈希函數f(),已有一個包含四個不同地址的空哈希表。

現在我們將張三帶入哈希函數計算得到位置1,即f('張三')=>1於是我們將張三的數據存到哈希表1的位置:

之后分別帶入西西,東東,得到不同地址f('西西')=>2f('東東')=>3,我們存入哈希表中對應位置:

很不巧,當我們帶入傑倫進行計算時,得到的位置信息也是1,像f('張三')=f('傑倫')這樣的情況,有個專業名詞叫沖突,而傑倫的數據將采用鏈表指針的形式緊跟張三之后,像這樣:

好了,現在我們要知道西西的性別,通過f('西西')=>2,我們直接去哈希表中位置為2的地方找,於是順利得知西西性別為女。而假設我們現在要找傑倫,因為f('傑倫')=>1,於是看位置為1的地方,即便第一個為張三,但我們還是很快定位到第二個數據即是我們想要答案,你看,即便是這樣也要比最初數據排列要更快。

哈希表因為哈希函數的作用,能在存放數據后快速讀取,即便發生了沖突,我們也可以通過鏈表將沖突數據相連。但需要注意的是,如果哈希表的哈希值范圍過小,容易造成大量沖突,這也會帶來與數組一樣的遍歷麻煩;反之,如果哈希值范圍給的過大,就會造成上述我們模擬例子中未存放數據的空地址,所以選定哈希值范圍也格外重要。

貳 ✿ 壹 堆Heap

堆通常可以看成是一顆完全二叉樹的數組對象,因為屬於二叉樹范疇,所以堆也是圖形的樹狀結構之一。堆總是滿足兩個條件。一是堆中某個節點的值總是不大於或不小於其父節點的值,二是堆總是一顆完全二叉樹。

完全二叉樹:當二叉樹的深度為k時,它的k層節點必須都是連續靠左並不可隔開的,並且1~k-1層的結點數都達到最大個數(即1~k-1層為一個滿二叉樹)。

為了方便展示,我們下方例子都使用根節點最小的堆,即每個節點必定大於自己的父節點,這種堆也稱為最小堆或小根堆。一個理想的小根堆如下圖:

可以看到上圖滿足完全二叉樹的情況,其次每個節點都比自己的父節點大。

另外還做個補充,二叉樹除了完全二叉樹(上面給的例子)還有滿二叉樹,所謂滿二叉樹即是:

除最后一層無任何子節點外,每一層上的所有結點都有兩個子結點的二叉樹。如果二叉樹的層數為k,那么節點總數就是(2^k)-1個,這就是滿二叉樹。

上圖例子就是一個滿二叉樹,可以看到滿二叉樹都是一個規則的三角形。

堆一般用於實現優先級隊列(priority queue),有同學就疑惑了,這堆還沒說清楚,怎么又來了個優先級隊列,我們先給個優先級隊列的概念,引用百度:

普通的隊列是一種先進先出的數據結構,元素在隊列尾追加,而從隊列頭刪除。在優先隊列中,元素被賦予優先級。當訪問元素時,具有最高優先級的元素最先刪除。優先隊列具有最高級先出 (first in, largest out)的行為特征。通常采用堆數據結構來實現。

OK,現在我們通過一個例子來理解優先級隊列和堆數據存儲與獲取過程。

假設有下圖一個小根堆,我們要在堆中添加一個為5的子節點。

由於在子節點6的下方有空缺,所以5先被放在這里。

但前面我們說了,小根堆的子節點都應該比父節點大,所以5和6應該互換位置,如下:

接着對比5和1,由於子節點5比父節點1大,所以無需調換位置。

現在我們說說堆取數據,堆中獲取數據滿足從最上層開始,並為之最上面的數永遠最小,比如現在我們將1取走。

由於最小數的位置空缺,所以現在要重新整理堆的解構,堆的規則就是將最后的數移動到最上方,所以6被移動到了最上層:

問題又來了,我們還是得滿足子節點要大於父節點的規則,堆再次進行整理,但問題是子節點3與子節點5都比6要小,這時候堆會選擇子節點中更小的一個與父節點互換,所以最后是3與6互換,如下:

換完之后,子節點4又比6小,所以再次互換,如下:

一直到這里,我們取一個數據的操作就算完成了,是不是有點麻煩,但堆也有它的優勢。

以小根堆為例,堆最上方的值永遠是最小數據,所以取出最小值的實際為O(1)。此外,取出數據重新排列結構時,必須將最尾端的數據提到最上面,然后再進行拍訊,所以排列執行時間與樹狀結構層級成正比。

假設節點個數為n,那么可知層數為log2n,重新整理堆的耗時為O(logn)。追加數據也需要作比較,同理也得反復跟父級做大小對比,以達到子節點大於父節點的條件,所以追加時整理耗時也為O(logn)。

綜合來說,如果總是要從數據結構中取最大或最小值,小根堆或者大根堆是不錯的選擇。

貳 ✿ 壹 二叉搜索樹Binary search tree

二叉搜索樹也是樹狀結構一種,它的一大特點是每個節點最多有2個子節點。除此之外二叉搜索樹具備如下特別,同時若樹的左子樹不為空,那么左子樹的節點均大於連接在左邊的任意子節點;若右子樹不為空,那么右子樹的節點均小於連接在右邊的任意子節點。讀起來有點繞口,我們來看個例子(偷個懶,不加顏色了):

如上圖,節點9的左子樹的任意節點均比9小,再往上看,節點15的左子樹的節點有9,3,8,12都比15小。我們再看第二特征,節點15的右子樹節點有23,17,28,他們都比15要大,就這么個意思。

通過這個特征我們不難得出,從根節點往左子樹看,只看左子樹分支,最尾端的一定是最小數,也就是3。而從根節點往右子樹看,位於右子樹分支的最尾端一定是最大數,也就是28。

接着我們說說二叉搜索樹添加節點的過程,比如我們要追加一個節點1,由於1比15小,所以它得往左子樹下面移動。

在跟9比較之后因為比9小,所以繼續往9的左分支下移,再跟3比較,最后添加到了3的左分支

而當我們要查找某個節點時,原理其實與插入節點一樣,查找的目標會以此與節點進行對比,直到找到對應的節點,這里就不再說過多描述。

最后說說二叉搜索樹刪除,比如還是我們上面添加節點1之后的解構,現在把節點9給刪除。

還記得二叉搜索樹的兩大特診嗎,節點9被刪除后,后續工作就是從節點9的左子樹分支中找到最大數拿過去替補,這里就是節點8,所以如下:

你看,8比左子樹的3大,比右子樹的12小,同時比父節點15小。

二叉搜索樹在查找的特點就是將目標與當前節點做比較,來決定是往左還是往右查找,二叉搜索樹比較次數與層級數有關,畢竟有幾層就得一直比較到底。當有n個節點,樹狀結構達到滿二叉樹的結構,最多只需要進行log2n次的比較和移動即可,所以耗時為O(logn)。而如果n個節點被排成一條分支,也就是一條直線,那就得從頭找到尾,時間復雜度為O(n)。

叄 ❀ 總

比較曲折,還是花了大半天的時間將剩余數據結構圖解講完了,其實有點后悔開了這個坑,最大的問題在於,我寫博客的時間甚至達到我學習這部分只是時間的三四倍,畫圖真的太累了...

所以后續我還是先保證自己能把知識學完,至於后續博客,盡力更新,那么到這里本文結束!


免責聲明!

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



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