数据结构之【树】全解

前中后序遍历
//分为在此次循环中判断左右结点的null,也可以在下次递归中判断。
public void preOrder(){//推荐
    if(this==null) return;//上一层就不该执行这个preOrder函数,只不过推迟到这一层了而已
    System.out.println(this);
    this.left.preOrder();
    this.right.preOrder();
}
//--------方法2-----------
public void preOrder() {
    System.out.println(this); //先输出父结点
    if(this.left != null) {
        this.left.preOrder();
    }
    if(this.right != null) {
        this.right.preOrder();
    }
}

树的深度

要点:

  • 递归的方法类似于归并法
  • 非递归的方法是利用队列
  • 用LinkedList实现队列
    • 作为List使用时,一般采用add压入/get获取。add(元素)add(索引,元素)。remove没有元素会异常
    • 作为Queue使用时,才会采用offer/poll/take等方法。
class TreeNode{
	int val;
	TreeNode left;
	TreeNode right;
	TreeNode(int val){
		this.val = val;
		left = null;
		right = null;
	}
	TreeNode(int val,TreeNode left,TreeNode right){
		this.val = val;
		this.left = left;
		this.right = right;
	}
}

import java.util.LinkedList;

public class DeepOfTree {
	
	// 方法1:递归
	public static int deep(TreeNode root){
		if(root == null)
			return 0;
		return 1+Math.max(deep(root.left),deep(root.right));//左右2支深度的最大值+1
	}
	
	// 方法2:非递归,利用树的层次遍历
	public static int deep2(TreeNode root) {
		if(root == null)
			return 0;
		
		TreeNode curNode = null;//当前节点
		int level = 0;//记录当前层
		int last=0; //用来记录当前层
		int cur = 0;//当前层访问的节点数
		LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
		queue.offer(root);//把根节点放入
		
		while(!queue.isEmpty()) {
			level++;//深度
			last = queue.size();//当前层的结点个数
			cur = 0;
			while(cur < last){
				cur++;//当前层访问的节点数
				curNode = queue.poll();
				if(curNode.left != null){
					queue.offer(curNode.left);
				}
				if(curNode.right != null) {
					queue.offer(curNode.right);
				}
			}
		}
		
		return level;
	}
	

	public static void main(String[] args) {
		TreeNode val_2 = new TreeNode(2);
		TreeNode val_4 = new TreeNode(4);
		TreeNode val_3 = new TreeNode(3,val_2,val_4);
		TreeNode val_6 = new TreeNode(6);
		TreeNode root = new TreeNode(5,val_3,val_6);
		
		System.out.println(deep2(root));
	}

}
线索二叉树

n个结点的二叉链表中含有n+1 【公式 2n-(n-1) =n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")

这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种

前驱节点:一个结点的前一个结点,称为前驱结点
后继结点:一个结点的后一个结点,称为后继结点

  • 要定义线索二叉树,思路是利用中序等遍历进行前后结点连接
  • 定义一个类成员pre,初始值为null,所以第一个结点的pre是null。然后将当前结点更新为pre,然后在中序遍历过程中的下一结点就可以取到pre。
public class ThreadedBinaryTree {//代码省略了Node的代码以及一切见文知意的代码



    /* 考虑如下的树
       ⊙
    ⊙      ⊙
  ⊙      ⊙   ⊙
*/

    //中序按序打印所有结点//非递归
    public void threadIterate() {//leftType=1代表原来指向空
        /*
思路:原来我们中序遍历是  1 左节点递归  2输出当前结点  3 右结点递归
对于任意子树,先打印其左子树,即左子树的最左下结点,然后打印当前结点,然后取找右结点,如果右节点为空,那么线索二叉树正好能找到下个结点,如果非空,那么就得去递归右子树
*/
        while(node!=null) {
            //循环找到最开始的节点,即左节点
            while(node.leftType==0) {
                node=node.leftNode;
            }
            //打印当前节点的值
            System.out.println(node.value);
            //如果当前节点的右指针指向的是后继节点,可能后继节点还有后继节点
            while(node.rightType==1) {//右子树肯定没有左节点
                node=node.rightNode;
                System.out.println(node.value);//打印直到该结点有右子树
                //因为有右子树的话可能就有了左结点,所以不能直接打印右子树
            }
            //替换遍历的节点
            node=node.rightNode;
        }
        //思想总结:找到第一个结点后,就只找后续结点就行。
        //先找到最左结点,打印该结点,然后试图通过右指针寻找后续,
        //如果遇到有右结点的情况,更新Node为右结点,继续循环
    }

    //思路:1递归左树 2处理当前(处理左空,输出,处理pre的右空)  3 递归递归右树
    ThreadedNode root;
    //用于临时存储前驱节点
    ThreadedNode pre=null;
    //用于临时存储当前遍历节点
    ThreadedNode node = root;
    //中序线索化二叉树//赋值与指向
    public void threadNodes(ThreadedNode node) {//默认传入的是根节点(也可以设置无参重载)
        //当前节点如果为null,直接返回
        if(node==null) {return;}

        //处理左子树
        threadNodes(node.leftNode);//如果左子树为空,就会立刻返回继续执行
        //pre是Tree的成员变量,递归也整体共享一个,所以我们需要按序用pre

        //处理前驱节点
        if(node.leftNode==null){
            node.leftNode=pre;
            node.leftType=1;
        }
        //处理前驱的右指针,如果前驱节点的右指针是null(没有指下右子树)//因为要判断pre.rightNode,所以需要先判断pre!=null
        if(pre!=null&&pre.rightNode==null) {//第一个判断:防止中序第一个结点//第二个判断:有右子树的话就无需指向了,这是只有单指向,不是双向的
            pre.rightNode=node;
            pre.rightType=1;
        }
        //更新共享的pre
        pre=node;

        //处理右子树
        threadNodes(node.rightNode);
    }
}
package com.atguigu.tree.threadedbinarytree;

import java.util.concurrent.SynchronousQueue;

// 构建测试环境
public class ThreadedBinaryTreeDemo {

	public static void main(String[] args) {
		//测试一把中序线索二叉树的功能
		HeroNode root = new HeroNode(1, "tom");
		HeroNode node2 = new HeroNode(3, "jack");
		HeroNode node3 = new HeroNode(6, "smith");
		HeroNode node4 = new HeroNode(8, "mary");
		HeroNode node5 = new HeroNode(10, "king");
		HeroNode node6 = new HeroNode(14, "dim");
		
		//二叉树,后面我们要递归创建, 现在简单处理使用手动创建
		root.setLeft(node2);
		root.setRight(node3);
		node2.setLeft(node4);
		node2.setRight(node5);
		node3.setLeft(node6);
		
		//测试中序线索化
		ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
		threadedBinaryTree.setRoot(root);
		threadedBinaryTree.threadedNodes();
		
		//测试: 以10号节点测试
		HeroNode leftNode = node5.getLeft();
		HeroNode rightNode = node5.getRight();
		System.out.println("10号结点的前驱结点是 ="  + leftNode); //3
		System.out.println("10号结点的后继结点是="  + rightNode); //1
		
		//当线索化二叉树后,能在使用原来的遍历方法
		//threadedBinaryTree.infixOrder();
		System.out.println("使用线索化的方式遍历 线索化二叉树");
		threadedBinaryTree.threadedList(); // 8, 3, 10, 1, 14, 6
		
	}
}
赫夫曼树(最优二叉树)

路径和路径长度:

路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。

路径长度:通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1

结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。

结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。如13×2

树的带权路径长度:所有的叶子结点的带权路径长度之和,记为WPL(weight path length),

最优二叉树:权值越大的结点离根节点越近的二叉树才是最优二叉树

赫夫曼树:WPL最小

构成赫夫曼树的步骤:

1)从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

2)取出根节点权值最小的两颗二叉树

3)组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

4)再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

Collections.sort(nodes);//重点
Node leftNode = nodes.get(0);
Node leftNode = nodes.get(0);
package com.atguigu.huffmantree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class HuffmanTree {

	public static void main(String[] args) {
		int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
		Node root = createHuffmanTree(arr);
	}
	

	// 创建赫夫曼树的方法
	/**
	 * @param arr 需要创建成哈夫曼树的数组
	 * @return 创建好后的赫夫曼树的root结点
	 */
	public static Node createHuffmanTree(int[] arr) {
		// 第一步为了操作方便
		// 1. 遍历 arr 数组
		// 2. 将arr的每个元素构成成一个Node
		// 3. 将Node 放入到ArrayList中
		List<Node> nodes = new ArrayList<Node>();
		for (int value : arr) {
			nodes.add(new Node(value));
		}
		
		//我们处理的过程是一个循环的过程
		while(nodes.size() > 1) {
		
			//排序 从小到大 
			Collections.sort(nodes);//重点
			System.out.println("nodes =" + nodes);
			
			//取出根节点权值最小的两颗二叉树 
			//(1) 取出权值最小的结点(二叉树)
			Node leftNode = nodes.get(0);
			//(2) 取出权值第二小的结点(二叉树)
			Node rightNode = nodes.get(1);
			
			//(3)构建一颗新的二叉树
			Node parent = new Node(leftNode.value + rightNode.value);
			parent.left = leftNode;
			parent.right = rightNode;
			
			//(4)从ArrayList删除处理过的二叉树
			nodes.remove(leftNode);
			nodes.remove(rightNode);
			//(5)将parent加入到nodes
			nodes.add(parent);
		}
		
		//返回哈夫曼树的root结点
		return nodes.get(0);
		
	}
}

// 创建结点类
// 为了让Node 对象持续排序Collections集合排序
// 让Node 实现Comparable接口
class Node implements Comparable<Node> {
	int value; // 结点权值
	char c; //字符
	Node left; // 指向左子结点
	Node right; // 指向右子结点

	@Override
	public int compareTo(Node o) {
		// TODO Auto-generated method stub
		// 表示从小到大排序
		return this.value - o.value;
	}
}
赫夫曼编码:

赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。

赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间

赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

原理剖析

通信领域中信息的处理方式1-定长编码

