新增自主学习—数据结构与算法

pull/2/head
wuyou 4 years ago
parent a47476f9bb
commit a86774120c

@ -11,4 +11,5 @@
* [✍ 解决方案](src/Solution/README) * [✍ 解决方案](src/Solution/README)
* [✍ 开放性问题](src/Open/README) * [✍ 开放性问题](src/Open/README)
* [✍ 科班学习](src/university/README) * [✍ 科班学习](src/university/README)
* [✍ 自主学习](src/study/README)
* [✍ 其它](src/Others/README) * [✍ 其它](src/Others/README)

@ -11,4 +11,5 @@
* [✍ 解决方案](src/Solution/README) * [✍ 解决方案](src/Solution/README)
* [✍ 开放性问题](src/Open/README) * [✍ 开放性问题](src/Open/README)
* [✍ 科班学习](src/university/README) * [✍ 科班学习](src/university/README)
* [✍ 自主学习](src/study/README)
* [✍ 其它](src/Others/README) * [✍ 其它](src/Others/README)

@ -0,0 +1,340 @@
## ArrayList源码分析
转载链接https://blog.csdn.net/weixin_36378917/article/details/81812210
### ArrayList的数据结构
ArrayList的底层数据结构就是一个数组数组元素的类型为Object类型对ArrayList的所有操作底层都是基于数组的。
### ArrayList的线程安全性
> 对ArrayList进行添加元素的操作的时候是分两个步骤进行的
- 第一步先在object[size]的位置上存放需要添加的元素;
- 第二步将size的值增加1
由于这个过程在多线程的环境下是不能保证具有原子性的因此ArrayList在多线程的环境下是线程不安全的。
> 具体举例说明
- 在单线程运行的情况下如果Size = 0添加一个元素后此元素在位置 0而且Size=1
- 而如果是在多线程情况下,比如有两个线程:
- 线程 A 先将元素存放在位置0但是此时 CPU 调度线程A暂停线程 B 得到运行的机会
- 线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 注意哦我们假设的是添加一个元素是要两个步骤哦而线程A仅仅完成了步骤1所以线程B也将元素存放在位置0
- 然后线程A和线程B都继续运行都增 加 Size 的值。
- 现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0而Size却等于 2这就是“线程不安全”了
> 解决办法
如果非要在多线程的环境下使用ArrayList就需要保证它的线程安全性
- 第一使用synchronized关键字
- 第二可以用Collections类中的静态方法synchronizedList()对ArrayList进行调用即可。
### ArrayList的继承关系
- ArrayList继承AbstractList抽象父类
- 实现了List接口规定了List的操作规范
- RandomAccess可随机访问
- Cloneable可拷贝
- Serializable可序列化
### ArrayList的主要成员变量
- 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时类内部使用默认缺省时对象数组的容量大小为10
```java
private static final int DEFAULT_CAPACITY = 10
```
- 当ArrayList的构造方法中显示指出ArrayList的数组长度为0时类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组
```
private static final Object[] EMPTY_ELEMENTDATA = {};
```
- 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
```java
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
```
- ArrayList的底层数据结构只是一个对象数组用于存放实际元素并且被标记为transient也就意味着在序列化的时候此字段是不会被序列化的
```java
transient Object[] elemetData;
```
- 实际ArrayList中存放的元素的个数默认时为0个元素
```java
private int size;
```
- ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE 8
```java
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE 8;
```
- 源码
```java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 缺省容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 缺省空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组
transient Object[] elementData;
// 实际元素大小默认为0
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
```
### ArrayList的构造方法
#### 无参构造方法
对于无参构造方法将成员变量elementData的值设为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
```java
public ArrayList() {
// 无参构造函数,设置元素数组为空
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
```
#### int类型参数构造方法
参数为希望的ArrayList的数组的长度initialCapacity。首先要判断参数initialCapacity与0的大小关系
- 如果initialCapacity大于0则创建一个大小为initialCapacity的对象数组赋给elementData。
- 如果initialCapacity等于0则将EMPTY_ELEMENTDATA赋给elementData。
- 如果initialCapacity小于0抛出异常非法的容量
```java
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { // 初始容量大于0
this.elementData = new Object[initialCapacity]; // 初始化元素数组
} else if (initialCapacity == 0) { // 初始容量为0
this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
} else { // 初始容量小于0抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
```
#### Collection<? extends E>类型构造方法
- 第一步将参数中的集合转化为数组赋给elementData
- 第二步参数集合是否是空。通过比较size与第一步中的数组长度的大小。
- 第三步如果参数集合为空则设置元素数组为空即将EMPTY_ELEMENTDATA赋给elementData
- 第四步如果参数集合不为空接下来判断是否成功将参数集合转化为Object类型的数组如果转化成Object类型的数组成功则将数组进行复制转化为Object类型的数组。
```java
public ArrayList(Collection<? extends E> c) { // 集合参数构造函数
elementData = c.toArray(); // 转化为数组
if ((size = elementData.length) != 0) { // 参数为非空集合
if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
} else { // 集合大小为空,则设置元素数组为空
this.elementData = EMPTY_ELEMENTDATA;
}
}
```
### ArrayList的add()方法
在add()方法中主要完成了三件事首先确保能够将希望添加到集合中的元素能够添加到集合中即确保ArrayList的容量判断是否需要扩容然后将元素添加到elementData数组的指定位置最后将集合中实际的元素个数加1。
```java
public boolean add(E e) { // 添加元素
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
```
### ArrayList的扩容机制
ArrayList的扩容主要发生在向ArrayList集合中添加元素的时候。由add()方法的分析可知添加前必须确保集合的容量能够放下添加的元素。主要经历了以下几个阶段:
- 第一在add()方法中调用ensureCapacityInternal(size + 1)方法来确定集合确保添加元素成功的最小集合容量minCapacity的值。参数为size+1代表的含义是如果集合添加元素成功后集合中的实际元素个数。**换句话说集合为了确保添加元素成功那么集合的最小容量minCapacity应该是size+1。**在ensureCapacityInternal方法中首先判断elementData是否为默认的空数组如果是minCapacity为minCapacity与集合默认容量大小中的较大值。
```java
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断元素数组是否为空数组
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取较大值
}
ensureExplicitCapacity(minCapacity);
}
```
- 第二调用ensureExplicitCapacity(minCapacity)方法来确定集合为了确保添加元素成功是否需要对现有的元素数组进行扩容。
- 首先将结构性修改计数器加一;
- 然后判断minCapacity与当前元素数组的长度的大小如果minCapacity比当前元素数组的长度的大小大的时候需要扩容进入第三阶段。
```java
private void ensureExplicitCapacity(int minCapacity) {
// 结构性修改加1
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
```
- 第三如果需要对现有的元素数组进行扩容则调用grow(minCapacity)方法参数minCapacity表示集合为了确保添加元素成功的最小容量。在扩容的时候首先将原元素数组的长度增大1.5倍oldCapacity + (oldCapacity >> 1)然后对扩容后的容量与minCapacity进行比较
- ①新容量小于minCapacity则将新容量设为minCapacity
- ②新容量大于minCapacity则指定新容量。
- 最后将旧数组拷贝到扩容后的新数组中。
```java
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
if (newCapacity - minCapacity < 0) //
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大于最大容量
newCapacity = hugeCapacity(minCapacity); // 指定新容量
// 拷贝扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
```
![img](数据结构与算法/70.png)
![img](数据结构与算法/70-16397070745812.png)
![img](数据结构与算法/70-16397070895984.png)
### ArrayList的set(int index,E element)方法
set(int index, E element)方法的作用是指定下标索引处的元素的值。在ArrayList的源码实现中方法内首先判断传递的元素数组下标参数是否合法然后将原来的值取出设置为新的值将旧值作为返回值返回。
```java
public E set(int index, E element) {
// 检验索引是否合法
rangeCheck(index);
// 旧值
E oldValue = elementData(index);
// 赋新值
elementData[index] = element;
// 返回旧值
return oldValue;
}
```
### ArrayList的indexOf(Object o)方法
indexOf(Object o)方法的作用是从头开始查找与指定元素相等的元素,如果找到,则返回找到的元素在元素数组中的下标,如果没有找到返回-1。与该方法类似的是lastIndexOf(Object o)方法,该方法的作用是从尾部开始查找与指定元素相等的元素。
查看该方法的源码可知该方法从需要查找的元素是否为空的角度分为两种情况分别讨论。这也意味着该方法的参数可以是null元素也意味着ArrayList集合中能够保存null元素。方法实现的逻辑也比较简单直接循环遍历元素数组通过equals方法来判断对象是否相同相同就返回下标找不到就返回-1。这也解释了为什么要把情况分为需要查找的对象是否为空两种情况讨论不然的话空对象调用equals方法则会产生空指针异常。
```java
// 从首开始查找数组里面是否存在指定元素
public int indexOf(Object o) {
if (o == null) { // 查找的元素为空
for (int i = 0; i < size; i++) //
if (elementData[i]==null)
return i;
} else { // 查找的元素不为空
for (int i = 0; i < size; i++) //
if (o.equals(elementData[i]))
return i;
}
// 没有找到,返回空
return -1;
}
```
### ArrayList的get(int index)方法
get(int index)方法是返回指定下标处的元素的值。get函数会检查索引值是否合法只检查是否大于size而没有检查是否小于0。如果所引致合法则调用elementData(int index)方法获取值。在elementData(int index)方法中返回元素数组中指定下标的元素,并且对其进行了向下转型。
```java
public E get(int index) {
// 检验索引是否合法
rangeCheck(index);
return elementData(index);
}
```
```java
E elementData(int index) {
return (E) elementData[index];
}
```
### ArrayList的remove(int index)方法
remove(int index)方法的作用是删除指定下标的元素。在该方法的源码中将指定下标后面一位到数组末尾的全部元素向前移动一个单位并且把数组最后一个元素设置为null这样方便之后将整个数组不再使用时会被GC可以作为小技巧。而需要移动的元素个数为size-index-1。
```java
public E remove(int index) {
// 检查索引是否合法
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 赋值为空有利于进行GC
elementData[--size] = null;
// 返回旧值
return oldValue;
}
```
### ArrayList总结
> ArrayList的优点
- ArrayList底层以数组实现是一种随机访问模式再加上它实现了RandomAccess接口因此查找也就是get的时候非常快。
- ArrayList在顺序添加一个元素的时候非常方便只是往数组里面添加了一个元素而已。
- 根据下标遍历元素,效率高
- 根据下标访问元素,效率高
- 可以自动扩容默认为每次扩容为原来的1.5倍
> ArrayList的缺点
- 插入和删除元素的效率不高
- 根据元素的值查找元素的下标需要遍历整个元素数组,效率不高
- 线程不安全

File diff suppressed because it is too large Load Diff

@ -0,0 +1,498 @@
## 基本介绍
> 为什么要有图
线性表和树:
- 线性表局限于一个直接前驱和一个直接后继的关系
- 树也只能有一个直接前驱也就是父节点
- 当我们需要表示多对多的关系时, 这里我们就用到了图
> 图的举例说明
图是一种数据结构
- 其中结点可以具有零个或多个相邻元素
- 两个结点之间的连接称为边
- 结点也可以称为顶点
![image-20220112153125409](数据结构与算法/image-20220112153125409.png)
## 常用概念
- 顶点(vertex)
- 边(edge)
- 路径
- 路径: 比如从 D -> C 的路径有
1) D->B->C
2) D->A->B->C
- 无向图
- **无向图** 顶点之间的连接没有方向比如A-B,
即可以是 A -> B 也可以 B -> A
![image-20220112153338401](数据结构与算法/image-20220112153338401.png)
- 有向图
- **有向图** 顶点之间的连接有方向比如A-B只能是 A-> B 不能是 B->A
![image-20220112153600907](数据结构与算法/image-20220112153600907.png)
- 带权图
- **带权图**:这种边带权值的图也叫网
![image-20220112153640279](数据结构与算法/image-20220112153640279.png)
## 图的表示方式
图的表示方式有两种:
- 二维数组表示(邻接矩阵)
- 链表表示(邻接表)
> 邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵对于n个顶点的图而言矩阵是的row和col表示的是1....n个点
![image-20220112154031167](数据结构与算法/image-20220112154031167.png)
> 邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间其实有很多边都是不存在会造成空间的一定损失
- 邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费,邻接表由数组+链表组成
![image-20220112165003746](数据结构与算法/image-20220112165003746.png)
## 快速入门案例
![image-20220112165336530](数据结构与算法/image-20220112165336530.png)
## 遍历介绍
所谓图的遍历,即是对结点的访问
一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:
- 深度优先遍历
- 广度优先遍历
### 深度优先遍历
> 图的深度优先搜索(Depth First Search)
- 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:**每次都在访问完当前结点后首先访问当前结点的第一个邻接结点**
- 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问
- 深度优先搜索是一个递归的过程
> 深度优先遍历算法步骤
1. 访问初始结点v并标记结点v为已访问
2. 查找结点v的第一个邻接结点w
3. 若w存在则继续执行4如果w不存在则回到第1步将从v的下一个结点继续
4. 若w未被访问对w进行深度优先遍历递归即把w当做另一个v然后进行步骤123
5. 查找结点v的w邻接结点的下一个邻接结点转到步骤3
### 深度优先搜索(DFS)
深度优先搜索Depth-First Search / DFS是一种**优先遍历子节点**而不是回溯的算法。
![深度优先搜索](数据结构与算法/深度优先搜索.jpg)
**DFS解决的是连通性的问题**。即给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能从起点连接到终点。起点和终点,也可以指的是某种起始状态和最终的状态。问题的要求并不在乎路径是长还是短,只在乎有还是没有。
#### 代码实现
```java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.*;
/**
* Depth-First Search(DFS)
* <p>
* 从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。
* <p>
* 数据结构:栈
* 父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点即可
*
* @author lry
*/
public class DepthFirstSearch {
/**
* 树节点
*
* @param <V>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TreeNode<V> {
private V value;
private List<TreeNode<V>> childList;
/**
* 二叉树节点支持如下
* @return
*/
public TreeNode<V> getLeft() {
if (childList == null || childList.isEmpty()) {
return null;
}
return childList.get(0);
}
public TreeNode<V> getRight() {
if (childList == null || childList.isEmpty()) {
return null;
}
return childList.get(1);
}
}
/**
* 模型:
* .......A
* ...../ \
* ....B C
* .../ \ / \
* ..D E F G
* ./ \ / \
* H I J K
*/
public static void main(String[] args) {
TreeNode<String> treeNodeA = new TreeNode<>("A", new ArrayList<>());
TreeNode<String> treeNodeB = new TreeNode<>("B", new ArrayList<>());
TreeNode<String> treeNodeC = new TreeNode<>("C", new ArrayList<>());
TreeNode<String> treeNodeD = new TreeNode<>("D", new ArrayList<>());
TreeNode<String> treeNodeE = new TreeNode<>("E", new ArrayList<>());
TreeNode<String> treeNodeF = new TreeNode<>("F", new ArrayList<>());
TreeNode<String> treeNodeG = new TreeNode<>("G", new ArrayList<>());
TreeNode<String> treeNodeH = new TreeNode<>("H", new ArrayList<>());
TreeNode<String> treeNodeI = new TreeNode<>("I", new ArrayList<>());
TreeNode<String> treeNodeJ = new TreeNode<>("J", new ArrayList<>());
TreeNode<String> treeNodeK = new TreeNode<>("K", new ArrayList<>());
// A->B,C
treeNodeA.getChildList().add(treeNodeB);
treeNodeA.getChildList().add(treeNodeC);
// B->D,E
treeNodeB.getChildList().add(treeNodeD);
treeNodeB.getChildList().add(treeNodeE);
// C->F,G
treeNodeC.getChildList().add(treeNodeF);
treeNodeC.getChildList().add(treeNodeG);
// D->H,I
treeNodeD.getChildList().add(treeNodeH);
treeNodeD.getChildList().add(treeNodeI);
// G->J,K
treeNodeG.getChildList().add(treeNodeJ);
treeNodeG.getChildList().add(treeNodeK);
System.out.println("非递归方式");
dfsNotRecursive(treeNodeA);
System.out.println();
System.out.println("前续遍历");
dfsPreOrderTraversal(treeNodeA, 0);
System.out.println();
System.out.println("后续遍历");
dfsPostOrderTraversal(treeNodeA, 0);
System.out.println();
System.out.println("中续遍历");
dfsInOrderTraversal(treeNodeA, 0);
}
/**
* 非递归方式
*
* @param tree
* @param <V>
*/
public static <V> void dfsNotRecursive(TreeNode<V> tree) {
if (tree != null) {
// 次数之所以用 Map 只是为了保存节点的深度,如果没有这个需求可以改为 Stack<TreeNode<V>>
Stack<Map<TreeNode<V>, Integer>> stack = new Stack<>();
Map<TreeNode<V>, Integer> root = new HashMap<>();
root.put(tree, 0);
stack.push(root);
while (!stack.isEmpty()) {
Map<TreeNode<V>, Integer> item = stack.pop();
TreeNode<V> node = item.keySet().iterator().next();
int depth = item.get(node);
// 打印节点值以及深度
System.out.print("-->[" + node.getValue().toString() + "," + depth + "]");
if (node.getChildList() != null && !node.getChildList().isEmpty()) {
for (TreeNode<V> treeNode : node.getChildList()) {
Map<TreeNode<V>, Integer> map = new HashMap<>();
map.put(treeNode, depth + 1);
stack.push(map);
}
}
}
}
}
/**
* 递归前序遍历方式
* <p>
* 前序遍历(Pre-Order Traversal) :指先访问根,然后访问子树的遍历方式,二叉树则为:根->左->右
*
* @param tree
* @param depth
* @param <V>
*/
public static <V> void dfsPreOrderTraversal(TreeNode<V> tree, int depth) {
if (tree != null) {
// 打印节点值以及深度
System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]");
if (tree.getChildList() != null && !tree.getChildList().isEmpty()) {
for (TreeNode<V> item : tree.getChildList()) {
dfsPreOrderTraversal(item, depth + 1);
}
}
}
}
/**
* 递归后序遍历方式
* <p>
* 后序遍历(Post-Order Traversal):指先访问子树,然后访问根的遍历方式,二叉树则为:左->右->根
*
* @param tree
* @param depth
* @param <V>
*/
public static <V> void dfsPostOrderTraversal(TreeNode<V> tree, int depth) {
if (tree != null) {
if (tree.getChildList() != null && !tree.getChildList().isEmpty()) {
for (TreeNode<V> item : tree.getChildList()) {
dfsPostOrderTraversal(item, depth + 1);
}
}
// 打印节点值以及深度
System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]");
}
}
/**
* 递归中序遍历方式
* <p>
* 中序遍历(In-Order Traversal):指先访问左(右)子树,然后访问根,最后访问右(左)子树的遍历方式,二叉树则为:左->根->右
*
* @param tree
* @param depth
* @param <V>
*/
public static <V> void dfsInOrderTraversal(TreeNode<V> tree, int depth) {
if (tree.getLeft() != null) {
dfsInOrderTraversal(tree.getLeft(), depth + 1);
}
// 打印节点值以及深度
System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]");
if (tree.getRight() != null) {
dfsInOrderTraversal(tree.getRight(), depth + 1);
}
}
}
```
#### 输出结果
```
非递归方式
-->[A,0]-->[C,1]-->[G,2]-->[K,3]-->[J,3]-->[F,2]-->[B,1]-->[E,2]-->[D,2]-->[I,3]-->[H,3]
前续遍历
-->[A,0]-->[B,1]-->[D,2]-->[H,3]-->[I,3]-->[E,2]-->[C,1]-->[F,2]-->[G,2]-->[J,3]-->[K,3]
后续遍历
-->[H,3]-->[I,3]-->[D,2]-->[E,2]-->[B,1]-->[F,2]-->[J,3]-->[K,3]-->[G,2]-->[C,1]-->[A,0]
中续遍历
-->[H,3]-->[D,2]-->[I,3]-->[B,1]-->[E,2]-->[A,0]-->[F,2]-->[C,1]-->[J,3]-->[G,2]-->[K,3]
```
### 广度优先遍历
> 图的广度优先搜索(Broad First Search)
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
> 广度优先遍历算法步骤
1. 访问初始结点v并标记结点v为已访问
2. 结点v入队列
3. 当队列非空时,继续执行,否则算法结束
4. 出队列取得队头结点u
5. 查找结点u的第一个邻接结点w
6. 若结点u的邻接结点w不存在则转到步骤3否则循环执行以下三个步骤
1. 若结点w尚未被访问则访问结点w并标记为已访问
2. 结点w入队列
3. 查找结点u的继w邻接结点后的下一个邻接结点w转到步骤6
### 广度优先搜索(BFS)
广度优先搜索Breadth-First Search / BFS是**优先遍历邻居节点**而不是子节点的图遍历算法。
![广度优先搜索](数据结构与算法/广度优先搜索.jpg)
**BFS一般用来解决最短路径的问题**。和深度优先搜索不同,广度优先的搜索是从起始点出发,一层一层地进行,每层当中的点距离起始点的步数都是相同的,当找到了目的地之后就可以立即结束。广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS。这种算法往往可以大大地提高搜索的效率。
#### 代码实现
```java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.*;
/**
* Breadth-First Search(BFS)
* <p>
* 从根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。
* <p>
* 数据结构:队列
* 父节点入队,父节点出队列,先左子节点入队,后右子节点入队。递归遍历全部节点即可
*
* @author lry
*/
public class BreadthFirstSearch {
/**
* 树节点
*
* @param <V>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TreeNode<V> {
private V value;
private List<TreeNode<V>> childList;
}
/**
* 模型:
* .......A
* ...../ \
* ....B C
* .../ \ / \
* ..D E F G
* ./ \ / \
* H I J K
*/
public static void main(String[] args) {
TreeNode<String> treeNodeA = new TreeNode<>("A", new ArrayList<>());
TreeNode<String> treeNodeB = new TreeNode<>("B", new ArrayList<>());
TreeNode<String> treeNodeC = new TreeNode<>("C", new ArrayList<>());
TreeNode<String> treeNodeD = new TreeNode<>("D", new ArrayList<>());
TreeNode<String> treeNodeE = new TreeNode<>("E", new ArrayList<>());
TreeNode<String> treeNodeF = new TreeNode<>("F", new ArrayList<>());
TreeNode<String> treeNodeG = new TreeNode<>("G", new ArrayList<>());
TreeNode<String> treeNodeH = new TreeNode<>("H", new ArrayList<>());
TreeNode<String> treeNodeI = new TreeNode<>("I", new ArrayList<>());
TreeNode<String> treeNodeJ = new TreeNode<>("J", new ArrayList<>());
TreeNode<String> treeNodeK = new TreeNode<>("K", new ArrayList<>());
// A->B,C
treeNodeA.getChildList().add(treeNodeB);
treeNodeA.getChildList().add(treeNodeC);
// B->D,E
treeNodeB.getChildList().add(treeNodeD);
treeNodeB.getChildList().add(treeNodeE);
// C->F,G
treeNodeC.getChildList().add(treeNodeF);
treeNodeC.getChildList().add(treeNodeG);
// D->H,I
treeNodeD.getChildList().add(treeNodeH);
treeNodeD.getChildList().add(treeNodeI);
// G->J,K
treeNodeG.getChildList().add(treeNodeJ);
treeNodeG.getChildList().add(treeNodeK);
System.out.println("递归方式");
bfsRecursive(Arrays.asList(treeNodeA), 0);
System.out.println();
System.out.println("非递归方式");
bfsNotRecursive(treeNodeA);
}
/**
* 递归遍历
*
* @param children
* @param depth
* @param <V>
*/
public static <V> void bfsRecursive(List<TreeNode<V>> children, int depth) {
List<TreeNode<V>> thisChildren, allChildren = new ArrayList<>();
for (TreeNode<V> child : children) {
// 打印节点值以及深度
System.out.print("-->[" + child.getValue().toString() + "," + depth + "]");
thisChildren = child.getChildList();
if (thisChildren != null && thisChildren.size() > 0) {
allChildren.addAll(thisChildren);
}
}
if (allChildren.size() > 0) {
bfsRecursive(allChildren, depth + 1);
}
}
/**
* 非递归遍历
*
* @param tree
* @param <V>
*/
public static <V> void bfsNotRecursive(TreeNode<V> tree) {
if (tree != null) {
// 跟上面一样,使用 Map 也只是为了保存树的深度,没这个需要可以不用 Map
Queue<Map<TreeNode<V>, Integer>> queue = new ArrayDeque<>();
Map<TreeNode<V>, Integer> root = new HashMap<>();
root.put(tree, 0);
queue.offer(root);
while (!queue.isEmpty()) {
Map<TreeNode<V>, Integer> itemMap = queue.poll();
TreeNode<V> node = itemMap.keySet().iterator().next();
int depth = itemMap.get(node);
//打印节点值以及深度
System.out.print("-->[" + node.getValue().toString() + "," + depth + "]");
if (node.getChildList() != null && !node.getChildList().isEmpty()) {
for (TreeNode<V> child : node.getChildList()) {
Map<TreeNode<V>, Integer> map = new HashMap<>();
map.put(child, depth + 1);
queue.offer(map);
}
}
}
}
}
}
```
#### 输出结果
```
递归方式
-->[A,0]-->[B,1]-->[C,1]-->[D,2]-->[E,2]-->[F,2]-->[G,2]-->[H,3]-->[I,3]-->[J,3]-->[K,3]
非递归方式
-->[A,0]-->[B,1]-->[C,1]-->[D,2]-->[E,2]-->[F,2]-->[G,2]-->[H,3]-->[I,3]-->[J,3]-->[K,3]
```

