chapter5. Java数据结构与java算法

一、数据结构和算法概述

数据结构包括:线性结构和非线性结构。
1线性结构

  1. 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
    一对一e.g. a[0] = 3;
  2. 线性结构有两种不同的存储结构,即顺序存储结构(数组)链式存储结构(链表)
    顺序存储的线性表称为顺序表,顺序表中的存储元素是连续
  3. 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息(链表可以充分利用碎片内存)
  4. 线性结构常见的有:数组、队列、链表和栈,后面我们会详细讲解.

2非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构

1、稀疏数组

chapter5. Java数据结构与java算法_第1张图片

稀疏数组的基本介绍

当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方法是:

  1. 记录数组一共有几行几列,有多少个不同的
  2. 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
    chapter5. Java数据结构与java算法_第2张图片
    从7x6=42变成了3x9=27的数组

题:把棋盘用稀疏数组保存下来
chapter5. Java数据结构与java算法_第3张图片

代码演示

chapter5. Java数据结构与java算法_第4张图片

写入磁盘(fw):
chapter5. Java数据结构与java算法_第5张图片

读取文件(fis和fr)
chapter5. Java数据结构与java算法_第6张图片

2. 队列

介绍:

  1. 队列是一个有序列表,可以用数组或是链表来实现。
  2. 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
  3. 示意图:(使用数组模拟队列示意图)
    chapter5. Java数据结构与java算法_第7张图片
    数组模拟队列思路:
  • 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
  • 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变而 rear则是随着数据输入而改变

addQueue

当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析

  1. 加入队列时,需要将尾指针往后移:先判断 front == rear (队列是否为空,是否满↓)-> rear+1 ,
  2. 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。
    rear == maxSize -1(队列满)

加入队列和删除代码:
!注意!
因为先进先出的特点,不管是增加值到队列还是删除,front和rear都是先++再增/删。

public class ArrayQueque{
	PSVM{
	}
}

class ArrayQueue{
	priavte int front;
	priavte int rear;
	priavte int maxSize;
	priavte int[] arr;
	
	public ArrayQueue(int arrMaxSize){
	maxSize = arrMaxSize;
	arr = new int[maxSize];
	front = -1;
	rear = -1;
	}
	
	//判断队列是否为空
	public boolean isEmpety(){
		return rear == front;
	}
	//判断队列是否满
	public boolean isFull(){
		return rear == (maxSize-1);
	}
	
	//取出队列的值:先进先出
	public int getQueue(){
		if(isEmpety())
		{
			throw new RuntimeException("队列空,不能取数据");// 抛出异常
		}else{
			front++//先进先出,所以需要后移
			return arr[Front];
		}
	}
	
	//添加值到队列
	public void addQueue(int value){
		if(isFull())
		{
			Sout("队列已满")}else{
			rear++;
			arr[rear] = value;
		}
		
	}
	遍历队列//
	public void showQueue()
	{
		if(isEmpety())
		{
			sout("队列空");
		}else{
			for(int i = 0; i<arr.length; i++
			sout("第"+i+"个是:"+arr[i]);
		}
		
	}
	
}

优化:数组模拟环形队列

对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)
分析说明:

  1. 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的
    时候需要注意 (rear + 1) % maxSize == front 满]
  2. rear == front [空]
  3. 分析示意图
    问题分析并优化
  4. 目前数组使用一次就不能用, 没有达到复用的效果
  5. 将这个数组使用算法,改进成一个环形的队列 取模:%
    chapter5. Java数据结构与java算法_第8张图片

3. 链表

链表是有序的列表

  1. 链表是以节点的方式来存储,是链式存储
  2. 每个节点包含 data 域, next 域:指向下一个节点.
  3. 如图:发现链表的各个节点不一定是连续存储.
  4. 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

3.1 单链表

增+查:

  1. 添加时不考虑顺序,直接加在链表尾部
    chapter5. Java数据结构与java算法_第9张图片

  2. 添加时考虑顺序,根据排名插入到指定位置;若排名存在,则添加失败。

chapter5. Java数据结构与java算法_第10张图片