i like like like java do you like a java       // 共40个字符(包括空格)  
105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97  //对应Ascii码

01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
按照二进制来传递信息,总的长度是  359   (包括空格)
在线转码 工具 :https://www.mokuge.com/tool/asciito16/ 
----------------------------------------
通信领域中信息的处理方式2-变长编码

i like like like java do you like a java       // 共40个字符(包括空格)

d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数
0=  ,  1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.

按照上面给各个字符规定的编码,则我们在传输  "i like like like java do you like a java" 数据时,编码就是 10010110100...  
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,我们还要进行举例说明, 不捉急)
---------------------------
通信领域中信息的处理方式3-赫夫曼编码

i like like like java do you like a java       // 共40个字符(包括空格)

d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)
//根据赫夫曼树,给各个字符
//规定编码 , 向左的路径为0
//向右的路径为1 , 编码如下:

o: 1000   u: 10010  d: 100110  y: 100111  i: 101
a : 110     k: 1110    e: 1111       j: 0000       v: 0001
l: 001          : 01

按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

长度为 : 133 
说明:
原来长度是  359 , 压缩了  (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性


注意, 这个赫夫曼树根据排序方法不同(不稳定造成的),也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

数据压缩(赫夫曼树应用)

使用赫夫曼编码来解码数据,具体要求是
前面我们得到了赫夫曼编码和对应的编码byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java"

思路:解码过程,就是编码的一个逆向操作。

package com.atguigu.huffmancode;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HuffmanCode {

	public static void main(String[] args) {
		
		//测试压缩文件
//		String srcFile = "d://Uninstall.xml";
//		String dstFile = "d://Uninstall.zip";
//		
//		zipFile(srcFile, dstFile);
//		System.out.println("压缩文件ok~~");
		
		
		//测试解压文件
		String zipFile = "d://Uninstall.zip";
		String dstFile = "d://Uninstall2.xml";
		unZipFile(zipFile, dstFile);
		System.out.println("解压成功!");
		
		/*
		String content = "i like like like java do you like a java";
		byte[] contentBytes = content.getBytes();
		System.out.println(contentBytes.length); //40
		
		byte[] huffmanCodesBytes= huffmanZip(contentBytes);
		System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes) + " 长度= " + huffmanCodesBytes.length);
		
		
		//测试一把byteToBitString方法
		//System.out.println(byteToBitString((byte)1));
		byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
		
		System.out.println("原来的字符串=" + new String(sourceBytes)); // "i like like like java do you like a java"
		*/
		
		
		
		//如何将 数据进行解压(解码)  
		//分步过程
		/*
		List<Node> nodes = getNodes(contentBytes);
		System.out.println("nodes=" + nodes);
		
		//测试一把,创建的赫夫曼树
		System.out.println("赫夫曼树");
		Node huffmanTreeRoot = createHuffmanTree(nodes);
		System.out.println("前序遍历");
		huffmanTreeRoot.preOrder();
		
		//测试一把是否生成了对应的赫夫曼编码
		Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
		System.out.println("~生成的赫夫曼编码表= " + huffmanCodes);
		
		//测试
		byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
		System.out.println("huffmanCodeBytes=" + Arrays.toString(huffmanCodeBytes));//17
		
		//发送huffmanCodeBytes 数组 */
		
		
	}
	
	//编写一个方法,完成对压缩文件的解压
	/**
	 * @param zipFile 准备解压的文件
	 * @param dstFile 将文件解压到哪个路径
	 */
	public static void unZipFile(String zipFile, String dstFile) {
		
		//定义文件输入流
		InputStream is = null;
		//定义一个对象输入流
		ObjectInputStream ois = null;
		//定义文件的输出流
		OutputStream os = null;
		try {
			//创建文件输入流
			is = new FileInputStream(zipFile);
			//创建一个和  is关联的对象输入流
			ois = new ObjectInputStream(is);
			//读取byte数组  huffmanBytes
			byte[] huffmanBytes = (byte[])ois.readObject();
			//读取赫夫曼编码表
			Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
			
			//解码
			byte[] bytes = decode(huffmanCodes, huffmanBytes);
			//将bytes 数组写入到目标文件
			os = new FileOutputStream(dstFile);
			//写数据到 dstFile 文件
			os.write(bytes);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		} finally {
			
			try {
				os.close();
				ois.close();
				is.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
			
		}
	}
	
	//编写方法,将一个文件进行压缩
	/**
	 * @param srcFile 你传入的希望压缩的文件的全路径
	 * @param dstFile 我们压缩后将压缩文件放到哪个目录
	 */
	public static void zipFile(String srcFile, String dstFile) {
		
		//创建输出流
		OutputStream os = null;
		ObjectOutputStream oos = null;
		//创建文件的输入流
		FileInputStream is = null;
		try {
			//创建文件的输入流
			is = new FileInputStream(srcFile);
			//创建一个和源文件大小一样的byte[]
			byte[] b = new byte[is.available()];
			//读取文件
			is.read(b);
			//直接对源文件压缩
			byte[] huffmanBytes = huffmanZip(b);
			//创建文件的输出流, 存放压缩文件
			os = new FileOutputStream(dstFile);
			//创建一个和文件输出流关联的ObjectOutputStream
			oos = new ObjectOutputStream(os);
			//把 赫夫曼编码后的字节数组写入压缩文件
			oos.writeObject(huffmanBytes); //我们是把
			//这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
			//注意一定要把赫夫曼编码 写入压缩文件
			oos.writeObject(huffmanCodes);
			
			
		}catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}finally {
			try {
				is.close();
				oos.close();
				os.close();
			}catch (Exception e) {
				// TODO: handle exception
				System.out.println(e.getMessage());
			}
		}
		
	}
	
	//编写一个方法,完成对压缩数据的解码
    //思路
	//1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
	//   重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
	//2.  赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码  =》 "i like like like java do you like a java"
	/**
	 * @param huffmanCodes 赫夫曼编码表 map
	 * @param huffmanBytes 赫夫曼编码得到的字节数组
	 * @return 就是原来的字符串对应的数组
	 */
	private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
		
		//1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
		StringBuilder stringBuilder = new StringBuilder();
		//将byte数组转成二进制的字符串
		for(int i = 0; i < huffmanBytes.length; i++) {
			byte b = huffmanBytes[i];
			//flag判断是不是最后一个字节//是最后一个字节则flag=false
			boolean flag = !(i == huffmanBytes.length - 1);
			stringBuilder.append(byteToBitString(flag, b));
		}
		//把字符串安装指定的赫夫曼编码进行解码
		//把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
		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) {
				//1010100010111...
				//递增的取出 key 1 
				String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
				b = map.get(key);
				if(b == null) {//说明没有匹配到
					count++;
				}else {
					//匹配到
					flag = false;
				}
			}
			list.add(b);
			i += count;//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;
		
	}
 	
	/**
	 * 将一个byte 转成一个二进制的字符串
	 * @param b 传入的 byte
	 * @param flag 标志是否需要补高位如果是true ,表示需要补高位;如果是false表示不补, 如果是最后一个字节,无需补高位
	 * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
	 */
	private static String byteToBitString(boolean flag, byte b) {
        /*首先了解下原来我们如何将String转成Byte的:
        huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte, 2);//以2进制解析
        //int转byte是直接截取,不会考虑符号位
        */
		//使用变量保存 b
		int temp = b; //将 b 转成 int//对应的十进制值不变
        String str;
        if(flag){//如果不是最后一组(凑不齐8位)//最后一个字节flag=false
            str = Integer.toBinaryString(temp);// <=127的正数得到的str.length<8
            while(str.length<8){
                str="0"+str;
            }
        }else{//如果是最后一组(可能凑不齐8位)
            str = Integer.toBinaryString(temp);//如何判断最后一组的位数?还是用字符串好
        }
        return str;
        /* 下面是尚硅谷原始代码,我上面的更容易理解
        //说明:负数的话,int的低8位都有数,再转成byte不变;正数的话,非0最高位可能索引可能不是-8,但是填充的都是0,我们取低8位后进行byte结果仍是正确的。
		if(flag) {//非最后一组时,flag为true
			temp |= 256; //按位或 256=... 0001 0000 0000  | 0000 0001 => 1 0000 0001
            //解释说明为什么需要这个if:
            //System.out.println( Integer.toBinaryString(8));
            //输出结果为1000,此时都没有低8位,后面也就没法取8位了,这里按位或后,是为了后面得到的str是9位的,这样就可以取低8位了
		}
		String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        //Byte没有对应的toBinaryString方法
		if(flag) {//非最后一组
			return str.substring(str.length() - 8);//int是4字节的,我们只需要1个字节的编码
		} else {//最后一组
			return str;
		}
		*/
	}
	
	//使用一个方法,将前面的方法封装起来,便于我们的调用.
	/**
	 * @param bytes 原始的字符串对应的字节数组
	 * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
	 */
	private static byte[] huffmanZip(byte[] bytes) {
		List<Node> nodes = getNodes(bytes);
		//根据 nodes 创建的赫夫曼树
		Node huffmanTreeRoot = createHuffmanTree(nodes);
		//对应的赫夫曼编码(根据 赫夫曼树)
		Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
		//根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
		byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
		return huffmanCodeBytes;
	}
	
	
    //将原来的字节码转成赫夫曼编码
	private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
		//bytes为原始字节码//huffmanCodes为每个字母对应的原始和赫夫曼编码//返回字符串的赫夫曼
        //原始字节码是补码
		//1.利用 huffmanCodes 将  bytes 转成  赫夫曼编码对应的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍历bytes 数组 
		for(byte b: bytes) {
			stringBuilder.append(huffmanCodes.get(b));
		}
		//System.out.println("测试 stringBuilder~~~=" + stringBuilder.toString());
		
		//将 "1010100010111111110..." 转成 byte[]
		
		//统计返回  byte[] huffmanCodeBytes 长度
		int len;//int len = (stringBuilder.length() + 7) / 8;
		if(stringBuilder.length() % 8 == 0) {
			len = stringBuilder.length() / 8;
		} else {
			len = stringBuilder.length() / 8 + 1;
		}
		//创建 存储压缩后的 byte数组
		byte[] huffmanCodeBytes = new byte[len];
		int index = 0;//记录是第几个byte
		for (int i = 0; i < stringBuilder.length(); i += 8) { //因为是每8位对应一个byte,所以步长 +8
				String strByte;
				if(i+8 > stringBuilder.length()) {//不够8位
					strByte = stringBuilder.substring(i);//剩余位放入
				}else{
					strByte = stringBuilder.substring(i, i + 8);//后面会处理不足的位置补0
				}	
				//将字符串strByte 转成一个byte,放入到 huffmanCodeBytes
				huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);//以2进制解析
            //在这里Integer.parseInt解析出来的永远是正数,强转byte后直接取的低8位,所以strByte无损失地转成了byte
           		 //huffmanCodeBytes[index] = Byte.parseByte(strByte, 2);//为什么不用这个:该函数解析不了负数(bug)
				index++;
		}
		return huffmanCodeBytes;
	}
	
	//生成赫夫曼树对应的赫夫曼编码
	//思路:
	//1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
	//   生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
	//2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
	static StringBuilder stringBuilder = new StringBuilder();//线程不安全,速度快,不会产生新的对象
	
	//为了调用方便,重载 getCodes
	private static Map<Byte, String> getCodes(Node root) {
		if(root == null) {
			return null;
		}
		//处理root的左子树
		getCodes(root.left, "0", stringBuilder);
		//处理root的右子树
		getCodes(root.right, "1", stringBuilder);
		return huffmanCodes;
	}
	
	/**
	 * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
	 * @param node  传入结点
	 * @param code  路径: 左子结点是 0, 右子结点 1
	 * @param stringBuilder 用于拼接路径
	 */
	private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        //首次传入的是根节点,code为""
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		stringBuilder2.append(code);//将code 加入到 stringBuilder2
		if(node != null) { //如果node == null不处理
			//判断当前node 是叶子结点还是非叶子结点
			if(node.data == null) { //非叶子结点!!!
				//递归处理
				getCodes(node.left, "0", stringBuilder2);//向左递归
				getCodes(node.right, "1", stringBuilder2);//向右递归
			} else { //叶子结点,将叶子几点的赫夫曼编码放入map<byte,String>
				//就表示找到某个叶子结点的最后
				huffmanCodes.put(node.data, stringBuilder2.toString());
			}
		}
	}
	
	//前序遍历的方法
	private static void preOrder(Node root) {
		if(root != null) {
			root.preOrder();
		}else {
			System.out.println("赫夫曼树为空");
		}
	}
	
    //接收原始的bytes数组,返回每个字母的计数List<Node>
	private static List<Node> getNodes(byte[] bytes) {
		ArrayList<Node> nodes = new ArrayList<Node>();
		//遍历 bytes , 统计 每一个byte出现的次数
		Map<Byte, Integer> counts = new HashMap<>();
		for (byte b : bytes) {
			Integer count = counts.get(b);
			if (count == null) { // Map还没有这个字符数据,第一次
				counts.put(b, 1);
			} else {
				counts.put(b, count + 1);
			}
		}

		//Map转成List<Node>
		for(Map.Entry<Byte, Integer> entry: counts.entrySet()) {
			nodes.add(new Node(entry.getKey(), entry.getValue()));
		}
		return nodes;
	}
	
	//可以通过List 创建对应的赫夫曼树
	private static Node createHuffmanTree(List<Node> nodes) {
		while(nodes.size() > 1) {
			//排序, 从小到大
			Collections.sort(nodes);
			//取出第1、2颗最小的二叉树
			Node leftNode = nodes.get(0);//List
			Node rightNode = nodes.get(1);
			//创建一颗新的二叉树,它的根节点 //没有data, 只有权值
			Node parent = new Node(null, leftNode.weight + rightNode.weight);
			parent.left = leftNode;
			parent.right = rightNode;
			
			//将已经处理的两颗二叉树从nodes删除
			nodes.remove(leftNode);
			nodes.remove(rightNode);
			//将新的二叉树,加入到nodes
			nodes.add(parent);
		}
		//nodes 最后的结点,就是赫夫曼树的根结点
		return nodes.get(0);
	}
}