File diff suppressed because it is too large Load Diff

@ -0,0 +1,71 @@
## 介绍
> 数据结构与算法的重要性
算法是程序的灵魂,优秀的程序可以在海量数据计算的时候,依然保持高速计算。
一般来讲程序使用了内存计算框架比如Spark和缓存技术比如Redis来优化程序再深入思考一下这些计算框架和缓存技术它的核心功能就是数据结构与算法。
拿实际工作经历说在Unix下开发服务器程序功能是要支持上千万人同时在线在上线前进行测试都OK可是上线后服务器就撑不住了公司的CTO对代码进行优化再次上线就坚如磐石。这是程序的灵魂——算法。
> 数据结构和算法的关系
- 数据data结构structure是一门研究组织数据方式的学科有了编程语言也就有了数据结构学号数据结构可以编写出更加漂亮更加有效率的代码。
- 要学好数据结构就要多多考虑如何将生活中遇到的问题,用程序去解决。
- 程序 = 数据结构 + 算法
- 数据结构是算法的基础,换言之,想要学好算法,就要把数据结构学到位
## 实际编程中遇到的问题
- 封装好的API
Java代码
```java
public static void main(String[] args) {
// write your code here
String str = "hello,world!hello,view6view!";
String newStr = str.replaceAll("hello","你好");
System.out.println(newStr);
}
```
试写出用单链表表示的字符串类以及字符串结点类的定义并依次实现它的构造函数、以及计算长度、串赋值判断两串相等、求子串、两串连接、求字串在串中位置等7个成员函数。
- 游戏逻辑设定
一个五子棋程序:
![img](数据结构与算法\J@HZIQS0B_BPA99GK$IX_6J.png)
如何判断优秀的输赢,并可以完成存盘退出和继续上局的功能棋盘—>二维数组—>稀疏数组—>写入文件【存档功能】—>读取文件—>稀疏数组—>二维数组—>棋盘【接上局】
- 约瑟夫(Josephu)问题
Josephu问题为设编号为123....n的n个人围坐一圈约定编号为K1<=k<=n的人从1开始报数数到m的那个人出列他的下一位又从1开始报数数到m的那个人又出列依次类推知道所有人出列为止由此产生一个出队编号的序列。
提示用一个不带头节点的循环列表来处理Josephu问题先构成一个由n个结点的单循环链表单向循环列表然后由k结点从1开始计数。计到m时对应结点从链表中删除然后再从被删除结点的下一个删除结点又开始从1开始技术直到最后一个结点从链表中删除算法结束。
- 其它常见问题
修路问题 —> 最小生成树(数+ 普利姆算法)
最短路径问题 —> 图 + 弗洛伊德算法
汉诺塔问题 —> 分治算法
八皇后问题 —>回溯法
## 线性结构和非线性结构
> 线性结构
- 线性结构作为最常用的数据结构,其特点是**数据元素之间存在一对一**的线性关系。
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储结构的线性表称为顺序表,顺序表中的存储元素是连续的。
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
- 线性结构常见的有:数组、队列、链表和栈,后面我会相信讲解。
> 非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构