改:根据某个元素,找到指定节点,并修改其部分内容。
思路(1) 先找到该节点,通过遍历,(2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname

删:chapter5. Java数据结构与java算法_第11张图片

**我的题解 - LeetCode 删除单链表的值:

https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof/solution/shan-chu-lian-biao-de-zhi-by-duo-bi-e-fvvx/**

面试题:

  1. 求单链表中有效节点的个数(如果是带头结点的链表,需求不统计头节点)

    1. 判断链表是否为空
    2. 定义辅助变流len
    3. 遍历时不包括head while len++
  2. 查找单链表中的倒数第 k 个结点 【新浪面试题】

**我的题解 - 返回倒数第k个结点到最后的链表 (剑指offer 第22题):

https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/solution/fan-hui-dao-shu-di-kge-jie-dian-dao-zui-pyh5b/**

  1. 单链表的反转【腾讯面试题,有点难度】
    1 定义一个结点 reverseHead() = new ListNode()
    2 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,放入新的链表reverseHead的最前端
    3 原来的链表的head.next = reverseHead.next

图解思路:⑪步
chapter5. Java数据结构与java算法_第12张图片

chapter5. Java数据结构与java算法_第13张图片

public static void reversetList(HeroNode head) {
//如果当前链表为空,或者只有一个节点,无需反转,直接返回
	if(head.next == null || head.next.next == null) {
	return ;
}
//定义一个辅助的指针(变量),帮助我们遍历原来的链表
	HeroNode cur = head.next;
	HeroNode next = null;// 指向当前节点[cur]的下一个节点
	HeroNode reverseHead = new HeroNode(0, "", "");
	//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表 reverseHead 的最前端
	//动脑筋
	while(cur != null) {
		next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
		cur.next = reverseHead.next;//将 cur 的下一个节点指向新的链表的最前端
		reverseHead.next = cur; //将 cur 连接到新的链表上
		cur = next;//让 cur 后移
	}
	//将 head.next 指向 reverseHead.next , 实现单链表的反转
	head.next = reverseHead.next;
}

**我的题解 - 链表翻转 LeetCode 24:

https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/lian-biao-fan-zhuan-by-duo-bi-e-8dgr/**

  1. 从尾到头打印单链表 【百度,要求方式 1:反向遍历 。 方式 2:Stack 栈】
    利用的数据结构,将各个结点压入栈中,利用先进后出的特点实现逆序打印chapter5. Java数据结构与java算法_第14张图片

chapter5. Java数据结构与java算法_第15张图片

3.2 双链表

管理单向链表的缺点分析:

  1. 单向链表,查找的方向只能是一个方向(需要一个一个遍历),而双向链表可以向前或者向后查找。
  2. 单向链表不能自我删除,需要找到前一个节点(辅助节点)才能删除。 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).

分析双向链表的格式、 如何完成遍历,添加,修改和删除的思路
格式:单链表 + pre[指向前一个节点]

分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现

  1. 遍历 方和 单链表一样,只是可以向前,也可以向后查找

  2. 添加 (默认添加到双向链表的最后)
    (1) 先找到双向链表的最后的节点(即next=null)
    //2.3.形成一个双向链接
    (2) temp.next = newHeroNode (指向后一个节点)
    (3) newHeroNode.pre = temp; (指向前一个节点)

  3. 修改 思路和 原来的单向链表一样.

  4. 删除
    (1) 因为是双向链表,因此,我们可以实现自我删除某个节点
    (2) 直接找到要删除的这个节点,比如 temp
    //3.4.形成一个双向链接
    (3) temp.pre.next = temp.next
    if(temp.next != null){
    (4) temp.next.pre = temp.pre; (如果是最后一个结点,就不需要执行此代码)
    }

3.3 约瑟夫环问题(使用单向环形链表解决)

Josephu 问题为:
设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
 提示
用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束

约瑟夫问题-创建环形链表的思路图解:
构建和遍历

  1. first指针始终指向第一个节点,
  2. 最后一个节点的next始终指向first
  3. 辅助指针(current)来找最后一个节点
    chapter5. Java数据结构与java算法_第16张图片

chapter5. Java数据结构与java算法_第17张图片

4.栈

栈的介绍:

栈(stack)

  1. 栈是一个先入后出(FILO-First In Last Out)的有序列表。
  2. 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的
    一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
  3. 根据栈的定义可知,最先放入栈中元素在栈底(入栈(push)),最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
    素最先删除(出栈(pop)),最先放入的元素最后删除

栈的应用场景

  1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以
    回到原来的程序中。
  2. 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆
    栈中。
  3. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
  4. 二叉树的遍历。
  5. 图形的深度优先(depth 一 first)搜索法。

实现栈的思路

  1. 使用数组来模拟栈
  2. 定义一个 top 来表示栈顶,初始化 为 -1
  3. 入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
  4. 出栈的操作, int value = stack[top]; top–, return value
  5. 遍历:从栈顶开始遍历, for(int i = top; i>0; i–)

栈实现综合计算器:中缀表达式

chapter5. Java数据结构与java算法_第18张图片
代码实现[1. 先实现一位数的运算, 2. 扩展到多位数的运算]

前缀(波兰式)、中缀:运算符在操作数之间、后缀表达式(后波兰式)

前缀(波兰式):从右向左扫描表达式
中缀:需要判断运算符的优先级,对机算计并不方便。
后缀表达式(后波兰式):一般中缀会转为后缀,后缀最易操作
从左向右扫描表达式

中缀表达式转后缀表达式

具体步骤如下:

  1. 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
  2. 从左至右扫描中缀表达式;
  3. 遇到操作数时,将其压 s2;
  4. 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
    1.如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
    2.否则,若优先级比栈顶运算符的高,也将运算符压入 s1;
    3.否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较;
  5. 遇到括号时:
    (1) 如果是左括号“(”,则直接压入 s1
    (2) 如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃
  6. 重复步骤 2 至 5,直到表达式的最右边
  7. 将 s1 中剩余的运算符依次弹出并压入 s2
  8. 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

举例说明:
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下
因此结果为 :“1 2 3 + 4 × + 5 –”

5. 递归

应用场景:迷宫问题/回溯
概念:递归就是方法自己调用自己,每次调用时传入不同的变量。
递归调用规则:

  1. 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
    1. 每个空间的数据(局部变量),是独立的.

chapter5. Java数据结构与java算法_第19张图片

递归可用于解决什么样的问题:

  1. 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
  2. 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
  3. 将用栈解决的问题–>第归代码比较简洁

递归需要遵守的重要规则

递归需要遵守的重要规则

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响, 比如 n 变量
  3. 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
  4. 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死循环:)
  5. 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或
    者返回时,该方法也就执行完毕

6 .排序算法Sort Algorithm(重要 面试经常考)

定义:排序是将一组数据,根据指定的顺序进行排序的过程

排序的分类:

1. 内部排序(8种,重点,面试经常考)
把需要处理的所有数据加载入内部存储器/内存中进行排序

  1. 外部排序
    数据量过大,无法全部加载到内存中,需要借助外部存储进行排序

chapter5. Java数据结构与java算法_第20张图片

算法时间复杂度(如何衡量一个程序执行时间)

  1. 事后统计法
    局限:
    需要先运行程序,需要等;
    依赖于计算机的硬件、软件等环境。

  2. 事前估算法
    通过分析某个算法的时间复杂度来判断哪个算法更优。

时间复杂度

时间频度:一个算法花费的时间与算法中语句的执行次数成正比。一个算法中的语句执行次数称为语句频度时间频度

基本案例:
chapter5. Java数据结构与java算法_第21张图片

随着n的变大,有三个特点:
忽略常数项,忽略低次项(n的一次方),忽略系数。
所以时间复杂度主要还是看n的高次方

函数T(n)可能不相同,时间复杂度O(n)可能相同

计算时间复杂度的Steps:

  1. 用常数1代替运行时间中的所有加法常数 T(n)=5n²+7n+6 => T(n)=5n²+7n+1
  2. 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = 5n²
  3. 去除最高阶项的系数 T(n) = 5n² => T(n) = n² => O(n²)

常见的时间复杂度 8 个必知的

常见的时间复杂度

  1. 常数阶O(1) :没有循环等复杂结构,就算有几十万行,也是O(1)

  2. 对数阶O(log2n) : 常数的n次方
    e.g while(i < n)
    i=i*2;
    2的 x 次方等于 n即可退出循环,那么 x = log2n也就是说当循环 log2n 次以后
    (N=a的x次方,x=loga N)

  3. 线性阶O(n) :一个for循环,这个for循环会执行n次,T(n)=n+1,时间复杂度为O(n)。

  4. 线性对数阶O(nlog2n) :for 套 对数阶 = O(n*log2n)

  5. 平方阶O(n²) :双层for循环

  6. 立方阶O(n³) :三层for循环

  7. k次方阶O(nk) :嵌套了k次for循环

  8. 指数阶O(2ⁿ) :尽量避免

说明: 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低 从图中可见,我们应该尽可能避免使用指数阶2ⁿ的算法
chapter5. Java数据结构与java算法_第22张图片

算法的时间复杂度

chapter5. Java数据结构与java算法_第23张图片

空间复杂度

基本介绍

  1. 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
  2. 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
  3. 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.

1. 冒泡排序Bubble Sorting

基本思想是:
通过对待 排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下 来没有进行过交换,就说明序列有序,因此要在排序过程中设置 一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

e.g. 原始数组:3,9,-1,10,20 数组大小:size=5
总共需要size-1次

第一趟排序
(1) 3, 9, -1, 10, 20 // 如果相邻的元素逆序就交换
(2) 3, -1, 9, 10, 20
(3) 3, -1, 9, 10, 20
(4) 3, -1, 9, 10, 20

第二趟排序
(1) -1, 3, 9, 10, 20 //交换
(2) -1, 3, 9, 10, 20
(3) -1, 3, 9, 10, 20

第三趟排序
(1) -1, 3, 9, 10, 20
(2) -1, 3, 9, 10, 20

第四趟排序
(1) -1, 3, 9, 10, 20

小结冒泡排序规则 for{for{}}
(1) 一共进行 数组的大小 size-1次 的大循环
(2) 每一趟排序的次数在逐渐的减少
(3) 优化:如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序

代码:
原数组:{3,9,-1,10,20};
chapter5. Java数据结构与java算法_第24张图片

原数组:{3,9,-1,10,-2};
chapter5. Java数据结构与java算法_第25张图片

  1. 优化:如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序
    使用flag (boolean)表示是否进行果交换
    优化过后,提前结束冒泡排序

chapter5. Java数据结构与java算法_第26张图片

计算排序时间:8w个冒泡排序是10秒
chapter5. Java数据结构与java算法_第27张图片
chapter5. Java数据结构与java算法_第28张图片

2. 选择排序

是从预排序的数据中,按指定的规则选出某一元素,再依照规定交换位置后达到排序的目的
从整个数组中找到最小值,跟arr[0]交换,(范围:arr[0] ~ arr[n-1])
第二次从数组中找到最小值,跟arr[1]交换。(范围:arr[1] ~ arr[n-1])

以此类推,总共需要size-1次,得到一个按排序码从小到大排列的有序序列。

原始的数组 : 101, 34, 119, 1
第一轮排序 :
1, 34, 119, 101

第二轮排序 :
1, 34, 119, 101
第三轮排序 :
1, 34, 101, 119

说明:

  1. 选择排序一共有 数组大小 - 1 轮排序
  2. 每1轮排序,又是一个循环, 循环的规则(代码)
    2.1先假定当前这个数是最小数 arr[0]
    2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
    2.3 当遍历到数组的最后时,就得到本轮最小数和下标
    2.4 交换 [代码中再继续说 ]

选择排序算法快于冒泡算法

public static void selectSort(int[] arr) {
//在推导的过程,我们发现了规律,因此,可以使用 for 来解决
//选择排序时间复杂度是 O(n^2)
	for (int i = 0; i < arr.length - 1; i++) {
		int minIndex = i;
		int min = arr[i];
		for (int j = i + 1; j < arr.length; j++) {
			if (min > arr[j]) { // 说明假定的最小值,并不是最小; 即前一个比后一个大,应该要换位
			min = arr[j]; // 重置 min;即换位
			minIndex = j; // 重置 minIndex;即换下标
			//本轮的最小值已经找到了,接下来需要进行交换↓
			}
		}
		// 将最小值放在 arr[0], 即交换
		if (minIndex != i) {
			arr[minIndex] = arr[i]; //把arr[i]跟上面找到的最小值的位置[minIndex]进行交换
			arr[i] = min; //min是最小值,放在最前面
	}
		System.out.println("第"+(i+1)+"轮后~~");
		System.out.println(Arrays.toString(arr));// 1, 34, 101,119
}

lintcode题2:
chapter5. Java数据结构与java算法_第29张图片

3. 插入排序Insertion Sorting

介绍:
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

插入排序(Insertion Sorting)的基本思想是:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

次数:size-1次

//插入排序 
public static void insertSort(int[] arr) 
{ 
	int insertVal = 0; int insertIndex = 0
	
	//使用 for 循环来把代码简化
	for(int i = 1; i < arr.length; i++) 
	{
	//定义待插入的数
		insertVal = arr[i];
		insertIndex = i - 1; // 即 arr[1]的前面这个数的下标
	// 给 insertVal 找到插入的位置
	// 说明
	// 1. insertIndex >= 0 保证在给 insertVal 找插入位置,不越界
	// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
	// 3. 就需要将 arr[insertIndex] 后移
		while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
			arr[insertIndex + 1] = arr[insertIndex];// arr[insertIndex]
			insertIndex--;
		}
	// 当退出 while 循环时,说明插入的位置找到, insertIndex + 1
	// 举例:理解不了,我们一会 debug
	//这里我们判断是否需要赋值
		if(insertIndex + 1 != i) {//该放的位置就是当前的位置
			arr[insertIndex + 1] = insertVal;
		}
	System.out.println("第"+i+"轮插入");
	System.out.println(Arrays.toString(arr));
}

插入和冒泡算法话费的时间差不多

4. 希尔排序shell

基本排序的更高效的版本,也称缩小增量排序
基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止

chapter5. Java数据结构与java算法_第30张图片
chapter5. Java数据结构与java算法_第31张图片

希尔排序法应用:

  1. 希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.
  2. 希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度
// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法, // 思路(算法) ===> 代码
public static void shellSort(int[] arr) {
	int temp = 0;
	int count = 0;
	// 根据前面的逐步分析,使用循环处理
	for (int gap = arr.length / 2; gap > 0; gap /= 2) {
		for (int i = gap; i < arr.length; i++) {
		// 遍历各组中所有的元素(共 gap 组,每组有个元素), 步长 gap
			for (int j = i - gap; j >= 0; j -= gap) {
			// 如果当前元素大于加上步长后的那个元素,说明交换
			if (arr[j] > arr[j + gap]) {
			temp = arr[j];
			arr[j] = arr[j + gap];
			arr[j + gap] = temp;
			}
		}
	}
	//System.out.println("希尔排序第" + (++count) + "轮 =" + Arrays.toString(arr));
}

交换希尔排序比插入慢
优化:移动法,比交换希尔排序 和插入 快很多,十分的厉害

//对交换式的希尔排序进行优化->移位法
public static void shellSort2(int[] arr) {
// 增量 gap, 并逐步的缩小增量
	for (int gap = arr.length / 2; gap > 0; gap /= 2) {
	// 从第 gap 个元素,逐个对其所在的组进行直接插入排序
		for (int i = gap; i < arr.length; i++) {
			int j = i;
			int temp = arr[j];
			if (arr[j] < arr[j - gap]) {
				while (j - gap >= 0 && temp < arr[j - gap]) {
				//移动
					arr[j] = arr[j-gap];
					j -= gap;
				}
			//当退出 while 后,就给 temp 找到插入的位置
				arr[j] = temp;
			}
	}
}

5. 快速排序(Quicksort)

介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。

基本思想是:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

步骤

1)找到中位数pivot,
2)一直找,找到左边比pivot大的;再一直找,找到右边比pivot小的。
3)进行交换;
4)递归 recursion
. 4.1 左递归
. 4.2 右递归

我的题解 - leetcode18 将数组的奇数放在数组的前半部分,偶数放在数组的后半部分:

https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/solution/qi-shu-fang-zai-shu-zu-de-qian-ban-bu-fe-v7mr/

我的题解 - LeetCode剑指40 k个数最小的元素

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/kge-shu-zui-xiao-de-yuan-su-by-duo-bi-e-vgnx/

快速排序十分快!8w数据0~1秒

快排实例:
原数组:[-9,78,0,23,-567,70]

思路:标志数x 在x左边找比x大的数, 在x右边找比x小的数, 进行交换

  1. 快速排序得到:x = 0的话,0左边的78比0大;0右边的-567比0小,进行交换,得到:【-9,-567,0,23,78,70】
  2. 进行左递归得到:【-567,-9,0,23,78,70】
  3. 进行右递归得到:【-567,-9,0,23,70,78

在这里插入图片描述

public class QuickSort {
public static void main(String[] args) {}
//1. 快速排序
public static void quickSort(int[] arr,int left, int right) {
	int l = left; //左下标
	int r = right; //右下标
//pivot 中轴值
	int pivot = arr[(left + right) / 2];
	int temp = 0; //临时变量,作为交换时使用
//while 循环的目的是让比 pivot 值小放到左边
//比 pivot 值大放到右边
	while( l < r) {
	//在 pivot 的左边一直找,找到大于等于 pivot 值,才退出
		while( arr[l] < pivot) {
			l += 1;
		}
	//在 pivot 的右边一直找,找到小于等于 pivot 值,才退出
		while(arr[r] > pivot) {
			r -= 1;
		}
	//如果 l >= r, 说明 pivot 的左右两的值,都是按顺序的。
	//已经按照左边全部是小于等于 pivot 值,右边全部是大于等于 pivot 值
		if( l >= r) {
			break;
		}
		
	//交换
		temp = arr[l];
		arr[l] = arr[r];
		arr[r] = temp;
	//如果交换完后,发现这个 左边的值arr[l] == pivot 值 相等 r--, 前移
		if(arr[l] == pivot) {
			r -= 1;
		}
	//如果交换完后,发现这个 arr[r] == pivot 值 相等 l++, 后移
		if(arr[r] == pivot) {
			l += 1;
		}
	}
	//递归前的判断: 如果 l == r, 必须 l++, r--, 否则为出现栈溢出
		if (l == r) {
			l += 1;
			r -= 1;
		}
	//2. 向左递归,即pivot左边进行排序
		if(left < r) {
			quickSort(arr, left, r);
		}
	//3. 向右递归,即pivot右边进行排序
		if(l < right ) {
			quickSort(arr, l, right);
		}
}

6. 归类排序Merge Sort

介绍:是利用归并的思想实现的排序方法,该算法采用经典的分治算法策略
(分治法:分阶段:将问题分成一些小问题然后递归求解,治阶段:则将分的阶段得到的答案修补在一起)

归并排序时间很短!!!8w数据0~1秒

题:合并两个有序序列

e.g.
分:-
将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8]
合并:
chapter5. Java数据结构与java算法_第32张图片
chapter5. Java数据结构与java算法_第33张图片

7. 基数排序

  1. 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  2. 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
  3. 基数排序(Radix Sort)是桶排序的扩展
  4. 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序基本思想

  1. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  2. 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤

基数排序十分快;8w1s,80w1s,800w1s,但是会耗费内存。

图文说明:
chapter5. Java数据结构与java算法_第34张图片
代码:

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class RadixSort {
public static void main(String[] args) {
//根据前面的推导过程,我们可以得到最终的基数排序代
int arr[] = { 53, 3, 542, 748, 14,214}radixSort(int[] arr)
}

//基数排序方法
public static void radixSort(int[] arr) 
{//1. 得到数组中最大的数的位数
	int max = arr[0]; //假设第一数就是最大数
	for(int i = 1; i < arr.length; i++) 
	{
		if (arr[i] > max) 
		{
		max = arr[i];
		}
	}
	//得到最大数是几位数 
	int maxLength = (max + "").length();//比如最大数为647,变成string之后,用length()可知长度为3。
	//定义一个二维数组,表示 10 个桶, 每个桶就是一个一维数组
	//说明
	//1. 二维数组包含 10 个一维数组
	//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为 arr.length
	//3. 名明确,基数排序是使用空间换时间的经典算法
	int[][] bucket = new int[10][arr.length];
	//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
	//可以这里理解
	//比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
	int[] bucketElementCounts = new int[10];
	//这里我们使用循环将代码处理
	
	for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
	//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位.. for(int j = 0; j < arr.length; j++) 
	{//取出每个元素的对应位的值
		int digitOfElement = arr[j] / n % 10;
	//放入到对应的桶中
		bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
		//bucketElementCounts[digitOfElement]=0 因为没有赋值,初始化为0;
		bucketElementCounts[digitOfElement]++;
	}
	//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
	int index = 0;
	//遍历每一桶,并将桶中是数据,放入到原数组
		for(int k = 0; k < bucketElementCounts.length; k++) 
		{//如果桶中,有数据,我们才放入到原数组
			if(bucketElementCounts[k] != 0) 
			{//循环该桶即第 k 个桶(即第 k 个一维数组), 放入
				for(int l = 0; l < bucketElementCounts[k]; l++) 
				{//取出元素放入到 arr
					arr[index++] = bucket[k][l];
				}
			}
	//第 i+1 轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
		bucketElementCounts[k] = 0;
		}
	System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
}

基数排序的说明:

  1. 基数排序是对传统桶排序的扩展,速度很快.
  2. 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
  3. 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在 r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
  4. 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9

8. 第八个堆排序跟二叉树相关,等二叉树那再讲

七个常用排序总结与对比

相关术语:

  1. 稳定:相同的数在排序之后位置不会变。比如 1a = 1b, [1a,56,8,5,6,2,8,1b], 1a在1b前面,排序后,1a任然在1b前面
  2. 不稳定: 1a在1b前面,排序后,1a可能在1b后面
  3. 内排序:在内存进行排序
  4. 外排序:在内存进行排序
  5. 时间复杂度:一个算法执行所消耗的时间
  6. 空间复杂度:一个算法执行所消耗的空间/内存大小
  7. n:数据规模
  8. k:桶的个数
  9. in-place:不占用额外内存空间
  10. out-place:占用额外内存空间

n越大,基数比其他的更快
n超过十后,基数比其他线性阶的更快
chapter5. Java数据结构与java算法_第35张图片

7. 查找算法:常见4个

  1. 顺序(线性)查找
  2. 二分查找/折半查找
  3. 插值查找
  4. 斐波那契查找

1. 顺序(线性)查找

【顺序查找】就是最简单的用for循环找某个值,传入一个数组和标志,有提示找到 并给出下标值

e.g.
有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称
【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值

public class SeqSearch {
	public static void main(String[] args) 
	{
		int arr[] = { 1, 9, 11, -1, 34, 89 };// 没有顺序的数组
		int index = seqSearch(arr, -11);
		if(index == -1) 
		{
			System.out.println("没有找到到");
		} 
		else 
		{
			System.out.println("找到,下标为=" + index);
		}
	}
/**
* 这里我们实现的线性查找是找到一个满足条件的值,就返回
* @param arr
* @param value
* @return
*/
	public static int seqSearch(int[] arr, int value) 
	{// 线性查找是逐一比对,发现有相同值,就返回下标
		for (int i = 0; i < arr.length; i++) {
			if(arr[i] == value) 
				return i;
		}
			return -1;
		}
}

