第十章 哈希表和二叉树
一、哈希表
1.介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
给定表 M,存在函数 f (key),对任意给定的关键值 key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表 M 为哈希(Hash)表,函数 f (key) 为哈希(Hash) 函数
基本概念
- 若关键字为 k,则其值存放在 f(k) 的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系 f 为散列函数,按这个思想建立的表为散列表
- 对不同的关键字可能得到同一散列地址,即 k1 ≠ k2,而 f(k1) = f(k2),这种现象称为冲突(Collision)。具有相同函数值的关键字对该散列函数来说称作同义词。综上所述,根据散列函数 f(k) 和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的 ”像“ 作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址
- 若对于关键字集合中的任一个关键字,经散列函数映像到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个 ”随机的地址“,从而减少冲突
哈希表可以通过数组 + 链表实现,也可以通过数组 + 二叉树实现
提出问题:
有一个公司,当有新员工来报到时,要求将该员工的信息加入(id,性别,年龄,住址…),当输入该员工的 id 时,要求查找到该员工的所有信息,要求使用哈希表
数据结构设计:
class HashTab{
EmplinkedList[]; //链表
empLinkedListArr; //链表数组
add();
list();
find();
散列函数(); //决定 id 对应到哪个链表
}
class EmpLinkedList{
Emp head = null; //头指针,指向当前链表的第一个雇员
add();
list();
find();
}
class Emp{
id;
name;
address;
}
2.代码实现
package com.sisyphus.hashtab;
import java.util.Scanner;
/**
* @Description: 哈希表$
* @Param: $
* @return: $
* @Author: Sisyphus
* @Date: 7/22$
*/
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;
}
}
}
}
//创建 HashTab 管理多条链表
class HashTab{
private EmpLinkedList[] empLinkedListArray;
private int size;//表示有多少条链表
//构造器
public HashTab(int size){
this.size = size;
//初始化 empLinkedListArray
empLinkedListArray = new EmpLinkedList[size];
//分别初始化每个链表
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;
}
}
//表示一个雇员
class Emp{
public int id;
public String name;
public Emp next; //next 默认为空
public Emp(int id, String name) {
super();
this.id = id;
this.name = name;
}
}
//创建 EmpLinkedList,表示链表
class EmpLinkedList{
//头指针,执行第一个 Emp,因此我们这个链表的 head 是直接指向第一个 Emp
private Emp head; //默认为 null
//添加雇员到链表
//说明
//1.假定,当添加雇员时,id 时自增长的,即 id 的分配总是从小到大
// 因此我们将该雇员直接加入到本链表的最后即可
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){ //说明链表为空
System.out.println("第" + (no + 1) + "链表为空");
return;
}
System.out.print("第" + (no + 1) + "条链表信息为为");
Emp curEmp = head; //辅助指针
while(true){
System.out.printf("=> id=%d name=%s\t",curEmp.id,curEmp.name);
if (curEmp.next == null){//说明 curEmp 已经是最后结点
break;
}
curEmp = curEmp.next;//后移,遍历
}
System.out.println();
}
//根据 id 查找雇员
//如果查找到,就返回 Emp,如果没有找到,就返回 null
public Emp findEmpById(int id){
//判断链表是否为空
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;
}
}
二、二叉树
1.介绍
二叉树(binary tree)是指树中节点的度不大于 2 的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树
相关术语:
- 结点:包含一个数据元素及若干指向子树分支的信息
- 结点的度:一个结点拥有子树的数目称为结点的度
- 叶子结点:也称为终端结点,没有子树的结点或者度为零的结点
- 分支结点:也称为非终端结点,度不为零的结点称为非终端结点
- 树的度:树中所有结点的度的最大值
- 结点的层次:从根结点开始,假设根结点为第 1 层,根结点的子结点为第 2 层,依此类推,如果某一个结点位于第 L 层,则其子结点位于 L + 1 层
- 树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度
- 有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树
- 无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树
10.森林:由 m (m ≥ 0) 棵互不相交的树构成一片森林。如果把一棵非空的树的根结点删除,则该树就变成了一片森林,森林中的树由原来根结点的各棵子树构成
特殊类型:
- 满二叉树:指的是深度为 k 且含有 2^k - 1 个结点的二叉树
- 完全二叉树:设二叉树的深度为 h,除第 h 层外,其他各层的结点数都达到最大个数,且第 h 层所有的结点都连续集中在最左边。也就是说,树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应
- 第 n 个元素的左子结点为 2n
- 第 n 个元素的右子结点为 2n + 1
- 第 n 个元素的父结点为 (n - 1)/ 2
性质:
- 二叉树的第 i 层上至多有 2^(i - 1) 个点(i ≥ 1)
- 深度为 h 的二叉树中至多含有 2^h - 1 个结点
- 若在任意一棵二叉树中,有 n0 个叶子结点,有 n2 个度为 2 的结点,则必有 n0 = n2 + 1
- 具有 n 个 结点的完全二叉树深度为 log2(x + 1) ,其中 x 表示不大于 n 的最大整数
- 若对一棵有 n 个结点的完全二叉树进行顺序编号(1 ≤ i ≤ n),那么,对于编号为 i (i ≥ 1)的结点:
当 i = 1 时,该结点为根,它无双亲结点
当 i > 1 时,该结点的双亲结点的编号为 i / 2
若 2i < n,则有编号为 2i 的左结点,否则没有左结点
若 2i + 1 ≤ n,则有编号为 2i + 1 的右节点,否则没有右节点
2.遍历二叉树
前序遍历二叉树
思路:
访问到一个结点后就打印该结点,并继续遍历其左右子树
遍历顺序:
GDAFEMHZ
代码实现:
//前序遍历
public void preOrder(){
//打印父结点
System.out.println(this);
//递归向左子树前序遍历
if (this.left != null){
this.left.preOrder();
}
//递归向右子树前序遍历
if (this.right != null){
this.right.preOrder();
}
}
中序遍历二叉树
思路:
访问到一个结点后将其暂存,遍历完左子树后,再打印该结点的值,然后遍历右子树
遍历顺序:
ADEFGHMZ
代码实现:
//中序遍历
public void infixOrder(){
//递归向左子树中序遍历
if (this.left != null){
this.left.infixOrder();
}
//打印父结点
System.out.println(this);
//递归向右子树中序遍历
if (this.right != null){
this.right.infixOrder();
}
}
后续遍历二叉树
思路:
访问到一个结点后将其暂存,遍历完左右子树后,再打印该结点的值
遍历顺序:
AEFDHZMG
代码实现:
//后序遍历
public void postOrder(){
//递归向左子树后序遍历
if (this.left != null){
this.left.postOrder();
}
//递归向右子树后序遍历
if (this.right != null){
this.right.postOrder();
}
//打印父结点
System.out.println(this);
}
层次遍历二叉树
思路:
建立一个循环队列,先将二叉树根结点入队列,然后出队列,访问根结点,如果它有左子树,则将左子树的根结点入队:如果它有右子树,则将右子树的根结点入队。然后出队列,对出队结点访问,如此反复,直到队列为空为止
遍历顺序:
ADMAFHZ
代码实现:
//层次遍历
public void levelOrder(){
ArrayDeque<HeroNode> queue = new ArrayDeque<>(20);
//首先将根结点加入队列中
queue.add(this);
//遍历二叉树
while(!queue.isEmpty()){
HeroNode tempNode = queue.poll();
System.out.println(tempNode);
if (tempNode.left != null){
queue.add(tempNode.left);
}
if (tempNode.right != null){
queue.add(tempNode.right);
}
}
}
3.查找二叉树
前序查找二叉树
思路:
- 首先拿根结点进行比较,如果相等,直接返回,否则左递归前序查找
- 如果左递归找到,直接返回,否则右递归前序查找
- 如果右递归找到返回,否则返回空
查到 H 结点
比较次数:
7
代码实现:
//前序遍历
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;
}
//1.左递归前序查找,找到结点,则返回,否继续判断
//2.当前的结点的右子结点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null){
resNode = this.right.preOrdersearch(no);
}
return resNode;
}
中序查找二叉树
思路:
- 首先左递归查找,如果找到,直接返回,否则和根结点比较
- 如果比较相等,直接返回,否则右递归中序查找
- 如果右递归找到则返回,否则返回空
查找 H 结点
比较次数:
6
代码实现:
//中序遍历
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;
}
后序查找二叉树
思路:
- 首先左递归查找,如果找到,直接返回,否则右递归中序查找
- 如果右递归找到,直接返回,否则和根结点比较
- 如果比较不相等则返回空
查找 H 结点
比较次数:
5
代码实现:
//后序遍历
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;
}
4.二叉树删除节点
思路:
- 如果删除的结点就是 root结点,则等价将二叉树置空
- 如果删除的结点是非叶子结点,则删除该子树
- 如果删除的结点是叶子结点,则删除该结点
- 因为我们的二叉树是单向的,所以只能判断当前结点的子结点是不是需要删除结点
- 如果当前结点的左子结点不为空,并且左子结点就是要删除的结点,执行 this.left = null,并 return
- 如果当前结点的右子结点不为空,并且左子结点就是要删除的结点,执行 this.right = null,并 return
- 如果第 1 步和第 2 步没有 return,那么我们就需要向左子树进行递归删除
- 如果第 3 步仍没有 return,则向右子树进行递归删除
5.二叉树综合实例
代码:
package com.sisyphus.tree;
import java.util.ArrayDeque;
/**
* @Description: 二叉树$
* @Param: $
* @return: $
* @Author: Sisyphus
* @Date: 7/23$
*/
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("前序遍历"); //12354
binaryTree.preOrder();
System.out.println("中序遍历"); //21534
binaryTree.infixOrder();
System.out.println("后序遍历"); //25431
binaryTree.postOrder();
//前序遍历
System.out.println("前序遍历");
HeroNode resNode = binaryTree.preOrderSearch(5);
if (resNode != null){
System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
System.out.println();
}else{
System.out.printf("没有找到 no=%d 的英雄",5);
System.out.println();
}
//中序遍历
System.out.println("中序遍历");
resNode = binaryTree.infixOrderSearch(5);
if (resNode != null){
System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
System.out.println();
}else{
System.out.printf("没有找到 no=%d 的英雄",5);
System.out.println();
}
//后序遍历
System.out.println("后序遍历");
resNode = binaryTree.postOrderSearch(5);
if (resNode != null){
System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
System.out.println();
}else{
System.out.printf("没有找到 no=%d 的英雄",5);
System.out.println();
}
System.out.println("===================================================");
//测试删除结点
System.out.println("删除前,前序遍历");
binaryTree.preOrder();
binaryTree.delNode(5);
System.out.println("删除后,前序遍历");
binaryTree.preOrder();
System.out.println("======================================================");
System.out.println("层次遍历");
binaryTree.levelOrder();
}
}
//定义 BinaryTree 二叉树
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 void levelOrder(){
if (this.root != null){
this.root.levelOrder();
}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 root.postOrderSearch(no);
}else{
return null;
}
}
}
//先创建 HeroNode 结点
class HeroNode{
private int no;
private String name;
private HeroNode left;
private HeroNode right;
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 + '\'' +
'}';
}
//递归删除结点
public void delNode(int no){
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);
}
//层次遍历
public void levelOrder(){
ArrayDeque<HeroNode> queue = new ArrayDeque<>(20);
//首先将根结点加入队列中
queue.add(this);
//遍历二叉树
while(!queue.isEmpty()){
HeroNode tempNode = queue.poll();
System.out.println(tempNode);
if (tempNode.left != null){
queue.add(tempNode.left);
}
if (tempNode.right != null){
queue.add(tempNode.right);
}
}
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//查找
//前序遍历
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;
}
//1.左递归前序查找,找到结点,则返回,否继续判断
//2.当前的结点的右子结点是否为空,如果不空,则继续向右递归前序查找
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;
}
}