@ -0,0 +1,548 @@
## 稀疏数组
> 基本介绍
当一个数组中大部分元素为0或者为同一个值的数组时可以用稀疏数组来保存该数组。
> 稀疏数组的处理方法是
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模数组中,从而缩小程序的规模
![image-20211128120651256](数据结构与算法\image-20211128120651256.png)
> 稀疏数组转换思路的分析
- 二维数组转稀疏数组的思路
1. 便利原始的二维数组得到有效数据的个数sum
2. 根据sum就可以创建稀疏数组`Integer sparseArr[][] = new Integer[sum+1][3] `
3. 将二维数组的有效数据存入到稀疏数组中
- 稀疏数组转原始的二维数组的思路
1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如`Integer[11][11] chessArr2`
2. 再读取稀疏数组后几行的数据,并赋值给原始的二维数组即可
![image-20211128121059816](数据结构与算法\image-20211128121059816.png)
> 代码实现
代码:
```java
public class SparseArray {
public static void main(String[] args) {
// 创建一个原始的二维数组 11 * 11
// 0表示没有棋子1表示黑子2表示篮子
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][4] = 2;
// 输出原始的二维数组
System.out.println("原始的二维数组~~");
printArray(chessArr1);
// 将二维数组 转 稀疏数组
// 1.先遍历二维数组,得到非零数据的个数
int sum = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++){
if(chessArr1[i][j] != 0){
sum++;
}
}
}
System.out.println("sum=" + sum);
// 2.创建对应的稀疏数组
int sparseArr[][] = new int[sum+1][3];
// 3.给稀疏数组赋值
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
// 3.遍历二维数组将非0的值存放在sparseArr中
int count = 0; // count 用于记录是第几个非0数据
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if(chessArr1[i][j] != 0){
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr1[i][j];
}
}
}
// 4.输出稀疏数组的形式
System.out.println("\n得到的稀疏数组");
for (int i = 0; i < sparseArr.length; i++){
System.out.printf("%d\t%d\t%d\t\n",sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
}
System.out.println();
// 将稀疏数组恢复成原始数组
// 1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
// 2.读取稀疏数组的后几行数据(从第二行开始),并复制给原始的二维数组即可
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
// 3.输出恢复后的二维数组
System.out.println("恢复后的二维数组~~");
printArray(chessArr2);
}
// 打印棋盘的方法
public static void printArray(int[][] array){
for (int[] row : array){
for (int data : row){
System.out.printf("%d\t", data);
}
System.out.println();
}
}
}
```
控制台输出结果:
```
原始的二维数组~~
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 2 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
sum=2
得到的稀疏数组
11 11 2
1 2 1
2 4 2
恢复后的二维数组~~
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 2 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
```
> 课后练习
1. 在代码实现的的基础上将稀疏数组保存到磁盘上比如map.data
2. 恢复原来的数组读取map.data进行恢复
编写两个函数就可以实现了
```java
/**
* 存储稀疏数组,相邻数据使用\t划分
* @param classpath 文件的存放路径
* @param sparseArr 稀疏数组对象
*/
public static void save(String classpath, int[][] sparseArr){
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(classpath);
for (int[] row : sparseArr){
fileWriter.write(row[0] + "\t" + row[1] + "\t" + row[2]);
fileWriter.write("\r\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读取二维稀疏数组,相邻数据使用\t划分
* @param classpath 文件的存放路径
* @return 二维的稀疏数组
*/
public static int[][] read(String classpath){
int[][] sparseArr = null;
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader(classpath));
String lineStr = null;
Integer lineCount = 0;
while ((lineStr = bufferedReader.readLine()) != null){
String[] tempStr = lineStr.split("\t");
if (lineCount.equals(0)){
// 稀疏数组的[0,2]位置记录了非0数据个数所以稀疏数组大小为[Integer.parseInt(tempStr[2]) + 1][3]
sparseArr = new int[Integer.parseInt(tempStr[2]) + 1][3];
}
sparseArr[lineCount][0] = Integer.parseInt(tempStr[0]);
sparseArr[lineCount][1] = Integer.parseInt(tempStr[1]);
sparseArr[lineCount][2] = Integer.parseInt(tempStr[2]);
lineCount++;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sparseArr;
}
```
## 队列
### 普通实现
> 应用场景
![image-20211128140347935](数据结构与算法\image-20211128140347935.png)
- 队列是一个有序列表,可以用**数组**或是**链表**实现
- 遵循**先入先出**的原则。即:先存入队列的数据,要先取出。后存入的要后取出。
> 使用数组模拟队列示意图
- 队列本身也是有序列表若使用数组的结构来存储队列的数据则队列数组的声明如下图其中MaxSize为队列的最大容量
- 因为队列的输出输入是分别从前后端来处理,因此需要两个变量**front和rear分别记录前后端的下标**front会随着数据输出而改变而rear会随着数据输入而改变
![img](file:///C:\Users\22130\AppData\Roaming\Tencent\Users\2213093478\TIM\WinTemp\RichOle\A84~7D70~`L7PZFPK51G`F3.png)
> 数组模拟队列实现
当我们将数据存入队列时称为”addQueue”addQueue 的处理需要有两个步骤:思路分析
1) 将尾指针往后移rear + 1 , 当 rear == front 【空】
2) 若尾指针 rear 小于队列的最大下标 MaxSize-1则将数据存入 rear 所指的数组元素中,否则无法存入数据。
reatr == MaxSize - 1[队列满]
> 代码实现
```java
public class ArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列
ArrayQueue arrayQueue = new ArrayQueue(3);
char key = ' '; // 接受用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列取数据");
System.out.println("h(head):查看队列头的数据");
while (loop) {
key = scanner.next().charAt(0); // 接收第一个字符
switch (key) {
case 's':
arrayQueue.showQueue();
break;
case 'a':
System.out.println("输入一个数:");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':
try {
int res = arrayQueue.getQueue();
System.out.printf("取出的数据是%d\n", res);
continue;
} catch (RuntimeException e){
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = arrayQueue.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (RuntimeException e){
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
// 使用数组模拟队列 - 编写一个ArrayQueue类非循环队列只能使用一次
class ArrayQueue {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据,模拟队列
// 创建队列的构造器
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
front = -1; // 指向队列头部,指向队列头部的数据的前一个位置
rear = -1; // 指向队列尾,指向队列尾部的数据
}
// 判断队列是否满
public boolean isFull() {
// 例如最大容量为5rear是指向队列尾部数据所以rear为4maxSize - 1的时候就为满了
return rear == maxSize - 1;
}
// 判断队列是否为空
public boolean isEmpty() {
// 因为不是循环队列,头尾不相连,所以rear == front 时队列就为空
return rear == front;
}
// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()){
System.out.println("队列满,不能加入数据~~");
return;
}
rear++;
arr[rear] = n;
}
// 数据出队列
public int getQueue(){
// 判断队列是否为空
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列为空,不能取数据~~");
}
front++;
return arr[front];
}
// 显示队列的所有数据
public void showQueue() {
// 遍历
if(isEmpty()){
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d] = %d\n", i, arr[i]);
}
}
// 显示队列的头数据,不是取数据而仅仅是显示
public int headQueue(){
// 判断队列是否为空
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列为空,没有数据~~");
}
return arr[front + 1];
}
}
```
> 此种方式存在的问题和优化方案
- 数组使用一次就不能使用了,没有达到复用的效果
- 使用算法,改成一个环形的队列:取模%
### 环形队列
> 三种队列的声明方式:
- front/head为头部数据的下标rear/tail为尾部数据的下标
![image-20211128153106723](数据结构与算法/image-20211128153106723.png)
- front为头部数据的下标的前一个位置rear为尾部数据的下标
![image-20211128153235225](数据结构与算法/image-20211128153235225.png)
- front为头部数据的下标rear为尾部数据的下标的后一个位置
![image-20211128153539290](数据结构与算法/image-20211128153539290.png)
**三种声明方式大同小异,思想都是相似的,此处以第三种实现方式为例**
> 分析
- front就指向队列的头部元素也就是说arr[front]就是队列的第一个元素front的初始值 = 0
- rear指向队列的尾部元素的后一个位置因为希望空出一个空间做为约定rear的初始值=0
- 当队列满时,条件是 `(rear +1) % maxSize= front` 【满】
- 对队列为空的条件,`rear == front` 【空】
- 队列中有效的数据的个数 `(rear + maxSize - front) % maxSize` 【rear=1 front=0元素个数为1】
- 我们就可以在原来的队列上修改得到一个环形队列
> 代码实现
```java
public class CircleArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列
CircleArray circleArray = new CircleArray(4); // 其队列的有效数据最大是3
char key = ' '; // 接受用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列取数据");
System.out.println("h(head):查看队列头的数据");
while (loop) {
key = scanner.next().charAt(0); // 接收第一个字符
switch (key) {
case 's':
circleArray.showQueue();
break;
case 'a':
System.out.println("输入一个数:");
int value = scanner.nextInt();
circleArray.addQueue(value);
break;
case 'g':
try {
int res = circleArray.getQueue();
System.out.printf("取出的数据是%d\n", res);
continue;
} catch (RuntimeException e){
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = circleArray.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (RuntimeException e){
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
class CircleArray {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头的下标初始值为0
private int rear; // 队列尾的下标的后一个位置,初始值为0
private int[] arr; // 该数据用于存放数据,模拟队列
// 创建队列的构造器
public CircleArray(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
front = 0; // 指向队列头部,指向队列头部的数据
rear = 0; // 指向队列尾,指向队列尾部的数据的后一个位置
}
// 判断队列是否满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 判断队列是否为空
public boolean isEmpty() {
// 因为不是循环队列,头尾不相连,所以rear == front 时队列就为空
return rear == front;
}
// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()){
System.out.println("队列满,不能加入数据~~");
return;
}
// 直接将数据加入
arr[rear] = n;
// 将 rear 后移 ,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
// 数据出队列
public int getQueue(){
// 判断队列是否为空
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列为空,不能取数据~~");
}
// front是指向队列的第一个元素
// 1.先把front对应的值保留到一个临时变量,如果考虑释放要设置arr[front] = 0
int value = arr[front];
// 2.将front后移考虑取模
front = (front + 1) % maxSize;
// 3.将临时保存的变量返回
return value;
}
// 显示队列的所有数据
public void showQueue() {
// 遍历
if(isEmpty()){
System.out.println("队列空的,没有数据~~");
return;
}
// 从front开始遍历遍历多少个元素
for (int i = front; i < front + size(); i++) {
int index = i % maxSize; // 加上size之后可能会超过数组范围需要进行取模
System.out.printf("arr[%d]=%d\n", index, arr[index]);
}
}
// 求出当前队列的有效数据个数
public int size(){
// 这里 + maxSize保证分子是正数也可以使用Math.abs(rear - front)
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据,不是取数据而仅仅是显示
public int headQueue(){
// 判断队列是否为空
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列为空,没有数据~~");
}
return arr[front];
}
}
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,553 @@
## 介绍
> 递归应用场景
看个实际应用场景,迷宫问题(回溯) 递归(Recursion)
> 递归的概念
简单的说:递归就是**方法自己调用自己**,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题。同时可以让代码变得简洁。
![See the source image](数据结构与算法/v2-36b0cfb3e79fd37d3589ea2a6ab50c35_b.jpg)
> 递归能解决什么样的问题呢
- 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题—>递归代码比较简洁
> 递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的不会相互影响比如n变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据
- 递归必须向退出递归的条件逼近否则就是无限递归出现StackOverflowError
- 当一个方法执行完毕或者遇到return就会返回遵守谁调用就将结果返回给谁同时当方法执行完毕或者返回时该方法也就执行完毕
## 递归—迷宫问题
说明:
- 小球得到的路径,和程序员设置的找路策略有关即:找路的**上下左右**的顺序相关
- 再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
- 测试回溯现象
- 思考**: **如何求出最短路径?
![See the source image](数据结构与算法/0-1577411144.gif)
> 代码实现
```java
/**
* @author wuyou
*/
public class Maze {
public static void main(String[] args) {
// 先创建一个二维数组,模拟迷宫
int[][] map = new int[8][7];
// 使用1表示墙上下边界全部置为1
for (int i = 0; i < map.length; i++) {
if (i == 0 || i == map.length - 1) {
for (int j = 0; j < map[i].length; j++) {
map[i][j] = 1;
}
} else {
map[i][0] = 1;
map[i][map[i].length - 1] = 1;
}
}
// 设置挡板1表示
map[3][1] = 1;
map[3][2] = 1;
// 打印地图
System.out.println("原本地图的情况");
printMap(map);
// 使用递归回溯
setWay(map, 1, 1);
System.out.println("小球走过并且标识的地图的情况:");
printMap(map);
}
/**
* 终点为右下角位置
* 当map[i][j]为0时表示没有走过
* 当map[i][j]为1时表示墙
* 当map[i][j]为2时表示通路可以走
* 当map[i][j]为3时表示该点走不通
* 在走迷宫时,需要确定一个测略(方法) 下 -> 右 -> 上 -> 左 如果该点走不通,再回溯
* @param map 地图
* @param i 出发点x坐标
* @param j 出发点y坐标
* @return
*/
public static boolean setWay(int[][] map, int i, int j) {
if (map[map.length - 2][map[map.length - 2].length - 2] == 2) {
// 说明通路已经找到
return true;
} else {
// 说明当前这个点还没有走过
if (map[i][j] == 0) {
// 按照策略 下 -> 右 -> 上 -> 左
// 假定该店是可以走通的
map[i][j] = 2;
// 向下走
if (setWay(map, i + 1, j)) {
return true;
}
// 向右走
else if (setWay(map, i ,j +1)) {
return true;
}
// 向上走
else if (setWay(map, i - 1, j)) {
return true;
}
// 向左走
else if (setWay(map, i - 1, j)) {
return true;
}
// 说明该点走不通,是思路,不过既然上下左右都走不通,那么这点不会经过的
else {
map[i][j] = 3;
return false;
}
}
// 说明该节点可能是1 2 32的出现是因为迷宫问题不会走重复路不然会绕圈
else {
return false;
}
}
}
/**
* 打印地图
* @param map 二维地图
*/
public static void printMap(int[][] map) {
// 打印地图
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
```
输出结果:
```
原本地图的情况
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
小球走过并且标识的地图的情况:
1 1 1 1 1 1 1
1 2 0 0 0 0 1
1 2 2 2 0 0 1
1 1 1 2 0 0 1
1 0 0 2 0 0 1
1 0 0 2 0 0 1
1 0 0 2 2 2 1
1 1 1 1 1 1 1
```
## BFS—最短迷宫问题
![See the source image](数据结构与算法/211727_72229.png)
> 算法的基本思路
常常我们有这样一个问题:从一个起点开始要到一个终点,我们要找寻一条最短的路径。
我们采用示例图来说明这个过程,在搜索的过程中,初始所有节点是**白色(代表了所有点都还没开始搜索)**把起点V0标志成**灰色表示即将辐射V0**,下一步搜索的时候,我们把所有的灰色节点访问一次,然后将其变成**黑色(表示已经被辐射过了)**,进而再将他们所能到达的节点标志成**灰色(因为那些节点是下一步搜索的目标点了)**但是这里有个判断就像刚刚的例子当访问到V1节点的时候它的下一个节点应该是V0和V4但是V0已经在前面被染成黑色了所以不会将它染灰色。这样持续下去直到目标节点V6被染灰色说明了下一步就到终点了没必要再搜索染色其他节点了此时可以结束搜索了整个搜索就结束了。然后根据搜索过程反过来把最短路径找出来下图中把最终路径上的节点标志成绿色。
> 整个过程如下图所示:
1. 初始全部都是白色(未访问)
![image-20211214103714508](数据结构与算法/image-20211214103714508.png)
2. 即将搜索起点V0灰色
![image-20211214103745652](数据结构与算法/image-20211214103745652.png)
3. 已搜索V0即将搜索V1、V2、V3
![image-20211214103828272](数据结构与算法/image-20211214103828272.png)
4. 终点V6被染灰色终止
![image-20211214103910207](数据结构与算法/image-20211214103910207.png)
5. 找到最短路径
![image-20211214103952075](数据结构与算法/image-20211214103952075.png)
> 广度优先搜索流程图
![image-20211214103633902](数据结构与算法/image-20211214103633902.png)
> 代码实现
```java
/**
*
* @author wuyou
*/
public class BFSMaze {
public static void main(String[] args) {
char[][] ditu = new char[10][10];
for (int i = 0; i < 10; i++) {
Arrays.fill(ditu[i], 'O');
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
ditu[0][i] = '#';
ditu[9][i] = '#';
ditu[i][0] = '#';
ditu[i][9] = '#';
}
}
// 创建围墙
ditu[1][3] = ditu[1][7] = ditu[2][3] = ditu[2][7] = '#';
ditu[3][5] = '#';
ditu[4][2] = ditu[4][3] = ditu[4][4] = '#';
ditu[5][4] = ditu[6][2] = ditu[6][6] = '#';
ditu[7][2] = ditu[7][3] = ditu[7][4] = ditu[7][6] = ditu[7][7] = '#';
ditu[8][1] = ditu[8][2] = '#';
System.out.println("迷宫地形图:");
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.print(ditu[i][j] + " ");
}
System.out.println();
}
// 设置起始点与终点。
int[] start = new int[]{1, 1};
int[] end = new int[]{1, 8};
int[][] d = bfs(ditu, start, end);
System.out.println("该地图最短路径长为:" + d[end[0]][end[1]]);
System.out.println("---上帝视角迷宫最短路径地形图:---");
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
// 有步数的情况下打印步数,没有步数的情况下打印对应的字符
if (d[i][j] > 0 && d[i][j] < Integer.MAX_VALUE) {
System.out.printf("%-5d", d[i][j]);
} else {
System.out.printf("%-5c", ditu[i][j]);
}
}
System.out.println();
}
}
/**
* bfs算法得到最端路径
* @param map 二维地图
* @param start 开始点
* @param end 终点
* @return
*/
public static int[][] bfs(char[][] map, int[] start, int[] end) {
// 移动的四个方向(下右上左)。
int[] dx = {1, 0, -1, 0};
int[] dy = {0, 1, 0, -1};
// 用队列存储对应点的横坐标与纵坐标。
Queue<int[]> que = new LinkedList<>();
// 到起始点的距离,我们先全部初始化为最大值。
int[][] min = new int[map.length][map[0].length];
for (int i = 0; i < min.length; i++) {
Arrays.fill(min[i], Integer.MAX_VALUE);
}
// 起始点的距离设为0
min[start[0]][start[1]] = 0;
// 将起始点入队
que.offer(start);
// 队列为空的情况跳出循环,即该迷宫走不出去,无解。
while (!que.isEmpty()) {
// 取出队列中最前端的点
int[] temp = que.poll();
// 如果是终点则结束
if (temp[0] == end[0] && temp[1] == end[1]) {
break;
}
// 四个方向循环
for (int i = 0; i < 4; i++) {
int y = temp[0] + dy[i];
int x = temp[1] + dx[i];
// 判断是否可以走,条件为该点不是墙,并且没有走过
if (map[y][x] != '#' && min[y][x] == Integer.MAX_VALUE) {
// 如果可以走则将该点的距离加1
min[y][x] = min[temp[0]][temp[1]] + 1;
// 将可以走的点入队
que.offer(new int[]{y, x});
}
}
}
// 返回所有点到到起始点的距离的二维数组。
return min;
}
}
```
输出结果:
```
迷宫地形图:
# # # # # # # # # #
# O O # O O O # O #
# O O # O O O # O #
# O O O O # O O O #
# O # # # O O O O #
# O O O # O O O O #
# O # O O O # O O #
# O # # # O # # O #
# # # O O O O O O #
# # # # # # # # # #
该地图最短路径长为13
---上帝视角迷宫最短路径地形图:---
# # # # # # # # # #
# O 1 # 7 8 9 # 13 #
# 1 2 # 6 7 8 # 12 #
# 2 3 4 5 # 9 10 11 #
# 3 # # # 11 10 11 12 #
# 4 5 6 # 10 11 12 13 #
# 5 # 7 8 9 # 13 14 #
# 6 # # # 10 # # O #
# # # 13 12 11 12 13 O #
# # # # # # # # # #
```
## 递归—八皇后问题
> 八皇后问题介绍
![image-20211214105020926](数据结构与算法/image-20211214105020926.png)
八皇后问题,是一个古老而著名的问题,是**回溯算法的典型案例**。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出在8×8格的国际象棋上摆放八个皇后使其不能互相攻击**任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法**。
> 八皇后问题算法思路分析
暴力解法(穷举解法):
1. 第一个皇后先放第一行第一列
2. 第二个皇后放在第二行第一列、然后判断是否OK 如果不OK继续放在第二列、第三列、依次把所有列都放完找到一个合适
3. 继续第三个皇后还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置算是找到了一个正确解
4. 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.
5. 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4的步骤
**说明**:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题:
arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行即第几个皇后arr[i] = val , val 表示第i+1个皇后放在第i+1行的第val+1列
> 代码实现
```java
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author wuyou
*/
public class Queue8 {
/**
* 定义一个max表示有多少个皇后
*/
final static int MAX = 8;
/**
* 定义一个Array数组保存皇后放置位置的结果比如array = {0,4,7,5,2,6,1,3}
*/
static int[] array = new int[MAX];
/**
* 如果是多线程,使用原子类能确保得到最终的情况数
*/
static AtomicInteger count = new AtomicInteger(0);
/**
* 统计判断的次数
*/
static AtomicInteger judgeCount = new AtomicInteger(0);
public static void main(String[] args) {
check(0);
System.out.println("总共有【" + count + "】情况");
System.out.println("总共有【" + judgeCount + "】次判断冲突的次数");
}
/**
* 放置第n + 1个皇后递归
* @param n 第 n + 1个皇后
*/
public static void check(int n) {
// 说明8个皇后已经放好
if (n == MAX) {
print();
count.getAndIncrement();
return;
}
// 依次放入皇后,并判断是否冲突
for (int i = 0; i < MAX; i++) {
// 把当前这个皇后放到该行的第i列
array[n] = i;
// 判断当放置第n个皇后放到第i列时是否冲突
if (judge(n)) {
// 不冲突接着放第n+1个皇后开始递归
check(n + 1);
}
// 如果冲突就继续指向array[n] = i;
}
}
/**
* 判断第 n + 1个皇后是否和前面皇后冲突
* @param n 表示第 n + 1个皇后
* @return 是否可以摆放
*/
public static boolean judge(int n) {
judgeCount.getAndIncrement();
for (int i = 0; i < n; i++) {
// array[i] == array[n] 表示判断第n个皇后和前面n-1个皇后是否在在同一列
// Math.abs(n - i) == Math.abs(array[n] - array[i]) 通过判断直角三角形两直角边是否相等确定是否在同一斜线
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {
return false;
}
}
return true;
}
/**
* 打印皇后的位置
*/
public static void print() {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
```
输出结果:
```
0 4 7 5 2 6 1 3
0 5 7 2 6 3 1 4
0 6 3 5 7 1 4 2
0 6 4 7 1 3 5 2
1 3 5 7 2 0 6 4
1 4 6 0 2 7 5 3
1 4 6 3 0 7 5 2
1 5 0 6 3 7 2 4
1 5 7 2 0 3 6 4
1 6 2 5 7 4 0 3
1 6 4 7 0 3 5 2
1 7 5 0 2 4 6 3
2 0 6 4 7 1 3 5
2 4 1 7 0 6 3 5
2 4 1 7 5 3 6 0
2 4 6 0 3 1 7 5
2 4 7 3 0 6 1 5
2 5 1 4 7 0 6 3
2 5 1 6 0 3 7 4
2 5 1 6 4 0 7 3
2 5 3 0 7 4 6 1
2 5 3 1 7 4 6 0
2 5 7 0 3 6 4 1
2 5 7 0 4 6 1 3
2 5 7 1 3 0 6 4
2 6 1 7 4 0 3 5
2 6 1 7 5 3 0 4
2 7 3 6 0 5 1 4
3 0 4 7 1 6 2 5
3 0 4 7 5 2 6 1
3 1 4 7 5 0 2 6
3 1 6 2 5 7 0 4
3 1 6 2 5 7 4 0
3 1 6 4 0 7 5 2
3 1 7 4 6 0 2 5
3 1 7 5 0 2 4 6
3 5 0 4 1 7 2 6
3 5 7 1 6 0 2 4
3 5 7 2 0 6 4 1
3 6 0 7 4 1 5 2
3 6 2 7 1 4 0 5
3 6 4 1 5 0 2 7
3 6 4 2 0 5 7 1
3 7 0 2 5 1 6 4
3 7 0 4 6 1 5 2
3 7 4 2 0 6 1 5
4 0 3 5 7 1 6 2
4 0 7 3 1 6 2 5
4 0 7 5 2 6 1 3
4 1 3 5 7 2 0 6
4 1 3 6 2 7 5 0
4 1 5 0 6 3 7 2
4 1 7 0 3 6 2 5
4 2 0 5 7 1 3 6
4 2 0 6 1 7 5 3
4 2 7 3 6 0 5 1
4 6 0 2 7 5 3 1
4 6 0 3 1 7 5 2
4 6 1 3 7 0 2 5
4 6 1 5 2 0 3 7
4 6 1 5 2 0 7 3
4 6 3 0 2 7 5 1
4 7 3 0 2 5 1 6
4 7 3 0 6 1 5 2
5 0 4 1 7 2 6 3
5 1 6 0 2 4 7 3
5 1 6 0 3 7 4 2
5 2 0 6 4 7 1 3
5 2 0 7 3 1 6 4
5 2 0 7 4 1 3 6
5 2 4 6 0 3 1 7
5 2 4 7 0 3 1 6
5 2 6 1 3 7 0 4
5 2 6 1 7 4 0 3
5 2 6 3 0 7 1 4
5 3 0 4 7 1 6 2
5 3 1 7 4 6 0 2
5 3 6 0 2 4 1 7
5 3 6 0 7 1 4 2
5 7 1 3 0 6 4 2
6 0 2 7 5 3 1 4
6 1 3 0 7 4 2 5
6 1 5 2 0 3 7 4
6 2 0 5 7 4 1 3
6 2 7 1 4 0 5 3
6 3 1 4 7 0 2 5
6 3 1 7 5 0 2 4
6 4 2 0 5 7 1 3
7 1 3 0 6 4 2 5
7 1 4 2 0 6 3 5
7 2 0 5 1 4 6 3
7 3 0 2 5 1 6 4
总共有【92】情况
总共有【15720】次判断冲突的次数
```

File diff suppressed because it is too large Load Diff

@ -0,0 +1,365 @@
## 介绍
> 在java中我们常用的查找有四种:
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
## 顺序查找
依次遍历数据,查找到就返回下标,没有查找到就返回-1
```java
import java.util.Arrays;
/**
* @author 22130
*/
public class SeqSearch {
public static void main(String[] args) {
// 没有顺序的数组
int[] array1 = new int[]{1, 9, 11, -1, 34, 89};
System.out.println("原本数组为:" + Arrays.toString(array1));
System.out.println("查找到11的下标" + seqSearch(array1, 11));
System.out.println("查找到35的下标" + seqSearch(array1, 34));
System.out.println("查找到2的下标" + seqSearch(array1, 2));
}
/**
* 线性查找,找到就返回下标,找不到就返回-1
* @param array
* @param value
* @return
*/
public static int seqSearch(int[] array, int value) {
// 线性查找,逐一匹配
for (int i = 0; i < array.length; i++) {
if (array[i] == value) {
return i;
}
}
return -1;
}
}
```
输出结果:
```
原本数组为:[1, 9, 11, -1, 34, 89]
查找到11的下标2
查找到35的下标4
查找到2的下标-1
```
## 二分查找
二分查找又叫折半查找,从有序列表的初始候选区`li[0:n]`开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。如果待查值小于候选区中间值,则只需比较中间值左边的元素,减半查找范围。依次类推依次减半。
- 二分查找的前提:**列表有序**
- 二分查找的有点:**查找速度快**
- 二分查找的时间复杂度为:**O(logn)**
![二分查找](数据结构与算法/二分查找.gif)
```java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author 22130
*/
public class BinarySearch {
public static void main(String[] args) {
int[] array = new int[]{1, 8, 10, 10, 10, 89, 1000, 1234};
System.out.println("原本的数组为:" + Arrays.toString(array));
System.out.println("测试递归二分法查找数字1000" + searchRecursive(array, 0, array.length - 1, 1000));
System.out.println("测试循环二分法查找数字1000" + searchLoop(array, 1000));
System.out.println("测试批量查找数字10" + searchBatchRecursive(array, 10));
}
/**
* 执行递归二分查找,返回第一次出现该值的位置
*
* @param array 已排序的数组
* @param start 开始位置0
* @param end 结束位置array.length-1
* @param findValue 需要找的值
* @return 值在数组中的位置从0开始。找不到返回-1
*/
public static int searchRecursive(int[] array, int start, int end, int findValue) {
// 如果数组为空,直接返回-1即查找失败
if (array == null) {
return -1;
}
if (start <= end) {
// 中间位置
int middle = (start + end) / 1;
// 中值
int middleValue = array[middle];
if (findValue == middleValue) {
// 等于中值直接返回
return middle;
} else if (findValue < middleValue) {
// 小于中值时在中值前面找
return searchRecursive(array, start, middle - 1, findValue);
} else {
// 大于中值在中值后面找
return searchRecursive(array, middle + 1, end, findValue);
}
} else {
// 返回-1即查找失败
return -1;
}
}
/**
* 循环二分查找,返回第一次出现该值的位置
*
* @param array 已排序的数组
* @param findValue 需要找的值
* @return 值在数组中的位置从0开始。找不到返回-1
*/
public static int searchLoop(int[] array, int findValue) {
// 如果数组为空,直接返回-1即查找失败
if (array == null) {
return -1;
}
// 起始位置
int start = 0;
// 结束位置
int end = array.length - 1;
while (start <= end) {
// 中间位置
int middle = (start + end) / 2;
// 中值
int middleValue = array[middle];
if (findValue == middleValue) {
// 等于中值直接返回
return middle;
} else if (findValue < middleValue) {
// 小于中值时在中值前面找
end = middle - 1;
} else {
// 大于中值在中值后面找
start = middle + 1;
}
}
// 返回-1即查找失败
return -1;
}
/**
* 批量查找要查找的值
* @param array
* @param findValue
* @return
*/
public static List<Integer> searchBatchRecursive(int[] array, int findValue) {
int index = searchRecursive(array, 0, array.length - 1, findValue);
int left = index - 1;
int right = index + 1;
while (left >= 0 && array[left] == findValue) {
left--;
}
while (right < array.length && array[right] == findValue) {
right++;
}
List<Integer> result = new ArrayList<>();
for (int i = left + 1; i < right; i++) {
result.add(i);
}
return result;
}
}
```
输出结果:
```
原本的数组为:[1, 8, 10, 10, 10, 89, 1000, 1234]
测试递归二分法查找数字10006
测试循环二分法查找数字10006
测试批量查找数字10[2, 3, 4]
```
## 插值查找
插值查找算法类似于二分查找不同的是插值查找每次从自适应mid处开始查找。将折半查找中的求mid索引的公式low表示左边索引lefthigh表示右边索引rightkey就是前面我们讲的findVal。优点类似于高数中的拉格朗日中值定理把有序链表看作一个函数表达式函数在闭区间上的整体的平均变化率与区间内某点的局部变化率的关系相近
![二分查找mid](数据结构与算法/二分查找mid.png) **改为** ![插值查找mid](数据结构与算法/插值查找mid.png)
**注意事项**
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
```java
/**
* @author 22130
*/
public class InsertValueSearchDemo {
public static void main(String[] args) {
int[] array = new int[]{1, 8, 10, 89, 1000, 1234};
System.out.println("找到数字10000的下标为" + insertValueSearch(array, 0, array.length - 1, 1000));
}
/**
* 插值查找
*
* @param arr 已排序的数组
* @param left 开始位置0
* @param right 结束位置array.length-1
* @param findValue
* @return
*/
public static int insertValueSearch(int[] arr, int left, int right, int findValue) {
//注意findVal < arr[0] findVal > arr[arr.length - 1] 必须需要, 否则我们得到的 mid 可能越界
if (left > right || findValue < arr[0] || findValue > arr[arr.length - 1]) {
return -1;
}
// 求出mid, 自适应
int mid = left + (right - left) * (findValue - arr[left]) / (arr[right] - arr[left]);
int midValue = arr[mid];
if (findValue > midValue) {
// 向右递归
return insertValueSearch(arr, mid + 1, right, findValue);
} else if (findValue < midValue) {
// 向左递归
return insertValueSearch(arr, left, mid - 1, findValue);
} else {
return mid;
}
}
}
```
输出结果:
```
找到数字10000的下标为4
```
## 斐波那契查找
黄金分割点是指把一条线段分割为两部分使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意想不到的效果。
斐波那契数列{1, 1,2, 3, 5, 8, 13,21, 34, 55 }发现斐波那契数列的两个相邻数的比例无限接近黄金分割值0.618。 斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置mid不再是中间或插值得到而是位于黄金分割点附近即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示:
![斐波那契查找](数据结构与算法/斐波那契查找.png)
> 对F(k-1)-1的理解
- 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 F[k]-1=F[k-1]-1+F[k-2]-1+1 。该式说明只要顺序表的长度为F[k]-1则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段即如上图所示。从而中间位置为mid=low+F(k-1)-1
- 类似的,每一子段也可以用相同的方式分割
- 但顺序表长度n不一定刚好等于F[k]-1所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可由以下代码得到顺序表长度增加后新增的位置从n+1到F[k]-1位置都赋为n位置的值即可。
```java
import java.util.Arrays;
/**
* @author 22130
*/
public class FibonacciSearchDemo {
final static int MAX_SIZE = 20;
public static void main(String[] args) {
int[] array = new int[]{1, 8, 10, 89, 1000, 1234};
System.out.println("找到数字10000的下标为" + fibonacciSearch(array, 1));
}
/**
* 因为后面我们mid=low+F(k-1)-1需要使用到斐波那契数列因此我们需要先获取到一个斐波那契数列
* <p>
* 非递归方法得到一个斐波那契数列
*
* @return
*/
private static int[] getFibonacci() {
int[] fibonacci = new int[MAX_SIZE];
fibonacci[0] = 1;
fibonacci[1] = 1;
for (int i = 2; i < MAX_SIZE; i++) {
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];
}
return fibonacci;
}
/**
* 编写斐波那契查找算法
* <p>
* 使用非递归的方式编写算法
*
* @param array 数组
* @param findValue 我们需要查找的关键码(值)
* @return 返回对应的下标,如果没有-1
*/
public static int fibonacciSearch(int[] array, int findValue) {
int low = 0;
int high = array.length - 1;
// 表示斐波那契分割数值的下标
int k = 0;
// 存放mid值
int mid = 0;
// 获取到斐波那契数列
int[] fibonacci = getFibonacci();
// 获取到斐波那契分割数值的下标
while (high > fibonacci[k] - 1) {
k++;
}
// 因为 fibonacci[k] 值可能大于 array 的 长度因此我们需要使用Arrays类构造一个新的数组
int[] temp = Arrays.copyOf(array, fibonacci[k]);
// 实际上需求使用array数组最后的数填充 temp
for (int i = high + 1; i < temp.length; i++) {
temp[i] = array[high];
}
System.out.println("构造的fibonacci数组长度为" + temp.length);
// 使用while来循环处理找到我们的数 findValue
while (low <= high) {
// F[k]-1=F[k-1]-1+F[k-2]-1+1
mid = low + fibonacci[k] - 1;
System.out.println("mid为" + mid);
if (findValue < temp[mid]) {
high = mid - 1;
// 在mid的前面有F[k-1]-1+F[k-2]-1+1 个数可以继续拆分成2个部分
// 下次mid就在这2个部分中间那个数F[k-1]-1 + 1
k--;
} else if (findValue > temp[mid]) {
low = mid + 1;
// 在mid的后面有F[k-1]-1个数mid本身是第F[k]-1个数
// 所以下次mid就是 本身 + 后面 =F[k]-1+F[k-1]-1=F[k+1)-1
k++;
} else {
// 查找的是小一点的数据
return Math.min(mid, high);
}
}
return -1;
}
}
```
输出结果:
```
构造的fibonacci数组长度为8
mid为7
mid为4
mid为2
mid为1
mid为0
找到数字10000的下标为0
```

@ -0,0 +1,437 @@
## 介绍
散列表Hash table也叫哈希表是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
![Hash-table-Representation](数据结构与算法/Hash-table-Representation.png)
> 相关缓存层产品
- Redis
- Memcache
> 结构
哈希表:
- 数组 + 链表
- 数组 + 二叉树
## 实例
> 哈希表(散列)-Google上机题
- 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id、性别、年龄、住址……)当输入该员工的id时要求查找到该员工的 所有信息.
- 要求:不使用数据库,尽量节省内存,速度越快越好 => 哈希表(散列)
> 散列函数分析
- 可以把每个字符的ASCII对应的数字相加作为下标比如abc = (a-96) + (b-96) + (c-96)a的ASCII是97这种方式的缺点就是哈希值很容易重复比如bbb、abc、cba
- 也可以使用幂的连乘,比如`abc1*27^0+2*27^1+3*27^2`解决越界问题可以使用大数运算java里的BigInteger
```java
/**
* 用bigint
*
* @param key
* @return
*/
public int hash3(String key) {
BigInteger hashvalue = new BigInteger("0");
BigInteger pow27 = new BigInteger("1");
for (int i = 0; i < key.length(); i++) {
// 比如abc,1*27^0+2*27^1+3*27^2
int letter = key.charAt(i) - 96;
// 把letter用bigint包装起来
BigInteger bigLetter = new BigInteger(letter + "");
hashvalue = hashvalue.add(bigLetter.multiply(pow27));
pow27 = pow27.multiply(new BigInteger(27 + ""));
}
return hashvalue.mod(new BigInteger(arrays.length + "")).intValue();
}
```
> 代码测试
用户实体Employee类
```java
/**
* 雇员实体类
*/
class Employee {
private int id;
private String name;
private Employee next;
public Employee(int id, String name) {
super();
this.id = id;
this.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 Employee getNext() {
return next;
}
public void setNext(Employee next) {
this.next = next;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", next=" + next +
'}';
}
}
```
雇员链表EmployeeLinkedList类
```java
/**
* 雇员链表类
*/
class EmployeeLinkedList {
/**
* 头指针指向第一个Emp因此我们这个链表的head是直接指向第一个Emp默认为null
*/
private Employee head;
/**
* 尾指针指向最后一个Emp
*/
private Employee tail;
/**
* 假定当添加雇员的时候id是自增长的直接加入到链表的最后就行
* @param employee
*/
public void add(Employee employee) {
// 如果是第一个雇员
if (head == null) {
head = employee;
tail = head;
return;
} else {
tail.setNext(employee);
tail = employee;
}
}
/**
* 显示员工
* @param i
*/
public void list(int i) {
if (head == null) {
System.out.println("第" + i + "条链表为空");
return;
} else {
System.out.print("第" + i + "条链表信息为:");
Employee temp = head;
while (temp != null) {
System.out.printf("=> id = %d, name = %s\t", temp.getId(), temp.getName());
temp = temp.getNext();
}
System.out.println();
}
}
/**
* 删除员工
* @param id
*/
public void delete(int id) {
// 判断链表是否为空
if (head == null) {
System.out.println("该元素不存在");
return;
}
if (head.getId() == id) {
head = head.getNext();
return;
}
Employee temp = head;
while (temp.getNext() != null) {
if (temp.getNext().getId() == id) {
temp.setNext(temp.getNext().getNext());
}
temp = temp.getNext();
}
}
/**
* 查找员工
* @param id
* @return
*/
public Employee find(int id) {
// 判断链表是否为空
if (head == null) {
System.out.println("该元素不存在");
return null;
}
Employee temp = head;
while (temp != null) {
if (temp.getId() == id) {
return temp;
}
temp = temp.getNext();
}
System.out.println("该元素不存在");
return null;
}
}
```
哈希表HashTable类
```java
/**
* 用于管理多条table
*/
class HashTable {
private EmployeeLinkedList[] employeeLinkedListArray;
private int size;
/**
* 构造函数初始化hashtable
* @param size
*/
public HashTable(int size) {
this.size = size;
// 初始化employeeLinkedListArray
employeeLinkedListArray = new EmployeeLinkedList[size];
// 分别初始化每个链表
for (int i = 0; i < size; i++) {
employeeLinkedListArray[i] = new EmployeeLinkedList();
}
}
/**
* 新增员工
* @param employee
*/
public void add(Employee employee) {
// 根据员工的id得到该员工应当添加到哪条链表
int employeeLinkedListNo = hashFun(employee.getId());
// 将employee添加到对应的链表
employeeLinkedListArray[employeeLinkedListNo].add(employee);
}
/**
* 删除员工
* @param id
*/
public void delete(int id) {
// 根据员工的id得到该员工应在到哪条链表
int employeeLinkedListNo = hashFun(id);
// 删除对应节点的链表
employeeLinkedListArray[employeeLinkedListNo].delete(id);
}
/**
* 查找员工
* @param id
* @return
*/
public Employee find(int id) {
int employeeLinkedListNo = hashFun(id);
Employee employee = employeeLinkedListArray[employeeLinkedListNo].find(id);
return employee;
}
/**
* 打印链表
*/
public void list() {
for (int i = 0; i < size; i++) {
employeeLinkedListArray[i].list(i);
}
}
/**
* 编写散列函数,使用一个简单取模法
* @param id
* @return
*/
public int hashFun(int id) {
return id % size;
}
}
```
测试类
```java
import java.util.Scanner;
/**
* @author wuyou
*/
public class HashTableDemo {
public static void main(String[] args) {
// 创建哈希表
HashTable hashTable = new HashTable(7);
// 写一个简单的菜单
String key = "";
Scanner scanner = new Scanner(System.in);
System.out.println("add 【添加员工】");
System.out.println("list 【显示员工】");
System.out.println("find 【查找员工】");
System.out.println("delete【删除员工】");
System.out.println("exit 【退出系统】");
int id;
Employee employee;
while (true) {
key = scanner.next();
switch (key) {
case "add":
System.out.println("输入id");
id = scanner.nextInt();
System.out.println("输入名字:");
String name = scanner.next();
// 创建雇员
employee = new Employee(id, name);
hashTable.add(employee);
System.out.println("添加成功,请重新输入选择菜单:");
break;
case "list":
hashTable.list();
break;
case "find":
System.out.println("请输入要查找的id");
id = scanner.nextInt();
employee = hashTable.find(id);
if (employee != null) {
System.out.println("查找成功,信息为:" + employee);
}
System.out.println("请重新输入选择菜单:");
break;
case "delete":
System.out.println("请输入要删除的id");
id = scanner.nextInt();
hashTable.delete(id);
System.out.println("删除成功,请重新输入选择菜单:");
break;
case "exit":
scanner.close();
System.exit(0);
default:
System.out.println("你的输入有误!请重写输入:");
break;
}
}
}
}
```
测试结果:
```
add 【添加员工】
list 【显示员工】
find 【查找员工】
delete【删除员工】
exit 【退出系统】
add
输入id
1
输入名字:
wuyou
添加成功,请重新输入选择菜单:
add
输入id
2
输入名字:
chenchen
添加成功,请重新输入选择菜单:
list
第0条链表为空
第1条链表信息为=> id = 1, name = wuyou
第2条链表信息为=> id = 2, name = chenchen
第3条链表为空
第4条链表为空
第5条链表为空
第6条链表为空
find
请输入要查找的id
2
查找成功信息为Employee{id=2, name='chenchen', next=null}
请重新输入选择菜单:
add
输入id
8
输入名字:
haha
添加成功,请重新输入选择菜单:
list
第0条链表为空
第1条链表信息为=> id = 1, name = wuyou => id = 8, name = haha
第2条链表信息为=> id = 2, name = chenchen
第3条链表为空
第4条链表为空
第5条链表为空
第6条链表为空
find
请输入要查找的id
8
查找成功信息为Employee{id=8, name='haha', next=null}
请重新输入选择菜单:
delete
请输入要删除的id
1
删除成功,请重新输入选择菜单:
list
第0条链表为空
第1条链表信息为=> id = 8, name = haha
第2条链表信息为=> id = 2, name = chenchen
第3条链表为空
第4条链表为空
第5条链表为空
第6条链表为空
find
请输入要查找的id
1
该元素不存在
请重新输入选择菜单:
add
输入id
1
输入名字:
wuyou
添加成功,请重新输入选择菜单:
list
第0条链表为空
第1条链表信息为=> id = 8, name = haha => id = 1, name = wuyou
第2条链表信息为=> id = 2, name = chenchen
第3条链表为空
第4条链表为空
第5条链表为空
第6条链表为空
exit
```

@ -0,0 +1,5 @@
<div style="color:#16b0ff;font-size:50px;font-weight: 900;text-shadow: 5px 5px 10px var(--theme-color);font-family: 'Comic Sans MS';">Study</div>
<span style="color:#16b0ff;font-size:20px;font-weight: 900;font-family: 'Comic Sans MS';">Introduction</span>:自主学习笔记!
## 🚀点击左侧菜单栏开始吧!

@ -0,0 +1,13 @@
* 🏁 数据结构与算法
* [✍ 数据结构源码分析](src/study/1 "数据结构源码分析")
* [✍ 概述](src/study/2 "概述")
* [✍ 稀疏数组和队列](src/study/3 "稀疏数组和队列")
* [✍ 链表](src/study/4 "链表")
* [✍ 栈](src/study/5 "栈")
* [✍ 递归](src/study/6 "递归")
* [✍ 排序算法](src/study/7 "排序算法")
* [✍ 查找算法](src/study/8 "查找算法")
* [✍ 哈希表](src/study/9 "哈希表")
* [✍ 树](src/study/10 "树")
* [✍ 图](src/study/11 "图")
* [✍ 十大算法](src/study/12 "十大算法")

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save