Java数据结构和算法3-查找_哈希表_二叉树

1、查找算法

在java中,我们常用的查找有四种:

(1)顺序(线性)查找

(2)二分查找/折半查找

(3)插值查找

(4)斐波那契查找

1、顺序(线性)查找

1、需求

有一个数列:{1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称。

2、代码

package com.xiaolun.search;

//线性查找
public class SeqSearch {
    
    
   public static void main(String[] args) {
    
    
      int arr[] = {
    
    1, 9, 11, -1, 34, 89};// 没有顺序的数组
      int index = seqSearch(arr, -11);
      if (index == -1) {
    
    
         System.out.println("没有找到");
      } else {
    
    
         System.out.println("找到,下标为=" + index);
      }
   }

   /**
    * 找到一个满足条件的值,就返回
    *
    * @param arr
    * @param value
    * @return
    */
   public static int seqSearch(int[] arr, int value) {
    
    
      // 线性查找是逐一比对,发现有相同值,就返回下标。
      for (int i = 0; i < arr.length; i++) {
    
    
         if (arr[i] == value) {
    
    
            return i;
         }
      }
      return -1;
   }
}

2、二分查找

1、思路

1. 首先确定该数组的中间的下标
mid = (left + right) / 2
2. 然后让需要查找的数 findVal 和 arr[mid] 比较
    2. 1 findVal > arr[mid] ,  说明你要查找的数在mid 的右边, 因此需要递归的向右查找
    2.2 findVal < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
    2.3  findVal == arr[mid] 说明找到,就返回

//什么时候我们需要结束递归?
1) 找到就结束递归 
2) 递归完整个数组,仍然没有找到findVal ,也需要结束递归  当 left > right 就需要退出

2、需求

(1) 请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。

(2){1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000。

3、代码

package com.xiaolun.search;

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

//二分查找算法,数组必须有序
public class BinarySearch {
    
    
   public static void main(String[] args) {
    
    
//    int arr[] = { 1,2,3};
//    int resIndex = binarySearch(arr, 0, arr.length - 1, 4);
//    System.out.println("resIndex=" + resIndex);

      int arr[] = {
    
    1, 1,2, 3};
      List<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 1);
      System.out.println("resIndexList=" + resIndexList);
   }

   /**
    * @param arr     数组
    * @param left    左边的索引
    * @param right   右边的索引
    * @param findVal 要查找的值
    * @return 如果找到就返回下标,反之,返回-1
    */
   public static int binarySearch(int[] arr, int left, int right, int findVal) {
    
    
      // 当left > right时,说明递归整个数组,但是没有找到
      if (left > right) {
    
    
         return -1;
      }

      int mid = (left + right) / 2;
      int midVal = arr[mid];

      if (findVal > midVal) {
    
     // 向右递归
         //当数组是 arr[] = { 1,2,3} 时,mid=2会返回
         return binarySearch(arr, mid + 1, right, findVal);
      } else if (findVal < midVal) {
    
     // 向左递归
         return binarySearch(arr, left, mid - 1, findVal);
      } else {
    
    
         return mid;
      }
   }

   /**
    * 当有多个相同的数值时,如何将所有的数值都查找到
    * 思路
    * 1、在找到mid索引值时,不要马上返回
    * 2、向mid索引值的左边扫描,将所有满足1000的元素找到,加入到集合ArrayList
    * 3、向mid索引值的右边扫描,将所有满足1000的元素找到,加入到集合ArrayList
    * 4、将ArrayList返回
    */
   public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
    
    
      if (left > right) {
    
    
         return new ArrayList<Integer>();
      }
      int mid = (left + right) / 2;
      int midVal = arr[mid];

      if (findVal > midVal) {
    
    
         return binarySearch2(arr, mid + 1, right, findVal);
      } else if (findVal < midVal) {
    
    
         return binarySearch2(arr, left, mid - 1, findVal);
      } else {
    
    
         List<Integer> resIndexlist = new ArrayList<Integer>();
         //向mid索引值的左边扫描,将所有满足1000的元素找到,加入到集合ArrayList
         int temp = mid - 1;
         while (true) {
    
    
            if (temp < 0 || arr[temp] != findVal) {
    
    //退出
               break;
            }
            //否则,将temp放入到resIndexlist
            resIndexlist.add(temp);
            temp -= 1; //temp左移
         }
         resIndexlist.add(mid); //将周阿金啊这个放进去
         //向mid索引值的右边扫描,将所有满足1000的元素找到,加入到集合ArrayList
         temp = mid + 1;
         while (true) {
    
    
            if (temp > arr.length - 1 || arr[temp] != findVal) {
    
    //退出
               break;
            }
            //否则,将temp放入到resIndexlist
            resIndexlist.add(temp);
            temp += 1; //temp右移
         }
         return resIndexlist;
      }
   }
}

3、插值查找

1、介绍

(1)插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。

(2)将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right。key 就是前面我们讲的 findVal:

image-20200928170647650

int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])

(3)举例

举例说明插值查找算法 1-100 的数组。

数组  arr = [1, 2, 3, ......., 100]

假如我们需要查找的值  1 

使用二分查找的话,我们需要多次递归,才能找到 1

使用插值查找算法
int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])

int mid = 0 + (99 - 0) * (1 - 1)/ (100 - 1) = 0 + 99 * 0 / 99 = 0 

比如我们查找的值 100

int mid = 0 + (99 - 0) * (100 - 1) / (100 - 1) = 0 + 99 * 99 / 99 = 0 + 99 = 99 

2、注意事项

(1)对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快。

(2)关键字分布不均匀的情况下,该方法不一定比折半查找要好。

3、代码

package com.xiaolun.search;


public class InsertValueSearch {
    
    
   public static void main(String[] args) {
    
    
      int arr[] = {
    
    1, 8, 10, 89, 1000, 1000, 1234};
      int index = insertValueSearch(arr, 0, arr.length - 1, 1000);
      System.out.println("index = " + index);
   }