2. 二分查找算法

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

课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.

二分查找的思路分析

  1. 首先确定该数组的中间的下标 mid = (left + right) / 2
  2. 然后让需要查找的数 findVal 和 arr[mid] 比较
    2.1 findVal > arr[mid] , 说明你要查找的数在mid 的右边, 因此需要递归的向右查找
    2.2 findVal < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
    2.3 findVal == arr[mid] 说明找到,就返回
  3. 结束递归.
    1. 找到就结束递归
    2. 递归完整个数组,仍然没有找到findVal ,也需要结束递归 当 left > right 就需要退出
public class BinarySearch {
	public static void main(String[] args) {
	int arr[] = { 1, 8, 10, 89,1000,1000, 1234 };
	// int resIndex = binarySearch(arr, 0, arr.length - 1, 1000);
	// System.out.println("resIndex=" + resIndex);
	List<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 1000);
	System.out.println("resIndexList=" + resIndexList);
	}
	
// 二分查找算法
/**
* @param arr数组
* @param left左边的索引
* @param right右边的索引
* @param findVal要查找的值
* @return 如果找到就返回下标,如果没有找到,就返回 -1
*/
	public static int binarySearch(int[] arr, int left, int right, int findVal) {
	// 当 left > right 时,说明递归整个数组,但是没有找到
	if (left > right) {
		return -1;
	}
	
	int mid = (left + right) / 2;
	int midVal = arr[mid];
	if (findVal > midVal) { // 向 右递归
		return binarySearch(arr, mid + 1, right, findVal);
	} else if (findVal < midVal) { // 向左递归
		return binarySearch(arr, left, mid - 1, findVal);
	} else {//找到了
		return mid;
	}
}

若要找的数,有多个相同的数值时,可将所有相同的值放入ArrayList

/*
* 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,
* 有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000
*
* 思路分析
* 1. 在找到 mid 索引值,不要马上返回
* 2. 向 mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 3. 向 mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 4. 将 Arraylist 返回
*/

public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
	// 当 left > right 时,说明递归整个数组,但是没有找到
	if (left > right) {
		return new ArrayList<Integer>();
	}
	int mid = (left + right) / 2;
	int midVal = arr[mid];
	
	if (findVal > midVal) { // 向 右递归
		return binarySearch2(arr, mid + 1, right, findVal);
	} else if (findVal < midVal) { // 向左递归
		return binarySearch2(arr, left, mid - 1, findVal);
	} else {
		// * 思路分析
		// * 1. 在找到 mid 索引值,不要马上返回
		// * 2. 向 mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
		// * 3. 向 mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
		// * 4. 将 Arraylist 返回
		List<Integer> resIndexlist = new ArrayList<Integer>();
			//向 mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
		int temp = mid - 1;
		
		while(true) {
			if (temp < 0 || arr[temp] != findVal) {//退出
				break;
				}
				//否则,就 temp 放入到 resIndexlist
		resIndexlist.add(temp);
		temp -= 1; //temp 左移
		}
		resIndexlist.add(mid); //
			//向 mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
		temp = mid + 1;
		while(true) {
			if (temp > arr.length - 1 || arr[temp] != findVal) {//退出
				break;
			}
			//否则,就 temp 放入到 resIndexlist
		resIndexlist.add(temp);
		temp += 1; //temp 右移
		}
		return resIndexlist;
		}
	}
}

我的题解 - 二分法找旋转有序数组的最小值

https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/solution/er-fen-fa-zhao-zui-xiao-zhi-by-duo-bi-e-ymq1/
chapter5. Java数据结构与java算法_第36张图片

3. 插值查找算法 (比上面两个更快,查找/递归次数更少)

插值查找原理介绍:

  1. 插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。
  2. 将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right.key 就是前面我们讲的 findVal
  3. int mid = low + (high - low) X (key - arr[low]) / (arr[high] - arr[low]) ;插值索引
    对应前面的代码公式:int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])
  4. 举例说明插值查找算法 1-100 的数组 下
    chapter5. Java数据结构与java算法_第37张图片

插值查找注意事项:
1)对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
2)关键字分布不均匀的情况下,该方法不一定比折半(二分)查找要好

//编写插值查找算法
//说明:插值查找算法,也要求数组是有序的
public static void main(String[] args) {
	int [] arr = new int[100];
	for(int i = 0; i < 100; i++) {
		arr[i] = i + 1;
	}
	int index = insertValueSearch(arr, 0, arr.length - 1, 1234);
	//int index = binarySearch(arr, 0, arr.length, 1);
	System.out.println("index = " + index);
	//System.out.println(Arrays.toString(arr));
}
/**
*
* @param arr 数组
* @param left 左边索引
* @param right 右边索引
* @param findVal 查找值
* @return 如果找到,就返回对应的下标,如果没有找到,返回-1
*/
public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
	System.out.println("插值查找次数~~");
//注意:findVal < arr[0] 和 findVal > arr[arr.length - 1] 必须需要
//否则我们得到的 mid 可能越界
	if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
		return -1;
	}
	// 求出 mid, 自适应
	int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
	int midVal = arr[mid];
	if (findVal > midVal) { // 说明应该向右边递归
		return insertValueSearch(arr, mid + 1, right, findVal);
	} else if (findVal < midVal) { // 说明向左递归查找
		return insertValueSearch(arr, left, mid - 1, findVal);
	} else {
		return mid;
	}
}

4. 斐波那契查找

斐波那契(黄金分割法)查找基本介绍:
1)黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
2)斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近 黄金分割值0.618

chapter5. Java数据结构与java算法_第38张图片

8. 哈希表(结构)

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

Hashtable是一个数组,里面7条linkedlist,每条list中的node是Emp类,每个node都包含一个emp的信息

其实就是数组(HashTable)里是链表,链表(linkedList)里是节点(head、)
chapter5. Java数据结构与java算法_第39张图片
chapter5. Java数据结构与java算法_第40张图片

应用案例及解法 – 哈希表(散列)-Google 上机题

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

要求:

  1. 不使用数据库,速度越快越好=>哈希表(散列)
  2. 添加时,保证按照 id 从低到高插入 [课后思考:如果 id 不是从低到高插入,但要求各条链表仍是从低到
    高,怎么解决?]
  3. 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
  4. 思路分析并画出示意图:↓

chapter5. Java数据结构与java算法_第41张图片

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 默认为 null
		
		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;
}

9. 二叉树

9.1 为什么需要树这种数据结构/树的优点(增删查快)

  1. 数组存储方式的分析
    优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
    缺点:如果要检索具体某个值(无序),或者插入值(按一定顺序)会整体移动,效率较低

  2. 链式存储方式的分析
    优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,
    删除效率也很好)。
    缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)

  3. 存储方式的分析
    能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,
    同时也可以保证数据的插入,删除,修改的速度。
    chapter5. Java数据结构与java算法_第42张图片

9.2 树的常用术语

树的常用术语(结合示意图理解):
1) 节点
2) 根节点
3) 父节点
4) 子节点
5) 叶子节点 (没有子节点的节点) e.g. AEFG
6) 节点的权(节点值)
7) 路径(从 root 节点找到该节点的路线)
8) 层
9) 子树
10) 树的高度(最大层数)
11) 森林 :多颗子树构成森林
chapter5. Java数据结构与java算法_第43张图片

9.3 二叉树的概念

  1. 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树

  2. 二叉树的子节点分为左节点和右节点
    3)示意图
    chapter5. Java数据结构与java算法_第44张图片

  3. 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树↓
    e.g. 结点数:2^3-1=8-1=7

  4. 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二
    层的叶子节点在右边连续,我们称为完全二叉树↓

chapter5. Java数据结构与java算法_第45张图片

9.4 二叉树的前中后序遍历的说明

使用前序,中序和后序对下面的二叉树进行遍历.
1) 前序遍历: 先输出父节点,再遍历左子树和右子树
从左往右 走过就输出
2) 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
从左往右 走到最底部开始走过就输出
(在下图D子树和G子树两个子树这种情况下,中序遍历时,必须是先左子节点,子树根节点,再输出右子结点。 G作为D子树的右子节点和G子树的子根节点,需要先输出F->D->H后,再输出G->I)。
(要注意是左子结点还是右子结点,必须按左中右顺序输出)
3) 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
从左往右 走到最底部开始,先输出叶/子节点。
4) 小结: 看输出父节点的顺序,就确定是前序,中序还是后序

