一、几种数据结构存储方式的分析
1)数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低。
2)链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值结点,只需要将插入节点,连接到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头结点开始遍历)。
3)树存储方式的分析
能提高数据存储,读取的效率,比如利用二叉排序树,既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
二、树的属性和性质
属性:
结点的度:有几个孩子(分支)
树的度:各结点的度的最大值
性质:
1)结点数=总度数+1
2)度为m的树、m叉树的区别
度为m的树:
① 任意结点的度<=m(最多m个孩子)
② 至少有一个结点度=m(有m个孩子)
③ 一定是非空树,至少有m+1个结点
m叉树:
① 任意结点的度<=m(最多m个孩子)
② 允许所有结点的度都<m
③ 可以是空树
3)度为m的树第 i 层至多有m^(i-1)个结点(i>=1)
m叉树第 i 层至多有m^(i-1)个结点(i>=1)
4)
5)
6)
三、二叉树
3.1常见的特殊二叉树
二叉排序树:一颗二叉树或者空二叉树,或者具有如下性质的二叉树:
左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有结点的关键字均大于根结点的关键字。
左子树和右子树各又是一颗二叉排序树。
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1.
3.2 二叉树的性质
1) 设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则n0=n2+1(叶子结点比二分支结点多一个)
假设树中结点总度数为n,则
① n=n0+n1+n2
② n=n1+2*n2+1 (树的结点数=总度数+1)
②-① 得 n0=n2+1
2) 3.3 二叉树的存储
顺序存储
二叉树的顺序存储,一定要把二叉树的结点编号与完全二叉树对应起来,这会导致大量的空间浪费。
结论:
二叉树的顺序存储结构,只适合存储完全二叉树
链式存储
每个结点有两个指针域,n个结点一共有2n个指针域只有n-1个结点被指向,因此有n+1时空的,可以用来构造线索二叉树。
3.4 二叉树的遍历
1) 先序遍历:根、左、右(NLR)
2)中序遍历:左、根、右(LNR)
3)后序遍历:左、右、根(LRN)
求树的深度
4)层序遍历
算法思想:
① 初始化一个辅助队列
② 根结点入队
③ 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④ 重复③直至队列为空
注意:
若只给出一颗二叉树的前 / 中 / 后 / 层 序遍历序列中的一种,不能唯一确定一颗二叉树。
要有两个遍历序列,且必须要有中序遍历序列,才可以确定唯一的二叉树。
(关键:找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点)
四、找二叉树结点的前驱和后继
(遍历过程中的前驱和后继、与树本身逻辑的前驱和后继概念不同)
如何找到指定结点p在遍历中的前驱和后继
1)土办法
思路:
从根结点出发,重新进行一次遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点。
① 当q==p时,pre为前驱
② 当pre==p时,q为后继
缺点:找前驱、后继很不方便;遍历操作必须从根结点开始
2)线索二叉树
思路:
前驱线索:(由左孩子指针充当)
后继线索(由由孩子指针充当)
标识变量tag:(判断每个结点的左右指针是孩子指针还是线索指针)
tag==0,表示指针指向孩子
tag==1,表示指针是”线索“
① 中序线索化
引用类型的pre可以使每个在调用函数时可以修改pre的值,可以影响初始定义的值
中序遍历的最后一个结点右孩子指针必为空
② 先序线索化
当访问到第三个结点D时,visit(D)把D指向了B(即D->lchild=B),pre=q;然后执行PreThread()时,里面参数是实则为(D->lchild=B),又执行q->lchild=pre此时(pre=q=D),故把D指向B出现闭环,死循环
③ 后序线索化
中序线索二叉树找中序后继
for循环中,先找到最左下结点,即第一个遍历结点,后判断循环,后p指向它的后继结点(遍历序列后继)。因为不需要递归,因此空间复杂度为O(1)
中序线索二叉树找中序前驱
同理先找到最右下结点,即最后一个遍历结点,后判断循环,后指向它的前驱结点
五、树(每个结点不仅限于两个分支)
1、树的存储结构
1)双亲表示法(顺序存储)
优点:
查指定结点的双亲很方便
缺点
查指定结点的孩子只能从头遍历,空数据导致遍历更慢。
2)孩子表示法(顺序+链式存储)
用一个数组来存储结点,每个结点除了存储这个结点的数据,还存储它的第一个子孩子的指针的一个链表
3)孩子兄弟表示法(链式存储)
孩子兄弟表示法实现树和二叉树的转化(树的问题转为熟悉的二叉树来处理)
即,每个结点左指针指向自己的第一个孩子,右指针指针这个结点的右兄弟
2、树的遍历
1)树的先根遍历:若树非空,先访问根结点,再依次对每个子树进行先根遍历
树使用孩子兄弟表示法转为二叉树此树的先根遍历序列与其对应的二叉树的先序序列相同
2)树的后根遍历:若树非空,先依次对每个子树进行后根遍历,最后再访问根结点
后根遍历序列与相对应二叉树的中序序列相同
3)树的层序遍历(广度优先遍历):- - - 用队列实现
六、森林
互不相交的树(森林)用孩子兄弟表示法转换为二叉树- - -(前提:森林中各个树的根结点之间是为兄弟关系)
1、森林的遍历
1)森林的先序遍历:若森林非空,则按如下规则进行遍历:
访问森林中第一棵树的根结点;
先序遍历第一棵树中根结点的子树森林;
先序遍历除去第一棵树之后剩余的树构成的森林。
森林的先序遍历效果等同于依次对各个树先根遍历,故可以把森林转为对应的二叉树,再先序遍历
2)森林的中序遍历:若森林非空,则按如下规则进行遍历:
中序遍历第一棵树中根结点的子树森林;
访问森林中第一棵树的根结点;
中序遍历除去第一棵树之后剩余的树构成的森林。
森林的中序遍历等效于对应的各个树的后根遍历,把森林转为对应的二叉树,再中序遍历。
七、特殊的二叉树
1、二叉排序树(二叉查找树)- - - BST
左子树节点值< 根结点值 < 右子树结点值
进行中序遍历,可以得到一个递增的有序序列
1)二叉排序树的查找
2)二叉排序树的插入
每次插入的新结点一定是叶子结点。因为使用了递归实现,最坏的空间复杂度O(h)
3)二叉排序树的构造- - - 在插入方法的基础上实现
先创建一个空树,然后循环调用插入函数,插入数据
3)二叉排序树的删除
2、平衡排序树
结点的平衡因子=左子树高-右子树高
调整最小不平衡子树
** f 的右下、p的右上 旋转替代**,被替代结点 f 的左孩子指针去连替代结点 p 的右子树;被替代结点的右孩子指针去连替代结点的左子树。
(总结:左连右;右连左—左右相连)
最后,f的父结点的左 / 右指向p
** f 的左下、p的左上 旋转替代** - - - 同理
八、哈夫曼树
树的带权路径长度:树种所有叶子结点的带权路径长度之和(WPL)
1、哈夫曼树
哈夫曼树:
在含有n个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
把n个结点划分为森林,取两个权值最小的结点,作为新结点的左右子树,新结点的权值为两者之和,从森林中删除这两个结点,重复上述步骤
给一个数组{13,7,8,3,29,6,1},要求转成一颗哈夫曼树
思路分析:
1)从小到大进行排序,将每一个数据(每个数据都是一个结点),每个结点都可以看成是一颗最简单的二叉树
2)取出根结点权值最小的两颗二叉树
3)组成一颗新的二叉树,该新的二叉树的根结点的权值是前面两颗二叉树根结点权值的和
4)再将这颗新的二叉树,以根结点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数组中,所有的数据都被处理,就得到一颗哈夫曼树。
代码如下:
public class HuffmanTree {
public static void main(String[] args) {
int [] arr={13,7,8,3,29,6,1};
Node huffmanTree = createHuffmanTree(arr);
PreShowHuffmanTree(huffmanTree);
}
// 先序遍历哈夫曼树
public static void PreShowHuffmanTree(Node root){
if (root!=null){
System.out.println(root);
PreShowHuffmanTree(root.left);
PreShowHuffmanTree(root.right);
}
}
// 创建哈夫曼树的方法
public static Node createHuffmanTree(int[] arr){
// 第一步为了操作方便
// 1、遍历arr数组
// 2、将arr的每个元素构成一个Node
// 3、将Node放入到ArrayList中
ArrayList<Node> nodes = new ArrayList<>();
for (int value:arr){
nodes.add(new Node(value));
}
while (nodes.size()>1){ //最后集合中只剩一个元素的时候结束
// 排序,从小到大
Collections.sort(nodes);
//System.out.println(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);
}
// 返回哈夫曼树的根结点 即:集合中最后一个元素
return nodes.get(0);
}
}
//创建结点类
// 为了让Node对象实现Collections集合排序
// 让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; // 从小到大排序
}
}
2、哈夫曼编码
固定长度编码(每个字符用相等长度的二进制表示)
可变长度编码(允许对不同字符用不等长的二进制位表示)
把叶子结点A转为分支结点,虽然会使WPL值减小,但会导致收到编码后解码出现错误
前缀编码:解码时无歧义
哈夫曼编码
把字符集中每个字符(例如上例中:A、B、C、D)都作为一个叶子结点,出现的频度作为该结点的权值,此为- - - 哈夫曼编码
根据哈夫曼树,给各个字符- - - 规定编码,向左的路径为0,向右的路径为1。
**哈夫曼编码满足前缀编码,即字符的编码都不能是其它字符编码的前缀。不会造成匹配的多义性- - -哈夫曼编码是无损的处理。
例题:
将给出的一段文本,比如“i like like like java do you like a java”,根据前面的讲的哈夫曼编码原理,对其进行数据压缩处理。
步骤:
1)根据哈夫曼编码原理,需要创建 “i like like like java do you like a java” 对应的哈夫曼树。
思路:
① Node { data 存放数据 ,weight 权值 ,left ,right}
②得到 “i like like like java do you like a java” 对应的byte[ ]数组
③编写一个方法,将准备构建哈夫曼树的Node结点放到List(集合中)形式{Node[data=97,wight=7]} - - - HashMap
体现 d:1,y:1,u:1,j:1,v:2,o:2,l:4,k:4,e:4,i:5,a:5, :9
④ 可以通过List创建对应的哈夫曼树
创建对应的哈夫曼树- - - 代码如下:
public class HuffmanCode {
public static void main(String[] args) {
String str="i like like like java do you like a java";
byte[] strBytes=str.getBytes();
System.out.println(strBytes.length);
//测试
Node2 huffmanTree = createHuffmanTree(strBytes);
PreShowHuffmanTree(huffmanTree);
}
// 先序遍历哈夫曼树
public static void PreShowHuffmanTree(Node2 root){
if (root!=null){
System.out.println(root);
PreShowHuffmanTree(root.left);
PreShowHuffmanTree(root.right);
}
}
// 创建哈夫曼树的方法
public static Node2 createHuffmanTree(byte[] strBytes){
//创建一个ArrayList
ArrayList<Node2> nodes = new ArrayList<>();
// 遍历bytes,统计每一个byte出现的次数->map[key,value]
HashMap<Byte, Integer> counts = new HashMap<>();
for (byte b:strBytes){
Integer count=counts.get(b);
if (count == null){ //Map还没有这个字符数据,第一次
counts.put(b,1);
}else {
counts.put(b,count+1);
}
}
// 把每一个键值对转成一个Node2对象,并加入到nodes集合
for (Map.Entry<Byte,Integer> entry:counts.entrySet()){
nodes.add(new Node2(entry.getKey(), entry.getValue()));
}
while (nodes.size()>1){ //最后集合中只剩一个元素的时候结束
// 排序,从小到大
Collections.sort(nodes); //有 Node2实现了Comparable接口重写了ComparTo
//System.out.println(nodes);
// 取出根结点权值最小的两棵二叉树
// (1)取出权值最小的结点(二叉树)
Node2 leftNode = nodes.get(0);
// (2)取出权值最小的结点(二叉树)
Node2 rightNode = nodes.get(1);
// (3)构建一颗新的二叉树 没有data,只有权值
Node2 parent = new Node2(null,leftNode.weight + rightNode.weight);
parent.left=leftNode;
parent.right=rightNode;
//(4)从ArrayList删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// (5)将parent加入到集合nodes中
nodes.add(parent);
}
// 返回哈夫曼树的根结点 即:集合中最后一个元素
return nodes.get(0);
}
}
//创建Node2,数据和权值
class Node2 implements Comparable<Node2>{
Byte data; // 存放数据本身,比如‘a’=>97
int weight; // 权值,表示字符出现的次数
Node2 left;
Node2 right;
public Node2(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node2 o) {
return this.weight-o.weight; //从小到大排序
}
@Override
public String toString() {
return "Node2{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
2)生成哈夫曼树对应的哈夫曼编码,如下表:
=01,a=100,d=11000,u=11001,e=1110,v=11011,i=101,y=11010,j=0010,k=1111,l=000,o=0011
main函数- - - 新增代码如下
// 生成哈夫曼树对应的哈夫曼编码
// 思路:
// 1、将哈夫曼编码表存放在Map<Byte,String>形式 key-对应字母,value-对应字母编码
// 例如 32->01 ......
HashMap<Byte,String> huffmanCodes = new HashMap<>();
// 2、在生成哈夫曼编码表示,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
StringBuilder stringBuilder=new StringBuilder();
getCodes(huffmanCodes,huffmanTree,"",stringBuilder); //stringBuilder下面定义了
System.out.println("生成的哈夫曼编码表"+huffmanCodes);
class HuffmanCode类中- - - 新增代码如下
/*
* 功能:将传入的node结点的所有叶子结点的哈夫曼编码得到,并放入到huffmanCodes集合
* huffmanCodes 哈夫曼编码表存放在Map<Byte,String>
* node 传入的结点
* code 路径 左子结点是0,右子结点是1
* stringBuilder 用于拼接路径
* */
private static void getCodes(HashMap huffmanCodes,Node2 node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将code加入到stringBuilder2
stringBuilder2.append(code);
if (node!=null){ // 如果node==null不处理
// 判断当前node是叶子结点还是非叶子结点
if (node.data==null){ //非叶子结点
//递归处理
//向左递归
getCodes(huffmanCodes,node.left,"0",stringBuilder2);
//向右递归
getCodes(huffmanCodes,node.right,"1",stringBuilder2);
}else { // 说明是叶子结点
// 表示找到某个叶子结点的最后
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
3) 使用哈夫曼编码来生成哈夫曼编码数据
main函数- - - 新增代码如下
//测试哈夫曼编码数据
byte[] huffmanCodeBytes = zip(strBytes, huffmanCodes);
System.out.println("huffmanCodeBytes="+Arrays.toString(huffmanCodeBytes));
class HuffmanCode类中- - - 新增代码如下
// 编写一个方法,将字符串对应的byte[]数组,通过生成的哈夫曼编码表,
// 返回一个哈夫曼编码压缩后的byte[]
/*
* bytes 这是原始的字符串对应的byte[]
* huffmanCodes 生成的哈夫曼编码 集合 map<key,value>
* 返回哈夫曼编码处理后的byte[]
* 对应的byte[] huffmanCodeBytes ,即8位对应一个byte,放入到huffmanCodeBytes
* huffmanCodeBytes[0]=10101000(补码)=>byte
* [推导 10101000=> 10101000-1=> 10100111(反码)=> 11011000 = -88
* */
private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
//1、利用huffmanCodes将bytes转成哈夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes 数组
for (byte b:bytes){
stringBuilder.append(huffmanCodes.get(b)); //添加对应字符的哈夫曼编码
}
// 将“101010001011111110...”转为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
String strbyte;
if (i+8>stringBuilder.length()){ //不够8位
strbyte=stringBuilder.substring(i);
}else {
strbyte=stringBuilder.substring(i,i+8);
}
// 将strbyte转成一个byte,放入到huffmanCodes
huffmanCodeBytes[index]=(byte) Integer.parseInt(strbyte,2);
index++;
}
return huffmanCodeBytes;
}
解码
变回之前的字符串(逆过程)
main函数- - - 新增代码如下
// 测试解码
byte[] sourceBytes=decode(huffmanCodes,huffmanCodeBytes);
System.out.println("原来的字符串="+new String(sourceBytes));
class HuffmanCode类中- - - 新增代码如下
/*
* 将一个byte转成一个二进制的字符串
* flag 标志是否需要补高位,如果是true,表示需要补高位
* return 返回的是b对应的二进制的字符串,(注意是按补码返回)
* */
private static String byteToBitString(boolean flag,byte b){
// 使用变量保存b
int temp = b; // 将b转成int
// 如果是整数还存在补高位
if(flag){
temp |=256; // 按位与256 0001 0000 0000
}
String str=Integer.toBinaryString(temp); // 返回的是temp二进制的补码
if (flag){
return str.substring(str.length()-8);
}else {
return str;
}
}
//编写一个方法,完成对应压缩数据的解码
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];
//判断是不是最后一个字节
boolean flag=(i==huffmanBytes.length-1);
stringBuilder.append(byteToBitString(!flag,b));
}
//把字符串安装指定的哈夫曼编码进行解码
// 把哈夫曼编码表进行调换,因为反向查询a->100 100->a
HashMap<String, Byte> stringByteHashMap = new HashMap<>();
for (Map.Entry<Byte,String> entry:huffmanCodes.entrySet()){
stringByteHashMap.put(entry.getValue(), entry.getKey());//反向
}
// 创建集合,存放byte
ArrayList<Byte> list = new ArrayList<>();
for (int i=0;i<stringBuilder.length();){
int count=1; // 小的计数器
boolean flag=true;
Byte b=null;
while (flag){
//取出一个'1' '0'
String key=stringBuilder.substring(i,i+count);
//i不动,count移动,直到匹配到一个字符
b=stringByteHashMap.get(key);
if (b==null) {//说明没有匹配到
count++;
}else {
//匹配到
flag=false;
}
}
list.add(b);
i+=count; // i直接移动到count
}
// 当for循环结束后,list中就存放了所有字符
// 把list中数据放入到byte[]并返回
byte b[] =new byte[list.size()];
for (int i=0;i<b.length;i++){
b[i]=list.get(i);
}
return b;
}