   /**
    * @param arr     数组
    * @param left    左边的索引
    * @param right   右边的索引
    * @param findVal 要查找的值
    * @return 如果找到就返回下标,反之,返回-1
    */
   public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
    
    
      /**
       * findVal < arr[0] 和 findVal > arr[arr.length - 1]都需要
       * 否则我们得到的mid可能越界
       */
      if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
    
    
         return -1;
      }

      // 求mid,自适应
      int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
      int midVal = arr[mid];
      if (findVal > midVal) {
    
     // 说明应该向右边递归查找
         return insertValueSearch(arr, mid + 1, right, findVal);
      } else if (findVal < midVal) {
    
     // 说明应该向左边递归查找
         return insertValueSearch(arr, left, mid - 1, findVal);
      } else {
    
    
         return mid;
      }
   }
}

4、斐波那契(黄金分割法)查找算法

1、介绍

(1)概述

斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即

mid=low+F(k-1)-1

(F代表斐波那契数列,k代表斐波那契数列的第k个元素),如下图所示

image-20200928202210429

(2)对于F(k-1)-1的理解

由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到

(F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1

该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如上图所示。从而中间位置为:

mid=low+F(k-1)-1

类似的,每一子段也可以用相同的方式分割

但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可。

while(n>fib(k)-1)
    k++;

2、代码

package com.xiaolun.search;

import java.util.Arrays;

//斐波那契算法
public class FibonacciSearch {
    
    
   public static int maxSize = 20;

   public static void main(String[] args) {
    
    
      int[] arr = {
    
    1, 8, 10,12};
      System.out.println("index=" + fibSearch(arr, 8));// 0
   }

   /**
    * 因为后面我们mid=low+F(k-1)-1,需要使用到斐波那契数列,所以我们需要获取一个斐波那契数列
    * 非递归方法获取一个斐波那契数列
    */
   public static int[] fib() {
    
    
      int[] f = new int[maxSize];
      f[0] = 1;
      f[1] = 1;
      for (int i = 2; i < maxSize; i++) {
    
    
         f[i] = f[i - 1] + f[i - 2];
      }
      return f;
   }

   /**
    * 非递归方式的斐波那契查找算法
    *
    * @param a   数组
    * @param key 我们需要查找的关键字(码)
    * @return 返回对应的下标,如果没有返回-1
    */
   public static int fibSearch(int[] a, int key) {
    
    
      int low = 0;
      int high = a.length - 1;
      int k = 0; //表示斐波那契分割数值的下标
      int mid = 0; //存放mid值
      int f[] = fib(); //获取斐波那契数列
      //获取斐波那契数值的下标,让F[k]-1长度大于顺序表的长度,之后顺序表不够的进行补零
      while (high > f[k] - 1) {
    
    
         k++;
      }
      /**
       * 因为f[k]值可能大于a的长度,因此我们需要使用Arrays类,构造一个新的数组
       * 并指向temp[],长度为f[k](斐波那契数列第k个元素)不足的部分使用0填充
       *
       */
      int[] temp = Arrays.copyOf(a, f[k]);
      /**
       * 实际上需要使用a数组的最后的数填充temp
       * temp = {1,8, 10, 89, 1000, 1234, 0, 0}  => {1,8, 10, 89, 1000, 1234, 1234, 1234}
       */
      for (int i = high + 1; i < temp.length; i++) {
    
    
         temp[i] = a[high];
      }

      // 使用while循环处理,找到我们的数key
      while (low <= high) {
    
     // 只要这个条件满足,就可以找
         mid = low + f[k - 1] - 1;
         if (key < temp[mid]) {
    
     //我们继续向数组的前面查找(左边)
            high = mid - 1;
            /**
             * 1、全部元素=前面的元素+后面的元素
             * 2、f[k] = f[k-1] + f[k-2]
             * 3、因为前面有 f[k-1]个元素,因此可以继续拆分 f[k-1] = f[k-2] + f[k-3]
             * 即在f[k-1]的前面继续查找 k--
             * 即下次循环 mid = f[k-1-1]-1
             */
            k--;
         } else if (key > temp[mid]) {
    
     // 我们应该继续在数组的后面查找(右边)
            low = mid + 1;
            /**
             * 1、全部元素=前面的元素+后面的元素
             * 2、f[k] = f[k-1] + f[k-2]
             * 3、因为后面有 f[k-2]个元素,因此可以继续拆分 f[k-2] = f[k-3] + f[k-4]
             * 即在f[k-1]的前面继续查找 k-=2
             * 即下次循环 mid = f[k-1-2]-1
             */
            k -= 2;
         } else {
    
     //找到
            //需要确定,返回的是那个下标
            if (mid <= high) {
    
    
               return mid;
            } else {
    
    
               return high;
            }
         }
      }
      return -1;
   }
}

2、哈希表(Hash table)

1、介绍

1、概述

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

image-20200928211813956

2、需求

有一个公司,当有新的员工来报道时,要求将该员工的信息加入
(id,性别,年龄,名字,住址…),当输入该员工的id时,要求查找到该员工的
所有信息。

要求:

(1)不使用数据库,速度越快越好=>哈希表(散列)。

(2)添加时,保证按照id从低到高插入。

使用链表来实现哈希表, 该链表不带表头,即链表的第一个结点就存放雇员信息。

image-20200928212049855

2、代码

1、员工表

package com.xiaolun.hashtab;

//表示一个雇员
public class Emp {
    
    
   public int id;
   public String name;
   public Emp next; //next 默认为 null

   public Emp(int id, String name) {
    
    
      super();
      this.id = id;
      this.name = name;
   }
}

2、员工链表

package com.xiaolun.hashtab;

//创建EmpLinkedList,表示链条。里面会存放很多数据
public class EmpLinkedList {
    
    
   //头指针,执行第一个雇员Emp,因此,我们这个链表的head是直接指向第一个Emp
   private Emp head; //默认为null

   /**
    * 添加雇员到链表
    * 1、假定,当添加雇员时,id是自增长,即id的分配总是从小到大
    * 因此,我们将该雇员直接加入到本链表的最后即可
    *
    * @param emp
    */
   public void add(Emp emp) {
    
    
      //如果是,添加第一个雇员
      if (head == null) {
    
    
         head = emp;
         return;
      }
      //如果不是第一个雇员,使用辅助指针,帮助定位到最后
      Emp curEmp = head;
      while (true) {
    
    
         if (curEmp.next == null) {
    
    //说明链表到最后
            break;
         }
         curEmp = curEmp.next; //后移
      }
      //退出时,直接将emp加入链表
      curEmp.next = emp;
   }

   //遍历链表中雇员的信息
   public void list(int no) {
    
    
      if (head == null) {
    
     //链表为null
         System.out.println("第" + (no + 1) + "链表为空");
         return;
      }
      System.out.print("当前"+(no + 1)+"链表的信息为:");
      Emp curEmp = head; //辅助指针
      while (true) {
    
    
         System.out.printf(" => id=%d name=%s\t \n", curEmp.id, curEmp.name);
         if (curEmp.next == null) {
    
    //说明curEmp已经到了最后节点
            break;
         }
         curEmp = curEmp.next; //后移,遍历
      }
   }

   /**
    * 根据id查雇员
    * 如果找到,就直接返回Emp,反之,返回null
    *
    * @param id
    * @return
    */
   public Emp findEmpById(int id) {
    
    
      //判断链表是否为null
      if (head == null) {
    
    
         System.out.println("链表为空");
         return null;
      }
      //辅助指针
      Emp curEmp = head;
      while (true) {
    
    
         if (curEmp.id == id) {
    
    //找到
            break;//curEmp指向要查找的雇员
         }
         if (curEmp.next == null) {
    
    //遍历当前链表中没有找到该雇员
            curEmp = null;
            break;
         }
         curEmp = curEmp.next;
      }
      return curEmp;
   }
}

3、哈希表

package com.xiaolun.hashtab;

//创建HashTab 管理多条链表
public class HashTab {
    
    
   private EmpLinkedList[] empLinkedListArray;
   private int size; //表示有多少条链表

   //构造器
   public HashTab(int size) {
    
    
      this.size = size;
      //初始化empLinkedListArray
      empLinkedListArray = new EmpLinkedList[size];
      /**
       * !!!这里不要初始化每一个链表,不然会报错。空指针异常。
       * 1、原因是,上面一句的确是将链表数组创建,但是里面全部为null。
       * 即(All elements are null)
       * 2、需要进行下面的for循环的初始化操作
       * 3、在执行下面的add添加操作时,null中不能添加元素,故报错。
       */
      for (int i = 0; i < size; i++) {
    
    
         empLinkedListArray[i] = new EmpLinkedList();
      }
   }

   //添加雇员
   public void add(Emp emp) {
    
    
      //根据雇员的id,得到该雇员应当添加到那条链表
      int empLinkedListNO = hashFun(emp.id);
      //将emp添加到对应的链表中
      empLinkedListArray[empLinkedListNO].add(emp);
   }

   //遍历所有链表,遍历hashtab
   public void list() {
    
    
      for (int i = 0; i < size; i++) {
    
    
         empLinkedListArray[i].list(i);
      }
   }

   //根据输入的id,查找雇员
   public void findEmpById(int id) {
    
    
      //使用散列函数确定到那条链表上查找
      int empLinkedListNO = hashFun(id);
      Emp emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
      if (emp != null) {
    
    //找到
         System.out.printf("在第%d条链表中找到雇员, id = %d\n", (empLinkedListNO + 1), id);
      } else {
    
    
         System.out.println("在哈希表中,没有找到雇员");
      }
   }

   //编写散列函数,使用一个简单取模法
   public int hashFun(int id) {
    
    
      return id % size;
   }
}

4、测试

package com.xiaolun.hashtab;

import java.util.Scanner;

public class HashTabDemo {
    
    
   public static void main(String[] args) {
    
    
      //创建哈希表
      HashTab hashTab = new HashTab(7);

      //写一个简单的菜单
      String key = "";
      Scanner scanner = new Scanner(System.in);
      while (true) {
    
    
         System.out.println("add: 添加雇员");
         System.out.println("list: 显示雇员");
         System.out.println("find: 查找雇员");
         System.out.println("exit: 退出系统");

         key = scanner.next();
         switch (key) {
    
    
            case "add":
               System.out.println("输入id");
               int id = scanner.nextInt();
               System.out.println("输入名字");
               String name = scanner.next();
               //创建雇员
               Emp emp = new Emp(id, name);
               hashTab.add(emp);
               break;
            case "list":
               hashTab.list();
               break;
            case "find":
               System.out.println("请输入要查找的id");
               id = scanner.nextInt();
               hashTab.findEmpById(id);
               break;
            case "exit":
               scanner.close();
               System.exit(0);
            default:
               break;
         }
      }
   }
}

3、树结构

1、介绍

1、概述

(1)数组存储方式的分析

优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。

缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低。

(2)链式存储方式的分析

优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。

缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)。