//创建Node ,待数据和权值
class Node implements Comparable<Node>  {
	Byte data; // 存放数据(字符)本身,比如'a' => 97 ' ' => 32
	int weight; //权值, 表示字符出现的次数
	Node left;
	Node right;
	public Node(Byte data, int weight) {
		this.data = data;
		this.weight = weight;
	}
	@Override
	public int compareTo(Node o) {
		// 从小到大排序
		return this.weight - o.weight;
	}
	
	public String toString() {
		return "Node [data = " + data + " weight=" + weight + "]";
	}
	
	//前序遍历
	public void preOrder() {
		System.out.println(this);
		if(this.left != null) {
			this.left.preOrder();
		}
		if(this.right != null) {
			this.right.preOrder();
		}
	}
}
package demo10;

public class Node implements Comparable<Node> {
    Byte data;
    int weight;
    Node left;
    Node right;
    public Node(Byte data,int weight) {
        this.data=data;
        this.weight=weight;
    }

    @Override
    public String toString() {
        return "Node [data=" + data + ", weight=" + weight + "]";
    }

    @Override
    public int compareTo(Node o) {
        return o.weight-this.weight;
    }
}
-----------------------------------------
    package demo10;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class TestHuffmanCode {

    public static void main(String[] args) {
        //		String msg="can you can a can as a can canner can a can.";
        //		byte[] bytes = msg.getBytes();
        //		//进行赫夫曼编码压缩
        //		byte[] b = huffmanZip(bytes);
        //		//使用赫夫曼编码进行解码
        //		byte[] newBytes = decode(huffCodes,b);
        //		System.out.println(new String(newBytes));
        String src="1.bmp";
        String dst="2.zip";
        //		try {
        //			zipFile(src, dst);
        //		} catch (IOException e) {
        //			e.printStackTrace();
        //		}
        try {
            unZip("2.zip", "3.bmp");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
	 * 文件的解压
	 */
    public static void unZip(String src,String dst) throws Exception {
        //创建一个输入流
        InputStream is  = new FileInputStream("2.zip");
        ObjectInputStream ois = new ObjectInputStream(is);
        //读取byte数组
        byte[] b = (byte[]) ois.readObject();
        //读取赫夫曼编码表
        Map<Byte, String> codes = (Map<Byte, String>) ois.readObject();
        ois.close();
        is.close();
        //解码
        byte[] bytes = decode(codes, b);
        //创建一个输出流
        OutputStream os  = new FileOutputStream(dst);
        //写出数据
        os.write(bytes);
        os.close();
    }

    /**
	 * 压缩文件
	 */
    public static void zipFile(String src,String dst) throws IOException {
        //创建一个输入流
        InputStream is = new FileInputStream(src);
        //创建一个和输入流指向的文件大小一样的byte数组
        byte[] b = new byte[is.available()];
        //读取文件内容
        is.read(b);
        is.close();
        //使用赫夫曼编码进行编码
        byte[] byteZip = huffmanZip(b);
        //输出流
        OutputStream os = new FileOutputStream(dst);
        ObjectOutputStream oos = new ObjectOutputStream(os);
        //把压缩后的byte数组写入文件
        oos.writeObject(byteZip);
        //把赫夫曼编码表写入文件
        oos.writeObject(huffCodes);
        oos.close();
        os.close();
    }

    /**
	 * 使用指定的赫夫曼编码表进行解码
	 */
    private static byte[] decode(Map<Byte, String> huffCodes, byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        //把byte数组转为一个二进制的字符串
        for(int i=0;i<bytes.length;i++) {
            byte b = bytes[i];
            //是否是最后一个。
            boolean flag = (i==bytes.length-1);
            sb.append(byteToBitStr(!flag,b));
        }
        //把字符串按照指定的赫夫曼编码进行解码
        //把赫夫曼编码的键值对进行调换
        Map<String, Byte> map = new HashMap<>();
        for(Map.Entry<Byte, String> entry:huffCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        //创建一个集合,用于存byte
        List<Byte> list = new ArrayList<>();
        //处理字符串
        for(int i=0;i<sb.length();) {
            int count=1;
            boolean flag = true;
            Byte b=null;
            //截取出一个byte
            while(flag) {
                String key = sb.substring(i, i+count);
                b = map.get(key);
                if(b==null) {
                    count++;
                }else {
                    flag=false;
                }
            }
            list.add(b);
            i+=count;
        }
        //把集合转为数组
        byte[] b = new byte[list.size()];
        for(int i=0;i<b.length;i++) {
            b[i]=list.get(i);
        }
        return b;
    }

    private static String byteToBitStr(boolean flag,byte b) {
        int temp=b;
        if(flag) {
            temp|=256;
        }
        String str = Integer.toBinaryString(temp);
        if(flag) {
            return str.substring(str.length()-8);
        }else {
            return str;
        }
    }

    /**
	 * 进行赫夫曼编码压缩的方法
	 */
    private static byte[] huffmanZip(byte[] bytes) {
        //先统计每一个byte出现的次数,并放入一个集合中
        List<Node> nodes = getNodes(bytes);
        //创建一颗赫夫曼树
        Node tree = createHuffmanTree(nodes);
        //创建一个赫夫曼编码表
        Map<Byte, String> huffCodes = getCodes(tree);
        //编码
        byte[] b = zip(bytes,huffCodes);
        return b;
    }

    /**
	 * 进行赫夫曼编码
	 */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffCodes) {
        StringBuilder sb = new StringBuilder();
        //把需要压缩的byte数组处理成一个二进制的字符串
        for(byte b:bytes) {
            sb.append(huffCodes.get(b));
        }
        //定义长度
        int len;
        if(sb.length()%8==0) {
            len=sb.length()/8;
        }else {
            len=sb.length()/8+1;
        }
        //用于存储压缩后的byte
        byte[] by = new byte[len];
        //记录新byte的位置
        int index = 0;
        for(int i=0;i<sb.length();i+=8) {
            String strByte;
            if(i+8>sb.length()) {
                strByte = sb.substring(i);
            }else {
                strByte = sb.substring(i, i+8);
            }
            byte byt = (byte)Integer.parseInt(strByte, 2);
            by[index]=byt;
            index++;
        }
        return by;
    }

    //用于临时存储路径
    static StringBuilder sb = new StringBuilder();
    //用于存储赫夫曼编码
    static Map<Byte, String> huffCodes = new HashMap<>();
    /**
	 * 根据赫夫曼树获取赫夫曼编码
	 */
    private static Map<Byte, String> getCodes(Node tree) {
        if(tree==null) {
            return null;
        }
        getCodes(tree.left,"0",sb);
        getCodes(tree.right,"1",sb);
        return huffCodes;
    }

    //从赫夫曼树转变为赫夫曼编码
    private static void getCodes(Node node, String code, StringBuilder sb) {
        StringBuilder sb2 = new StringBuilder(sb);
        sb2.append(code);
        if(node.data==null) {
            getCodes(node.left, "0", sb2);
            getCodes(node.right, "1", sb2);
        }else {
            huffCodes.put(node.data, sb2.toString());
        }
    }

	//创建赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while(nodes.size()>1) {
            //排序
            Collections.sort(nodes);
            //取出两个权值最低的二叉树
            Node left = nodes.get(nodes.size()-1);
            Node right = nodes.get(nodes.size()-2);
            //创建一颗新的二叉树
            Node parent = new Node(null, left.weight+right.weight);
            //把之前取出来的两颗二叉树设置为新创建的二叉树的子树
            parent.left=left;
            parent.right=right;
            //把前面取出来的两颗二叉树删除
            nodes.remove(left);
            nodes.remove(right);
            //把新创建的二叉树放入集合中
            nodes.add(parent);
        }
        return nodes.get(0);
    }

    //把byte数组转为node集合
    private static List<Node> getNodes(byte[] bytes) {
        List<Node> nodes = new ArrayList<>();
        //存储每一个byte出现了多少次。
        Map<Byte, Integer> counts = new HashMap<>();
        //统计每一个byte出现的次数
        for(byte b:bytes) {
            Integer count = counts.get(b);
            if(count==null) {
                counts.put(b, 1);
            }else {
                counts.put(b, count+1);
            }
        }
        //把每一个键值对转为一个node对象
        for(Map.Entry<Byte, Integer> entry:counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }
}
二叉排序树(查找树BST)
package com.atguigu.binarysorttree;

public class BinarySortTreeDemo {

	public static void main(String[] args) {
		int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
		BinarySortTree binarySortTree = new BinarySortTree();
		//循环的添加结点到二叉排序树
		for(int i = 0; i< arr.length; i++) {
			binarySortTree.add(new Node(arr[i]));
		}
		
		//中序遍历二叉排序树
		System.out.println("中序遍历二叉排序树~");
		binarySortTree.infixOrder(); // 1, 3, 5, 7, 9, 10, 12
		
		//测试一下删除叶子结点
	    
	    binarySortTree.delNode(12);
	   
	 
	    binarySortTree.delNode(5);
	    binarySortTree.delNode(10);
	    binarySortTree.delNode(2);
	    binarySortTree.delNode(3);
		   
	    binarySortTree.delNode(9);
	    binarySortTree.delNode(1);
	    binarySortTree.delNode(7);
	    
		
		System.out.println("root=" + binarySortTree.getRoot());
		
		
		System.out.println("删除结点后");
		binarySortTree.infixOrder();
	}

}

//创建二叉排序树
class BinarySortTree {
	private Node root;

	//查找要删除的结点
	public Node search(int value) {
		if(root == null) {
			return null;
		} else {
			return root.search(value);
		}
	}
	
	//查找父结点
	public Node searchParent(int value) {
		if(root == null) {
			return null;
		} else {
			return root.searchParent(value);
		}
	}
	
	//编写方法: 
	//1. 返回的 以node 为根结点的二叉排序树的最小结点的值
	//2. 删除node 为根结点的二叉排序树的最小结点
	/**
	 * @param node 传入的结点(当做二叉排序树的根结点)
	 * @return 返回的 以node 为根结点的二叉排序树的最小结点的值
	 */
	public int delRightTreeMin(Node node) {
		Node target = node;
		//循环的查找左子节点,就会找到最小值
		while(target.left != null) {
			target = target.left;
		}
		//这时 target就指向了最小结点
		//删除最小结点
		delNode(target.value);
		return target.value;
	}
	
	
	//删除结点//根据值删除节点
	public void delNode(int value) {
		if(root == null) {
			return;
		}else {
			//1.需求先去找到要删除的结点  targetNode
			Node targetNode = search(value);
			//如果没有找到要删除的结点
			if(targetNode == null) {
				return;
			}
			//如果我们发现当前这颗二叉排序树只有一个结点
			if(root.left == null && root.right == null) {
				root = null;//处理树的root
				return;
			}//下面else是由多个结点
			
			//去找到targetNode的父结点
			Node parent = searchParent(value);
			//如果要删除的结点是叶子结点
			if(targetNode.left == null && targetNode.right == null) {
				//判断targetNode 是父结点的左子结点,还是右子结点
				if(parent.left != null && parent.left.value == value) { //是左子结点//判断左值的条件是左面有结点才能判断
					parent.left = null;
				} else if (parent.right != null && parent.right.value == value) {//是由子结点
					parent.right = null;
				}
			} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点
				int minVal = delRightTreeMin(targetNode.right);
				targetNode.value = minVal;
				
			} else { // 删除只有一颗子树的结点
				//如果要删除的结点有左子结点 
				if(targetNode.left != null) {
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.left;
						} else { //  targetNode 是 parent 的右子结点
							parent.right = targetNode.left;
						} 
					} else {
						root = targetNode.left;
					}
				} else { //如果要删除的结点有右子结点 
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.right;
						} else { //如果 targetNode 是 parent 的右子结点
							parent.right = targetNode.right;
						}
					} else {
						root = targetNode.right;
					}
				}
			}
		}
	}
	
	//添加结点的方法
	public void add(Node node) {
		if(root == null) {
			root = node;//如果root为空则直接让root指向node
		} else {
			root.add(node);
		}
	}
	//中序遍历
	public void infixOrder() {
		if(root != null) {
			root.infixOrder();
		} else {
			System.out.println("二叉排序树为空,不能遍历");
		}
	}
}

//创建Node结点
class Node {
	int value;
	Node left;
	Node right;
	public Node(int value) {
		this.value = value;
	}
	
	//查找要删除的结点//根据值先找到结点
	public Node search(int value) {//返回删除的结点,没有则返回null
		if(value == this.value) { //找到就是该结点
			return this;
		} else if(value < this.value) {//如果查找的值小于当前结点,向左子树递归查找
			//如果左子结点为空
			if(this.left  == null) {
				return null;
			}
			return this.left.search(value);
		} else { //如果查找的值不小于当前结点,向右子树递归查找
			if(this.right == null) {
				return null;
			}
			return this.right.search(value);
		}
		
	}
	//查找要删除结点的父结点
	/**
	 * @param value 要找到的结点的值
	 * @return 返回的是要删除的结点的父结点,如果没有就返回null
	 */
	public Node searchParent(int value) {
		//如果当前结点就是要删除的结点的父结点,就返回
		if((this.left != null && this.left.value == value) || 
				(this.right != null && this.right.value == value)) {
			return this;
		} else {
			//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
			if(value < this.value && this.left != null) {
				return this.left.searchParent(value); //向左子树递归查找
			} else if (value >= this.value && this.right != null) {
				return this.right.searchParent(value); //向右子树递归查找
			} else {
				return null; // 没有找到父结点
			}
		}
	}
	
	@Override
	public String toString() {
		return "Node [value=" + value + "]";
	}

	//添加结点的方法
	//递归的形式添加结点,注意需要满足二叉排序树的要求
	public void add(Node node) {
		if(node == null) {
			return;
		}
		//判断传入的结点的值,和当前子树的根结点的值关系
		if(node.value < this.value) {
			//如果当前结点左子结点为null
			if(this.left == null) {
				this.left = node;
			} else {
				//递归的向左子树添加
				this.left.add(node);
			}
		} else { //添加的结点的值大于 当前结点的值
			if(this.right == null) {
				this.right = node;
			} else {
				//递归的向右子树添加
				this.right.add(node);
			}
		}
	}
	
	//中序遍历
	public void infixOrder() {
		if(this.left != null) {
			this.left.infixOrder();
		}
		System.out.println(this);
		if(this.right != null) {
			this.right.infixOrder();
		}
	}
	
}
2-3树

2-3树是一棵自平衡的多路查找树,它并不是一棵二叉树,具有如下性质:

(1)每个节点有1个或2个key,对应的子节点为2个子节点或3个子节点;

(2)所有叶子节点到根节点的长度一致;即叶子在同一层

