树堆
树堆(Treap)
在某些极端情况下,二叉查找树有可能退化为链表,所以前辈们想尽了各种优化策略,这就涉及到二叉树的自平衡。二叉树的自平衡方式有多种,如红黑树、AVL等,包括今天要讲的树堆(Treap)。
一、树堆的特性和原理
树堆是一种随机化平衡二叉搜索树,结合了二叉堆和二叉查找树的特性(Treap=Tree+Heap)。
实现原理很简单,在树中维护一个”优先级“,”优先级“采用随机数的方法生成,但是”优先级“必须满足二叉堆的性质,当然是“最大堆”或者“最小堆”都可以的。如下图:
从上图可以看出:
1)节点中的值满足二叉查找树特性
2)节点中的优先级满足最大堆特性
下面归纳下树堆的一些特性:
1)键值特性
树堆的每个节点包含一个键值和一个随机的优先级值。键值用于节点的比较和排序,优先级值用于平衡树的结构。
2)二叉查找树性质
树堆维护了二叉搜索树的性质,即对于任意节点,它的左子树中的所有节点的键值小于该节点的键值,右子树中的所有节点的键值大于该节点的键值。
3)堆性质
树堆也维护了堆的性质,即对于任意节点,它的优先级值大于其子节点的优先级值。
4)随机化平衡
树堆通过随机生成节点的优先级值来实现平衡。节点的优先级值和键值无关,它的随机性保证了树的平衡性质。
5)插入操作
当插入一个新节点时,树堆首先按照二叉查找树性质找到插入的位置,然后根据节点的优先级值,通过旋转操作保持堆性质和查找树性质。
6)删除操作
删除一个节点时,树堆首先根据二叉查找树性质找到要删除的节点,然后通过旋转操作将其删除,并保持堆性质和查找树性质。
由于采用最大和最小堆的效果一样,本文采用最大堆进行讨论。
二、 插入节点
当插入一个新节点时,树堆首先按照二叉查找树性质找到插入的位置,然后根据节点的优先级值,判断是否需要通过旋转操作来保持堆性质和查找树性质。
说明:我们知道各个节点的优先级是采用随机数的方法,那么就存在一个问题,当我们按照二叉查找树规则插入一个节点后,优先级有可能不满足最大堆定义。显然,此时跟维护堆一样,如果当前节点的优先级比根大就旋转,如果当前节点是根的左孩子就右旋,如果当前节点是根的右孩子就左旋。如下图所示:
上图中右旋只展示了一次,而左旋则展示了三次递归的过程。其实理解了AVL的旋转,就能很容易理解这里的旋转了。旋转代码与AVL的完全一样。
三、 删除节点
跟普通的二叉查找树一样,删除结点存在三种情况。
1. 叶子结点(待删除的节点没有子节点)
跟普通查找树一样,直接删除本节点即可。
2. 单孩子结点(待删除的节点有一个孩子)
跟普通查找树一样操作:直接让其孩子节点取代被删除的节点,孩子节点以下的节点关系无须变动。
3. 满孩子结点(待删除的节点有两个孩子)
在treap中移除满孩子结点有两种方式。
1) 第一种
普通二叉查找树一样,找到左子树的最大节点或者右子树的最小节点,然后copy元素的值,但不拷贝其优先级(以免破坏堆属性),如下图所示:
2) 第二种
第二种是采用旋转的方法:因为Treap树满足堆性质,所以只需要把要删除的节点旋转到叶子节点上,然后直接删除就可以了。具体的方法就是每次找到优先级最大的儿子,向与其相反的方向旋转,直到那个节点被旋转到了叶节点,然后直接删除。删除最多进行O(h)次旋转,期望复杂度是 O(logN)。示意图如下所示:
四、代码实现
下面用代码简单实现下:
树堆的定义:
1 public class Node { 2 int key, priority; 3 Node left, right; 4 Node(int key) { 5 this.key = key; 6 this.priority = new Random().nextInt(); 7 } 8 }
树堆的操作:
1 public class Treap { 2 Node root; 3 4 /** 5 * 左旋 6 * @param node 7 * @return 8 */ 9 private Node leftRotate(Node node) { 10 Node newRoot = node.right; 11 node.right = newRoot.left; 12 newRoot.left = node; 13 return newRoot; 14 } 15 16 17 /** 18 * 右旋 19 * @param node 20 * @return 21 */ 22 private Node rightRotate(Node node) { 23 Node newRoot = node.left; 24 node.left = newRoot.right; 25 newRoot.right = node; 26 return newRoot; 27 } 28 29 public Node insert(Node root, int key) { 30 if (root == null) { 31 return new Node(key); 32 } 33 if (key <= root.key) { 34 root.left = insert(root.left, key); 35 if (root.left.priority > root.priority) { 36 root = rightRotate(root); 37 } 38 } else { 39 root.right = insert(root.right, key); 40 if (root.right.priority > root.priority) { 41 root = leftRotate(root); 42 } 43 } 44 return root; 45 } 46 47 public void insert(int key) { 48 root = insert(root, key); 49 } 50 51 /** 52 * 中序遍历 53 * @param node 54 */ 55 private void inOrderTraversal(Node node) { 56 if (node != null) { 57 inOrderTraversal(node.left); 58 System.out.print(node.key + " "); 59 inOrderTraversal(node.right); 60 } 61 } 62 63 public void inOrderTraversal() { 64 inOrderTraversal(root); 65 } 66 67 public static void main(String[] args) { 68 Treap treap = new Treap(); 69 int[] testData = {5, 3, 7, 2, 4, 6, 8}; 70 for (int key : testData) { 71 treap.insert(key); 72 } 73 treap.inOrderTraversal(); 74 } 75 }
五、 树堆的优缺点分析
1. 优点
1)简单而高效的实现
树堆的实现相对较简单,只需要维护两个关键属性:BST的有序性和堆的优先级。这使得它易于实现和理解。
2)动态操作高效
树堆在频繁的插入和删除操作方面表现出色,因为它们利用了随机的优先级来维护树的平衡,通常不需要进行复杂的平衡调整。
3)随机性能好
在随机输入的情况下,树堆的性能通常很好,因为优先级的随机性可以保持树的平衡。
2. 缺点
1)不适用于特定的有序数据
当数据的顺序是特定的,而不是随机的时候,树堆的性能可能不如其他平衡树结构。
2)空间开销较大
每个节点需要存储两个额外的信息:优先级和子树的大小,这可能会增加内存开销。
六、树堆与红黑树的比较
1. 时间复杂度
红黑树和树堆在查找、插入和删除操作上的平均性能是相似的,都是O(logN)。这是因为它们都是自平衡的二叉搜索树,通过调整树的结构来维护平衡性,以确保这些操作的时间复杂度保持在对数级别。
2. 空间复杂度
树堆的空间复杂度通常较低,适用于需要高效的优先队列操作的场景。而红黑树的空间复杂度较高,但由于其自平衡性质,它在搜索、插入和删除等操作上具有良好的性能。
3. 实现复杂度
树堆的实现复杂度相对较低,特别是在插入和删除操作上。红黑树相对复杂一些,具有更好的平衡性,可以保证树的高度较小,从而在查找操作上表现更好。
总体来说,如果需要频繁的插入和删除操作,并且对查找操作的性能要求不是特别高,那么树堆可能是一个更好的选择,因为它们在动态插入和删除方面更有效率。如果对查找操作的性能要求较高,希望确保树的平衡性,或者需要一种稳定的数据结构,那么红黑树可能更适合,因为它们在保持平衡和查找操作上具有更好的性能保证。
参考链接:
https://blog.imallen.wang/2015/11/15/2016-07-16-treapshu-ji-javashi-xian/
https://zhuanlan.zhihu.com/p/653926922