(3)树存储方式的分析

能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

2、树的常用术语

image-20200929165147099
节点
根节点
父节点
子节点
叶子节点 (没有子节点的节点)
节点的权(节点值)
路径(从root节点找到该节点的路线)
层
子树
树的高度(最大层数)
森林 :多颗子树构成森林

3、二叉树

(1)概述

  • 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。其子节点分为左节点和右节点。
image-20200929165343516
  • 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。
  • 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。如果将(61)节点删除,就不是完全二叉树了,因为叶子节点不连续了。
image-20200929165608056

(2)遍历

使用前序,中序和后序对下面的二叉树进行遍历。

前序遍历: 先输出父节点,再遍历左子树和右子树。

中序遍历: 先遍历左子树,再输出父节点,再遍历右子树。

后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点。

小结: 看输出父节点的顺序,就确定是前序,中序还是后序。

(3)遍历步骤

image-20200929165801835

(2)查找节点

image-20200929185432763

(3)删除节点

image-20200929185516106

2、代码

1、英雄类

package com.xiaolun.tree;


public class HeroNode {
    
    
   private int no;
   private String name;
   private HeroNode left; //默认为null
   private HeroNode right; //默认为null

   public HeroNode(int no, String name) {
    
    
      this.no = no;
      this.name = name;
   }