(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key。

2-3树例图

为什么会有2-3树这种数据结构呢?是因为他的查询复杂度比平衡二叉树还高吗?

其实不是的,实际上2-3树的查询时间复杂度也是为 O(logN) ,而出现这种多路查找树,主要是跟内存与磁盘交互有关。我们知道在内存IO的速度比磁盘IO要快的多的多,但是同样空间大小的内存比硬盘要贵的多的多,像TB级别的数据库不可能全部读出来放到内存中去,太过昂贵,而且也没必要,大部分数据是不经常用的,所以就需要内存与外存互相结合,而如果用平衡二叉树这种数据结构,在大数据量的情况下,树肯定会很高,此时查个数据对磁盘读个几千上万次那肯定是不行的(有人可能说把数据的索引文件全部放到内存中,然后把源数据放在硬盘中,这样在内存中定位到源数据Id,然后去外存中取源数据,这样肯定是不行的,不要以为索引文件很小,像搜索引擎的倒排索引文件比源文件还要大),所以用多路查找树这种数据结构,高阶的情况下,树不用很高就可以标识很大的数据量了,检索次数就大大减少了,用这种数据结构去磁盘中存取数据,磁盘IO次数的次数也会很少。

插入:

插入一定在叶子结点。

  • (1)如果待插入的节点只有1个key,则直接插入即可;
  • (2)如果待插入的节点有2个key,则对节点进行分裂,即2个key加上待插入的key,这3个key分裂成1个key跟两个子节点,然后将分裂之后的3个key中的父节点看作向上层插入的key,然后重复(1)、(2)步骤,直到满足2-3树的定义性质。
删除:

两个判断:

  • ①删除的是什么节点?
  • ②删除了节点之后是否符合满足2-3树的性质?

2-3树有4种节点:1.仅1个key的叶子节点;2.有 2个key的叶子节点;3.仅1个key的非叶子节点;4.有2个key的非叶子节点。即 1个key与2个key的节点是否为叶子节点 的组合。下面就从简单到复杂的情况开始分析:

  • (1)当删除的节点是2个key的叶子节点,则将要删除的目标key删除即可,此时原来待删除的2个key的叶子节点,变成1个key的叶子节点,但是符合2-3树2-3树删除情况(1)
  • (2)当删除的节点是2个key的非叶子节点,则此时使用中序遍历找到待删除节点的后继节点,然后将后继节点与待删除节点位置互换,此时就将问题转化为删除节点为叶子节点(平衡树的非叶子节点中序遍历后继节点肯定叶子节点),如果该叶子是2个key,则跟情况(1)一样,如果该节点是只有1个key,则跟后面的情况(4)一样;2-3树删除情况(2)-1
  • (3)当删除的节点是1个key的非叶子节点,实际上操作跟情况(2)是一样的,即使用中序遍历找到待删除节点的后继节点,然后将后继节点与待删除节点位置互换,此时问题转化为删除节点为叶子节点
  • (4)当删除的节点是1个key的叶子节点,则将节点删除,此时树肯定不满足2-3树的性质,也即肯定需要调整,但要分情况来进行调整,而总结起来就是当前待删除的1个key的叶子节点,兄弟节点与父节点,分别是1个key还是2个key,即:
    • a.当父节点是1个key(即此时仅有一个兄弟节点),兄弟节点是2个key,则将兄弟节点的一个key上移成父节点,而父节点下移成子节点,也即跟2个key中插入新节点类似,拆成一父两子,此时树满足2-3树,完成调整。
    • b.当父节点是1个key,兄弟节点也是1个key,则此时将父节点与兄弟节点合并,将合并后的节点看成当前节点,然后重复(4)的判断,即判断合并后的当前节点的兄弟节点与父节点的情况,然后走对应的a.b.c处理,直到满足2-3树,完成调整。
    • c.当父节点是2个key,即此时有两个兄弟节点,而兄弟节点又可能有多种情况,穷举起来有:删除节点的位置左中右3个,以及另外两个兄弟节点是否为1个key或2个key的4种情况,总共3*4=12种。即,
      • i.若删除的是左或右节点,且中间节点只有1个key,则此时父节点的一个key下移,与中间节点合并,此时父节点为1个key,两个子节点,树满足2-3树,完成调整;2-3树删除情况(4ci)
      • ii.若删除的是左或右节点,且中间节点有2个key,则此时父节点的一个key下移,中间节点的一个key上移与父节点合并,此时父节点为2个key,3个子节点,树满足2-3树,完成调整;
      • iii.若删除的是中间节点,且右节点只有1个key,则此时父节点的一个key下移,与右节点合并,此时父节点为1个key,两个子节点,树满足2-3树,完成调整;
      • iv.若删除的是中间节点,且右节点有2个key,则此时父节点的一个key下移,右节点的一个key上移与父节点合并,此时父节点为2个key,3个子节点,树满足2-3树,完成调整。2-3树删除情况(4civ)-2

注:i与ii删除左或右节点两种情况,中间节点1个key或2个key两种情况,兄弟节点1个key或2个key两种情况,总共 2x2x2=8 种;删除中间节点一种情况,iii与iv右节点1个key或2个key两种情况,左节点1个或2个key两种情况,总共 1x2x2=4 种; 4+8=12 种全齐,虽然场景有12种,但是处理的方式只有2种,一种是父节点下移与子节点合并,另一种是父节点下移成单独一个子节点,然后2个key的子节点上移一个key与父节点合并。

多路查找树

二叉树的问题分析:
二叉树的操作效率较高,但是也存在问题。
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:
问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度.

B树:

B-tree树即B树,B即Balanced,平衡的意思。有人把B-tree翻译成B-树,容易让人产生误解。会以为B-树是一种树,而B树又是另一种树。实际上,B-tree就是指的B树。

对于m叉树,B树的特征:

  • 1)树中每个结点至多有m 棵子树(注:m指的是树的阶);
  • 2)根节点的儿子数为:[2,M];(除非根节点为唯一结点)
  • 3)除根结点之外的所有非叶子结点子树:[ceil(m/2),m] ,(ceil为向上取整。);
  • 4)所有的非叶子结点中包含以下数据:(n,A0,K1,A1,K2,…,Kn,An)
  • 所有叶子结点在同一层

查找分析:

  • 1 在B树中查找结点
  • 2 在结点中找关键字
  • 因为B树通常存储在磁盘上,所以1操作是在磁盘上进行的
  • 2 操作是在内存中进行的
  • 即在磁盘上找到指针p所指结点后,先将结点中的信息读入内存,然后再利用顺序查找或折半查找查询等于K的关键字。
  • 显然,在磁盘上进行一次查找耗费的时间多得多,因此,在磁盘上进行查找的次数,即待查找自所在结点再B树上的层次数,是决定B树查找效率的首要因素。

B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。

在有N个关键字的B树上进行查找时,从根节点到关键字所在结点查找最深高度是

h < = l o g ⌈ m / 2 ⌉ ( N + 1 2 ) + 1 h<=log_{\lceil m/2\rceil }(\frac{N+1}{2})+1 h<=logm/2(2N+1)+1

为什么非叶结点至少为ceil(m/2):

  • 如果有一个非叶结点的子树小于m/2,那么这个结点的子树很少,压力很小,为什么不帮别的同级结点分担一些呢?
  • 假如分担了一些后会造成同级结点的子树没有同时大于m/2。即没法分担了,同级非叶结点他们也刚好只有ceil(m/2)个结点了,他们说你给我分担了我又得分担别人的,何必折腾呢?那么看似根本用不到m阶的B树啊,m-1阶的B数可能也能存下现有结点啊。那么我们此时的问题是用不到m阶B树,那m-1的B树能存下当前结点吗?
  • 我们知道满完全二叉树的总结点树是 2 h − 1 2^h-1 2h1,第i层有 2 i − 1 2^i-1 2i1个结点。同理对于满完全m叉树, m h − 1 m^h-1 mh1,第i层有 m i − 1 m^{i-1} mi1个结点。
  • 我们考虑情况最多的情况,前h-1层全满,第h-1层非叶结点只有一个非叶结点关联了ceil(m)-1个叶结点,其余都关联了ceil(m/2)个结点。此时他的总结点个数为: m h − 1 + m h − 1 × c e i l ( m / 2 ) − 1 m^{h-1}+m^{h-1}×ceil(m/2)-1 mh1+mh1×ceil(m/2)1
  • 要证明: m h − 1 + m h − 1 × c e i l ( m / 2 ) − 1 < ( m − 1 ) h − 1 m^{h-1}+m^{h-1}×ceil(m/2)-1 <(m-1)^h-1 mh1+mh1×ceil(m/2)1<(m1)h1
  • 即要证明: m h − 1 + m h − 1 × c e i l ( m / 2 ) < ( m − 1 ) h m^{h-1}+m^{h-1}×ceil(m/2) <(m-1)^h mh1+mh1×ceil(m/2)<(m1)h
  • 即可以证明:
    • m为偶数时: m h − 1 + m h − 1 × m / 2 < ( m − 1 ) h m^{h-1}+m^{h-1}×m/2 <(m-1)^h mh1+mh1×m/2<(m1)h=== m h − 1 + m h / 2 < ( m − 1 ) h m^{h-1}+m^{h}/2 <(m-1)^h mh1+mh/2<(m1)h
    • m为奇数时: m h − 1 + m h − 1 × ( m + 1 ) / 2 < ( m − 1 ) h m^{h-1}+m^{h-1}×(m+1)/2 <(m-1)^h mh1+mh1×(m+1)/2<(m1)h

文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入

将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中

B+树

B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找

  • 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
  • 不可能在非叶子结点命中
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  • 更适合文件索引系统
  • 有n棵子树的结点中含有n个关键字
  • 所有的叶结点中包含了全部关键字的信息,即指向含这些关键字记录的指针,且叶子结点本身依关键字的大小从小到大顺序连接
  • 所有非重点结点可以看成是索引部分,结点中仅含有其子树(根节点)中最大(或最小)关键字

B*树定义了非叶子结点关键字个数至少为(2/3)×M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。

B*树分配新结点的概率比B+树要低,空间使用率更高

平衡二叉树avl

Self-balancing binary search tree

AVL是个人名

深度为h的平衡树最少结点数为Nh,N0=0,N1=1,N2=2,且

N h = N h − 1 + N h − 2 + 1 N_h=N_{h-1}+N_{h-2}+1 Nh=Nh1+Nh2+1

为了保证树的结构左右两端数据大致平衡降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树,使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1

https://blog.csdn.net/qq_25940921/article/details/82183093

左旋

如图,对于E结点的左旋,即固定E与E的父节点的连线l,把E往左面放,那自然就轮到S连接上原来的连线l了

左旋

右旋

同样,对于S的右旋,我们也是把S放到那条线的右面

右旋

总结:一个节点是可以在一条线上任意滑动的(主要都没有涉及子树为空)

插入新结点:

由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后,有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右

关注点是要往哪个结点上(设置A)插入,然后向上回溯2代,判断A在祖父结点的位置

在左结点的左结点上插入新的结点

平衡二叉树旋转时候不计入插入的结点,然后可能调整父、祖父两代

平衡二叉树左左情况进行右旋平衡二叉树右右情况进行左旋

如下图,我们永远以A结点为轴旋转

平衡二叉树左右情况进行左旋再右旋

