1、数据结构和算法内容介绍
1.1 几个经典的算法面试题
- 有一个字符串 str1=“sss1 222sss ss2ww2ww2ww2”,和一个子串 str2=“ss2ww2”,现在要判断 str1中是否包含 str2,如果存在,就返回第一次出现的位置,如果没有,则返回 -1。要求用最快的速度来完成匹配。
- 解决方案:暴力匹配、KMP算法(部分匹配表)
- 汉诺塔游戏:将A塔中的所有圆盘移动到C塔。并且规定小圆盘不能放在大圆盘之上,在三根柱子之间一次只能移动一个圆盘。
- 解决方案:分治算法
- 八皇后问题:是回溯算法的点心案例。在 8*8 格国际象棋上摆放八个皇后,使其不可以相互攻击。即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种解法?
- 解决方案:回溯方法
- 马踏棋盘:马踏棋盘算法也被称为骑士周游问题,将马放在国际象棋的 8*8 棋盘,马按照 马走日 的方式进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格
- 解决方案:会使用到图的 深度优化遍历算法(DFS)+ 贪心算法优化
1.2 数据结构和算法的重要性
2、数据结构和算法概述
2.1 数据结构和算法的关系
- 程序 = 数据结构 + 算法
2.2 线性结构和非线性结构
2.2.1 线性结构
- 特点:数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表成为顺序表,顺序表中的存储元素是连续的
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数组、队列、链表、栈,
2.2.2 非线性结构
- 非线性结构包括:二维数组、多维数组,广义表,数结构,图结构
2.3 实际过程中遇到的问题
- 字符串替换问题
str.replaceAll("JAVA","尚硅谷。。");
// 单链表
- 五子棋问题(二维数组-稀疏数组)
- 约瑟夫丢手帕问题(单向环形链表)
- 修路问题(最小生成树【加权值】【数据结构】+ 普里姆算法)
- 最短路径问题(图+弗洛伊德算法)
- 汉诺塔(分支算法)
- 八皇后问题(回溯法)
3、稀疏数组和队列(sparse array)
3.1 稀疏数组
3.1.1 实际的需求
- 编写的五子棋程序中,有存盘退出和续上盘的功能
- 分析问题
- 因为该二维数组的很多的默认值为0,因此记录了很多没有意义的数据 ---- 稀疏数组
3.1.2 稀疏数组的基本介绍
- 基本介绍
- 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组
- 稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
3.1.3 实现思路1(二维数组 => 稀疏数组)
- 遍历原始的二维数组,得到有效数据的个数
- 根据 sum 就可以创建稀疏数组
sparseArr int[sum + 1][3]
- 将二维数组的有效数据存入到 稀疏数组 中
3.1.4 实现思路2(稀疏数组 => 原始二维数组)
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
chessArr2 = int[11][11];
- 再读取稀疏数组中后几行的数据,并赋值给二维数组
3.1.5 代码实现
package sparseArray;
/**
* @author houbj
* @date 2020/12/7 14:39
*/
public class SparseArray {
public static void main(String[] args) {
/**
* 1. 创建一个二维数组 11 * 11
* 2. 0:表示没有棋子, 1:表示有黑子, 2:表示白子
*/
int chessArr[][] = new int[11][12];
System.out.println(chessArr.length+"行," +chessArr[0].length+ "列");
chessArr[1][2] = 1;
chessArr[2][3] = 2;
chessArr[10][6] = 5;
/**
* 3. 输出原始数组
*/
System.out.println("1.原始数组输出:");
for (int[] arr: chessArr){
for(int a: arr) {
System.out.printf("%d\t",a);
}
System.out.println();
}
/**
* 4. 二维数组转稀疏数组
*/
int sum = 0;
for (int[] arr: chessArr){
for(int a: arr) {
if (a != 0) sum ++;
}
}
System.out.println("总共有 "+ sum + " 个数据。");
/**
* 5. 创建稀疏数组
*/
int sparseArr[][] = new int[sum+1][3];
sparseArr[0][0] = chessArr.length;
sparseArr[0][1] = chessArr[0].length;
sparseArr[0][2] = sum;
int tag = 0;
for (int i = 0; i < chessArr.length ; i ++) {
for (int j = 0; j < chessArr[0].length; j++ ){
if (chessArr[i][j] != 0) {
tag ++;
sparseArr[tag][0] = i;
sparseArr[tag][1] = j;
sparseArr[tag][2] = chessArr[i][j];
}
}
}
/**
* 6. 稀疏数组输出
*/
System.out.println("2.稀疏数组输出:");
for (int[] arr: sparseArr) {
for (int a: arr) {
System.out.printf("%d\t",a);
}
System.out.println();
}
int chaseArr2 [][] = new int[sparseArr[0][0]][sparseArr[0][1]];
for (int i = 1 ; i < sparseArr.length ; i ++) {
chaseArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
System.out.println("3.原始数组输出:");
for (int[] arr: chaseArr2){
for(int a: arr) {
System.out.printf("%d\t",a);
}
System.out.println();
}
}
}
3.2 队列
3.2.1 数组模拟队列
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而rear则是随着数据的输入而改变
- 代码实现:
package queueArray;
/**
* @author houbj
* @date 2020/12/7 15:52
*/
public class QueueArray {
public static void main(String[] args) {
ArrayQueue arrayQueue = new ArrayQueue(4);
arrayQueue.addQueue(5);
arrayQueue.addQueue(6);
arrayQueue.show();
System.out.println("队列头为:"+arrayQueue.showFront());
arrayQueue.popQueue();
arrayQueue.show();
System.out.println("队列头为:"+arrayQueue.showFront());
}
}
class ArrayQueue {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据,模拟队列
public ArrayQueue(int arrSize){
maxSize = arrSize;
arr = new int[maxSize];
front = -1;
rear = -1;
}
public boolean isFull(){
return rear == maxSize-1;
}
public boolean isEmpty(){
return rear == front;
}
public void addQueue(int n){
if (isFull()) {
System.out.println("队列已满...");
} else {
rear ++ ;
arr[rear] = n;
}
}
public int popQueue(){
if (isEmpty()) {
throw new RuntimeException("队列为空...");
} else {
front ++ ;
return arr[front];
}
}
public void show(){
if (isEmpty()) {
System.out.println("队列为空...");
} else {
System.out.println("队列为:");
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
}
public int showFront(){
if (isEmpty()) {
throw new RuntimeException("队列为空...");
} else {
return arr[front+1];
}
}
}
3.2.2 数组模拟队列实现环形队列
- 理解思路:
- front变量的含义做一个调整:front就指向队列的第一个元素,也就是说 arr[front]就是队列的第一个元素front的初始值 = 0
- rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间位置作为约定。rear的初始值 = 0
- 当队列满时,条件是 (rear+1)% maxSize = front
- 对队列为空的条件,rear == front
- 队列中有效的数据个数为:(rear + maxSize - front)% maxSize
package queueArray;
/**
* @author houbj
* @date 2020/12/7 20:22
*/
public class CircleQueueArray {
public static void main(String[] args) {
CircleArray circleArrayQueue = new CircleArray(5);
circleArrayQueue.addQueue(1);
circleArrayQueue.addQueue(2);
circleArrayQueue.addQueue(3);
circleArrayQueue.addQueue(4);
circleArrayQueue.addQueue(5);
circleArrayQueue.showQueue();
circleArrayQueue.getQueue();
circleArrayQueue.showQueue();
circleArrayQueue.addQueue(6);
circleArrayQueue.showQueue();
}
}
class CircleArrayQueue {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据,模拟队列
public CircleArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new int[maxSize];
}
public boolean isFull(){
System.out.println( "full --- rear: "+ rear + " , front:"+front);
return (rear + 1) % maxSize == front;
}
public boolean isEmpty(){
return rear==front;
}
public void addQueue(int n){
if (isFull()) {
System.out.println("队列满,不能添加数据。。");
return;
}
System.out.println("addQueue -- rear: "+ rear + " , front:"+front);
arr[rear] = n;
rear = (rear + 1) % maxSize;
System.out.println("addQueue -- 显示队列:");
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n", i%maxSize, arr[i%maxSize]);
}
}
public int popQueue(){
if (isEmpty()) {
throw new RuntimeException("队列空,不可以删除数据。。");
}
int value = arr[front];
front = (front+1)%maxSize;
return value;
}
public void show(){
if (isEmpty()) {
System.out.println("队列空。。。");
return;
}
System.out.println("显示队列:");
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n", i%maxSize, arr[i%maxSize]);
}
}
public int size(){
return (rear+maxSize-front)%maxSize;
}
}
4、链表
4.1 基本概述
- 链表是以节点的方式来存储的
- 每个节点包含data域,next域:指向下一个节点
- 链表分带头节点的链表和没有头节点的链表
- 头节点
- 不存放具体的数据
- 作用就是表示单链表的头
4.2 单链表
4.2.1 单链表的实现
package LinkedList;
/**
* @author houbj
* @date 2020/12/8 11:54
*/
public class SingleLinked {
public static void main(String[] args) {
ChargeList chargeList = new ChargeList();
chargeList.addNode(new HeroNode(1,"l", "ll"));
chargeList.addNode(new HeroNode(2,"d", "dd"));
chargeList.addNode(new HeroNode(3,"c", "cc"));
chargeList.show();
chargeList.show2();
}
}
/**
* 定义一个类:管理linkedList
*/
class ChargeList{
/**
* 先初始化一个头节点,头节点不要动
*/
private HeroNode head = new HeroNode(0,"","");
/**
* 添加链表
* @param heroNode
*/
public void addNode(HeroNode heroNode) {
/**
* 找到当前链表的最后节点,将next域指向最新节点
*/
HeroNode temp = this.head;
while (true) {
if (temp.next == null) {
temp.next = heroNode;
break;
}
temp = temp.next;
}
}
/**
* 显示链表
*/
public void show(){
System.out.println("遍历链表 -- : ");
if (head.next == null) {
System.out.println("链表为空");
} else {
HeroNode node = head.next;
while (true) {
if (node == null) {
break;
} else {
System.out.println(node);
node = node.next;
}
}
}
}
public void show2(){
System.out.println("遍历链表 -- : ");
HeroNode temp = this.head;
while (true) {
if (temp.next == null) {
return;
}
temp = temp.next;
System.out.println(temp);
}
}
}
/**
* 每个对象就是一个节点
*/
class HeroNode{
public int num;
public String name;
public String nickName;
HeroNode next;
public HeroNode(int num, String name, String nickName){
this.num = num;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"num=" + num +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
", next=" + next +
'}';
}
}
4.2.2 单链表面试题
- 查找单链表中的倒数第K个节点
- 实例:
HeroNode temp = head.next;
while(temp != null) {
length++;
temp = temp.next;
}
- 单链表的反转
/**
* 错误实例
*/
public void reverse(HeroNode node){
if (node.next == null || node.next.next == null) {
System.out.println("无需反转----");
return;
}
HeroNode temp = node.next;
HeroNode newNode = new HeroNode(0,"","");
HeroNode tag;
boolean flag = true;
while (flag) {
if (temp.next == null) {
flag = false;
}
tag = newNode.next;
newNode.next = temp;
newNode.next.next = tag;
temp = temp.next;
}
System.out.println(newNode);
}
/**
* 正确实例
*/
public void reverseNode(HeroNode node) {
if (node.next == null || node.next.next == null) {
System.out.println("无需反转----");
return;
}
HeroNode cur = head.next; // 定义一个辅助的指针变量,帮助我们遍历原来的链表
HeroNode next = null; // 指向当前节点(cur)的下一个节点
HeroNode newNode = new HeroNode(0,"",""); //遍历一个新节点,放置在这个头节点后面
while (cur!= null) {
next = cur.next; //先保存当前节点的下一个节点
cur.next = newNode.next; // 将cur下一个节点指向新链表的最前端
newNode.next = cur;
cur = next;
}
node.next = newNode.next;
System.out.println(node);
}
/**
* 正确实例
*/
public void reverse2(HeroNode node){
if (node.next == null || node.next.next == null) {
System.out.println("无需反转----");
return;
}
HeroNode temp = node.next;
HeroNode newNode = new HeroNode(0,"","");
HeroNode tag;
HeroNode tag2;
boolean flag = true;
while (flag) {
if (temp.next == null) {
flag = false;
}
tag = temp.next;
tag2 = newNode.next;
newNode.next = temp;
newNode.next.next = tag2;
temp = tag;
}
System.out.println(newNode);
}
- 从尾到头打印单链表、
- 方式1:先将原来的链表反转在进行打印(会破坏原来的单链表结构)
- 方式2:可以利用栈数据结构,将各节点压入栈中,然后利用栈的先进后出的特点,实现逆序打印
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
stack.add("jack");
stack.add("tom");
stack.add("smith");
while (stack.size() >0) {
System.out.println(stack.pop());
}
}
- 合并两个有序的单链表,合并之后的链表依然有序
4.3 双向链表
4.3.1 与单向链表的对比
- 单向链表,查找的方向只能是一个方向,而双向链表查找的方向可以向前也可以向后
- 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,需要找到temp的下一个节点来删除
4.3.2 双向链表的删除
- 找到要删除的节点
- temp.pre.next = temp.next
- temp.next.pre = temp.pre
4.3.3 实例
- 添加
class ChargeDoubleLinked{
DoubleHeroNode head = new DoubleHeroNode(0, "", "");
public DoubleHeroNode getHead(){
return head;
}
public void addNode(DoubleHeroNode node){
DoubleHeroNode temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = node;
node.pre = temp;
}
}
4.4 单向环形链表
4.4.1 约瑟夫问题
- 问题:
- 设编号为1,2,… n的n个人围坐一圈,约定编号为k的人从1开始报数,数到m的人出列,他的下一位又从1开始报数,数到m的人又出列,以此类推,直到所有的人出列为止,由此产生一个出队编号的顺序
- 解决方案:
- 用一个不带头节点的单向循环链表来处理,构成一个有n个节点的单循环链表
- 单向循环链表的实现
package LinkedList;
/**
* 单向循环链表
* @author houbj
* @date 2020/12/9 10:21
*/
public class CircleLinked {
public static void main(String[] args) {
ChargeCircleNode node = new ChargeCircleNode(new CircleNode(1,"11", "111"));
node.show();
node.add(new CircleNode(2,"22", "111"));
node.show();
}
}
class ChargeCircleNode{
private CircleNode head;
public ChargeCircleNode(CircleNode node){
this.head = node;
head.next = node;
}
public CircleNode getHead(){
return head;
}
public void add(CircleNode node){
if (head == null) {
throw new RuntimeException("没有该循环链表");
}
CircleNode temp = head;
while (true) {
if (temp.next == head) {
break;
}
temp = temp.next;
}
temp.next = node;
node.next = head;
}
public void show(){
if (head == null) {
System.out.println("单向环形链表为空");
return;
}
CircleNode temp = head;
while (true) {
System.out.println(temp);
if (temp.next == head) {
System.out.println("node - next:"+ temp.next);
break;
}
temp = temp.next;
}
}
}
class CircleNode{
private int id;
private String name;
private String nickName;
CircleNode next;
public CircleNode(int id,String name,String nickName){
this.id = id;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "CircleNode{" +
"id=" + id +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
5、栈
5.1 栈的介绍
- 栈是一个先乳后厨的有序列表
- 栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为栈顶,另一端为固定的一端,称为栈底
5.2 栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完之后,再将地址取出,以回到原来的程序中
- 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数、区域变量等数据存入堆栈中
- 表达式的转换和求值
- 二叉树的遍历
- 图形的深度优先(depth-first)搜索法
5.3 用数组模拟栈的使用
- 实现思路
- 定义一个top来表示栈顶,初始化为 -1
- 入栈的操作:当有数据添加到栈中时,top++、stack[top] = data
- 出栈的操作:int value = stack[top]、top–
- 实现
package Stack;
import java.util.Arrays;
/**
* 用数组模拟栈
* @author houbj
* @date 2020/12/9 12:00
*/
public class ArrayStack {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
stack.pushStack("l");
stack.pushStack("i");
stack.pushStack("i");
stack.pushStack("i");
stack.pushStack("i");
stack.pushStack("i");
System.out.println(stack);
stack.popStack();
stack.popStack();
stack.popStack();
stack.popStack();
stack.popStack();
stack.popStack();
stack.popStack();
System.out.println(stack);
}
private int top;
private String[] str;
public ArrayStack(int num){
str = new String[num];
top = -1;
}
public void pushStack(String strInfo) {
if (top + 1 >= str.length) {
System.out.println("该栈已满---");
return;
}
top++;
str[top] = strInfo;
}
public void popStack(){
if (top ==-1) {
System.out.println("该栈已空---");
return;
}
System.out.println(str[top] + " 已出栈 ---");
str[top] = null;
top--;
}
@Override
public String toString() {
return "ArrayStack{" +
"top=" + top +
", str=" + Arrays.toString(str) +
'}';
}
}
5.4 实际需求
5.4.1 表达式求值1
输入一个表达式【7 * 2 * 2 - 5 + 1 - 5 + 3 + 3】,计算结果
- 思路分析
- 通过一个index值(索引),遍历表达式
- 如果发现是一个数字,就直接入数栈
- 如果发现扫面到的是一个符号,就分两种情况
如果发现当前的符号栈为空,符号直接入栈
如果符号栈中有操作符,则比较操作符的优先级,如果栈中的优先级大于或者等于栈中的操作符,就需要从数栈中pop出两个数,在符号栈中pop出一个符号,再进行运算,将得到的结果入数栈,然后将未入栈的操作符入栈
如果当前操作符的优先级大于栈中的操作符,就直接入符号栈
- 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行
- 最后在数栈中只有一个数字,就是表达式结果
5.4.2 表达式求值2
输入一个表达式【711 * 22 * 2 - 35 + 1 - 5 + 3 + 3】,计算结果
5.4.3 表达式求值3
输入一个表达式【(711 * 22 * 2 - 35) + 1 - 5 + 3 + 3】,计算结果
5.5 前缀、中缀、后缀表达式
5.5.1 前缀表达式
- 前缀表达式又称为波兰式,前缀表达式的运算符位于操作数之前
- 举例:
- (3+4)✖️5-6对应的前缀表达式就是 - ✖️ + 3 4 5 6
- 从右往左扫描
5.5.2 中缀表达式
- 中缀表达式就是常见的运算表达式
5.5.3 后缀表达式(逆波兰表达式)
- 与前缀表达式相似,只是运算符位于操作数之后
- 举例:
- (3+4)✖️5-6对应的后缀表达式就是 3 4 + 5 ✖️ 6 -
- 从左往右扫描
6、递归
6.1 简述
- 递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁
6.2 简单实例
package Stack;
/**
* @author houbj
* @date 2020/12/9 17:27
*/
public class Recursion {
public static void main(String[] args) {
System.out.println("test1----- :");
test1(4);
System.out.println("test2----- :");
test2(4);
System.out.println("test3----- :");
System.out.println(test3(3));
}
public static void test1(int n){
if (n>2) {
test1(n - 1);
}
System.out.println("n = " + n);
}
public static void test2(int n){
if (n>2) {
test2(n - 1);
} else
System.out.println("n = " + n);
}
public static int test3(int n) {
if (n == 1) {
return 1;
} else return test3(n-1) * n;
}
}
// 输出结果:
test1----- :
n = 2
n = 3
n = 4
test2----- :
n = 2
test3----- :
6
6.3 递归用于解决的问题
- 各种数学问题:8皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子问题等
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
- 将用栈解决的问题 -> 递归代码比较简洁
6.4 使用递归需要遵守的规则
- 执行一个方法时,就要创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响
- 如果方法中使用的是引用类型的变量,就会共享该引用类型的数据
- 递归必须向退出递归的条件逼近,否则就是无限递归了
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就讲结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
6.5 实例解决
6.1 迷宫问题
- 说明
- 小球得到的最短路径和程序员设置的找路的策略有关,即:找路的上下左右的顺序相关
- 在得到小球 的路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
- 测试回溯现象
- 思考:如何求出最短路径
-
- 代码示例
public class MiGong {
public static void main(String[] args) {
int [][] map = new int[8][7];
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][map[0].length -1]= 1;
}
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[map.length -1 ][i] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
show(map);
System.out.println( "----- ");
run(map, 1,1);
show(map);
}
/**
* 1 表示墙,0 表示没走过,2 表示路径 3 表示路不通
* 策略 下右上左
* @param map
* @param i
* @param j
* @return
*/
public static boolean run (int [][] map, int i, int j ) {
if (map[6][5] == 2) {
return true;
} else {
if (map[i][j] == 0) {
// 按照策略来走
map[i][j] = 2;
if (run(map, i+1, j)) { //下
return true;
} else if (run(map, i, j+1)){
return true;
} else if(run(map, i-1, j)) {
return true;
} else if(run(map, i, j -1)) {
return true;
} else {
map[i][j] = 3;
return false;
}
} else return false;
}
}
public static void show(int [][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length; j++) {
System.out.print(map[i][j] + " " );
}
System.out.println();
}
}
}
6.2 八皇后问题(回溯算法)
- 解决方案
- 用一个一维数组解决{0,4,7,5,2,6,1,3}
- 代码实例
package recursion;
/**
* @author houbj
* @date 2020/12/10 10:57
*/
public class EightQueen {
public static int max = 8;
public static int[] arr = new int[max];
public static void main(String[] args) {
EightQueen eightQueen = new EightQueen();
eightQueen.run(0);
}
public void run(int n){
if (n == max) {
show();
return;
}
for (int i = 0; i <max ; i++) {
arr[n] = i;
if (check(n)) {
run(n+1);
}
}
}
/**
* 判断是否符合规则
*/
public boolean check(int n){
for (int i = 0; i < n; i++) {
if (arr[i] == arr[n] || Math.abs(n - i) == Math.abs(arr[n]-arr[i])) {
return false;
}
}
return true;
}
/**
* 输出
*/
public static void show(){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+ " ");
}
System.out.println();
}
}
7、算法的时间复杂度
7.1 事后统计的方法和事前估算的方法
7.1.1 事后统计的方法
- 这种方法可行,但是存在两个问题:一是要想对设计的算法的运行性能进行评估,需要实际运行该程序。二是所得到的统计量依赖于计算机的硬件、软件等环境因素
- 这种方式,要在同一台计算机相同状态下运行,才能比较那个算法速度更快
7.1.2 事前估算的方法
- 通过分析某个算法的时间复杂度来判断哪个算法更优化
7.2 时间频度
- 一个算法花费的时间与算法中语句的执行次数成正比,哪个算法中语句执行的次数多,他花费的时间就越多。
- 一个算法中语句的执行次数称为语句频度或时间频度,记为T(n)
- 忽略常数项
- T(n) = 2n + 10 -> T(n) = 2 * n
- 忽略低次项
- T(n) = 2n^2 + 3n + 10 -> T(n) = 2n^2
7.3 时间复杂度
- 一般情况下,算法中的基本操作语句的重复次数是问题规模n的某个函数,用T(n)表。若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/F(n)的极限值为不等于0的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度
- T(n)不同,但时间复杂度可能相同。如:T(n)=n2+7n+6与T(n)=3n2+2n+2,他们的T(n)不同,但时间复杂度相同,都为O(n^2)
- 计算时间复杂度的方法:
- 用常数1代替运行时间中的所有加法常数
- 修改后的运行次数函数中,只保留最高阶项
- 去除最高阶项的系数
7.4 常见的时间复杂度
7.4.1 分类
- 常数阶O(1)
- 无论代码执行了多少行,只要是没有循环等复杂的结构,那么这个代码的时间复杂度都为 O(1)
int i = 1;
int j = 0;
i++;
j++;
int n = j + i ;
- 对数阶O(log2^n)
int i = 1;
while(i<n) {
i = i * 2; // log2^n
}
int i = 1;
while(i<n) {
i = i * 3; // log3^n
}
- 线性阶O(n)
for (int i = 0; i < n; i++) {
j = i;
i ++ ;
}
- 线性对数阶O(nlog2^n)
for (int i = 0; i < n; i++) {
j = i;
i ++ ;
while (m < n) {
i = i * 2;
}
}
- 平方阶O(n^2)
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
i++;
j++;
}
}
- 立方阶O(n^3)
- 三次循环
- k次方阶O(n^k)
- 指数阶O(2^n)
7.4.2说明
- 常见的算法时间复杂度由小到大依次为:O(1)<O(log2^n)<O(n)< O(nlog2n)<O(n2)<O(n3)<O(nk)<O(2^n)
7.5 平均时间复杂度、最坏时间复杂度
7.5.1 平均时间复杂度
- 指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间
7.5.2 最坏时间复杂度
- 最坏情况下的时间复杂度称为最坏时间复杂度。一般讨论的时间复杂度是最坏情况下的时间复杂度
- 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长
- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关
7.6 算法的空间复杂度
1.类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
2.空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
3.在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.
8、排序算法
8.1 分类
8.1.1 内部排序
- 将需要处理的所有数据都加载到内部的存储器中进行排序
- 插入排序(直接插入排序、希尔排序)、选择排序(简单选择排序、堆排序)、交换排序(冒泡排序、快速排序)、归并排序、基数排序
8.1.2 外部排序法
- 数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
8.2 冒泡排序
8.2.1 基本介绍
- 冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻的元素的值,若发现逆序则交换,使值较大的元素逐渐从前段移动到后部,就像水底下的气泡一样逐渐向上冒
- 因为在排序过程中,各个元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较
8.2.2 代码实例
package sort;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/10 16:35
*/
public class BubbleSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int [] arr){
int tag = 0;
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
tag = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tag;
}
}
}
}
}
8.2.3 代码优化(设置flag)
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/10 16:35
*/
public class BubbleSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int [] arr){
int tag = 0;
boolean flag = false;
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
flag = true;
tag = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tag;
}
}
if (!flag) {
break;
} else {
flag = false; // 重置 flag,进行下一次判断
}
System.out.println("进行一次循环: " + Arrays.toString(arr));
}
}
}
8.3 选择排序
8.3.1 基本介绍
8.3.2 代码示例
import java.util.Arrays;
/**
* 选择排序
* @author houbj
* @date 2020/12/10 17:22
*/
public class ChooseSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int [] arr){
int tag;
for (int i = 0; i < arr.length; i++) {
for (int j = i+1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
tag = arr[i];
arr[i] = arr[j];
arr[j] = tag;
}
}
}
}
}
8.4 插入排序
8.4.1 基本介绍
- 是对于要排序的元素以插入的方式寻找该元素的适当位置,以达到排序的目的
8.4.2 代码示例
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/10 21:44
*/
public class InsertSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int [] arr) {
for (int i = 1; i <= arr.length -1 ; i++) {
int indexValue = arr[i];
int index = i - 1;
while (index >= 0 && indexValue < arr[index]) {
arr[index+1] = arr[index];
index -- ;
}
arr[index + 1] = indexValue;
System.out.println(indexValue);
System.out.println(Arrays.toString(arr));
}
}
}
8.4.3 存在的问题
- 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响
8.5 希尔排序
8.5.1 基本介绍
- 希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为 缩小增量排序
8.5.2 基本思想
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
- 随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰好被分成一组,算法便终止
8.5.3 代码实例
- 方法一(交换法):
package sort;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/14 10:12
*/
public class ShellSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr){
int temp = 0;
for (int tag = arr.length / 2 ; tag > 0; tag /= 2) {
for (int i = tag; i < arr.length; i++) {
for (int j = i - tag; j >= 0 ; j -= tag) {
if (arr[j+tag] < arr[j]) {
temp = arr[j+tag];
arr[j+tag] = arr[j];
arr[j] = temp;
}
}
}
// System.out.println(Arrays.toString(arr));
// System.out.println("--");
}
}
}
- 方法二(移动法):
package sort;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/14 10:12
*/
public class ShellSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr);
System.out.println(Arrays.toString(arr));
arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void shellSort(int [] arr) {
int temp = 0;
for (int tag = arr.length / 2 ; tag > 0; tag /= 2) {
for (int i = tag; i < arr.length ; i++) {
int j = i;
temp = arr[j];
while (j - tag >= 0 && temp < arr[j-tag]) {
arr[j] = arr[j-tag];
j -= tag;
}
arr[j] = temp;
}
}
}
}
8.6 快速排序
8.6.1 基本介绍
- 快速排序是对于冒泡排序的一种改进
- 基本思想是:通过一次排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
8.6.2 代码实例
package sort;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/14 14:58
*/
public class QuickSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
sort(arr, 0 , arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void sort(int [] arr, int left, int right) {
int l = left;
int r = right;
int middle = arr[(left+right)/2];
System.out.println("middle:" + middle);
// = 0;
// while 循环的目的是 把比middle小的值放在左边,比middle大的值放在右边
while (l < r) {
while (arr[l] < middle) {
++ l;
}
while (arr[r] > middle) {
-- r;
}
// 如果 l >= r,代表左边全部是小于middle,右边大于middle
if (l >= r) {
break;
}
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
// 交换过后发现 array[l] == middle, l 前移
if (arr[l] == middle) {
r--;
}
if (arr[r] == middle) {
l++;
}
}
if (l == r) {
++l;
--r;
}
if (left < r) {
sort(arr, left, r);
}
if (right > l) {
sort(arr, l, right);
}
}
}
8.7 归并排序
8.7.1 基本介绍
- 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法
- 该算法采用经典的分治策略
- 可以看出这种结构很像一种完全二叉树,此处的归并排序我们采用递归去实现(也可采用迭代的方式去实现)
- 分的阶段可以理解为就是递归拆分子序列的过程
8.7.2 分治法
- 分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之
8.7.3 代码示例
public class MergeSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 46,12,33,89,32,27,29};
int [] temp = new int[arr.length];
sort(arr, 0, arr.length-1, temp);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr,int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
// 向左递归进行分解
sort(arr, left, mid, temp);
// 向右递归进行分解
sort(arr, mid+1, right, temp);
merge(arr,left,mid,right,temp);
}
}
/**
*
* @param arr 原始数组
* @param left 左边有序序列的初始索引
* @param middle 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int middle, int right,int[] temp) {
int l = left;
int r = middle + 1;
int t = 0; // 指向当前数组的索引
// step 1:
// 先把左右两边(有序)的数组按照规则填充到temp数组
// 直到左右两边的有序序列,有一边处理完为止
while (l <= middle && r <= right) {
// 如果左边的有序元素小于右边的有序元素 则将左边元素填充至temp
if (arr[l] <= arr[r]) {
temp[t] = arr[l];
t++;
l++;
} else {
temp[t] = arr[r];
t++;
r++;
}
}
// step 2:
// 把有剩余数据的一边数据依次全部填充到temp
while (l <= middle) {
temp[t] = arr[l];
l ++;
t ++;
}
while (r <= right) {
temp[t] = arr[r];
r ++;
t ++;
}
// step 3:
// 将temp数组的元素拷贝到arr
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
tempLeft++;
t++;
}
}
}
8.8 基数排序(桶排序)
8.8.1 基本介绍
- 基数排序(radix sort)属于“分配式排序”,又称“桶子法(bucket sort)”或 bin sort 。顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法属于稳定性的排序,基数排序法是效率高的稳定性排序法
- 基数排序 是 桶排序 的扩展
- 基数排序是1887年赫尔曼·何乐礼发明的。他的实现是这样的:将整数按位数切割成不同的数字,然后按每个位数分别比较
8.8.2 基数排序基本思想
- 将所有待比较数值统一为同样的数位长度, 数位较短的数前面补0。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成之后,数列就变成一个有序序列
- 以***空间换时间***的金典算法
8.8.3 代码实例
package sort;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/15 11:38
*/
public class RadixSort {
public static void main(String[] args) {
int [] arr = new int[]{11,23 ,2, 3647,46,12,33,89,32,27,29,138,0};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr){
// 1. 得到数组中最大数的位数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
int maxLength = (max+"").length(); // 该数组中最大数的长度
int bucket[][] = new int[10][arr.length]; // 定义一个二维数组表示一个桶
int[] bucketElementCount = new int[arr.length];
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
// 针对每个数的各个位,放置在不同的桶中
for (int j = 0; j < arr.length; j++) {
int digitElement = arr[j]/n%10; // 取出每个元素各个位上的值,放在桶中
bucket[digitElement][bucketElementCount[digitElement]] = arr[j];
bucketElementCount[digitElement] ++;
}
int index = 0 ; //按照桶中元素的顺序,将桶中的数据依次取出,放如原来的数组中
for (int j = 0; j < bucketElementCount.length; j++) {
if (bucketElementCount[j] != 0) {
for (int k = 0; k < bucketElementCount[j]; k++) {
arr[index++] = bucket[j][k];
}
}
bucketElementCount[j] = 0;
}
System.out.println("第" + (i+1)+"轮:"+ Arrays.toString(arr));
}
}
}
常用排序算法比对
- 稳定
- 不稳定
- 内排序:所有排序操作都在内存中完成
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能运行
- 时间复杂度:一个算法执行所消耗的时间
- 空间复杂度:运行完一个程序所需要的内存大小
- n:数据规模
- k:桶的个数
- in-space:不占用额外内存
- out-space:占用额外内存
9、查找算法
9.1 线性查找
9.1.1 代码实例
package Search;
/**
* @author houbj
* @date 2020/12/15 15:06
*/
public class LineSearch {
public static void main(String[] args) {
int [] arr = new int[]{0, 2, 11, 12, 23, 27, 29, 32, 33, 46, 89, 138, 3647};
int n = search(arr, 23);
System.out.println("在第"+ n+"个位置找到该数据");
}
/**
* 找到一个就返回
* @param arr
* @param num
* @return
*/
public static int search(int[] arr, int num) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == num) {
return i + 1;
}
}
return -1;
}
}
9.2 二分查找\折半查找
9.2.1 条件
- 要检索的数组是顺序的
9.2.2 代码实例
package Search;
import java.util.ArrayList;
/**
* 二分查找
* @author houbj
* @date 2020/12/15 15:20
*/
public class BinarySearch {
public static void main(String[] args) {
int [] arr = new int[]{0, 2, 11, 12, 23, 27, 29, 32, 33,33,33,33,33, 46, 89, 138, 3647};
int n = search(arr, 0, arr.length-1, 29);
System.out.println("在第"+ n+"个位置找到该数据");
ArrayList list = NotOneSearch(arr, 0, arr.length-1, 33);
System.out.println(list.toString());
}
/**
* 数组中只有一个目标数
* @param arr
* @param left
* @param right
* @param findVal
* @return
*/
private static int search(int[] arr, int left, int right, int findVal) {
if (left > right) {
return -1;
}
int mid = (left + right)/2;
if (findVal < arr[mid]) {
return search(arr,left, mid-1, findVal);
} else if (findVal > arr[mid]) {
return search(arr, mid+1, right, findVal) ;
} else {
return mid;
}
}
/**
* 数组中只有一个目标数
* @param arr
* @param left
* @param right
* @param findVal
* @return
*/
private static ArrayList NotOneSearch(int[] arr, int left, int right, int findVal) {
ArrayList list = new ArrayList();
if (left > right) {
return list;
}
int mid = (left + right)/2;
if (findVal < arr[mid]) {
return NotOneSearch(arr,left, mid-1, findVal);
} else if (findVal > arr[mid]) {
return NotOneSearch(arr, mid+1, right, findVal) ;
} else {
list.add(mid);
int temp = mid -1;
while (true) { // 左边查找
if (temp < 0 || arr[temp] != findVal) {
break;
}
list.add(temp);
temp --;
}
temp = mid+1;
while (true) { // 左边查找
if (temp > arr.length-1 || arr[temp] != findVal) {
break;
}
list.add(temp);
temp ++;
}
}
return list;
}
}
9.3 插值查找
9.3.1 原理介绍
- 插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找
- mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])
9.3.2 代码实例
package Search;
import java.util.ArrayList;
/**
* @author houbj
* @date 2020/12/15 16:12
*/
public class InsertValueSearch {
public static void main(String[] args) {
int [] arr = new int[]{0, 2, 11, 12, 23, 27, 29, 32, 33,33,33,33,33, 46, 89, 138, 3647};
ArrayList list = NotOneSearch(arr, 0, arr.length-1, 3);
System.out.println(list.toString());
}
/**
* 数组中只有一个目标数
* @param arr
* @param left
* @param right
* @param findVal
* @return
*/
private static ArrayList NotOneSearch(int[] arr, int left, int right, int findVal) {
ArrayList list = new ArrayList();
// arr[0] > findVal || arr[arr.length-1] < findVal 必须需要的
// 否则我们得到的mid可能越界
if (left > right || arr[0] > findVal || arr[arr.length-1] < findVal) {
return list;
}
int mid = left +(right-left)* (findVal-arr[left]) / (arr[right]-arr[left]);
if (findVal < arr[mid]) {
return NotOneSearch(arr,left, mid-1, findVal);
} else if (findVal > arr[mid]) {
return NotOneSearch(arr, mid+1, right, findVal) ;
} else {
list.add(mid);
int temp = mid -1;
while (true) { // 左边查找
if (temp < 0 || arr[temp] != findVal) {
break;
}
list.add(temp);
temp --;
}
temp = mid+1;
while (true) { // 左边查找
if (temp > arr.length-1 || arr[temp] != findVal) {
break;
}
list.add(temp);
temp ++;
}
}
return list;
}
}
9.3.3 注意
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
9.4 斐波那契查找算法(黄金分割)
9.4.1 斐波那契(黄金分割法)查找基本介绍
- 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比
- 斐波那契数列{1,1,2,3,5,8,13,21,34,55},发现斐波那契数列的两个相邻数的比例,无限接近 黄金分割值0.618
9.4.2 原理分析
- 斐波那契(黄金分割法)原理与前两种相似,仅仅改变了中间节点(mid)位置,mid不再是中间或者插值得到,而是位于黄金分割点附近,即:mid=low+F[k-1] -1
- 对于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即可,由以下代码得到,顺序表长度增加后,需要在新增的位置中赋值为最后一个元素的值
while(n>fib(k)-1)
k++;
9.4.3 代码实例
package Search;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/16 10:04
*/
public class FibSearch {
public static int maxSize = 10;
public static void main(String[] args) {
int [] arr = new int[]{0, 2, 11, 12, 23, 27 , 138, 3647};
int n = search(arr, 222);
System.out.println(n);
}
private static int search(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
int k = 0; // 表示斐波那契分割数值下标
int mid = 0;
int f[] = fib(); // 获取斐波那契数列
while (high > f[k]-1) {
k++;
}
// 因为F[k]可能大于数组长度,所以要补齐
int[] temp = Arrays.copyOf(arr, f[k]);
for (int i = high+1; i < temp.length; i++) {
temp[i] = arr[high];
}
while (low <= high) {
mid=low+f[k-1]-1;
if (key < temp[mid]) { // 应该向左边查找
high = mid -1;
k--;
} else if (key > temp[mid]) { // 应该向数组的后面查找
low = mid +1;
k -= 2;
} else {
if (mid <= high) {
return mid;
} else {
return high;
}
}
}
return -1;
}
public static int[] fib(){
int[] fib = new int[maxSize];
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i < fib.length; i++) {
fib[i] = fib[i-1] + fib[i-2];
}
return fib;
}
}
10、哈希表(数据结构)(散列)
10.1 哈希表的基本介绍
- 散列表(Hash Table,也叫哈希表),是根据关键码值(key value)而进行访问的数据结构。也就是说,他通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
10.2 应用实例(Google公司)
10.2.1 题目
- 有一个公司,当有新员工来报道时,要求该员工的信息加入(id,性别,年龄,住址…),当输入该员工的id时,要求查到该员工的所有信息
- 要求
- 不使用数据库,速度越快越好 -----> 哈希表(散列)
- 添加时,保证按照id从低到高插入【其他问题:如果id不是从低到高插入,但要求个条链表仍然时从低到高,怎么解决?】
10.2.2 解题思路
- 使用链表来实现哈希表,该链表不带头节点(即:链表第一个结点就存放雇员信息)
10.2.3 代码实例
package HashTable;
/**
* @author houbj
* @date 2020/12/16 14:16
*/
public class HashTable {
public static void main(String[] args) {
HashTab hashTab = new HashTab(7);
hashTab.add(new Employee(1,"li", 12,"www"));
hashTab.add(new Employee(2,"lLi", 12,"www"));
hashTab.add(new Employee(3,"lLLi", 12,"www"));
hashTab.add(new Employee(5,"lLLLi", 12,"www"));
hashTab.add(new Employee(0,"lLLLLi", 12,"www"));
hashTab.show();
}
}
class HashTab{
private EmployeeLinkedList[] employeeLinkedLists;
private int size;
public HashTab(int size) {
this.size = size;
this.employeeLinkedLists = new EmployeeLinkedList[size];
for (int i = 0; i < size; i++) {
this.employeeLinkedLists[i] = new EmployeeLinkedList();
}
}
public void add(Employee emp){
int num = hasFunction(emp.getId());
employeeLinkedLists[num].add(emp);
}
public void show(){
for (int i = 0; i < size; i++) {
employeeLinkedLists[i].list(i+1);
}
}
/**
* 编写散列函数
* @param id
* @return
*/
public int hasFunction(int id){
return id%size;
}
}
class EmployeeLinkedList{
private Employee head;
/**
* 添加雇员
* @param employee
*/
public void add(Employee employee){
if (this.head == null) {
this.head = employee;
return;
}
Employee emp = head;
while (true) {
if (emp.next == null) {
break;
}
emp = emp.next;
}
emp.next = employee;
}
/**
* 显示
* @param num
*/
public void list(int num){
if (this.head == null) {
System.out.println("第 " +num+ " 条链为空。。。");
return;
}
System.out.print("第 " +num+ " 条链,信息为:");
Employee emp = head;
while (true) {
System.out.printf("= > id = %d , name = %s ",head.getId(), head.getName());
if (emp.next == null ){
break;
}
emp = emp.next;
}
System.out.println();
}
public Employee getHead() {
return head;
}
public void setHead(Employee head) {
this.head = head;
}
}
class Employee{
private int id;
private String name;
private int age;
private String number;
public Employee next;
public Employee(int id, String name, int age, String number) {
this.id = id;
this.name = name;
this.age = age;
this.number = number;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}
11、二叉树
11.1 为什么需要树这种数据结构
11.1.1 数组存储方式分析
- 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
- 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
11.1.2 链式存储方式分析
- 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
- 缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
11.1.3 树存储方式分析
- 能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
11.2 二叉树的示意图
11.3 二叉树的概念
-
树有很多种,每个节点上最多只能有两个子节点的树称为二叉树
-
二叉树的子节点分为 左节点 和 右节点
-
满二叉树 :如果该二叉树的所有叶子节点都在最后一层,并且节点总数 = 2 ^ n - ,n为层数,我们称为满二叉树
-
完全二叉树 :如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,并且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
11.4 二叉树的遍历(前序、中序、后序)
11.4.1 概述
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
- 总结:看输出父节点的顺序,就确定是前序、中序还是后序
11.5 二叉树的查找(前序、中序、后序)
11.6 二叉树的删除(前序、中序、后序)
11.6.1 问题描述
- 如果删除的节点是叶子节点,则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树
- 测试,删除掉5号叶子节点和三号子树
11.6.2 实现思路
- 因为二叉树是单向的,所以我们是判断当前节点是否需要删除节点,而不能去判断当前这个节点是不是需要删除节点
- 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将 this.left = null
- 如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将 this.right = null
- 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
- 如果第4步没有删除节点,则应该向右子树进行递归删除
11.7 二叉树的遍历、查询、删除代码实例
package Tree;
/**
* @author houbj
* @date 2020/12/16 16:20
*/
public class BinaryTree {
public static void main(String[] args) {
BTree bTree = new BTree();
Node node1 = new Node(1,"songj");
Node node2 = new Node(2,"wuy");
Node node3 = new Node(3,"ljy");
Node node4 = new Node(4,"lc");
Node node5 = new Node(5,"guansheng");
node1.setLeft(node2);
node1.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
bTree.setNode(node1);
bTree.preShow();
bTree.midShow();
bTree.postShow();
bTree.preSearch(5);
bTree.midSearch(3);
bTree.postSearch(4);
bTree.delete(3);
bTree.preShow();
bTree.delete(5);
bTree.preShow();
}
}
/**
* 定义一个二叉树
*
*/
class BTree{
private Node node;
/**
* 删除
* @param id
*/
public void delete(int id) {
if (this.node.getId() == id) {
this.node = null;
return;
} else {
this.node.delete(id);
}
}
public void preSearch(int id){
Node nodeSearch = null;
nodeSearch = node.preOrderSearch(id);
if (nodeSearch != null) {
System.out.println(nodeSearch);
} else {
System.out.println("前序:未找到该数据");
}
}
public void midSearch(int id){
Node nodeSearch = null;
nodeSearch = node.midOrderSearch(id);
if (nodeSearch != null) {
System.out.println(nodeSearch);
} else {
System.out.println("后序:未找到该数据");
}
}
public void postSearch(int id){
Node nodeSearch = null;
nodeSearch = node.postOrderSearch(id);
if (nodeSearch != null) {
System.out.println(nodeSearch);
} else {
System.out.println("后序:未找到该数据");
}
}
public void preShow(){
System.out.println("前序遍历----- ");
if (this.node != null) {
this.node.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
public void midShow(){
System.out.println("中序遍历----- ");
if (this.node != null) {
this.node.midOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
public void postShow(){
System.out.println("后序遍历----- ");
if (this.node != null) {
this.node.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
public Node getNode() {
return node;
}
public void setNode(Node node) {
this.node = node;
}
}
class Node{
private int id;
private String name;
private Node left;
private Node right;
public Node(int id, String name) {
this.id = id;
this.name = name;
}
public void delete(int id){
if (this.left != null && this.left.id == id) {
this.left = null;
return;
}
if (this.right != null && this.right.id == id) {
this.right = null;
return;
}
if (this.left != null) {
this.left.delete(id);
}
if (this.right != null) {
this.right.delete(id);
}
}
/**
* 前序查找
* @param id
* @return
*/
public Node preOrderSearch(int id){
if (this.id == id) {
return this;
}
Node node = null;
if (this.left != null) {
node = this.left.preOrderSearch(id);
}
if (null != node) {
return node;
}
if (this.right != null) {
node = this.right.preOrderSearch(id);
}
return node;
}
/**
* 中序查找
* @param id
* @return
*/
public Node midOrderSearch(int id){
Node node = null;
if (this.left != null) {
node = this.left.midOrderSearch(id);
}
if (null != node) {
return node;
}
if (this.id == id) {
return this;
}
if (this.right != null) {
node = this.right.midOrderSearch(id);
}
return node;
}
/**
* 后序查找
* @param id
* @return
*/
public Node postOrderSearch(int id){
Node node = null;
if (this.left != null) {
node = this.left.postOrderSearch(id);
}
if (null != node) {
return node;
}
if (this.right != null) {
node = this.right.postOrderSearch(id);
}
if (null != node) {
return node;
}
if (this.id == id) {
node = this;
}
return node;
}
/**
* 前序遍历
*/
public void preOrder(){
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
/**
* 中序遍历
*/
public void midOrder(){
if (this.left != null) {
this.left.midOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.midOrder();
}
}
/**
* 后序遍历
*/
public void postOrder(){
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
}
11.8 顺序存储二叉树
11.8.1 特点
- 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成为数组
- 顺序存储二叉树通常只考虑完全二叉树
- 第n个元素的左子节点为 2*n + 1
- 第n个元素的右子节点为 2*n+2
- 第n个元素的父节点为(n-1)/2
11.8.2 代码实例
- 需求:给你一个数组{1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。前序遍历的结果应当为1,2,4,5,3,6,7
- 代码
package Tree;
/**
* 顺序存储二叉树
* @author houbj
* @date 2020/12/17 11:07
*/
public class OrderBTree {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5,6,7};
ArrayTree arrayTree = new ArrayTree(arr);
arrayTree.preShowArray(0);
}
}
class ArrayTree{
private int[] array;
public ArrayTree(int[] array) {
this.array = array;
}
/**
* 前序遍历
* @param index
*/
public void preShowArray(int index){
if (array == null || array.length == 0) {
System.out.println("数组为空----");
return;
}
System.out.println(array[index]);
if ((index*2+1) < array.length) {
preShowArray(index*2+1);
}
if ((index*2+2) < array.length) {
preShowArray(index*2+2);
}
}
}
11.8.3 应用实例
- 八大排序算法中的堆排序,就会使用到顺序存储二叉树
11.9 线索化二叉树
11.9.1 基本介绍
- n个节点的二叉链表中含有n+1公式 2n-(n-1) = n+1个空指针域。利用二叉链表中的空指针域,存放指向节点在 某种遍历次序 下的前驱和后继节点的指针(这种附加的指针称为“线索”)
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可以分为前序线索二叉树、中序线索二叉树和后序线索二叉树
- 一个节点的前一个节点,称为前驱节点
- 一个节点的后一个节点,称为后继节点
11.9.2 应用案例
- 问题描述
- 将下面的二叉树,进行中序线索二叉树。中序遍历的数列为{8,3,10,1,14,6}
- 问题分析
- left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
- right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点.
- 代码示例
package Tree;
/**
* 中序遍历线索二叉树
* @author houbj
* @date 2020/12/18 10:23
*/
public class MidThreadedBTree {
public static void main(String[] args) {
MidNode root = new MidNode(1, "l");
MidNode node3 = new MidNode(3, "l");
MidNode node8 = new MidNode(8, "l");
MidNode node10 = new MidNode(10, "l");
MidNode node6 = new MidNode(6, "l");
MidNode node14 = new MidNode(14, "l");
root.setLeft(node3);
node3.setRight(node10);
node3.setLeft(node8);
root.setRight(node6);
node6.setLeft(node14);
MidBTree midBTree = new MidBTree();
midBTree.setNode(root);
midBTree.threadedNodes(root);
MidNode midNode = node10.getLeft();
System.out.println("node10 - left : " + midNode);
System.out.println("node10 - right : " + node10.getRight());
midBTree.show();
}
}
class MidBTree{
private MidNode node;
// 为了实现线索化,需要创建要给指向当前节点的前驱节点的指针
// 在进行线索化时,pre 总是保留前一个结点
private MidNode pre = null;
/**
* 编写对二叉树进行中序线索化的方法
* @param node
*/
public void threadedNodes(MidNode node){
if (node == null) return;
// 1. 先线索化左子树
threadedNodes(node.getLeft());
// 2. 再线索化当前结点
if ( node.getLeft() == null) {
node.setLeft(pre);
node.setLeftTag(1);
}
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightTag(1);
}
pre = node;
// 3. 最后线索化右子树
threadedNodes(node.getRight());
}
/**
* 遍历
*/
public void show(){
MidNode temp = node;
while (temp != null){
while (temp.getLeftTag() == 0) {
temp = temp.getLeft();
}
System.out.println(temp);
while (temp.getRightTag() == 1) {
temp = temp.getRight();
System.out.println(temp);
}
temp = temp.getRight();
}
}
public MidNode getNode() {
return node;
}
public void setNode(MidNode node) {
this.node = node;
}
}
class MidNode{
private int id;
private String name;
private MidNode left;
private MidNode right;
private int leftTag;
private int rightTag;
public MidNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "MidNode{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public int getLeftTag() {
return leftTag;
}
public void setLeftTag(int leftTag) {
this.leftTag = leftTag;
}
public int getRightTag() {
return rightTag;
}
public void setRightTag(int rightTag) {
this.rightTag = rightTag;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MidNode getLeft() {
return left;
}
public void setLeft(MidNode left) {
this.left = left;
}
public MidNode getRight() {
return right;
}
public void setRight(MidNode right) {
this.right = right;
}
}
12、树结构的实际应用
12.1 堆排序
12.1.1 堆排序的基本介绍
- 堆排序 是利用堆这种数据结构而设计的一种排序算法
- 堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
12.1.2 大顶堆
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为 大顶堆 ,注意:没有要求结点的左孩子的值和右孩子的值的大小关系
- 大顶堆举例说明
- 大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号
12.1.3 小顶堆
- 每个节点的值都小于或等于其左右孩子结点的值,称为 小顶堆
- 小顶堆的特点:
- arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
- 一般升序采用大顶堆,降序采用小顶堆
12.1.4 堆排序的基本思想
- 将待排序序列构造成一个大顶堆
- 此时,整个序列的最大值就是堆顶的根结点
- 将其与末尾元素进行交换,此时末尾就成为最大值
- 然后将剩余n-1个元素重新构造成一个堆,这样就会得到n个元素的次小值,如此反复指向,便能得到一个有序序列
- 注意:可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
12.1.5 堆排序实例
- 要求:给一个数组{4,6,8,5,9},要求使用堆排序法,将数组升序排序
12.2 赫夫曼树
12.2.1 基本介绍
- 给定n个权值作为n个叶子结点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为 最优二叉树
- 赫夫曼树 是带权路径长度最短的树,权值较大的结点离根很近
12.2.2 其他概念
- 路径和路径长度:在一颗树中,从一个节点往下可以达到的孩子或者孙子结点之间的通路,称为***路径***。通路中分支的数目称为 路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为 L-1
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该 结点的权。结点的带权路径长度为:从根结点到该节点之间的路径长度与该节点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有的 叶子结点 的带权路径长度之和,记为 WPL ,权值越大的结点离根结点越近的二叉树才是最优二叉树
- WPL最小二叉树的就是赫夫曼树
12.2.3 赫夫曼树创建思路
- 题目:
- 给一个数组 {13,7,8,3,29,6,1},要求转成一颗赫夫曼树
- 构成赫夫曼树的步骤:
- 从小到达进行排序,每一个数据都是一个结点,每个节点可以看成是一颗最简单的二叉树
- 取出根结点权值最小的两颗二叉树
- 组成一颗最新的二叉树,该新的二叉树的根结点的权值是前面两颗二叉树根结点权值的和
- 再将这颗新的二叉树,以根结点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
- 代码实例:
package Tree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author houbj
* @date 2020/12/18 15:27
*/
public class HuffmanTree {
public static void main(String[] args) {
preOrder(CreateHuffmanTree(new int[]{13,7,8,3,29,6,1}));
}
public static HuffmanNode CreateHuffmanTree(int[] arr){
List<HuffmanNode> nodes = new ArrayList<HuffmanNode>();
for (int value: arr) {
nodes.add(new HuffmanNode(value));
}
System.out.println("node : " + nodes);
HuffmanNode node = null;
while (nodes.size() > 1) {
Collections.sort(nodes);
System.out.println("nodeCompare : " + nodes);
HuffmanNode left = nodes.get(0);
HuffmanNode right = nodes.get(1);
node = new HuffmanNode(left.getValue() + right.getValue());
node.setLeft(left);
node.setRight(right);
nodes.remove(left);
nodes.remove(right);
nodes.add(node);
System.out.println("nodes : " + nodes);
}
return nodes.get(0);
}
public static void preOrder(HuffmanNode node){
if (node!= null ){
node.preOrder();
} else {
System.out.println("node 为空-----");
}
}
}
class HuffmanNode implements Comparable<HuffmanNode>{
private int value;
private HuffmanNode left;
private HuffmanNode right;
public HuffmanNode(int value) {
this.value = value;
}
/**
* 前序遍历
*/
public void preOrder(){
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public HuffmanNode getLeft() {
return left;
}
public void setLeft(HuffmanNode left) {
this.left = left;
}
public HuffmanNode getRight() {
return right;
}
public void setRight(HuffmanNode right) {
this.right = right;
}
@Override
public String toString() {
return "HuffmanNode{" +
"value=" + value +
'}';
}
@Override
public int compareTo(HuffmanNode o) {
return this.value - o.value;
}
}
12.3 赫夫曼编码
12.3.1 基本介绍
- 赫夫曼编码是一种编码方式,属于一种程序算法
- 赫夫曼编码是赫夫曼在电信通信中的经典应用之一
- 赫夫曼编码广泛的用于数据文件压缩,其压缩效率通常在20%-90%之间
- 赫夫曼码是***可变字长***编码(VLC)的一种,Huffman于1952年提出的一种编码方式,称为最佳编码
12.3.2 注意
- 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的
12.3.3 示例
- 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 // 各个字符对应的个数
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)
- 根据赫夫曼树,给各个字符规定编码 , 向左的路径为0,向右的路径为1 , 编码如下:
- o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 " " : 01
- 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
- 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
- 长度为 : 133
- 说明:
- 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
- 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
12.4 二叉排序树(BST)
12.4.1 基本介绍
- 对于一个二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值大于当前节点
- 如果有相同的值,可以将其放在左子节点或右子节点
12.4.2 二叉排序树的添加和中序遍历
package Tree;
/**
* @author houbj
* @date 2020/12/21 14:37
*/
public class BinarySortTree {
public static void main(String[] args) {
int[] value = {7,3,10,12,5,1,9};
BinaryTreeCreate btCreate = new BinaryTreeCreate();
for (int i = 0; i < value.length; i++) {
btCreate.addNode(new BinarySortNode(value[i]));
}
btCreate.midShow();
}
}
class BinaryTreeCreate{
BinarySortNode root ;
public void addNode(BinarySortNode node){
if (root == null) {
root = node;
} else {
root.add(node);
}
}
public void midShow(){
if (root == null ){
System.out.println("根结点为空---");
return;
}else {
root.midShow();
}
}
}
class BinarySortNode{
private int value;
private BinarySortNode left;
private BinarySortNode right;
/**
* 添加
* @param node
*/
public void add(BinarySortNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
/**
* 中序遍历
*/
public void midShow(){
if (this.left != null) {
this.left.midShow();
}
System.out.println(this);
if (this.right != null) {
this.right.midShow();
}
}
public BinarySortNode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public BinarySortNode getLeft() {
return left;
}
public void setLeft(BinarySortNode left) {
this.left = left;
}
public BinarySortNode getRight() {
return right;
}
public void setRight(BinarySortNode right) {
this.right = right;
}
@Override
public String toString() {
return "BinarySortNode{" +
"value=" + value +
'}';
}
}
12.4.3 二叉排序树的删除
- 二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
- 删除叶子节点(2,5,9,12)
- 删除只有一颗子树的节点(1)
- 删除有两颗子树的节点(7,3,10)
- 删除叶子节点
if (node.getLeft() == null && node.getRight() == null) { // 删除叶子节点
System.out.println("该节点为叶子节点---,删除叶子节点-------:"+ node.getValue());
if (parentNode.getLeft() != null && parentNode.getLeft().getValue() == value) {
parentNode.setLeft(null);
} else if(parentNode.getRight() != null && parentNode.getRight().getValue() == value) {
parentNode.setRight(null);
} else {
System.out.println("找错了--- ");
}
}
- 删除有一个子节点的节点
if (node.getLeft() != null) {
if (parentNode != null ){
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getLeft());
} else if (parentNode.getRight().getValue() ==value){
parentNode.setRight(node.getLeft());
}
} else {
this.root = node.getLeft();
}
} else if (node.getRight() != null){
if (parentNode != null ) {
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getRight());
} else if (parentNode.getRight().getValue() == value){
parentNode.setRight(node.getRight());
}
} else {
this.root = node.getRight();
}
}
- 删除有两个子节点的节点
else if(node.getLeft() !=null && node.getRight() != null){ // 删除有两个子节点的节点
System.out.println("该节点为叶子节点---,删除有2个子节点的节点-------:"+ node.getValue());
int minValue = findMinNode(node.getRight());
node.setValue(minValue);
}
public BinarySortNode findNodeParent(int value){
if (root == null) {
return null;
} else {
return this.root.searchParentNode(value);
}
}
12.4.4 删除代码实例
package Tree;
/**
* @author houbj
* @date 2020/12/21 14:37
*/
public class BinarySortTree {
public static void main(String[] args) {
int[] value = {7,3,10,12,5,1,9,2};
BinaryTreeCreate btCreate = new BinaryTreeCreate();
for (int i = 0; i < value.length; i++) {
btCreate.addNode(new BinarySortNode(value[i]));
}
btCreate.midShow();
btCreate.delete(3);
btCreate.midShow();
}
}
class BinaryTreeCreate{
BinarySortNode root ;
public void addNode(BinarySortNode node){
if (root == null) {
root = node;
} else {
root.add(node);
}
}
public void midShow(){
if (root == null ){
System.out.println("根结点为空---");
return;
}else {
root.midShow();
}
}
/**
* 找到右节点上的最小节点
*/
public int findMinNode(BinarySortNode node){
BinarySortNode searchNode = node;
while (searchNode.getLeft() != null) {
searchNode = searchNode.getLeft();
}
delete(searchNode.getValue());
return searchNode.getValue();
}
/**
* 查找要删除的节点
* @param value
* @return
*/
public BinarySortNode findNode(int value){
if (root == null) {
return null;
} else {
return this.root.searchNode(value);
}
}
/**
* 查找要删除节点的父节点
* @param value
* @return
*/
public BinarySortNode findNodeParent(int value){
if (root == null) {
return null;
} else {
return this.root.searchParentNode(value);
}
}
public void delete(int value) {
if (root == null) {
System.out.println("根结点为空");
return;
}else {
BinarySortNode node = findNode(value);
System.out.println("node : " + node);
if (node == null ) {
System.out.println("该节点不存在--- ");
return;
} else {
if (this.root.getLeft() == null && this.root.getRight() == null){
System.out.println("该树只有一个节点");
this.root=null;
return;
}else {
BinarySortNode parentNode = findNodeParent(value);
if (node.getLeft() == null && node.getRight() == null) { // 删除叶子节点
System.out.println("该节点为叶子节点---,删除叶子节点-------:"+ node.getValue());
if (parentNode.getLeft() != null && parentNode.getLeft().getValue() == value) {
parentNode.setLeft(null);
} else if(parentNode.getRight() != null && parentNode.getRight().getValue() == value) {
parentNode.setRight(null);
} else {
System.out.println("找错了--- ");
}
}
else if(node.getLeft() !=null && node.getRight() != null){ // 删除有两个子节点的节点
System.out.println("该节点为叶子节点---,删除有2个子节点的节点-------:"+ node.getValue());
int minValue = findMinNode(node.getRight());
node.setValue(minValue);
}
else { //删除有一个子节点的节点
System.out.println("该节点为叶子节点---,删除有一个子节点的节点-------:"+ node.getValue());
if (node.getLeft() != null) {
if (parentNode != null ){
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getLeft());
} else if (parentNode.getRight().getValue() ==value){
parentNode.setRight(node.getLeft());
}
} else {
this.root = node.getLeft();
}
} else if (node.getRight() != null){
if (parentNode != null ) {
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getRight());
} else if (parentNode.getRight().getValue() == value){
parentNode.setRight(node.getRight());
}
} else {
this.root = node.getRight();
}
}
}
}
}
}
}
}
class BinarySortNode{
private int value;
private BinarySortNode left;
private BinarySortNode right;
/**
* 查找要删除节点
* @param value
* @return
*/
public BinarySortNode searchNode(int value){
if (this.value == value) {
return this;
} else if (value < this.value) {
if (this.left == null) {
return null;
}
return this.left.searchNode(value);
} else {
if (this.right == null) {
return null;
}
return this.right.searchNode(value);
}
}
/**
* 查找要删除元素的父节点
* @param value
* @return
*/
public BinarySortNode searchParentNode(int value){
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
if (value < this.value && this.left != null) {
return this.left.searchParentNode(value);
} else if (value >= this.value && this.right != null) {
return this.right.searchParentNode(value);
} else {
return null;
}
}
}
/**
* 添加
* @param node
*/
public void add(BinarySortNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
/**
* 中序遍历
*/
public void midShow(){
if (this.left != null) {
this.left.midShow();
}
System.out.println(this);
if (this.right != null) {
this.right.midShow();
}
}
public BinarySortNode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public BinarySortNode getLeft() {
return left;
}
public void setLeft(BinarySortNode left) {
this.left = left;
}
public BinarySortNode getRight() {
return right;
}
public void setRight(BinarySortNode right) {
this.right = right;
}
@Override
public String toString() {
return "BinarySortNode{" +
"value=" + value +
'}';
}
}
12.5 平衡二叉树(AVL树)
12.5.1 BST存在以下问题
- 左子树全部为空,从形式上看,更像一个单链表.
- 插入速度没有影响
- 查询速度明显降低(因为需要依次比较), 不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
- 解决方案-平衡二叉树(AVL)
12.5.2 平衡二叉树基本介绍
- 平衡二叉树也叫平衡二叉搜索树,又被称为AVL树,可以保证查询效率较高
- 具有一下特点:它是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两颗子树都是一颗平衡二叉树。
- 平衡二叉树的常用实现方法有:红黑树、AVL、替罪羊树、Treap、伸展树等
12.5.3 单旋转(左旋转)
- 图示
- 代码实例
/**
* 添加 元素的时候进行左旋
* @param node
*/
public void add(AVLNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
// 如果左子树高度 小于 右子树高度,则 左旋
if(findRightTreeHeight() - findLeftTreeHeight() > 1) {
leftRotate();
}
}
/**
* 左旋
*/
private void leftRotate(){
System.out.println("左旋");
// 以当前根节点的值创建新的节点
AVLNode node = new AVLNode(value);
// 把新节点的左子树设置为新节点的左子树
node.left = left;
// 把新节点的右子树设置为设置节点的右子树的左子树
node.right = right.left;
// 把当前节点的值替换为 右子节点的值
value = right.value;
// 将此节点的 right 替换
right = right.right;
// 将 此节点 的 left 替换
left = node;
}
12.5.4 单旋转(右旋转)
- 图示
- 代码实例
/**
* 添加时进行右旋
* @param node
*/
public void add(AVLNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
// 如果左子树高度 小于 右子树高度,则 左旋
if(findRightTreeHeight() - findLeftTreeHeight() > 1) {
leftRotate();
}
if (findLeftTreeHeight() - findRightTreeHeight() > 1) {
rightRotate();
}
}
/**
* 右旋
*/
public void rightRotate(){
System.out.println("右旋-");
AVLNode node = new AVLNode(value);
node.left = left.right;
node.right = right;
value = left.value;
left = left.left;
right = node;
}
12.5.5 双旋转
- 单旋转存在的问题(在某些情况下,单旋转不能完成平衡二叉树的转换)
- {10, 11, 7, 6, 8, 9},运行上述代码并未转成AVL树
- {2, 1, 6, 5, 7, 3}, 运行上述代码并未转成AVL树
- 图示
- 问题分析
- 问题分析出来: 在满足右旋转条件时,要判断
(1)如果 是 左子树的 右子树高度 大于左子树的左子树时:
(2)就是 对 当前根节点的左子树,先进行 左旋转,
(3)然后, 在对当前根节点进行右旋转即可 - 否则,直接对当前节点(根节点)进行右旋转.即可
- 代码实例
/**
* 添加
* @param node
*/
public void add(AVLNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
// 如果左子树高度 小于 右子树高度,则 左旋
if(findRightTreeHeight() - findLeftTreeHeight() > 1) {
if (right!=null && right.findLeftTreeHeight() > right.findLeftTreeHeight()) {
right.rightRotate();
leftRotate();
} else {
leftRotate();
}
}
if (findLeftTreeHeight() - findRightTreeHeight() > 1) {
if (left != null && left.findRightTreeHeight() > left.findLeftTreeHeight()) {
left.leftRotate();
rightRotate();
} else {
rightRotate();
}
}
}
12.5.6 代码实例
package Tree;
/**
* @author houbj
* @date 2020/12/22 11:42
*/
public class AVLTree {
public static void main(String[] args) {
int[] arr = {4,3,5,6,7,8};
CreateAVLTree btCreate = new CreateAVLTree();
for (int i = 0; i < arr.length; i++) {
btCreate.addNode(new AVLNode(arr[i]));
}
btCreate.midShow();
System.out.println("Tree height : " + btCreate.root.findNodeHeight());
System.out.println("Tree left height : " + btCreate.root.findLeftTreeHeight());
System.out.println("Tree right height : " + btCreate.root.findRightTreeHeight());
arr = new int[]{10, 12, 8, 9, 7, 6};
btCreate = new CreateAVLTree();
for (int i = 0; i < arr.length; i++) {
btCreate.addNode(new AVLNode(arr[i]));
}
btCreate.midShow();
System.out.println("Tree height : " + btCreate.root.findNodeHeight());
System.out.println("Tree left height : " + btCreate.root.findLeftTreeHeight());
System.out.println("Tree right height : " + btCreate.root.findRightTreeHeight());
System.out.println("root :"+ btCreate.root);
arr = new int[]{10, 11, 7, 6, 8, 9};
btCreate = new CreateAVLTree();
for (int i = 0; i < arr.length; i++) {
btCreate.addNode(new AVLNode(arr[i]));
}
btCreate.midShow();
System.out.println("Tree height : " + btCreate.root.findNodeHeight());
System.out.println("Tree left height : " + btCreate.root.findLeftTreeHeight());
System.out.println("Tree right height : " + btCreate.root.findRightTreeHeight());
System.out.println("root :"+ btCreate.root);
arr = new int[]{2, 1, 6, 5, 7, 3};
btCreate = new CreateAVLTree();
for (int i = 0; i < arr.length; i++) {
btCreate.addNode(new AVLNode(arr[i]));
}
btCreate.midShow();
System.out.println("Tree height : " + btCreate.root.findNodeHeight());
System.out.println("Tree left height : " + btCreate.root.findLeftTreeHeight());
System.out.println("Tree right height : " + btCreate.root.findRightTreeHeight());
System.out.println("root :"+ btCreate.root);
}
}
class CreateAVLTree{
AVLNode root ;
public void addNode(AVLNode node){
if (root == null) {
root = node;
} else {
root.add(node);
}
}
public void midShow(){
if (root == null ){
System.out.println("根结点为空---");
return;
}else {
root.midShow();
}
}
/**
* 找到右节点上的最小节点
*/
public int findMinNode(AVLNode node){
AVLNode searchNode = node;
while (searchNode.getLeft() != null) {
searchNode = searchNode.getLeft();
}
delete(searchNode.getValue());
return searchNode.getValue();
}
/**
* 查找要删除的节点
* @param value
* @return
*/
public AVLNode findNode(int value){
if (root == null) {
return null;
} else {
return this.root.searchNode(value);
}
}
/**
* 查找要删除节点的父节点
* @param value
* @return
*/
public AVLNode findNodeParent(int value){
if (root == null) {
return null;
} else {
return this.root.searchParentNode(value);
}
}
public void delete(int value) {
if (root == null) {
System.out.println("根结点为空");
return;
}else {
AVLNode node = findNode(value);
System.out.println("node : " + node);
if (node == null ) {
System.out.println("该节点不存在--- ");
return;
} else {
if (this.root.getLeft() == null && this.root.getRight() == null){
System.out.println("该树只有一个节点");
this.root=null;
return;
}else {
AVLNode parentNode = findNodeParent(value);
if (node.getLeft() == null && node.getRight() == null) { // 删除叶子节点
System.out.println("该节点为叶子节点---,删除叶子节点-------:"+ node.getValue());
if (parentNode.getLeft() != null && parentNode.getLeft().getValue() == value) {
parentNode.setLeft(null);
} else if(parentNode.getRight() != null && parentNode.getRight().getValue() == value) {
parentNode.setRight(null);
} else {
System.out.println("找错了--- ");
}
}
else if(node.getLeft() !=null && node.getRight() != null){ // 删除有两个子节点的节点
System.out.println("该节点为叶子节点---,删除有2个子节点的节点-------:"+ node.getValue());
int minValue = findMinNode(node.getRight());
node.setValue(minValue);
}
else { //删除有一个子节点的节点
System.out.println("该节点为叶子节点---,删除有一个子节点的节点-------:"+ node.getValue());
if (node.getLeft() != null) {
if (parentNode != null ){
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getLeft());
} else if (parentNode.getRight().getValue() ==value){
parentNode.setRight(node.getLeft());
}
} else {
this.root = node.getLeft();
}
} else if (node.getRight() != null){
if (parentNode != null ) {
if (parentNode.getLeft().getValue() == value) {
parentNode.setLeft(node.getRight());
} else if (parentNode.getRight().getValue() == value){
parentNode.setRight(node.getRight());
}
} else {
this.root = node.getRight();
}
}
}
}
}
}
}
}
class AVLNode{
private int value;
private AVLNode left;
private AVLNode right;
/**
* 左旋
*/
private void leftRotate(){
System.out.println("左旋-");
// 以当前根节点的值创建新的节点
AVLNode node = new AVLNode(value);
// 把新节点的左子树设置为新节点的左子树
node.left = left;
// 把新节点的右子树设置为设置节点的右子树的左子树
node.right = right.left;
// 把当前节点的值替换为 右子节点的值
value = right.value;
// 将此节点的 right 替换
right = right.right;
// 将 此节点 的 left 替换
left = node;
}
/**
* 右旋
*/
public void rightRotate(){
System.out.println("右旋-");
AVLNode node = new AVLNode(value);
node.left = left.right;
node.right = right;
value = left.value;
left = left.left;
right = node;
}
/**
* 找到左子树的高度
* @return
*/
public int findLeftTreeHeight(){
if (left == null) {
return 0;
} else {
return left.findNodeHeight();
}
}
/**
* 找到右子树的高度
* @return
*/
public int findRightTreeHeight(){
if (right == null) {
return 0;
} else {
return right.findNodeHeight();
}
}
/**
* 查找树的高度
* @return
*/
public int findNodeHeight(){
return Math.max(left == null ? 0 : left.findNodeHeight(), right == null ? 0 : right.findNodeHeight()) + 1;
}
/**
* 查找要删除节点
* @param value
* @return
*/
public AVLNode searchNode(int value){
if (this.value == value) {
return this;
} else if (value < this.value) {
if (this.left == null) {
return null;
}
return this.left.searchNode(value);
} else {
if (this.right == null) {
return null;
}
return this.right.searchNode(value);
}
}
/**
* 查找要删除元素的父节点
* @param value
* @return
*/
public AVLNode searchParentNode(int value){
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
if (value < this.value && this.left != null) {
return this.left.searchParentNode(value);
} else if (value >= this.value && this.right != null) {
return this.right.searchParentNode(value);
} else {
return null;
}
}
}
/**
* 添加
* @param node
*/
public void add(AVLNode node){
if (node == null) {
return;
}
if (this.value > node.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
// 如果左子树高度 小于 右子树高度,则 左旋
if(findRightTreeHeight() - findLeftTreeHeight() > 1) {
if (right!=null && right.findLeftTreeHeight() > right.findLeftTreeHeight()) {
right.rightRotate();
leftRotate();
} else {
leftRotate();
}
}
if (findLeftTreeHeight() - findRightTreeHeight() > 1) {
if (left != null && left.findRightTreeHeight() > left.findLeftTreeHeight()) {
left.leftRotate();
rightRotate();
} else {
rightRotate();
}
}
}
/**
* 中序遍历
*/
public void midShow(){
if (this.left != null) {
this.left.midShow();
}
System.out.println(this);
if (this.right != null) {
this.right.midShow();
}
}
public AVLNode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public AVLNode getLeft() {
return left;
}
public void setLeft(AVLNode left) {
this.left = left;
}
public AVLNode getRight() {
return right;
}
public void setRight(AVLNode right) {
this.right = right;
}
@Override
public String toString() {
return "AVLNode{" +
"value=" + value +
'}';
}
}
13、多路查找树
13.1 二叉树与B树
13.1.1 二叉树的问题分析
- 二叉树需要加载到内存中,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多,就存在以下问题:
- 问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度会有影响
- 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度
13.1.2 多叉树
- 在二叉树中,每个节点都有数据项,最多有两个节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树
- 2-3树、2-3-4树就是多叉树,多叉树通过组织节点,减少树的高度,能对二叉树进行优化
13.1.3 B树(多叉树)
- B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率
- 如图B树通过重新组织节点, 降低了树的高度.
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
- 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于***文件存储系统以及数据库系统***中
- 节点的度:节点下面子节点的个数
- 树的度M:所有的节点中最大的度
13.2 2-3树
13.2.1 2-3树基本介绍
- 2-3树是最简单的B树,具有一下特点:
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 2-3树是由二节点和三节点构成的树
13.2.2 2-3-4 树
- 也是一种B树
13.3 B树、B+树和B*树
13.3.1 B树的介绍
- B-tree即B树,B即balanced,平衡
- 有人把B-tree翻译成B-树,容易让人产生误解,会以为B-树是一种树,而B树又是另外一种树,实际上,B-tree就是指的是B树
13.3.2 B树的说明
- B树的阶:节点最多子节点的个数。比如 2-3树的阶是3,2-3-4树的阶是4
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
13.3.3 B+树的介绍
- B+树是B树的变体,也是一种多路搜索树
13.3.4 B+树的说明
- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的
- 不可能在非叶子结点命中
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然
13.3.5 B*树的介绍
- B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针
13.3.6 B*树的说明
- B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2
- 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高
14、 图
14.1 图的基本介绍
14.1.1 为什么要有图?
- 前面我们学习了线性表和树
- 线性表局限于一个直接前驱和一个直接后继的关系
- 树也只能有一个前驱也就是父节点
- 当我们需要多对多的关系时,我们就需要图
14.1.2 图的举例说明
- 图是一种数据结构,其中节点可以具有零个或者多个相邻元素。
- 两个节点之间的连接称为边,节点也称为顶点
14.1.3 图的常用概念
- 顶点
- 边
- 路径:D->B->C
- 无向图:顶点之间的连接没有方向,比如:A-B,既可以是A->B,也可以是B->A
- 有向图
- 带权图:边带权值的图也叫网,如下:
14.2 图的表示方式
14.2.1 图的表示方式有两种
- 二维数组表示(邻接矩阵)
- 链表表示(邻接表)
14.2.2 邻接矩阵
- 邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1…n个点
14.2.3 邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失
- 接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
14.2.4 图的邻接矩阵表示方式(代码实现)
package Graph;
import java.util.ArrayList;
/**
* 图
* @author houbj
* @date 2020/12/24 09:45
*/
public class Graph {
private ArrayList<String> vertexList; //存储顶点的集合
private int[][] edges; // 存储图对应的邻接矩阵
private int numOfEdges; // 边的数量
public static void main(String[] args) {
int n = 5; // 节点个数为5
String vertexValue[] = {"A","B", "C", "D", "E"};
Graph graph = new Graph(n);
for (String value:vertexValue){
graph.insertVertex(value);
}
graph.insertEdges(1,3,3);
graph.insertEdges(4,3,2);
graph.show();
}
public Graph(int n){ // 顶点个数
vertexList = new ArrayList<String>(n);
edges = new int[n][n];
numOfEdges = 0;
}
/**
* 插入节点
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
*
* @param v1 第一个元素顶点的下标
* @param v2 第二个元素顶点的下标
* @param weight 权值
*/
public void insertEdges(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
/**
* 得到节点的个数
* @return
*/
public int getNumOfVertex(){
return vertexList.size();
}
/**
*
* @return 返回边的数目
*/
public int getNumOfEdges(){
return numOfEdges;
}
/**
*
* @param n
* @return 返回对应下标的节点值
*/
public String getInsertVertex(int n){
return vertexList.get(n);
}
/**
* 返回权值
* @param v1
* @param v2
* @return
*/
public int getWeight(int v1, int v2){
return edges[v1][v2];
}
public void show(){
System.out.print(" ");
for (int i = 0; i < vertexList.size(); i++) {
System.out.print(vertexList.get(i) + " ");
}
System.out.println();
for (int i = 0; i < edges.length; i++) {
System.out.print(vertexList.get(i) + " " );;
for (int j = 0; j < edges[i].length; j++) {
System.out.print(edges[i][j] + " ");
}
System.out.println();
}
}
}
14.3 图的遍历
14.3.1 深度优先遍历
- 图的深度优先搜索
- 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点
- 这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问
- 显然,深度优先搜索是一个***递归***的过程
- 深度优先遍历算法步骤
- 访问初始结点v,并标记结点v为已访问
- 查找结点v的第一个邻接结点w
- 若w存在,则继续执行 4 ,如果w不存在,则回到第1步,将从v的下一个结点继续
- 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)
- 查找结点v的w邻接结点的下一个邻接结点,转到步骤3
- 代码示例
package Graph;
import java.util.ArrayList;
/**
* 图
* @author houbj
* @date 2020/12/24 09:45
*/
public class Graph {
private ArrayList<String> vertexList; //存储顶点的集合
private int[][] edges; // 存储图对应的邻接矩阵
private int numOfEdges; // 边的数量
private boolean[] isVisited;
public static void main(String[] args) {
int n = 5; // 节点个数为5
String vertexValue[] = {"A","B", "C", "D", "E"};
Graph graph = new Graph(n);
for (String value:vertexValue){
graph.insertVertex(value);
}
graph.insertEdges(1,3,3);
graph.insertEdges(4,3,2);
graph.show();
graph.dfs();
}
/**
* 得到第一个邻接节点的下标
* @param index
* @return
*/
public int getFirstNeighbor(int index){
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 0){
return i;
}
}
return -1;
}
/**
* 根据前一个领结节点的下标获取下一个领结节点
* @param v1
* @param v2
* @return
*/
public int getNextNeighbor(int v1, int v2){
for (int i = v2 + 1; i < vertexList.size(); i++) {
if (edges[v1][i] > 0) {
return i;
}
}
return -1;
}
public void dfs(boolean[] isVisited, int i) {
System.out.print(getInsertVertex(i) + "->");
isVisited[i] = true;
int w = getFirstNeighbor(i);
while (w != -1) {
if (!isVisited[w]) {
dfs(isVisited,w);
} else { //如果已经被访问过
w= getNextNeighbor(i, w);
}
}
}
/**
* 对dfs进行重载,遍历我们的所有节点,并进行DFS
*/
public void dfs(){
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) {
dfs(isVisited,i);
}
}
}
public Graph(int n){ // 顶点个数
vertexList = new ArrayList<String>(n);
edges = new int[n][n];
numOfEdges = 0;
isVisited = new boolean[n];
}
/**
* 插入节点
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
*
* @param v1 第一个元素顶点的下标
* @param v2 第二个元素顶点的下标
* @param weight 权值
*/
public void insertEdges(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
/**
* 得到节点的个数
* @return
*/
public int getNumOfVertex(){
return vertexList.size();
}
/**
*
* @return 返回边的数目
*/
public int getNumOfEdges(){
return numOfEdges;
}
/**
*
* @param n
* @return 返回对应下标的节点值
*/
public String getInsertVertex(int n){
return vertexList.get(n);
}
/**
* 返回权值
* @param v1
* @param v2
* @return
*/
public int getWeight(int v1, int v2){
return edges[v1][v2];
}
public void show(){
System.out.print(" ");
for (int i = 0; i < vertexList.size(); i++) {
System.out.print(vertexList.get(i) + " ");
}
System.out.println();
for (int i = 0; i < edges.length; i++) {
System.out.print(vertexList.get(i) + " " );;
for (int j = 0; j < edges[i].length; j++) {
System.out.print(edges[i][j] + " ");
}
System.out.println();
}
}
}
14.3.2 广度优先遍历
- 图的广度优先搜索
- 类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
- 广度优先遍历算法步骤
- 访问初始结点v并标记结点v为已访问。
- 结点v入队列
- 当队列非空时,继续执行,否则算法结束。
- 出队列,取得队头结点u。
- 查找结点u的第一个邻接结点w。
- 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
1 若结点w尚未被访问,则访问结点w并标记为已访问。
2 结点w入队列
3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。
15、常用算法
15.1 二分查找算法(非递归)
15.1.2 代码实例
package tenAlto;
/**
* 二分查找,非递归
* @author houbj
* @date 2020/12/25 09:36
*/
public class BinarySearch {
public static void main(String[] args) {
int [] arr = new int[]{1,20,34,36,46,100};
int result = search(arr,2);
System.out.println("result : "+result);
}
public static int search(int[] arr, int target){
int left = 0;
int right = arr.length-1;
int count = 1;
while (left<=right) {
System.out.println("count: " + count++);
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target){
right = mid-1;
} else {
left = mid+1;
}
}
return -1;
}
}
15.2 分治算法
15.2.1 分治算法介绍
- 分治算法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或者更多相同或相似的子问题,再把子问题分成更小的子问题…直到最后子问题可以简单的直接求解,愿问题的解即子问题的解的合并。
- 可以解决的问题
- 二分搜索
- 大整数乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
15.2.2 分治算法的基本步骤
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
- 合并:将各个子问题的解合并为原问题的解
15.2.3 汉诺塔
- 思路分析
- 如果是有一个盘, A->C
- 如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2. 上面的盘
- 先把 最上面的盘 A->B
- 把最下边的盘 A->C
- 把B塔的所有盘 从 B->C
- 代码实例
package tenAlto;
/**
* 汉诺塔
* @author houbj
* @date 2020/12/25 10:13
*/
public class HanNuoTower {
public static void main(String[] args) {
tower(3,'A','B','C');
}
public static void tower(int num, char a, char b, char c){
if (num == 1) {
System.out.println("第一个盘从 "+ a + " -->" + c);
} else {
// (1)先把 最上面的盘 A->B,移动过程中回使用到C
tower(num-1,a,c,b);
// (2)把下面的盘放到C
System.out.println("第"+ num +"个盘,从" + a +"-->" +c);
// 把B塔所有的盘从B 到 C
tower(num-1, b, a, c);
}
}
}
15.3 动态规划算法
15.3.1 动态规划算法介绍
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
- 动态规划可以通过填表的方式来逐步推进,得到最优解
15.3.2 用动态规划算法解决-背包问题
- 背包问题:
- 有一个背包,容量为4磅 , 现有如下物品
- 要求达到的目标为装入的背包的总价值最大,并且重量不超出
- 要求装入的物品不能重复
- 思路分析
- 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
- 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包
- 算法的主要思想,利用动态规划来解决
每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值
- 得到下面的结果
v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是0
当w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
// 当 准备加入的新增的商品的容量小于等于当前背包的容量,
// 装入的方式:
v[i-1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值
v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值
当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
15.4 KMP算法
15.4.1 字符串匹配问题
- 有一个字符串 str1= “硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好”,和一个子串 str2=“尚硅谷你尚硅你”
- 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
15.4.2 暴力匹配算法-字符串匹配问题
- 如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
- 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
- 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
- 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
- 代码实例
package tenAlto;
/**
* @author houbj
* @date 2020/12/25 13:46
*/
public class ViolenceMatch {
public static void main(String[] args) {
String str1= "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
String str2 = "尚硅谷你尚硅你";
System.out.println(match(str1,str2));
}
public static int match(String str1, String str2){
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0; // 指向 s1 的索引
int j = 0; // 指向 s2 的索引
while (i<s1Len && j < s2Len) { // 保证匹配时,不越界
if (s1[i] == s2[j]) {
i++;
j++;
} else {
i = i-(j-1);
j = 0;
}
}
if (j == s2.length) {
return i - j;
}
return -1;
}
}
15.4.3 KMP算法
- KMP是一个解决模式串在文本中串中是否出现过,如果出现过,最早出现的位置的经典算法
- Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法.
- KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间
15.4.4 使用KMP算法解决-字符串匹配问题
- 有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”
- 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置
- 如果没有,则返回-1
- 代码实例
package tenAlgo;
import java.util.Arrays;
/**
* @author houbj
* @date 2020/12/25 15:53
*/
public class KMPALgo {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
// String str3 = "ABAABBB";
int[] next = kmpNext(str2);
System.out.println("next : " + Arrays.toString(next));
System.out.println("index : " + kmpSearch(str1, str2, next));
}
public static int kmpSearch(String str1, String str2, int[] next){
for (int i = 0, j = 0; i < str1.length(); i++) {
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {
return i-j+1;
}
}
return -1;
}
/**
* 获取到一个字符串的部分匹配值
* @param str 子串
* @return
*/
public static int[] kmpNext(String str){
int[] next = new int[str.length()];
next[0] = 0;
for (int i = 1, j = 0; i < str.length(); i++) {
while (j > 0 && str.charAt(i) != str.charAt(j)) {
// System.out.println("i : " + i + " j : "+ j);
j = next[j-1];
// System.out.println("- " + j);
}
if (str.charAt(i) == str.charAt(j)) {
j ++;
}
next[i] = j;
System.out.println(j);
}
return next;
}
}
15.5 贪心算法
15.5.1 应用场景-集合覆盖问题
- 假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
15.5.2 贪心算法介绍
- 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
- 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
15.5.3 穷举法
- 如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2ⁿ -1 个
15.5.4 贪婪算法
- 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
- 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
- 将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的- 地区在下次比较时去掉。
- 重复第1步直到覆盖了全部的地区