八、树结构(应用)
文章目录
1、堆排序
堆排序描述:堆排序是利用堆这种数据结构而设计的一种排序算法。它的最好、最坏和平均时间复杂度都是O(nlogn)
堆是这样一种数据结构
- 大顶堆:每个结点的值大于等于其左右孩子的结点的值
- 小顶堆:每个结点的值小于等于其左右孩子结点的值
数组和二叉树可以相互转换,我们可以利用数组的存储结构,二叉树的逻辑结构进行排序
排序步骤:
- 将无序序列构建成一个堆,根据升序或者降序构造大顶堆或者小顶堆(以大顶堆为例)
- 将堆顶元素与末尾元素交换,将最大的元素放置在数组的尾端
- 重新调整以满足堆结构,继续交换堆顶元素与数组末尾元素,直到整个序列有序
图解:
1、构造大顶堆
2、交换堆顶和末尾的元素
3、将数组长度减1,继续构造新的大顶堆。
代码实现
public class HeapSort {
public static void main(String[] args){
int[] arr={4,6,8,5,9,-44,11,98,3};
//将无序序列构建成一个堆,根据升序或者降序的需求构造大顶堆或者小顶堆
//arr.length-1/2是为了找到最后一个非叶子结点
for (int i=arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
//将堆顶元素与末尾元素交换,将最大的元素放置在数组的尾端
//重新调整以满足堆的结构,继续交换堆顶元素与数组末尾元素,直到整个序列有序
for(int j=arr.length-1;j>0;j--){
int temp;
//交换
temp=arr[j];
arr[j]=arr[0];
arr[0]=temp;
adjustHeap(arr,0,j);
}
System.out.println(Arrays.toString(arr));
}
/**
* @param arr 数组(二叉树)
* @param i 传进的非叶子结点坐标
* @param length 数组的长度
*/
public static void adjustHeap(int[] arr,int i,int length){
int temp=arr[i];
for(int k=i*2+1;k<length;k=k*2+1){
//找到最大的子结点
if(k+1<length&&arr[k]<arr[k+1]){
k++;
}
if(temp<arr[k]){
arr[i]=arr[k];
i=k;//将i等于非叶子结点的左子结点坐标
}
//将非叶子结点的值赋给与它交换的子结点
arr[i]=temp;
}
}
}
2.霍夫曼树
**概念:**霍夫曼树是一种特殊的二叉树,给定n个权值作为n个叶子结点,若该树的带权路径长度达到最小,这样的二叉树称为最优二叉树,也称为霍夫曼树。即霍夫曼树就是带权路径长度WPL最小的二叉树。
树的路径长度是从树根到每一结点路径长度之和。
带权路径长度是路径的长度乘以结点上的权值
图中的带权路径长度为:
5*2+4*2+8*1=26
霍夫曼树的构造图解:
代码实现
public static Node huffmanTree(int[] arr) {
List<Node> nodes = new ArrayList<>();
//将数组中的值转换成结点
for (int vale : arr) {
nodes.add(new Node(vale));
}
while (nodes.size() > 1) {
//按权重的值从小到大排序
Collections.sort(nodes);
//取出权值较小的两个结点组成二叉树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//以这两个结点的权重相加的值组成他们的父节点,
Node parent = new Node(leftNode.value + rightNode.value);
parent.left=leftNode;
parent.right=rightNode;
//将此二叉树的两个子结点从list中移除,并将父节点加入到list中构成新的list
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
return nodes.get(0);
}
3、霍夫曼编码
概念:霍夫曼编码是可变字长编码方式(VLC)的一种,该方法完全依据字符出现的概率来编码。
定长编码是采用ASCII对字符编码
字母 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
二进制字符 | 000 | 001 | 010 | 011 | 100 | 101 |
这样真正传输的数据就是编码后的**”000001010011011100101“**,对方接收时可以按照3位一分来译码,但是如果传输的数据量很大,这样的二进制串的数量将会是非常庞大。
霍夫曼是将每个字符出现的频率作为权值构造霍夫曼树,按照权值最小路径长度进行编码
假设六个字母的频率A:27,B:8,C:15, D:15, E:30 ,F:5
构建霍夫曼树:
对从树根到叶子经过的路径的0或1来编码,做路径为0,右路径为1,因此可得霍夫曼编码树为
字母 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
二进制字符 | 01 | 1001 | 101 | 00 | 11 | 1000 |
所以采用霍夫曼编码为:01100110100111000
将生成的霍夫曼的编码每8位转成一个byte[]={-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28}
压缩率:(40-17)/40=57%
Note:霍夫曼排序方法的同会造成霍夫曼编码的不同,但是wpl是一样的,都是最小的。
代码实现:
-
构造霍夫曼树
public static Node createHuffmanTree(List<Node> nodes) { while (nodes.size() > 1) { //排序 Collections.sort(nodes); Node leftNode = nodes.get(0); Node rightNode = nodes.get(1); Node parentNode = new Node(null, leftNode.weight + rightNode.weight); parentNode.leftNode = leftNode; parentNode.rightNode = rightNode; //在集合中删除子结点并加入父结点 nodes.remove(leftNode); nodes.remove(rightNode); nodes.add(parentNode); } return nodes.get(0); }
-
根据霍夫曼树生成霍夫曼编码
private static void getCodes(Node node, String code, StringBuilder stringBuilder) { StringBuilder stringBuilder2 = new StringBuilder(stringBuilder); stringBuilder2.append(code); if (node != null) { if (node.data == null) { //向左递归 getCodes(node.leftNode, "0", stringBuilder2); //向右递归 getCodes(node.rightNode, "1", stringBuilder2); } else { //表明是叶子结点 huffmanCodes.put(node.data, stringBuilder2.toString()); } } }
-
对霍夫曼编码进行压缩(每8位压缩成一个byte)
byte[]={-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28}
private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){ StringBuilder stringBuilder=new StringBuilder(); //遍历byte数组 for(byte b:bytes){ stringBuilder.append(huffmanCodes.get(b)); } //煤8位转成一个byte int len ; if(stringBuilder.length()%8==0){ len=stringBuilder.length()/8; }else { len=stringBuilder.length()/8+1; } //创建存储压缩后的数组 byte[] huffmanCodeBytes=new byte[len]; int index=0; for (int i=0;i<stringBuilder.length();i+=8){ String strByte; if(i+8>stringBuilder.length()){ strByte=stringBuilder.substring(i); }else { strByte=stringBuilder.substring(i,i+8); } //将strByte转成一个byte,放入到huffmanCodeBytes,二进制每8位表示一个byte huffmanCodeBytes[index]= (byte) Integer.parseInt(strByte,2); index++; } return huffmanCodeBytes; }
4、霍夫曼解码
霍夫曼解码是将byte[]数组转成二进制字符串“1000100001…”,然后对照霍夫曼表解出对应的字符串。
代码实现
-
将byte数组转成二进制字符串
/** * j将byte转成二进制字符串 * @param flag 标志着是否需要补高位,为true时需要补高位,false时不需要 * @param b 传入的byte * @return 对应的二进制字符串 */ private static String byteToBitString(boolean flag, byte b) { //将b转成int int temp = b; //如果是正数我们需要补高位 if (flag) { temp |= 256;//与256按位或 } String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制补码 if (flag) { return str.substring(str.length() - 8); } else { return str; } }
-
对照霍夫曼表生成对应的字符串"1000111000"===>“i like like like java do you like a java”
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) { //1、先得到huffmanBytes,对应的二进制字符串"1000111000" StringBuilder stringBuilder = new StringBuilder(); //将byte数组转成二进制字符串 for (int i = 0; i < huffmanBytes.length; i++) { byte b = huffmanBytes[i]; boolean flag = (i == huffmanBytes.length - 1); stringBuilder.append(byteToBitString(!flag, b)); } //将字符串按照指定的霍夫曼编码进行解码 //将霍夫曼编码表进行调换,可以进行反向查询,即 a->100, Map<String, Byte> map = new HashMap<String, Byte>(); for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) { map.put(entry.getValue(), entry.getKey()); } //创建集合存放Byte List<Byte> list = new ArrayList<>(); //i可以理解为索引扫描StringBuilder for (int i = 0; i < stringBuilder.length(); ) { int count = 1; boolean flag = true; Byte b = null; while (flag) { //1010100000111..... //递增取出key String key = stringBuilder.substring(i, i + count); b = map.get(key); if (b == null) {//说明没有匹配到 count++; } else { flag = false; } } list.add(b); i += count; } //当for循环结束后,list中就存放了"i like like like java do you like a java" //把list中的数据放入到byte[],并返回 byte b[] = new byte[list.size()]; for (int i = 0; i < b.length; i++) { b[i] = list.get(i); } return b; }
note:
- 霍夫曼编码是采用字节来处理的
- 文件中的字符重复率越高,使用霍夫曼编码效果越明显。
5、二叉排序树(BinarySortTree)
概念:在非叶子结点中,左子结点的值小于结点值,右子结点的值大于等于结点值,这样的二叉树称为二叉排序树。
代码实现
-
添加
//添加结点 public void add(Node node) { //判断添加的结点是否为空 if (node == null) { return; } if (node.value < this.value) { if (this.left == null) { this.left = node; } else { this.left.add(node); } } if (node.value > this.value) { if (this.right == null) { this.right = node; } else { this.right.add(node); } } }
-
删除(分三种情况)
-
叶子结点的删除
-
删除的节点中只含有一个子树
-
删除的结点中含有两个子树
-
public void delNode(int value) {
if (root == null) {
return;
} else {
//1、找到要删除的目标结点
Node targetNode = searchTargetNode(value);
if (targetNode == null) {
return;
}
//如果发现这颗二叉树只有一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
//2、找到要删除结点的父结点
Node parentNode = searchParentNode(value);
//如果要删除的结点为叶子结点
if (targetNode.left == null && targetNode.right == null) {
//判断targetNode是parentNode的左子结点还是右子结点
if (parentNode.left != null && parentNode.left.value == value) {
parentNode.left = null;
} else if (parentNode.right != null && parentNode.right.value == value) {
parentNode.right = null;
}
}else if(targetNode.left!=null&&targetNode.right!=null){
//删除的结点含有两颗子树
int minVal=delRightTreeMin(targetNode.right);
targetNode.value=minVal;
}else {
//删除的结点含有一颗子树的
//如果目标结点的子结点是左子结点
if(targetNode.left!=null){
if(parentNode!=null){
if(parentNode.left.value==value){
parentNode.left=targetNode.left;
}else {
parentNode.right=targetNode.left;
}
}else {
root=targetNode.left;
}
}else {
if(parentNode!=null){
if(parentNode.left.value==value){
parentNode.left=targetNode.right;
}else {
parentNode.right=targetNode.right;
}
}else {
root=targetNode.right;
}
}
}
}
}
6、平衡二叉树(AVL树)
概念:当向一个二叉排序树中顺序的天剑的元素时,这时的二叉排序树将会退化成链表,其查询效率就会下降,为了解决这样的问题,引入了一个平衡因子,规定为二叉排序树的左右子树的高度差不能大于1,这样的二叉树排序树称为平衡二叉树。
6.1左旋转
(右子树高度-左子树高度)>1时,进行左旋转
- 使用当前结点的值创建新的结点
- 把新结点的左子树设置为当前结点的左子树
- 把新结点的右子树设置为当前结点右子树的左子树
- 把当前结点的值设置为右子结点的值
- 把当前结点的右子树设置为当前结点的右子树的右子树
- 把当前结点的左子树(左子结点)设置为新的结点
private void leftRotate(){
//使用当前根结点的值创建新的结点
Node newNode= new Node(value);
//把新结点的左子树设置为当前结点的左子树
newNode.left=left;
//把新结点的右子树设为当前结点右子树的左子树
newNode.right=right.left;
//把当前结点的值设为右子结点的值
value=right.value;
//把当前结点的右子树设置为当前结点右子树的右子树
right=right.right;
//把当前结点的左子树(左子结点)设置为新的结点
left=newNode;
}
6.2右旋转
(左子树高度-右子树高度)>1时,进行右旋转
- 以当前结点的值创建新的结点
- 把新结点的右子树设置为当前结点的右子树
- 把新结点的左子树设置为当前结点的左子树的右子树
- 把当前结点的值换为左子结点的值
- 把当前结点的左子树设置为左子树的左子树
- 把当前结点的右子(右子结点)设置为新的结点
private void rightRotate(){
Node newNode=new Node(value);
newNode.right=right;
newNode.left=left.right;
value=left.value;
left=left.left;
right=newNode;
}
6.3双旋转
-
左旋转–>右旋转
if(leftHeight()-rightHeight()>1){ //如果当前结点的左子树的右子树高度大于当前结点左子树的左子树高度,则需要对当前结点的左子树进行坐旋转,然后对当前结点进行右旋转 if(left!=null&&left.rightHeight()>left.leftHeight()){ left.leftRotate(); rightHeight(); } rightRotate(); } }
-
右旋转–>左旋转
if(rightHeight()-leftHeight()>1){ if(right!=null&&right.leftHeight()>right.rightHeight()){ rightRotate(); leftRotate(); } leftRotate(); return; }
7、多路查找树
存在的意义:普通的二叉树的一个结点只能存一个元素,比如BST、AVL等。而多路查找树的每个结点可以包含n个元素和n+1个孩子结点,如B树,这这样做的目的是为了减少数的高度,增加树的度,这样可以降低内存读取外存的次数(将相关的数据尽量集中在一起,以便依次读取读取多个数据),提高效率。
定义:在二叉树中,允许每个结点可以有更多的数据项和更多的子结点,这就是多叉树。如2-3树
7.1B(Blance)树
2-3树是最简单的B树结构,其有如下特点
2-3树所有叶子结点都在同一层(B树都满足这个条件)
有两个子结点的结点叫二结点,二节点要么没有子结点,要么有两个子结点
有三个子结点的结点叫三结点,三结点要么没有子结点,要么有三个子结点
2-3树是由二结点和三结点构成的树。
网上找的图解
- B树的阶:结点的最多子结点个数,如2-3树的阶是3,2-3-4的阶是4
- B树的搜索:从根结点开始,从结点的关键字(有序)序列进行二分查找,如果找到则结束,否则进入查询关键字的子结点;重复,直到所对应的子结点指针为空,或已经是叶子结点。
- 关键字的集合分布在在整棵树中,即叶子结点和非叶子结点都存放有数据
- 搜索可以在非叶子节点中结束
- 其它搜索性能等价于在关键字全集内做一次二分查找。
7.2B+树
网上找的图解
- B+树的搜索与B树也基本相同,其区别是B+树只有到达叶子结点时才会找到,其性能也等价于在关键字的全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子结点(稠密索引)),其链表中的关键字也恰好是有序的
- 不可能在非叶子结点中找到
- 非叶子结点相当于叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- B树和B+数都有各自的应用场景,不能说B+树就比B树要好。
7.3B*树
B*树是B+树的变体,在B+树的非根和非叶子结点增加指向兄弟的指针
网上找的图解
- B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2
- 从第一个特点我们可以看出,B*树分配的新结点概率比B+树要低,空间使用率更高。