   public int getNo() {
    
    
      return no;
   }

   public void setNo(int no) {
    
    
      this.no = no;
   }

   public String getName() {
    
    
      return name;
   }

   public void setName(String name) {
    
    
      this.name = name;
   }

   public HeroNode getLeft() {
    
    
      return left;
   }

   public void setLeft(HeroNode left) {
    
    
      this.left = left;
   }

   public HeroNode getRight() {
    
    
      return right;
   }

   public void setRight(HeroNode right) {
    
    
      this.right = right;
   }

   @Override
   public String toString() {
    
    
      return "HeroNode [no=" + no + ", name=" + name + "]";
   }

   /**
    * 递归删除节点
    * 1、如果删除的节点是叶子节点,则删除该节点
    * 2、如果删除的节点是非叶子节点,则删除该子树
    */
   public void delNode(int no) {
    
    

      /**
       * 思路
       * 1、因为我们的二叉树是单向的,因此,我们判断当前节点的子节点是否是需要删除的节点,
       * 而不能去判断当前这个节点是不是需要删除的节点。
       * 2、如果当前节点的左子节点不为空,且删除的节点为左子节点,那么就将this.left=null;
       * 并且返回(删除后需要结束递归)
       * 3、右子节点同上。
       * 4、如果第2,3步没有删除节点,那么就需要向左子树进行递归删除
       * 5、如果第4步也没有删除节点,那么应该向右子树递归删除
       */
      if (this.left != null && this.left.no == no) {
    
    
         this.left = null;
         return;
      }
      if (this.right != null && this.right.no == no) {
    
    
         this.right = null;
         return;
      }
      if (this.left != null) {
    
    
         this.left.delNode(no);
      }
      if (this.right != null) {
    
    
         this.right.delNode(no);
      }
   }

   //前序遍历的方法
   public void preOrder() {
    
    
      System.out.println(this); //先输出父节点
      //递归向左子树前序遍历
      if (this.left != null) {
    
    
         this.left.preOrder();
      }
      //递归向右子树前序遍历
      if (this.right != null) {
    
    
         this.right.preOrder();
      }
   }

   //中序遍历
   public void infixOrder() {
    
    
      //递归向左子树前序遍历
      if (this.left != null) {
    
    
         this.left.infixOrder();
      }
      //输出父节点
      System.out.println(this);
      //递归向右子树前序遍历
      if (this.right != null) {
    
    
         this.right.infixOrder();
      }
   }

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


   /**
    * 前序遍历查找
    *
    * @param no 查找no
    * @return 如果找到就返回该Node, 反之,返回 null
    */
   public HeroNode preOrderSearch(int no) {
    
    
      System.out.println("进入前序遍历");
      //比较当前节点
      if (this.no == no) {
    
    
         return this;
      }

      /**
       * 1、判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
       * 2、如果左递归前序查找,找到节点,就返回
       */
      HeroNode resNode = null;
      if (this.left != null) {
    
    
         resNode = this.left.preOrderSearch(no);
      }
      if (resNode != null) {
    
    //说明我们的左子树找到
         return resNode;
      }
      if (this.right != null) {
    
    
         resNode = this.right.preOrderSearch(no);
      }
      return resNode;
   }

   //中序遍历查找
   public HeroNode infixOrderSearch(int no) {
    
    
      HeroNode resNode = null;
      if (this.left != null) {
    
    
         resNode = this.left.infixOrderSearch(no);
      }
      if (resNode != null) {
    
    
         return resNode;
      }
      System.out.println("进入中序查找");
      if (this.no == no) {
    
    
         return this;
      }
      //否则继续进行右递归的中序查找
      if (this.right != null) {
    
    
         resNode = this.right.infixOrderSearch(no);
      }
      return resNode;
   }

   //后序遍历查找
   public HeroNode postOrderSearch(int no) {
    
    

      HeroNode resNode = null;
      if (this.left != null) {
    
    
         resNode = this.left.postOrderSearch(no);
      }
      if (resNode != null) {
    
    
         return resNode;
      }

      if (this.right != null) {
    
    
         resNode = this.right.postOrderSearch(no);
      }
      if (resNode != null) {
    
    
         return resNode;
      }
      System.out.println("进入后序查找");
      if (this.no == no) {
    
    
         return this;
      }
      return resNode;
   }
}

2、二叉树

package com.xiaolun.tree;

//定义二叉树
public class BinaryTree {
    
    
   private HeroNode root;

   public void setRoot(HeroNode root) {
    
    
      this.root = root;
   }

   //删除节点
   public void delNode(int no) {
    
    
      if (root != null) {
    
    
         //如果只有一个root节点,那么立即判断root是不是就是要删除的节点
         if (root.getNo() == no) {
    
    
            root = null;
         } else {
    
    
            //递归删除
            root.delNode(no);
         }
      } else {
    
    
         System.out.println("空树,不能删除~");
      }
   }

   //前序遍历
   public void preOrder() {
    
    
      if (this.root != null) {
    
    
         this.root.preOrder();
      } else {
    
    
         System.out.println("二叉树为空,无法遍历");
      }
   }

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

   //后序遍历
   public void postOrder() {
    
    
      if (this.root != null) {
    
    
         this.root.postOrder();
      } else {
    
    
         System.out.println("二叉树为空,无法遍历");
      }
   }

   //前序遍历
   public HeroNode preOrderSearch(int no) {
    
    
      if (root != null) {
    
    
         return root.preOrderSearch(no);
      } else {
    
    
         return null;
      }
   }

