樹形結構是很是重要的一種數據結構。咱們能夠經過平衡二叉樹來實現排序問題,用樹結構來表示源程序的語法結構,樹也能夠表示數據庫或文件系統。而且不少容器的底層都是樹結構。算法
下面先來了解關於樹結構的名詞有哪些: 數據庫
這裏只解釋了二叉樹(一個節點只有左右兩個孩子的狀況),固然樹不必定只有兩個孩子,好比下文會出現的並查集和Trie,但咱們能夠把大多數樹轉化爲二叉樹,這樣更便於讓咱們理解概念。數組
在開始二叉樹以前再來看關於二叉樹中的概念:bash
滿二叉樹:一個深度爲k且有2^k - 1個結點的二叉樹稱爲滿二叉樹網絡
徹底二叉樹:徹底二叉樹從根結點到倒數第二層知足完美二叉樹,最後一層能夠不徹底填充,其葉子結點都靠左對齊。數據結構
平衡二叉樹:每一個節點的左右子樹深度差不超過1ui
絕對平衡樹:只有最後一層是葉子節點,而且知足二分搜索樹的特色spa
若是如今有一個數組,給出一個數(這個數在數組中),要求找到這個數在這個數組的下標位置,咱們只能一個一個去遍歷查找,這個時候的時間複雜度是O(n),爲了加快查找速度,若是咱們獲得的數組是排好序的,咱們可使用折半查找法,這樣時間複雜度就爲O(logn)了,速度會很快。而折半查找的思路就是利用了二分搜索的思想。設計
先來看二分搜索樹的節點構造:3d
class Node{
int data;//節點值
Node left;//左孩子
Node right;//右孩子
}
複製代碼
節點的左子樹的全部節點小於這個節點,節點的右子樹的全部節點大於這個節點。
二分搜索樹的插入操做十分簡單:
插入的平均時間複雜度爲O(logn)
刪除的操做稍微麻煩一些,將要刪除的節點分爲三種狀況
刪除的平均時間複雜度爲O(logn)
查找和插入的方法相似,這裏就不贅述了
查找的平均時間複雜度爲O(logn)
若是想遍歷一棵二叉樹有兩種方法,深度優先遍歷和廣度優先遍歷,而深度優先遍歷又能夠分爲前序,中序,後序遍歷。
下面來看個圖來看具體的遍歷時如何的:
堆是一種徹底二叉樹,而且一個節點要大於(小於)它的左右孩子
來看堆的節點構造
class Node{
int data;//節點值
Node left;//左孩子
Node right;//右孩子
}
複製代碼
對於堆來講他更注重將最大的放在首位和堆性質的維護。這裏由於堆是一個徹底二叉樹,因此能夠用數組來保存堆中的元素。
如圖(如下使用最小堆,即每一個節點小於它的孩子節點):
那麼對於一個節點,咱們能夠很容易的找到這個節點的雙親結點和左右節點。
//得到雙親結點的下標
int getParent(int index){
return (index + 1) / 2 - 1;
}
//得到左孩子
int getLeftChild(int index){
return index * 2 + 1;
}
//得到右孩子
int getRightChild(int index){
return index * 2 + 2;
}
複製代碼
堆可以實現優先隊列,在現實生活中也有優先隊列的應用,例如排隊的時候會先讓老人和小孩到隊列前面。
徹底d叉樹,根最小。能夠想成原來咱們的堆是二叉堆,這個d能夠爲2,3,4
以上咱們討論的堆是比較每一個節點的data,若是這個data是一種很龐大的數據結構,那麼會很耗時。這時咱們能夠用索引堆,在原來堆的基礎上用一個索引數組來存儲數據元素的位置,即索引堆裏面包含兩個數組
二項堆是二項樹的集合。二項樹也是一種樹結構,二項樹的第K棵樹有2k個結點,高度是k;深度爲d的結點共有個結點。二項堆就由一組二項樹所構成。
還有斐波那契堆,Pairing堆等等。
線段樹也是一種平衡二叉樹,可是它節點和平衡二叉樹的節點有些不一樣,它的每一個節點能夠看做是由一段數組組成,若是一個節點的數組是[l,r]而他的左孩子和它的右孩子值就是數組空間爲[l,mid]和[mid+1,r]的值(這裏值的意思是[left,right]按照本身的意願得到的樹,能夠輸left*right,也能夠是left+right),mid通常取作l+(r-l)/2
看一下線段樹的關鍵字段:
//tree表示線段樹中的全部節點,和層次遍歷的順序同樣,和堆的物理結構同樣
//上圖中tree[0]就是0-7的值,tree[3]就是0-1的值
private int[] tree;
複製代碼
線段樹可以解決一些算法問題,例如
若是用線段樹這種結構就有一個方便的思路解決以上問題
這裏假設咱們要求求解[1,7]的值咱們直接獲取tree[8]+tree[4]+tree[2]+便可,即[1,1]+[2,3]+[4,7]就獲得[1,7]的值。若是將0-7採用二分搜索樹的方法,並從1加到7那個就是7*O(logn)的複雜度。
線段樹主要針對於查詢和修改,至於增長和刪除咱們不討論。
這裏直接看若是須要查詢一段區間該如何操做(懂了區間查詢以後單個元素查詢也會簡單一些)
這裏直接放一個例子來理解簡單一些,例如如今我要查詢[1,7]的值,讓查找的值爲x返回:
[2,3]的值就是tree[4],[4,7]的值就是tree[2],[1,1]的值就是tree[8]
大概流程就是上面所述,可能一開始看有點晦澀,但結合代碼來看使用遞歸仍是挺清晰的
修改的操做和查詢的思路同樣的,不過要對遍歷過的節點進行修改,這裏就不贅述了
實際上,可以用線段樹解決的問題都能
字典樹不是二叉樹,和它的名字同樣,首先來看他的節點結構(根節點是不包含字符的,根節點的c是空)
class Node{
char c;//當前節點的字符
Node[] next;//當前節點的下一個節點
boolean end;//判斷這個字母是否是一個結尾
}
複製代碼
咱們能夠發現字典樹的每一個節點由若干個指向下一節點的指針,總體字典樹的結構以下圖:
能夠看到上面的圖用深度優先中序遍歷的話他每遍歷到葉子節點都是一個單詞,如and,as,at,cn,com。
在之前的電話簿系統中,若是這時咱們用二分搜索樹去保存每一個聯繫人的信息,假設咱們的通信錄有一千萬個聯繫人信息,那麼咱們經過名字去查詢這個聯繫人的信息是很是費時的O(logn)。而若是咱們用字典樹,好比咱們要查詢一個名叫and的聯繫人,咱們經過根節點在next中找a,在a節點中找n,再在n節點中找b(若是這裏全是英文字母那麼next也只有26種狀況),這時咱們的時間複雜度只和這個單詞的長度有關O(K),K爲單詞長度。那麼可見這種狀況下字典樹的優點很是明顯。固然,字典樹的這種設計也就是典型的用空間換取時間的思路。
字典樹的操做主要爲增,刪,查
在開始以前,解釋一下end的做用,例如咱們有一個as和一個ass(壞笑.jpg),咱們如何判斷ass這個單詞是隻有ass仍是還有as這個單詞呢?咱們須要對每個節點進行一個標識,標識這個單詞是否在這裏是一個完整的單詞。例如ass,最後一個s中end就要爲true,由於ass最後的s是ass的結尾。而若是還有個as單詞,那麼第一個s就要爲true來表明as這個單詞以s結束。
增長的邏輯就很簡單了,例如增長一個geek
例如上圖中咱們要刪除an這個單詞
例如如今咱們要查找and
並查集是一種樹形結構,又叫「不相交集合」,保持了一組不相交的動態集合,每一個集合經過一個表明來識別,表明即集合中的某個成員,一般選擇根作這個表明。
能夠解決鏈接問題,網絡中節點鏈接狀態,路徑問題。
舉個例子,例如咱們有若干個城鎮,不一樣城鎮之間可能會有相連的道路,將這些城鎮當作節點,而後判斷哪些城鎮之間是能夠通路的。
先看左邊的圖,也就是合併前的圖,咱們能夠說d和c是連通的,g和e是連通的,c和f是不相通的。
若是這時咱們想在b城鎮和f城鎮之間連通,這時,咱們不能直接將b和f相連,應該讓f的根節點加到d的根節點下,那麼這裏f的根節點是e,b的根節點是a,也就是直接讓a成爲e的雙親節點,如圖。
咱們能夠看到並查集的邏輯結構是一種樹結構,但每一個節點有的只是本身的父親節點。
由於並查集主要是來解決路徑查找問題的,因此相應的對並查集就須要一個操做isConnect來判斷兩個節點是否相連。若是兩個節點以後能夠連通,那麼就須要union操做將兩個節點連通
boolean isConnect(Node p, Node q)
經過上面的分析咱們知道了判斷節點是否相連主要比較的是根節點,因此只要獲得p的根節點和q的根節點再判斷兩個節點是否相同便可
void unionElements(Node p, Node q)
和一開始分析的時候同樣,咱們能夠先求得p節點的根節點,再求得q節點的根節點,讓q節點的根節點的雙親結點指向p節點的根節點便可。
有可能出現全部的節點都在一條鏈上,而咱們能夠將整棵樹弄成只有兩層。如圖
下一章將會總結AVL,紅黑樹,B+樹