數據結構 + 算法-todo

0、總綱-數據結構分布圖

<br />
# 1、基本的數據結構知識樹
[圖片上傳失敗...(image-14e376-1599732804125)]<br />

<br />
# 2、10種最常見的數據結構
數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳表、圖、Trie樹<br />

<br />
## 2.0、數據結構分類
| 邏輯結構 | 線性結構 | 非線性結構 |
| :---: | :---: | :---: |
| | 順序表(比如數組便實現了邏輯上的順序表)、棧、隊列 等 | 樹、圖、堆 |
| 物理結構 | 順序儲存結構 | 鏈式儲存結構 |
| | 數組 | 鏈表 |

<br />
## 2.1、數組

<br />
### 2.1.1、數組的邏輯結構
[圖片上傳失敗...(image-d149f1-1599732804125)]

<br />
### 2.1.2、數組在內存中的存儲形式
[圖片上傳失敗...(image-e72c88-1599732804125)]

<br />
### 2.1.3、基本數據類型在java內存中的存儲方式
[圖片上傳失敗...(image-137ba4-1599732804125)]<br />這里需要關注一個點,int類型的基本數據類型,作為局部變量時是直接存儲在棧幀中的 局部變量表 中的;但是作為成員變量,是 引用 儲存在 方法區 中,值“1”是儲存在堆中,引用指向這個“1”的地址;作為int[]數組如圖。
> 這個點參考 java中,成員變量 int a = 1, a存在哪, 1存在哪 (存在JVM哪)?
> “成員變量的引用儲存在方法區”這個概念適用于特定的JVM模型,見Java方法區、堆、棧、本地方法區及新生代、老年代、元空間整合,在“經典的JVM內存布局”中應該在Metaspace(元數據區)中的klass(類元信息)區中(見《碼出高效Java開發手冊》-P128中對“元數據區”的描述,里面提到“......其他內容包括類元信息、 字段 、靜態屬性、方法、常量等都移至源空間內”)
> 按照另一種說法,“方法區又稱為永久代”--見Java字符串在內存中的存儲位置

    [圖片上傳失敗...(image-7083c1-1599732804125)]