   //中序遍历
   public HeroNode infixOrderSearch(int no) {
    
    
      if (root != null) {
    
    
         return root.infixOrderSearch(no);
      } else {
    
    
         return null;
      }
   }

   //后续遍历
   public HeroNode postOrderSearch(int no) {
    
    
      if (root != null) {
    
    
         return this.root.postOrderSearch(no);
      } else {
    
    
         return null;
      }
   }
}

3、测试

package com.xiaolun.tree;

public class BinaryTreeDemo {
    
    
   public static void main(String[] args) {
    
    
      //创建一颗二叉树
      BinaryTree binaryTree = new BinaryTree();
      //创建需要的节点
      HeroNode root = new HeroNode(1, "宋江");
      HeroNode node2 = new HeroNode(2, "吴用");
      HeroNode node3 = new HeroNode(3, "卢俊义");
      HeroNode node4 = new HeroNode(4, "林冲");
      HeroNode node5 = new HeroNode(5, "关胜");

      //手动创建二叉树
      root.setLeft(node2);
      root.setRight(node3);
      node3.setRight(node4);
      node3.setLeft(node5);
      binaryTree.setRoot(root);

      //测试
//    System.out.println("前序遍历"); // 1,2,3,5,4
//    binaryTree.preOrder();

//    System.out.println("中序遍历");
//    binaryTree.infixOrder(); // 2,1,5,3,4
//    
//    System.out.println("后序遍历");
//    binaryTree.postOrder(); // 2,5,4,3,1

      //前序遍历查找
      //前序遍历的次数:4
//    System.out.println("前序遍历方式~~~");
//    HeroNode resNode = binaryTree.preOrderSearch(5);
//    if (resNode != null) {
    
    
//       System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
//    } else {
    
    
//       System.out.printf("没有找到 no = %d 的英雄", 5);
//    }

      //中序遍历查找
      //中序遍历的次数:3
//    System.out.println("中序遍历方式~~~");
//    HeroNode resNode = binaryTree.infixOrderSearch(5);
//    if (resNode != null) {
    
    
//       System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
//    } else {
    
    
//       System.out.printf("没有找到 no = %d 的英雄", 5);
//    }

      //后序遍历查找
      //后序遍历的次数:2
//    System.out.println("后序遍历方式~~~");
//    HeroNode resNode = binaryTree.postOrderSearch(5);
//    if (resNode != null) {
    
    
//       System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
//    } else {
    
    
//       System.out.printf("没有找到 no = %d 的英雄", 5);
//    }

      System.out.println("删除前,前序遍历");
      binaryTree.preOrder(); //  1,2,3,5,4
      binaryTree.delNode(5);
      System.out.println("删除后,前序遍历:");
      binaryTree.preOrder(); // 1,2,3,4
   }
}

4、顺序存储二叉树

1、介绍

1、概述

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,如下图所示:

image-20200929185846960

2、特点

(1)顺序二叉树通常只考虑完全二叉树

(2)第n个元素的左子节点为 2 * n + 1

(3)第n个元素的右子节点为 2 * n + 2

(4)第n个元素的父节点为 (n-1) / 2

n : 表示二叉树中的第几个元素(从0开始编号)

2、代码

1、要求

给你一个数组 *{*1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为 1,2,4,5,3,6,7。

2、代码

(1)顺序二叉树

package com.xiaolun.tree;

//创建一个 ArrBinaryTree ,实现顺序二叉树的遍历
public class ArrBinaryTree {
    
    
   private int[] arr;  //存储数据结点的数组

   public ArrBinaryTree(int[] arr) {
    
    
      this.arr = arr;
   }

   public void preOrder() {
    
    
      this.preOrder(0);
   }

   /**
    * 编写一个方法,完成顺序存储二叉树的前序遍历
    *
    * @param index 数组的下标
    */
   public void preOrder(int index) {
    
    
      //如果数组为空,或者 arr.length = 0
      if (arr == null || arr.length == 0) {
    
    
         System.out.println("数组为空,不能按照二叉树的前序遍历");
      }
      //输出当前这个元素
      System.out.println(arr[index]);
      //向左递归遍历
      if ((index * 2 + 1) < arr.length) {
    
    
         preOrder(2 * index + 1);
      }
      //向右递归遍历
      if ((index * 2 + 2) < arr.length) {
    
    
         preOrder(2 * index + 2);
      }
   }
}

(2)测试

package com.xiaolun.tree;

public class ArrBinaryTreeDemo {
    
    
   public static void main(String[] args) {
    
    
      int[] arr = {
    
     1, 2, 3, 4, 5, 6, 7 };
      //创建一个ArrBinaryTree
      ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
      arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7
   }
}

5、线索化二叉树

1、介绍

1、背景

将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树:

image-20200929191246448

当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 },但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上。如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,我们可以使用线索二叉树。

2、概述

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

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

(3)一个结点的前一个结点,称为前驱结点。

(4)一个结点的后一个结点,称为后继结点。

3、案例

将上面的二叉树,转换成中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}

image-20200929191558196

当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况:

(1)left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点。

(2)right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点。

4、遍历线索二叉树

因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致。

2、代码

1、英雄类

package com.xiaolun.tree.threadedbinarytree;

//先创建 HeroNode 结点
class HeroNode {
    
    
   private int no;
   private String name;
   private HeroNode left;
   private HeroNode right;
   /**
    * 说明
    * 1. 如果 leftType == 0 表示指向的是左子树, 如果 1 则表示指向前驱结点
    * 2. 如果 rightType == 0 表示指向是右子树, 如果 1 表示指向后继结点
    */
   private int leftType;
   private int rightType;


   public int getLeftType() {
    
    
      return leftType;
   }

   public void setLeftType(int leftType) {
    
    
      this.leftType = leftType;
   }

   public int getRightType() {
    
    
      return rightType;
   }

   public void setRightType(int rightType) {
    
    
      this.rightType = rightType;
   }

   public HeroNode(int no, String name) {
    
    
      this.no = no;
      this.name = name;
   }