知乎上的例子:
chapter5. Java数据结构与java算法_第46张图片

9.5 二叉树遍历应用实例(前序,中序,后序)

chapter5. Java数据结构与java算法_第47张图片

public class BinaryTreeDemo {
	public static void main(String[] args) {
	//先需要创建一颗二叉树
	BinaryTree binaryTree = new BinaryTree();
	//创建需要的结点
	HeroNode root = new HeroNode(1, "宋江");
	HeroNode node2 = new HeroNode(2, "吴用");
	HeroNode node3 = new HeroNode(3, "卢俊义");
	HeroNode node4 = new HeroNode(4, "林冲");
	HeroNode node5 = new HeroNode(5, "关胜");
	//说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
	root.setLeft(node2);
	root.setRight(node3);
	node3.setRight(node4);
	node3.setLeft(node5);
	binaryTree.setRoot(root);
	//测试
	System.out.println("前序遍历"); // 1,2,3,5,4
	binaryTree.preOrder();
	//测试
	System.out.println("中序遍历");
	binaryTree.infixOrder(); // 2,1,5,3,4
	//
	System.out.println("后序遍历");
	binaryTree.postOrder(); // 2,5,4,3,1
	}
}


//定义 BinaryTree 二叉树
class BinaryTree {
	private HeroNode root;
	public void setRoot(HeroNode root) {
	this.root = root;
}
//前序遍历
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("二叉树为空,无法遍历");
		}
	}
}



//先创建 HeroNode 结点
class HeroNode {
	private int no;
	private String name;
	private HeroNode left; //默认 null
	private HeroNode right; //默认 null
	public HeroNode(int no, String name) {
		this.no = no;
		this.name = name;
	}
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public HeroNode getLeft() {
		return left;
	}
	public void setLeft(HeroNode left) {
		this.left = left;
	}
	public HeroNode getRight() {
		return right;
	}
	public void setRight(HeroNode right) {
		this.right = right;
	}
	@Override
	public String toString() {
		return "HeroNode [no=" + no + ", name=" + name + "]";
	}
	
	//编写前序遍历的方法
	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);
		}
}

二叉树-查找指定节点
要求

  1. 请编写前序查找,中序查找和后序查找的方法。
  2. 并分别使用三种查找方式,查找 heroNO = 5 的节点
  3. 并分析各种查找方式,分别比较了多少次
  4. 思路分析图解
    chapter5. Java数据结构与java算法_第48张图片

代码实现:

public class BinaryTreeDemo {
public static void main(String[] args) {
//先需要创建一颗二叉树
BinaryTree binaryTree = new BinaryTree();
//创建需要的结点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
//说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
//测试
System.out.println("前序遍历"); // 1,2,3,5,4
binaryTree.preOrder();
//测试
System.out.println("中序遍历");
binaryTree.infixOrder(); // 2,1,5,3,4
//
System.out.println("后序遍历");
binaryTree.postOrder(); // 2,5,4,3,1
//前序遍历
//前序遍历的次数 :4
// System.out.println("前序遍历方式~~~");
// HeroNode resNode = binaryTree.preOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
// } else {
// System.out.printf("没有找到 no = %d 的英雄", 5);
// }
//中序遍历查找
//中序遍历 3 次
// System.out.println("中序遍历方式~~~");
// HeroNode resNode = binaryTree.infixOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
// } else {
// System.out.printf("没有找到 no = %d 的英雄", 5);
// }
//后序遍历查找
//后序遍历查找的次数 2 次
System.out.println("后序遍历方式~~~");
HeroNode resNode = binaryTree.postOrderSearch(5);
if (resNode != null) {
System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
} else {
System.out.printf("没有找到 no = %d 的英雄", 5);
}
}
}
//定义 BinaryTree 二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if(this.root != null) {
this.root.preOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
//中序遍历
public void infixOrder() {
if(this.root != null) {
this.root.infixOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
//后序遍历
public void postOrder() {
if(this.root != null) {
this.root.postOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
//前序遍历
public HeroNode preOrderSearch(int no) {
if(root != null) {
return root.preOrderSearch(no);
} else {
return null;
}
}
//中序遍历
public HeroNode infixOrderSearch(int no) {
if(root != null) {
return root.infixOrderSearch(no);
}else {
return null;
}
}
//后序遍历
public HeroNode postOrderSearch(int no) {
if(root != null) {
return this.root.postOrderSearch(no);
}else {
return null;
}
}
}
//先创建 HeroNode 结点
class HeroNode {
private int no;
private String name;
private HeroNode left; //默认 null
private HeroNode right; //默认 null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + "]";
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父结点
//递归向左子树前序遍历
if(this.left != null) {
this.left.preOrder();
}
//递归向右子树前序遍历
if(this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder() {
//递归向左子树中序遍历
if(this.left != null) {
this.left.infixOrder();
}
//输出父结点
System.out.println(this);
//递归向右子树中序遍历
if(this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder() {
if(this.left != null) {
this.left.postOrder();
}
if(this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
//前序遍历查找
/**
*
* @param no 查找 no
* @return 如果找到就返回该 Node ,如果没有找到返回 null
*/
public HeroNode preOrderSearch(int no) {
System.out.println("进入前序遍历");
//比较当前结点是不是
if(this.no == no) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if(this.left != null) {
resNode = this.left.preOrderSearch(no);
}
if(resNode != null) {//说明我们左子树找到
return resNode;
}
//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;
}
}

9.6 二叉树-删除节点

要求

  1. 如果删除的节点是叶子节点,则删除该节点
  2. 如果删除的节点是非叶子节点,则删除该子树.
  3. 测试,删除掉 5 号叶子节点 和 3 号子树.
  4. 完成删除思路分析

chapter5. Java数据结构与java算法_第49张图片

递归删除结点代码:

//HeroNode 类增加方法
//递归删除结点
//1.如果删除的节点是叶子节点,则删除该节点
//2.如果删除的节点是非叶子节点,则删除该子树
public void delNode(int no) {
//思路
/*
* 1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断
当前这个结点是不是需要删除结点.
2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回
(结束递归删除)
3. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回
(结束递归删除)
4. 如果第 2 和第 3 步没有删除结点,那么我们就需要向左子树进行递归删除
5. 如果第 4 步也没有删除结点,则应当向右子树进行递归删除. */
//2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回(结束递归删除)
	if(this.left != null && this.left.no == no) {
		this.left = null;
		return;
	}
//3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回(结束递归删除)
	if(this.right != null && this.right.no == no) {
		this.right = null;
		return;
	}
//4.我们就需要向左子树进行递归删除
	if(this.left != null) {
		this.left.delNode(no);
	}
//5.则应当向右子树进行递归删除
	if(this.right != null) {
	
		this.right.delNode(no);
	}
}
//在 BinaryTree 类增加方法
//删除结点
public void delNode(int no) {
	if(root != null) {
	//如果只有一个 root 结点, 这里立即判断 root 是不是就是要删除结点
		if(root.getNo() == no) {
			root = null;
		} else {
	//递归删除
		root.delNode(no);
		}
	}else{
		System.out.println("空树,不能删除~");
	}
}
//在 BinaryTreeDemo 类增加测试代码:
//测试一把删除结点
	System.out.println("删除前,前序遍历");
	binaryTree.preOrder(); // 1,2,3,5,4
	binaryTree.delNode(5);
	//binaryTree.delNode(3);
	System.out.println("删除后,前序遍历");
	binaryTree.preOrder(); // 1,2,3,4

10.1.8 二叉树-删除节点
思考题(课后练习)
1) 如果要删除的节点是非叶子节点,现在我们不希望将该非叶子节点为根节点的子树删除,需要指定规则, 假如
规定如下:
2) 如果该非叶子节点 A 只有一个子节点 B,则子节点 B 替代节点 A
3) 如果该非叶子节点 A 有左子节点 B 和右子节点 C,则让左子节点 B 替代节点 A。
4) 请大家思考,如何完成该删除功能, 老师给出提示.(课后练习)
5) 后面在讲解 二叉排序树时,在给大家讲解具体的删除方法

我的题解 – 二叉树镜像

https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof/solution/er-cha-shu-de-jing-xiang-by-duo-bi-e-fuhf/

9-2 顺序存储二叉树

基本说明:
数组的存储方式和树的存储方式可以互相转换。

chapter5. Java数据结构与java算法_第50张图片
要求:

  1. 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
  2. 要求在遍历数组 arr 时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历

顺序存储二叉树的特点:

  1. 顺序二叉树通常只考虑完全二叉树
  2. 第 n 个元素的左子节点为 2 * n + 1
  3. 第 n 个元素的右子节点为 2 * n + 2
  4. 第 n 个元素的父节点为 (n-1) / 2
  5. n : 表示二叉树中的第几个元素(按 0 开始编号如图所示)

9-2.1 顺序存储二叉树遍(数组转二叉树:)

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

public class ArrBinaryTreeDemo {
	public static void main(String[] args) {
		int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
		//创建一个 ArrBinaryTree
		ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
		arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7
	}
}
//编写一个 ArrayBinaryTree, 实现顺序存储二叉树遍历
class ArrBinaryTree {
	private int[] arr;//存储数据结点的数组
	public ArrBinaryTree(int[] arr) {
	this.arr = arr;
}
//重载 preOrder
	public void preOrder() {
		this.preOrder(0);
	}
	//编写一个方法,完成顺序存储二叉树的前序遍历
	/**
	*
	* @param index 数组的下标
	*/
	public void preOrder(int index) {
	//如果数组为空,或者 arr.length = 0
		if(arr == null || arr.length == 0) {
			System.out.println("数组为空,不能按照二叉树的前序遍历");
		}
		//输出当前这个元素
		System.out.println(arr[index]);
		//向左递归遍历
		if((index * 2 + 1) < arr.length) {
			preOrder(2 * index + 1 );
		}
		//向右递归遍历
		if((index * 2 + 2) < arr.length) {
			preOrder(2 * index + 2);
		}
	}
}

作业:
课后练习:请同学们完成对数组以二叉树中序,后序遍历方式的代码. 10.2.3 顺序存储二叉树应用实例

ps:八大排序算法中的堆排序,就会使用到顺序存储二叉树, 关于堆排序,放在<<树结构实际应用>> 章节讲解。

9-3 线索化二叉树

先看一个问题
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7
问题分析:

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

9-3.1 线索二叉树基本介绍

  1. n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域存放指向
    该结点在某种遍历次序下的前驱和后继结点的指针
    (这种附加的指针称为**“线索”**)
  2. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质
    的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  3. 一个结点的前一个结点,称为前驱结点
  4. 一个结点的后一个结点,称为后继结点

9-3.2 线索二叉树应用案例

应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10,1,14,6}

思路分析: 中序遍历的结果:{8, 3, 10, 1, 14,6}
说明: 当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:
1) left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的
就是前驱节点.
2) right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向
的是后继节点.

public class ThreadedBinaryTreeDemo {
	public static void main(String[] args) {
	//测试一把中序线索二叉树的功能
	HeroNode root = new HeroNode(1, "tom");
	HeroNode node2 = new HeroNode(3, "jack");
	HeroNode node3 = new HeroNode(6, "smith");
	HeroNode node4 = new HeroNode(8, "mary");
	HeroNode node5 = new HeroNode(10, "king");
	HeroNode node6 = new HeroNode(14, "dim");
	//二叉树,后面我们要递归创建, 现在简单处理使用手动创建
	root.setLeft(node2);
	root.setRight(node3);
	node2.setLeft(node4);
	node2.setRight(node5);
	node3.setLeft(node6);
	//测试中序线索化
	ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
	threadedBinaryTree.setRoot(root);
	threadedBinaryTree.threadedNodes();
	//测试: 以 10 号节点测试
	HeroNode leftNode = node5.getLeft();
	HeroNode rightNode = node5.getRight();
	System.out.println("10 号结点的前驱结点是 =" + leftNode); //3
	System.out.println("10 号结点的后继结点是=" + rightNode); //1
	//当线索化二叉树后,能在使用原来的遍历方法
	//threadedBinaryTree.infixOrder();
	System.out.println("使用线索化的方式遍历 线索化二叉树");
	threadedBinaryTree.threadedList(); // 8, 3, 10, 1, 14, 6
	}
}
//定义 ThreadedBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
	private HeroNode root;
	//为了实现线索化,需要创建要给指向当前结点的前驱结点的指针
	//在递归进行线索化时,pre 总是保留前一个结点
	private HeroNode pre = null;
	public void setRoot(HeroNode root) {
		this.root = root;
	}
