前文 數據結構與算法——經常使用數據結構及其Java實現 總結了基本的數據結構,相似的,本文準備總結一下一些常見的高級的數據結構及其常見算法和對應的Java實現以及應用場景,務求理論與實踐一步到位。html
跳躍列表是對有序的鏈表增長上附加的前進連接,增長是以隨機化的方式進行的,因此在列表中的查找能夠快速的跳過部分列表。是一種隨機化數據結構,基於並聯的鏈表,其效率可比擬於紅黑樹和AVL樹(對於大多數操做須要O(logn)平均時間),可是實現起來更容易且對併發算法友好。redis 的 sorted SET 就是用了跳躍表。java
性質:node
能夠看到,這裏一共有4層,最上面就是最高層(Level 3),最下面的層就是最底層(Level 0),而後每一列中的鏈表節點中的值都是相同的,用指針來鏈接着。跳躍表的層數跟結構中最高節點的高度相同。理想狀況下,跳躍表結構中第一層中存在全部的節點,第二層只有一半的節點,並且是均勻間隔,第三層則存在1/4的節點,而且是均勻間隔的,以此類推,這樣理想的層數就是logN。mysql
所有代碼在此git
查找:
從最高層的鏈表節點開始,相等則中止查找;若是比當前節點要大和比當前層的下一個節點要小,那麼則往下找;不然在當前層繼續日後比較,以此類推,一直找到最底層的最後一個節點,若是找到則返回,反之則返回空。github
插入:
要插入,首先須要肯定插入的層數,這裏有幾種方法。1. 拋硬幣,只要是正面就累加,直到碰見反面才中止,最後記錄正面的次數並將其做爲要添加新元素的層;2. 統計機率,先給定一個機率p,產生一個0到1之間的隨機數,若是這個隨機數小於p,則將高度加1,直到產生的隨機數大於機率p才中止,根據給出的結論,當機率爲1/2或者是1/4的時候,總體的性能會比較好(其實當p爲1/2的時候,就是拋硬幣的方法)。當肯定好要插入的層數k之後,則須要將元素都插入到從最底層到第k層。redis
刪除:
在各個層中找到包含指定值的節點,而後將節點從鏈表中刪除便可,若是刪除之後只剩下頭尾兩個節點,則刪除這一層。算法
平衡二叉樹的定義都不怎麼準,即便是維基百科。我在這裏大概說一下,左右子樹高度差用 HB(k) 來表示,當 k=0 爲徹底平衡二叉樹,當 k<=1 爲AVL樹,當 k>=1 可是接近平衡的是紅黑樹,其它平衡的還有如Treap、替罪羊樹等,總之就是高度能保持在O(logn)級別的二叉樹。紅黑樹是一種自平衡二叉查找樹,也被稱爲"對稱二叉B樹",保證樹的高度在[logN,logN+1](理論上,極端的狀況下能夠出現RBTree的高度達到2*logN,但實際上很難遇到)。它是複雜的,但它的操做有着良好的最壞運行時間:它能夠在O(logn)時間內作查找,插入和刪除。sql
紅黑樹是每一個節點都帶有顏色屬性的二叉查找樹,顏色爲紅色或黑色。在二叉查找樹強制通常要求之外,有以下額外要求:數據庫
這些約束確保了紅黑樹的關鍵特性:從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長。結果是這個樹大體上是平衡的(AVL樹平衡程度更高)。由於操做好比插入、刪除和查找某個值的最壞狀況時間都要求與樹的高度成比例,這個在高度上的理論上限容許紅黑樹在最壞狀況下都是高效的,而不一樣於普通的二叉查找樹。
要知道爲何這些性質確保了這個結果,注意到性質4致使了路徑不能有兩個毗連的紅色節點就足夠了。最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。由於根據性質5全部最長的路徑都有相同數目的黑色節點,這就代表了沒有路徑能多於任何其餘路徑的兩倍長。並且插入和刪除操做都只須要<=3次的節點旋轉操做,而AVL樹可能須要O(logn)次。正是由於這種時間上的保證,紅黑樹普遍應用於 Nginx 和 Node.js 等的 timer 中,Java 8 中 HashMap 與 ConcurrentHashMap 也由於用紅黑樹取代了鏈表,性能有所提高。
class Node<T>{ public T value; public Node<T> parent; public boolean isRed; public Node<T> left; public Node<T> right; }
查找:
由於每個紅黑樹也是一個特殊的二叉查找樹,所以紅黑樹上的查找操做與普通二叉查找樹相同,可見上文,這裏再也不贅述。
然而,在紅黑樹上進行插入操做和刪除操做會致使再也不匹配紅黑樹的性質。恢復紅黑樹的性質須要少許(logn)的顏色變動(實際是很是快速的)和不超過三次樹旋轉(對於插入操做是兩次)。雖然插入和刪除很複雜,但操做時間仍能夠保持爲O(logn)。
左、右旋:
左左狀況對應右旋,右右狀況對應左旋,同AVL樹,可見上文
插入:
插入操做首先相似於二叉查找樹的插入,只是任何一個插入的新結點的初始顏色都爲紅色,由於插入黑點會增長某條路徑上黑結點的數目,從而致使整棵樹黑高度的不平衡,因此爲了儘量維持全部性質新插入節點老是先設爲紅色,但仍是可能會違返紅黑樹性質,亦即在新插入節點的父節點爲紅色節點的時候,這時就須要經過一系列操做來使紅黑樹保持平衡。破壞性質的狀況有:
1. 叔叔節點也爲紅色。 2. 叔叔節點爲空,且祖父節點、父節點和新節點處於一條斜線上。 3. 叔叔節點爲空,且祖父節點、父節點和新節點不處於一條斜線上。
一、D是新插入節點,將父節點和叔叔節點與祖父節點的顏色互換,而後D的祖父節點A變成了新插入節點,若是A的父節點是紅色則繼續調整
二、C是新插入節點,將B節點進行右旋操做,而且和父節點A互換顏色,若是B和C節點都是右節點的話,只要將操做變成左旋就能夠了。
三、C是新插入節點,將C節點進行左旋,這樣就從 3 轉換成 2了,而後針對 2 進行操做處理就好了。2 操做作了一個右旋操做和顏色互換來達到目的。若是樹的結構是下圖的鏡像結構,則只須要將對應的左旋變成右旋,右旋變成左旋便可。
若是上面的3中狀況若是對應的操做是在右子樹上,作對應的鏡像操做就是了。
刪除:
刪除操做首先相似於二叉查找樹的刪除,若是刪除的是紅色節點或者葉子則不須要特別的紅黑樹定義修復(可是須要二叉查找樹的修復),黑色節點則須要修復。刪除修復操做分爲四種狀況(刪除黑節點後):
1. 兄弟節點是紅色的。 2. 兄弟節點是黑色的,且兄弟節點的子節點都是黑色的。 3. 兄弟節點是黑色的,且兄弟節點的左子節點是紅色的,右節點是黑色的(兄弟節點在右邊),若是兄弟節點在左邊的話,就是兄弟節點的右子節點是紅色的,左節點是黑色的。 4. 兄弟節點是黑色的,且右子節點是是紅色的(兄弟節點在右邊),若是兄弟節點在左邊,則就是對應的就是左節點是紅色的。
刪除操做最複雜的操做,整體思想是從兄弟節點借調黑色節點使樹保持局部的平衡,若是局部的平衡達到了,就看總體的樹是不是平衡的,若是不平衡就接着向上追溯調整。
一、將兄弟節點提高到父節點,轉換以後就會變成後面的狀態 2,3,或者4了,從待刪除節點開始調整
二、兄弟節點能夠消除一個黑色節點,由於兄弟節點和兄弟節點的子節點都是黑色的,因此能夠將兄弟節點變紅,這樣就能夠保證樹的局部的顏色符合定義了。這個時候須要將父節點A變成新的節點,繼續向上調整,直到整顆樹的顏色符合RBTree的定義爲止
三、左邊的紅色節點借調過來,這樣就能夠轉換成狀態 4 了,3是一箇中間狀態,是由於根據紅黑樹的定義來講,下圖並非平衡的,他是經過case 2操做完後向上回溯出現的狀態。之因此會出現3和後面的4的狀況,是由於能夠經過借用侄子節點的紅色,變成黑色來符合紅黑樹定義5
四、是真正的節點借調操做,經過將兄弟節點以及兄弟節點的右節點借調過來,並將兄弟節點的右子節點變成紅色來達到借調兩個黑節點的目的,這樣的話,整棵樹仍是符合RBTree的定義的。
注意,上述4種的鏡像狀況就進行鏡像處理便可,左對右,右對左。
B樹有一種說法是二叉查找樹,每一個結點只存儲一個關鍵字,等於則命中,小於走左結點,大於走右結點,這樣的話上一篇文章就已經說過了。可是實際上這樣翻譯是一種錯誤,B樹就是 B-tree 亦即B-樹。
B-樹(B-tree)是一種自平衡的樹,可以保持數據有序。這種數據結構可以讓查找數據、順序訪問、插入數據及刪除的動做,都在對數時間內完成。B-樹,歸納來講是一個通常化的二叉查找樹,能夠擁有多於2個子節點(多路查找樹)。與自平衡二叉查找樹不一樣,B-樹爲系統大塊數據的讀寫操做作了優化。B-樹減小定位記錄時所經歷的中間過程,從而加快存取速度。B-樹這種數據結構能夠用來描述外部存儲。這種數據結構常被應用在數據庫和文件系統的實現上,好比MySQL索引就用了B+樹。
B-樹能夠看做是對二叉查找樹的一種擴展,即他容許每一個節點有M-1個子節點。
B+樹是對B-樹的一種變形樹,在B-樹基礎上,爲葉子結點增長鏈表指針,它與B-樹的差別在於:
mysql中廣泛使用B+樹作索引,但在實現上又根據聚簇索引和非聚簇索引而不一樣。所謂聚簇索引,就是指主索引文件和數據文件爲同一份文件,聚簇索引主要用在Innodb存儲引擎中。在該索引實現方式中B+Tree的葉子節點上的data就是數據自己,key爲主鍵,若是是通常索引的話,data便會指向對應的主索引。在B+Tree的每一個葉子節點增長一個指向相鄰葉子節點的指針,就造成了帶有順序訪問指針的B+Tree。作這個優化的目的是爲了提升區間訪問的性能。非聚簇索引就是指B+Tree的葉子節點上的data,並非數據自己,而是數據存放的地址。主索引和輔助索引沒啥區別,只是主索引中的key必定得是惟一的。主要用在MyISAM存儲引擎中。非聚簇索引比聚簇索引多了一次讀取數據的IO操做,因此查找性能上會差一些。
通常來講,索引自己也很大,不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級,因此評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤I/O操做次數的漸進複雜度。換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數。
B-Tree:若是一次檢索須要訪問4個節點,數據庫系統設計者利用磁盤預讀原理,把節點的大小設計爲一個頁,那讀取一個節點只須要一次I/O操做,完成此次檢索操做,最多須要3次I/O(根節點常駐內存)。數據記錄越小,每一個節點存放的數據就越多,樹的高度也就越小,I/O操做就少了,檢索效率也就上去了。
B+Tree:非葉子節點只存key,大大滴減小了非葉子節點的大小,那麼每一個節點就能夠存放更多的記錄,樹更矮了,I/O操做更少了。因此B+Tree擁有更好的性能。
Java定義:
public class BTree<Key extends Comparable<Key>, Value> { private static final int M = 4;// private Node root; // root of the B-tree private int height; // height of the B-tree private int n; // number of key-value pairs in the B-tree private static final class Node { private int m; // number of children private Entry[] children = new Entry[M]; // the array of children // create a node with k children private Node(int k) { m = k; } } private static class Entry { private Comparable key; private final Object val; private Node next; // helper field to iterate over array entries public Entry(Comparable key, Object val, Node next) { this.key = key; this.val = val; this.next = next; } } }
查找:
相似於二叉樹的查找。
public Value get(Key key) { return search(root, key, height); } private Value search(Node x, Key key, int ht) { Entry[] children = x.children; if (ht == 0) { for (int j = 0; j < x.m; j++) { if (eq(key, children[j].key)) return (Value) children[j].val; } } else { for (int j = 0; j < x.m; j++) { if (j+1 == x.m || less(key, children[j+1].key)) return search(children[j].next, key, ht-1); } } return null; }
插入:
首先要找到合適的插入位置直接插入,若是形成節點溢出就要分裂該節點,並用處於中間的key提高並插入到父節點去,直到當前插入節點不溢出爲止。
// split node in half private Node split(Node h) { Node t = new Node(M/2); h.m = M/2; for (int j = 0; j < M/2; j++) t.children[j] = h.children[M/2+j]; return t; } public void put(Key key, Value val) { if (key == null) throw new IllegalArgumentException("argument key to put() is null"); Node u = insert(root, key, val, height); n++; if (u == null) return; // need to split root Node t = new Node(2); t.children[0] = new Entry(root.children[0].key, null, root); t.children[1] = new Entry(u.children[0].key, null, u); root = t; height++; } private Node insert(Node h, Key key, Value val, int ht) { int j; Entry t = new Entry(key, val, null); // external node if (ht == 0) { for (j = 0; j < h.m; j++) { if (less(key, h.children[j].key)) break; } } // internal node else { for (j = 0; j < h.m; j++) { if ((j+1 == h.m) || less(key, h.children[j+1].key)) { Node u = insert(h.children[j++].next, key, val, ht-1); if (u == null) return null; t.key = u.children[0].key; t.next = u; break; } } } for (int i = h.m; i > j; i--) h.children[i] = h.children[i-1]; h.children[j] = t; h.m++; if (h.m < M) return null; else return split(h); }
刪除:
首先要找到節點所在位置,而後刪除,若是當前節點key數量少於M/2 則要從兄弟或者父節點借key,可是這樣維護起來麻煩,通常採起懶刪除作法,亦即不是真正的刪除,只是標記一下刪除了而已。
是B+樹的變體,在B+樹的非根和非葉子結點再增長指向兄弟的指針。
Trie(讀做try)樹又稱字典樹、單詞查找樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不只限於字符串),因此常常被搜索引擎系統用於文本詞頻統計。它的優勢是:利用字符串的公共前綴來減小查詢時間,最大限度地減小無謂的字符串比較,查詢效率比哈希樹高。Trie的核心思想是空間換時間:利用字符串的公共前綴來下降查詢時間的開銷以達到提升效率的目的。
Trie樹的基本性質:
例子:
add adbc bye
對應樹:
Java定義:
class TrieNode { char c;// 該節點的數據 int occurances;//前節點所對應的字符串在字典樹裏面出現的次數 Map<Character, TrieNode> children;//當前節點的子節點,保存的是它的下一個節點的字符 }
插入:
//新插入的字符串s,以及當前待插入的字符c在s中的位置 int insert(String s, int pos) { //若是插入空串,則直接返回 //此方法調用時從pos=0開始的遞歸調用,pos指的是插入的第pos個字符 if (s == null || pos >= s.length()) return 0; // 若是當前節點沒有孩子節點,則new一個 if (children == null) children = new HashMap<Character, TrieNode>(); //獲取待插入字符的對應節點 char c = s.charAt(pos); TrieNode n = children.get(c); if (n == null) {//當前待插入字符不存在於子節點中 n = new TrieNode(c);//新建立一個節點 children.put(c, n);//新建節點變爲子節點 } //插入的結束時直到最後一個字符插入,返回的結果是該字符串出現的次數 //不然繼續插入下一個字符 if (pos == s.length() - 1) { n.occurances++; return n.occurances; } else { return n.insert(s, pos + 1); } }
刪除:
//待刪除的字符串s,以及當前待刪除的字符c在s中的位置 boolean remove(String s, int pos) { if (children == null || s == null) return false; //取出第pos個字符,若不存在,則返回false char c = s.charAt(pos); TrieNode n = children.get(c); if (n == null) return false; //遞歸出口是已經到了字符串的最後一個字符,若occurances=0,表明已經刪除了 //不然繼續遞歸到最後一個字符 boolean ret; if (pos == s.length() - 1) { int before = n.occurances; n.occurances = 0; ret = before > 0; } else { ret = n.remove(s, pos + 1); } //刪除以後,必須刪除沒必要要的字符 //好比保存的「Harlan」被刪除了,那麼若是n保存在葉子節點,意味着它雖然被標記着不存在了,可是還佔着空間 //因此必須刪除,可是若是「Harlan」刪除了,可是Trie裏面還保存這「Harlan1994」,那麼就不須要刪除字符了 if (n.children == null && n.occurances == 0) { children.remove(n.c); if (children.size() == 0) children = null; } return ret; }
求一個字符串出現的次數:
TrieNode lookup(String s, int pos) { if (s == null) return null; //若是找的次數已經超過了字符的長度,說明,已經遞歸到超過字符串的深度了,代表字符串不存在 if (pos >= s.length() || children == null) return null; //若是恰好到了字符串最後一個,則只須要返回最後一個字符對應的結點,若節點爲空,則代表不存在該字符串 else if (pos == s.length() - 1) return children.get(s.charAt(pos)); //不然繼續遞歸查詢下去,直到沒有孩子節點了 else { TrieNode n = children.get(s.charAt(pos)); return n == null ? null : n.lookup(s, pos + 1); } }
以上kookup方法返回值是一個TrieNode,要找某個字符串出現的次數,只須要看其中的n.occurances便可。
要看是否包含某個字符串,只須要看是否爲空節點便可。
圖(Graph)是一種複雜的非線性結構,在圖中,每一個元素均可以有>=0個前驅,也能夠有>=0個後繼,也就是說,元素之間的關係是任意的。其標準定義爲:圖是由頂點的有窮非空集合和頂點之間邊的集合組成,一般表示爲:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
按照邊無方向和有方向分爲無向圖(通常做爲圖的表明)和有向圖,邊有權值就叫作加權圖,還有加權有向圖。圖的表示方法有:鄰接矩陣(VxV的布爾矩陣,很耗空間)、邊的數組(每一個邊做爲一個數組元素,實現起來須要檢查全部邊,耗時間)、鄰接表數組(一個頂點爲索引的列表數組,通常是圖的最佳表示方法)。
圖的用處很廣,好比社交網絡、計算機網絡、CG中的可達性分析、任務調度、拓補排序等等。
圖的java實現完整代碼在這,下面是部分:
public class Graph { private static final String NEWLINE = System.getProperty("line.separator"); private final int V; private int E; private Bag<Integer>[] adj; public Graph(int V) { this.V = V; this.E = 0; adj = (Bag<Integer>[]) new Bag[V]; for (int v = 0; v < V; v++) { adj[v] = new Bag<Integer>(); } } public Graph(In in) { try { this.V = in.readInt(); adj = (Bag<Integer>[]) new Bag[V]; for (int v = 0; v < V; v++) { adj[v] = new Bag<Integer>(); } int E = in.readInt(); for (int i = 0; i < E; i++) { int v = in.readInt(); int w = in.readInt(); addEdge(v, w); } } catch (NoSuchElementException e) { throw new IllegalArgumentException("invalid input format in Graph constructor", e); } } public void addEdge(int v, int w) { E++; adj[v].add(w); adj[w].add(v); } //返回頂點v的相鄰頂點 public Iterable<Integer> adj(int v) { return adj[v]; } }
public class DepthFirstSearch { private boolean[] marked; // marked[v] = is there an s-v path? private int count; // number of vertices connected to s public DepthFirstSearch(Graph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } // depth first search from v private void dfs(Graph G, int v) { count++; marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) { dfs(G, w); } } } public boolean marked(int v) { return marked[v]; } public int count() { return count; } }
深度優先能夠得到一個初始節點到另外一個頂點的路徑,可是該路徑不必定是最短的(取決於圖的表示方法和遞歸設計),廣度優先才能得到最短路徑。
public class BreadthFirstPaths { private static final int INFINITY = Integer.MAX_VALUE; private boolean[] marked; // marked[v] = is there an s-v path private int[] edgeTo; // edgeTo[v] = previous edge on shortest s-v path private int[] distTo; // distTo[v] = number of edges shortest s-v path public BreadthFirstPaths(Graph G, int s) { marked = new boolean[G.V()]; distTo = new int[G.V()]; edgeTo = new int[G.V()]; validateVertex(s); bfs(G, s); assert check(G, s); } public BreadthFirstPaths(Graph G, Iterable<Integer> sources) { marked = new boolean[G.V()]; distTo = new int[G.V()]; edgeTo = new int[G.V()]; for (int v = 0; v < G.V(); v++) distTo[v] = INFINITY; validateVertices(sources); bfs(G, sources); } // breadth-first search from a single source private void bfs(Graph G, int s) { Queue<Integer> q = new Queue<Integer>(); for (int v = 0; v < G.V(); v++) distTo[v] = INFINITY; distTo[s] = 0; marked[s] = true; q.enqueue(s); while (!q.isEmpty()) { int v = q.dequeue(); for (int w : G.adj(v)) { if (!marked[w]) { edgeTo[w] = v; distTo[w] = distTo[v] + 1; marked[w] = true; q.enqueue(w); } } } } public Iterable<Integer> pathTo(int v) { validateVertex(v); if (!hasPathTo(v)) return null; Stack<Integer> path = new Stack<Integer>(); int x; for (x = v; distTo[x] != 0; x = edgeTo[x]) path.push(x); path.push(x); return path; } }
對於有向加權圖的單點最短路徑能夠用Dijkstra算法。
樹是一個無環連通圖,最小生成樹是原圖的極小連通子圖,且包含原圖中的全部 n 個結點,而且有保持圖連通的最少的邊(若是是加權的就是權值之和最小)。最小生成樹普遍用於電路設計、航線規劃、電線規劃等領域。
以圖上的邊爲出發點依據貪心策略逐次選擇圖中最小邊爲最小生成樹的邊,且所選的當前最小邊與已有的邊不構成迴路。
代碼在這。
從任意一個頂點開始,每次選擇一個與當前頂點集最近的一個頂點,並將兩頂點之間的邊加入到樹中。Prim算法在找當前最近頂點時使用到了貪心算法。
代碼在這。
紅黑樹深刻剖析及Java實現
算法導論
算法第四版
紅黑樹 - 維基百科
紅黑樹(五)之 Java的實現
B樹、B-樹、B+樹、B*樹
B樹 - 維基百科
淺談算法和數據結構: 十 平衡查找樹之B樹
數據庫設計原理知識--B樹、B-樹、B+樹、B*樹都是什麼
B+/-Tree原理及mysql的索引分析
跳躍表原理和實現
跳躍表(Skip list)原理與java實現
Trie樹詳解