数据结构与算法——常用高级数据结构及其Java实现
前文 数据结构与算法——常用数据结构及其Java实现 总结了基本的数据结构,类似的,本文准备总结一下一些常见的高级的数据结构及其常见算法和对应的Java实现以及应用场景,务求理论与实践一步到位。
跳跃表
跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表。是一种随机化数据结构,基于并联的链表,其效率可比拟于红黑树和AVL树(对于大多数操作需要O(logn)平均时间),但是实现起来更容易且对并发算法友好。redis 的 sorted SET 就是用了跳跃表。
性质:
- 由很多层结构组成;
- 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
- 最底层的链表包含了所有的元素;
- 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
- 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
可以看到,这里一共有4层,最上面就是最高层(Level 3),最下面的层就是最底层(Level 0),然后每一列中的链表节点中的值都是相同的,用指针来连接着。跳跃表的层数跟结构中最高节点的高度相同。理想情况下,跳跃表结构中第一层中存在所有的节点,第二层只有一半的节点,而且是均匀间隔,第三层则存在1/4的节点,并且是均匀间隔的,以此类推,这样理想的层数就是logN。
查找:
从最高层的链表节点开始,相等则停止查找;如果比当前节点要大和比当前层的下一个节点要小,那么则往下找;否则在当前层继续往后比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
插入:
要插入,首先需要确定插入的层数,这里有几种方法。1. 抛硬币,只要是正面就累加,直到遇见反面才停止,最后记录正面的次数并将其作为要添加新元素的层;2. 统计概率,先给定一个概率p,产生一个0到1之间的随机数,如果这个随机数小于p,则将高度加1,直到产生的随机数大于概率p才停止,根据给出的结论,当概率为1/2或者是1/4的时候,整体的性能会比较好(其实当p为1/2的时候,就是抛硬币的方法)。当确定好要插入的层数k以后,则需要将元素都插入到从最底层到第k层。
删除:
在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
红黑树
平衡二叉树的定义都不怎么准,即使是维基百科。我在这里大概说一下,左右子树高度差用 HB(k) 来表示,当 k=0 为完全平衡二叉树,当 k<=1 为AVL树,当 k>=1 但是接近平衡的是红黑树,其它平衡的还有如Treap、替罪羊树等,总之就是高度能保持在O(logn)级别的二叉树。红黑树是一种自平衡二叉查找树,也被称为"对称二叉B树",保证树的高度在[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。它是复杂的,但它的操作有着良好的最坏运行时间:它可以在O(logn)时间内做查找,插入和删除。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,有如下额外要求:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点,亦即空节点)。
- 每个红色节点的子节点必须是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的(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. 叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上。
1、D是新插入节点,将父节点和叔叔节点与祖父节点的颜色互换,然后D的祖父节点A变成了新插入节点,如果A的父节点是红色则继续调整
2、C是新插入节点,将B节点进行右旋操作,并且和父节点A互换颜色,如果B和C节点都是右节点的话,只要将操作变成左旋就可以了。
3、C是新插入节点,将C节点进行左旋,这样就从 3 转换成 2了,然后针对 2 进行操作处理就行了。2 操作做了一个右旋操作和颜色互换来达到目的。如果树的结构是下图的镜像结构,则只需要将对应的左旋变成右旋,右旋变成左旋即可。
如果上面的3中情况如果对应的操作是在右子树上,做对应的镜像操作就是了。
删除:
删除操作首先类似于二叉查找树的删除,如果删除的是红色节点或者叶子则不需要特别的红黑树定义修复(但是需要二叉查找树的修复),黑色节点则需要修复。删除修复操作分为四种情况(删除黑节点后):
1. 兄弟节点是红色的。 2. 兄弟节点是黑色的,且兄弟节点的子节点都是黑色的。 3. 兄弟节点是黑色的,且兄弟节点的左子节点是红色的,右节点是黑色的(兄弟节点在右边),如果兄弟节点在左边的话,就是兄弟节点的右子节点是红色的,左节点是黑色的。 4. 兄弟节点是黑色的,且右子节点是是红色的(兄弟节点在右边),如果兄弟节点在左边,则就是对应的就是左节点是红色的。
删除操作最复杂的操作,总体思想是从兄弟节点借调黑色节点使树保持局部的平衡,如果局部的平衡达到了,就看整体的树是否是平衡的,如果不平衡就接着向上追溯调整。
1、将兄弟节点提升到父节点,转换之后就会变成后面的状态 2,3,或者4了,从待删除节点开始调整
2、兄弟节点可以消除一个黑色节点,因为兄弟节点和兄弟节点的子节点都是黑色的,所以可以将兄弟节点变红,这样就可以保证树的局部的颜色符合定义了。这个时候需要将父节点A变成新的节点,继续向上调整,直到整颗树的颜色符合RBTree的定义为止
3、左边的红色节点借调过来,这样就可以转换成状态 4 了,3是一个中间状态,是因为根据红黑树的定义来说,下图并不是平衡的,他是通过case 2操作完后向上回溯出现的状态。之所以会出现3和后面的4的情况,是因为可以通过借用侄子节点的红色,变成黑色来符合红黑树定义5
4、是真正的节点借调操作,通过将兄弟节点以及兄弟节点的右节点借调过来,并将兄弟节点的右子节点变成红色来达到借调两个黑节点的目的,这样的话,整棵树还是符合RBTree的定义的。
注意,上述4种的镜像情况就进行镜像处理即可,左对右,右对左。
B树相关
B树有一种说法是二叉查找树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点,这样的话上一篇文章就已经说过了。但是实际上这样翻译是一种错误,B树就是 B-tree 亦即B-树。
B-树
B-树(B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B-树,概括来说是一个一般化的二叉查找树,可以拥有多于2个子节点(多路查找树)。与自平衡二叉查找树不同,B-树为系统大块数据的读写操作做了优化。B-树减少定位记录时所经历的中间过程,从而加快存取速度。B-树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上,比如MySQL索引就用了B+树。
B-树可以看作是对二叉查找树的一种扩展,即他允许每个节点有M-1个子节点。
- 根节点至少有两个子节点
- 每个节点有M-1个key,并且以升序排列
- 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
- 其它节点至少有M/2个子节点,至多M个,非叶子结点存储指向关键字范围的子结点,所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
B+树
B+树是对B-树的一种变形树,在B-树基础上,为叶子结点增加链表指针,它与B-树的差异在于:
- 有k个子结点的结点必然有k个关键码
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中,非叶子结点相当于是叶子结点(包含所有关键字)的索引(稀疏索引),叶子结点才是存储(关键字)数据的数据层。所以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+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
Trie树
Trie(读作try)树又称字典树、单词查找树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。Trie的核心思想是空间换时间:利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树的基本性质:
- 每个节点最多包含R个子节点(R为字母表的大小,又称为R向单词查找树)
- 根节点不包含字符,除根节点意外每个节点只包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符串不相同。
例子:
add adbc bye
对应树:
Java定义:
class TrieNode { char c;// 该节点的数据 int occurances;//前节点所对应的字符串在字典树里面出现的次数 Map<Character, TrieNode> children;//当前节点的子节点,保存的是它的下一个节点的字符 }
插入:
- 从头到尾遍历字符串的每一个字符
- 从根节点开始插入,若该字符存在,那就不用插入新节点,要是不存在,则插入新节点
- 然后顺着插入的节点一直按照上述方法插入剩余的节点
- 为了统计每一个字符串出现的次数,应该在最后一个节点插入后occurances++,表示这个字符串出现的次数增加一次
//新插入的字符串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); } }
删除:
- 从root结点的孩子开始(因为每一个字符串的第一个字符肯定在root节点的孩子里),判断该当前节点是否为空,若为空且没有到达所要删除字符串的最后一个字符,则不存在该字符串。若已经到达叶子结点但是并没有遍历完整个字符串,说明整个字符串也不存在,例如要删除的是'harlan1994',而有'harlan'.
- 只有当要删除的字符串找到时并且最后一个字符正好是叶子节点时才需要删除,而且任何删除动作,只能发生在叶子节点。例如要删除'byebye',但是字典里还有'byebyeha',说明byebye不需要删除,只需要更改occurances=0即可标志字典里已经不存在'byebye'这个字符串了
- 当遍历到最后一个字符时,也就是说字典里存在该字符,必须将当前节点的occurances设为0,这样标志着当前节点代表的这个字符串已经不存在了,而要不要删除,需要考虑2中所提到的情况,也就是说,只有删除只发生在叶子节点上。
//待删除的字符串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 个结点,并且有保持图连通的最少的边(如果是加权的就是权值之和最小)。最小生成树广泛用于电路设计、航线规划、电线规划等领域。
kruskal算法
以图上的边为出发点依据贪心策略逐次选择图中最小边为最小生成树的边,且所选的当前最小边与已有的边不构成回路。
代码在这。
prim算法
从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。Prim算法在找当前最近顶点时使用到了贪心算法。
代码在这。
参考与感谢
红黑树深入剖析及Java实现
算法导论
算法第四版
红黑树 - 维基百科
红黑树(五)之 Java的实现
B树、B-树、B+树、B*树
B树 - 维基百科
浅谈算法和数据结构: 十 平衡查找树之B树
数据库设计原理知识--B树、B-树、B+树、B*树都是什么
B+/-Tree原理及mysql的索引分析
跳跃表原理和实现
跳跃表(Skip list)原理与java实现
Trie树详解