//重载一把 threadedNodes 方法
	public void threadedNodes() {
		this.threadedNodes(root);
	}
//遍历线索化二叉树的方法
	public void threadedList() {
	//定义一个变量,存储当前遍历的结点,从 root 开始
		HeroNode node = root;
		while(node != null) {
		//循环的找到 leftType == 1 的结点,第一个找到就是 8 结点
		//后面随着遍历而变化,因为当 leftType==1 时,说明该结点是按照线索化
		//处理后的有效结点
			while(node.getLeftType() == 0) {
				node = node.getLeft();
			}
//打印当前这个结点
		System.out.println(node);
//如果当前结点的右指针指向的是后继结点,就一直输出
			while(node.getRightType() == 1) {
//获取到当前结点的后继结点
				node = node.getRight();
				System.out.println(node);
			}
//替换这个遍历的结点
		node = node.getRight();
		}
	}
//编写对二叉树进行中序线索化的方法
/**
*
* @param node 就是当前需要线索化的结点
*/
	public void threadedNodes(HeroNode node) {
	//如果 node==null, 不能线索化
		if(node == null) {
			return;	
		}
//(一)先线索化左子树
		threadedNodes(node.getLeft());
//(二)线索化当前结点[有难度]
//处理当前结点的前驱结点
//以 8 结点来理解
//8 结点的.left = null , 8 结点的.leftType = 1
	if(node.getLeft() == null) {
	//让当前结点的左指针指向前驱结点
		node.setLeft(pre);
	//修改当前结点的左指针的类型,指向前驱结点
		node.setLeftType(1);
	}
//处理后继结点
	if (pre != null && pre.getRight() == null) {
	//让前驱结点的右指针指向当前结点
		pre.setRight(node);
	//修改前驱结点的右指针类型
		pre.setRightType(1);
	}
//!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
	pre = node;
//(三)在线索化右子树
	threadedNodes(node.getRight());
	}
//删除结点
	public void delNode(int no) {
		if(root != null) {
		//如果只有一个 root 结点, 这里立即判断 root 是不是就是要删除结点
			if(root.getNo() == no) {
				root = null;
			} else {
			//递归删除
				root.delNode(no);
			}
		}else{
			System.out.println("空树,不能删除~");
		}
	}