   public int getNo() {
    
    
      return no;
   }

   public void setNo(int no) {
    
    
      this.no = no;
   }

   public String getName() {
    
    
      return name;
   }

   public void setName(String name) {
    
    
      this.name = name;
   }

   public HeroNode getLeft() {
    
    
      return left;
   }

   public void setLeft(HeroNode left) {
    
    
      this.left = left;
   }

   public HeroNode getRight() {
    
    
      return right;
   }

   public void setRight(HeroNode right) {
    
    
      this.right = right;
   }

   @Override
   public String toString() {
    
    
      return "HeroNode [no=" + no + ", name=" + name + "]";
   }
}

2、线索二叉树

package com.xiaolun.tree.threadedbinarytree;

//定义 ThreadedBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
    
    
   private HeroNode root;

   /**
    * 为了实现线索化,需要创建要给指向当前结点的前驱结点的指针
    * 在递归进行线索化时,pre 总是保留前一个结点
    */
   private HeroNode pre = null;

   public void setRoot(HeroNode root) {
    
    
      this.root = root;
   }

   //重载threadedNodes方法
   public void threadedNodes() {
    
    
      this.threadedNodes(root);
   }


   /**
    * 二叉树进行中序线索化的方法
    *
    * @param node 就是当前需要线索化的结点
    */
   public void threadedNodes(HeroNode node) {
    
    
      //如果 node==null, 不能线索化
      if (node == null) {
    
    
         return;
      }

      //(一)先线索化左子树
      threadedNodes(node.getLeft());

      /**
       * (二)线索化当前结点[有难度]
       * 处理当前结点的前驱结点
       * 以 8 结点来理解
       * 8 结点的.left = null , 8 结点的.leftType = 1
       */
      if (node.getLeft() == null) {
    
    
         //让当前结点的左指针指向前驱结点
         node.setLeft(pre);
         //修改当前结点的左指针的类型,指向前驱结点
         node.setLeftType(1);
      }

      //处理后继节点
      if (pre != null && pre.getRight() == null) {
    
    
         //让前驱结点的右指针指向当前结点
         pre.setRight(node);
         //修改前驱结点的右指针类型
         pre.setRightType(1);
      }
      //!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
      pre = node;

      //(三)在线索化右子树
      threadedNodes(node.getRight());
   }

   //遍历线索化二叉树
   public void threadedList() {
    
    
      //定义一个变量,存储当前遍历的结点,从 root 开始
      HeroNode node = root;
      while (node != null) {
    
    
         /**
          * 循环的找到 leftType == 1 的结点,第一个找到就是 8 结点
          * 后面随着遍历而变化,因为当 leftType==1 时,说明该结点是按照线索化
          * 处理后的有效结点
          */
         while (node.getLeftType() == 0) {
    
    
            node = node.getLeft();
         }

         //打印当前节点
         System.out.println(node);
         //如果当前结点的右指针指向的是后继结点,就一直输出
         while (node.getRightType() == 1) {
    
    
            //获取到当前结点的后继结点
            node = node.getRight();
            System.out.println(node);
         }
         //替换这个遍历的结点(准备下一次操作)
         node = node.getRight();
      }
   }
}

3、测试

package com.xiaolun.tree.threadedbinarytree;

//测试中序线索二叉树
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

      //当线索化二叉树后,能在使用原来的遍历方法
      System.out.println("使用线索化的方式遍历 线索化二叉树");
      threadedBinaryTree.threadedList(); // 8, 3, 10, 1, 14, 6
   }
}

6、树结构的实际应用

1、堆排序

1、介绍

1、概述

(1)堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。

(2)堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。

(3)每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

image-20200929200631969

(4)大顶堆特点

// i 对应第几个节点,i从0开始编号(左右子节点)
arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]  

(5)小顶堆特点

image-20200929200741452
 // i 对应第几个节点,i从0开始编号
arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]

(6)一般升序采用大顶堆,降序采用小顶堆。

2、堆排序的基本思想

1、将待排序序列构造成一个大顶堆(数是以数组的形式来存储)
2、此时,整个序列的最大值就是堆顶的根节点。
3、将其与末尾元素进行交换,此时末尾就为最大值。
4、然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。

3、图解

步骤一:构造初始堆。将给定无序序列构造成一个大顶堆。

(1)假定原始的数组 [4, 6, 8, 5, 9],且假设给定无序序列结构如下:

image-20200929211940041

(2)我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整。

image-20200929212005656

(3)找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。

image-20200929212027869

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。

image-20200929212051049

此时,我们就将一个无序序列构造成了一个大顶堆。

步骤二:将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

(1)将堆顶元素 9 和末尾元素 4 进行交换

image-20200929212218908

重新调整结构,使其继续满足堆定义:

image-20200929212242320

(2)再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8。

image-20200929212301350

(3)后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

image-20200929212324675

总结:

1、将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
2、将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
3、重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

2、代码

package com.xiaolun.tree;

import java.util.Arrays;

//堆排序
public class HeapSort {
    
    
   public static void main(String[] args) {
    
    
      int arr[] = {
    
    4, 6, 8, 5, 9}; //将数组进行升序排列(大顶堆)
      heapSort(arr);
      System.out.println("排序后输出=" + Arrays.toString(arr));
   }

   //堆排序的方法
   public static void heapSort(int arr[]) {
    
    
      int temp = 0;
      System.out.println("堆排序");

      //分步完成
//    adjustHeap(arr, 1, arr.length);
//    System.out.println("第一次调整:" + Arrays.toString(arr)); // 4, 9, 8, 5, 6
//    
//    adjustHeap(arr, 0, arr.length);
//    System.out.println("第二次调整:" + Arrays.toString(arr)); // 9,6,8,5,4

      /**
       * 1、将无序序列构建成一个堆,根据升序需求选择大顶堆,下面是一个大鼎退
       * 其中,arr.length / 2 - 1 表示非叶子节点
       * 只考虑完全二叉树
       */
      for (int i = arr.length / 2 - 1; i >= 0; i--) {
    
    
         adjustHeap(arr, i, arr.length);
      }

      /**
       * 2、将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
       * 3、重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,
       * 直到整个序列有序。
       */
      for (int j = arr.length - 1; j > 0; j--) {
    
    
         //交换
         temp = arr[j];
         arr[j] = arr[0];
         arr[0] = temp;
         adjustHeap(arr, 0, j);
      }
   }