中间结点先旋,然后第1结点再旋

平衡二叉树右左情况进行右旋再左旋

平衡二叉树的构建:

平衡二叉树构建的过程,就是节点插入的过程,插入失衡情况就上面4种

删除

普通二叉排序树删除节点的过程:

  • (1)如果删除的是 叶节点,可以直接删除;
  • (2)如果被删除的元素有一个子节点,可以将子节点直接移到被删除元素的位置;
  • (3)如果有两个子节点,这时候就采用中序遍历,找到待删除的节点的后继节点,将其与待删除的节点互换,此时待删除节点的位置已经是叶子节点,可以直接删除。
  • 为什么是后继结点: 后继结点一定没有左节点

平衡二叉树节点的删除:

删除的情况会复杂一点,复杂的原因主要在于删除了节点之后要维系二叉树的平衡,但是删除二叉树节点总结起来就两个判断:

  • ①删除的是什么类型的节点?
    • 1.叶子节点;
    • 2.只有左子树或只有右子树;
    • 3.既有左子树又有右子树。
  • ②删除了节点之后是否导致失衡?

针对①这三种节点类型,再引入判断②,所以处理思路分别是:

(1)当删除的节点是叶子节点,则将节点删除,然后从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;如果中间过程发现失衡,则判断属于哪种类型的失衡(左左,左右,右左,右右),然后进行调整。

(2)删除的节点只有左子树或只有右子树,这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除,然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了,从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,如果中间过程发现失衡,则根据失衡的类型进行调整。

(3)删除的节点既有左子树又有右子树,这种情况又比上面这种多一步,就是中序遍历,找到待删除节点的前驱或者后驱都行,然后与待删除节点互换位置,然后把待删除的节点删掉,后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。

最后总结一下,平衡二叉树是一棵高度平衡的二叉树,所以查询的时间复杂度是 O(logN) 。插入的话上面也说,失衡的情况有4种,左左,左右,右左,右右,即一旦插入新节点导致失衡需要调整,最多也只要旋转2次,所以,插入复杂度是 O(1) ,但是平衡二叉树也不是完美的,也有缺点,从上面删除处理思路中也可以看到,就是删除节点时有可能因为失衡,导致需要从删除节点的父节点开始,不断的回溯到根节点,如果这棵平衡二叉树很高的话,那中间就要判断很多个节点。所以后来也出现了综合性能比其更好的树—-红黑树,后面再讲。

问题分析
1. 当符合右旋转的条件时
2. 如果它的左子树的右子树高度大于它的左子树的高度
3. 先对当前这个结点的左节点进行左旋转
4. 在对当前结点进行右旋转的操作即可
package com.atguigu.avl;

public class AVLTreeDemo {

	public static void main(String[] args) {
		//int[] arr = {4,3,6,5,7,8};
		//int[] arr = { 10, 12, 8, 9, 7, 6 };
		int[] arr = { 10, 11, 7, 6, 8, 9 };  
		//创建一个 AVLTree对象
		AVLTree avlTree = new AVLTree();
		//添加结点
		for(int i=0; i < arr.length; i++) {
			avlTree.add(new Node(arr[i]));
		}
		
		//遍历
		System.out.println("中序遍历");
		avlTree.infixOrder();
		
		System.out.println("在平衡处理~~");
		System.out.println("树的高度=" + avlTree.getRoot().height()); //3
		System.out.println("树的左子树高度=" + avlTree.getRoot().leftHeight()); // 2
		System.out.println("树的右子树高度=" + avlTree.getRoot().rightHeight()); // 2
		System.out.println("当前的根结点=" + avlTree.getRoot());//8
		
	}
}

// 创建AVLTree
class AVLTree {
	private Node root;

	public Node getRoot() {
		return root;
	}

	// 查找要删除的结点
	public Node search(int value) {
		if (root == null) {
			return null;
		} else {
			return root.search(value);
		}
	}

	// 查找父结点
	public Node searchParent(int value) {
		if (root == null) {
			return null;
		} else {
			return root.searchParent(value);
		}
	}

	// 编写方法:
	// 1. 返回的 以node 为根结点的二叉排序树的最小结点的值
	// 2. 删除node 为根结点的二叉排序树的最小结点
	/**
	 * @param node 传入的结点(当做二叉排序树的根结点)
	 * @return 返回的 以node 为根结点的二叉排序树的最小结点的值
	 */
	public int delRightTreeMin(Node node) {
		Node target = node;
		// 循环的查找左子节点,就会找到最小值
		while (target.left != null) {
			target = target.left;
		}
		// 这时 target就指向了最小结点
		// 删除最小结点
		delNode(target.value);
		return target.value;
	}

	// 删除结点
	public void delNode(int value) {
		if (root == null) {
			return;
		} else {
			// 1.需求先去找到要删除的结点 targetNode
			Node targetNode = search(value);
			// 如果没有找到要删除的结点
			if (targetNode == null) {
				return;
			}
			// 如果我们发现当前这颗二叉排序树只有一个结点
			if (root.left == null && root.right == null) {
				root = null;
				return;
			}

			// 去找到targetNode的父结点
			Node parent = searchParent(value);
			// 如果要删除的结点是叶子结点
			if (targetNode.left == null && targetNode.right == null) {
				// 判断targetNode 是父结点的左子结点,还是右子结点
				if (parent.left != null && parent.left.value == value) { // 是左子结点
					parent.left = null;
				} else if (parent.right != null && parent.right.value == value) {// 是由子结点
					parent.right = null;
				}
			} else if (targetNode.left != null && targetNode.right != null) { // 删除有两颗子树的节点
				int minVal = delRightTreeMin(targetNode.right);
				targetNode.value = minVal;

			} else { // 删除只有一颗子树的结点
				// 如果要删除的结点有左子结点
				if (targetNode.left != null) {
					if (parent != null) {
						// 如果 targetNode 是 parent 的左子结点
						if (parent.left.value == value) {
							parent.left = targetNode.left;
						} else { // targetNode 是 parent 的右子结点
							parent.right = targetNode.left;
						}
					} else {
						root = targetNode.left;
					}
				} else { // 如果要删除的结点有右子结点
					if (parent != null) {
						// 如果 targetNode 是 parent 的左子结点
						if (parent.left.value == value) {
							parent.left = targetNode.right;
						} else { // 如果 targetNode 是 parent 的右子结点
							parent.right = targetNode.right;
						}
					} else {
						root = targetNode.right;
					}
				}
			}
		}
	}

	// 添加结点的方法
	public void add(Node node) {
		if (root == null) {
			root = node;// 如果root为空则直接让root指向node
		} else {
			root.add(node);
		}
	}

	// 中序遍历
	public void infixOrder() {
		if (root != null) {
			root.infixOrder();
		} else {
			System.out.println("二叉排序树为空,不能遍历");
		}
	}
}

// 创建Node结点
class Node {
	int value;
	Node left;
	Node right;

	public Node(int value) {
		this.value = value;
	}

	// 返回左子树的高度
	public int leftHeight() {
		if (left == null) {
			return 0;
		}
		return left.height();
	}

	// 返回右子树的高度
	public int rightHeight() {
		if (right == null) {
			return 0;
		}
		return right.height();
	}

	// 返回 以该结点为根结点的树的高度
	public int height() {
		return Math.max(left == null ? 0 : left.height(), 
                        right == null ? 0 : right.height()) + 1;
        //精髓:return递归,每层递归+1
	}
	
	//左旋转方法
	private void leftRotate() {
		//创建新的结点,以当前根结点的值
		Node newNode = new Node(value);
		//把新的结点的左子树设置成当前结点的左子树
		newNode.left = left;
		//把新的结点的右子树设置成带你过去结点的右子树的左子树
		newNode.right = right.left;
		//把当前结点的值替换成右子结点的值
		value = right.value;
		//把当前结点的右子树设置成当前结点右子树的右子树
		right = right.right;
		//把当前结点的左子树(左子结点)设置成新的结点
		left = newNode;
	}
	
	//右旋转
	private void rightRotate() {
		Node newNode = new Node(value);
		newNode.right = right;
		newNode.left = left.right;
		value = left.value;
		left = left.left;
		right = newNode;
	}

	// 查找要删除的结点
	/**
	 * @param value 希望删除的结点的值
	 * @return 如果找到返回该结点,否则返回null
	 */
	public Node search(int value) {
		if (value == this.value) { // 找到就是该结点
			return this;
		} else if (value < this.value) {// 如果查找的值小于当前结点,向左子树递归查找
			// 如果左子结点为空
			if (this.left == null) {
				return null;
			}
			return this.left.search(value);
		} else { // 如果查找的值不小于当前结点,向右子树递归查找
			if (this.right == null) {
				return null;
			}
			return this.right.search(value);
		}
	}

	// 查找要删除结点的父结点
	public Node searchParent(int value) {//找不到返回null,找到返回该Node
		// 如果当前结点就是要删除的结点的父结点,就返回
		if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
			return this;
		} else {
			// 如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
			if (value < this.value && this.left != null) {
				return this.left.searchParent(value); // 向左子树递归查找
			} else if (value >= this.value && this.right != null) {
				return this.right.searchParent(value); // 向右子树递归查找
			} else {
				return null; // 没有找到父结点
			}
		}
	}

	@Override
	public String toString() {
		return "Node [value=" + value + "]";
	}

	// 添加结点的方法
	// 递归的形式添加结点,注意需要满足二叉排序树的要求
	public void add(Node node) {
		if (node == null) {
			return;
		}

		// 判断传入的结点的值,和当前子树的根结点的值关系
		if (node.value < this.value) {
			// 如果当前结点左子结点为null
			if (this.left == null) {
				this.left = node;
			} else {
				// 递归的向左子树添加
				this.left.add(node);
			}
		} else { // 添加的结点的值大于 当前结点的值
			if (this.right == null) {
				this.right = node;
			} else {
				// 递归的向右子树添加
				this.right.add(node);
			}
		}
		
		//当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
		if(rightHeight() - leftHeight() > 1) {
			//如果它的右子树的左子树的高度大于它的右子树的右子树的高度
			if(right != null && right.leftHeight() > right.rightHeight()) {
				//先对右子结点进行右旋转
				right.rightRotate();
				//然后在对当前结点进行左旋转
				leftRotate(); //左旋转..
			} else {
				//直接进行左旋转即可
				leftRotate();
			}
			return ; //必须要!!!
		}
		
		//当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
		if(leftHeight() - rightHeight() > 1) {
			//如果它的左子树的右子树高度大于它的左子树的高度
			if(left != null && left.rightHeight() > left.leftHeight()) {
				//先对当前结点的左结点(左子树)->左旋转
				left.leftRotate();
				//再对当前结点进行右旋转
				rightRotate();
			} else {
				//直接进行右旋转即可
				rightRotate();
			}
		}
	}

	// 中序遍历
	public void infixOrder() {
		if (this.left != null) {
			this.left.infixOrder();
		}
		System.out.println(this);
		if (this.right != null) {
			this.right.infixOrder();
		}
	}
}