//前序遍历
	public void preOrder() {
		if(this.root != null) {
			this.root.preOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
	//中序遍历
	public void infixOrder() {
		if(this.root != null) {
			this.root.infixOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
//后序遍历
	public void postOrder() {
		if(this.root != null) {
			this.root.postOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
//前序遍历
	public HeroNode preOrderSearch(int no) {
		if(root != null) {
			return root.preOrderSearch(no);
		} else {
			return null;
		}
	}
//中序遍历
	public HeroNode infixOrderSearch(int no) {
		if(root != null) {
			return root.infixOrderSearch(no);
		}else {
			return null;
		}
	}
//后序遍历
	public HeroNode postOrderSearch(int no) {
		if(root != null) {
			return this.root.postOrderSearch(no);
		}else {
			return null;
		}
	}
}
//先创建 HeroNode 结点
class HeroNode {
	private int no;
	private String name;
	private HeroNode left; //默认 null
	private HeroNode right; //默认 null
//说明
//1. 如果 leftType == 0 表示指向的是左子树, 如果 1 则表示指向前驱结点
//2. 如果 rightType == 0 表示指向是右子树, 如果 1 表示指向后继结点
	private int leftType;
	private int rightType;
	public int getLeftType() {
	return leftType;
}
	public void setLeftType(int leftType) {
		this.leftType = leftType;
	}
	public int getRightType() {
		return rightType;
	}
	public void setRightType(int rightType) {
		this.rightType = rightType;
	}
	public HeroNode(int no, String name) {
		this.no = no;
		this.name = name;
	}
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public HeroNode getLeft() {
		return left;
	}
	public void setLeft(HeroNode left) {
		this.left = left;
	}
	public HeroNode getRight() {
		return right;
	}
	public void setRight(HeroNode right) {
		this.right = right;
	}
	@Override
	public String toString() {
	return "HeroNode [no=" + no + ", name=" + name + "]";
	}
//递归删除结点
//1.如果删除的节点是叶子节点,则删除该节点
//2.如果删除的节点是非叶子节点,则删除该子树
public void delNode(int no) {
//思路
/*
* 1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断
当前这个结点是不是需要删除结点. 2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回
(结束递归删除)
3. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回
(结束递归删除)
4. 如果第 2 和第 3 步没有删除结点,那么我们就需要向左子树进行递归删除
5. 如果第 4 步也没有删除结点,则应当向右子树进行递归删除. */
//2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回(结
束递归删除)
if(this.left != null && this.left.no == no) {
	this.left = null;
	return;
}
//3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回(结
束递归删除)
if(this.right != null && this.right.no == no) {
this.right = null;
return;
}
//4.我们就需要向左子树进行递归删除
if(this.left != null) {
this.left.delNode(no);
}
//5.则应当向右子树进行递归删除
if(this.right != null) {
this.right.delNode(no);
}
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父结点
//递归向左子树前序遍历
if(this.left != null) {
this.left.preOrder();
}
//递归向右子树前序遍历
if(this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder() {
//递归向左子树中序遍历
if(this.left != null) {
this.left.infixOrder();
}
//输出父结点
System.out.println(this);
//递归向右子树中序遍历
if(this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder() {
if(this.left != null) {
this.left.postOrder();
}
if(this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
//前序遍历查找
/**
*
* @param no 查找 no
* @return 如果找到就返回该 Node ,如果没有找到返回 null
*/
public HeroNode preOrderSearch(int no) {
System.out.println("进入前序遍历");
//比较当前结点是不是
if(this.no == no) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if(this.left != null) {
resNode = this.left.preOrderSearch(no);
}
if(resNode != null) {//说明我们左子树找到
return resNode;
}
//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;
}
}

遍历线索化二叉树

  1. 说明:对前面的中序线索化的二叉树, 进行遍历
  2. 分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历
    线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次
    序应当和中序遍历保持一致。
  3. 代码
//ThreadedBinaryTree 类
//遍历线索化二叉树的方法
public void threadedList() {
//定义一个变量,存储当前遍历的结点,从 root 开始
HeroNode node = root;
while(node != null) {

//循环的找到 leftType == 1 的结点,第一个找到就是 8 结点
//后面随着遍历而变化,因为当 leftType==1 时,说明该结点是按照线索化
//处理后的有效结点
while(node.getLeftType() == 0) {
node = node.getLeft();
}
//打印当前这个结点
System.out.println(node);
//如果当前结点的右指针指向的是后继结点,就一直输出
while(node.getRightType() == 1) {
//获取到当前结点的后继结点
node = node.getRight();
System.out.println(node);
}
//替换这个遍历的结点
node = node.getRight();
}
}

10.3.5 线索化二叉树的课后作业:
我这里讲解了中序线索化二叉树,前序线索化二叉树和后序线索化二叉树的分析思路类似,同学们作为课后作
业完成

我的题解 - LeetCode54. 二叉搜索树的第k大节点:(搜索树=排序树:左小root中右大)

https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/solution/er-cha-sou-suo-shu-de-di-kda-jie-dian-by-srqu/

10 树结构实际应用

10.1 堆排序

基本介绍

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

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

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

  4. 大顶堆举例说明

我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:
大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号
chapter5. Java数据结构与java算法_第51张图片

  1. 小顶堆举例说明
    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
    chapter5. Java数据结构与java算法_第52张图片

  2. 一般升序采用大顶堆,降序采用小顶堆

10.1.1堆排序基本思想

堆排序的基本思想是:
1)将待排序序列(如无序序列)构造成一个堆,根据升序、降序选择大顶堆、小顶堆
2)将顶堆元素与末尾元素交换,将最大元素放在数组末尾。
3)重新调整结构,使其满足对定义,然后继续交换对顶元素与当前末尾元素,反复执行调整+交换步骤,直至真个序列有序。

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

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

步骤一:
循环次数:for(int i = arr.length/2-1; i > = 0; i–)循环体: adjustHeap(arr,i,arr.length)
chapter5. Java数据结构与java算法_第53张图片chapter5. Java数据结构与java算法_第54张图片
chapter5. Java数据结构与java算法_第55张图片
chapter5. Java数据结构与java算法_第56张图片

步骤2:
顶元素跟最尾部的元素换,temp = arr[j]; arr[j]=arr[0]; arr[0]=temp;

然后继续从arr[0]开始,最大的放在最上面,即(arr[0])4 跟右边的8(6跟8比 8大)换
adjustHeap(arr,0,arr.length)

循环arr.length-1次。for(int j = arr,length-1; j>0;j–)
chapter5. Java数据结构与java算法_第57张图片
chapter5. Java数据结构与java算法_第58张图片
chapter5. Java数据结构与java算法_第59张图片
chapter5. Java数据结构与java算法_第60张图片
chapter5. Java数据结构与java算法_第61张图片

堆排序代码实现
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。 代码实现↓
说明:
1)堆排序不是很好理解,通过Debug 帮助大家理解堆排序
2)堆排序的速度非常快,在我的机器上 8百万数据 3 秒左右。时间复杂度为线性对数阶 O(nlogn)

代码相关解释

  1. arr[i]=arr[k]:换值了,原本放6的位置就放9 (把9换上去)
    2.1 下面这个arr[i] 是i=k 是如果执行了i=k的话,原本放9的位置就放6 (把6换下去)
    2.2没有执行的话就是等于原本的数
public static void main(String[] args) {
//要求将数组进行升序排序
	int arr[] = {4, {4, 6, 8, 5, 9};

public static void heapSort(int arr[]) {
	int temp = 0;
	System.out.println("堆排序!!");
	// //分步完成
	// adjustHeap(arr, 1, arr.length);
	// System.out.println("第一次" + Arrays.toString(arr)); // 4, 9, 8, 5, 6
	//
	// adjustHeap(arr, 0, arr.length);
	// System.out.println("第 2 次" + Arrays.toString(arr)); // 9,6,8,5,4
	//完成我们最终代码
	//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
	for(int i = arr.length / 2 -1; i >=0; i--) {
		adjustHeap(arr, i, arr.length);
	}
/*
* 2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换
步骤,直到整个序列有序。
*/
	for(int j = arr.length-1;j >0; j--) {
//交换
	temp = arr[j];
	arr[j] =arr[0];
	arr[0] = temp;
	adjustHeap(arr, 0, j);
	}
	//System.out.println("数组=" + Arrays.toString(arr));
	}
	//将一个数组(二叉树), 调整成一个大顶堆
	/**
	* 功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
	* 举例 int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
* 如果我们再次调用 adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
* @param arr 待调整的数组
* @param i 表示非叶子结点在数组中索引
* @param lenght 表示对多少个元素继续调整, length 是在逐渐的减少
*/
public static void adjustHeap(int arr[], int i, int lenght) {
	int temp = arr[i];//先取出当前元素的值,保存在临时变量
//开始调整
//说明
//1. k = i * 2 + 1 k 是 i 结点的左子结点
	for(int k = i * 2 + 1; k < lenght; k = k * 2 + 1) {
		if(k+1 < lenght && arr[k] < arr[k+1]) { //说明左子结点的值小于右子结点的值
		k++; // k 指向右子结点
		}
		if(arr[k] > temp) { //如果子结点大于父结点
			arr[i] = arr[k]; //把较大的值赋给当前结点
			i = k; //!!! i 指向 k,继续循环比较
		} else {
			break;//!
		}
	}
	//当 for 循环结束后,我们已经将以 i 为父结点的树的最大值,放在了 最顶(局部)
	arr[i] = temp;//将 temp 值放到调整后的位置
	}
}

10.2 赫夫曼树

11.2.1 基本介绍

  1. 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl:weighted pass length)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
  2. 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近

赫夫曼树几个重要概念和举例说明

  1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
  2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
    点的带权路径长度为
    :从根结点到该结点之间的路径长度与该结点的权的乘积(权乘路径长度)。
  3. 树的带权路径长度wpl:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path
    length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  4. WPL 最小的就是赫夫曼树

11.2.3 赫夫曼树创建思路图解
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.  思路分析(示意图):
{13, 7, 8, 3, 29, 6, 1}
构成赫夫曼树的步骤:

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
    尚硅谷 Java 数据结构和算法
    更多 Java –大数据 –前端 –python 人工智能 -区块链资料下载,可访问百度:尚硅谷官网 第 256页
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数
    据都被处理,就得到一颗赫夫曼树
  5. 图解

代码实现:


package com.atguigu.huffmantree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTree {
public static void main(String[] args) {
int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
Node root = createHuffmanTree(arr);
//测试一把
preOrder(root); //
}
//编写一个前序遍历的方法
public static void preOrder(Node root) {
if(root != null) {
root.preOrder();
}else{
System.out.println("是空树,不能遍历~~");
}
}
// 创建赫夫曼树的方法
/**
*
* @param arr 需要创建成哈夫曼树的数组
* @return 创建好后的赫夫曼树的 root 结点
*/
public static Node createHuffmanTree(int[] arr) {
// 第一步为了操作方便
// 1. 遍历 arr 数组
// 2. 将 arr 的每个元素构成成一个 Node
// 3. 将 Node 放入到 ArrayList 中
List<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
//我们处理的过程是一个循环的过程
while(nodes.size() > 1) {
//排序 从小到大
Collections.sort(nodes);
System.out.println("nodes =" + nodes);
//取出根节点权值最小的两颗二叉树
//(1) 取出权值最小的结点(二叉树)
Node leftNode = nodes.get(0);
//(2) 取出权值第二小的结点(二叉树)
Node rightNode = nodes.get(1);
//(3)构建一颗新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//(4)从 ArrayList 删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5)将 parent 加入到 nodes
nodes.add(parent);
}
//返回哈夫曼树的 root 结点
return nodes.get(0);
}
}
// 创建结点类
// 为了让 Node 对象持续排序 Collections 集合排序
// 让 Node 实现 Comparable 接口
class Node implements Comparable<Node> {
int value; // 结点权值
Node left; // 指向左子结点
Node right; // 指向右子结点
//写一个前序遍历
public void preOrder() {
System.out.println(this);
if(this.left != null) {

this.left.preOrder();
}
if(this.right != null) {
this.right.preOrder();
}
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
@Override
public int compareTo(Node o) {
// TODO Auto-generated method stub
// 表示从小到大排序
return this.value - o.value;
}
}

10.3 赫夫曼编码

基本介绍

  1. 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
  2. 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
  3. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
  4. 赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码

原理剖析
chapter5. Java数据结构与java算法_第62张图片
在这里插入图片描述

步骤如下:

传输的 字符串

  1. i like like like java do you like a java
  2. d:1 y:1:u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
  3. 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值

构成赫夫曼树的步骤:

  1. 从小到大进行排序,
    将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序,不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理, 就得到一颗赫夫曼树
    chapter5. Java数据结构与java算法_第63张图片
  1. 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 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
  1. 按照上面的赫夫曼编码,我们的"i like like like java do you like a
    java" 字符串对应的编码为 (注 意这里我们使用的无损压缩)
    10101001101111011110100110111101111010011011110111101000011000011100110011110000110
    01111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133
    6) 长度为 : 133 说明: 原来长度是 359 , 压缩了 (359-133) 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性赫夫曼编码是无损处理方案
  • 注意事项
    注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是
    一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果我们让每次生成的新的二叉树总是排在权
    值相同的二叉树的最后一个,则生成的二叉树为

最佳实践-数据压缩(创建赫夫曼树)
将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数
据 压 缩 处 理 , 形 式 如
"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100
110111101111011100100001100001110
"步骤 1:根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树.

思路:前面已经分析过了,而且我们已然讲过了构建赫夫曼树的具体实现
代码实现:

//可以通过 List 创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
	while(nodes.size() > 1) {
	//排序, 从小到大
	Collections.sort(nodes);
	//取出第一颗最小的二叉树
	Node leftNode = nodes.get(0);
	//取出第二颗最小的二叉树
	Node rightNode = nodes.get(1);
	//创建一颗新的二叉树,它的根节点 没有 data, 只有权值
	Node parent = new Node(null, leftNode.weight + rightNode.weight);
	parent.left = leftNode;
	parent.right = rightNode;
	//将已经处理的两颗二叉树从 nodes 删除
	nodes.remove(leftNode);
	nodes.remove(rightNode);
	//将新的二叉树,加入到 nodes
	nodes.add(parent);
	}
	//nodes 最后的结点,就是赫夫曼树的根结点
	return nodes.get(0)
}

最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)
我们已经生成了 赫夫曼树, 下面我们继续完成任务

  1. 生成赫夫曼树对应的赫夫曼编码 , 如下表: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011

  2. 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下. 10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001010
    00101111111100110001001010011011100

  3. 思路:前面已经分析过了,而且我们讲过了生成赫夫曼编码的具体实现。

  4. 代码实现:

//为了调用方便,我们重载 getCodes
private static Map<Byte, String> getCodes(Node root) {
		if(root == null) {
		return null;
		}
	//处理 root 的左子树
	getCodes(root.left, "0", stringBuilder);
	//处理 root 的右子树
	getCodes(root.right, "1", stringBuilder);
	return huffmanCodes;
}


/**
* 功能:将传入的 node 结点的所有叶子结点的赫夫曼编码得到,并放入到 huffmanCodes 集合
* @param node 传入结点
* @param code 路径: 左子结点是 0, 右子结点 1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
	StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将 code 加入到 stringBuilder2
	stringBuilder2.append(code);
	if(node != null) { //如果 node == null 不处理
	//判断当前 node 是叶子结点还是非叶子结点
		if(node.data == null) { //非叶子结点
		//递归处理
		//向左递归
			getCodes(node.left, "0", stringBuilder2);
		//向右递归
			getCodes(node.right, "1", stringBuilder2);
		} else { //说明是一个叶子结点
		//就表示找到某个叶子结点的最后
			huffmanCodes.put(node.data, stringBuilder2.toString());
		}
	}
}

最佳实践-数据解压(使用赫夫曼编码解码)
使用赫夫曼编码来解码数据,具体要求是

  1. 前面我们得到了赫夫曼编码和对应的编码
    byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
  2. 现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java"
  3. 思路:解码过程,就是编码的一个逆向操作。
  4. 代码实现:
/**
* 将一个 byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的 Java 基础 二进制的原码,反码,补
码
* @param b 传入的 byte
* @param flag 标志是否需要补高位如果是 true ,表示需要补高位,如果是 false 表示不补, 如果是最后一个
字节,无需补高位
* @return 是该 b 对应的二进制的字符串,(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
	//使用变量保存 b
	int temp = b; //将 b 转成 int
	//如果是正数我们还存在补高位
	if(flag) {
		temp |= 256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001
	}
		String str = Integer.toBinaryString(temp); //返回的是 temp 对应的二进制的补码
	if(flag) {
		return str.substring(str.length() - 8);
	} else {
		return str;
	}
}
//编写一个方法,完成对压缩数据的解码
/**
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
//1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111... StringBuilder stringBuilder = new StringBuilder();
//将 byte 数组转成二进制的字符串
	for(int i = 0; i < huffmanBytes.length; i++) {
	byte b = huffmanBytes[i];
	//判断是不是最后一个字节
	boolean flag = (i == huffmanBytes.length - 1);
	stringBuilder.append(byteToBitString(!flag, b));
	}
	//把字符串安装指定的赫夫曼编码进行解码
	//把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
	Map<String, Byte> map = new HashMap<String,Byte>();
	for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
	map.put(entry.getValue(), entry.getKey());
	}
	//创建要给集合,存放 byte
	List<Byte> list = new ArrayList<>();
	//i 可以理解成就是索引,扫描 stringBuilder
	for(int i = 0; i < stringBuilder.length(); ) {
		int count = 1; // 小的计数器
		boolean flag = true;
		Byte b = null;
		while(flag) {
		//1010100010111... //递增的取出 key 1
			String key = stringBuilder.substring(i, i+count);//i 不动,让 count 移动,指定匹配到一个字符
			b = map.get(key);
			if(b == null) {//说明没有匹配到
				count++;
			}else {
			//匹配到
				flag = false;
			}
		}
		list.add(b);
		i += count;//i 直接移动到 count
	}
	//当 for 循环结束后,我们 list 中就存放了所有的字符 "i like like like java do you like a java"
	//把 list 中的数据放入到 byte[] 并返回
	byte b[] = new byte[list.size()];
	for(int i = 0;i < b.length; i++) {
		b[i] = list.get(i);
	}
	return b;
}

我的题解 - LeetCode42 数组中连续最大数:

https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/solution/lian-xu-zi-shu-zu-de-zui-da-he-by-duo-bi-afbw/

10.4 二叉排序树

解决方案分析
使用数组
1)数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢. [示意图]
2)数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。[示意图]

使用链式存储-链表
1)不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。[示意图]

使用二叉排序树
1)插入删除速度快

二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点

比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
chapter5. Java数据结构与java算法_第64张图片

二叉排序树创建和遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创
建成对应的二叉排序树为 :
11.4.5 二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

  1. 删除叶子节点 (比如:2, 5, 9, 12)
  2. 删除只有一颗子树的节点 (比如:1)
  3. 删除有两颗子树的节点. (比如:7, 3,10 )
  4. 操作的思路分析
    chapter5. Java数据结构与java算法_第65张图片

二叉树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

  1. 删除叶子节点 (比如:2, 5, 9, 12)
    思路
    (1) 需求先去找到要删除的结点 targetNode
    (2) 找到 targetNode 的 父结点 parent (子节点都有父结点)
    (3) 确定 targetNode 是 parent 的左子结点 还是右子结点
    (4) 根据前面的情况来对应删除
    左子结点 parent.left = null
    右子结点 parent.right = null

  2. 删除只有一颗子树的节点 (比如:1)
    (1) 需求先去找到要删除的结点 targetNode
    (2) 找到 targetNode 的 父结点 parent
    (3) 确定 targetNode 的子结点(2)是左子结点还是右子结点
    (4) targetNode 是 parent 的左子结点还是右子结点
    (5) 如果 targetNode 有
    左子结点

    5.1 如果 targetNode 是 parent 的左子结点
    parent.left = targetNode.left;
    5.2 如果 targetNode 是 parent 的右子结点
    parent.right = targetNode.left;
    就是把target删了,target的子节点不删除,所有需要安排位置

    (6) 如果 targetNode 有右子结点
    6.1 如果 targetNode 是 parent 的左子结点
    parent.left = targetNode.right;
    6.2 如果 targetNode 是 parent 的右子结点
    parent.right = targetNode.right

  3. 删除有两颗子树的节点. (比如:7, 3,10 )
    思路
    (1) 需求先去找到要删除的结点 targetNode
    (2) 找到 targetNode 的 父结点 parent
    (3) 从 targetNode 的右子树找到最小的结点
    (4) 用一个临时变量,将 最小结点的值保存 temp = 11
    (5) 删除该最小结点
    (6) targetNode.value = temp

  4. 操作的思路分析

10.5 AVL平衡二叉排序树

chapter5. Java数据结构与java算法_第66张图片

基本介绍

1)平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高
2)具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。

单旋转(左旋转) 右高

  1. 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
  2. 思路分析(示意图)
    问题:当插入8时
    rightHeight()-leftHeight()>1成立,此时,不再是一颗avl树了.

处理方法–进行左旋转:
1.创建一个新的节点newNode(以当前root根结点:4 这个值创建)创建一个新的节点,值等于当前根节点的值
int value;//字段
Node newNode = new Node(value);

2.把新节点的左子树设置了当前节点的左子树
newNode.left = left;

3.把新节点的右子树设置为当前节点的右子树的左子树
newNode.right = right.left;

4.把当前节点的值换为右子节点的值
value = right.value"

5.把当前节点的右子树设置成右子树的右子树
right = right.right

6.把当前节点的左子树/左子节点设置为新节点
left = newNode;
chapter5. Java数据结构与java算法_第67张图片
代码实现:

  1. 算出左右的高度
  2. 左旋转代码
  3. 左旋转的条件
private void leftRotate() {
	//创建新的结点,以当前根结点的值
	Node newNode = new Node(value);
	//把新的结点的左子树设置成当前结点的左子树
	newNode.left = left;
	//把新的结点的右子树设置成带你过去结点的右子树的左子树
	newNode.right = right.left;
	//把当前结点的值替换成右子结点的值
	value = right.value;
	//把当前结点的右子树设置成当前结点右子树的右子树
	right = right.right;
	//把当前结点的左子树(左子结点)设置成新的结点
	left = newNode;
}


右旋转:左比右高

  1. 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
  2. 思路分析(示意图)
    问题:当插入6时
    leftHeight()-rightHeight()>1成立,此时,不再是一颗av树了

处理方式–进行右旋转:(就是降低左子树的高度),这里是将9这个节点,通过右旋转,到右子树

1.创建一个新的节点newNode(以10这个值创建)创建一个新的节点,值等于当前根节点的值
int value;//字段
newNode = newNode(value);

2.把新节点的右子树设置了当前节点的右子树
newNode.right = right;

3.把新节点的左子树设置为当前节点的左子树的右子树
newNode.left = left.right;

4.把当前节点的值换为左子节点的值
value = left.value;

5.把当前节点的左子树设置成左子树的左子树
left = left.left;

6.把当前节点的右子树设置为新节点
right=newNode;
chapter5. Java数据结构与java算法_第68张图片

private void rightRotate() {
	Node newNode = new Node(value);
	newNode.right = right;
	newNode.left = left.right;
	value = left.value;
	left = left.left;
	right = newNode;
}

双旋转:旋转后不是平衡二叉树AVL

在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树
chapter5. Java数据结构与java算法_第69张图片

问题分析,以数组 { 10, 11, 7, 6, 8, 9 }为例子:

  1. 当符号右旋转的条件时
  2. 如果它的左子树的右子树高度大于它的左子树的高度
    2.1 先对当前这个结点的左节点进行左旋转
    即 7 为新节点,新节点指向6,8替换7,8指向新节点和9
    2.2 对当前结点进行右旋转的操作即可
    chapter5. Java数据结构与java算法_第70张图片
    代码:
    进行右旋转前,先进行判断
    转完之后一定要马上return,否则会继续执行下面的代码。
    chapter5. Java数据结构与java算法_第71张图片

chapter5. Java数据结构与java算法_第72张图片

//完整代码参考本地文件_笔记.pdf

多路查找树

B树

二叉树存在的问题——所以需要多叉树:
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:
问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
问题2:节点海量,也会造成二叉树的高度很大会降低操作速度

多叉树 multiway tree(2-3,树2-3-4树)
1)在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
2)多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
3)举例说明(下面2-3树就是一颗多叉树)
chapter5. Java数据结构与java算法_第73张图片

B树的基本介绍

B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。
chapter5. Java数据结构与java算法_第74张图片

  1. 如图B树通过重新组织节点, 降低了树的高度.
  2. 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
  3. 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中(性能好)

ps:
结点的度:A结点下面有两棵子树,A的度为2
树的度:所有的结点里面度最大的值就是树的度。

2-3树基本介绍

2-3树最简单的B树结构, 具有如下特点:

  1. 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  2. 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点. (不存在只有一个结点)
  3. 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点. (不存在只有一个结点和两个结点)
    2-3树是由二节点和三节点构成的树。
  4. 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面 3 个条件。
  5. 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则

其它说明
除了 23 树,还有 234 树等,概念和 23 树类似,也是一种 B 树。 如图
chapter5. Java数据结构与java算法_第75张图片

B 树、B+树和 B*树

B 树的介绍(Balanced tree/ b-tree)

前面已经介绍了 2-3 树和 2-3-4 树,他们就是 B 树(英语:B-tree 也写成 B-树),这里我们再做一个说明,我们在学习 Mysql 时,经常听到说某种类型的索引是基于 B 树或者 B+树的,如图
chapter5. Java数据结构与java算法_第76张图片

对上图的说明:

  1. B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
  2. B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询
    关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  3. 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据. 4) 搜索有可能在非叶子结点结束
  4. 其搜索性能等价于在关键字全集内做一次二分查找

B+树的介绍

B+树是 B 树的变体,也是一种多路搜索
chapter5. Java数据结构与java算法_第77张图片

对上图的说明:

  1. B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性
    能也等价于在关键字全集做一次二分查找
  2. 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)
    恰好是有序的。
  3. 不可能在非叶子结点命中
  4. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  5. 更适合文件索引系统
  6. B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然.

B*树的介绍

B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针。

chapter5. Java数据结构与java算法_第78张图片

 B*树的说明:

  1. B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的
    1/2。
  2. 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高

概念:

1)顶点(vertex)
2)边(edge)
3)路径无向图(右图)

chapter5. Java数据结构与java算法_第79张图片
4)有向图
5)代权图

chapter5. Java数据结构与java算法_第80张图片

图的表示方式

图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。

1 邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1…n个点。
chapter5. Java数据结构与java算法_第81张图片
图快速入门案例
思路分析:
1) ArryList arr 用于存储顶点的集合
arr.add(vertex)
2) 需要矩阵:二维数组int[] []edges,来存储图对应的领结矩阵

3) 需要表示遍数目的变量:numOfEdege
		edges[v1][v2] = weight; //weight =1 直接连接到,weight = 0连不到
		edges[v2][v1] = weight; //无向的,所以连或不连都是一致的

创建图+插入结点/边+代码:

//核心代码,汇总在后面
//插入结点
public void insertVertex(String vertex) {
	vertexList.add(vertex);
}
//添加边
/**
* @param v1 表示点的下标即使第几个顶点 "A"-"B" "A"->0 "B"->1
* @param v2 第二个顶点对应的下标
* @param weight 表示
*/
public void insertEdge(int v1, int v2, int weight) {
	edges[v1][v2] = weight;
	edges[v2][v1] = weight;
	numOfEdges++;
}

图的遍历

图遍历介绍
所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略: (1)深度优先遍历 (2)广度优先遍历

图的深度优先搜索(DFS:Depth First Search)

概念:优先纵向挖掘深入
深度优先遍历基本思想
1)深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
2)我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
3)显然,深度优先搜索是一个递归的过程