   /**
    * 将一个数组(二叉树), 调整成一个大顶堆
    * 功能: 完成将以 i 对应的非叶子结点的树调整成大顶堆
    * 举例int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
    * 如果我们再次调用adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
    *
    * @param arr    待调整的数组
    * @param i      表示非叶子结点在数组中索引
    * @param length 表示对多少个元素继续调整, length 是在逐渐的减少
    */
   public static void adjustHeap(int arr[], int i, int length) {
    
    
      int temp = arr[i];//先取出当前元素的值,保存在临时变量

      /**
       * 开始调整
       * 1、k = i * 2 + 1 k 是 i 结点的左子结点
       * k = k * 2 + 1 表示当前节点的左子节点
       */
      //找到最大值,并放到适当的位置
      for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
    
    
         //k + 1 < length 保证 右子节点存在
         if (k + 1 < length && arr[k] < arr[k + 1]) {
    
     //说明左子结点的值小于右子结点的值
            k++; // k 指向右子结点
         }
         if (arr[k] > temp) {
    
     //假设此时为arr[k]对应的是右子节点, 并且如果子结点大于父结点
            arr[i] = arr[k]; //把较大的值赋给当前结点
            i = k; //!!! i 指向 k,继续循环比较(可能还有左/右子树)
         } else {
    
    
            break; //从左至右,从下至上。此时下面的已经调整好了。
         }
      }
      //当 for 循环结束后,我们已经将以 i 为父结点的树的最大值,放在了 最顶(局部)
      arr[i] = temp;//将 temp 值放到调整后的位置
   }
}

2、赫夫曼树

1、介绍

1、概述

(1)给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。

(2)赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

2、几个重要概念和举例说明

(1)路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。

(2)结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。

(3)树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。

(4)WPL 最小的就是赫夫曼树。

2、图解

(1)需求

给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树。

(2)步骤

(1)从小到大进行排序, 将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树。
(2)取出根节点权值最小的两颗二叉树。 
(3)组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和。  
(4)再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树。
image-20200929212324675 image-20200929212324675

2、代码

1、节点类

package com.xiaolun.huffmantree;

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

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

   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、测试