2.2 红黑树

参考:https://mp.weixin.qq.com/s/X3zYwQXxq93P_XUzFmKluQ

插入和查询复杂度都是logn

2.2.1 五个特性

性质1:每个节点要么是黑色,要么是红色。

性质2:根节点是黑色。 (黑土地孕育黑树根 )

性质3:每个叶子节点(null)是黑色。(空节点也是结点)

性质4:没有两个相邻的红色节点并没有说不能出现连续的黑色节点

其他说法:没有两个相邻的红色节点。红色节点不能有红色父节点或红色子节点。红色节点的儿子是黑色的。

插入的结点先定义为红色再改

性质5:每个结点到他的叶子结点的路径上的黑结点数目相同。

从节点(包括根)到其任何后代NULL节点(叶子结点下方挂的两个空节点,并且认为他们是黑色的)的每条路径都具有相同数量的黑色节点

从性质5又可以推出:

  • 性质5.1:如果一个结点存在黑色子结点,那么该结点肯定有两个子结点

图1就是一颗简单的红黑树。其中Nil为叶子结点,并且它是黑色的。(值得提醒注意的是,在Java中,叶子结点是为null的结点。)

img

红黑树并不是一个 完美 平衡二叉查找树,从图1可以看到,根结点P的左子树显然比右子树高,但左子树和右子树的黑结点的层数是相等的,也即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点(性质5)。所以我们叫红黑树这种平衡为黑色完美平衡

下面代码是java8中HashMap的内部类:红黑树TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;//是否为红结点
    
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
}

介绍到此,为了后面讲解不至于混淆,我们还需要来约定下红黑树一些结点的叫法

img

我们把正在处理(遍历)的结点叫做当前结点,如图2中的D,它的父亲叫做父结点,它的父亲的另外一个子结点叫做兄弟结点,父亲的父亲叫做祖父结点。

前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。

  • 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图3。
  • 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如图4。
  • 变色:结点的颜色由红变黑或由黑变红。

img

img

我们先忽略颜色,可以看到旋转操作不会影响旋转结点的父结点,父结点以上的结构还是保持不变的

左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了。
右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。

所以旋转操作是局部的。另外可以看出旋转能保持红黑树平衡的一些端详了:当一边子树的结点少了,那么向另外一边子树“借”一些结点;当一边子树的结点多了,那么向另外一边子树“租”一些结点。

但要保持红黑树的性质,结点不能乱挪,还得靠变色了。怎么变?具体情景又不同变法,后面会具体讲到,现在只需要记住红黑树总是通过旋转和变色达到自平衡

balabala了这么多,相信你对红黑树有一定印象了,那么现在来考考你:

*思考题1:黑结点可以同时包含一个红子结点和一个黑子结点吗?* (答案见文末)

2.2.2 红黑树查找

因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:

  1. 从根结点开始查找,把根结点设置为当前结点;
  2. 若当前结点为空,返回null;
  3. 若当前结点不为空,用当前结点的key跟查找key作比较;
  4. 若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
  5. 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
  6. 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;

如图5所示。

img

图5 二叉树查找流程图

非常简单,但简单不代表它效率不好。正由于红黑树总保持黑色完美平衡,所以它的查找最坏时间复杂度为O(2lgN),也即整颗树刚好红黑相隔的时候。能有这么好的查找效率得益于红黑树自平衡的特性,而这背后的付出,红黑树的插入操作功不可没~


2.2.3 红黑树插入

插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父结点很简单,跟查找操作区别不大:

  • 父结点是黑色:直接插入
  • 父节点是红色
    • 叔叔是空的:旋转+变色
    • 叔叔是红色:父和叔变成黑色,祖父变为红色
    • 叔叔是黑色:(可能是插入后往上调整时候造成的)父节点左旋+变色父和祖父。此外还要

找到插入位置没什么好讲的,插入到叶结点而已。

插入位置已经找到,把插入结点放到正确的位置就可以啦,但插入结点是应该是什么颜色呢?答案是红色。理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡。

回顾一下最重要的两条性质

  • 性质4:每个红色结点的两个子结点一定都是黑色。

    性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

我的思路

思路:插入的结点为红色,因为红黑树是黑色节点要求严格。

尽管网上有很多插入的方法,但是我还是希望看我的文章的能以下面的方式记忆,因为我觉得我的方法你理解了后很容易记,在这里我只简写几个分类,至于他们代表什么意思,希望你看完下面5个局面后再看看我为什么这么简写,然后如果你觉得我的分类很好的话,我希望收获你的掌声

  • ① 根节点,很简单,不需要特殊记
  • ② 父节点是黑色,我们直接插入红色节点即可,也没什么需要特殊记的,这里还没凸显我所提的优势。下面我开始讲我的简写
  • ③ 3红情况:即新结点,父节点,叔叔结点都是红色。新结点没什么好说的,因为本来就是红的,这是新结点的初始颜色。父和叔是同一层的结点,那同时变色也无所谓了。而祖父结点变色也无所谓,因为祖父代表的是一个子树。这里的无所谓指的是父节点和叔结点的红色与祖父结点的黑色调换下无所谓,调换完还是满足性质的,而又满足了我们插入新结点后的要求。
  • ④ 2红右左:即新结点和父节点是红色,简写里不提的叔叔结点说明他不是红色,即他可以不存在,也可以是黑色,但就是不能是红色,因为我们的简写是2红。此外还有要求,“右”指的是新结点是父节点的右树,“左”指的是父结点是祖父结点的左树。这里写的右左是由顺序的,是从子代到祖代的顺序的。“左”不要理解为父节点是叔结点的左树,没有这个逻辑。我们要做的是让第二个"左"旋成2红右右的情况,结合后面的图比较好理解些。旋完后进入的是逻辑⑤
  • ⑤ 2红左左:即新结点和父节点是红色,“1左”指的是新结点是父节点的左树,“2左”指的是父结点是祖父结点的左树。好了,简写意思说明白了,那怎么处理呢?想想平衡树我们为什么左旋/右旋,因为一个分支高度高太多了,扁担很不平衡了,所以你挑扁担的中心点就往树高的那边挪,即我们要把树高那边的结点作为新的子树根节点。
  • 镜像问题,如2红右右是同样的道理
  • 旋转时候的注意点:旋转的时候只影响插入节点,父节点,祖父节点这几层的结点。
  • 调整完一次后,把原来祖父节点的那一层作为新节点插入到上面一层,然后判断有没有5条性质,再调整。

这就是我的思路,希望提供给你另外一种想法,如果觉得不适合你,那希望你能总结你自己的。这个思路不仅仅是为了解释,同时JDK8的HashMap也是这个顺序分析的,个人认为把我的思路看完后代码大概瞅一眼后就知道他怎么写的了

**局面1:**插入根节点

新结点(A)位于树根,没有父结点。

img

(空心三角形代表结点下面的子树)

这种局面,直接让新结点变色为黑色,规则2得到满足。同时,黑色的根结点使得每条路径上的黑色结点数目都增加了1,所以并没有打破规则5。

img

**局面2:**父节点为黑

新结点(B)的父结点是黑色。

这种局面,新插入的红色结点B并没有打破红黑树的规则,所以不需要做任何调整。

img

局面3: 父节点为红+叔节点为红。

新结点(D)的父结点和叔叔结点都是红色。(3红)

解决:父叔变黑色、祖父变红色

img

上面情况可以直接总结为让BC变黑色,A变红色。下面的说明是分步操作的

两个红色结点B和D连续,违反了规则4。因此我们先让结点B变为黑色:

img

这样一来,结点B所在路径凭空多了一个黑色结点,打破了规则5。因此我们让结点A变为红色:

img

这时候,结点A和C又成为了连续的红色结点,我们再让结点C变为黑色:

img

经过上面的调整,这一局部重新符合了红黑树的规则。

局面4:2红右左

解释:新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的右孩子,父结点(B)是祖父结点的左孩子。(2红右左)

新节点红,父节点红,叔节点黑/无,新节点为父节点右树,父节点为祖父节点左树。

img

此时旋转的根节点:插入节点。只影响父节点

我们以结点B为轴(B上面不影响),做一次左旋转,使得新结点D成为父结点,原来的父结点B成为D的左孩子:

img

这样一来,进入了局面5。

**局面5:**2红左左

解释:新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的左孩子,父结点(B)是祖父结点的左孩子。

解决方案:右旋+换色

此时旋转的结点:父节点。会旋转祖父,但祖父的父节点不动

新节点红,父节点红,叔叔黑/无,新节点为父节点左树,父节点为祖父节点左树。

img

我们以结点A为轴,做一次右旋转,使得结点B成为祖父结点,结点A成为结点B的右孩子:

img

接下来,我们让结点B变为黑色,结点A变为红色:

img

经过上面的调整,这一局部重新符合了红黑树的规则。

以上就是红黑树插入操作所涉及的5种局面。

或许有人会问,如果局面4和局面5当中的父结点B是祖父结点A的右孩子该怎么办呢?

很简单,如果局面4中的父结点B是右孩子,则成为了局面5的镜像,原本的右旋操作改为左旋;如果局面5中的父结点B是右孩子,则成为了局面4的镜像,原本的左旋操作改为右旋。