<br />
### 2.2.4、字符串數組在java內存中的存儲方式
> String arr = "aa" 和 String arr = new String("aa") 的區分參見 Java String —— 字符串常量池

    [圖片上傳失敗...(image-99a6d5-1599732804125)]<br />

    > 這里涉及到一個點,字符串常量池是否存在,存在的話在堆中的哪個區(新生代、老年代)?
    > 應該是用到的模型不一樣,原始的模型,字符串常量池的存在是在堆中的Perm區(即永久代,JDK8及以后Perm區被“淘汰”,Perm區中的字符串常量移至堆內存(不知道是在Yong區還是在Old區) -- 見《碼出高效Java開發手冊》-P128
    > 字符串常量池應該既不在新生代,也不在老年代(需要判斷是否參與GC回收,這點不確定-todo)--見[字符串常量池和運行時常量池是在堆還是在方法區?](https://www.cnblogs.com/cosmos-wong/p/12925299.html)

    [圖片上傳失敗...(image-9458be-1599732804125)]

<br />
### 2.2.5、對象數組在java內存中的存儲方式
[圖片上傳失敗...(image-76a710-1599732804125)]

<br />
### 2.2.6、二維數組在java內存中的存儲方式
參考上述#2.2.1、#2.2.2、#2.2.3,下圖以 元素為基本數據類型 為例<br />[圖片上傳失敗...(image-538485-1599732804125)]

<br />
### 2.2.7、Java中的ArrayList
ArrayList底層是數組,它在內存中的存儲可以參考 對象 -> 數組 的存儲形式<br />[圖片上傳失敗...(image-4075f4-1599732804125)]
> 在 數組 和 ArrayList 中做選擇時,是否選用ArrayList的標準有兩個:
> 1、是否會用到ArrayList的大部分方法
> 2、是否數組不能滿足需要

<br />
#### 2.2.7.1、(相對比數組)ArrayList的優點

    - 將很多數組的操作細節封裝起來了,一些操作都是性能比較高的寫法
    - 支持動態擴容(數組的擴容需要手動敲代碼實現 新建大容量數組 -> 復制數組元素)

<br />
#### 2.2.7.2、(相對比數組)ArrayList的缺點

    - ArrayList不支持存放基本數據類型
    - AarrayList的方法實現(相對比數組)沒有那么清晰,從代碼易讀性方面有一定的理解成本

<br />
### 2.2.8、數組的應用場景

    - 應用于性能要求比較高的開發場景,比如底層框架開發,需要將性能壓榨到極致
    - 對于內存使用要求沒那么苛刻,因為數組需要一段連續的內存空間,即使內存總容量大于數組要求的總容量,但是沒有足夠多的連續空間,數組也不能生存

<br />

<br />
## 2.2、鏈表

<br />
### 2.2.1、鏈表的邏輯結構
[圖片上傳失敗...(image-8bde3f-1599732804125)]<br />[圖片上傳失敗...(image-d1acde-1599732804125)]

<br />
### 2.2.2、單向鏈表在內存中的存儲形式
> 對于鏈表的同一個節點,節點和值是一塊分配的,內存地址是連續的;不同節點之間可能是連續的,也可能是跳躍的。
> java中的實現是 LinkedList(貌似除了LinkedList,沒有其他的集合/java類底層使用了鏈表,LinkedHashMap底層除了散列表還用多維護了一個雙向鏈表用于排序)

    [圖片上傳失敗...(image-328be2-1599732804125)]

<br />
### 2.2.3、雙向鏈表在內存中的存儲形式
參考 #2.2.1 和 #2.2.2<br />
<br />[圖片上傳失敗...(image-5ce2b1-1599732804125)]

<br />
### 2.2.4、鏈表vs數組的性能比較(時間復雜度)
> 數組適用于:讀多 寫少 場景
> 鏈表使用與:頻繁地插入、刪除元素 場景

    |  | 查找 | 更新 | 插入 | 刪除 |
    | :---: | :---: | :---: | :---: | :---: |
    | 數組 | O(1) | O(1) | O(n) | O(n) |
    | 鏈表 | O(n) | O(1) | O(1) | O(1) |

<br />
## 2.3、棧(stack)

<br />
### 2.3.1、棧的邏輯結構
> 參考單向鏈表

    [圖片上傳失敗...(image-efef83-1599732804125)]

<br />
### 2.3.2、棧在內存中的存儲形式
參考#2.2.2<br />

<br />
### 2.3.3、棧的應用場景
面包屑導航<br />[圖片上傳失敗...(image-9f19b0-1599732804125)]
> 棧的出棧和入棧順序相反,便于“回溯”歷史

<br />
## 2.4、隊列

<br />
### 2.4.1、隊列的邏輯結構
[圖片上傳失敗...(image-1fa00-1599732804125)]

<br />
### 2.4.2、隊列在內存中的存儲形式
參考#2.2.2<br />

<br />
### 2.4.3、隊列的應用場景

    - 多線程中,爭奪 `公平鎖` 的等待隊列

    [圖片上傳失敗...(image-3d0d9d-1599732804125)]

    - 網絡爬蟲實現網站抓取時,先將待爬取的URL放入隊列,再依照先后順序來抓取和解析

    [圖片上傳失敗...(image-9c2b2d-1599732804125)]

    - #2.7.2 二叉堆是實現堆排序及 **優先隊列**   的基礎
    >  優先隊列:優先隊列不再遵循“先入先出”的原則,分兩種情況
    >    - 最大優先隊列,無論入隊順序如何,都是當前最大的元素先出隊
    >    - 最小優先隊列,無論入隊順序如何,都是當前最小的元素先出隊

<br />
## 2.5、散列表
> HashMap的底層實現的是 散列表(JDK1.8之后還有散列表轉 數組+紅黑樹 的延伸)

<br />
### 2.5.1、散列表的邏輯結構
[圖片上傳失敗...(image-619a25-1599732804125)]
> 圖中的“hashCode就是對象在JVM中的內存地址”一說法不對,hashCode確實是對象的唯一身份標識,但不是內存地址,獲取對象的hashCode使用的是navie方法(不是java代碼實現的)
> 對key的hashCode進行hash運算,求hash值的方式(HashMap源碼):

    ```java
    (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
    ```
    > 根據hash值求數組下標(HashMap源碼-630行):
    > 為什么要進行這一步操作,直接使用hash值作為數組下標不好嗎?-- hash值的范圍大概為40億,內存放不下,所以要對hash值進行(取模 或 與運算+位移)運算來對這些Entry分組,可以理解成一個hash槽是一組元素

    ```java
    i = (n - 1) & hash
    ```
    > 還有一種比較簡單的方法(對hashCode進行取模),根據key的hashCode求數組下標(《漫畫算法》-P55):

    ```java
    index = HashCode(key) % Array.length;
    ```
    > 本節(#2.5.1)參考:
    > - HashMap源碼(JDK1.6)
    > - 《漫畫算法》-P54前后
    > - 《碼出高效》-P206
    > - [HashMap中Entry以及Hash值的理解](https://blog.csdn.net/linton1/article/details/90084524)
    > - [總結HashMap實現原理分析](https://blog.csdn.net/hefenglian/article/details/79763634)
    > - [JAVA 兩個對象不同為什么他們的hashcode有可能相同](https://blog.csdn.net/n13151125/article/details/83623296) 
    > - [HashMap如何計算數組下標](https://blog.csdn.net/duihsa156165/article/details/106860412/) 
    > - [Java 對象的內存地址是否就是hash code?](https://blog.csdn.net/Freeman19960215/article/details/104429334) 

<br />
### 2.5.2、hash碰撞的原因
key的hash值返回的是int類型,int類型的值最多有2^32個--是有限的,所以一定存在相同的hash值<br />2.5.3、規避hash碰撞的方式<br />當hash值相等時,通過equals判等,判斷key是否一致。因為equals()方法和hashCode()方法是有語義關聯的,需要滿足:<br />A.equals(B) == true --> A.hashCode == B.hashCode
> 正是因為這個關系,所以重寫equals方法的同時一定要重寫hashCode,因為對象原hashCode還是根據對象自身的內存地址計算的,同一個Class new出來的對象的hashCode自然不會一樣,所以equals自然也不會相等
> 這里參見:為什么重寫equals一定要重寫hashcode?

    又知,不同對象的hashCode就是各自在JVM中的內存地址,所以一定是唯一的(如果采用的計算hashCode的算法不一致,可能出現一樣的hashCode,出現了hashCode一樣的情況會怎么樣這里就不清楚了-  )

<br />
### 2.5.3、散列表在內存中的存儲形式
參照#2.1.2和#2.2.2<br />

<br />
### 2.5.4、散列表(HashMap)的優缺點
> 這里列的可能并不全,就醬紫吧。。

<br />
#### 2.5.4.1、散列表(HashMap)的優點

    - 作為KV對形式的數據結構,HashMap 的優勢就是查找和操作的時間復雜度都是O (1)

<br />
#### 2.5.4.2、散列表(HashMap)的缺點

    - 依賴hash算法,有一定的時間成本,并且存儲的key需要重寫hashCode方法(String默認重寫了hashCode方法)
    - 擴容對性能的損耗(參考數組的擴容)
    - 是線程不安全的(為什么是線程不安全的?見#2.5.7.1)

<br />
### 2.5.5、散列表(HashMap)的應用場景
參考HashMap的應用場景<br />

<br />
### 2.5.6、附:Java中幾種數據結構的優缺點對比
| | 遍歷速度 | 插入刪除速度 | 隨機訪問速度 | 備注 |
| --- | --- | --- | --- | --- |
| Set | 快 | 快 | 慢 | 比list多占一個指針的存儲空間 |
| List | 快 | 慢 | 快 | 必須之前知道數組元素個數,因為申請內存是連續長度明確的 |
| HashMap | 慢 | 快 | 快 | 適合海量數據,o(1)的隨機訪問速度,不是可遍歷 |
| 變體Set | 快 | 快 | 快 | set的基礎上多占一個List的控件,不過各種性能都好 |
| 變體List | 快 | 插入快不能刪除 | 快 | 各種性能都好就是不能有刪除操作 |

    > 本節(#2.5.5參考[Set、List、HashMap優缺點比較,高性能集合](https://www.cnblogs.com/541g/p/10014289.html))

<br />
### 2.5.7、HashMap的幾個常見問題

<br />
#### 2.5.7.1、HashMap為什么是線程不安全的

    - put的時候,多線程造成數據不一致(這個好理解)
    - 多線程情況下,HashMap的擴容操作可能因為resize導致 死循環/死鏈(有點繞,還是沒太理得過來-  )

    [圖片上傳失敗...(image-77cf59-1599732804125)]

<br />
#### 2.5.7.2、什么情況下發生resize(擴容)操作?
java this.threshold = tableSizeFor(initialCapacity) ... if (++size > threshold) resize();
當實際總元素個數(HashMap.size)大于閾值(桶元素容量 * 加載因子,即threshold = loadFactor * capacity,沒擴容前capacity就是初始容量 threshold )時擴容
> 參考:
> - 源碼
> - HashMap的擴容機制---resize()中的#3
> - HashMap默認容量為何為16?中的 #擴容 部分

<br />
#### 2.5.7.2、HashMap的加載因子是干什么的?加載因子為什么是0.75?
負載因子是用以權衡資源利用率和分配空間的系數(可以理解為一個加權系數)<br />提高空間利用率和減少查詢成本的折中,主要是泊松分布的話,0.75的概率最小<br />

    > 參考:
    >    - 《碼出高效Java開發手冊》-P206~P212
    >    - [HashMap默認加載因子為什么選擇0.75?(阿里)](https://www.cnblogs.com/aspirant/p/11470928.html)

<br />
#### 2.5.7.3、HashMap的初始容量為什么是16?
為了降低hash碰撞的幾率((參見為什么HashMap的初始容量是16文末)
> “初始容量”指的是 桶元素的多少 -- 即數組的大小
> HashMap中設置默認大小的語句是 (而不是直接給出 16,是照顧到可讀性,實際上 16不涉及運算,自然是 16的效率更高),源碼:

    ```java

/**

  • The default initial capacity - MUST be a power of two.
    */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    ```
    > 這里可以回憶有16把椅子坐12個人的例子,即12個人的會議,安排16把椅子,產生同一把椅子坐兩個人(hash碰撞)的概率要小很多;而不是一個極大數(是16)的原因是考慮空間占用
    > 選用16(即24)正好是2的整數次冪,不選用23(貌似)是考慮擴容概率/還是hash的均勻分布?(記得看到過這個說法,但是忘了出處是哪里)
    > 參考:
    > - 《碼出高效Java開發手冊》-P206~P212

<br />
#### 2.5.7.4、為什么HashMap的擴容是2的冪次方?
同理初始容量為16(即2`4),數組的總容量是2的整數次冪,能夠保證不同key算出的hash值相同的概率更小,讓hash分布更均勻,為后續的其他操作(查找、插入、刪除等)降低時間復雜度
> 參考:
> - 為什么hashMap的容量擴容時一定是2的冪次

<br />
## 2.6、二叉樹
> 推薦一個學習樹結構的網站 https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

<br />
### 2.6.1、樹
樹(tree)是n(n≥0)個節點的有限集。當n=0時,稱為空樹。在任意非空樹中,有如下特點:

    - 有且僅有一個特定的稱為根的節點
    - 當n>1時,其余節點可分為m(m>0)個互不相交的有限集,每一個集合本身又是一個樹,稱為根的子樹

    [圖片上傳失敗...(image-1c05de-1599732804125)]<br />

<br />
### 2.6.2、二叉樹
每個節點最多有兩個孩子節點的樹

<br />
#### 2.6.2.1、幾種二叉樹的邏輯結構
[圖片上傳失敗...(image-ea65b7-1599732804125)]

    - 二叉樹節點的兩個孩子節點,一個被稱為左孩子(left child),一個被稱為右孩子(right child),這兩個孩子節點的順序是固定的,就像人的左手就是左手,右手就是右手,不能顛倒順序或混淆
    - “滿二叉樹”--二叉樹的一種特殊形式:一個二叉樹的所有非葉子節點都存在左右孩子,并且所有葉子節點都在同一層級上,那么這個樹就是滿二叉樹

    [圖片上傳失敗...(image-e9ecf-1599732804125)]

    - “完全二叉樹”--二叉樹的另一種特殊形式:對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號從1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為從1到n的節點位置相同,則這個二叉樹為完全二叉樹

    [圖片上傳失敗...(image-cb6828-1599732804125)]<br />

<br />
#### 2.6.2.2、二叉樹的兩種物理結構

<br />
##### 2.6.2.2.1、鏈式儲存結構
[圖片上傳失敗...(image-845766-1599732804125)]

<br />
##### 2.6.2.2.2、數組
[圖片上傳失敗...(image-55279c-1599732804125)]<br />

<br />
### 2.6.3、二叉樹的應用場景
二叉樹包含許多特殊的形式,每一種形式都有自己的作用(比如上面提的 滿二叉樹、完全二叉樹),但是最主要的應用還是進行查找操作和維持相對順序這兩個方面。

<br />
####

<br />
#### 2.6.3.1、進行查找操作(使用二叉查找樹-binary search treee)
二叉查找樹在二叉樹的基礎上增加了以下幾個條件:

    - 如果左子樹不為空,則左子樹上所有的節點的值均小于根節點的值
    - 如果右子樹不為空,則右子樹上所有的節點的值均大于根節點的值
    - 左、右子樹也都是二叉查找樹
    > 這樣的話,在二叉查找樹構建的時候,對樹內各節點已經進行了排序、分組,保證了二叉樹的有序性,查找的時候自然會省事很多

    [圖片上傳失敗...(image-828f1-1599732804125)]
    > 以下,關于二叉查找樹的查找方式、構建方式等參考[[經驗] 二叉查找樹(GIF動圖講解)](https://bbs.elecfans.com/jishu_1152233_1_2.html)

<br />
#### 2.6.3.1.1、二叉查找樹(BST)的查找方式(動圖)
在二叉查找樹b中查找x的過程:<br />a、若b是空樹,則搜索失敗,否則<br />b、若x等于b的根節點的數據域之值,則查找成功;否則<br />c、若x小于b的根節點的數據域之值,則搜索左子樹;否則<br />d、查找右子樹<br />[圖片上傳失敗...(image-ddf88b-1599732804125)]

<br />
##### 2.6.3.1.2、從有序數組構造一個二叉查找樹(BST)的方式(動圖)
[圖片上傳失敗...(image-3f1b1-1599732804125)

<br />
##### 2.6.3.1.3、往BST(二叉查找樹)中插入元素的方式
向一個二叉搜索樹b中插入一個節點s的算法,過程為:<br />a、若b是空樹,則將s所指節點作為根節點插入,否則<br />b、若s->data等于b的根節點的數據域之值,則返回,否則<br />c、若s->data小于b的根節點的數據域之值,則把s所指的節點插入到左子樹中,否則<br />d、把s所指節點插入到右子樹中(新插入的節點總是葉子節點)
> 分析二叉查找樹的定義,發現:不會存在“插入了一個節點導致原樹結構發生重排”的情況

    [圖片上傳失敗...(image-d2262e-1599732804125)]

<br />
##### 2.6.3.1.4、BST轉成有序數組
[圖片上傳失敗...(image-222ad1-1599732804125)]

<br />
#### 2.6.3.2、維持相對順序(使用二叉排序樹-binary sort tree)
> 暈。。二叉排序樹其實就是二叉查找樹,只是應用的場景不一樣,根據功能不同,又起了個名字

    具體實現參見#2.6.3.1.2 和 #2.6.3.1.3<br />

<br />
### 2.6.4、二叉樹的遍歷方式(4種)
> 關注二叉樹的遍歷方式是因為
> 對于傳統的線性結構進行遍歷的操作是線性操作,比較簡單;相對比線性結構的遍歷,二叉樹是非線性結構,對其遍歷之前需要將非線性關聯的各節點轉化成一個線性的序列,之后對這個線性序列進行遍歷--這就造成,遍歷的方式不一樣,遍歷出的序列順序也不同。所以,我們需要分析不同的遍歷方式,根據實際需要選擇遍歷方式

    [圖片上傳失敗...(image-2891a5-1599732804125)]
    > 本節(#2.6.4)參考:
    > - 《漫畫算法》-P71~P87
    > - [【圖解數據結構】一組動畫徹底理解二叉樹三種遍歷](http://www.lxweimin.com/p/45d75aeb3b01) 
    > - [二叉樹三種遍歷(動態圖+代碼深入理解)](https://blog.csdn.net/weixin_45525272/article/details/105837185)

<br />
#### 2.6.4.1、深度優先遍歷(3種)
> 特點是縱向的“一頭扎到底”

    - **三種深度優先遍歷的代碼實現 ** 
    ```java

/**

  • 使用LinkedList遞歸構建二叉樹

  • @param inputList 輸入序列

  • @return TreeNode 頭結點
    */
    public static TreeNode createBinaryTree(LinkedList<Integer> inputList) {
    TreeNode node = null;
    if (inputList == null || inputList.isEmpty()) {
    return null;
    }

     /**
      * 移除鏈表頭節點
      */
     Integer data = inputList.removeFirst();
     if (data != null) {
     node = new TreeNode(data);
     node.leftChild = createBinaryTree(inputList);
     node.rightChild = createBinaryTree(inputList);
     }
     return node;
     }
    

/**

  • 遞歸進行二叉樹的前序遍歷
  • @param node 二叉樹節點
    */
    public static void preOrderTravel(TreeNode node) {
    if (node == null) {
    return;
    }
    System.out.println(node.data);
    preOrderTravel(node.leftChild);
    preOrderTravel(node.rightChild);
    }

/**

  • 使用棧的方式(棧具有回溯性,遞歸也具有回溯性)進行二叉樹的(非遞歸)前序遍歷
  • @param root 二叉樹的根節點
    */
    public static void preOrderTravelWithStack(TreeNode root) {
    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode treeNode = root;
    while (treeNode != null || !stack.isEmpty()) {
    // 迭代訪問節點的左孩子,并入棧
    while (treeNode != null) {
    System.out.println(treeNode.data);
    stack.push(treeNode);
    treeNode = treeNode.leftChild;
    }
    // 如果節點沒有左孩子,則彈出棧頂節點,訪問節點右孩子
    if (!stack.isEmpty()) {
    treeNode = stack.pop();
    treeNode = treeNode.rightChild;
    }
    }
    }

/**

  • 遞歸進行二叉樹的中序排列
  • @param node 二叉樹節點
    */
    public static void inOrderTravel(TreeNode node) {
    if (node == null) {
    return;
    }
    inOrderTravel(node.leftChild);
    System.out.println(node.data);
    inOrderTravel(node.rightChild);
    }

/**

  • 遞歸進行二叉樹的后序遍歷
  • @param node 二叉樹節點
    */
    public static void postOrderTravel(TreeNode node) {
    if (node == null) {
    return;
    }
    postOrderTravel(node.leftChild);
    postOrderTravel(node.rightChild);
    System.out.println(node.data);
    }

/**

  • 二叉樹的節點
    */
    private static class TreeNode {
    int data;
    TreeNode leftChild;
    TreeNode rightChild;

    TreeNode(int data) {
    this.data = data;
    }
    }

    public static void main(String[] args) {
    /**
    * LinkedList底層使用的是雙向鏈表,維護的是有序的數據集合。既支持FIFO,也支持FILO(看LinkedList源碼,發現新加入的元素是放到鏈表尾的,刪除的時候是刪除表頭元素,這樣看應該是FIFO)
    * 通過debug也證實了,元素按照3-2-9-10-8-4的順序進入樹的構建序列
    */
    LinkedList<Integer> inputList = new LinkedList<Integer>(Arrays.asList(new Integer[]{3, 2, 9, null, null, 10,
    null, null, 8, null, 4
    }));
    TreeNode treeNode = createBinaryTree(inputList);

     System.out.println("非遞歸實現-借用棧的回溯性-前序遍歷");
     preOrderTravelWithStack(treeNode);
    
     System.out.println("遞歸實現-前序遍歷");
     preOrderTravel(treeNode);
    
     System.out.println("遞歸實現-中序遍歷");
     inOrderTravel(treeNode);
    
     System.out.println("遞歸實現-后序遍歷");
     postOrderTravel(treeNode);
    

    }


        - **代碼運行結果 ** 

        [圖片上傳失敗...(image-e1012f-1599732804125)]

        - **代碼中的二叉樹結構** 

        [圖片上傳失敗...(image-ec1ad2-1599732804125)]<br />


<br />
        ##### 2.6.4.1.1、前序遍歷
        根 -> 左子樹 -> 左... -> 右子樹 -> 右...<br />[圖片上傳失敗...(image-d17149-1599732804125)]<br />[圖片上傳失敗...(image-73a67e-1599732804125)]

<br />
        ##### 2.6.4.1.2、中序遍歷
        左子樹(左葉子節點) -> 根節點 -> 右子樹 -> 根節點 -> 右子樹 -> ...<br />[圖片上傳失敗...(image-9e4534-1599732804125)]<br />[圖片上傳失敗...(image-753ff-1599732804125)]

<br />
        ##### 2.6.4.1.3、后序遍歷
        左子樹(左葉子節點) -> 右子樹 -> 根節點 -> 右子樹 -> 根節點 -> ...<br />[圖片上傳失敗...(image-cb10e0-1599732804125)]<br />[圖片上傳失敗...(image-736d9-1599732804125)

<br />
        #### 2.6.4.2、廣度優先遍歷(1種)
        > 特點是橫向的優先


<br />
        ##### 2.6.4.2.1、層序遍歷
        二叉樹按照從根節點到葉子節點的層次關系,一層一層橫向遍歷各個節點<br />[圖片上傳失敗...(image-7eef0b-1599732804125)]

<br />
        ##### 2.6.4.2.2、層序遍歷的代碼實現
        ```java
/**
 * 遞歸實現-二叉樹的層序遍歷
 *
 * @param root 二叉樹的根節點
 */
public static void levelOrderTravelsal(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        // 插入節點到隊列
        queue.offer(root);
        while (!queue.isEmpty()) {
        // Retrieves and removes the head of this queue
        TreeNode node = queue.poll();
        System.out.println(node.data);
        if (node.leftChild != null) {
        queue.offer(node.leftChild);
        }
        if (node.rightChild != null) {
        queue.offer(node.rightChild);
        }
        }
        }
        ```

<br />
        ##### 2.6.4.2.3、代碼執行結果
        [圖片上傳失敗...(image-918bfb-1599732804125)]<br />


<br />
        ## 2.7、堆(heap)
        二叉堆本質是完全二叉樹,通常可以被看作為一棵樹的 **** 對象,常見的堆有二叉堆、斐波那契堆、Pairing堆。(有些書上--比如《漫畫算法》P89--直接用“二叉堆”代替“堆”,不曉得二者有木有區別)堆滿足以下兩點性質:

        - 堆中某個節點的值總是不大于或不小于其父節點的值
        - 堆總是一棵完全二叉樹
        > 這個網站除了樹的學習之外,還可以模擬樹的結構學習 [https://www.cs.usfca.edu/~galles/visualization/Algorithms.html](https://www.cs.usfca.edu/~galles/visualization/Algorithms.html)
        > 這一節參考:
        > - 度娘
        > - 《漫畫算法》-P89
        > - [基本數據結構――堆的基本概念及其操作](https://www.cnblogs.com/JVxie/p/4859889.html)


<br />
        ### 2.7.1、二叉堆(這里暫且理解為 二叉堆是堆中的一個子分類)

<br />
        #### 2.7.1.1、二叉堆的邏輯結構和內存結構
        參見#2.6.2.1的 完全二叉樹
        > 子節點和父節點的下標滿足 2 * parentIndex + 1 = childIndex 或者 2 * parentIndex + 2 = childrenIndex

        [圖片上傳失敗...(image-6b9bb1-1599732804125)]<br />


<br />
        #### 2.7.1.2、最大堆(或叫 大根堆)
        最大堆的任何一個父節點的值,都大于或等于它左、右孩子節點的值<br />


<br />
        #### 2.7.1.3、最小堆(或叫 小根堆)
        最小堆的任何一個父節點的值,都小于或等于它左、右孩子節點的值<br />


<br />
        #### 2.7.1.4、二叉堆的幾種操作
        -- 粗粒度操作分類

        - 插入節點

        插入節點時,插入的位置是完全二叉樹的最后一個位置。隨后可能需要進行 上浮 操作進行調整
        > 插入節點的時間復雜度為 O(1) ~ O(logn)

        - 刪除節點

        刪除節點時,刪除的是處于堆頂點的節點,同時將最后一個位置的節點補到原本堆頂的位置,再對那個節點進行 下沉 操作)
        > 刪除節點的時間復雜度為 O(logn)

        - 構建二叉樹

        這個概念貌似等同于“堆排序”,就是把一個無序的完全二叉樹調整為二叉堆,本質就是讓所有的非葉子節點依次 下沉(從最后一個非葉子節點開始)
        > 構建二叉樹的時間復雜度為 O(1) ~ O(n)?(暈。。這個不等式不曉得咋并項了  )


<br />-- 細粒度操作分類

        - 上浮 shift_up
        - 下沉 shift_down
        - 插入 push
        - 彈出 pop
        - 取頂 top
        - 堆排序 heap_sort




<br />
        #### 2.7.1.5、二叉堆的代碼實現
        > 二叉堆的存儲方式并不是鏈式存儲,是順序存儲。換言之,二叉堆所有的節點都存儲在數組中。

        ```java
/**
 * (對葉子節點進行)單節點“上浮”調整
 *
 * @param array 待調整的堆
 */
public static void upAdjust(int[] array) {
        int childIndex = array.length - 1;
        // 假定這個是左子節點,子節點和父節點的下標滿足 2 * parentIndex + 1 = childIndex 或者 2 * parentIndex + 2 = childrenIndex
        int parentIndex = (childIndex - 1) / 2;
        // temp保存插入的葉子節點值,用于最后的賦值
        int temp = array[childIndex];
        while (childIndex > 0 && temp < array[parentIndex]) {
        // 無需真正交換,單向賦值即可--這里其實是父節點下沉
        array[childIndex] = array[parentIndex];
        // 指針上移,父節點變成子節點
        childIndex = parentIndex;
        // 指針上移,父節點的父節點變成父節點
        parentIndex = (parentIndex - 1) / 2;
        }
        // temp的上移操作完成,進行實際賦值
        array[childIndex] = temp;
        }

/**
 * (對指定節點進行)單節點“下沉”調整
 *
 * @param array       待調整的堆
 * @param parentIndex 要“下沉”的父節點
 * @param length      堆的有效大小
 */
public static void downAdjust(int[] array, int parentIndex, int length) {
        // temp保存父節點值,用于最后的賦值
        int temp = array[parentIndex];
        // 左子節點
        int childIndex = 2 * parentIndex + 1;
        while (childIndex < length) {
        // 如果有右孩子,且右孩子小于左孩子的值,則定位到右孩子
        if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
        childIndex++;
        }
        // 如果父節點小于任何一個孩子的值,則直接跳出
        if (temp <= array[childIndex]) {
        break;
        }
        // 無需真正交換,單向賦值即可
        array[parentIndex] = array[childIndex];
        // 指針下移,子節點變父節點
        parentIndex = childIndex;
        // 指針下移,子節點的子節點變子節點
        childIndex = 2 * childIndex + 1;
        }
        // 下沉操作完成,賦值
        array[parentIndex] = temp;
        }

/**
 * 構建最小堆
 *
 * @param array 待調整的堆
 */
public static void buildHeap(int[] array) {
        // 從最后一個非葉子節點開始,一次做“下沉”調整
        for (int i = (array.length - 2) / 2; i >= 0; i--) {
        downAdjust(array, i, array.length);
        }
        }

public static void main(String[] args) {
        // 這個數組只用一次葉子節點的上浮操作就能形成最小堆
        int[] array = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        // 對葉子節點進行上移操作,得到最小堆
        upAdjust(array);
        System.out.println(Arrays.toString(array));

        array = new int[]{7, 1, 3, 10, 5, 2, 8, 9, 6};
        // 構建最小堆
        buildHeap(array);
        System.out.println(Arrays.toString(array));
        }
        ```

<br />
        #### 2.7.1.6、代碼運行結果
        [圖片上傳失敗...(image-8689d0-1599732804125)]

        - 第一個數組的節點上浮操作

        [圖片上傳失敗...(image-ce02ea-1599732804125)]

<br />
        ### 2.7.2、二叉堆的應用-優先隊列(以二叉堆作為實現基礎的數據結構)

        - 二叉堆是實現堆排序及 **優先隊列**   的基礎
        >  優先隊列:優先隊列不再遵循“先入先出”的原則,分兩種情況
        >    - 最大優先隊列,無論入隊順序如何,都是當前最大的元素先出隊
        >    - 最小優先隊列,無論入隊順序如何,都是當前最小的元素先出隊

        所以,最大優先隊列可以使用最大堆實現,最小優先隊列可以使用最小堆實現<br />


<br />
        #### 2.7.2.1、優先隊列的代碼實現
        ```java
public class PriorityQueue {
    private int[] array;

    // 等同于 int size = 0;
    private int size;

    public PriorityQueue() {
        // 隊列初始長度為32
        array = new int[32];
    }

    /**
     * 入隊,入隊元素若更大則上浮
     * @param key 入隊元素
     */
    public void enQueue(int key){
        // 隊列長度超出范圍,擴容
        if (size >= array.length){
            resize();
        }
        array[size++] = key;
        upAdjust();
    }

    /**
     * 出隊
     * @return int
     * @throws Exception
     */
    public int deQueue() throws Exception{
        if (size <= 0){
            throw new Exception("the queue is empty!");
        }
        // 獲取堆頂元素
        int head = array[0];
        // 讓最后一個元素移動到堆頂
        array[0] = array[--size];
        downAdjust();
        return head;
    }

    /**
     * “上浮”調整
     */
    private void upAdjust(){
        int childIndex = size - 1;
        // 因為 -1/2=0(補碼、移碼相關),所以childIndex==0時,parentIndex==0
        int parentIndex = (childIndex - 1) / 2;
        // temp保存插入的葉子節點值,用于最后的賦值
        int temp = array[childIndex];
        while (childIndex > 0 && temp > array[parentIndex]){
            // 無須真正交換,單向賦值即可
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = parentIndex / 2;
        }
        array[childIndex] = temp;
    }

    /**
     * “下沉”調整
     */
    private void downAdjust(){
        // temp保存父節點的值,用于最后的賦值
        int parentIndex = 0;
        int temp = array[parentIndex];
        int childIndex = 1;
        while (childIndex < size){
            // 如果有右孩子,且右孩子大于左孩子的值,則定位到右孩子
            if (childIndex + 1 < size && array[childIndex + 1] > array[childIndex]){
                childIndex++;
            }
            // 如果父節點大于任何一個孩子的值,直接跳出
            if (temp >= array[childIndex]){
                break;
            }
            // 無須真正交換,單向賦值即可
            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * childIndex + 1;
        }
        array[parentIndex] = temp;
    }

    /**
     * 隊列擴容
     */
    private void resize(){
        // 隊列容量翻倍
        int newSize = this.size * 2;
        this.array = Arrays.copyOf(this.array, newSize);
    }

    public static void main(String[] args) throws Exception {
        // 最大優先隊列
        PriorityQueue priorityQueue = new PriorityQueue();
        priorityQueue.enQueue(3);
        priorityQueue.enQueue(5);
        priorityQueue.enQueue(10);
        priorityQueue.enQueue(2);
        priorityQueue.enQueue(7);
        System.out.println("原隊列:" + JSON.toJSONString(priorityQueue.array) + "\n出隊元素: " + priorityQueue.deQueue() +
                "\n新隊列:" + JSON.toJSONString(priorityQueue.array));
        System.out.println("原隊列:" + JSON.toJSONString(priorityQueue.array) + "\n出隊元素: " + priorityQueue.deQueue() +
                "\n新隊列:" + JSON.toJSONString(priorityQueue.array));
    }
}

<br />
#### 2.7.2.2、代碼執行結果
[圖片上傳失敗...(image-bdb22b-1599732804125)]<br />
<br />2.8、跳表<br />
<br />2.9、圖<br />
<br />2.10、Trie樹(單次查找樹/前綴樹/字典樹)<br />
<br />

<br />
# 3、10種最常見的算法
遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分值算法、回溯算法、動態規劃、字符串匹配算法<br />
<br />

    > 參考視頻:[https://time.geekbang.org/column/article/40036?gk_activity=0](https://time.geekbang.org/column/article/40036?gk_activity=0)
    > userName : 15072170772
    > pwd : alishuangji666
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。