package com.xiaolun.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);
      preOrder(root);
   }

   //编写前序遍历的方法
   public static void preOrder(Node root) {
    
    
      if (root != null) {
    
    
         root.preOrder();
      } else {
    
    
         System.out.println("是空树,不能遍历~~");
      }
   }


   /**
    * 创建赫夫曼树的方法
    *
    * @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);
   }
}

3、赫夫曼编码

1、介绍

1、介绍

(1)概述

赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法

赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。赫夫曼码是可变字长编码(VLC)的一种。

(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  // 各个字符对应的个数

按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值。同时,此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性。
image-20200930140053384

根据赫夫曼树,给各个字符规定编码 , 向左的路径为0,向右的路径为1 , 编码如下,此时长度变成了133。

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

(3)需求

将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩和解压处理。

2、代码

1、节点类

package com.xiaolun.huffmancode;

//创建Node ,有数据和权值
public 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();
      }
   }
}

2、编码类

package com.xiaolun.huffmancode;

import java.util.*;

public class HuffmanCode {
    
    
   public static void main(String[] args) {
    
    
      String content = "i like like like java do you like a java";
      byte[] contentBytes = content.getBytes();
      System.out.println(contentBytes.length); //40

      /*
      System.out.println("---------------------------");
      List<Node> nodes = getNodes(contentBytes);
      System.out.println("nodes=" + nodes);

      System.out.println("---------------------------");
      //创建赫夫曼树
      System.out.println("赫夫曼树");
      Node huffmanTreeRoot = createHuffmanTree(nodes);
      System.out.println("前序遍历");
      huffmanTreeRoot.preOrder();

      System.out.println("---------------------------");
      Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
      //{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
      System.out.println("生成的霍夫曼编码= " + huffmanCodes);

      System.out.println("---------------------------");
      byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
      //[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
      System.out.println("huffmanCodeBytes=" + Arrays.toString(huffmanCodeBytes));//17
       */
      //上面注释的语句等价于下面的代码
      byte[] huffmanCodeBytes = huffmanZip(contentBytes);
      System.out.println("huffmanCodeBytes=" + Arrays.toString(huffmanCodeBytes));


      byte[] sourceBytes = decode(huffmanCodes, huffmanCodeBytes);
      System.out.println("解码后输出=" + new String(sourceBytes)); // "i like like like java do you like a java"
   }

   /**
    * 完成对于压缩数据的解码
    * 1、将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
    * 先转成赫夫曼编码对应的二进制字符串 10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001010
    * 00101111111100110001001010011011100
    * 2、将赫夫曼编码对应的二进制字符串“10101000101111111.....”对照赫夫曼编码,进而转成字符串
    * 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 对应的 二进制的字符串 , 形式(113) 1010100010111...
      StringBuilder stringBuilder = new StringBuilder();

      //将 byte 数组转成二进制的字符串
      for (int i = 0; i < huffmanBytes.length; i++) {
    
    
         byte b = huffmanBytes[i];
         //判断是不是最后一个字节
         boolean flag = (i == huffmanBytes.length - 1); //为true后就不用再补高位了
         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());
      }
      //map反向后-->{000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}
      System.out.println("map反向后-->" + map);

      //创建要给集合,存放 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) {
    
    
            /**
             * 1、1010100010111... 递增的取出 key -> 1
             * 2、i 不动,让 count 移动,指定匹配到一个字符
             */
            String key = stringBuilder.substring(i, 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) {
    
    
      //使用变量保存 b
      int temp = b; //将 b 转成 int
      //如果是正数我们还存在补高位
      if (flag) {
    
    
         temp |= 256; //按位或 256 1 0000 0000 | 0000 0001 => 1 0000 0001
      }
      String str = Integer.toBinaryString(temp); //返回的是 temp 对应的二进制的补码
      if (flag) {
    
    
         return str.substring(str.length() - 8); //取字符串后面的8位
      } else {
    
    
         return str;
      }
   }

   /**
    * 使用一个方法,将前面的数据封装起来,便于我们的调用。
    *
    * @param bytes 原始的字符串对应的字节数组
    * @return 经过霍夫曼编码处理后的字节数组(压缩后的数组)
    */
   private static byte[] huffmanZip(byte[] bytes) {
    
    
      List<Node> nodes = getNodes(bytes);
      Node huffmanTreeRoot = createHuffmanTree(nodes);
      Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
      byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
      return huffmanCodeBytes;
   }

   /**
    * 将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
    *
    * @param bytes        这是原始的字符串对应的 byte[]
    * @param huffmanCodes 生成的赫夫曼编码 map类型
    * @return 返回赫夫曼编码处理后的 byte[]
    * 举例:
    * 1、传入String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes();
    * 因此,传入的byte就是 contentBytes
    * 2、返回的 "10101000101111111100100010111111110010001011111111001
    * 001010011011100011100000110111010001111001010001011111111
    * 00110001001010011011100" 这个字符传对应的  byte[] huffmanCodeBytes
    * (8位一组 对应一个 byte,放入到对应的huffmanCodeBytes数组中)
    * huffmanCodeBytes[0] =  10101000(补码) => byte  [推导(将其变成反码)  10101000 => 10101000 - 1 =>
    * 10100111(反码)=> (反码的原码,符号位不变,取反)11011000= -88 ]
    * => huffmanCodeBytes[1] = -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));
      }

      /**
       * 1、将"1010100010111111110..." 转换成 byte[]来发送,
       * 原本的数据长度为40 -》133 -》字节数组发送
       * 2、下面的代码等价于:
       * int len = (stringBuilder.length() + 7) / 8;
       */
      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(回头写代码)
      //因为是每8位对应一个byte,所以步长对应 +8
      for (int i = 0; i < stringBuilder.length(); i += 8) {
    
    
         String strByte;
         if (i + 8 > stringBuilder.length()) {
    
    
            //不够8位,下面的含义是从i位取到最后
            strByte = stringBuilder.substring(i);
         } else {
    
    
            strByte = stringBuilder.substring(i, i + 8);
         }
         //将strByte 转成 byte,放到huffmanCodeBytes
         huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
         index++;
      }
      return huffmanCodeBytes;
   }

   /**
    * 生成赫夫曼树对应的赫夫曼编码
    * 思路:
    * 1、将赫夫曼编码以Map<Byte,String>格式存放
    * 2、生成赫夫曼编码表
    * {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>();
   //3、生成的赫夫曼编码表需要去拼接路径,定义一个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) {
    
    
      StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
      //将 code 加入到 stringBuilder2
      stringBuilder2.append(code);
      if (node != null) {
    
     //如果 node == null 不处理
         //判断当前 node 是叶子结点还是非叶子结点
         if (node.data == null) {
    
     //非叶子结点
            //向左递归
            getCodes(node.left, "0", stringBuilder2);
            //向右递归
            getCodes(node.right, "1", stringBuilder2);
         } else {
    
     //说明是一个叶子结点
            //就表示找到某个叶子结点的最后
            huffmanCodes.put(node.data, stringBuilder2.toString());
         }
      }
   }

   //前序遍历的方法
   private static void preOrder(Node root) {
    
    
      if (root != null) {
    
    
         root.preOrder();
      } else {
    
    
         System.out.println("赫夫曼树为空");
      }
   }

   /**
    * @param bytes 接收一个字节数组
    * @return 返回list, 形式为  [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
    */
   private static List<Node> getNodes(byte[] bytes) {
    
    
      //1创建一个ArrayList
      ArrayList<Node> nodes = new ArrayList<Node>();

      //遍历 bytes , 统计每一个 byte出现的次数->map[key,value]
      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);
         }
      }

      /**
       *把每一个键值对转成一个Node对象,并加入到nodes集合中
       * 下面的方法表示map的遍历
       */
      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);
         //取出第一颗最小的二叉树
         Node leftNode = nodes.get(0);
         //取出第二颗最小的二叉树
         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);
   }
}
 //向左递归
        getCodes(node.left, "0", stringBuilder2);
        //向右递归
        getCodes(node.right, "1", stringBuilder2);
     } else { //说明是一个叶子结点
        //就表示找到某个叶子结点的最后
        huffmanCodes.put(node.data, stringBuilder2.toString());
     }
  }

}

//前序遍历的方法
private static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println(“赫夫曼树为空”);
}
}

/**
* @param bytes 接收一个字节数组
* @return 返回list, 形式为 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]…],
*/
private static List getNodes(byte[] bytes) {
//1创建一个ArrayList
ArrayList nodes = new ArrayList();

  //遍历 bytes , 统计每一个 byte出现的次数->map[key,value]
  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);
     }
  }

  /**
   *把每一个键值对转成一个Node对象,并加入到nodes集合中
   * 下面的方法表示map的遍历
   */
  for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
     nodes.add(new Node(entry.getKey(), entry.getValue()));
  }
  return nodes;

}

//可以通过 List 创建对应的赫夫曼树
private static Node createHuffmanTree(List nodes) {
while (nodes.size() > 1) {
//排序, 从小到大
Collections.sort(nodes);
//取出第一颗最小的二叉树
Node leftNode = nodes.get(0);
//取出第二颗最小的二叉树
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);

}
}






















猜你喜欢

转载自blog.csdn.net/weixin_43334389/article/details/114114139