练习1

给定下面这颗红黑树,新插入的结点是21:

img

显然,新结点21和它的父结点22是连续的红色结点,违背了规则4,我们应该如何调整呢?

让我们回顾一下刚才讲的5种局面,当前的情况符合局面3:

“新结点的父结点和叔叔结点都是红色。”

于是我们经过三次变色,22变为黑色,25变为红色,27变为黑色:

img

经过上面的调整,以结点25为根的子树符合了红黑树规则,但结点25和结点17成为了连续的红色结点,违背规则4。

于是,我们把结点25看做一个新结点,正好符合局面5的镜像:

“新结点的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的右孩子,父结点是祖父结点的右孩子”

于是我们以根结点17为轴进行左旋转,使得结点17成为了新的根结点:

img

接下来,让结点17变为黑色,结点13变为红色:

img

如此一来,我们的红黑树变得重新符合规则。

2.2.4 红黑树删除

红黑树插入已经够复杂了,但删除更复杂,也是红黑树最复杂的操作了。但稳住,胜利的曙光就在前面了!

红黑树的删除操作也包括两部分工作:一查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。

二叉树删除结点找替代结点有3种情情景:

  • 情景1:若删除结点无子结点,直接删除
  • 情景2:若删除结点只有一个子结点,用子结点替换删除结点
  • 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点

情况1,待删除的结点没有子结点:

img

上图中,待删除的结点12是叶子结点,没有孩子,因此直接删除即可:

img

情况2,待删除的结点有一个孩子:

img

上图中,待删除的结点13只有左孩子,于是我们让左孩子结点11取代被删除的结点,结点11以下的结点关系无需变动:

img

情况3,待删除的结点有两个孩子:

img

上图中,待删除的结点5有两个孩子,这种情况比较复杂。此时,我们需要选择与待删除结点最接近的结点来取代它。

上面的例子中,结点3仅小于结点5,结点6仅大于结点5,两者都是合适的选择。但习惯上我们选择仅大于待删除结点的结点,也就是结点6来取代它。

于是我们复制结点6到原来结点5的位置:

img

被选中的结点6,仅大于结点5,因此一定没有左孩子。所以我们按照情况1或情况2的方式,删除多余的结点6:

img

补充说明下,情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树种最左结点。那么可以拿前继结点(删除结点的左子树最右结点)替代吗?可以的。但习惯上大多都是拿后继结点来替代,后文的讲解也是用后继结点来替代。另外告诉大家一种找前继和后继结点的直观的方法(不知为何没人提过,大家都知道?):把二叉树所有结点投射在X轴上,所有结点都是从左到右排好序的,所有目标结点的前后结点就是对应前继和后继结点。如图16所示。

img

图16 二叉树投射x轴后有序

接下来,讲一个重要的思路:**删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!**话很苍白,我们看图17。在不看键值对的情况下,图17的红黑树最终结果是删除了Q所在位置的结点!这种思路非常重要,大大简化了后文讲解红黑树删除的情景!

img

图17 删除结点换位思路

基于此,上面所说的3种二叉树的删除情景可以相互转换并且最终都是转换为情景1!

  • 情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
  • 情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。

二叉树删除结点情景关系图如图18所示。

img

图18 二叉树删除情景转换

综上所述,**删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。**有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了。

同样的,我们也是先来总体看下删除操作的所有情景,如图19所示。

img

图19 红黑树删除情景

哈哈,是的,即使简化了还是有9种情景!但跟插入操作一样,存在左右对称的情景,只是方向变了,没有本质区别。同样的,我们还是来约定下,如图20所示。

img

图20 删除操作结点的叫法约定

图20的字母并不代表结点Key的大小。R表示替代结点,P表示替代结点的父结点,S表示替代结点的兄弟结点,SL表示兄弟结点的左子结点,SR表示兄弟结点的右子结点。灰色结点表示它可以是红色也可以是黑色。

值得特别提醒的是,R是即将被替换到删除结点的位置的替代结点,在删除前,它还在原来所在位置参与树的子平衡,平衡后再替换到删除结点的位置,才算删除完成。

万事具备,我们进入最后的也是最难的讲解。

删除情景1:替换结点是红色结点

我们把替换结点换到了删除结点的位置时,由于替换结点时红色,删除也了不会影响红黑树的平衡,只要把替换结点的颜色设为删除的结点的颜色即可重新平衡。

处理:颜色变为删除结点的颜色

删除情景2:替换结点是黑结点

当替换结点是黑色时,我们就不得不进行自平衡处理了。我们必须还得考虑替换结点是其父结点的左子结点还是右子结点,来做不同的旋转操作,使树重新平衡。

删除情景2.1:替换结点是其父结点的左子结点
删除情景2.1.1:替换结点的兄弟结点是红结点
若兄弟结点是红结点,那么根据性质4,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景,我们按图21处理,得到删除情景2.1.2.3(后续讲解,这里先记住,此时R仍然是替代结点,它的新的兄弟结点SL和兄弟结点的子结点都是黑色)。

处理:

  • 将S设为黑色
  • 将P设为红色
  • 对P进行左旋,得到情景2.1.2.3
  • 进行情景2.1.2.3的处理

img

图21 删除情景2.1.1

删除情景2.1.2:替换结点的兄弟结点是黑结点
当兄弟结点为黑时,其父结点和子结点的具体颜色也无法确定(如果也不考虑自底向上的情况,子结点非红即为叶子结点Nil,Nil结点为黑结点),此时又得考虑多种子情景。

删除情景2.1.2.1:替换结点的兄弟结点的右子结点是红结点,左子结点任意颜色
即将删除的左子树的一个黑色结点,显然左子树的黑色结点少1了,然而右子树又又红色结点,那么我们直接向右子树“借”个红结点来补充黑结点就好啦,此时肯定需要用旋转处理了。如图22所示。

处理:

  • 将S的颜色设为P的颜色
  • 将P设为黑色
  • 将SR设为黑色
  • 对P进行左旋

img

图22 删除情景2.1.2.1

平衡后的图怎么不满足红黑树的性质?前文提醒过,R是即将替换的,它还参与树的自平衡,平衡后再替换到删除结点的位置,所以R最终可以看作是删除的。另外图2.1.2.1是考虑到第一次替换和自底向上处理的情况,如果只考虑第一次替换的情况,根据红黑树性质,SL肯定是红色或为Nil,所以最终结果树是平衡的。如果是自底向上处理的情况,同样,每棵子树都保持平衡状态,最终整棵树肯定是平衡的。后续的情景同理,不做过多说明了。

删除情景2.1.2.2:替换结点的兄弟结点的右子结点为黑结点,左子结点为红结点
兄弟结点所在的子树有红结点,我们总是可以向兄弟子树借个红结点过来,显然该情景可以转换为情景2.1.2.1。图如23所示。

处理:

  • 将S设为红色
  • 将SL设为黑色
  • 对S进行右旋,得到情景2.1.2.1
  • 进行情景2.1.2.1的处理

img

图23 删除情景2.1.2.2

删除情景2.1.2.3:替换结点的兄弟结点的子结点都为黑结点
好了,此次兄弟子树都没红结点“借”了,兄弟帮忙不了,找父母呗,这种情景我们把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。但为什么需要把兄弟结点设为红色呢?显然是为了在P所在的子树中保证平衡(R即将删除,少了一个黑色结点,子树也需要少一个),后续的平衡工作交给父辈们考虑了,还是那句,当每棵子树都保持平衡时,最终整棵总是平衡的。

处理:

  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理

img

图24 情景2.1.2.3

删除情景2.2:替换结点是其父结点的右子结点
好啦,右边的操作也是方向相反,不做过多说明了,相信理解了删除情景2.1后,肯定可以理解2.2。

删除情景2.2.1:替换结点的兄弟结点是红结点
处理:

  • 将S设为黑色
  • 将P设为红色
  • 对P进行右旋,得到情景2.2.2.3
  • 进行情景2.2.2.3的处理

img

图25 删除情景2.2.1

删除情景2.2.2:替换结点的兄弟结点是黑结点
删除情景2.2.2.1:替换结点的兄弟结点的左子结点是红结点,右子结点任意颜色
处理:

  • 将S的颜色设为P的颜色
  • 将P设为黑色
  • 将SL设为黑色
  • 对P进行右旋

img

图26 删除情景2.2.2.1

删除情景2.2.2.2:替换结点的兄弟结点的左子结点为黑结点,右子结点为红结点
处理:

  • 将S设为红色
  • 将SR设为黑色
  • 对S进行左旋,得到情景2.2.2.1
  • 进行情景2.2.2.1的处理

img

图27 删除情景2.2.2.2

删除情景2.2.2.3:替换结点的兄弟结点的子结点都为黑结点
处理:

  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理

img

图28 删除情景2.2.2.3

综上,红黑树删除后自平衡的处理可以总结为:

  1. 自己能搞定的自消化(情景1)
  2. 自己不能搞定的叫兄弟帮忙(除了情景1、情景2.1.2.3和情景2.2.2.3)
  3. 兄弟都帮忙不了的,通过父母,找远方亲戚(情景2.1.2.3和情景2.2.2.3)

哈哈,是不是跟现实中很像,当我们有困难时,首先先自己解决,自己无力了总兄弟姐妹帮忙,如果连兄弟姐妹都帮不上,再去找远方的亲戚了。这里记忆应该会好记点~

最后再做个习题加深理解(请不熟悉的同学务必动手画下):

***习题2:请画出图29的删除自平衡处理过程。

img


思考题和习题答案

思考题1:黑结点可以同时包含一个红子结点和一个黑子结点吗?
答:可以。如下图的F结点:

img

习题1:请画出图15的插入自平衡处理过程。
答:

img

习题2:请画出图29的删除自平衡处理过程。
答:

img

2.2.5 JDK8 HashMap中的 红黑树

博客:https://blog.csdn.net/hancoder/article/details/107829728

参考

红黑树:https://www.cnblogs.com/LiaHon/p/11203229.html

刷题

判断树是对称的

leetcode101

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值