目录
二叉检索树
定义
代码实现
二叉树的平衡
优先队列
概述
实现
本文需要对树结构有个基础的认识,如有需要可以看
哈夫曼编码树
引用:3.3哈夫曼编码树 (xjtuse-guide.github.io)
基本定义
路径长度:两个结点之间路径上的分支数
树的外部路径长度:各叶结点到根结点的路径长度之和
树的内部路径长度:各非叶结点到根结点的路径长度之和
树的带权路径长度:树中所有叶子结点的带权路径长度之和
说的通俗一些,就是我们对树中的元素访问的频率不同,比如字母a用的多,v用的少,这个使用频率就是一个权值,访问频率大的元素,我们更想把它放在“浅”的地方,而很少访问的元素,我们就可以把它放在“深”一点的地方,也就是路径长度大的地方,用加权计算可以得到左后的带权路径长度。
而我们的哈夫曼编码树就是希望通过计算机生成一个最优排布,让树的带权路径长度最短,相对来说访问效率也就最快
代码分析
eg.访问速度的缩短比较显然,下面的例子体现哈夫曼树的其他优势
利用Huffman树的特性为使用频率不同的字符编写不等长的编码,从而缩短整个文件的长度
This is isinglass
■ t的频度是1 h的频度是1 i的频度是4 a的频度是1
s的频度是5 n的频度是1 g的频度是1 I的频度是1
如果采用等长的编码形式,上面的八个字母则需要三位二进制编码 长度=15*3=45
按照上面字母出现的频度创建一个Huffman树
我们需要完成两件事,第一是定义二叉树的节点信息(包含权值),第二则是构建哈夫曼树
构建节点数据类型,letter和HuffTreeNode类放一起,难度相对不高
class Letter { char element;//字母 double weight;//字母出现的频率 public Letter(char element, double weight) { this.element = element; this.weight = weight; } //get和set一类的省略了 } class HuffTreeNode { Letter letter; HuffTreeNode left;//左子结点 HuffTreeNode right;//右子结点 public Letter getLetter() { return letter;} public void setLetter(Letter letter) { this.letter = letter; } public HuffTreeNode getLeft() { return left; } public void setLeft(HuffTreeNode left) {this.left = left; } public HuffTreeNode getRight() { return right; } public void setRight(HuffTreeNode right) {this.right = right;} }
下面是哈夫曼树的生成过程,用文字描述就是
1.将两个访问频率最小的节点(x,y)挂在一个新节点z下,z的访问频率等于二者之和
2.将z加入排序中,和其他的节点放在一起(剔除x,y),重复1,2过程
3.当结点序列中只剩下一个结点,它的访问频率是100%,这就是树的root。
public class HuffmanTree { //简单使用冒泡排序 private void sort(HuffTreeNode[] nodes) { int flags = 0; for (int i = 0; i < nodes.length-1; i++) { for (int j = 0; j < nodes.length-1-i; j++) { if (nodes[j].letter.weight > nodes[j + 1].letter.weight) { HuffTreeNode temp = nodes[j]; nodes[j] = nodes[j + 1]; nodes[j + 1] = temp; flags = 1;//不是有序的,flags设置为1; } } if (flags == 0) return; } } public HuffTreeNode generateHuffTree(Letter[] letters) { HuffTreeNode[] nodes = new HuffTreeNode[letters.length]; for (int i = 0; i < letters.length; i++) { nodes[i] = new HuffTreeNode(); nodes[i].letter = letters[i]; }//初始化数组 while (nodes.length > 1) { sort(nodes); HuffTreeNode node1 = nodes[0]; HuffTreeNode node2 = nodes[1]; HuffTreeNode newTree = new HuffTreeNode(); Letter temp = new Letter('0',node1.getLetter().getWeight()+node2.getLetter().getWeight()); newTree.setLetter(temp); newTree.setLeft(node1); newTree.setRight(node2); //完成一次子树生成 HuffTreeNode[] nodes2 = new HuffTreeNode[nodes.length - 1];//新的结点数组,长度减一 for (int i = 2; i < nodes.length; i++) { nodes2[i - 2] = nodes[i]; } nodes2[nodes2.length - 1] = newTree; //新的结点数组初始化完成 nodes = nodes2; } return nodes[0]; } //下面还有部分
截止到这里,我们已经完成了对哈夫曼树的构建和生成,下面将用一种输出方法将结果更好的表现出来,而这种输出方法也更好的说明了哈夫曼树体现出优势的核心原因
public void print(HuffTreeNode root,String code){ if(root != null) { print(root.getLeft(),code+"0"); print(root.getRight(),code+"1"); if(root.getLeft() == null && root.getRight() == null) { String m=root.getLetter().getElement()+"频数:"+root.getLetter().getWeight()+" 哈夫曼编码:"+code; System.out.println(m); } } } public static void main(String[] args) { Letter a = new Letter('a', 1); Letter g = new Letter('g', 1); Letter h = new Letter('h', 1); Letter l = new Letter('l', 1); Letter n = new Letter('n', 1); Letter t = new Letter('t', 1); Letter i = new Letter('i', 4); Letter s = new Letter('s', 5); Letter[] test = {a, g, h, l, n, t, i, s}; HuffmanTree huffmanTree = new HuffmanTree(); huffmanTree.print(huffmanTree.generateHuffTree(test),""); } }
结果如下,每个字母占用的bits位大大减少了,空间压力减少了很多
n频数:1.0 哈夫曼编码:000 t频数:1.0 哈夫曼编码:001 i频数:4.0 哈夫曼编码:01 a频数:1.0 哈夫曼编码:1000 g频数:1.0 哈夫曼编码:1001 h频数:1.0 哈夫曼编码:1010 l频数:1.0 哈夫曼编码:1011 s频数:5.0 哈夫曼编码:11
分析总结
哈夫曼树(Huffman Tree),也称为最优二叉树(Optimal Binary Tree),是一种用于数据压缩的树形结构。它由David A. Huffman于1952年提出,并被广泛应用于数据压缩算法中,如Huffman压缩算法。
哈夫曼树的特点是,树中具有较小权值的节点离根节点较近,而具有较大权值的节点离根节点较远。这样可以通过赋予频率高的字符较短的编码,从而实现对数据的高效压缩。
哈夫曼树的构建过程如下:
- 创建一个包含所有字符及其对应权值的叶子节点集合。
- 从叶子节点集合中选择两个权值最小的节点作为左右子节点,创建一个新的父节点,其权值为两个子节点的权值之和。
- 将新创建的父节点加入到节点集合中,并从节点集合中移除已经使用的子节点。
- 重复步骤2和步骤3,直到只剩下一个节点,即根节点,构建完成哈夫曼树。
构建完成的哈夫曼树具有以下性质:
- 所有叶子节点都代表输入数据中的字符。
- 所有非叶子节点都是由两个子节点合并而来。
- 树的根节点具有最大的权值。
在哈夫曼树中,每个字符都可以通过根节点到达叶子节点的路径来进行编码。路径上的左分支表示编码为0,右分支表示编码为1。由于权值较小的节点离根节点较近,所以频率高的字符具有较短的编码,而频率低的字符具有较长的编码。
哈夫曼树在数据压缩中的应用是通过将字符映射为对应的哈夫曼编码,将原始数据中的每个字符替换为对应的编码,从而实现数据的压缩存储。解压缩时,根据哈夫曼编码的映射关系,将编码逐个解析为原始的字符
二叉检索树
引用:3.4二叉检索树 (xjtuse-guide.github.io)
定义
二叉搜索树(Binary Search Tree,BST)是一种基于二叉树的数据结构,具有以下性质:
二叉检索树的任何一个结点,设其值为K,则该结点左子树中任意一个结点的值都小于K;该结点右子树中任意一个结点的值都大于或等于K。
- 对于二叉搜索树的每个节点,其左子树和右子树都是二叉搜索树。
代码实现
对于BST,其接口定义如下(知识点预告)
public interface BSTADT <K extends Comparable<K>, V> { public void insert(K key, V value);//插入结点 public V remove(K key);//根据key值删除结点 public boolean update(BinNode<K, V> rt, K key, V value);//更新结点值 public V search(K key);//搜索key所对应结点的value public int getHeight(BinNode<K, V> rt);//获得树高 public boolean isEmpty();//判断是否为空 public void clear();//清空树 }
这里针对<K,V>增加一些补充说明
泛型类型参数K和V在
BinNode
类中用于指定节点的键和值的类型。通过使用泛型,可以在定义BinNode
类时延迟指定具体的键和值的类型,使得BinNode
类在不同场景下可以适应不同类型的数据。如果你直接定义一个
BinNode
类而不使用泛型,那么节点的键和值的类型将被具体化,即在定义类时就要明确指定键和值的具体类型。例如,如果你定义一个BinNode
类如下:public class BinNode { private int key; private String value; private BinNode left; private BinNode right; // 省略其他代码 }
在这种情况下,节点的键类型将始终为
int
,值类型将始终为String
。这意味着你不能将不同类型的键和值存储在同一个BinNode
对象中。而使用泛型类型参数K和V的
BinNode
类允许你在创建对象时指定键和值的具体类型,例如:BinNode<Integer, String> node = new BinNode<>(10, "Hello");
在这个例子中,
BinNode
的键类型被指定为Integer
,值类型被指定为String
,因此node
对象的键是整数类型,值是字符串类型。这里的K,V,rt,Binnode都只是参数名称,都是可以改的,随便写什么 让这两个东西表示为泛型的关键是<>,其中的变量为泛型
也就是说,针对同一个类,我们可以在构造的时候改变其中参数的类型,可以提高代码的灵活性和重用性,使得
BinNode
类可以适应不同类型的数据。
定义的接口暂时搁置,下面我们还需要对树上的结点进行定义,需要注意的是二叉检索树本质上是一个有序数组,在定义的时候需要关注增减逻辑
构建Javabean,这里依旧将get和set省略
public class BinNode<K extends Comparable<K>, V> { private K key; private V value; private BinNode<K, V> left; private BinNode<K, V> right; public BinNode(K key, V value){ left = right = null; this.key=key; this.value=value; } public boolean isLeaf() { return left == null && right == null; } }
接下来实现查找功能,这部分逻辑类似二分查找
public V search(K key) { return search(root, key); } private V search(BinNode<K, V> rt, K key) { if (key == null) throw new IllegalArgumentException("key is null"); // 检查参数合法性 if (rt == null) return null; // 树为空,未找到键 int cmp = key.compareTo(rt.getKey()); if (cmp < 0) return search(rt.getLeft(), key); // 小于当前键值,继续向左子树查找 else if (cmp > 0) return search(rt.getRight(), key); // 大于当前键值,继续向右子树查找 else return rt.getValue(); // 键相等,找到节点并返回值 // 注意:不需要捕获异常和打印异常信息 }
当找不到该值的时候,直接返回Null,方便实践中进一步处理,当然我们也可以选择直接通过try-catch将找不到value的情况作为错误抛出。
查找和删除最小的元素
private K getMinNode(BinNode<K, V> rt) { if (rt.getLeft() == null) return rt.getKey(); else return getMinNode(rt.getLeft()); } //返回的是更新以后的根结点 private BinNode<K, V> removeMinNode(BinNode<K, V> rt) { if (rt.getLeft() == null) { return rt.getRight(); }//最后一个结点的右结点,为空则返回空,否则返回结点 rt.setLeft(removeMinNode(rt.getLeft()));//不断递归更新 //保证了二叉检索树的规范性 return rt; }
在二叉检索树中插入和删除元素
public void insert(K key, V value) { try { if (key == null) { throw new Exception("Key is null, insert fault!"); } if (value == null) { throw new Exception("Value is null, insert fault!."); } } catch (Exception e) { e.printStackTrace(); } root = insertHelp(root, key, value); } private BinNode<K, V> insertHelp(BinNode<K, V> rt, K key, V value) { if (rt == null) { return new BinNode<K, V>(key, value); } if (key.compareTo(rt.getKey()) < 0) { rt.setLeft(insertHelp(rt.getLeft(), key, value)); }//跟查找结点有异曲同工之妙 else if (key.compareTo(rt.getKey()) > 0) { rt.setRight(insertHelp(rt.getRight(), key, value)); } else { rt.setValue(value); } return rt; }
若删除结点有两个结点,则比较特殊,要考虑二叉检索树的结构不被破坏。我们采用的方法是:
为了在保持二叉搜索树的有序性的同时删除该节点,我们可以选择右子树中的最小节点(或者左子树中的最大节点)来替换待删除的节点。这个替换节点一定没有左子节点,因此可以直接删除它。然后,将替换节点的键和值复制到待删除节点的位置,并进一步递归删除替换节点。
/** * * @param key 关键字 * @return 删除的结点的值 */ public V remove(K key) { removeValue = null; try { if (key == null) throw new Exception("Key is null, remove failure"); root = removeHelp(root, key); return removeValue; } catch (Exception e) { e.printStackTrace(); return null; } } private BinNode<K, V> removeHelp(BinNode<K, V> rt, K key) { if (rt == null) return null; if (key.compareTo(rt.getKey()) < 0) { rt.setLeft(removeHelp(rt.getLeft(), key)); } else if (key.compareTo(rt.getKey()) > 0) { rt.setRight(removeHelp(rt.getRight(), key)); } else { if (rt.getLeft() == null) { removeValue = rt.getValue(); rt = rt.getRight(); //左子结点为空,直接将右子结点作为当前根结点 } else if (rt.getRight() == null) { removeValue = rt.getValue(); rt = rt.getLeft(); //右子结点为空,直接将左子结点作为当前根结点 } else { //待删除结点有两个子结点 rt.setKey((K) getMinNode(rt.getRight()).getKey()); rt.setValue((V) getMinNode(rt.getRight()).getValue()); //将当前结点的key和value更新为右子树中的最小结点的值 rt.setRight(removeMinNode(rt.getRight())); //将当前结点的右子结点进行更新 } } return rt; }
关于“将右子树中的最小节点替换待删除的节点”具体步骤:
在待删除节点的右子树中找到最小的节点。最小节点是指右子树中最左边的节点。
将最小节点的键和值复制到待删除节点。
删除最小节点。由于最小节点没有左子节点,所以可以用removeMinNode进行操作
源代码
public class BinarySearchTree<K extends Comparable<K>, V> implements BSTADT<K, V> { private BinNode<K, V> root; private V removeValue; public BinarySearchTree() { root = null; } public BinNode<K, V> getRoot() { return root; } public void insert(K key, V value) { try { if (key == null) { throw new Exception("Key is null, insert fault!"); } if (value == null) { throw new Exception("Value is null, insert fault!."); } } catch (Exception e) { e.printStackTrace(); } root = insertHelp(root, key, value); } private BinNode<K, V> insertHelp(BinNode<K, V> rt, K key, V value) { if (rt == null) { return new BinNode<K, V>(key, value); } if (key.compareTo(rt.getKey()) < 0) { rt.setLeft(insertHelp(rt.getLeft(), key, value)); }//跟删除结点有异曲同工之妙 else if (key.compareTo(rt.getKey()) > 0) { rt.setRight(insertHelp(rt.getRight(), key, value)); } else { rt.setValue(value); }//key值相同则更新 return rt; } /** * * @param key 关键字 * @return 删除的结点的值 */ public V remove(K key) { removeValue = null; try { if (key == null) throw new Exception("Key is null, remove failure"); root = removeHelp(root, key); return removeValue; } catch (Exception e) { e.printStackTrace(); return null; } } private BinNode<K, V> removeHelp(BinNode<K, V> rt, K key) { if (rt == null) return null; if (key.compareTo(rt.getKey()) < 0) { rt.setLeft(removeHelp(rt.getLeft(), key)); } else if (key.compareTo(rt.getKey()) > 0) { rt.setRight(removeHelp(rt.getRight(), key)); } else { if (rt.getLeft() == null) { removeValue = rt.getValue(); rt = rt.getRight(); //左子结点为空,直接将右子结点作为当前根结点 } else if (rt.getRight() == null) { removeValue = rt.getValue(); rt = rt.getLeft(); //右子结点为空,直接将左子结点作为当前根结点 } else { //待删除结点有两个子结点 rt.setKey((K) getMinNode(rt.getRight()).getKey()); rt.setValue((V) getMinNode(rt.getRight()).getValue()); //将当前结点的key和value更新为右子树中的最小结点的值 rt.setRight(removeMinNode(rt.getRight())); //将当前结点的右子结点进行更新 } } return rt; } private BinNode getMinNode(BinNode<K, V> rt) { if (rt.getLeft() == null) return rt; else return getMinNode(rt.getLeft()); } //返回的是更新以后的根结点 private BinNode<K, V> removeMinNode(BinNode<K, V> rt) { if (rt.getLeft() == null) { return rt.getRight(); } rt.setLeft(removeMinNode(rt.getLeft())); //保证了二叉检索树的规范性 return rt; } public V search(K key) { return search(root, key); } private V search(BinNode<K, V> rt, K key) { try { if (key == null) throw new Exception("key is null"); if (rt == null) return null; if (key.compareTo(rt.getKey()) < 0) return search(rt.getLeft(), key);//小于当前key值则往左子树查找 if (key.compareTo(rt.getKey()) > 0) return search(rt.getRight(), key);//大于当前key值则往右子树查找 return rt.getValue();//找到值 } catch (Exception e) { e.printStackTrace(); return null; } } public boolean update(K key, V value) { return update(root, key, value); } public boolean update(BinNode<K, V> rt, K key, V value) { try { if (key == null) throw new Exception("Key is null, update failure."); if (value == null) throw new Exception("value is null, update failure"); if (key.compareTo(rt.getKey()) == 0) { rt.setValue(value); return true; } if (key.compareTo(rt.getKey()) < 0) return update(rt.getLeft(), key, value); if (key.compareTo(rt.getKey()) > 0) return update(rt.getRight(), key, value); return false; } catch (Exception e) { e.printStackTrace(); return false; } } public boolean isEmpty() { return root == null; } public void clear() { root = null; } public int getHeight(BinNode<K, V> rt) { int height = 0; if (rt == null) return 0; else height++; height += Math.max(getHeight(rt.getLeft()), getHeight(rt.getRight())); return height; } }
二叉树的平衡
平衡性
平衡因子:左右子树的高度(深度)之差
只要平衡因子的绝对值小于等于1,就说明这棵树是平衡的
对于二叉检索树,如果给定的元素序列顺序性好,则平衡性很差
当元素按照顺序插入到树中时,每次插入的元素都比前一个元素大。这样,新插入的元素都会成为当前树中最大的元素,并被放置在已有节点的一侧。
例如,一个二叉搜索树,元素按照升序顺序插入。每次插入的元素都比之前的元素大,导致树的形状变得类似于链表,所有节点都被放置在右子树上。这种情况下,树的高度将达到最大值,树的平衡性较差
如何解决
- 使用平衡二叉搜索树(例如AVL树、红黑树等)。这些树在插入和删除元素时会自动进行平衡调整,以保持树的平衡性。(不做展开)
- 通过旋转来解决平衡因子被破坏的情况,常用的旋转操作包括左旋和右旋。这些操作可以应用于特定的节点,以修复平衡因子被破坏的子树。
- 其他调整策略,如插入和删除操作时的节点颜色变换、双旋转等。(不做展开)
首先我们区分树不平衡的四种情况,死记硬背就好(图片来源见文章开头)
其中LL调整和RR调整只需要进行一次旋转,LR和RL则需要进行2次旋转,至于为什么需要两次旋转,可以参考下图 图片来源http://t.csdnimg.cn/XRWd6
可以看到旋转一次解决不了问题,需要一些更加奇妙的方法
至于问什么选择结点“7”来旋转,需阅读一下上文引用
左旋核心代码
TreeNode newRoot = node.right;
//选中需要旋转的点
node.right = newRoot.left;
//把干扰旋转的点纳入自己的名下
//把newRoot的旧小弟变成自己的小弟
newRoot.left = node;
//成为newRoot的新小弟代码部分我做一个补充,毕竟咱是JAVA(只是简单写了左右旋的逻辑,具体怎么旋,在哪个结点旋转------参考大佬吧)
class TreeNode { int val; TreeNode left; TreeNode right; public TreeNode(int val) { this.val = val; left = null; right = null; } } public class BinaryTreeRotation { public static TreeNode leftRotate(TreeNode root, TreeNode node) { if (node == null || node.right == null) { return root; } TreeNode newRoot = node.right; //选中需要旋转的点 node.right = newRoot.left; //把干扰旋转的点纳入自己的名下 //把newRoot的旧小弟变成自己的小弟 newRoot.left = node; //成为newRoot的新小弟 if (root == node) { root = newRoot; } else { TreeNode parent = findParent(root, node); if (parent.left == node) { parent.left = newRoot; } else { parent.right = newRoot; } } return root; } public static TreeNode rightRotate(TreeNode root, TreeNode node) { if (node == null || node.left == null) { return root; } TreeNode newRoot = node.left; node.left = newRoot.right; newRoot.right = node; if (root == node) { root = newRoot; } else { TreeNode parent = findParent(root, node); if (parent.left == node) { parent.left = newRoot; } else { parent.right = newRoot; } } return root; } public static TreeNode findParent(TreeNode root, TreeNode node) { if (root == null || node == null) { return null; } if (root.left == node || root.right == node) { return root; } TreeNode parent = findParent(root.left, node); if (parent == null) { parent = findParent(root.right, node); } return parent; } public static void main(String[] args) { // 创建一个二叉树 TreeNode root = new TreeNode(1); root.left = new TreeNode(2); root.right = new TreeNode(3); root.left.left = new TreeNode(4); root.left.right = new TreeNode(5); // 执行左旋操作 root = leftRotate(root, root.left); // 执行右旋操作 root = rightRotate(root, root.left); // 遍历打印旋转后的二叉树 inorderTraversal(root); } public static void inorderTraversal(TreeNode root) { if (root != null) { inorderTraversal(root.left); System.out.print(root.val + " "); inorderTraversal(root.right); } } }
时间复杂度
- 平衡二叉检索树的操作代价为O(logn)
- 非平衡的二叉检索树最差的代价为O(n)
- 插入、删除的代价与搜索代价类同
- 周游一个二叉检索树的代价为O(n)
使一个二叉检索树保持平衡才能真正发挥二叉检索树的作用
当树的高度较大时,查询、插入和删除操作的性能可能会受到影响。平衡的二叉搜索树可以保持树的高度较低,从而提供更好的性能保证。
优先队列
引:3.5优先队列 (xjtuse-guide.github.io)
概述
优先队列所需要的操作
插入: 增加一个带有重要级别的元素,插入到队列中的位置并不在意
删除: 队列中的重要级别最高的那个元素
获得头元素: 队列中的重要级别最高的那个元素
一般队列
插入:增加一个元素,这个元素被插入到队列中队尾
删除:剩除一个队列中队头的那个元素
获得头元素:获得队列中队头的那个元素
实现
优先队列的实现方法多样,可以使用二叉树,线性表排序.......我们下面使用的方法是用堆来实现优先队列,其余的需要习性探索喽
最大值堆(本文中的都是大顶堆)
任意一个结点的关键字值都大于或者等于其任意一个子结点存储的值
最小值堆
任意一个结点的关键字值都小于或者等于其任意一个子结点存储的值
(这就是所谓的堆属性)
堆的两条性质
从结构性质看,堆是一棵完全二叉树,故可以用数组代替链表形式来实现之
从堆序性质看,堆能够快速的找出重要级别最高的元素
重要级别最高的元素是根元素,对根元素的访问是最快的获取速度
根据二叉树的递归定义,我们考虑任意子树也应该是堆,那么应该有下面的结论
在堆中,对于每一个结点X。X的父亲的重要级别高于(或等于)X的重要级别。除了根结点之外(该结点没有父亲)
下面是树和堆的区分
树(Tree)和堆(Heap)是两种不同的数据结构,它们具有以下主要差异: 结构特点: 树是一种非线性的数据结构,由节点和边组成,节点之间存在层次关系。 每个节点可以有零个或多个子节点。 堆是一种特殊的树结构,通常指的是二叉堆。 堆是一个完全二叉树,具有特定的堆属性。 在堆中,每个节点的值都满足一定的顺序关系,如父节点的值大于(或小于)其子节点的值。 排序方式: 树的排序方式可以根据具体的树结构和应用场景而定。 例如,二叉搜索树按照节点值的大小进行排序,AVL树和红黑树也具有特定的排序性质。 堆的排序方式是根据堆属性进行排序。 在最小堆中,每个节点的值都小于其子节点的值,根节点是最小值。 在最大堆中,每个节点的值都大于其子节点的值,根节点是最大值。 主要应用: 树常用于表示层次关系,例如文件系统、组织结构等。 它们提供了高效的搜索、插入和删除操作。 堆主要用于实现优先队列,提供了在常数时间内获取最大或最小元素的能力。 它在堆排序、图算法(如最短路径算法)等领域有广泛应用。 平衡性: 树的平衡性在不同类型的树中有所不同,二叉搜索树中的平衡性由节点的插入和删除操作决定, 而平衡二叉树(如AVL树和红黑树)通过自平衡操作来维持平衡性。 堆不一定是平衡的,仅满足堆属性即可。 在二叉堆中,可以通过堆化操作来保持堆属性,但并不保证树的左右子树高度的平衡。
下面我们需要研究的就是, 基于堆属性实现插入删除等
对于任何一个新插入的值,它最终所处的位置一定不能改变原来的堆属性,将过程分为
- 将要插入的元素插入到堆中的“最后一个元素“
- 循环{
- 比较这个元素和其父的重要性
- 满足堆的性质则结束,否则将这个元素和其父元素交换
- 如果这个元素已经成为根元素也结束}
这部分代码如下, 需要注意堆顶元素编号为1public void insert(int number) { try { if (isFull()) { throw new Exception("Array is full!"); } int temp = ++currentSize; array[temp] = number;//将number放在数组最后 while ((temp != 1) && (array[temp] > array[getParent(temp)])) { swap(array, temp, getParent(temp)); temp = getParent(temp);//如果比父结点的值大则于其交换 }//注意根结点的下标为1,不是0 } catch (Exception e) { e.printStackTrace(); } }
删除: 队列中的重要级别最高的那个元素
- 保留根结点所维护的元素
- 将“最后”结点的元素拷贝到根结点
- 删除“最后”结点
- 将根结点通过比较交换,直到成为叶结点的时候结束(来源最小值)
删除的代码分析
/** * 删除堆顶元素 */ public void deleteMax() { try { if (isEmpty()) { throw new Exception("Array is empty!"); } else { swap(array, 1, currentSize--);//将堆顶元素放到最后,同时删除 if (currentSize != 0) siftDown(1); } } catch (Exception e) { e.printStackTrace(); } } /** * 小的值下沉 * * @param pos 当前位置 */ private void siftDown(int pos) { try { if (pos < 0 || pos > currentSize) { throw new Exception("Illegal position!"); } while (!isLeaf(pos)) { int j = getLeft(pos); if ((j < currentSize) && (array[j] < array[j + 1])) j++; //跟左右子树中较大的值交换 if (array[pos] > array[j]) return; //当前值已经比子树中的值都大,则返回 swap(array, pos, j);//交换 pos = j; } } catch (Exception e) { e.printStackTrace(); } }
getLeft返回的是左子树下标的值,而j+1则表示右子树的下标。这里还需要同时注意,我们删除的方法为currentSize--,也就是说数字仍在数组中但是变成了一个“野数字”,不允许访问,当需要大量删除时,我们需要增加相应方法,比如复制重建一个数组。
创建堆
可以按照一个元素一个元素的方式插入
时间代价是O(nlogn)=n(插入)*log(n)排序
//不是指初始化,初始化可以先全部插入在排序,那就是n+log(n)==O(n)
按照堆可以被存放到数组这种特性,当所有的元素都已存入到数组时,可以采取更高效的策略
时间代价是O(n),直接进行排序、
对于数组中任意位置i上的元素,其左子树结点在位置2i上,右子树在左子树后的单元(2i+1)中,它的父结点则在i/2向下取整。
对于完全二叉树,叶结点近乎占了一半,所以对于初始化的数组来说,其中有一半以上的元素满足堆序,调整顺序从下到上,从右到左,调整不满足堆序的结点
private int getLeft(int i) { return 2 * i; } private int getRight(int i) { return 2 * i + 1; } private int getParent(int i) { return i / 2; }} private void buildHeap() { for (int i = currentSize / 2; i > 0; i--) { siftDown(i);//对每个非叶子结点进行下沉操作 //从右到左,从下到上 } }
siftDown函数的实现在上方,接受需要调整的元素下标。这里的bulidHeap函数是指数字已经在堆中了
一个完整的大顶堆的实现
public class MaxHeap { private static final int DEFAULT_CAPACITY = 10;//默认大小 private int currentSize;//当前堆的大小 private int[] array;//堆数组 public MaxHeap() { this.array = new int[DEFAULT_CAPACITY + 1]; currentSize = 0; } public MaxHeap(int size) { this.array = new int[size + 1]; currentSize = 0; } public MaxHeap(int[] array) { this.array = new int[array.length + 1]; for (int i = 0; i < array.length; i++) { this.array[i + 1] = array[i]; }//从1开始算 currentSize = array.length; buildHeap(); } public void insert(int number) { } public int findMax() { return array[1]; } public void deleteMax() {} private void siftDown(int pos) {} public void print() { for (int i = 1; i <= currentSize; i++) { System.out.print(array[i] + " "); } } public void heapSort() { while (currentSize != 0) { System.out.print(findMax() + " "); deleteMax(); } } private boolean isEmpty() { return currentSize == 0; } private boolean isFull() { return currentSize == array.length - 1; } private boolean isLeaf(int i) { return i > currentSize / 2; } private void buildHeap() { for (int i = currentSize / 2; i > 0; i--) { siftDown(i);//对每个非叶子结点进行下沉操作 //从右到左,从下到上 } } private int getLeft(int i) {} private int getRight(int i) {} private int getParent(int i) {} private void swap(int[] array, int x, int y) {} public static void main(String[] args) throws Exception { int[] test = {1, 2, 6, 4, 5, 7, 3}; MaxHeap maxHeap = new MaxHeap(test); maxHeap.print(); maxHeap.deleteMax(); System.out.println(); System.out.println("delete 7:"); maxHeap.print(); maxHeap.insert(7); System.out.println(); System.out.println("insert 7:"); maxHeap.print(); System.out.println(); System.out.println("heapsort: "); maxHeap.heapSort(); } }
测试结果为
7 5 6 4 2 1 3 delete 7: 6 5 3 4 2 1 insert 7: 7 5 6 4 2 1 3 heapsort: 7 6 5 4 3 2 1
以上,感谢