步骤:
1)访问初始结点v,并标记结点v为已访问。
2)查找结点v的第一个邻接结点w。
3)若w存在,则继续执行4,如果w不存在,则回到第1步,将从v的下一个结点继续。
4)若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。
5)查找结点v的w邻接结点的下一个邻接结点,转到步骤3。看一个具体案例分析:

//核心代码
//深度优先遍历算法
//i 第一次就是 0
private void dfs(boolean[] isVisited, int i) {
//首先我们访问该结点,输出
System.out.print(getValueByIndex(i) + "->");
//将结点设置为已经访问
isVisited[i] = true;
//查找结点 i 的第一个邻接结点 w
int w = getFirstNeighbor(i);
while(w != -1) {//说明有
if(!isVisited[w]) {
dfs(isVisited, w);
}
//如果 w 结点已经被访问过
w = getNextNeighbor(i, w);
}
}
//对 dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的结点,进行 dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
dfs(isVisited, i);
}
}
}

图的广度优先搜索(BFS:Broad First Search)

广度优先遍历基本思想
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点

步骤
1)访问初始结点v并标记结点v为已访问。
2)结点v入队列
3)当队列非空时,继续执行,否则算法结束。 while(!=null)
4)出队列,取得队头结点u。
5)查找结点u的第一个邻接结点w。
6)若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
6.1 若结点w尚未被访问,则访问结点w并标记为已访问。
6.2 结点w入队列
6.3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。

//对一个结点进行广度优先遍历的方法
private void bfs(boolean[] isVisited, int i) {
	int u ; // 表示队列的头结点对应下标
	int w ; // 邻接结点 w
//队列,记录结点访问的顺序
	LinkedList queue = new LinkedList();	
//访问结点,输出结点信息
	System.out.print(getValueByIndex(i) + "=>");
//标记为已访问
	isVisited[i] = true;
//将结点加入队列
	queue.addLast(i);
	while( !queue.isEmpty()) {
//取出队列的头结点下标
		u = (Integer)queue.removeFirst();
//得到第一个邻接结点的下标 w
		w = getFirstNeighbor(u);
		while(w != -1) {//找到
//是否访问过
			if(!isVisited[w]) {
			System.out.print(getValueByIndex(w) + "=>");
//标记已经访问
			isVisited[w] = true;
//入队
			queue.addLast(w);
}
//以 u 为前驱点,找 w 后面的下一个邻结点
			w = getNextNeighbor(u, w); //体现出我们的广度优先
		}
	}
}
//遍历所有的结点,都进行广度优先搜索
public void bfs() {
	isVisited = new boolean[vertexList.size()];
	for(int i = 0; i < getNumOfVertex(); i++) {
		if(!isVisited[i]) {
		bfs(isVisited, i);
		}
	}
}

图的深度优先VS 广度优先

chapter5. Java数据结构与java算法_第82张图片
深度优先遍历顺序为 1->2->4->8->5->3->6->7
广度优先算法的遍历顺序为:1->2->3->4->5->6->7->8

2 邻接表

10邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
2)邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
chapter5. Java数据结构与java算法_第83张图片

常用 10 种算法

  1. 二分查找非递归

  2. 分治算法的设计模型

  3. 动态规划

1. 二分查找(非递归)

题:数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.

public class BinarySearchNoRecur {
	public static void main(String[] args) {
	//测试
	int[] arr = {1,3, 8, 10, 11, 67, 100};
	int index = binarySearch(arr, 100);
	System.out.println("index=" + index);//
	}
	//二分查找的非递归实现
	/**
	*
	* @param arr 待查找的数组, arr 是升序排序
	* @param target 需要查找的数
	* @return 返回对应下标,-1 表示没有找到
	*/
	public static int binarySearch(int[] arr, int target) {
		int left = 0;
		int right = arr.length - 1;
		while(left <= right) { //说明继续查找
			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;
	}
}

ps:
我的非递归二分查找相关题目(leetcode)
我的题解 - LeetCode用二分法找最小值:
https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/solution/er-fen-fa-zhao-zui-xiao-zhi-by-duo-bi-e-ymq1/

2. 分治算法

介绍
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

分治算法的基本步骤分治法在每一层递归上都有三个步骤:
1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
2)解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3)合并:将各个子问题的解合并为原问题的解。

chapter5. Java数据结构与java算法_第84张图片

分治算法可以求解的一些经典问题:
二分搜索
大整数乘法
棋盘覆盖
合并排序
快速排序
线性时间选择
最接近点对问题
循环赛日程表
汉诺塔

2.1 分治算法解决汉诺塔问题:

chapter5. Java数据结构与java算法_第85张图片
汉诺塔游戏的演示和思路分析:

  1. 如果是有一个盘, A->C
    如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2. 上面的盘
  2. 先把 最上面的盘 A->B
  3. 把最下边的盘 A->C
  4. 把 B 塔的所有盘 从 B->C

汉诺塔游戏的代码实现:


package com.atguigu.dac;
public class Hanoitower {
	public static void main(String[] args) {
		hanoiTower(5, 'A', 'B', 'C');
	}
	//汉诺塔的移动的方法
	//使用分治算法
	public static void hanoiTower(int num, char a, char b, char c) {
	//如果只有一个盘
		if(num == 1) {
		System.out.println("第 1 个盘从 " + a + "->" + c);
		} else {
	//如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的一个盘 2. 上面的所有盘
	//1. 先把 最上面的所有盘 A->B, 移动过程会使用到 c
		hanoiTower(num - 1, a, c, b);
	//2. 把最下边的盘 A->C
		System.out.println("第" + num + "个盘从 " + a + "->" + c);
	//3. 把 B 塔的所有盘 从 B->C , 移动过程使用到 a 塔
		hanoiTower(num - 1, b, a, c);
		}
	}
}

3. 动态规划算法

介绍
1)动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
2)动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3)与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
4)动态规划可以通过填表的方式来逐步推进,得到最优解.

3.1 动态规划算法最佳实践-背包(01)问题

背包问题:有一个背包,容量为4磅 , 现有如下物品
chapter5. Java数据结构与java算法_第86张图片
要求
1)达到的目标为装入的背包的总价值最大,并且重量不超出
2)要求装入的物品不能重复

思路分析和图解
3)背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
4)这里的问题属于01背包,即每个物品最多放一个。(而无限背包可以转化为01背包。)
5) 算法的主要思想,利用动态规划来解决。详细思路分析



思路分析和图解
1)算法的主要思想,利用动态规划来解决。
2)每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。
3)再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:
(1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是0
(2) 当w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当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]]} :
chapter5. Java数据结构与java算法_第87张图片

public class KnapsackProblem {
	public static void main(String[] args) {
	// TODO Auto-generated method stub
		int[] w = {1, 4, 3};//物品的重量
		int[] val = {1500, 3000, 2000}; //物品的价值 这里 val[i] 就是前面讲的 v[i]
		int m = 4; //背包的容量
		int n = val.length; //物品的个数
	//创建二维数组,
	//v[i][j] 表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值
		int[][] v = new int[n+1][m+1];
	//为了记录放入商品的情况,我们定一个二维数组
		int[][] path = new int[n+1][m+1];
	//初始化第一行和第一列, 这里在本程序中,可以不去处理,因为默认就是 0
		for(int i = 0; i < v.length; i++) {
			v[i][0] = 0; //将第一列设置为 0
		}
		for(int i=0; i < v[0].length; i++) {
			v[0][i] = 0; //将第一行设置 0
		}
	//根据前面得到公式来动态规划处理
	for(int i = 1; i < v.length; i++) { //不处理第一行 i 是从 1 开始的
		for(int j=1; j < v[0].length; j++) {//不处理第一列, j 是从 1 开始的
		//公式
			if(w[i-1]> j) { // 因为我们程序 i 是从 1 开始的,因此原来公式中的 w[i] 修改成 w[i-1]
				v[i][j]=v[i-1][j];
			} else {
			//说明:
			//因为我们的 i 从 1 开始的, 因此公式需要调整成
			//v[i][j]=Math.max(v[i-1][j], val[i-1]+v[i-1][j-w[i-1]]);
			//v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
			//为了记录商品存放到背包的情况,我们不能直接的使用上面的公式,需要使用 if-else 来体
			现公式
				if(v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
					v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
				//把当前的情况记录到 path
				path[i][j] = 1;
				} else {
					v[i][j] = v[i - 1][j];
				}
			}
		}
	}
	//输出一下 v 看看目前的情况
	for(int i =0; i < v.length;i++) {
		for(int j = 0; j < v[i].length;j++) {
			System.out.print(v[i][j] + " ");
		}
		System.out.println();
	}
System.out.println("============================");
	//输出最后我们是放入的哪些商品
	//遍历 path, 这样输出会把所有的放入情况都得到, 其实我们只需要最后的放入
	// for(int i = 0; i < path.length; i++) {
	// for(int j=0; j < path[i].length; j++) {
	// if(path[i][j] == 1) {
	// System.out.printf("第%d 个商品放入到背包\n", i);
	// }
	// }
	// }
	//动脑筋
	int i = path.length - 1; //行的最大下标
	int j = path[0].length - 1; //列的最大下标
	while(i > 0 && j > 0 ) { //从 path 的最后开始找
		if(path[i][j] == 1) {
			System.out.printf("第%d 个商品放入到背包\n", i);
			j -= w[i-1]; //w[i-1]
			}
				i--;
			}
		}
	}
}

4. KMP字符串查找算法(Knuth-Morris-Pratt)

KMP法算法概念
KMP法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次
回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间

KMP搜索算法需要: 原字符串,需要查找的字符串(子串)和 部分匹配表

部分匹配表概念:“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。
chapter5. Java数据结构与java算法_第88张图片

部分匹配表的代码:
核心**:while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j-1];
}**

字符串匹配问题:

  1. 有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”
  2. 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
  3. 要求:使用 KMP 算法完成判断,不能使用简单的暴力匹配算法
public class KMPAlgorithm {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String str1 = "BBC ABCDAB ABCDABCDABDE";
		String str2 = "ABCDABD";
		//String str2 = "BBC";
		int[] next = kmpNext("ABCDABD"); //[0, 1, 2, 0]
		System.out.println("next=" + Arrays.toString(next));
		int index = kmpSearch(str1, str2, next);
		System.out.println("index=" + index); // 15 了
		}
	//写出我们的 kmp 搜索算法
	/**
	*
	* @param str1 源字符串
	* @param str2 子串
	* @param next 部分匹配表, 是子串对应的部分匹配表
	* @return 如果是-1 就是没有匹配到,否则返回第一个匹配的位置
	*/
	public static int kmpSearch(String str1, String str2, int[] next) {
	//遍历
		for(int i = 0, j = 0; i < str1.length(); i++) {
		//需要处理 str1.charAt(i) != str2.charAt(j), 去调整 j 的大小
		//KMP 算法核心点, 可以验证... 
			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()) {//找到了 // j = 3 i
				return i - j + 1;
			}
		}
			return -1;
		}
//获取到一个字符串(子串) 的部分匹配值表
	public static int[] kmpNext(String dest) {
		//创建一个 next 数组保存部分匹配值
		int[] next = new int[dest.length()];
		next[0] = 0; //如果字符串是长度为 1 部分匹配值就是 0
		for(int i = 1, j = 0; i < dest.length(); i++) {
			//当 dest.charAt(i) != dest.charAt(j) ,我们需要从 next[j-1]获取新的 j
			//直到我们发现 有 dest.charAt(i) == dest.charAt(j)成立才退出
			**//这时 kmp 算法的核心点**
			while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
				j = next[j-1];
			}
			//当 dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值就是+1
			if(dest.charAt(i) == dest.charAt(j)) {
				j++;
			}
			next[i] = j;
		}
		return next;
	}
}

贪心算法Greedy

贪心就是每次选的都是最优的,但结果不一定是最优的,
就像每次都选覆盖最多城市的电台,但结果并非最优结果。
介绍

  1. 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
  2. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

应用案例:
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号

思路:
使用贪婪算法,效率高:

  1. 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
  2. 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
  3. 将这个电台加入到一个集合中(比如 ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
  4. 重复第 1 步直到覆盖了全部的地区

图解:
chapter5. Java数据结构与java算法_第89张图片
chapter5. Java数据结构与java算法_第90张图片

创建广播电台,放入Map

public static void main(String[] args) {
	//创建广播电台,放入到 Map <电台,电台可覆盖的地区>
	HashMap<String,HashSet<String>> broadcasts = new HashMap<String, HashSet<String>>();

	//先添加:电台可覆盖的地区
	HashSet<String> hashSet1 = new HashSet<String>();
	hashSet1.add("北京");
	hashSet1.add("上海");
	hashSet1.add("天津");
	
	HashSet<String> hashSet2 = new HashSet<String>();
	hashSet2.add("广州");
	hashSet2.add("北京");
	hashSet2.add("深圳");
	
	HashSet<String> hashSet3 = new HashSet<String>();
	hashSet3.add("成都");
	hashSet3.add("上海");
	hashSet3.add("杭州");
	
	HashSet<String> hashSet4 = new HashSet<String>();
	hashSet4.add("上海");
	hashSet4.add("天津");
	
	HashSet<String> hashSet5 = new HashSet<String>();
	hashSet5.add("杭州");
	hashSet5.add("大连");
	
	//把电台K1~k5 加入到 broadcasts map中
	broadcasts.put("K1", hashSet1);
	broadcasts.put("K2", hashSet2);
	broadcasts.put("K3", hashSet3);
	broadcasts.put("K4", hashSet4);
	broadcasts.put("K5", hashSet5);
	
	//allAreas 用于存放所有的地区
	HashSet<String> allAreas = new HashSet<String>();
	allAreas.add("北京");
	allAreas.add("上海");
	allAreas.add("天津");
	allAreas.add("广州");
	allAreas.add("深圳");
	allAreas.add("成都");
	allAreas.add("杭州");
	allAreas.add("大连");
	
	//创建 一个叫select的ArrayList, 用于存放选择的电台
	ArrayList<String> selects = new ArrayList<String>();
	
	//定义一个临时的集合, 在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集 即allAreas里的地区 和当前电台覆盖地区的交集
	HashSet<String> tempSet = new HashSet<String>();
	
	//定义给 maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的 key
	//如果 maxKey 不为 null , 则会加入到 selects
	String maxKey = null;
	while(allAreas.size() != 0) { // 如果 allAreas 不为 0, 则表示还没有覆盖到所有的地区(即allAreas 里还有其他地区)
	
	//每进行一次 while,需要maxKey清空
		maxKey = null;
	
	//遍历 broadcasts, 取出对应 key(k1~k5)
		for(String key : broadcasts.keySet()) {
		//每进行一次 for循环,要把tempSet清空
		tempSet.clear();
		
		//当前这个 key 能够覆盖的地区
		HashSet<String> areas = broadcasts.get(key);
		tempSet.addAll(areas);
		
		//求出 tempSet 和 allAreas 集合的**交集**, 交集会赋给 tempSet
		tempSet.retainAll(allAreas);
		
		//如果当前这个集合包含的未覆盖地区的数量,比 maxKey 指向的集合地区还多
		//就需要重置 maxKey
		// **tempSet.size() >broadcasts.get(maxKey).size()) 体现出贪心算法的特点,每次都选择最优的**
		if(tempSet.size() > 0 &&
		(maxKey == null || tempSet.size() >broadcasts.get(maxKey).size())){
			maxKey = key;
		}
	}
	//maxKey != null, 就应该将 maxKey 加入 selects
		if(maxKey != null) {
			selects.add(maxKey);
		//将 maxKey 指向的广播电台覆盖的地区,从 allAreas 去掉
			allAreas.removeAll(broadcasts.get(maxKey));
		}
	}
	System.out.println("得到的选择结果是" + selects);//[K1,K2,K3,K5]
	}
}

贪心算法注意事项和细节

  1. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
  2. 比如上题的算法选出的是 K1, K2, K3, K5,符合覆盖了全部的地区
  3. 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果 K2 的使用成本低于 K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的.

普利姆算法prim

普里姆算法介绍

1)普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图
2)普利姆的算法如下:
(1)设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
(2)若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
(3)若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
(4)重复步骤②,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
(5)提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.

普里姆算法最佳实践(修路问题)chapter5. Java数据结构与java算法_第91张图片

1)有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
2)各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
3)问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?

思路分析:
1)将10条边,连接即可,但是总的里程数不是最小.
2)正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.

最小生成树
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。
1)给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
2)N个顶点,一定有N-1条边
3)包含全部顶点
4)N-1条边都在图中
5)举例说明(如图:)
6)求最小生成树的算法主要是普里姆算法克鲁斯卡尔算法
chapter5. Java数据结构与java算法_第92张图片

你可能感兴趣的:(第一部分:java基础,数据结构,算法,java)