数据结构与算法(11)—— 树
1.堆排序(难)
1.1 基本介绍
对数组升序排列采用大顶堆,对数组降序排列采用小顶堆。
1.2 堆排序的思想
基本思想:
1)将待排序序列构造成一个大顶堆(数组)
2)此时,整个序列的最大值就是堆顶的根节点
3)将其与末尾元素进行交换,此时末尾就为最大值
4)然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如次反复执行,就能得到一个有序序列了。
1.3 代码实现
public class HeapSort {
public static void main(String[] args) {
//将数组升序排列
int[] arr = {4,6,8,5,9,13,17,21,25};
heapSort(arr);
}
//堆排序方法
public static void heapSort(int[] arr){
int temp = 0;
System.out.println("堆排序");
//将无序序列构建成一个堆,根据升序降序选择大顶堆或小顶堆
//从下往上遍历使之成为大顶堆
for (int i = arr.length/2 - 1;i >= 0;i--){
adjustHeap(arr,i,arr.length);
}
System.out.println(Arrays.toString(arr));
//将堆顶元素其与末尾元素进行交换,此时末尾就为最大值
//重新调整结构使其满足堆定义,然后继续交换堆顶元素与末尾元素
for (int j = arr.length-1;j > 0;j--){
//交换动作
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr,0,j);
}
System.out.println(Arrays.toString(arr));
}
//将一个数组(二叉树)调整成大顶堆
/**
* 功能:将二叉树中以i(非叶子节点)为根节点的树调整为大顶堆
* 举例:int arr[] = {4,6,8,5,9} i=1 =====>>>>>得到{4,9,8,5,6}
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 对多少个元素进行调整,length是逐渐减少的
*/
public static void adjustHeap(int[] arr,int i ,int length){
int temp = arr[i];//取出当前元素的值保存在临时变量
//开始调整
//k=i*2+1 k是节点i的左子节点
for (int k = i*2+1;k < length;k = k*2+1){
if (k+1 < length && arr[k] < arr[k+1]){//说明左子节点的值小于右子节点的值
k++; //k指向右子节点
}
if (arr[k] > temp){//如果子节点大于父节点
arr[i] = arr[k];//将子节点中较大的值给i节点
i = k;//!!! i指向k继续比较
}else{
break;//!!!
}
}
//当for循环结束后我们已经将以i为父节点的树的最大值放在了最顶上(局部调整成大顶堆)
arr[i] = temp;//将temp放置到调整之后的位置
}
}
2.赫夫曼树
2.1 基本介绍
给定n个权值作为n个叶子结点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman Tree)
赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近
2.2 构建赫夫曼树的步骤
1)将数据从小到大进行排序,每个数据都是一个节点,每个节点都可以看做是一颗最简单的二叉树;
2)取出根节点权值最小的两颗二叉树
3)组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
4)再将,这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树。
例如:将数组{1,3,6,7,8,13,29}转化为如下赫夫曼树
2.3 代码实现
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13,7,8,3,29,6,1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
//编写一个前序遍历的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else{
System.out.println("树是空树,无法遍历");
}
}
public static Node createHuffmanTree(int[] arr){
//遍历arr数组,将arr的每个元素构建成一个Node,将Node放入ArrayList中
ArrayList<Node> nodes = new ArrayList<>();
for (int value:arr){
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
//先将ArrayList排序
Collections.sort(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对象支持排序,让Node实现Comparable接口
class Node implements Comparable<Node>{
int value;//节点权值
Node left;//指向左子节点
Node right;//指向右子节点
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
//表示从小到大排序
return this.value - o.value;
}
//添加前序遍历的方法以方便测试
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
}
3.赫夫曼编码(Huffman Coding)
3.1 赫夫曼编码原理
1)传输的字符串 ;
2)统计各个字符对应的个数;
3)将上面字符出现的次数构建一颗赫夫曼树,次数作为权值;
注意:这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl一样,都是最小的。
4)根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为0,向右的路径为1;
如图:e对应的赫夫曼编码就是1111。
3.2 实践–数据压缩
功能:根据赫夫曼编码压缩数据的原理,需要创建字符串“i like like like java do you like a java”对应的赫夫曼树。
思路:
(1) Node {data(存放数据),weight(权值),left,right}
//创建Node,带数据和权值
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 int compareTo(Node o) {
//从小到大排序
return this.weight - o.weight;
}
@Override
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();
}
}
}
(2)得到字符串对应的byte[]数组
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes(); //40
List<Node> nodes = getNodes(contentBytes);
(3)编写方法,将准备构建赫夫曼树的Node放入List中
//获取各个树节点组成的集合
/**
*
* @param bytes 接受字节数组
* @return 返回的就是List形式
*/
private static List<Node> getNodes(byte[] bytes){
//创建ArrayList
ArrayList<Node> nodes = new ArrayList<>();
//遍历 统计每个byte出现的次数---->>>>map
HashMap<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);
}
}
//把每个键值对转成Node对象,并加入nodes集合
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
(4)通过List创建对应的赫夫曼树
//通过List创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes){
while (nodes.size() > 1){
//排序,从小到大
Collections.sort(nodes);
//取出第一颗最小的二叉树
Node leftNode = nodes.get(0);
//取出第二颗最小的二叉树
Node rightNode = nodes.get(1);
//创建一颗新的二叉树
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);
}
(5)使用赫夫曼树来生成对应的赫夫曼编码
//生成赫夫曼树对应的赫夫曼编码表
//将赫夫曼编码表存放在Map<Byte,String>形式
static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
//在生成赫夫曼编码表时,需要拼接路径,定义一个Stringbuilder存储某个叶子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
/**
* 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCode集合中
* @param node 传入结点
* @param code 路径:左子节点为0,右子节点为1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2
stringBuilder2.append(code);
if (node != null){//如果node等于空不处理
//判断当前节点是叶子节点还是非叶子节点
if(node.data == null){//非叶子节点
//递归处理
//向左
getCodes(node.left,"0",stringBuilder2);
//向右递归
getCodes(node.right,"1",stringBuilder2);
}else{//说明是叶子节点
//表示找到了某个叶子节点的最后
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
(6)将赫夫曼编码以字符数组的形式输出
//将一个字符串对应的byte[]数组,通过生成的赫夫曼编码表返回一个赫夫曼编码压缩后的byte[]
/**
*
* @param bytes 原始字符串对应的byte[]
* @param huffmanCodes 生成的赫夫曼编码
* @return
*/
private static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCodes){
//利用huffmanCodes 将 bytes转成赫夫曼对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for (byte b : bytes){
stringBuilder.append(huffmanCodes.get(b));
}
//将对应的字符串转为byte[]
//统计返回的赫夫曼编码的长度
int len;
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=i+8){//每8位对应一个byte
String strByte;
if (i+8 > stringBuilder.length()){//最后不够8位
strByte = stringBuilder.substring(i);
}else{
strByte = stringBuilder.substring(i,i+8);
}
//将strByte转成byte,放入到huffmanCodeBytes
huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte,2);
index++;
}
return huffmanCodeBytes;
}