前文 数据结构与算法——经常使用数据结构及其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树详解