视频链接:https://www.bilibili.com/video/BV1HQ4y1d7th
视频范围P87 - P120
1.哈希表(线性结构)
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
需求:
- 添加学生编号时按照从低到高的顺序
- 使用链表来实现哈希表,该链表不带表头
学生类:
package hashtable;
public class Student {
public int id;
public String name;
//指向下一个结点的指针
public Student next;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
学生链表类:
package hashtable;
public class StudentLinkedList {
private Student head;
//添加结点
//如果添加时
public void add(Student newStudent){
//如果添加时是第一个学生对象,则直接复制给第一个结点
if (head == null){
head = newStudent;
return;
}
Student temp = head;
while (true){
if (temp.next == null){
break;
}
//继续往后查找
temp = temp.next;
}
//追加了新的结点
temp.next = newStudent;
}
//查看数据
public void list(int no){
if (head == null){
System.out.println("第" + no + "链表是空...");
return;
}
Student temp = head;
while (true){
System.out.printf("id = %d name = %s\t",temp.id,temp.name);
if (temp.next == null){
break;
}
temp = temp.next;
}
System.out.println();
}
//根据学生的编号查询结点
public Student findById(int id){
if (head == null){
System.out.println("空链表...");
return null;
}
Student temp = head;
while (true){
if (temp.id == id){
break;
}
if (temp.next == null){
temp = null;
break;
}
temp = temp.next;
}
return temp;
}
}
哈希表:
package hashtable;
public class HashTable {
private StudentLinkedList[] studentLinkedLists;
private int size;
public HashTable(int size) {
this.size = size;
studentLinkedLists = new StudentLinkedList[size];
//数组中添加链表对象
for (int i = 0; i < size; i++) {
studentLinkedLists[i] = new StudentLinkedList();
}
}
//哈希函数
public int hashCodes(int sid){
return sid % size;
}
//添加学生
public void add(Student student){
//决定了数组中的下标
int hashVal = hashCodes(student.id);
//添加到指定的链表中
studentLinkedLists[hashVal].add(student);
}
//查看哈希表中的元素
public void list(){
for (int i = 0; i < size; i++) {
studentLinkedLists[i].list(i);
}
}
//根据学员编号查询
public void findByStudentId(int sid){
int hashVal = hashCodes(sid);
Student students = studentLinkedLists[hashVal].findById(sid);
if (students != null){
System.out.printf("在第%d条链表中找到了学员编号是:%d\n",(hashVal + 1),sid);
}else{
System.out.println("整个hash表中未找到学员");
}
}
}
测试类:
package hashtable;
public class Test {
public static void main(String[] args) {
HashTable hashTable = new HashTable(10);
//添加学员结点
Student student1 = new Student(1,"张三");
Student student2 = new Student(2,"张四");
Student student3 = new Student(2,"张五");
Student student4 = new Student(3,"张六");
Student student5 = new Student(3,"张七");
hashTable.add(student1);
hashTable.add(student2);
hashTable.add(student3);
hashTable.add(student4);
hashTable.add(student5);
hashTable.list();
hashTable.findByStudentId(3);
}
}
2.树
数组存储方式:
- 优点:通过下标方式访问元素,速度快,对于有序数组还可以使用二分查找提高检索速度
- 缺点:如果要检索具体某个值,或者插入值会整体移动,效率较低
树存储方式分析:
- 能提高数据存储,读取的效率,比如可以使用二叉树既可以保证数据检索速度
- 也可以保证数据的插入,删除,修改的速度
2.1 二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要
二叉树特点:每个结点最多只能有两棵子树,且有左右之分
可以使用前序,中序,后序对二叉树进行遍历
- 前序遍历:先输出父结点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再遍历父结点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后遍历父结点
结点类:
package tree;
public class Node {
private int no;
private String name;
private Node left;
private Node right;
public Node(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 Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
", name='" + name + '\'' +
", left=" + left +
", right=" + right +
'}';
}
//前序遍历【遍历所有的】
public void preSelect(){
//先输出父结点
System.out.println(this);
if (this.left != null){
this.left.preSelect();
}
if (this.right != null){
this.right.preSelect();
}
}
//中序遍历
public void infixSelect(){
//左结点,父结点,右结点
if (this.left != null){
this.left.infixSelect();
}
System.out.println(this);
if (this.right != null){
this.right.infixSelect();
}
}
//后序遍历
public void postSelcet(){
if (this.left != null){
this.left.postSelcet();
}
if (this.right != null){
this.right.postSelcet();
}
System.out.println(this);
}
//前序遍历查找【根据结点编号】
public Node preSearch(int no){
//判断是否是当前结点
if (this.no == no){
return this;
}
//查询左子结点
Node lnode = null;
if (this.left != null){
lnode = this.left.preSearch(no);
}
if (lnode != null){
return lnode;
}
//查询当前结点的右子结点,如果不为空,则继续递归前序查找
if (this.right != null){
lnode = this.right.preSearch(no);
}
return lnode;
}
//中序遍历查找【根据结点编号】
public Node infixSearch(int no){
Node node = null;
//查询左子结点
if (this.left != null){
node = this.left.infixSearch(no);
}
if (node != null){
return node;
}
//判断是否是当前结点
if (this.no == no){
return this;
}
//查询当前结点的右子结点,如果不为空,则继续递归前序查找
if (this.right != null){
node = this.right.infixSearch(no);
}
return node;
}
//后序遍历查找【根据结点编号】
public Node postSearch(int no){
Node node = null;
//查询左子结点
if (this.left != null){
node = this.left.postSearch(no);
}
if (node != null){
return node;
}
//查询当前结点的右子结点,如果不为空,则继续递归前序查找
if (this.right != null){
node = this.right.postSearch(no);
}
if (node != null){
return node;
}
//判断是否是当前结点
if (this.no == no){
return this;
}
return node;
}
//删除结点两种情况:
//1.删除的结点是叶子结点
//2.删除的结点是子树,非叶子结点
public void delNode(int no){
//1.当前结点左结点不为空,并且左子结点就是需要删除的结点 this.left = null
//2.当前结点右结点不为空,并且右子结点就是需要删除的结点,this.right = null
//3.如果1,2步没有执行,那么需要向左子树进行递归删除
//4.如果第3步没有执行,那么则向右子树进行递归删除
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);
}
}
}
二叉树类:
package tree;
public class BinaryTree {
private Node root;
public void setRoot(Node node){
this.root = node;
}
//前序遍历
public void preSelect(){
if (this.root != null){
this.root.preSelect();
}else{
System.out.println("空二叉树,无法遍历...");
}
}
//中序遍历
public void infixSelect(){
if (this.root != null){
this.root.infixSelect();
}else{
System.out.println("空二叉树,无法遍历...");
}
}
//后序遍历
public void postSelect(){
if (this.root != null){
this.root.postSelcet();
}else{
System.out.println("空二叉树,无法遍历...");
}
}
//前序查询【根据结点编号】
public Node preNode(int no){
if (root != null){
return root.preSearch(no);
}else{
System.out.println("空二叉树,无法遍历...");
return null;
}
}
//中序查询【根据结点编号】
public Node infixNode(int no){
if (root != null){
return root.infixSearch(no);
}else{
System.out.println("空二叉树,无法遍历...");
return null;
}
}
//中序查询【根据结点编号】
public Node postNode(int no){
if (root != null){
return root.postSearch(no);
}else{
System.out.println("空二叉树,无法遍历...");
return null;
}
}
//删除结点
public void delNode(int no){
if (root != null){
if (root.getNo() == no){
root = null;
}else{
root.delNode(no);
}
}else {
System.out.println("空二叉树,无法删除...");
}
}
}
测试类:
package tree;
public class Test {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
Node root = new Node(1,"孙尚香");
Node node2 = new Node(2,"夏侯惇");
Node node3 = new Node(3,"貂蝉");
Node node4 = new Node(4,"吕布");
Node node5 = new Node(5,"虞姬");
Node node6 = new Node(6,"王昭君");
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setRight(node6);
binaryTree.setRoot(root);
//删除结点
binaryTree.delNode(5);
binaryTree.preSelect();
System.out.println("-----------------------");
binaryTree.infixSelect();
System.out.println("-----------------------");
binaryTree.postSelect();
//前序查找【根据编号】
Node node = binaryTree.preNode(5);
if (node != null){
System.out.printf("信息为:id = %d name = %s",node.getNo(),node.getName());
}else {
System.out.println("没有找到结点");
}
}
}
结点树:
运行结果:
2.2 顺序存储二叉树
二叉树的顺序存储就是用一组连续的存储单元(数组)存放二叉树中的结点元素,一般按照二叉树结点自上向下、自左向右的顺序存储。
- 顺序二叉树通常只考虑完全二叉树
- 第n个元素的左子结点为 2 ∗ n + 1 2*n+1 2∗n+1
- 第n个元素的右子结点为 2 ∗ n + 2 2*n+2 2∗n+2
- 第n个元素的父结点为 ( n − 1 ) / 2 (n-1)/2 (n−1)/2
package tree;
public class ArrayBinaryTree {
private int[] arrays;
public ArrayBinaryTree(int[] arrays) {
this.arrays = arrays;
}
//前序遍历顺序存储二叉树
public void preSelect(int index){
if (this.arrays == null || arrays.length == 0){
System.out.println("存储数组为空....");
}
//向左递归
if ((index * 2 + 1) < arrays.length){
preSelect(index * 2 + 1);
}
//向右递归
if ((index * 2 + 2) < arrays.length){
preSelect(index * 2 + 2);
}
}
}
2.3 线索化二叉树
- 在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化
- 对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
- 一个结点的前一个结点,称之为前驱结点,一个结点后一个结点称之为后继结点
结点类:
package clue;
public class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
//0表示指向的是左子树
//1表示指向的前驱
private int noLeft;
//0表示指向的是右子树
//1表示指向的后继
private int noRight;
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;
}
public int getNoLeft() {
return noLeft;
}
public void setNoLeft(int noLeft) {
this.noLeft = noLeft;
}
public int getNoRight() {
return noRight;
}
public void setNoRight(int noRight) {
this.noRight = noRight;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//删除结点
//删除的可能是一个叶子结点,也可能是非叶子结点
public void delNode(int no){
//1.如果当前结点左结点不为空,并且左子结点正好是要删除的,this.left = null;
//2.如果当前结点右结点不为空,并且右子结点正好是要删除的,this.right = null;
//3. 1和2都没有执行,左子树递归
//4.3没有执行,向右递归找右子树
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 preSelect(){
System.out.println(this);
if (this.left != null){
this.left.preSelect();
}
if (this.right != null){
this.right.preSelect();
}
}
//前序遍历【根据编号查找】
public HeroNode preSelect(int no){
if (this.no == no){
return this;
}
HeroNode temp = null;
//往左递归
if (this.left != null){
temp = this.left.preSelect(no);
}
if (temp != null){
return temp;
}
//往右递归
if (this.right != null){
temp = this.right.preSelect(no);
}
return temp;
}
}
线索化二叉链表树:
package clue;
public class ClueBianryTree {
private HeroNode root;
//当前结点前驱结点指针
private HeroNode pre;
public void setRoot(HeroNode root) {
this.root = root;
}
//调用这个方法等于把非线索化二叉树转换为线索化二叉树
public void clueNode(){
this.clueNode(root);
}
//把普通二叉树转换为线索化二叉树
public void clueNode(HeroNode node){
//判断是否可以线索化当前结点
if (node == null){
return;
}
//先线索化左子树
clueNode(node.getLeft());
//处理当前结点前驱
if (node.getLeft() == null){
node.setLeft(pre);
node.setNoLeft(1);
}
//处理当前结点后继
if (pre != null && pre.getRight() == null){
pre.setRight(node);
pre.setNoRight(1);
}
//保证处理完一个结点后,就需要让当前结点作为下一个结点的前驱
pre = node;
//线索化右子树
clueNode(node.getRight());
}
//遍历线索化的二叉树
public void clueList(){
//临时结点
HeroNode node = root;
while (node != null){
//向左查询头结点
while (node.getNoLeft() == 0){
node = node.getLeft();
}
System.out.println(node);
//查找后继结点
while (node.getNoRight() == 1){
node = node.getRight();
System.out.println(node);
}
node = node.getRight();
}
}
}
测试类:
package clue;
public class TestApp {
public static void main(String[] args) {
HeroNode root = new HeroNode(1,"吕布");
HeroNode node2 = new HeroNode(3,"貂蝉");
HeroNode node3 = new HeroNode(6,"曹操");
HeroNode node4 = new HeroNode(8,"刘备");
HeroNode node5 = new HeroNode(10,"关羽");
HeroNode node6 = new HeroNode(14,"张飞");
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
ClueBianryTree clueBianryTree = new ClueBianryTree();
clueBianryTree.setRoot(root);
//把普通二叉树线索化为线索二叉树
clueBianryTree.clueNode();
//获取10的前驱和后继
HeroNode left = node5.getLeft();
HeroNode right = node5.getRight();
System.out.println("10号结点前驱是:" + left.getNo() + " 后继是:" + right.getNo());
//把线索化二叉树按照中序遍历查询
clueBianryTree.clueList();
}
}
运行结果: