diff --git a/_navbar.md b/_navbar.md index 801e195..a4814ee 100644 --- a/_navbar.md +++ b/_navbar.md @@ -11,4 +11,5 @@ * [✍ 解决方案](src/Solution/README) * [✍ 开放性问题](src/Open/README) * [✍ 科班学习](src/university/README) + * [✍ 自主学习](src/study/README) * [✍ 其它](src/Others/README) \ No newline at end of file diff --git a/_sidebar.md b/_sidebar.md index 801e195..a4814ee 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -11,4 +11,5 @@ * [✍ 解决方案](src/Solution/README) * [✍ 开放性问题](src/Open/README) * [✍ 科班学习](src/university/README) + * [✍ 自主学习](src/study/README) * [✍ 其它](src/Others/README) \ No newline at end of file diff --git a/src/study/1.md b/src/study/1.md new file mode 100644 index 0000000..8719f88 --- /dev/null +++ b/src/study/1.md @@ -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 extends AbstractList + implements List, 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类型构造方法 + +- 第一步,将参数中的集合转化为数组赋给elementData; + +- 第二步,参数集合是否是空。通过比较size与第一步中的数组长度的大小。 + +- 第三步,如果参数集合为空,则设置元素数组为空,即将EMPTY_ELEMENTDATA赋给elementData; + +- 第四步,如果参数集合不为空,接下来判断是否成功将参数集合转化为Object类型的数组,如果转化成Object类型的数组成功,则将数组进行复制,转化为Object类型的数组。 + +```java +public ArrayList(Collection 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的缺点 + +- 插入和删除元素的效率不高 +- 根据元素的值查找元素的下标需要遍历整个元素数组,效率不高 +- 线程不安全 \ No newline at end of file diff --git a/src/study/10.md b/src/study/10.md new file mode 100644 index 0000000..fb2a70e --- /dev/null +++ b/src/study/10.md @@ -0,0 +1,5084 @@ +介绍 + +> 为什么需要树这种数据结构 + +- 数组存储方式的分析 + - 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。 + - 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 +- 链式存储方式的分析 + - 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。 + - 缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历) + +- 树存储方式的分析 + - 能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度 + +> 定义 + +**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成,是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归 + +> 数的分类 + +- **二叉查找树(BST)**:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表 +- **平衡二叉树(AVL)**:通过旋转解决了平衡的问题,但是旋转操作效率太低 +- **红黑树**:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多 +- **B树**:通过将二叉树改为多路平衡查找树,解决了树过高的问题 +- **B+树**:在B树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效 + +## 二叉树的概念和常用术语 + +> 树的常用术语(结合示意图理解) + +![image-20211217104036824](数据结构与算法/image-20211217104036824.png) + +- 节点 + +- 根节点 + +- 父节点 + +- 子节点 + +- 叶子节点 (没有子节点的节点) + +- 节点的权(节点值) + +- 路径(从root节点找到该节点的路线) + +- 层 + +- 子树 + +- 树的高度(最大层数) + +- 森林 :多颗子树构成森林 + +> 二叉树的概念 + +- 树有很多种,每个节点**最多只能有两个子节点**的一种形式称为二叉树 + +```java +public class TreeNode { + // 数据域 + private Object data; + // 左孩子指针 + private TreeNode leftChild; + // 右孩子指针 + private TreeNode rightChild; +} +``` + +- 二叉树的子节点分为左节点和右节点 + +![image-20211217104357883](数据结构与算法/image-20211217104357883.png) + +- 如果该二叉树的**所有叶子节点都在最后一层**,并且结点总数= 2^n -1 , n 为层数,则我们称为**满二叉树** + +![image-20211217104705066](数据结构与算法/image-20211217104705066.png) + +- 设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,我们称为**完全二叉树** + +![image-20211217105020257](数据结构与算法/image-20211217105020257.png) + +## 树的遍历 + +### 前序遍历(Preorder Traversal + +**实现原理**:`先访问根节点,然后访问左子树,最后访问右子树`。 + +**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。 + +![前序遍历](数据结构与算法/前序遍历.gif) + +```java +// 递归实现 +public void preOrderTraverse1(TreeNode root) { + if (root != null) { + System.out.print(root.val + "->"); + preOrderTraverse1(root.left); + preOrderTraverse1(root.right); + } +} + +// 非递归实现 +public void preOrderTraverse2(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.empty()) { + if (node != null) { + System.out.print(node.val + "->"); + stack.push(node); + node = node.left; + } else { + TreeNode tem = stack.pop(); + node = tem.right; + } + } +} +``` + +### 中序遍历(Inorder Traversal) + +**实现原理**:`先访问左子树,然后访问根节点,最后访问右子树`。 + +**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。 + +![中序遍历](数据结构与算法/中序遍历.gif) + +```java +// 递归实现 +public void inOrderTraverse1(TreeNode root) { + if (root != null) { + inOrderTraverse1(root.left); + System.out.print(root.val + "->"); + inOrderTraverse1(root.right); + } +} + +// 非递归实现 +public void inOrderTraverse2(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.isEmpty()) { + if (node != null) { + stack.push(node); + node = node.left; + } else { + TreeNode tem = stack.pop(); + System.out.print(tem.val + "->"); + node = tem.right; + } + } +} +``` + +### 后序遍历(Postorder Traversal) + +**实现原理**:`先访问左子树,然后访问右子树,最后访问根节点`。 + +**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。 + +![后序遍历](数据结构与算法/后序遍历.gif) + +```java +// 递归实现 +public void postOrderTraverse1(TreeNode root) { + if (root != null) { + postOrderTraverse1(root.left); + postOrderTraverse1(root.right); + System.out.print(root.val + "->"); + } +} + +// 非递归实现 +public void postOrderTraverse2(TreeNode root) { + TreeNode cur, pre = null; + + Stack stack = new Stack<>(); + stack.push(root); + + while (!stack.empty()) { + cur = stack.peek(); + if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) { + System.out.print(cur.val + "->"); + stack.pop(); + pre = cur; + } else { + if (cur.right != null) + stack.push(cur.right); + if (cur.left != null) + stack.push(cur.left); + } + } +``` + +### 层次遍历 + +```java +public void levelOrderTraverse(TreeNode root) { + if (root == null) { + return; + } + + Queue queue = new LinkedList(); + queue.add(root); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); + System.out.print(node.val + "->"); + + if (node.left != null) { + queue.add(node.left); + } + if (node.right != null) { + queue.add(node.right); + } + } +} +``` + +## 二叉树实例 + +### 前、中、后序遍历 + +> 代码实现 + +```java +import java.util.Stack; + +/** + * @author wuyou + */ +public class BinaryTreeDemo { + public static void main(String[] args) { + TreeNode root = new TreeNode(new Hero(0, "根节点")); + TreeNode node1 = new TreeNode(new Hero(1, "节点1")); + TreeNode node2 = new TreeNode(new Hero(2, "节点2")); + TreeNode node3 = new TreeNode(new Hero(3, "节点3")); + TreeNode node4 = new TreeNode(new Hero(4, "节点4")); + TreeNode node5 = new TreeNode(new Hero(5, "节点5")); + TreeNode node6 = new TreeNode(new Hero(6, "节点6")); + root.setLeftChild(node1); + root.setRightChild(node2); + node1.setLeftChild(node3); + node1.setRightChild(node4); + node3.setLeftChild(node5); + node3.setRightChild(node6); + + // 前序遍历 + System.out.println("\n递归实现前序遍历:"); + preOrderTraverse1(root); + System.out.println("\n非递归实现前序遍历:"); + preOrderTraverse2(root); + + // 中序遍历 + System.out.println("\n递归实现中序遍历:"); + inOrderTraverse1(root); + System.out.println("\n非递归实现中序遍历:"); + inOrderTraverse2(root); + + // 中序遍历 + System.out.println("\n递归实现后序遍历:"); + postOrderTraverse1(root); + System.out.println("\n非递归实现后序遍历:"); + postOrderTraverse2(root); + } + + /** + * 递归实现前序遍历 + * + * @param root 根节点 + */ + public static void preOrderTraverse1(TreeNode root) { + if (root != null) { + System.out.print(root.getData() + "->"); + preOrderTraverse1(root.getLeftChild()); + preOrderTraverse1(root.getRightChild()); + } + } + + /** + * 非递归实现前序遍历 + * + * @param root 根节点 + */ + public static void preOrderTraverse2(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.empty()) { + if (node != null) { + System.out.print(node.getData() + "->"); + stack.push(node); + node = node.getLeftChild(); + } else { + TreeNode tem = stack.pop(); + node = tem.getRightChild(); + } + } + } + + /** + * 递归实现中序遍历 + * + * @param root 根节点 + */ + public static void inOrderTraverse1(TreeNode root) { + if (root != null) { + inOrderTraverse1(root.getLeftChild()); + System.out.print(root.getData() + "->"); + inOrderTraverse1(root.getRightChild()); + } + } + + /** + * 非递归实现中序遍历 + * + * @param root 根节点 + */ + public static void inOrderTraverse2(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.isEmpty()) { + if (node != null) { + stack.push(node); + node = node.getLeftChild(); + } else { + TreeNode tem = stack.pop(); + System.out.print(tem.getData() + "->"); + node = tem.getRightChild(); + } + } + } + + /** + * 递归实现后序遍历 + * + * @param root + */ + public static void postOrderTraverse1(TreeNode root) { + if (root != null) { + postOrderTraverse1(root.getLeftChild()); + postOrderTraverse1(root.getRightChild()); + System.out.print(root.getData() + "->"); + } + } + + /** + * 非递归实现后序遍历 + * + * @param root + */ + public static void postOrderTraverse2(TreeNode root) { + TreeNode cur, pre = null; + + Stack stack = new Stack<>(); + stack.push(root); + + while (!stack.empty()) { + cur = stack.peek(); + if ((cur.getLeftChild() == null && cur.getRightChild() == null) || (pre != null && (pre == cur.getLeftChild() || pre == cur.getRightChild()))) { + System.out.print(cur.getData() + "->"); + stack.pop(); + pre = cur; + } else { + if (cur.getRightChild() != null) { + stack.push(cur.getRightChild()); + } + if (cur.getLeftChild() != null) { + stack.push(cur.getLeftChild()); + } + } + } + } +} + +/** + * 数节点类 + */ +class TreeNode { + /** + * 数据域 + */ + private Object data; + /** + * 左孩子指针 + */ + private TreeNode leftChild; + /** + * 右孩子指针 + */ + private TreeNode rightChild; + + public TreeNode(Object data) { + this.data = data; + leftChild = null; + rightChild = null; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public TreeNode getLeftChild() { + return leftChild; + } + + public void setLeftChild(TreeNode leftChild) { + this.leftChild = leftChild; + } + + public TreeNode getRightChild() { + return rightChild; + } + + public void setRightChild(TreeNode rightChild) { + this.rightChild = rightChild; + } +} + +/** + * 英雄实体类 + */ +class Hero { + /** + * id + */ + private int id; + /** + * 姓名 + */ + private String name; + + public Hero(int id, String name) { + 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; + } + + @Override + public String toString() { + return "Hero{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} +``` + +输出结果: + +![image-20211217114710063](数据结构与算法/image-20211217114710063.png) + +``` +递归实现前序遍历: +Hero{id=0, name='根节点'}->Hero{id=1, name='节点1'}->Hero{id=3, name='节点3'}->Hero{id=5, name='节点5'}->Hero{id=6, name='节点6'}->Hero{id=4, name='节点4'}->Hero{id=2, name='节点2'}-> +非递归实现前序遍历: +Hero{id=0, name='根节点'}->Hero{id=1, name='节点1'}->Hero{id=3, name='节点3'}->Hero{id=5, name='节点5'}->Hero{id=6, name='节点6'}->Hero{id=4, name='节点4'}->Hero{id=2, name='节点2'}-> +递归实现中序遍历: +Hero{id=5, name='节点5'}->Hero{id=3, name='节点3'}->Hero{id=6, name='节点6'}->Hero{id=1, name='节点1'}->Hero{id=4, name='节点4'}->Hero{id=0, name='根节点'}->Hero{id=2, name='节点2'}-> +非递归实现中序遍历: +Hero{id=5, name='节点5'}->Hero{id=3, name='节点3'}->Hero{id=6, name='节点6'}->Hero{id=1, name='节点1'}->Hero{id=4, name='节点4'}->Hero{id=0, name='根节点'}->Hero{id=2, name='节点2'}-> +递归实现后序遍历: +Hero{id=5, name='节点5'}->Hero{id=6, name='节点6'}->Hero{id=3, name='节点3'}->Hero{id=4, name='节点4'}->Hero{id=1, name='节点1'}->Hero{id=2, name='节点2'}->Hero{id=0, name='根节点'}-> +非递归实现后序遍历: +Hero{id=5, name='节点5'}->Hero{id=6, name='节点6'}->Hero{id=3, name='节点3'}->Hero{id=4, name='节点4'}->Hero{id=1, name='节点1'}->Hero{id=2, name='节点2'}->Hero{id=0, name='根节点'}-> +``` + +### 前、中、后序查找 + +> 代码实现 + +```java +import java.util.Stack; + +/** + * @author wuyou + */ +public class BinaryTreeDemo { + public static void main(String[] args) { + TreeNode root = new TreeNode(new Hero(0, "根节点")); + TreeNode node1 = new TreeNode(new Hero(1, "节点1")); + TreeNode node2 = new TreeNode(new Hero(2, "节点2")); + TreeNode node3 = new TreeNode(new Hero(3, "节点3")); + TreeNode node4 = new TreeNode(new Hero(4, "节点4")); + TreeNode node5 = new TreeNode(new Hero(5, "节点5")); + TreeNode node6 = new TreeNode(new Hero(6, "节点6")); + root.setLeftChild(node1); + root.setRightChild(node2); + node1.setLeftChild(node3); + node1.setRightChild(node4); + node3.setLeftChild(node5); + node3.setRightChild(node6); + + // 前序查找 + System.out.println("递归前序查找:" + preOrderSearch1(root, 2)); + System.out.println("非递归前序查找:" + preOrderSearch2(root, 2)); + + // 中序查找 + System.out.println("递归中序查找:" + inOrderSearch1(root, 2)); + System.out.println("非递归中序查找:" + inOrderSearch2(root, 2)); + + // 后序查找 + System.out.println("递归后序查找:" + postOrderSearch1(root, 2)); + System.out.println("非递归后序查找:" + postOrderSearch2(root, 2)); + } + + /** + * 递归实现前序搜索 + * @param root + * @param id + * @return + */ + public static Hero preOrderSearch1(TreeNode root, int id) { + if (root != null) { + Hero hero = (Hero) root.getData(); + if (hero.getId() == id) { + return hero; + } + hero = preOrderSearch1(root.getLeftChild(), id); + if (hero != null) { + return hero; + } + hero = preOrderSearch1(root.getRightChild(), id); + if (hero != null) { + return hero; + } + } + return null; + } + + /** + * 非递归实现前序搜索 + * @param root + * @param id + * @return + */ + public static Hero preOrderSearch2(TreeNode root, int id) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.empty()) { + if (node != null) { + Hero hero = (Hero) node.getData(); + if (hero.getId() == id) { + return hero; + } + stack.push(node); + node = node.getLeftChild(); + } else { + TreeNode tem = stack.pop(); + node = tem.getRightChild(); + } + } + return null; + } + + /** + * 递归实现中序查找 + * @param root + * @param id + * @return + */ + public static Hero inOrderSearch1(TreeNode root, int id) { + if (root != null) { + Hero hero = inOrderSearch1(root.getLeftChild(), id); + if (hero != null) { + return hero; + } + hero = (Hero) root.getData(); + if (hero.getId() == id) { + return hero; + } + hero = inOrderSearch1(root.getRightChild(), id); + if (hero != null) { + return hero; + } + } + return null; + } + + /** + * 非递归实现中序查找 + * @param root + * @param id + * @return + */ + public static Hero inOrderSearch2(TreeNode root, int id) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.isEmpty()) { + if (node != null) { + Hero hero = (Hero) node.getData(); + if (hero.getId() == id) { + return hero; + } + stack.push(node); + node = node.getLeftChild(); + } else { + TreeNode tem = stack.pop(); + node = tem.getRightChild(); + } + } + return null; + } + + /** + * 递归实现后序查找 + * @param root + * @param id + * @return + */ + public static Hero postOrderSearch1(TreeNode root, int id) { + if (root != null) { + Hero hero = postOrderSearch1(root.getLeftChild(), id); + if (hero != null) { + return hero; + } + hero = postOrderSearch1(root.getRightChild(), id); + if (hero != null) { + return hero; + } + hero = (Hero) root.getData(); + if (hero.getId() == id) { + return hero; + } + } + return null; + } + + /** + * 非递归实现后序查找 + * @param root + * @param id + * @return + */ + public static Hero postOrderSearch2(TreeNode root, int id) { + TreeNode cur, pre = null; + + Stack stack = new Stack<>(); + stack.push(root); + + while (!stack.empty()) { + cur = stack.peek(); + if ((cur.getLeftChild() == null && cur.getRightChild() == null) || (pre != null && (pre == cur.getLeftChild() || pre == cur.getRightChild()))) { + Hero hero = (Hero) cur.getData(); + if (hero.getId() == id) { + return hero; + } + stack.pop(); + pre = cur; + } else { + if (cur.getRightChild() != null) { + stack.push(cur.getRightChild()); + } + if (cur.getLeftChild() != null) { + stack.push(cur.getLeftChild()); + } + } + } + return null; + } +} +``` + +输出结果: + +![image-20211217114710063](数据结构与算法/image-20211217114710063.png) + +``` +递归前序查找:Hero{id=2, name='节点2'} +非递归前序查找:Hero{id=2, name='节点2'} +递归中序查找:Hero{id=2, name='节点2'} +非递归中序查找:Hero{id=2, name='节点2'} +递归后序查找:Hero{id=2, name='节点2'} +非递归后序查找:Hero{id=2, name='节点2'} +``` + +## 二叉树删除节点 + +### 直接删除节点 + +> 要求 + +- 如果删除的节点是叶子节点,则删除该节点 + +- 如果删除的节点是非叶子节点,则删除该子树 + +> 代码实现 + +```java +/** + * 递归实现前序直接删除 + * @param root + * @param id + * @return + */ +public static TreeNode preOrderDelete1(TreeNode root, int id) { + if (root != null) { + Hero hero = (Hero) root.getData(); + if (hero.getId() == id) { + return null; + } + root.setLeftChild(preOrderDelete1(root.getLeftChild(), id)); + root.setRightChild(preOrderDelete1(root.getRightChild(), id)); + } + return root; +} +``` + +### 按照规则删除节点 + +> 要求 + +如果要删除的节点是非叶子节点,现在我们不希望将该非叶子节点为根节点的子树删除,**需要指定规则如下**: + +- 如果该非叶子节点A只有一个子节点B,则子节点B替代节点A + +- 如果该非叶子节点A有左子节点B和右子节点C,则让左子节点B替代节点A + +> 代码实现 + +使用一个三元表达式就可以直接解决 + +```java + /** + * 递归实现前序按照规则删除节点 + * @param root + * @param id + * @return + */ + public static TreeNode preOrderDelete2(TreeNode root, int id) { + if (root != null) { + Hero hero = (Hero) root.getData(); + if (hero.getId() == id) { + // 通过一个三元表达式解决,左子节点不为空就返回左子节点,否则返回右子节点 + return root.getLeftChild() != null ? root.getLeftChild() : root.getRightChild(); + } + root.setLeftChild(preOrderDelete2(root.getLeftChild(), id)); + root.setRightChild(preOrderDelete2(root.getRightChild(), id)); + } + return root; + } +``` + +## 顺序存储二叉树 + +### 介绍 + +> 基本说明 + +从数据存储来看,**数组存储方式和树的存储方式**可以相互转换,即数组可以转换成树,树也可以转换成数组. + +![image-20211220181256216](数据结构与算法/image-20211220181256216.png) + +> 要求 + +- 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 7] + +- 要求在遍历数组 arr时,仍然可以以**前序遍历**,**中序遍历**和**后序遍历**的方式完成结点的遍历 + +> 顺序存储二叉树的**特点**: + +- 顺序二叉树通常只考虑完全二叉树 + +- 第n个元素的左子节点为 2 * n + 1 + +- 第n个元素的右子节点为 2 * n + 2 + +- 第n个元素的父节点为 (n-1) / 2 +- n : 表示二叉树中的第几个元素(按0开始编号) + +> 代码实现前、中、后序遍历 + +```java +/** + * @author wuyou + */ +public class ArrayBinaryTreeDemo { + public static void main(String[] args) { + int[] array = new int[]{ 1, 2, 3, 4, 5, 6, 7}; + ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(array); + System.out.println("\n前序遍历:"); + preOrderTraverse(arrayBinaryTree, 0); + + System.out.println("\n中序遍历:"); + inOrderTraverse(arrayBinaryTree, 0); + + System.out.println("\n后序遍历:"); + postOrderTraverse(arrayBinaryTree, 0); + } + + /** + * 递归实现前序遍历顺序存储二叉树 + * @param arrayBinaryTree + * @param index + */ + public static void preOrderTraverse(ArrayBinaryTree arrayBinaryTree, int index) { + int[] array = arrayBinaryTree.getArray(); + if (index < 0 || index > array.length) { + throw new RuntimeException("数据不合法!"); + } + System.out.print(array[index] + " "); + int leftIndex = 2 * index + 1; + int rightIndex = 2 * index + 2; + if (leftIndex < array.length) { + preOrderTraverse(arrayBinaryTree, leftIndex); + } + if (rightIndex < array.length) { + preOrderTraverse(arrayBinaryTree, rightIndex); + } + } + + /** + * 递归实现中序遍历顺序存储二叉树 + * @param arrayBinaryTree + * @param index + */ + public static void inOrderTraverse(ArrayBinaryTree arrayBinaryTree, int index) { + int[] array = arrayBinaryTree.getArray(); + if (index < 0 || index > array.length) { + throw new RuntimeException("数据不合法!"); + } + int leftIndex = 2 * index + 1; + int rightIndex = 2 * index + 2; + if (leftIndex < array.length) { + inOrderTraverse(arrayBinaryTree, leftIndex); + } + System.out.print(array[index] + " "); + if (rightIndex < array.length) { + inOrderTraverse(arrayBinaryTree, rightIndex); + } + } + + /** + * 递归实现后序遍历顺序存储二叉树 + * @param arrayBinaryTree + * @param index + */ + public static void postOrderTraverse(ArrayBinaryTree arrayBinaryTree, int index) { + int[] array = arrayBinaryTree.getArray(); + if (index < 0 || index > array.length) { + throw new RuntimeException("数据不合法!"); + } + int leftIndex = 2 * index + 1; + int rightIndex = 2 * index + 2; + if (leftIndex < array.length) { + postOrderTraverse(arrayBinaryTree, leftIndex); + } + if (rightIndex < array.length) { + postOrderTraverse(arrayBinaryTree, rightIndex); + } + System.out.print(array[index] + " "); + } +} + +class ArrayBinaryTree { + private int[] array; + + public ArrayBinaryTree(int[] array) { + this.array = array; + } + + public int[] getArray() { + return array; + } + + public void setArray(int[] array) { + this.array = array; + } +} +``` + +运行结果: + +``` +前序遍历: +1 2 4 5 3 6 7 +中序遍历: +4 2 5 1 6 3 7 +后序遍历: +4 5 2 6 7 3 1 +``` + +> 顺序存储二叉树应用实例 + +- 八大排序算法中的**堆排序**,就会使用到顺序存储二叉树 + +### 线索化二叉树 + +> 基本介绍 + +![image-20211220191257563](数据结构与算法/image-20211220191257563.png) + +- n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索") +- 这种加上了线索的二叉链表称为**线索链表**,相应的二叉树称为**线索二叉树(Threaded BinaryTree)**。根据线索性质的不同,线索二叉树可分为**前序线索二叉树**、**中序线索二叉树**和**后序线索二叉树**三种 +- 一个结点的前一个结点,称为**前驱**结点 +- 一个结点的后一个结点,称为**后继**结点 + +> 思路分析 + +![image-20211220191257563](数据结构与算法/image-20211220191257563.png) + +中序遍历的结果:{8, 3, 10, 1, 14, 6} + +说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况: + +- left 指向的是左子树,也可能是指向的前驱节点,比如 ① 节点 left 指向的左子树,,而 ⑩ 节点的 left 指向的就是前驱节点 +- right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点 + +> 代码实现 + +```java +/** + * @author wuyou + */ +public class ThreadedBinaryTreeDemo { + public static void main(String[] args) { + Object[] array = {1, 3, 6, 8, 10, 14}; + + // 测试中序线索化 + Node root1 = ThreadBinaryTree.createBinaryTree(array, 0); + ThreadBinaryTree tree1 = new ThreadBinaryTree(); + tree1.inThreadOrder(root1); + + System.out.println("中序按后继节点遍历线索二叉树结果:"); + tree1.inThreadList(root1); + + System.out.println("\n中序按前驱节点遍历线索二叉树结果:"); + tree1.inPreThreadList(root1); + + // 测试前序线索化 + Node root2 = ThreadBinaryTree.createBinaryTree(array, 0); + ThreadBinaryTree tree2 = new ThreadBinaryTree(); + tree2.preThreadOrder(root2); + + System.out.println("\n前序按后继节点遍历线索二叉树结果:"); + tree2.preThreadList(root2); + + // 测试后续线索化 + Node root3 = ThreadBinaryTree.createBinaryTree(array, 0); + ThreadBinaryTree tree3 = new ThreadBinaryTree(); + tree3.postThreadOrder(root3); + + System.out.println("\n后序按后继节点遍历线索二叉树结果:"); + tree3.postThreadList(root3); + } +} + +class ThreadBinaryTree { + /** + * 线索化时记录前一个节点 + */ + private Node preNode; + + /** + * 通过数组构造一个二叉树(完全二叉树) + * @param array + * @param index + * @return + */ + static Node createBinaryTree(Object[] array, int index) { + Node node = null; + if(index < array.length) { + node = new Node(array[index]); + node.left = createBinaryTree(array, index * 2 + 1); + node.right = createBinaryTree(array, index * 2 + 2); + + // 记录节点的父节点(后序遍历的时候需要使用) + if (node.left != null) { + node.left.parent = node; + } + + if (node.right != null) { + node.right.parent = node; + } + } + return node; + } + + /** + * 提取出前、中、后序线索化相同的处理方法,私有方法 + * @param node + */ + private void threadNode(Node node) { + // 左指针为空,将左指针指向前驱节点 + if(node.left == null) { + node.left = preNode; + node.isLeftThread = true; + } + + // 前一个节点的后继节点指向当前节点 + if(preNode != null && preNode.right == null) { + preNode.right = node; + preNode.isRightThread = true; + } + // 没处理完一个节点后,让当前节点是下一个节点的前驱节点 + preNode = node; + } + + + /** + * 前序线索化二叉树 + * @param node + */ + void preThreadOrder(Node node) { + if(node == null) { + return; + } + + // 处理当前节点 + threadNode(node); + + // 处理左子树 + if(!node.isLeftThread) { + preThreadOrder(node.left); + } + + // 处理右子树 + if(!node.isRightThread) { + preThreadOrder(node.right); + } + } + + /** + * 中序线索化二叉树 + * @param node 节点 + */ + void inThreadOrder(Node node) { + if(node == null) { + return; + } + + // 处理左子树 + inThreadOrder(node.left); + + // 处理当前节点 + threadNode(node); + + // 处理右子树 + inThreadOrder(node.right); + } + + /** + * 后序线索化二叉树 + * @param node 节点 + */ + void postThreadOrder(Node node) { + if(node == null) { + return; + } + + // 处理左子树 + postThreadOrder(node.left); + + // 处理右子树 + postThreadOrder(node.right); + + // 处理当前节点 + threadNode(node); + } + + /** + * 前序遍历线索二叉树(按照后继线索遍历) + * @param node + */ + void preThreadList(Node node) { + while(node != null) { + while(!node.isLeftThread) { + System.out.print(node.data + ", "); + node = node.left; + } + System.out.print(node.data + ", "); + node = node.right; + } + } + + /** + * 中序遍历线索二叉树,按照后继方式遍历(思路:找到最左子节点开始) + * @param node + */ + void inThreadList(Node node) { + // 找中序遍历方式开始的节点 + while(node != null && !node.isLeftThread) { + node = node.left; + } + while(node != null) { + System.out.print(node.data + ", "); + // 如果右指针是线索 + if(node.isRightThread) { + node = node.right; + } else { + // 如果右指针不是线索,找到右子树开始的节点 + node = node.right; + while(node != null && !node.isLeftThread) { + node = node.left; + } + } + } + } + + /** + * 中序遍历线索二叉树,按照前驱方式遍历(思路:找到最右子节点开始倒序遍历) + * @param node + */ + void inPreThreadList(Node node) { + // 找最后一个节点 + while(node.right != null && !node.isRightThread) { + node = node.right; + } + while(node != null) { + System.out.print(node.data + ", "); + // 如果左指针是线索 + if(node.isLeftThread) { + node = node.left; + } else { + // 如果左指针不是线索,找到左子树开始的节点 + node = node.left; + while(node.right != null && !node.isRightThread) { + node = node.right; + } + } + } + } + + /** + * 后序遍历线索二叉树,按照后继方式遍历(思路:找到最左子节点开始) + * @param node + */ + void postThreadList(Node node) { + Node temp = node; + // 找最左边遍历方式开始的节点 + while(temp != null && !temp.isLeftThread) { + temp = temp.left; + } + Node preNode = null; + while(temp != null) { + if (temp.isRightThread) { + System.out.print(temp.data + ", "); + preNode = temp; + temp = temp.right; + } else { + // 如果上一个处理的节点是当前节点的右节点 + if (temp.right == preNode) { + System.out.print(temp.data + ", "); + // 如果当前节点temp和node相等说明后续遍历完成 + if (temp == node) { + return; + } + preNode = temp; + // 获取当前节点的父节点,避免死循环 + temp = temp.parent; + } else { + // 如果从左节点的进入则找到右子树的最左节点 + temp = temp.right; + while (temp != null && !temp.isLeftThread) { + temp = temp.left; + } + } + } + } + } +} + +class Node { + /** + * 数据域 + */ + Object data; + /** + * 左指针域 + */ + Node left; + /** + * 右指针域 + */ + Node right; + /** + * 父节点的指针(为了后序线索化使用) + */ + Node parent; + /** + * 左指针域类型 false:指向子节点、true:前驱或后继线索 + */ + boolean isLeftThread = false; + /** + * 右指针域类型 false:指向子节点、true:前驱或后继线索 + */ + boolean isRightThread = false; + + Node(Object data) { + this.data = data; + } +} +``` + +输出结果: + +![image-20211220191257563](数据结构与算法/image-20211220191257563.png) + +``` +中序按后继节点遍历线索二叉树结果: +8, 3, 10, 1, 14, 6, +中序按前驱节点遍历线索二叉树结果: +6, 14, 1, 10, 3, 8, +前序按后继节点遍历线索二叉树结果: +1, 3, 8, 10, 6, 14, +后序按后继节点遍历线索二叉树结果: +8, 10, 3, 14, 6, 1, +``` + +## 赫夫曼树 + +### 基本介绍 + +- 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的**带权路径长度(wpl)**达到最小,称这样的二叉树为**最优二叉树**,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。 + +- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。 + +> 赫夫曼树几个重要概念和举例说明 + +- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。**若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1** +- **结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权** +- **结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积** + +- **树的带权路径长度**:树的带权路径长度规定为所有**叶子结点的带权路径长度之和**,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。 +- **WPL最小的就是赫夫曼树** + +![image-20211222104348969](数据结构与算法/image-20211222104348969.png) + +> 赫夫曼树创建思路图解 + +给你一个数列 {7, 5, 2, 4},要求转成一颗赫夫曼树 + +构成赫夫曼树的步骤: + +1. 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和 +2. 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推 +3. 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树 + +![img](数据结构与算法/09563QS5-1.png) + +### 代码实现1 + +```java +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * @author wuyou + */ +public class HuffmanTree1 { + public static class Node { + E data; + double weight; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + } + + public static void main(String[] args) { + List nodes = new ArrayList(); + + nodes.add(new Node("A", 40.0)); + nodes.add(new Node("B", 8.0)); + nodes.add(new Node("C", 10.0)); + nodes.add(new Node("D", 30.0)); + nodes.add(new Node("E", 10.0)); + nodes.add(new Node("F", 2.0)); + + Node root = HuffmanTree1.createTree(nodes); + + System.out.println(breadthFirst(root)); + } + + /** + * 构造哈夫曼树 + * + * @param nodes + * 节点集合 + * @return 构造出来的哈夫曼树的根节点 + */ + private static Node createTree(List nodes) { + // 只要nodes数组中还有2个以上的节点 + while (nodes.size() > 1) { + quickSort(nodes); + // 获取权值最小的两个节点 + Node left = nodes.get(nodes.size()-1); + Node right = nodes.get(nodes.size()-2); + + // 生成新节点,新节点的权值为两个子节点的权值之和 + Node parent = new Node(null, left.weight + right.weight); + + // 让新节点作为两个权值最小节点的父节点 + parent.leftChild = left; + parent.rightChild = right; + + // 删除权值最小的两个节点,删除了一个节点之后,size已经变小了,所以两次size - 1就等同于删除了倒数两个树 + nodes.remove(nodes.size()-1); + nodes.remove(nodes.size()-1); + + // 将新节点加入到集合中 + nodes.add(parent); + } + + return nodes.get(0); + } + + /** + * 将指定集合中的i和j索引处的元素交换 + * + * @param nodes + * @param i + * @param j + */ + private static void swap(List nodes, int i, int j) { + Node tmp; + tmp = nodes.get(i); + nodes.set(i, nodes.get(j)); + nodes.set(j, tmp); + } + + /** + * 实现快速排序算法,用于对节点进行排序 + * + * @param nodes + * @param start + * @param end + */ + private static void subSort(List nodes, int start, int end) { + if (start < end) { + // 以第一个元素作为分界值 + Node base = nodes.get(start); + // i从左边搜索,搜索大于分界值的元素的索引 + int i = start; + // j从右边开始搜索,搜索小于分界值的元素的索引 + int j = end + 1; + while (true) { + // 找到大于分界值的元素的索引,或者i已经到了end处 + while (i < end && nodes.get(++i).weight >= base.weight) { + + } + // 找到小于分界值的元素的索引,或者j已经到了start处 + while (j > start && nodes.get(--j).weight <= base.weight) { + + } + + if (i < j) { + swap(nodes, i, j); + } else { + break; + } + } + + swap(nodes, start, j); + + // 递归左边子序列 + subSort(nodes, start, j - 1); + // 递归右边子序列 + subSort(nodes, j + 1, end); + } + } + + /** + * 快速排序 + * @param nodes + */ + private static void quickSort(List nodes){ + subSort(nodes, 0, nodes.size()-1); + } + + /** + * 广度优先遍历 + * @param root + * @return + */ + private static List breadthFirst(Node root){ + Queue queue = new ArrayDeque(); + List list = new ArrayList(); + + if(root!=null){ + // 将根元素加入“队列” + queue.offer(root); + } + + while(!queue.isEmpty()){ + // 将该队列的“队尾”元素加入到list中 + list.add(queue.peek()); + Node p = queue.poll(); + + // 如果左子节点不为null,将它加入到队列 + if(p.leftChild != null){ + queue.offer(p.leftChild); + } + + // 如果右子节点不为null,将它加入到队列 + if(p.rightChild != null){ + queue.offer(p.rightChild); + } + } + + return list; + } +} +``` + +输出结果: + +``` +[Node[data=null, weight=100.0], Node[data=A, weight=40.0], Node[data=null, weight=60.0], Node[data=null, weight=30.0], Node[data=D, weight=30.0], Node[data=C, weight=10.0], Node[data=null, weight=20.0], Node[data=null, weight=10.0], Node[data=E, weight=10.0], Node[data=F, weight=2.0], Node[data=B, weight=8.0]] +``` + +### 代码实现2 + +```java +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author wuyou + */ +public class HuffmanTree2 { + /** + * 实现Comparable接口,方便排序 + * @param + */ + public static class Node implements Comparable { + E data; + double weight; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + + @Override + public int compareTo(Node o) { + return (int) (this.weight - o.weight); + } + } + + public static void main(String[] args) { + int[] array = {13, 7, 8, 3, 29, 6, 1}; + List nodes = new ArrayList<>(); + for (int i = 1; i <= array.length; i++) { + nodes.add(new Node("节点" + i, array[i - 1])); + } + + Node tree = HuffmanTree2.createTree(nodes); + HuffmanTree2.preOrderTraverse(tree); + } + + /** + * 构建huffman树 + * @param nodes + * @return + */ + private static Node createTree(List nodes) { + // 直到nodes数组中只剩下一个节点 + while (nodes.size() > 1) { + // 进行排序,从小到大 + Collections.sort(nodes); + // 取出权值最小的两棵树 + Node left = nodes.get(0); + Node right = nodes.get(1); + // 构建一颗新的二叉树 + Node parent = new Node(null, left.weight + right.weight); + parent.leftChild = left; + parent.rightChild = right; + + // 从ArrayList中删除处理过的二叉树 + nodes.remove(left); + nodes.remove(right); + + // 将parent加入到nodes中 + nodes.add(parent); + } + return nodes.get(0); + } + + /** + * 递归前序遍历打印 + * @param root + */ + private static void preOrderTraverse(Node root) { + if (root != null) { + System.out.println(root.toString() + "->"); + preOrderTraverse(root.leftChild); + preOrderTraverse(root.rightChild); + } + } +} +``` + +输出结果: + +``` +Node[data=null, weight=67.0]-> +Node[data=节点5, weight=29.0]-> +Node[data=null, weight=38.0]-> +Node[data=null, weight=15.0]-> +Node[data=节点2, weight=7.0]-> +Node[data=节点3, weight=8.0]-> +Node[data=null, weight=23.0]-> +Node[data=null, weight=10.0]-> +Node[data=null, weight=4.0]-> +Node[data=节点7, weight=1.0]-> +Node[data=节点4, weight=3.0]-> +Node[data=节点6, weight=6.0]-> +Node[data=节点1, weight=13.0]-> +``` + +## 赫夫曼编码 + +### 基本介绍 + +- 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法 +- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一 +- 赫夫曼编码广泛地用于数据文件压缩,其压缩率通常在20%~90%之间 +- 赫夫曼码是**可变字长编码(VLC)**的一种,Huffman于1952年提出一种编码方法,称之为最佳编码 + +### 原理剖析 + +> 通信领域中信息的处理方式1——定长编码 + +``` +i like like like java do you like a java // 共40个字符(包括空格) + +105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 // 对应Ascii码 + +01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 // 对应的二进制 +``` + +- 按照二进制来传递信息,总的长度是 359 (包括空格) + +> 通信领域中信息的处理方式2——变长编码 + +``` +i like like like java do you like a java // 共40个字符(包括空格) + +// 各个字符对应的个数 +d:1 +y:1 +u:1 +j:2 +v:2 +o:2 +l:4 +k:4 +e:4 +i:5 +a:5 +space:9 + +// 各个对应的赫夫曼编码 +0=space +1=a +10=i +11=e +100=k +101=l +110=o +111=v +1000=j +1001=u +1010=y +1011=d +``` + +- 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推 + +- 按照上面给各个字符规定的编码,则我们在传输 "i like like like java do you like a java" 数据时,编码就是 `10010110100... ` +- 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码 + +> 通信领域中信息的处理方式3——赫夫曼编码 + +``` +i like like like java do you like a java // 共40个字符(包括空格) + +// 各个字符对应的个数 +d:1 +y:1 +u:1 +j:2 +v:2 +o:2 +l:4 +k:4 +e:4 +i:5 +a:5 +space:9 +``` + +- 按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值 + +![image-20211222133209602](数据结构与算法/image-20211222133209602.png) + +``` +// 根据赫夫曼树,给各个字符 +// 规定编码 , 向左的路径为0 +// 向右的路径为1 , 编码如下: +o: 1000 +u: 10010 +d: 100110 +y: 100111 +i: 101 +a: 110 +k: 1110 +e: 1111 +j: 0000 +v: 0001 +l: 001 + : 01 +``` + +- 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为(注意这里我们使用的无损压缩),长度为 : 133 + +``` +1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 +``` + +- 说明: + - 原来长度是 359,压缩了 (359-133) / 359 = 62.9% + - 此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性(因为每个字符都是叶子节点,所以不会是其他编码的前缀,抵达叶子节点的路径唯一,对应的编码就不同) + +- 注意,这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的,比如如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为: + +![image-20211222134244269](数据结构与算法/image-20211222134244269.png) + +- 为了码字的方差较小,合并值在排序得时候应该排到较后的位置 + +### 最佳实践—数据压缩 + +将给出的一段文本,比如 "i like like like java do you like a java" , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,形式如: + +``` +1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 +``` + +1. 根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树. +2. 生成赫夫曼树对应的赫夫曼编码 。 + +3. 使用赫夫曼编码来生成赫夫曼编码数据,即按照上面的赫夫曼编码,将"i like like like java do you like a java"字符串生成对应的编码数据,形式如下 + +``` +1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 +``` + +> 代码实现 + +```java +import java.util.*; + +/** + * @author wuyou + */ +public class HuffmanCode { + public static class Node implements Comparable> { + E data; + double weight; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + + @Override + public int compareTo(Node o) { + return (int) (this.weight - o.weight); + } + } + + public static void main(String[] args) { + String content = "i like like like java do you like a java"; + byte[] bytes = content.getBytes(); + List> nodes = getNodes(bytes); + Node tree = createHuffmanTree(nodes); + Map huffmanCode = getHuffmanCode(tree); + System.out.println("编码前的字节长度为:" + bytes.length); + byte[] zip = zip(bytes, huffmanCode); + System.out.println("编码为:" + Arrays.toString(zip)); + System.out.println("编码后的字节长度为:" + zip.length); + } + + /** + * 封装huffman压缩 + * @param bytes 原始的字节数组 + * @return 压缩后的字节数组 + */ + private static byte[] huffmanZip(byte[] bytes) { + List> nodes = getNodes(bytes); + // 根据nodes创建赫夫曼树 + Node tree = createHuffmanTree(nodes); + // 根据赫夫曼树得到赫夫曼编码 + Map huffmanCode = getHuffmanCode(tree); + // 根据赫夫曼编码,得到压缩后的赫夫曼编码字节数组 + byte[] zip = zip(bytes, huffmanCode); + return zip; + } + + /** + * 将字符数组通过huffman编码进行压缩 + * @param bytes 原始字符串对应的byte[] + * @param huffmanCode 对应的huffman编码Map + * @return 处理后的byte[] + */ + private static byte[] zip(byte[] bytes, Map huffmanCode) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte b : bytes) { + stringBuilder.append(huffmanCode.get(b)); + } + // 通过一个简单的算法获得len,也可以使用三目运算符 + int len = (stringBuilder.length() + 7) / 8; + byte[] result = new byte[len]; + // 因为每8位对应一个byte,所以步长+8 + for (int i = 0, j = 0; i < stringBuilder.length(); i += 8, j++) { + String b; + if (i + 8 > stringBuilder.length()) { + b = stringBuilder.substring(i); + } else { + b = stringBuilder.substring(i, i + 8); + } + // 2用于补码转换 + result[j] = (byte) Integer.parseInt(b, 2); + } + return result; + } + + /** + * 将tree的所有叶子节点的转换得到对应的赫夫曼编码Map + * @param tree 传入树的根节点 + * @return + */ + private static Map getHuffmanCode(Node tree) { + Map encodingChar = new HashMap<>(); + encodeChar(tree, "", encodingChar); + return encodingChar; + } + + /** + * 递归方法,具体方法实现获取赫夫曼编码 + * @param node + * @param encoding + * @param encodingChar + */ + private static void encodeChar(Node node, String encoding, Map encodingChar) { + // 如果是叶子节点直接返回,因为在父节点已经完成了它的编码 + if (isLeaf(node)) { + encodingChar.put(node.data, encoding); + return; + } + // 左右递归编码 + encodeChar(node.leftChild, encoding + '0', encodingChar); + encodeChar(node.rightChild, encoding + '1', encodingChar); + } + + /** + * 判断当前节点是不是叶子节点 + * @param node + * @return + */ + private static boolean isLeaf(Node node) { + if (node.rightChild == null && node.rightChild == null) { + return true; + } + return false; + } + + /** + * 创建赫夫曼树 + * @param nodes + * @return + */ + private static Node createHuffmanTree(List> nodes) { + while (nodes.size() > 1) { + Collections.sort(nodes); + // 获取权值最小的两个节点 + Node left = nodes.get(0); + Node right = nodes.get(1); + // 生成新的节点 + Node parent = new Node(null, left.weight + right.weight); + + // 将两个原来节点设置为子节点 + parent.leftChild = left; + parent.rightChild = right; + + // 删除原来两个节点 + nodes.remove(left); + nodes.remove(right); + + // 插入父节点 + nodes.add(parent); + } + return nodes.get(0); + } + + /** + * 获取字节数Node链表 + * @param array + * @return + */ + private static List> getNodes(byte[] array) { + List> nodes = new ArrayList<>(); + + HashMap counts = new HashMap<>(); + for (byte b : array) { + Integer count = counts.get(b); + if (count != null) { + counts.put(b, count + 1); + } else { + counts.put(b, 1); + } + } + for (Map.Entry entry : counts.entrySet()) { + nodes.add(new Node<>(entry.getKey(), entry.getValue())); + } + return nodes; + } + + /** + * 非递归实现前序遍历 + * @param root 根节点 + */ + private static void preOrderTraverse(Node root) { + Stack stack = new Stack<>(); + Node node = root; + while (node != null || !stack.empty()) { + if (node != null) { + System.out.print(node + " -> "); + stack.push(node); + node = node.leftChild; + } else { + Node tem = stack.pop(); + node = tem.rightChild; + } + } + } +} +``` + +输出结果: + +``` +编码前的字节长度为:40 +编码为:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] +编码后的字节长度为:17 +``` + +### 最佳实践—数据解压 + +#### 问题分析 + +> 有个关键的问题! + +- 使用赫夫曼编码进行压缩的时候,比如测试的待压缩的字符串为`hello word`,这个字符串对应的字节数组为: + +``` +[104, 101, 108, 108, 111, 32, 119, 111, 114, 100] +``` + +- 生成赫夫曼编码如下: + +``` +byte:32,string:000 +byte:114,string:001 +byte:100,string:010 +byte:101,string:011 +byte:119,string:100 +byte:104,string:101 +byte:108,string:110 +byte:111,string:111 +``` + +- 对该字符串进行压缩之后字节码字符串为: + +``` +101011110110111000100111001010 +为了方便查看,间隔如下所示: +101 011 110 110 111 000 100 111 001 010 +``` + +- 压缩之后的对应字节数组为: + +``` +[-81, 110, 39, 10] +就是上面压缩的字节码字符串对8取余,每8位存入数组byte[],最后不足8位的话:正数高位补0,负数高位补符号位 + +压缩的字节码字符串: +101011110110111000100111001010 + +为了方便查看,间隔如下所示: +10101111 01101110 00100111 001010 + +对应的字节数组为: +10101111 01101110 00100111 00001010(在高位补0) + +所以也就是: +[-81, 110, 39, 10] +``` + +- 根据压缩的字节数组进行解压,问题出现了,我们不知道压缩数组的长度,就无法准确还原!! + +``` +对字节数组[-81, 110, 39, 10]按照上面得到的赫夫曼编码进行解压 + +首先待解压的字节数组对应的二进制格式为: +10101111 01101110 00100111 00001010 + +因为之前压缩的时候最后一个字节可能不足8位,进行了补位处理,所以我们需要知道压缩前的字节码长度,才能正确的解压!!!!! + +如果不知道原来的长度,我们进行解压的使用的字符数组就变成了: +10101111 01101110 00100111 1010 + +而不是原来的: +10101111 01101110 00100111 001010 + +因为不知道字节码长度,最后一位又是正数进行了补位,所以解压的时候就出现了数据丢失: +原本最后一位001010 => 变成了1010 +``` + +- 解决办法,封装一个对象用于处理解压和压缩 + +```java +public static class HuffmanZipData { + /** + * 压缩之后的字节数组数据 + */ + byte[] data; + /** + * 压缩字符对应的huffman编码Map + */ + Map huffmanCode; + /** + * 数组压缩前的长度 + */ + int originalLength; + public HuffmanZipData(byte[] data, Map huffmanCode, int originalLength) { + this.data = data; + this.huffmanCode = huffmanCode; + this.originalLength = originalLength; + } + } +``` + +#### 代码实现 + +为了更好的实现数据压缩和解压,对压缩的代码也进行了了修改,利用了封装的`HuffmanZipData`对象,所有代码如下 + +```java +import java.util.*; + +/** + * @author wuyou + */ +public class HuffmanCode { + public static class HuffmanZipData { + /** + * 压缩之后的字节数组数据 + */ + byte[] data; + /** + * 压缩字符对应的huffman编码Map + */ + Map huffmanCode; + /** + * 数组压缩前的长度 + */ + int originalLength; + public HuffmanZipData(byte[] data, Map huffmanCode, int originalLength) { + this.data = data; + this.huffmanCode = huffmanCode; + this.originalLength = originalLength; + } + } + + public static class Node implements Comparable> { + E data; + double weight; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + + @Override + public int compareTo(Node o) { + return (int) (this.weight - o.weight); + } + } + + public static void main(String[] args) { + String content = "hello word\n" + + "世界弥漫着焦躁不安的气息,因为每一个人都急于从自己的枷锁中解放出来。\n" + + "--尼采\n" + + "你还要警惕自己内心泛滥的爱,孤独的人总会迫不及待地向与他邂逅的人伸出自己的手。\n" + + "--尼采"; + byte[] bytes = content.getBytes(); + HuffmanZipData huffmanZipData = huffmanZip(bytes); + + byte[] uZip = huffmanUZip(huffmanZipData); + System.out.println("解码后的字符为:\n" + new String(uZip)); + } + + /** + * 封装huffman解压缩 + * @param huffmanZipData 压缩对象 + * @return + */ + private static byte[] huffmanUZip(HuffmanZipData huffmanZipData) { + String s = bytesToString(huffmanZipData.data, huffmanZipData.originalLength); + byte[] decode = decode(huffmanZipData.huffmanCode, s); + return decode; + } + + /** + * 封装huffman压缩 + * @param bytes 原始的字节数组 + * @return 压缩后的字节数组 + */ + private static HuffmanZipData huffmanZip(byte[] bytes) { + List> nodes = getNodes(bytes); + // 根据nodes创建赫夫曼树 + Node tree = createHuffmanTree(nodes); + // 根据赫夫曼树得到赫夫曼编码 + Map huffmanCode = getHuffmanCode(tree); + // 根据赫夫曼编码,得到压缩后的赫夫曼编码字节数组 + return zip(bytes, huffmanCode); + } + + /** + * 对字符串进行解码 + * @param huffmanCode huffman编码列表 + * @param s + * @return + */ + private static byte[] decode(Map huffmanCode, String s) { + Map map = new HashMap<>(); + // 反向赋值map + for (Map.Entry entry : huffmanCode.entrySet()) { + map.put(entry.getValue(), entry.getKey()); + } + List list = new ArrayList<>(); + for (int i = 0, j = 0; i < s.length() && j <= s.length();) { + String temp = s.substring(i, j); + Byte tempByte = map.get(temp); + if (tempByte == null) { + j++; + } else { + list.add(tempByte); + i = j; + } + } + // 因为IO流读取文件是通过数组,所以返回要转成数组 + byte[] result = new byte[list.size()]; + for (int i = 0; i < list.size(); i++) { + result[i] = list.get(i); + } + return result; + } + + /** + * 将byte数组转成对应的二进制字符串 + * @param bytes 赫夫曼编码得到的字节数组 + * @return 原来的字符串对应的数组 + */ + private static String bytesToString(byte[] bytes, int originalLength) { + // 先得到 bytes对应的二进制字符串 + StringBuilder stringBuilder = new StringBuilder(); + // 将byte数组转成二进制的字符串 + // 负数需要符号位,所以一定是8位 + for (int i = 0; i < bytes.length - 1; i++) { + stringBuilder.append(byteToBitString(bytes[i])); + } + stringBuilder.append(byteToBitString(bytes[bytes.length - 1], originalLength)); + return stringBuilder.toString(); + } + + /** + * + * 将一个byte 转成一个二进制的字符串 + * @param b 传入的byte + * @return b对应的二进制的字符串,按照补码返回 + */ + private static String byteToBitString(byte b) { + // 使用变量保存b + int temp = b; + // 如果是正数,需要进行补高位 + // 按位或 1 0000 0000 | 0000 0001 = 1 0000 0001, (256 | 1 = 257), 然后截取后面8位 + temp |= 256; + // 返回temp对应的二进制的补码 + String s = Integer.toBinaryString(temp); + return s.substring(s.length() - 8); + } + + /** + * 重载方法,处理最后一个字节 + * @param b + * @param originalLength + * @return + */ + private static String byteToBitString(byte b, int originalLength) { + // 使用变量保存b + int temp = b; + // 如果是正数,需要进行补高位 + // 按位或 1 0000 0000 | 0000 0001 = 1 0000 0001, (256 | 1 = 257), 然后截取后面8位 + // 如果是最后一位,并且是非负数,不需要补高位和截取后面8位,但是位数不确定需要判断 + if (b >= 0 && b <= Byte.MAX_VALUE) { + // 计算需要补位的位数 + int endCount = originalLength % 8; + // 如果最后也是8位的正数,也需要补齐到8位 + endCount = (endCount == 0 ? 8 : endCount); + // 得到进行或运算的操作基数,比如补到6位就需要与 100 0000(2^6)进行或运算 + int renRadix = (int) Math.pow(2, endCount); + temp |= renRadix; + String s = Integer.toBinaryString(temp); + return s.substring(s.length() - endCount); + } + temp |= 256; + // 返回temp对应的二进制的补码 + String s = Integer.toBinaryString(temp); + return s.substring(s.length() - 8); + } + + /** + * 将字符数组通过huffman编码进行压缩 + * @param bytes 原始字符串对应的byte[] + * @param huffmanCode 对应的huffman编码Map + * @return 处理后的byte[] + */ + private static HuffmanZipData zip(byte[] bytes, Map huffmanCode) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte b : bytes) { + stringBuilder.append(huffmanCode.get(b)); + } + // 原本的字符串长度 + int originalLength = stringBuilder.length(); + // 通过一个简单的算法获得len,也可以使用三目运算符 + int len = (stringBuilder.length() + 7) / 8; + byte[] result = new byte[len]; + // 因为每8位对应一个byte,所以步长+8 + for (int i = 0, j = 0; i < stringBuilder.length(); i += 8, j++) { + String b; + if (i + 8 > stringBuilder.length()) { + b = stringBuilder.substring(i); + } else { + b = stringBuilder.substring(i, i + 8); + } + // 2用于补码转换 + result[j] = (byte) Integer.parseInt(b, 2); + } + return new HuffmanZipData(result, huffmanCode, originalLength); + } + + /** + * 将tree的所有叶子节点的转换得到对应的赫夫曼编码Map + * @param tree 传入树的根节点 + * @return + */ + private static Map getHuffmanCode(Node tree) { + Map encodingChar = new HashMap<>(); + encodeChar(tree, "", encodingChar); + return encodingChar; + } + + /** + * 递归方法,具体方法实现获取赫夫曼编码 + * @param node + * @param encoding + * @param encodingChar + */ + private static void encodeChar(Node node, String encoding, Map encodingChar) { + // 如果是叶子节点直接返回,因为在父节点已经完成了它的编码 + if (isLeaf(node)) { + encodingChar.put(node.data, encoding); + return; + } + // 左右递归编码 + encodeChar(node.leftChild, encoding + '0', encodingChar); + encodeChar(node.rightChild, encoding + '1', encodingChar); + } + + /** + * 判断当前节点是不是叶子节点 + * @param node + * @return + */ + private static boolean isLeaf(Node node) { + if (node.rightChild == null && node.rightChild == null) { + return true; + } + return false; + } + + /** + * 创建赫夫曼树 + * @param nodes + * @return + */ + private static Node createHuffmanTree(List> nodes) { + while (nodes.size() > 1) { + Collections.sort(nodes); + // 获取权值最小的两个节点 + Node left = nodes.get(0); + Node right = nodes.get(1); + // 生成新的节点 + Node parent = new Node(null, left.weight + right.weight); + + // 将两个原来节点设置为子节点 + parent.leftChild = left; + parent.rightChild = right; + + // 删除原来两个节点 + nodes.remove(left); + nodes.remove(right); + + // 插入父节点 + nodes.add(parent); + } + return nodes.get(0); + } + + /** + * 获取字节数Node链表 + * @param array + * @return + */ + private static List> getNodes(byte[] array) { + List> nodes = new ArrayList<>(); + + HashMap counts = new HashMap<>(); + for (byte b : array) { + Integer count = counts.get(b); + if (count != null) { + counts.put(b, count + 1); + } else { + counts.put(b, 1); + } + } + for (Map.Entry entry : counts.entrySet()) { + nodes.add(new Node<>(entry.getKey(), entry.getValue())); + } + return nodes; + } + + /** + * 非递归实现前序遍历 + * @param root 根节点 + */ + private static void preOrderTraverse(Node root) { + Stack stack = new Stack<>(); + Node node = root; + while (node != null || !stack.empty()) { + if (node != null) { + System.out.print(node + " -> "); + stack.push(node); + node = node.leftChild; + } else { + Node tem = stack.pop(); + node = tem.rightChild; + } + } + } +} +``` + +#### 输出结果 + +``` +解码后的字符为: +hello word +世界弥漫着焦躁不安的气息,因为每一个人都急于从自己的枷锁中解放出来。 +--尼采 +你还要警惕自己内心泛滥的爱,孤独的人总会迫不及待地向与他邂逅的人伸出自己的手。 +--尼采 +``` + +### 最佳实践——文件压缩 + +#### 代码实现 + +```java +import java.io.*; + +/** + * @author wuyou + */ +public class FileZipDemo { + public static void main(String[] args) throws IOException { + test1(); + } + + /** + * 测试文件压缩 + * @throws IOException + */ + private static void test1() throws IOException { + // 测试压缩 + String s = "E:\\文件\\JavaScript百炼成仙.pdf"; + String d = "E:\\文件\\JavaScript百炼成仙.zip"; + zipFile(s, d); + } + + /** + * 实现文件压缩操作 + * @param srcFile + * @param dstFile + */ + public static void zipFile(String srcFile, String dstFile) throws IOException { + // 创建输出输出流 + InputStream inputStream = null; + OutputStream outputStream = null; + ObjectOutputStream objectOutputStream = null; + try { + // 初始化输入输出流 + inputStream = new FileInputStream(srcFile); + outputStream = new FileOutputStream(dstFile); + objectOutputStream = new ObjectOutputStream(outputStream); + + // 读取文件 + byte[] srcByte = new byte[inputStream.available()]; + inputStream.read(srcByte); + + // 得到文件对应的赫夫曼编码对象 + HuffmanCode.HuffmanZipData zip = HuffmanCode.huffmanZip(srcByte); + + // 进行写入 + objectOutputStream.writeObject(zip); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + if (objectOutputStream != null) { + objectOutputStream.close(); + } + } + } +} + +``` + +输出结果: + +![image-20211231194359684](数据结构与算法/image-20211231194359684.png) + +### 最佳实践——文件解压缩 + +#### 代码实现 + +```java +import java.io.*; + +/** + * @author wuyou + */ +public class FileZipDemo { + public static void main(String[] args) throws IOException { + test2(); + } + + /** + * 测试文件解压缩 + * @throws IOException + */ + private static void test2() throws IOException { + // 测试解压缩 + String s = "E:\\文件\\JavaScript百炼成仙.zip"; + String d = "E:\\文件\\JavaScript百炼成仙(1).pdf"; + unZipFile(s, d); + } + + /** + * 实现文件解压缩操作 + * @param zipFile + * @param dstFile + * @throws IOException + */ + public static void unZipFile(String zipFile, String dstFile) throws IOException { + // 定义文件输入输出流 + InputStream inputStream = null; + OutputStream outputStream = null; + ObjectInputStream objectInputStream = null; + + try { + // 初始化输入输出流 + inputStream = new FileInputStream(zipFile); + objectInputStream = new ObjectInputStream(inputStream); + outputStream = new FileOutputStream(dstFile); + + // 读取对象 + HuffmanCode.HuffmanZipData huffmanZipData = (HuffmanCode.HuffmanZipData) objectInputStream.readObject(); + + // 进行解码 + byte[] uZip = HuffmanCode.huffmanUZip(huffmanZipData); + + // 写数据到文件 + outputStream.write(uZip); + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + if (objectInputStream != null) { + objectInputStream.close(); + } + } + } +} +``` + +输出结果: + +![image-20211231200015376](数据结构与算法/image-20211231200015376.png) + +### 赫夫曼编码压缩文件注意事项 + +- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt 、pdf等文件 + +- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) + +- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显 + +## 二叉排序树 + +### 分析 + +> 使用数组 + +- 数组未排序 + - 优点:直接在数组尾添加,速度快 + - 缺点:查找速度慢 + +- 数组排序 + - 优点:可以使用二分查找,查找速度快 + - 缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢 + +> 使用链式存储—链表 + +- 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动 +- 使用二叉排序树 + +> 二叉排序树 + +BST: (Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大 + +**特别说明**:如果有相同的值,可以将该节点放在左子节点或右子节点 + +比如针对数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为: + +![image-20211231201625846](数据结构与算法/image-20211231201625846.png) + +### 二叉排序树创建和遍历 + +一个数组**创建**成对应的二叉排序树,并使用**中序遍历二叉排序树**,比如: 数组为 *Array*(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 : + +![image-20211231202151469](数据结构与算法/image-20211231202151469.png) + +> 代码实现 + +```java +import java.util.Stack; + +/** + * @author wuyou + */ +public class BinarySortTreeDemo { + public static class Node implements Comparable> { + E data; + double weight; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + + @Override + public int compareTo(Node o) { + return (int) (this.weight - o.weight); + } + } + + public static void main(String[] args) { + int[] array = {7, 3, 10, 12, 5, 1, 9}; + Node tree = new Node<>(null, array[0]); + for (int i = 1; i < array.length; i++) { + Node node = new Node<>(null, array[i]); + add(tree, node); + } + infixOrderTraverse(tree); + } + + /** + * 向二叉排序树中添加节点 + * @param tree + * @param node + */ + private static void add(Node tree, Node node) { + if (node == null) { + return; + } + if (node.weight < tree.weight) { + // 如果当前左子节点为null + if (tree.leftChild == null) { + tree.leftChild = node; + } else { + // 递归的向左子树添加 + add(tree.leftChild, node); + } + } else { + if (tree.rightChild == null) { + tree.rightChild = node; + } else { + // 递归向右子树添加 + add(tree.rightChild, node); + } + } + } + + /** + * 非递归实现中序遍历 + * @param root 根节点 + */ + private static void infixOrderTraverse(Node root) { + Stack stack = new Stack<>(); + Node node = root; + while (node != null || !stack.empty()) { + if (node != null) { + stack.push(node); + node = node.leftChild; + } else { + Node temp = stack.pop(); + System.out.print(temp + " -> "); + node = temp.rightChild; + } + } + } +} +``` + +输出结果: + +``` +Node[data=null, weight=1.0] -> Node[data=null, weight=3.0] -> Node[data=null, weight=5.0] -> Node[data=null, weight=7.0] -> Node[data=null, weight=9.0] -> Node[data=null, weight=10.0] -> Node[data=null, weight=12.0] -> +``` + +- 中序遍历的结果刚好是递增的 + +### 二叉排序树的删除 + +二叉排序树的删除情况比较复杂,有下面三种情况需要考虑: + +- 删除叶子节点 (比如:2,5,9,12) +- 删除只有一颗子树的节点 (比如:1) +- 删除有两颗子树的节点. (比如:7,3,10) + +![image-20211231225803400](数据结构与算法/image-20211231225803400.png) + +#### 思路分析 + +> 第一种情况: 删除叶子节点,比如:(2, 5, 9, 12) + +- 需求先去找到要删除的结点 targetNode +- 找到 targetNode 的 父结点 parent +- 确定 targetNode 是 parent 的左子结点 还是右子结点 +- 根据前面的情况来对应删除 左子结点 parent.left = null 右子结点 parent.right = null; + +> 第二种情况: 删除只有一颗子树的节点,比如(1) + +- 需求先去找到要删除的结点 targetNode +- 找到 targetNode 的 父结点 parent +- 确定 targetNode 的子结点是左子结点还是右子结点 +- targetNode 是 parent 的左子结点还是右子结点 +- 如果 targetNode 有左子结点 + - 如果 targetNode 是 parent 的左子结点 parent.left = targetNode.left + - 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.left + +- 如果 targetNode 有右子结点 + + - 如果 targetNode 是 parent 的左子结点 parent.left = targetNode.right + + - 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.right + +> 情况三 : 删除有两颗子树的节点,比如:(7, 3,10 ) + +- 需求先去找到要删除的结点 targetNode +- 找到 targetNode 的 父结点 parent +- 从 targetNode 的右子树找到最小的结点 +- 用一个临时变量,将最小结点的值保存 temp +- 删除该最小结点 +- targetNode.value = temp + +#### 代码实现 + +```java +import java.util.Stack; + +/** + * @author wuyou + */ +public class BinarySortTreeDemo { + public static class Node implements Comparable> { + E data; + double weight; + Node parent; + Node leftChild; + Node rightChild; + + public Node(E data, double weight) { + super(); + this.data = data; + this.weight = weight; + } + + @Override + public String toString() { + return "Node[data=" + data + ", weight=" + weight + "]"; + } + + @Override + public int compareTo(Node o) { + return (int) (this.weight - o.weight); + } + + public Node deleteLeaf() { + Node parent = this.parent; + if (parent != null) { + parent.deleteChild(this); + } + return parent; + } + + private void deleteChild(Node node) { + if (this.leftChild == node) { + this.leftChild = null; + return; + } + if (this.rightChild == node) { + this.rightChild = null; + } + } + + public boolean isLeftChild(Node node) { + return this.leftChild == node; + } + + public void swap(Node node) { + this.data = (E) node.data; + this.weight = node.weight; + } + } + + + public static void main(String[] args) { + int[] array = {7, 3, 10, 1, 5, 9 , 12, 2, 11}; + Node tree = new Node<>("节点1", array[0]); + for (int i = 1; i < array.length; i++) { + Node node = new Node<>("节点" + (i + 1), array[i]); + add(tree, node); + } + infixOrderTraverse(tree); + + testSolution1(tree); + } + + private static void testSolution1(Node tree) { + System.out.println("\n测试删除节点第一种情况:"); + tree = delete(tree, 2); + infixOrderTraverse(tree); + } + + private static void testSolution2(Node tree) { + System.out.println("\n测试删除节点第二种情况:"); + tree = delete(tree, 1); + infixOrderTraverse(tree); + } + + private static void testSolution3(Node tree) { + System.out.println("\n测试删除节点第三种情况:"); + tree = delete(tree, 3); + tree = delete(tree, 7); + infixOrderTraverse(tree); + } + + /** + * 删除二叉排序树指定的节点 + * @param tree + * @param value + * @return + */ + public static Node delete(Node tree, int value) { + Node node = tree; + Node search = search(node, value); + if (search == null) { + return tree; + } else { + // 如果删除节点为叶子节点 + if (search.leftChild == null && search.rightChild == null) { + // 如果删除叶子节点返回父节点为null,说明是根节点,并且整棵树就只有这一个节点,所以返回null + return search.deleteLeaf() != null ? tree : null; + } else { + // 删除有两颗子树的节点 + if (search.leftChild != null && search.rightChild != null) { + // 找到右子树最小的节点 + Node min = findRightTreeMin(search.rightChild); + // 将最小的值的节点与当前查找的节点进行交换值 + search.swap(min); + // 调用方法删除最小节点 + Node parent = min.parent; + // 需要判断右子树最小的节点的parent是不是与search相等,如果不判断直接执行parent.deleteChild(min);可能造成数据丢失,比如右子树是一个递增序列的情况 + if (parent == search) { + Node minRightChild = min.rightChild; + parent.rightChild = minRightChild; + // 要对minRightChild进行判断,及时更新parent + if (minRightChild != null) { + minRightChild.parent = parent; + } + } else { + parent.deleteChild(min); + } + return tree; + } + // 删除只有一颗子树的节点 + else { + Node parent = search.parent; + Node temp = search.leftChild != null ? search.leftChild : search.rightChild; + if (parent != null) { + if (parent.isLeftChild(search)) { + parent.leftChild = temp; + } + else { + parent.rightChild = temp; + } + // 别忘记了parent也要更新 + temp.parent = parent; + return tree; + } else { + // 说明当前查找到要删除的点已经是根节点了,删除当前节点然后返回它的子节点就行了 + return temp; + } + } + } + } + } + + /** + * 找到右子树的最小值的节点 + * @param rightTree 右子树 + * @return 右子树最小的节点 + */ + public static Node findRightTreeMin(Node rightTree) { + Node node = rightTree; + // 循环查找左节点 + while (node.leftChild != null) { + node = node.leftChild; + } + return node; + } + + /** + * 查找要删除的节点 + * @param value + * @return 找到返回节点,否则返回null + */ + public static Node search(Node tree, int value) { + Node node = tree; + while (node != null) { + if (value == node.weight) { + return node; + } else if (value < node.weight) { + node = node.leftChild; + } else { + node = node.rightChild; + } + } + return null; + } + + /** + * 向二叉排序树中添加节点 + * @param tree + * @param node + */ + public static void add(Node tree, Node node) { + if (node == null) { + return; + } + if (node.weight < tree.weight) { + // 如果当前左子节点为null + if (tree.leftChild == null) { + tree.leftChild = node; + node.parent = tree; + } else { + // 递归的向左子树添加 + add(tree.leftChild, node); + } + } else { + if (tree.rightChild == null) { + tree.rightChild = node; + node.parent = tree; + } else { + // 递归向右子树添加 + add(tree.rightChild, node); + } + } + } + + /** + * 非递归实现中序遍历 + * @param root 根节点 + */ + public static void infixOrderTraverse(Node root) { + Stack stack = new Stack<>(); + Node node = root; + while (node != null || !stack.empty()) { + if (node != null) { + stack.push(node); + node = node.leftChild; + } else { + Node temp = stack.pop(); + System.out.print(temp + " -> "); + node = temp.rightChild; + } + } + } +} +``` + +输出结果: + +``` +Node[data=节点4, weight=1.0] -> Node[data=节点8, weight=2.0] -> Node[data=节点2, weight=3.0] -> Node[data=节点5, weight=5.0] -> Node[data=节点1, weight=7.0] -> Node[data=节点6, weight=9.0] -> Node[data=节点3, weight=10.0] -> Node[data=节点9, weight=11.0] -> Node[data=节点7, weight=12.0] -> +测试删除节点第一种情况: +Node[data=节点4, weight=1.0] -> Node[data=节点2, weight=3.0] -> Node[data=节点5, weight=5.0] -> Node[data=节点1, weight=7.0] -> Node[data=节点6, weight=9.0] -> Node[data=节点3, weight=10.0] -> Node[data=节点9, weight=11.0] -> Node[data=节点7, weight=12.0] -> +测试删除节点第二种情况: +Node[data=节点8, weight=2.0] -> Node[data=节点2, weight=3.0] -> Node[data=节点5, weight=5.0] -> Node[data=节点1, weight=7.0] -> Node[data=节点6, weight=9.0] -> Node[data=节点3, weight=10.0] -> Node[data=节点9, weight=11.0] -> Node[data=节点7, weight=12.0] -> +测试删除节点第三种情况: +Node[data=节点4, weight=1.0] -> Node[data=节点8, weight=2.0] -> Node[data=节点5, weight=5.0] -> Node[data=节点6, weight=9.0] -> Node[data=节点3, weight=10.0] -> Node[data=节点9, weight=11.0] -> Node[data=节点7, weight=12.0] -> +Process finished with exit code 0 +``` + +## 平衡二叉树 + +### 基本介绍 + +> 看一个案例(说明二叉排序树可能的问题) + +给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST),并分析问题所在 + +![image-20220105172309410](数据结构与算法/image-20220105172309410.png) + +存在的问题分析: + +- 左子树全部为空,从形式上看,更像一个单链表 +- 插入速度没有影响 +- 查询速度明显降低(因为需要依次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢 +- 解决方案-平衡二叉树(AVL) + +> 基本介绍 + +- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以**保证查询效率较高** + +- 具有以下**特点**:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树平衡二叉树的常用实现方法有[红黑树](https://baike.baidu.com/item/红黑树/2413209)、[AVL](https://baike.baidu.com/item/AVL/7543015)、[替罪羊树](https://baike.baidu.com/item/替罪羊树/13859070)、[Treap](https://baike.baidu.com/item/Treap)、[伸展树](https://baike.baidu.com/item/伸展树/7003945)等。 + +- 举例说明,看看下面哪些AVL树,为什么?(第1、2个树是,第3个树不是) + +![image-20220105172537892](数据结构与算法/image-20220105172537892.png) + +- 这种左右子树的高度相差不超过 1 的树为平衡二叉树 + +### AVL树的平衡调整 + +转载链接:https://blog.csdn.net/isunbin/article/details/81707606 + +整个实现过程是通过在一棵平衡二叉树中依次插入元素(按照二叉排序树的方式),若出现不平衡,则要根据新插入的结点与最低不平衡结点的位置关系进行相应的调整。分为LL型、RR型、LR型和RL型4种类型。 + +#### LL型调整 + +> 由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下图是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。 + +![img](数据结构与算法/20150818212028853.png) + +>LL型调整的一般形式如下图所示,表示在A的左孩子B的左子树BL(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下: +> +>①将A的左孩子B提升为新的根结点; +> +>②将原来的根结点A降为B的右孩子; +> +>③各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。 + +![img](数据结构与算法/20150818221513880.png) + +> 代码实现 + +![img](数据结构与算法/20180816194335560.png) + +```c +BTNode *ll_rotate(BTNode *y) +{ + BTNode *x = y->left; + y->left = x->right; + x->right = y; + + y->height = max(height(y->left), height(y->right)) + 1; + x->height = max(height(x->left), height(x->right)) + 1; + + return x; +} +``` + +#### RR型调整 + +> 由于在A的右孩子(R)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。 + +![img](数据结构与算法/20150818215441436.png) + +> RR型调整的一般形式如下图所示,表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下: +> +> - 将A的右孩子B提升为新的根结点; +> - 将原来的根结点A降为B的左孩子; +> - 各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。 + +![img](数据结构与算法/20150818220942825.png) + +​ + +> 代码实现 + +![img](数据结构与算法/20180816194715558.png) + +```c +BTNode *rr_rotate(struct Node *y) +{ + BTNode *x = y->right; + y->right = x->left; + x->left = y; + + + y->height = max(height(y->left), height(y->right)) + 1; + x->height = max(height(x->left), height(x->right)) + 1; + + return x; +} +``` + +#### LR型调整 + +> 由于在A的左孩子(L)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。下图是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。 + +![img](数据结构与算法/20150818222514855.png) + +>LR型调整的一般形式如下图所示,表示在A的左孩子B的右子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下: +> +>①将B的左孩子C提升为新的根结点; +> +>②将原来的根结点A降为C的右孩子; +> +>③各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。 + +![img](数据结构与算法/20150818224419149.png) + +> 代码实现 + +![img](数据结构与算法/20180816195733482.png) + +```c +BTNode* lr_rotate(BTNode* y) +{ + BTNode* x = y->left; + y->left = rr_rotate(x); + return ll_rotate(y); +} +``` + +#### RL型调整 + +> 由于在A的右孩子(R)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。 + +![img](数据结构与算法/20150818224940731.png) + +> RL型调整的一般形式如下图所示,表示在A的右孩子B的左子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下: +> +> ①将B的左孩子C提升为新的根结点; +> +> ②将原来的根结点A降为C的左孩子; +> +> ③各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。 + +![img](数据结构与算法/20150818230041580.png) + +> 代码实现 + +![img](数据结构与算法/20180816200720830.png) + +```c +Node* rl_rotate(Node* y) +{ + Node * x = y->right; + y->right = ll_rotate(x); + return rr_rotate(y); +} +``` + +#### 旋转总结 + +- 右旋:最小不平衡子树的BF和它的子树BF符号相同且最小不平衡子树的BF大于0 +- 左旋:最小不平衡子树的BF和它的子树BF符号相同且最小不平衡子树的BF小于零 +- 左右旋:最小不平衡子树的BF与它的子树的BF符号相反时且最小不平衡子树的BF大于0时,需要对节点先进行一次向左旋使得符号相同后,在向右旋转一次完成平衡操作 +- 右左旋:最小不平衡子树的BF与它的子树的BF符号相反时且最小不平衡子树的BF小于0时,需要对节点先进行一次向右旋转使得符号相同时,在向左旋转一次完成平衡操作 + +![在这里插入图片描述](数据结构与算法/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpc2hhbmxlaWxpeGlu,size_16,color_FFFFFF,t_70.png) + +### 代码实现 + +代码继承原来的`BinarySortTreeDemo`类,并且新增一个`Node`的构造方法 + +```java +public Node(Node parent, E data, double weight) { + this.parent = parent; + this.data = data; + this.weight = weight; +} +``` + +代码如下: + +```java +import java.util.LinkedList; +import java.util.Queue; + +/** + * @author wuyou + */ +public class AVLTreeDemo extends BinarySortTreeDemo{ + private Node root; + private final int LEFT = 1; + private final int RIGHT = -1; + private final int MAX_LEFT = 2; + private final int MAX_RIGHT = -2; + + public static void main(String[] args) { + AVLTreeDemo bbt = new AVLTreeDemo(); + bbt.put(3); + bbt.put(2); + bbt.put(1); + bbt.put(4); + bbt.put(5); + bbt.put(6); + bbt.put(7); + bbt.put(10); + bbt.put(9); + bbt.midOrderErgodic(); + System.out.println(); + System.out.println("-----各节点平衡状况-----"); + bbt.sequenceErgodic(); + System.out.println(); + + bbt.delete(5); + bbt.delete(2); + bbt.midOrderErgodic(); + System.out.println(); + System.out.println("-----各节点平衡状况-----"); + bbt.sequenceErgodic(); + System.out.println(); + } + + /** + * 插入节点 + * @param weight + */ + public void put(int weight) { + putData(root, weight); + } + + private boolean putData(Node node, int weight) { + if(node == null) { + node = new Node(null, weight); + root = node; + return true; + } + int t; + Node tempBefore; + Node temp = node; + do { + tempBefore = temp; + t = (int) (temp.weight - weight); + if(t < 0) { + temp = temp.rightChild; + } + else if(t > 0) { + temp = temp.leftChild; + } + else { + // 说明是已经包含相同值,不允许插入 + return false; + } + } while(temp != null); + Node newNode = new Node(tempBefore, null, weight); + if(t < 0) { + tempBefore.rightChild = newNode; + } + else if(t > 0) { + tempBefore.leftChild = newNode; + } + rebuild(tempBefore); + return true; + + } + + /** + * 平衡二叉树的方法 + * @param node + */ + public void rebuild(Node node) { + // 向parent循环判断是否需要需要旋转 + while(node != null) { + if(calcNodeBalanceValue(node) == MAX_LEFT) { + fixAfterInsertion(node, LEFT); + } + else if(calcNodeBalanceValue(node) == MAX_RIGHT) { + fixAfterInsertion(node, RIGHT); + } + node = node.parent; + } + } + + /** + * 调整树的结构 + * @param node + * @param type + */ + public void fixAfterInsertion(Node node, int type) { + if(type == LEFT) { + Node leftChild = node.leftChild; + // 右旋 + if(leftChild.leftChild != null) { + rightRotation(node); + } + // 左右旋 + else if(leftChild.rightChild != null) { + leftRotation(leftChild); + rightRotation(node); + } + } + else if(type == RIGHT) { + Node rightChild = node.rightChild; + // 左旋 + if(rightChild.rightChild != null) { + leftRotation(node); + } + // 右左旋 + else if(rightChild.leftChild != null) { + rightRotation(rightChild); + leftRotation(node); + } + } + } + + public Node rightRotation(Node node) { + if (node != null) { + Node leftChild = node.leftChild; + node.leftChild = leftChild.rightChild; + // 如果leftChild的右节点存在,则需要将该右节点的父节点指定给node节点 + if (leftChild.rightChild != null) { + leftChild.rightChild.parent = node; + } + leftChild.parent = node.parent; + if (node.parent == null) { + this.root = leftChild; + } + // 即node节点在它原父节点的右子树中 + else if (node.parent.rightChild == node) { + node.parent.rightChild = leftChild; + } + else if (node.parent.leftChild == node) { + node.parent.leftChild = leftChild; + } + leftChild.rightChild = node; + node.parent = leftChild; + return leftChild; + } + return null; + } + + public Node leftRotation(Node node) { + if (node != null) { + Node rightChild = node.rightChild; + node.rightChild = rightChild.leftChild; + if (rightChild.leftChild != null) { + rightChild.leftChild.parent = node; + } + rightChild.parent = node.parent; + if(node.parent == null) { + this.root = rightChild; + } + else if(node.parent.rightChild == node) { + node.parent.rightChild = rightChild; + } + else if(node.parent.leftChild == node) { + node.parent.leftChild = rightChild; + } + rightChild.leftChild = node; + node.parent = rightChild; + return rightChild; + } + return null; + } + + /** + * 计算node节点的BF值 + * @param node + * @return + */ + public int calcNodeBalanceValue(Node node) { + if(node != null) { + return getHeightByNode(node); + } + return 0; + } + + private int getHeightByNode(Node node) { + return getChildDepth(node.leftChild) - getChildDepth(node.rightChild); + } + + private int getChildDepth(Node node) { + if(node == null) { + return 0; + } + return 1 + Math.max(getChildDepth(node.leftChild), getChildDepth(node.rightChild)); + } + + /** + * 删除指定val值的节点 + * @param val + * @return + */ + public boolean delete(int val) { + Node node = getNode(val); + if(node == null) { + return false; + } + boolean flag = false; + Node p = null; + Node parent = node.parent; + Node leftChild = node.leftChild; + Node rightChild = node.rightChild; + if(leftChild == null && rightChild == null) { + if(parent != null) { + if(parent.leftChild == node) { + parent.leftChild = null; + } + else if(parent.rightChild == node) { + parent.rightChild = null; + } + } + else { + this.root = null; + } + + p = parent; + node = null; + flag = true; + } + else if(leftChild == null && rightChild != null) { + if(parent != null && parent.weight > val) { + parent.leftChild = rightChild; + } + else if(parent != null && parent.weight < val) { + parent.rightChild = rightChild; + } + else { + this.root = rightChild; + } + p = parent; + node = null; + flag = true; + } + else if(leftChild != null && rightChild == null) { + if(parent != null && parent.weight > val) { + parent.leftChild = leftChild; + } + else if(parent != null && parent.weight < val) { + parent.rightChild = leftChild; + } + else { + this.root = leftChild; + } + + p = parent; + node = null; + flag = true; + } + else if(leftChild != null && rightChild != null) { + Node successor = getSuccessor(node); + int tempData = (int) successor.weight; + if(delete(tempData)) { + node.data = tempData; + } + p = successor; + successor = null; + flag = true; + } + + if(flag) { + this.rebuild(p); + } + return true; + } + + + /** + * 获得指定节点 + * @param key + * @return + */ + public Node getNode(int key) { + + Node node = root; + int t; + do { + t = (int) (node.weight - key); + if(t > 0) { + node = node.leftChild; + } + else if(t < 0) { + node = node.rightChild; + } + else { + return node; + } + } while(node != null); + return null; + } + + /** + * 获得指定节点的后继 + * 找到node节点的后继节点 + * 1、先判断该节点有没有右子树,如果有,则从右节点的左子树中寻找后继节点,没有则进行下一步 + * 2、查找该节点的父节点,若该父节点的右节点等于该节点,则继续寻找父节点, + * 直至父节点为Null或找到不等于该节点的右节点。 + * 理由,后继节点一定比该节点大,若存在右子树,则后继节点一定存在右子树中,这是第一步的理由 + * 若不存在右子树,则也可能存在该节点的某个祖父节点(即该节点的父节点,或更上层父节点)的右子树中, + * 对其迭代查找,若有,则返回该节点,没有则返回null + * @param node + * @return + */ + public Node getSuccessor(Node node) { + if(node.rightChild != null) { + Node rightChild= node.rightChild; + while(rightChild.leftChild != null) { + rightChild = rightChild.leftChild; + } + return rightChild; + } + Node parent = node.parent; + while(parent != null && (node == parent.rightChild)) { + node = parent; + parent = parent.parent; + } + return parent; + } + + /** + * 中序遍历 + */ + public void midOrderErgodic() { + this.midOrderErgodic(this.root); + } + + private void midOrderErgodic(Node node) { + if(node != null) { + this.midOrderErgodic(node.leftChild); + System.out.print(node.weight + ", "); + this.midOrderErgodic(node.rightChild); + } + } + + /** + * 层序遍历 + */ + public void sequenceErgodic() { + if(this.root == null) { + return; + } + Queue queue = new LinkedList<>(); + Node temp = null; + queue.add(this.root); + while(!queue.isEmpty()) { + temp = queue.poll(); + System.out.println("当前节点值:" + temp.weight + ", 左子树深度:" + getChildDepth(temp.leftChild) + ", 右子树深度:" + getChildDepth(temp.rightChild) + ", BF:" + calcNodeBalanceValue(temp)); + if(temp.leftChild != null) { + queue.add(temp.leftChild); + } + if(temp.rightChild != null) { + queue.add(temp.rightChild); + } + } + } +} +``` + +输出结果: + +``` +1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0, +-----各节点平衡状况----- +当前节点值:4.0, 左子树深度:2, 右子树深度:3, BF:-1 +当前节点值:2.0, 左子树深度:1, 右子树深度:1, BF:0 +当前节点值:6.0, 左子树深度:1, 右子树深度:2, BF:-1 +当前节点值:1.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:3.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:5.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:9.0, 左子树深度:1, 右子树深度:1, BF:0 +当前节点值:7.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:10.0, 左子树深度:0, 右子树深度:0, BF:0 + +1.0, 2.0, 4.0, 6.0, 7.0, 9.0, 10.0, +-----各节点平衡状况----- +当前节点值:4.0, 左子树深度:2, 右子树深度:3, BF:-1 +当前节点值:2.0, 左子树深度:1, 右子树深度:0, BF:1 +当前节点值:9.0, 左子树深度:2, 右子树深度:1, BF:1 +当前节点值:1.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:6.0, 左子树深度:0, 右子树深度:1, BF:-1 +当前节点值:10.0, 左子树深度:0, 右子树深度:0, BF:0 +当前节点值:7.0, 左子树深度:0, 右子树深度:0, BF:0 +``` + +- 可以发现每次新增节点和删除节点都会对平衡性进行检查,判断是否需要旋转 + +## 多路查找树 + +### 基本介绍 + +> 存在的问题分析 + +二叉树的操作效率较高,但是也存在问题,请看下面的二叉树 + +![image-20220106104432336](数据结构与算法/image-20220106104432336.png) + +二叉树需要加载到内存的,如果二叉树的节点少没什么问题,但是如果二叉树的节点很多(比如1亿)就存在如下问题: + +- 问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响 +- 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度 + +> 多叉树 + +- 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree) +- 接下来的`2-3树`,`2-3-4树`就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化 +- 举例说明(下面`2-3树`就是一颗多叉树) + +![image-20220106104834722](数据结构与算法/image-20220106104834722.png) + +> B树的基本介绍 + +B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率 + +![image-20220106105104540](数据结构与算法/image-20220106105104540.png) + +- 如图B树通过重新组织节点, 降低了树的高度 +- **文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入** +- 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素,B树(B+)广泛应用于文件存储系统以及数据库系统中 + +> 2-3树基本介绍 + +`2-3树`是最简单的B树结构,具有如下特点: + +- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件) +- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点 +- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点 +- `2-3树`是由二节点和三节点构成的树。 + +### 2-3树应用案例 + +将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成2-3树,并保证数据插入的大小顺序(演示一下构建2-3树的过程) + +![image-20220106105643012](数据结构与算法/image-20220106105643012.png) + +> 插入规则 + +- `2-3树`的所有叶子节点都在同一层(只要是B树都满足这个条件) +- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点,二节点能存放一个数据 +- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点,三节点能存放两个数据 +- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面3个条件 +- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则 + +> 代码实现 + +GitHub上面的代码实现:https://github.com/Albertpv95/Tree23/blob/master/Tree23.java + +相对简单的实现,但是代码还是有一些bug,节点不是有序的 + +```java +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; + +/** + * @author wuyou + */ +public class Tree23,Value> { + public static void main(String[] args) { + Tree23 tree23 = new Tree23<>(); + int[] array = {16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20}; + for (int i = 0; i < array.length; i++) { + tree23.insert("节点" + (i + 1), array[i]); + } + tree23.sequenceErgodic(); + } + /** + * 根节点 + */ + private Node23 root = new Node23(); + + public Node23 getRoot() { + return root; + } + + /** + * 层序遍历 + */ + public void sequenceErgodic() { + if(this.root == null) { + return; + } + Queue queue = new LinkedList<>(); + Node23 temp = null; + queue.add(this.getRoot()); + while(!queue.isEmpty()) { + temp = queue.poll(); + System.out.println(temp); + for (int i = 0; i < Node23.N; i++) { + if (temp.childNodes[i] != null) { + queue.add(temp.childNodes[i]); + } + } + } + } + + /** + *查找含有key的键值对 + * @param key + * @return 返回键值对中的value + */ + public Value find(Key key) { + Node23 curNode = root; + int childNum; + while (true) { + if ((childNum = curNode.findItem(key)) != -1) { + return (Value) curNode.itemData[childNum].value; + } + // 假如到了叶子节点还没有找到,则树中不包含key + else if (curNode.isLeaf()) { + return null; + } else { + curNode = getNextChild(curNode,key); + } + } + } + + /** + * 在key的条件下获得结点的子节点(可能为左子结点,中间子节点,右子节点) + * @param node + * @param key + * @return 返回子节点,若结点包含key,则返回传参结点 + */ + private Node23 getNextChild(Node23 node,Key key){ + for (int i = 0; i < node.getItemNum(); i++) { + if (node.getData(i).key.compareTo(key)>0){ + return node.getChild(i); + } + else if (node.getData(i).key.compareTo(key) == 0){ + return node; + } + } + return node.getChild(node.getItemNum()); + } + + /** + * 最重要的插入函数 + * @param key + * @param value + */ + public void insert(Key key,Value value){ + Data data = new Data(key,value); + Node23 curNode = root; + // 一直找到叶节点 + while(true){ + if (curNode.isLeaf()){ + break; + }else{ + curNode = getNextChild(curNode,key); + for (int i = 0; i < curNode.getItemNum(); i++) { + // 假如key在node中则进行更新 + if (curNode.getData(i).key.compareTo(key) == 0){ + curNode.getData(i).value =value; + return; + } + } + } + } + + // 若插入key的结点已经满了,即3-结点插入 + if (curNode.isFull()){ + split(curNode,data); + } + // 2-结点插入 + else { + // 直接插入即可 + curNode.insertData(data); + } + } + + /** + * 这个函数是裂变函数,主要是裂变结点。 + * 这个函数有点复杂,我们要把握住原理就好了 + * @param node 被裂变的结点 + * @param data 要被保存的键值对 + */ + private void split(Node23 node, Data data) { + Node23 parent = node.getParent(); + // newNode用来保存最大的键值对 + Node23 newNode = new Node23(); + // newNode2用来保存中间key的键值对 + Node23 newNode2 = new Node23(); + Data mid; + + if (data.key.compareTo(node.getData(0).key)<0){ + newNode.insertData(node.removeItem()); + mid = node.removeItem(); + node.insertData(data); + }else if (data.key.compareTo(node.getData(1).key)<0){ + newNode.insertData(node.removeItem()); + mid = data; + }else{ + mid = node.removeItem(); + newNode.insertData(data); + } + if (node == root){ + root = newNode2; + } + /** + * 将newNode2和node以及newNode连接起来 + * 其中node连接到newNode2的左子树,newNode + * 连接到newNode2的右子树 + */ + newNode2.insertData(mid); + newNode2.connectChild(0,node); + newNode2.connectChild(1,newNode); + /** + * 将结点的父节点和newNode2结点连接起来 + */ + connectNode(parent,newNode2); + } + + /** + * 链接node和parent + * @param parent + * @param node node中只含有一个键值对结点 + */ + private void connectNode(Node23 parent, Node23 node) { + Data data = node.getData(0); + if (node == root){ + return; + } + // 假如父节点为3-结点 + if (parent.isFull()){ + // 爷爷结点(爷爷救葫芦娃) + Node23 gParent = parent.getParent(); + Node23 newNode = new Node23(); + Node23 temp1,temp2; + Data itemData; + + if (data.key.compareTo(parent.getData(0).key)<0){ + temp1 = parent.disconnectChild(1); + temp2 = parent.disconnectChild(2); + newNode.connectChild(0,temp1); + newNode.connectChild(1,temp2); + newNode.insertData(parent.removeItem()); + + itemData = parent.removeItem(); + parent.insertData(itemData); + parent.connectChild(0,node); + parent.connectChild(1,newNode); + }else if(data.key.compareTo(parent.getData(1).key)<0){ + temp1 = parent.disconnectChild(0); + temp2 = parent.disconnectChild(2); + Node23 tempNode = new Node23(); + + newNode.insertData(parent.removeItem()); + newNode.connectChild(0,newNode.disconnectChild(1)); + newNode.connectChild(1,temp2); + + tempNode.insertData(parent.removeItem()); + tempNode.connectChild(0,temp1); + tempNode.connectChild(1,node.disconnectChild(0)); + + parent.insertData(node.removeItem()); + parent.connectChild(0,tempNode); + parent.connectChild(1,newNode); + } else{ + itemData = parent.removeItem(); + + newNode.insertData(parent.removeItem()); + newNode.connectChild(0,parent.disconnectChild(0)); + newNode.connectChild(1,parent.disconnectChild(1)); + parent.disconnectChild(2); + parent.insertData(itemData); + parent.connectChild(0,newNode); + parent.connectChild(1,node); + } + // 进行递归 + connectNode(gParent,parent); + } + // 假如父节点为2结点 + else{ + if (data.key.compareTo(parent.getData(0).key)<0){ + Node23 tempNode = parent.disconnectChild(1); + parent.connectChild(0,node.disconnectChild(0)); + parent.connectChild(1,node.disconnectChild(1)); + parent.connectChild(2,tempNode); + }else{ + parent.connectChild(1,node.disconnectChild(0)); + parent.connectChild(2,node.disconnectChild(1)); + } + parent.insertData(node.getData(0)); + } + } + + /** + * 保存key和value的键值对 + * @param + * @param + */ + private class Data,Value>{ + private Key key; + private Value value; + + public Data(Key key, Value value) { + this.key = key; + this.value = value; + } + + @Override + public String toString() { + return "Data{" + + "Key=" + key + + ", Value=" + value + + "}"; + } + } + + /** + * 保存树结点的类 + * @param + * @param + */ + private class Node23,Value>{ + private static final int N = 3; + /** + * 该结点的父节点 + */ + private Node23 parent; + /** + * 子节点,子节点有3个,分别是左子节点,中间子节点和右子节点 + */ + private Node23[] childNodes = new Node23[N]; + /** + * 代表结点保存的数据(为一个或者两个) + */ + private Data[] itemData = new Data[N - 1]; + /** + * 结点保存的数据个数 + */ + private int itemNum = 0; + + @Override + public String toString() { + return "Node23{" + + "hashCode=" + this.hashCode() + + ", parent‘s hashCode=" + ((parent == null) ? null : parent.hashCode()) + + ", childNodes count=" + notNullCount(childNodes) + + ", itemData =" + Arrays.toString(itemData) + + ", itemNum=" + itemNum + + '}'; + } + + private int notNullCount(Object[] objects) { + int count = 0; + for (Object o : objects) { + if (o != null) { + count++; + } + } + return count; + } + + /** + * 判断是否是叶子结点 + * @return + */ + private boolean isLeaf(){ + // 假如不是叶子结点。必有左子树(可以想一想为什么?) + return childNodes[0] == null; + } + + /** + * 判断结点储存数据是否满了 + * (也就是是否存了两个键值对) + * @return + */ + private boolean isFull(){ + return itemNum == N-1; + } + + /** + * 返回该节点的父节点 + * @return + */ + private Node23 getParent(){ + return this.parent; + } + + /** + * 将子节点连接 + * @param index 连接的位置(左子树,中子树,还是右子树) + * @param child + */ + private void connectChild(int index,Node23 child){ + childNodes[index] = child; + if (child != null){ + child.parent = this; + } + } + + /** + * 解除该节点和某个结点之间的连接 + * @param index 解除链接的位置 + * @return + */ + private Node23 disconnectChild(int index){ + Node23 temp = childNodes[index]; + childNodes[index] = null; + return temp; + } + + /** + * 获取结点左或右的键值对 + * @param index 0为左,1为右 + * @return + */ + private Data getData(int index){ + return itemData[index]; + } + + /** + * 获得某个位置的子树 + * @param index 0为左指数,1为中子树,2为右子树 + * @return + */ + private Node23 getChild(int index){ + return childNodes[index]; + } + + /** + * @return 返回结点中键值对的数量,空则返回-1 + */ + public int getItemNum(){ + return itemNum; + } + + /** + * 寻找key在结点的位置 + * @param key + * @return 结点没有key则放回-1 + */ + private int findItem(Key key){ + for (int i = 0; i < itemNum; i++) { + if (itemData[i] == null){ + break; + }else if (itemData[i].key.compareTo(key) == 0){ + return i; + } + } + return -1; + } + + /** + * 向结点插入键值对:前提是结点未满 + * @param data + * @return 返回插入的位置 0或则1 + */ + private int insertData(Data data){ + itemNum ++; + for (int i = N -2; i >= 0 ; i--) { + if (itemData[i] == null){ + continue; + }else{ + if (data.key.compareTo(itemData[i].key)<0){ + itemData[i+1] = itemData[i]; + }else{ + itemData[i+1] = data; + return i+1; + } + } + } + itemData[0] = data; + return 0; + } + + /** + * 移除最后一个键值对(也就是有右边的键值对则移右边的,没有则移左边的) + * @return 返回被移除的键值对 + */ + private Data removeItem(){ + Data temp = itemData[itemNum - 1]; + itemData[itemNum - 1] = null; + itemNum --; + return temp; + } + } +} +``` + +输出结果: + +``` +Node23{hashCode=460141958, parent‘s hashCode=null, childNodes count=2, itemData =[Data{Key=节点4, Value=32}, null], itemNum=1} +Node23{hashCode=1163157884, parent‘s hashCode=460141958, childNodes count=3, itemData =[Data{Key=节点10, Value=28}, Data{Key=节点2, Value=24}], itemNum=2} +Node23{hashCode=1956725890, parent‘s hashCode=460141958, childNodes count=3, itemData =[Data{Key=节点6, Value=26}, Data{Key=节点8, Value=10}], itemNum=2} +Node23{hashCode=356573597, parent‘s hashCode=1163157884, childNodes count=0, itemData =[Data{Key=节点1, Value=16}, null], itemNum=1} +Node23{hashCode=1735600054, parent‘s hashCode=1163157884, childNodes count=0, itemData =[Data{Key=节点11, Value=38}, Data{Key=节点12, Value=20}], itemNum=2} +Node23{hashCode=21685669, parent‘s hashCode=1163157884, childNodes count=0, itemData =[Data{Key=节点3, Value=12}, null], itemNum=1} +Node23{hashCode=2133927002, parent‘s hashCode=1956725890, childNodes count=0, itemData =[Data{Key=节点5, Value=14}, null], itemNum=1} +Node23{hashCode=1836019240, parent‘s hashCode=1956725890, childNodes count=0, itemData =[Data{Key=节点7, Value=34}, null], itemNum=1} +Node23{hashCode=325040804, parent‘s hashCode=1956725890, childNodes count=0, itemData =[Data{Key=节点9, Value=8}, null], itemNum=1} +``` + +> 其它说明 + +除了23树,还有234树等,概念和23树类似,也是一种B树 + +![image-20220112141918931](数据结构与算法/image-20220112141918931.png) + +### B树、B+树和B*树 + +#### B树的介绍 + +- B-tree树即B树,B即Balanced,平衡的意思 +- 有人把B-tree翻译成B-树,容易让人产生误解,会以为B-树是一种树,而B树又是另一种树,实际上,B-tree就是指的B树 +- 之前的2-3树和2-3-4树,他们就是B树(英语:B-tree 也写成B-树) +- 我们在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如图: + +![image-20220112142359262](数据结构与算法/image-20220112142359262.png) + +#### B树的说明 + +- B树的阶:节点的最多子节点个数(比如2-3树的阶是3,2-3-4树的阶是4) +- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点 +- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据 +- 搜索有可能在非叶子结点结束 +- 其搜索性能等价于在关键字全集内做一次二分查找 + +#### B树的代码实现 + +GitHub地址:https://github.com/biello/B-Tree + +##### 源代码 + +```java +import java.util.LinkedList; + +/** + * @Author: BieLu + */ +public class Btree { + public static void main(String[] args) { + testDeleteCase5(); + } + + public static void testDeleteCase1() { + BTreeNode node = new BTreeNode(3); + node = node.insert(35); + node = node.insert(13); + node = node.insert(17); + node = node.insert(2); + node.print(); + node = node.delete(2); + node.print(); + } + + public static void testDeleteCase2() { + BTreeNode node = new BTreeNode(3); + node = node.insert(35); + node = node.insert(13); + node = node.insert(17); + node = node.insert(2); + node.print(); + node = node.delete(35); + node.print(); + } + + public static void testDeleteCase3() { + BTreeNode node = new BTreeNode(3); + node = node.insert(35); + node = node.insert(13); + node = node.insert(17); + node = node.insert(22); + node.print(); + node = node.delete(13); + node.print(); + } + + public static void testDeleteCase4() { + BTreeNode node = new BTreeNode(3); + node = node.insert(2); + node = node.insert(13); + node = node.insert(15); + node = node.insert(25); + node = node.insert(23); + node = node.insert(35); + node = node.insert(24); + node = node.insert(4); + node = node.insert(17); + node = node.delete(4); + node = node.delete(17); + node = node.delete(15); + node.print(); + System.out.println("-----------------"); + node = node.delete(24); + node.print(); + System.out.println("-----------------"); + node = node.delete(23); + node.print(); + System.out.println("-----------------"); + node = node.delete(35); + node.print(); + System.out.println("-----------------"); + } + + public static void testDeleteCase5() { + BTreeNode node = new BTreeNode(3); + node = node.insert(3); + node = node.insert(24); + node = node.insert(37); + node = node.insert(45); + node = node.insert(50); + node = node.insert(53); + node = node.insert(61); + node = node.insert(90); + node = node.insert(100); + node = node.insert(70); + System.out.println("--------原树:---------"); + node.print(); + System.out.println("--------删除50后:---------"); + node = node.delete(50); + node.print(); + System.out.println("--------删除53后:---------"); + node = node.delete(53); + node.print(); + System.out.println("--------删除37后:---------"); + node = node.delete(37); + node.print(); + } + +} +/** + * @Author: BieLu + * Date: 2018/3/13 23:12 + * Desc: M阶B-Tree的节点 + */ +class BTreeNode { + /** + * B树的阶 + */ + int M; + + /** + * 关键字列表 + */ + LinkedList values; + + /** + * 父节点 + */ + BTreeNode parent; + + /** + * 孩子列表 + */ + LinkedList children; + + /** + * 构造一棵空的B-树 + */ + private BTreeNode() { + this.values = new LinkedList(); + this.children = new LinkedList(); + } + + /** + * 构造一棵空的m阶B-树 + * + * @param m B-树的阶 + */ + public BTreeNode(int m) { + this(); + if(m < 3) { + throw new RuntimeException("The order of B-Tree should be greater than 2."); + } + this.M = m; + } + + /** + * 根据父节点构造一个空的孩子节点 + * + * @param parent 父节点 + */ + public BTreeNode(BTreeNode parent) { + this(parent.M); + this.parent = parent; + } + + /** + * 往B-树里插数据,先找到根节点,从根节点往下找插入的位置,由于 + * B-树不允许有重复的数据,如果插入已有的值则抛出异常插入. + * 插入可能会产生新的根节点,会导致当前节点不再是根节点,返回新的根节点 + * + * @param e 要插入的元素 + * @return 插入完成后的根节点 + */ + public BTreeNode insert(int e) { + if(isEmpty()) { + values.add(e); + children.add(new BTreeNode(this)); + children.add(new BTreeNode(this)); + return this; + } + BTreeNode p = getRoot().search(e); + if(!p.isEmpty()) { + throw new RuntimeException("cannot insert duplicate key to B-Tree, key: " + e); + } + insertNode(p.parent, e, new BTreeNode(p.M)); + return getRoot(); + } + + /** + * 向指定节点内插入关键字和关键字右侧的孩子节点 + * + * @param node 插入的节点 + * @param e 待插入关键字 + * @param eNode 待关键字右侧的孩子节点 + */ + private void insertNode(BTreeNode node, int e, BTreeNode eNode) { + int valueIndex = 0; + while(valueIndex < node.values.size() && node.values.get(valueIndex) < e) { + valueIndex++; + } + node.values.add(valueIndex, e); + eNode.parent = node; + node.children.add(valueIndex+1, eNode); + if(node.values.size() > M-1) { + // 获取上升关键字 + int upIndex = M/2; + int up = node.values.get(upIndex); + // 当前节点分为左右两部分,左部的parent不变,右部的parent放在上升关键字右侧 + BTreeNode rNode = new BTreeNode(M); + rNode.values = new LinkedList(node.values.subList(upIndex+1, M)); + rNode.children = new LinkedList(node.children.subList(upIndex+1, M+1)); + /* 由于rNode.children是从node.children分离出来的,其parent仍指向node, + 所以需要将rNode.children的parent改为指向rNode + */ + for(BTreeNode rChild : rNode.children) { + rChild.parent = rNode; + } + node.values = new LinkedList(node.values.subList(0, upIndex)); + node.children = new LinkedList(node.children.subList(0, upIndex+1)); + // 从根节点中上升,选取上升关键字作为新的根节点 + if(node.parent == null) { + node.parent = new BTreeNode(M); + node.parent.values.add(up); + node.parent.children.add(node); + node.parent.children.add(rNode); + rNode.parent = node.parent; + return; + } + // 父节点增加关键字,递归调用 + insertNode(node.parent, up, rNode); + } + } + + /** + * 从B-树里删除关键字,先找到根节点,从根节点往下找要删除的关键字,如果 + * 关键字不存在,抛出删除异常;如果存在,若关键字不是最下层非终端节点(叶子节点的 + * 上一层),此时只需要关键字和关键字节点紧邻的右子树中的最小值N互换,然后删除N, + * 至此已转化为待删除关键字在最下层非终端节点的情况,所以只需讨论关键字在最下层 + * 非终端节点的情况。此时分为三种情况: + * 1. 被删除关键字所在的节点关键字数大于等于 ceil(M/2) + * 2. 被删除关键字所在的节点关键字数等于 ceil(M/2)-1,且该节点相邻右兄弟(或左兄弟) + * 中的关键字数大于 ceil(M/2)-1,只需将该兄弟节点中的最小(或最大)关键字上移到 + * 双亲节点中,而将双亲节点中小于(或大于)该上移动关键字的紧邻关键字下移到被删 + * 关键字所在的节点中。 + * 3. 被删除关键字所在的节点关键字数等于 ceil(M/2)-1,且左右兄弟节点的关键字数都等于 + * ceil(M/2)-1,假设该节点有右兄弟A,则在删除关键字之后,它所在的节点中剩余的关键字和 + * 孩子引用,加上双亲节点中的指向A的左侧关键字一起,合并到A中去(若没有右兄弟则合并到 + * 左兄弟中)。此时双亲节点的关键字数减少了一个,若因此导致其关键字数小于ceil(M/2)-1, + * 则对双亲节点做递归处理。 + * + * 删除可能会产生新的根节点,会导致当前节点不再是根节点 + * + * @param e 要删除的元素 + * @return 删除完成后的根节点 + */ + public BTreeNode delete(int e) { + if(isEmpty()) { + return this; + } + BTreeNode p = getRoot().search(e); + if(p.isEmpty()) { + throw new RuntimeException("the key to be deleted is not exist, key: " + e); + } + int valueIndex = 0; + while(valueIndex < p.values.size() && p.values.get(valueIndex) < e) { + valueIndex++; + } + // 如果p不是最下层非终端节点 + if(!p.children.get(0).isEmpty()) { + BTreeNode rMin = p.children.get(valueIndex); + while(!rMin.children.get(0).isEmpty()) { + rMin = rMin.children.get(0); + } + p.values.set(valueIndex, rMin.values.get(0)); + return delete(rMin, valueIndex, 0); + } + return delete(p, valueIndex, 0); + } + + /** + * 删除指定节点中的关键字和孩子,并处理删除后打破B-树规则的情况 + * @param target 目标节点 + * @param valueIndex 关键字索引 + * @param childIndex 孩子索引 + * @return 删除完成后的根节点 + */ + private BTreeNode delete(BTreeNode target, int valueIndex, int childIndex) { + target.values.remove(valueIndex); + target.children.remove(childIndex); + if(target.children.size() >= Math.ceil(M/2.0)) { + return target.getRoot(); + } + if(target.isRoot()) { + if(target.children.size() > 1) { + return target; + }else { + BTreeNode newRoot = target.children.get(0); + newRoot.parent = null; + return newRoot; + } + } + int parentChildIndex = 0; + while(parentChildIndex < target.parent.children.size() && target.parent.children.get(parentChildIndex) != target) { + parentChildIndex++; + } + if(parentChildIndex > 0 && target.parent.children.get(parentChildIndex-1).values.size() >= Math.ceil(M/2.0)) { + // 左兄弟关键字数大于 ceil(M/2)-1 + int downKey = target.parent.values.get(parentChildIndex-1); + BTreeNode leftSibling = target.parent.children.get(parentChildIndex-1); + int upKey = leftSibling.values.remove(leftSibling.values.size()-1); + BTreeNode mergeChild = leftSibling.children.remove(leftSibling.children.size()-1); + target.values.add(0, downKey); + target.children.add(0, mergeChild); + target.parent.values.set(parentChildIndex-1, upKey); + return target.getRoot(); + }else if(parentChildIndex < target.parent.children.size()-1 && + target.parent.children.get(parentChildIndex+1).values.size() >= Math.ceil(M/2.0)) { + // 右兄弟关键字数大于 ceil(M/2)-1 + int downKey = target.parent.values.get(parentChildIndex); + BTreeNode rightSibling = target.parent.children.get(parentChildIndex+1); + int upKey = rightSibling.values.remove(0); + BTreeNode mergeChild = rightSibling.children.remove(0); + target.values.add(downKey); + target.children.add(mergeChild); + target.parent.values.set(parentChildIndex, upKey); + return target.getRoot(); + }else { + // 左右兄弟关键字数都不大于 ceil(M/2)-1 + int parentValueIndex; + if(parentChildIndex > 0) { + // 如果有左兄弟,和左兄弟合并 + BTreeNode leftSibling = target.parent.children.get(parentChildIndex-1); + // 加上父节点关键字 + parentValueIndex = parentChildIndex - 1; + int downKey = target.parent.values.get(parentValueIndex); + leftSibling.values.add(downKey); + // 加上目标节点的剩余信息 + leftSibling.values.addAll(target.values); + target.children.forEach(c -> c.parent=leftSibling); + leftSibling.children.addAll(target.children); + }else { + // 没有左兄弟和右兄弟合并 + BTreeNode rightSibling = target.parent.children.get(parentChildIndex+1); + // 加上父节点关键字 + parentValueIndex = parentChildIndex; + int downKey = target.parent.values.get(parentValueIndex); + rightSibling.values.add(0, downKey); + // 加上目标节点的剩余信息 + rightSibling.values.addAll(0, target.values); + target.children.forEach(c -> c.parent=rightSibling); + rightSibling.children.addAll(0, target.children); + } + // 递归删除父节点关键字和孩子 + return delete(target.parent, parentValueIndex, parentChildIndex); + } + } + + /** + * 从当前节点往下查找目标值target + * + * @param target + * @return 找到则返回找到的节点,不存在则返回叶子节点 + */ + public BTreeNode search(int target) { + if(isEmpty()) { + return this; + } + int valueIndex = 0; + while(valueIndex < values.size() && values.get(valueIndex) <= target) { + if(values.get(valueIndex) == target) { + return this; + } + valueIndex++; + } + return children.get(valueIndex).search(target); + } + + /** + * 获取根节点 + * + * @return 根节点 + */ + public BTreeNode getRoot() { + BTreeNode p = this; + while(!p.isRoot()) { + p = p.parent; + } + return p; + } + + /** + * 判断当前节点是否是空节点 + * + * @return 空节点返回true, 非空节点返回false + */ + public boolean isEmpty() { + if(values == null || values.size() == 0) { + return true; + } + return false; + } + + /** + * 判断当前节点是否是根节点 + * + * @return 是根节点返回true, 不是返回false + */ + public boolean isRoot() { + return parent == null; + } + + /* + * 清空当前节点, 保留父关系 + */ + public void clear() { + values.clear(); + children.clear(); + } + + /** + * 以当前节点为根,在控制台打印B-树 + */ + public void print() { + printNode(this, 0); + } + + /** + * 控制台打印节点的递归调用 + * + * @param node 要打印节点 + * @param depth 递归深度 + */ + private void printNode(BTreeNode node, int depth) { + StringBuilder sb = new StringBuilder(); + for(int i = 1; i < depth; i++) { + sb.append("| "); + } + if(depth > 0) { + sb.append("|----"); + } + sb.append(node.values); + System.out.println(sb.toString()); + for(BTreeNode child : node.children) { + printNode(child, depth+1); + } + } +} +``` + +##### 结果 + +输出结果: + +``` +--------原树:--------- +[45] +|----[24] +| |----[3] +| | |----[] +| | |----[] +| |----[37] +| | |----[] +| | |----[] +|----[53, 90] +| |----[50] +| | |----[] +| | |----[] +| |----[61, 70] +| | |----[] +| | |----[] +| | |----[] +| |----[100] +| | |----[] +| | |----[] +--------删除50后:--------- +[45] +|----[24] +| |----[3] +| | |----[] +| | |----[] +| |----[37] +| | |----[] +| | |----[] +|----[61, 90] +| |----[53] +| | |----[] +| | |----[] +| |----[70] +| | |----[] +| | |----[] +| |----[100] +| | |----[] +| | |----[] +--------删除53后:--------- +[45] +|----[24] +| |----[3] +| | |----[] +| | |----[] +| |----[37] +| | |----[] +| | |----[] +|----[90] +| |----[61, 70] +| | |----[] +| | |----[] +| | |----[] +| |----[100] +| | |----[] +| | |----[] +--------删除37后:--------- +[45, 90] +|----[3, 24] +| |----[] +| |----[] +| |----[] +|----[61, 70] +| |----[] +| |----[] +| |----[] +|----[100] +| |----[] +| |----[] +``` + + + +#### B+树的介绍 + +B+树是B树的变体,也是一种多路搜索树 + +![image-20220112143133153](数据结构与算法/image-20220112143133153.png) + +#### B+树的说明 + +- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找 +- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的 +- 不可能在非叶子结点命中 +- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层 +- 更适合文件索引系统 +- **B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然** + +#### B+树的代码实现 + +GitHub地址:https://github.com/jiaguofang/b-plus-tree + +##### 源代码 + +```java +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class BPlusTree, V> { + + public static enum RangePolicy { + EXCLUSIVE, INCLUSIVE + } + + /** + * The branching factor used when none specified in constructor. + */ + private static final int DEFAULT_BRANCHING_FACTOR = 128; + + /** + * The branching factor for the B+ tree, that measures the capacity of nodes + * (i.e., the number of children nodes) for internal nodes in the tree. + */ + private int branchingFactor; + + /** + * The root node of the B+ tree. + */ + private Node root; + + public BPlusTree() { + this(DEFAULT_BRANCHING_FACTOR); + } + + public BPlusTree(int branchingFactor) { + if (branchingFactor <= 2) { + throw new IllegalArgumentException("Illegal branching factor: " + + branchingFactor); + } + this.branchingFactor = branchingFactor; + root = new LeafNode(); + } + + /** + * Returns the value to which the specified key is associated, or + * {@code null} if this tree contains no association for the key. + * + *

+ * A return value of {@code null} does not necessarily indicate that + * the tree contains no association for the key; it's also possible that the + * tree explicitly associates the key to {@code null}. + * + * @param key + * the key whose associated value is to be returned + * + * @return the value to which the specified key is associated, or + * {@code null} if this tree contains no association for the key + */ + public V search(K key) { + return root.getValue(key); + } + + /** + * Returns the values associated with the keys specified by the range: + * {@code key1} and {@code key2}. + * + * @param key1 + * the start key of the range + * @param policy1 + * the range policy, {@link RangePolicy#EXCLUSIVE} or + * {@link RangePolicy#INCLUSIVE} + * @param key2 + * the end end of the range + * @param policy2 + * the range policy, {@link RangePolicy#EXCLUSIVE} or + * {@link RangePolicy#INCLUSIVE} + * @return the values associated with the keys specified by the range: + * {@code key1} and {@code key2} + */ + public List searchRange(K key1, RangePolicy policy1, K key2, + RangePolicy policy2) { + return root.getRange(key1, policy1, key2, policy2); + } + + /** + * Associates the specified value with the specified key in this tree. If + * the tree previously contained a association for the key, the old value is + * replaced. + * + * @param key + * the key with which the specified value is to be associated + * @param value + * the value to be associated with the specified key + */ + public void insert(K key, V value) { + root.insertValue(key, value); + } + + /** + * Removes the association for the specified key from this tree if present. + * + * @param key + * the key whose association is to be removed from the tree + */ + public void delete(K key) { + root.deleteValue(key); + } + + @Override + public String toString() { + Queue> queue = new LinkedList>(); + queue.add(Arrays.asList(root)); + StringBuilder sb = new StringBuilder(); + while (!queue.isEmpty()) { + Queue> nextQueue = new LinkedList>(); + while (!queue.isEmpty()) { + List nodes = queue.remove(); + sb.append('{'); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + Node node = it.next(); + sb.append(node.toString()); + if (it.hasNext()) { + sb.append(", "); + } + if (node instanceof BPlusTree.InternalNode) { + nextQueue.add(((InternalNode) node).children); + } + } + sb.append('}'); + if (!queue.isEmpty()) { + sb.append(", "); + } else { + sb.append('\n'); + } + } + queue = nextQueue; + } + + return sb.toString(); + } + + private abstract class Node { + List keys; + + int keyNumber() { + return keys.size(); + } + + abstract V getValue(K key); + + abstract void deleteValue(K key); + + abstract void insertValue(K key, V value); + + abstract K getFirstLeafKey(); + + abstract List getRange(K key1, RangePolicy policy1, K key2, + RangePolicy policy2); + + abstract void merge(Node sibling); + + abstract Node split(); + + abstract boolean isOverflow(); + + abstract boolean isUnderflow(); + + @Override + public String toString() { + return keys.toString(); + } + } + + private class InternalNode extends Node { + List children; + + InternalNode() { + this.keys = new ArrayList(); + this.children = new ArrayList(); + } + + @Override + V getValue(K key) { + return getChild(key).getValue(key); + } + + @Override + void deleteValue(K key) { + Node child = getChild(key); + child.deleteValue(key); + if (child.isUnderflow()) { + Node childLeftSibling = getChildLeftSibling(key); + Node childRightSibling = getChildRightSibling(key); + Node left = childLeftSibling != null ? childLeftSibling : child; + Node right = childLeftSibling != null ? child + : childRightSibling; + left.merge(right); + deleteChild(right.getFirstLeafKey()); + if (left.isOverflow()) { + Node sibling = left.split(); + insertChild(sibling.getFirstLeafKey(), sibling); + } + if (root.keyNumber() == 0) { + root = left; + } + } + } + + @Override + void insertValue(K key, V value) { + Node child = getChild(key); + child.insertValue(key, value); + if (child.isOverflow()) { + Node sibling = child.split(); + insertChild(sibling.getFirstLeafKey(), sibling); + } + if (root.isOverflow()) { + Node sibling = split(); + InternalNode newRoot = new InternalNode(); + newRoot.keys.add(sibling.getFirstLeafKey()); + newRoot.children.add(this); + newRoot.children.add(sibling); + root = newRoot; + } + } + + @Override + K getFirstLeafKey() { + return children.get(0).getFirstLeafKey(); + } + + @Override + List getRange(K key1, RangePolicy policy1, K key2, + RangePolicy policy2) { + return getChild(key1).getRange(key1, policy1, key2, policy2); + } + + @Override + void merge(Node sibling) { + @SuppressWarnings("unchecked") + InternalNode node = (InternalNode) sibling; + keys.add(node.getFirstLeafKey()); + keys.addAll(node.keys); + children.addAll(node.children); + + } + + @Override + Node split() { + int from = keyNumber() / 2 + 1, to = keyNumber(); + InternalNode sibling = new InternalNode(); + sibling.keys.addAll(keys.subList(from, to)); + sibling.children.addAll(children.subList(from, to + 1)); + + keys.subList(from - 1, to).clear(); + children.subList(from, to + 1).clear(); + + return sibling; + } + + @Override + boolean isOverflow() { + return children.size() > branchingFactor; + } + + @Override + boolean isUnderflow() { + return children.size() < (branchingFactor + 1) / 2; + } + + Node getChild(K key) { + int loc = Collections.binarySearch(keys, key); + int childIndex = loc >= 0 ? loc + 1 : -loc - 1; + return children.get(childIndex); + } + + void deleteChild(K key) { + int loc = Collections.binarySearch(keys, key); + if (loc >= 0) { + keys.remove(loc); + children.remove(loc + 1); + } + } + + void insertChild(K key, Node child) { + int loc = Collections.binarySearch(keys, key); + int childIndex = loc >= 0 ? loc + 1 : -loc - 1; + if (loc >= 0) { + children.set(childIndex, child); + } else { + keys.add(childIndex, key); + children.add(childIndex + 1, child); + } + } + + Node getChildLeftSibling(K key) { + int loc = Collections.binarySearch(keys, key); + int childIndex = loc >= 0 ? loc + 1 : -loc - 1; + if (childIndex > 0) { + return children.get(childIndex - 1); + } + + return null; + } + + Node getChildRightSibling(K key) { + int loc = Collections.binarySearch(keys, key); + int childIndex = loc >= 0 ? loc + 1 : -loc - 1; + if (childIndex < keyNumber()) { + return children.get(childIndex + 1); + } + return null; + } + } + + private class LeafNode extends Node { + List values; + LeafNode next; + + LeafNode() { + keys = new ArrayList(); + values = new ArrayList(); + } + + @Override + V getValue(K key) { + int loc = Collections.binarySearch(keys, key); + return loc >= 0 ? values.get(loc) : null; + } + + @Override + void deleteValue(K key) { + int loc = Collections.binarySearch(keys, key); + if (loc >= 0) { + keys.remove(loc); + values.remove(loc); + } + } + + @Override + void insertValue(K key, V value) { + int loc = Collections.binarySearch(keys, key); + int valueIndex = loc >= 0 ? loc : -loc - 1; + if (loc >= 0) { + values.set(valueIndex, value); + } else { + keys.add(valueIndex, key); + values.add(valueIndex, value); + } + if (root.isOverflow()) { + Node sibling = split(); + InternalNode newRoot = new InternalNode(); + newRoot.keys.add(sibling.getFirstLeafKey()); + newRoot.children.add(this); + newRoot.children.add(sibling); + root = newRoot; + } + } + + @Override + K getFirstLeafKey() { + return keys.get(0); + } + + @Override + List getRange(K key1, RangePolicy policy1, K key2, + RangePolicy policy2) { + List result = new LinkedList(); + LeafNode node = this; + while (node != null) { + Iterator kIt = node.keys.iterator(); + Iterator vIt = node.values.iterator(); + while (kIt.hasNext()) { + K key = kIt.next(); + V value = vIt.next(); + int cmp1 = key.compareTo(key1); + int cmp2 = key.compareTo(key2); + if (((policy1 == RangePolicy.EXCLUSIVE && cmp1 > 0) || (policy1 == RangePolicy.INCLUSIVE && cmp1 >= 0)) + && ((policy2 == RangePolicy.EXCLUSIVE && cmp2 < 0) || (policy2 == RangePolicy.INCLUSIVE && cmp2 <= 0))) { + result.add(value); + } else if ((policy2 == RangePolicy.EXCLUSIVE && cmp2 >= 0) + || (policy2 == RangePolicy.INCLUSIVE && cmp2 > 0)) { + return result; + } + } + node = node.next; + } + return result; + } + + @Override + void merge(Node sibling) { + @SuppressWarnings("unchecked") + LeafNode node = (LeafNode) sibling; + keys.addAll(node.keys); + values.addAll(node.values); + next = node.next; + } + + @Override + Node split() { + LeafNode sibling = new LeafNode(); + int from = (keyNumber() + 1) / 2, to = keyNumber(); + sibling.keys.addAll(keys.subList(from, to)); + sibling.values.addAll(values.subList(from, to)); + + keys.subList(from, to).clear(); + values.subList(from, to).clear(); + + sibling.next = next; + next = sibling; + return sibling; + } + + @Override + boolean isOverflow() { + return values.size() > branchingFactor - 1; + } + + @Override + boolean isUnderflow() { + return values.size() < branchingFactor / 2; + } + } +} +``` + +##### 结果 + +测试用例: + +```java +import org.junit.Assert; +import org.junit.Test; + +public class BPlusTreeTest { + @Test + public void test() { + BPlusTree bpt = new BPlusTree(4); + bpt.insert(0, "a"); + bpt.insert(1, "b"); + bpt.insert(2, "c"); + bpt.insert(3, "d"); + bpt.insert(4, "e"); + bpt.insert(5, "f"); + bpt.insert(6, "g"); + bpt.insert(7, "h"); + bpt.insert(8, "i"); + bpt.insert(9, "j"); + bpt.delete(1); + bpt.delete(3); + bpt.delete(5); + bpt.delete(7); + bpt.delete(9); + Assert.assertEquals(bpt.search(0), "a"); + Assert.assertEquals(bpt.search(1), null); + Assert.assertEquals(bpt.search(2), "c"); + Assert.assertEquals(bpt.search(3), null); + Assert.assertEquals(bpt.search(4), "e"); + Assert.assertEquals(bpt.search(5), null); + Assert.assertEquals(bpt.search(6), "g"); + Assert.assertEquals(bpt.search(7), null); + Assert.assertEquals(bpt.search(8), "i"); + Assert.assertEquals(bpt.search(9), null); + } + + @Test + public void testSearchRange() { + BPlusTree bpt = new BPlusTree(4); + bpt.insert(0, "a"); + bpt.insert(1, "b"); + bpt.insert(2, "c"); + bpt.insert(3, "d"); + bpt.insert(4, "e"); + bpt.insert(5, "f"); + bpt.insert(6, "g"); + bpt.insert(7, "h"); + bpt.insert(8, "i"); + bpt.insert(9, "j"); + Assert.assertArrayEquals( + bpt.searchRange(3, BPlusTree.RangePolicy.EXCLUSIVE, 7, + BPlusTree.RangePolicy.EXCLUSIVE).toArray(), new String[] { "e", + "f", "g" }); + Assert.assertArrayEquals( + bpt.searchRange(3, BPlusTree.RangePolicy.INCLUSIVE, 7, + BPlusTree.RangePolicy.EXCLUSIVE).toArray(), new String[] { "d", + "e", "f", "g" }); + Assert.assertArrayEquals( + bpt.searchRange(3, BPlusTree.RangePolicy.EXCLUSIVE, 7, + BPlusTree.RangePolicy.INCLUSIVE).toArray(), new String[] { "e", + "f", "g", "h" }); + Assert.assertArrayEquals( + bpt.searchRange(3, BPlusTree.RangePolicy.INCLUSIVE, 7, + BPlusTree.RangePolicy.INCLUSIVE).toArray(), new String[] { "d", + "e", "f", "g", "h" }); + } +} +``` + + + +#### B*树的介绍 + +B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针 + +![image-20220112144107978](数据结构与算法/image-20220112144107978.png) + +#### B*树的说明 + +- B树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2* +- 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高 diff --git a/src/study/11.md b/src/study/11.md new file mode 100644 index 0000000..ee8e14c --- /dev/null +++ b/src/study/11.md @@ -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) + *

+ * 从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。 + *

+ * 数据结构:栈 + * 父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点即可 + * + * @author lry + */ +public class DepthFirstSearch { + + /** + * 树节点 + * + * @param + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TreeNode { + private V value; + private List> childList; + + /** + * 二叉树节点支持如下 + * @return + */ + public TreeNode getLeft() { + if (childList == null || childList.isEmpty()) { + return null; + } + return childList.get(0); + } + + public TreeNode 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 treeNodeA = new TreeNode<>("A", new ArrayList<>()); + TreeNode treeNodeB = new TreeNode<>("B", new ArrayList<>()); + TreeNode treeNodeC = new TreeNode<>("C", new ArrayList<>()); + TreeNode treeNodeD = new TreeNode<>("D", new ArrayList<>()); + TreeNode treeNodeE = new TreeNode<>("E", new ArrayList<>()); + TreeNode treeNodeF = new TreeNode<>("F", new ArrayList<>()); + TreeNode treeNodeG = new TreeNode<>("G", new ArrayList<>()); + TreeNode treeNodeH = new TreeNode<>("H", new ArrayList<>()); + TreeNode treeNodeI = new TreeNode<>("I", new ArrayList<>()); + TreeNode treeNodeJ = new TreeNode<>("J", new ArrayList<>()); + TreeNode 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 + */ + public static void dfsNotRecursive(TreeNode tree) { + if (tree != null) { + // 次数之所以用 Map 只是为了保存节点的深度,如果没有这个需求可以改为 Stack> + Stack, Integer>> stack = new Stack<>(); + Map, Integer> root = new HashMap<>(); + root.put(tree, 0); + stack.push(root); + + while (!stack.isEmpty()) { + Map, Integer> item = stack.pop(); + TreeNode 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 treeNode : node.getChildList()) { + Map, Integer> map = new HashMap<>(); + map.put(treeNode, depth + 1); + stack.push(map); + } + } + } + } + } + + /** + * 递归前序遍历方式 + *

+ * 前序遍历(Pre-Order Traversal) :指先访问根,然后访问子树的遍历方式,二叉树则为:根->左->右 + * + * @param tree + * @param depth + * @param + */ + public static void dfsPreOrderTraversal(TreeNode tree, int depth) { + if (tree != null) { + // 打印节点值以及深度 + System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]"); + + if (tree.getChildList() != null && !tree.getChildList().isEmpty()) { + for (TreeNode item : tree.getChildList()) { + dfsPreOrderTraversal(item, depth + 1); + } + } + } + } + + /** + * 递归后序遍历方式 + *

+ * 后序遍历(Post-Order Traversal):指先访问子树,然后访问根的遍历方式,二叉树则为:左->右->根 + * + * @param tree + * @param depth + * @param + */ + public static void dfsPostOrderTraversal(TreeNode tree, int depth) { + if (tree != null) { + if (tree.getChildList() != null && !tree.getChildList().isEmpty()) { + for (TreeNode item : tree.getChildList()) { + dfsPostOrderTraversal(item, depth + 1); + } + } + + // 打印节点值以及深度 + System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]"); + } + } + + /** + * 递归中序遍历方式 + *

+ * 中序遍历(In-Order Traversal):指先访问左(右)子树,然后访问根,最后访问右(左)子树的遍历方式,二叉树则为:左->根->右 + * + * @param tree + * @param depth + * @param + */ + public static void dfsInOrderTraversal(TreeNode 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) + *

+ * 从根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。 + *

+ * 数据结构:队列 + * 父节点入队,父节点出队列,先左子节点入队,后右子节点入队。递归遍历全部节点即可 + * + * @author lry + */ +public class BreadthFirstSearch { + + /** + * 树节点 + * + * @param + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TreeNode { + private V value; + private List> childList; + } + + /** + * 模型: + * .......A + * ...../ \ + * ....B C + * .../ \ / \ + * ..D E F G + * ./ \ / \ + * H I J K + */ + public static void main(String[] args) { + TreeNode treeNodeA = new TreeNode<>("A", new ArrayList<>()); + TreeNode treeNodeB = new TreeNode<>("B", new ArrayList<>()); + TreeNode treeNodeC = new TreeNode<>("C", new ArrayList<>()); + TreeNode treeNodeD = new TreeNode<>("D", new ArrayList<>()); + TreeNode treeNodeE = new TreeNode<>("E", new ArrayList<>()); + TreeNode treeNodeF = new TreeNode<>("F", new ArrayList<>()); + TreeNode treeNodeG = new TreeNode<>("G", new ArrayList<>()); + TreeNode treeNodeH = new TreeNode<>("H", new ArrayList<>()); + TreeNode treeNodeI = new TreeNode<>("I", new ArrayList<>()); + TreeNode treeNodeJ = new TreeNode<>("J", new ArrayList<>()); + TreeNode 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 + */ + public static void bfsRecursive(List> children, int depth) { + List> thisChildren, allChildren = new ArrayList<>(); + for (TreeNode 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 + */ + public static void bfsNotRecursive(TreeNode tree) { + if (tree != null) { + // 跟上面一样,使用 Map 也只是为了保存树的深度,没这个需要可以不用 Map + Queue, Integer>> queue = new ArrayDeque<>(); + Map, Integer> root = new HashMap<>(); + root.put(tree, 0); + queue.offer(root); + + while (!queue.isEmpty()) { + Map, Integer> itemMap = queue.poll(); + TreeNode 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 child : node.getChildList()) { + Map, 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] +``` + diff --git a/src/study/12.md b/src/study/12.md new file mode 100644 index 0000000..5d7ca1a --- /dev/null +++ b/src/study/12.md @@ -0,0 +1,1999 @@ +## 二分查找算法(非递归) + +### 分析 + +- 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找 + +- 二分查找法的运行时间为对数时间O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n步,假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7) + +### 代码案例 + +```java +/** + * @author wuyou + */ +public class BinarySearch { + public static void main(String[] args) { + int[] array = {1, 3, 8, 10, 11, 67, 100}; + System.out.println(searchLoop(array, 11)); + } + + /** + * 二分查找的非递归实现 + * @param array 待查找的数组 + * @param findValue 需要查找的数 + * @return 返回对应下标,-1表示没有找到 + */ + public static int searchLoop(int[] array, int findValue) { + if (array == null) { + return -1; + } + int left = 0; + int right = array.length - 1; + while (left <= right) { + int mid = (left + right) / 2; + if (array[mid] == findValue) { + return mid; + }else if (array[mid] > findValue) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return -1; + } +} +``` + +## 分治算法 + +### 分析 + +- 分治法是一种很重要的算法,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。 +- 这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… +- 分治算法可以求解的一些经典问题 + - 二分搜索 + - 大整数乘法 + - 棋盘覆盖 + - 合并排序 + - 快速排序 + - 线性时间选择 + - 最接近点对问题 + - 循环赛日程表 + - 汉诺塔 + +> 分治算法的基本步骤 + +分治法在每一层递归上都有三个步骤: + +- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题 +- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题 +- 合并:将各个子问题的解合并为原问题的解 + +### 最佳实践-汉诺塔问题 + +> 汉诺塔的传说 + +汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。 + +假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。 + +#### 代码 + +```java +/** + * @author 22130 + */ +public class HannoTower { + public static void main(String[] args) { + move(3, 'A', 'B', 'C'); + } + + /** + * 汉诺塔移动 + *

+ * 以2个盘子来分析最为清晰: + * num = 1: + * 倒数第1步:将mid移动到end + * num = 2: + * 倒数第2步:将start移动到end + * 倒数第3步:将start移动到mid + * 所以打印顺序为: + * 第1个盘子从【A】移动到【B】 + * 第2个盘子从【A】移动到【C】 + * 第1个盘子从【B】移动到【C】 + *

+ * @param num + * @param start + * @param mid + * @param end + */ + public static void move(int num, char start, char mid, char end) { + // 当只有1个盘子的时候,我们直接把盘子从start柱子上移动到end柱子上面去 + if (num == 1) { + // print1 + System.out.println("第1个盘子从【" + start + "】移动到【" + end + "】"); + } else { + // 当盘子>=2的时候,我们直接把盘子从start柱子上移动到mid柱子上面去 + // end与mid交换 + // move1 + move(num - 1, start, end, mid); + // print2 + System.out.println("第" + num + "个盘子从【" + start + "】移动到【" + end + "】"); + // 然后把第2个盘子移动,相当于把mid放到end上面去 + // move2 + move(num - 1, mid, start, end); + } + } +} +``` + +#### 输出结果 + +``` +第1个盘子从【A】移动到【C】 +第2个盘子从【A】移动到【B】 +第1个盘子从【C】移动到【B】 +第3个盘子从【A】移动到【C】 +第1个盘子从【B】移动到【A】 +第2个盘子从【B】移动到【C】 +第1个盘子从【A】移动到【C】 +``` + +## 动态规划算法 + +> 应用场景—背包问题 + +背包问题:有一个背包,容量为4磅 , 现有如下物品 + +| **物品** | **重量** | **价格** | +| -------- | -------- | -------- | +| 吉他(G) | 1 | 1500 | +| 音响(S) | 4 | 3000 | +| 电脑(L) | 3 | 2000 | + +- 要求达到的目标为装入的背包的总价值最大,并且重量不超出 + +- 要求装入的物品不能重复 + +### 动态规划算法介绍 + +- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法 + +- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解 + +- 与分治法不同的是,**适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。** ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 ) + +- 动态规划可以通过**填表的方式**来逐步推进,得到最优解 + +- 虽然动态规划的最终版本 (降维再去维) 大都不是递归,但解题的过程还是离开不递归的。新手可能会觉得动态规划思想接受起来比较难,确实,动态规划求解问题的过程不太符合人类常规的思维方式,我们需要切换成机器思维。使用动态规划思想解题,首先要明确动态规划的三要素。动态规划三要素: + + - `重叠子问题`:切换机器思维,自底向上思考 + - `最优子结构`:子问题的最优解能够推出原问题的优解 + - `状态转移方程`: + + $$ + dp[n] = dp[n-1] + dp[n-2] + $$ + + + +### 最佳实践-背包问题 + +> 思路分析 + +- 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大 +- 其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用) +- 这里的问题属于01背包,即每个物品最多放一个 +- 无限背包可以转化为01背包 +- 算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据第i个物品的价值和重量来确定是否需要将该物品放入背包中 + +#### 代码实现 + +```java +import java.util.Arrays; + +/** + * @author wuyou + */ +public class KnapsackProblem { + public static void main(String[] args) { + // 物品的重量 + int[] weight = {3, 5, 4}; + // 物体的价值 + int[] value = {1500, 3000, 2000}; + // 背包的容量 + int V = 17; + // 物品的个数 + int N = value.length; + test3(V, N, weight, value); + } + + /** + * 测试0 - 1背包 + * @param V + * @param N + * @param weight + * @param value + */ + public static void test1(int V, int N, int[] weight, int[] value) { + System.out.println("0-1背包\n背包重量:" + V + " kg\n物体种类:" + N + " 种\n物体重量:" + Arrays.toString(weight) + "\n物体价值:" + Arrays.toString(value)); + System.out.println("解法一,背包编号为:" + zeroOnePack(V, N, weight, value)); + System.out.println("解法二,物体总价值为:" + zeroOnePack1(V, N, weight, value)); + } + + /** + * 测试多重背包 + * @param V + * @param N + * @param weight + * @param value + */ + public static void test2(int V, int N, int[] weight, int[] value) { + // 每个物品的次数限制 + int[] num = {2, 2, 2}; + System.out.println("多重背包\n背包重量:" + V + " kg\n物体种类:" + N + " 种\n物体重量:" + Arrays.toString(weight) + "\n物体价值:" + Arrays.toString(value) + "\n每个物品数量:" + Arrays.toString(num)); + System.out.println("解法一,物体总价值为:" + manyPack(V, N, weight, value, num)); + } + + /** + * 测试完全背包 + * @param V + * @param N + * @param weight + * @param value + */ + public static void test3(int V, int N, int[] weight, int[] value) { + System.out.println("完全背包\n背包重量:" + V + " kg\n物体种类:" + N + " 种\n物体重量:" + Arrays.toString(weight) + "\n物体价值:" + Arrays.toString(value)); + System.out.println("解法一,背包编号为:" + completePack(V, N, weight, value)); + System.out.println("解法二,物体总价值为:" + completePack1(V, N, weight, value)); + } + + /** + * 0-1背包问题 + * @param V 背包容量 + * @param N 物品种类 + * @param weight 物品重量 + * @param value 物品价值 + * @return 物品的放入顺序 + */ + public static String zeroOnePack(int V, int N, int[] weight, int[] value){ + + //初始化动态规划数组 + int[][] dp = new int[N+1][V+1]; + //为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算 + for(int i=1;i j) { + dp[i][j] = dp[i-1][j]; + } else { + dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i-1]]+value[i-1]); + } + } + } + //则容量为V的背包能够装入物品的最大值为 + int maxValue = dp[N][V]; + //逆推找出装入背包的所有商品的编号 + int j=V; + String numStr=""; + for(int i=N;i>0;i--){ + //若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的 + if(dp[i][j]>dp[i-1][j]){ + numStr = i + " " + numStr; + j=j-weight[i-1]; + } + if(j==0) { + break; + } + } + return numStr; + } + + /** + * 0-1背包的优化解法 + * 思路: + * 只用一个一维数组记录状态,dp[i]表示容量为i的背包所能装入物品的最大价值 + * 用逆序来实现 + */ + public static int zeroOnePack1(int V,int N,int[] weight,int[] value) { + //动态规划 + int[] dp = new int[V + 1]; + for (int i = 1; i < N + 1; i++) { + //逆序实现 + for (int j = V; j >= weight[i - 1]; j--) { + dp[j] = Math.max(dp[j - weight[i - 1]] + value[i - 1], dp[j]); + } + } + return dp[V]; + } + + /** + * 第三类背包:多重背包 + * @param V + * @param N + * @param weight + * @param value + * @param num + * @return + */ + public static int manyPack(int V,int N,int[] weight,int[] value,int[] num){ + //初始化动态规划数组 + int[][] dp = new int[N+1][V+1]; + //为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算 + for(int i=1;i j) { + dp[i][j] = dp[i-1][j]; + } else{ + // 考虑物品的件数限制 + int maxV = Math.min(num[i-1], j/weight[i-1]); + for(int k=0;kMath.max(dp[i-1][j],dp[i-1][j-k*weight[i-1]]+k*value[i-1]) ? dp[i][j]:Math.max(dp[i-1][j],dp[i-1][j-k*weight[i-1]]+k*value[i-1]); + } + } + } + } + return dp[N][V]; + } + + /** + * 第二类背包:完全背包 + * 思路分析: + * 01背包问题是在前一个子问题(i-1种物品)的基础上来解决当前问题(i种物品), + * 向i-1种物品时的背包添加第i种物品;而完全背包问题是在解决当前问题(i种物品) + * 向i种物品时的背包添加第i种物品。 + * 推公式计算时,f[i][y] = max{f[i-1][y], (f[i][y-weight[i]]+value[i])}, + * 注意这里当考虑放入一个物品 i 时应当考虑还可能继续放入 i, + * 因此这里是f[i][y-weight[i]]+value[i], 而不是f[i-1][y-weight[i]]+value[i]。 + * @param V + * @param N + * @param weight + * @param value + * @return + */ + public static String completePack(int V,int N,int[] weight,int[] value){ + //初始化动态规划数组 + int[][] dp = new int[N+1][V+1]; + //为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算 + for(int i=1;i j) { + dp[i][j] = dp[i-1][j]; + } else { + dp[i][j] = Math.max(dp[i-1][j],dp[i][j-weight[i-1]]+value[i-1]); + } + } + } + //则容量为V的背包能够装入物品的最大值为 + int maxValue = dp[N][V]; + int j=V; + String numStr=""; + for(int i=N;i>0;i--){ + //若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的 + while(dp[i][j]>dp[i-1][j]){ + numStr = i+" "+numStr; + j=j-weight[i-1]; + } + if(j==0) { + break; + } + } + return numStr; + } + + /** + * 完全背包的第二种解法 + * 思路: + * 只用一个一维数组记录状态,dp[i]表示容量为i的背包所能装入物品的最大价值 + * 用顺序来实现 + */ + public static int completePack1(int V,int N,int[] weight,int[] value){ + //动态规划 + int[] dp = new int[V+1]; + for(int i=1;i 思路分析 + +一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果) + +此题的突破口在台阶数;台阶数不一样,结果就不一样;由台阶数来找规律。 + +| 台阶数 | 跳法 | +| ------ | ------ | +| 1 | 1 | +| 2 | 2 | +| 3 | 3 | +| 4 | 5 | +| 5 | 8 | +| 6 | 13 | +| ...... | ...... | + + +通过分析台阶数,可以看出当台阶数为n时,跳法数为前两个之和,设跳法数为函数f(n),则有: +$$ +f (n) = f(n-1)+ f(n-2) +$$ +这是一个一个斐波那切数列,所以转化为求解斐波那切数列问题 + +#### 代码实现 + +```java +/** + * @author wuyou + */ +public class SolveStep { + final static int MAX_STEP = 2; + + public static void main(String[] args) { + System.out.println("方法一走20阶楼梯有:【" + f1(20) + "】种走法"); + int[] array = new int[21]; + for (int i = 0; i < array.length; i++) { + array[i] = -1; + } + System.out.println("方法二走20阶楼梯有:【" + f2(20, array) + "】种走法"); + System.out.println("方法三走20阶楼梯有:【" + f3(20) + "】种走法"); + } + + /** + * 使用递归进行求解 + * f(n-1) 可以理解为:最后一步走1台阶有多少种情况 + * f(n-2) 可以理解为:最后一步走2台阶有多少种情况 + * @param n 当前的接替数 + * @return + */ + public static int f1(int n) { + return (n <= MAX_STEP ? n : f1(n - 1) + f1(n - 2)); + } + + /** + * 自顶向下 (递归) 动态规划(备忘录) + * @param n + * @return + */ + public static int f2(int n,int[] bak) { + if(n < MAX_STEP) { + return 1; + } + + if(bak[n] != -1) { + return bak[n]; + } + // n = 2时,result就等于1 + 1总共2种 + int result = f2(n - 1, bak) + f2(n - 2, bak); + // bak[2]就记录最多有2种走法 + bak[n] = result; + return result; + } + + /** + * 自底先上法 + * @param n + * @return + */ + public static int f3(int n) { + if(n <= MAX_STEP) { + return n; + } + int[] Memo = new int[n+1]; + Memo[1]=1; + Memo[2]=2; + for(int i=3;i<=n;i++) + { + Memo[i]=Memo[i-1]+Memo[i-2]; + } + return Memo[n]; + } +} +``` + +#### 输出结果 + +``` +方法一走20阶楼梯有:【10946】种走法 +方法二走20阶楼梯有:【10946】种走法 +方法三走20阶楼梯有:【10946】种走法 +``` + +## KMP算法 + +> 字符串匹配问题 + +现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1 + +> 暴力匹配算法 + +如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有: + +- 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符 + +- 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0 +- 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间 +- 暴力匹配算法实现 + +### 暴力匹配 + +#### 代码实现 + +```java +/** + * @author wuyou + */ +public class ViolenceMatch { + public static void main(String[] args) { + // 测试暴力匹配算法 + String str1 = "KMP算法详解及其Java实现,KMP算法。"; + String str2 = "Java实现"; + System.out.println(violenceMatch(str1, str2)); + String str3 = "算法"; + System.out.println(violenceMatch(str1, str3)); + } + + public static int violenceMatch(String str1, String str2) { + char[] s1 = str1.toCharArray(); + char[] s2 = str2.toCharArray(); + + int len1 = s1.length; + int len2 = s2.length; + + // 用于指向s1 + int i = 0; + // 用于指向s2 + int j = 0; + // 保证匹配不越界 + while (i < len1 && j < len2) { + // 匹配成功 + if (s1[i] == s2[j]){ + i++; + j++; + } + // 没有匹配成功 + else { + i = i - (j - 1); + j = 0; + } + } + // 判断是否匹配成功 + if (j == len2) { + return i - j; + } + return -1; + } +} +``` + +#### 输出结果 + +``` +9 +3 +``` + +### KMP算法介绍 + +- KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法 + +- Knuth-Morris-Pratt **字符串查找算法**,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法 + +- **KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间** + +- 参考资料:https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html + +> 思路分析 + +前缀和后缀的概念 + +![See the source image](数据结构与算法/OIP-C.wflLyS9o2yrQwIBi8z9z1QHaCt) + +`部分匹配值`就是`前缀`和`后缀`的最长的共有元素的长度 + +![img](数据结构与算法/20140725232020608.jpeg) + +所以KMP算法可以简单理解为: + +- 先得到子串的部分匹配表 +- 使用部分匹配表完成KMP匹配 + +#### 代码实现 + +```java +/** + * @author wuyou + */ +public class KMPAlgorithm { + public static void main(String[] args) { + // 测KMP匹配算法 + String str1 = "Java实现实现JavaJava实现Java"; + String str2 = "Java实现Java"; + + String str3 = "BBC ABCDAB ABCDABCDABDE"; + String str4 = "ABCDABD"; + System.out.println(kmp(str1, str2)); + System.out.println(kmp(str3, str4)); + } + + public static int kmp(String str1, String str2) { + if (str2.length() > str1.length() || str1 == null || str2 == null) { + return -1; + } + int[] next = kmpNext(str2); + int search = kmpSearch(str1, str2, next); + return search; + } + + /** + * kmp搜索 + * @param str1 源字符串 + * @param str2 子串 + * @param next 部分匹配表,是字串对应的部分匹配表 + * @return + */ + private static int kmpSearch(String str1, String str2, int[] next) { + char[] array1 = str1.toCharArray(); + char[] array2 = str2.toCharArray(); + // 遍历 + for (int i = 0, j = 0; i < array1.length; i++) { + // kmp算法核心 + while (j > 0 && array1[i] != array2[j]) { + j = next[j - 1]; + } + if (array1[i] == array2[j]) { + j++; + } + if (j == array2.length) { + return i - j + 1; + } + } + return -1; + } + + /** + * 算法核心!!!:获取资一个字符串的部分匹配表 + * @param des + * @return + */ + private static int[] kmpNext(String des) { + // 创建一个next数组保存部分匹配值 + int[] next = new int[des.length()]; + char[] charArray = des.toCharArray(); + // 如果字符串长度为1部分匹配值为0 + next[0] = 0; + // i代表数组下标,j代表匹配的前缀个数 + for (int i = 1, j = 0; i < des.length() ;i++) { + // 当这个条件满足时候,前缀个数就+1 + if (charArray[i] == charArray[j]) { + j++; + } + // 连续相等的字符长度断开 + else { + // 但是当前字符又和第1个字符相等,从1开始 + if (charArray[i] == charArray[0]) { + j = 1; + } + // 当前字符和第1个也不相等,从0开始 + else { + j = 0; + } + } + next[i] = j; + } + return next; + } +} +``` + +#### 输出结果 + +```java +12 +15 +``` + +## 贪心算法 + +### 贪心算法介绍 + +`贪心算法`是`动态规划`算法的一个子集,可以更高效解决一部分更特殊的问题。实际上,用贪心算法解决问题的思路,并不总能给出最优解。因为它在每一步的决策中,选择目前最优策略,不考虑全局是不是最优。 + +- 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法 + +- 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果 + +**贪心算法+双指针求解** + +- 给一个孩子的饼干应当尽量小并且能满足孩子,大的留来满足胃口大的孩子 +- 因为胃口小的孩子最容易得到满足,所以优先满足胃口小的孩子需求 +- 按照从小到大的顺序使用饼干尝试是否可满足某个孩子 +- 当饼干 j >= 胃口 i 时,饼干满足胃口,更新满足的孩子数并移动指针 `i++ j++ res++` +- 当饼干 j < 胃口 i 时,饼干不能满足胃口,需要换大的 `j++` + +### 应用场景-集合覆盖问题 + +假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 **如何选择最少的广播台,让所有的地区都可以接收到信号** + +| 广播台 | 覆盖地区 | +| ------ | ---------------------- | +| K1 | "北京", "上海", "天津" | +| K2 | "广州", "北京", "深圳" | +| K3 | "成都", "上海", "杭州" | +| K4 | "上海", "天津" | +| K5 | "杭州", "大连" | + +> 思路分析 + +如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。 + +假设总的有n个广播台,则广播台的组合总共有2ⁿ -1 个,假设每秒可以计算10个子集, 如图: + +| 广播台数量n | 子集总数2ⁿ | 需要的时间 | +| ----------- | ---------- | ---------- | +| 5 | 32 | 3.2秒 | +| 10 | 1024 | 102.4秒 | +| 32 | 4294967296 | 13.6年 | +| 100 | 1.26*100³º | 4x10²³年 | + +> 使用贪婪算法,效率高 + +目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合: + +- 遍历所有的广播电台, 找到一个**覆盖了最多未覆盖的地区的电台**(此电台可能包含一些已覆盖的地区,但没有关系) + +- 将这个电台加入到一个集合中(比如ArrayList),想办法把该电台覆盖的地区在下次比较时去掉) +- 重复第1步直到覆盖了全部的地区 + +#### 代码实现 + +```java +import java.util.*; + +/** + * @author wuyou + */ +public class GreedyAlgorithm { + public static void main(String[] args) { + String[][] k = {{"北京", "上海", "天津"}, + {"广州", "北京", "深圳"}, + {"成都", "上海", "杭州"}, + {"上海", "天津"}, + {"杭州", "大连"}}; + test(k); + } + + public static void test(String[][] k) { + // 创建广播电台,放到Map + HashMap> broadcasts = new HashMap<>(16); + for (int i = 0; i < k.length; i++) { + HashSet set = new HashSet<>(); + for (String s : k[i]) { + set.add(s); + } + broadcasts.put("k" + (i + 1), set); + } + + // 存放所有的地区 + HashSet allAreas = new HashSet<>(); + for (String[] strings : k) { + for (String s : strings) { + if (!allAreas.contains(s)) { + allAreas.add(s); + } + } + } + + System.out.println(f(broadcasts, allAreas)); + } + + /** + * 获取可以覆盖所有地区的最少广播台 + * @param broadcasts + * @param allAreas + * @return + */ + public static List f(HashMap> broadcasts, HashSet allAreas) { + // 存放选择的电台集合 + List selects = new ArrayList<>(); + + // 存放遍历过程中电台覆盖的地区和当前还没有覆盖的地区的交际 + HashSet tempSet = new HashSet<>(); + // maxKey存放当前遍历阶段能够覆盖最大覆盖地区对应的电台key + String maxKey = null; + + while (allAreas.size() != 0) { + // maxKey置空 + maxKey = null; + // 存放当前遍历阶段的最大交集个数 + int max = 0; + // 遍历,取出最大的key + for (String key : broadcasts.keySet()) { + HashSet areas = broadcasts.get(key); + // tempSet清空 + tempSet.clear(); + tempSet.addAll(areas); + // 求出tempSet和allAreas集合的交集,赋值给tempSet + tempSet.retainAll(allAreas); + if (tempSet.size() > 0 && + (maxKey == null || tempSet.size() > max)) { + maxKey = key; + max = tempSet.size(); + } + } + if (maxKey != null) { + selects.add(maxKey); + // 将maxKey指向的广播电台覆盖的地区,从allAreas去掉 + allAreas.removeAll(broadcasts.get(maxKey)); + } + } + return selects; + } +} +``` + +#### 输出结果 + +``` +[k1, k2, k3, k5] +``` + +## 普里姆算法 + +### 分析 + +> 应用场景-修路问题 + +![image-20220113221220475](数据结构与算法/image-20220113221220475.png) + +- 有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通 + +- 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里 + +- 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短? +- 将10条边,连接即可,但是总的里程数不是最小 +- **正确的思路**,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少 + +> 最小生成树 + +修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST + +- 给定一个带权的无向连通图如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树 +- N个顶点,一定有N-1条边 +- 包含全部顶点 +- N-1条边都在图中 +- 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法 + +![image-20220113222305120](数据结构与算法/image-20220113222305120.png) + +### 普里姆算法介绍 + +普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图 + +普利姆的算法如下: + +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条边 + +> 思路步骤 + +![image-20220113221220475](数据结构与算法/image-20220113221220475.png) + +1. 从``顶点开始处理:`` + +$$ +A-C[7]、A-G[2]、A-B[5] +$$ + +2. 开始,将A、G 顶点和它们相邻的还没有访问的顶点进行处理:`` + +$$ +A-C[7]、A-B[5]、G-B[3]、G-E[4]、G-F[6] +$$ + +3. ``开始,将A、G、B 顶点和它们相邻的还没有访问的顶点进行处理:`` + +$$ +A-C[7]、G-E[4]、G-F[6]、B-D[9] +$$ + +​ 4.``开始,将A、G、B、E顶点和它们相邻的还没有访问的顶点进行处理:`` +$$ +A-C[7]、E-C[8]、E-F[5]、G-F[6]、B-D[9] +$$ +​ 5.``开始,将A、G、B、E、F顶点和它们相邻的还没有访问的顶点进行处理:`` +$$ +A-C[7]、E-C[8]、B-D[9]、F-D[4] +$$ + +6. ``开始,将A、G、B、E、F、D顶点和它们相邻的还没有访问的顶点进行处理:`` + +$$ +A-C[7]、E-C[8] +$$ + +### 最佳实践-修路问题 + +#### 代码实现 + +```java +import java.util.HashSet; +import java.util.Set; + +/** + * @author wuyou + */ +public class PrimAlgorithm { + public final static int MAX = 10000; + private static class MGraph { + /** + * 节点个数 + */ + int vertx; + /** + * 存放节点数据 + */ + char[] data; + /** + * 存放边的权值 + */ + int[][] weight; + + private MGraph(int vertx) { + this.vertx = vertx; + data = new char[vertx]; + weight = new int[vertx][vertx]; + } + + public MGraph(int vertx, char[] data, int[][] weight) { + this(vertx); + int i, j; + for (i = 0; i < vertx; i++) { + this.data[i] = data[i]; + for (j = 0; j < vertx; j++) { + this.weight[i][j] = weight[i][j]; + } + } + } + } + + /** + * 打印数组 + * @param mGraph + */ + public static void showGraph(MGraph mGraph) { + for (int[] link : mGraph.weight) { + for (int x : link) { + System.out.printf("%5s\t", x); + } + System.out.println(); + } + } + + /** + * prim算法得到最小生成树 + * @param mGraph + * @param v 表示从第一个顶点开始 + */ + public static void prim(MGraph mGraph, int v) { + // 元素的个数 + int size = mGraph.vertx; + // 标记节点是否访问过 + boolean[] visited = new boolean[size]; + // 把当前节点标记为已经访问 + visited[v] = true; + // 标记访问了几个节点 + int count = 1; + // 存储访问了的节点 + Set set = new HashSet(); + set.add(v); + while (count < size) { + // 将minWeight初始化成一个大数,后面在遍历过程中,会被替换 + int minWeight = MAX; + int minRow = -1; + int minCol = -1; + for (Integer next : set) { + for (int i = 0; i < size; i++) { + if (visited[i] == false) { + if (mGraph.weight[next][i] < minWeight) { + minWeight = mGraph.weight[next][i]; + minRow = next; + minCol = i; + } + } + } + } + if (minRow == -1 || minCol == -1) { + System.err.println(" => 无法连通!!!"); + return; + } + System.out.println("<" + mGraph.data[minRow] + "," + mGraph.data[minCol] + ">, 权值:" + minWeight); + count++; + visited[minCol] = true; + set.add(minCol); + } + } + + public static void main(String[] args) { + char[] chars = {'A', 'B', 'C', 'D', 'E', 'F', 'G'}; + //邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通 + int[][] weight = { + {10000, 5, 7, 10000, 10000, 10000, 2}, + {5, 10000, 10000, 9, 10000, 10000, 3}, + {7, 10000, 10000, 10000, 8, 10000, 10000}, + {10000, 9, 10000, 10000, 10000, 4, 10000}, + {10000, 10000, 8, 10000, 10000, 5, 4}, + {10000, 10000, 10000, 4, 5, 10000, 6}, + {2, 3, 10000, 10000, 4, 6, 10000} + }; + MGraph mGraph = new MGraph(chars.length, chars, weight); + showGraph(mGraph); + prim(mGraph, 0); + } +} +``` + +#### 输出结果 + +``` +10000 5 7 10000 10000 10000 2 + 5 10000 10000 9 10000 10000 3 + 7 10000 10000 10000 8 10000 10000 +10000 9 10000 10000 10000 4 10000 +10000 10000 8 10000 10000 5 4 +10000 10000 10000 4 5 10000 6 + 2 3 10000 10000 4 6 10000 +, 权值:2 +, 权值:3 +, 权值:4 +, 权值:5 +, 权值:4 +, 权值:7 +``` + +## 克鲁斯卡尔算法 + +### 分析 + +> 应用场景-公交站问题 + +看一个应用场景和问题: + +![image-20220114100735575](数据结构与算法/image-20220114100735575.png) + +- 某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通 + +- 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里 +- 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短? + +### 克鲁斯卡尔算法介绍 + +- 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法 +- 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路 +- 具体做法:**首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止** + +### 最佳实践-公交站问题 + +#### 代码实现 + +```java +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author wuyou + */ +public class KruskalAlgorithm { + private final static int INF = Integer.MAX_VALUE; + + private static class MGraph { + /** + * 边的个数 + */ + int edgeNum; + /** + * 顶点数组 + */ + char[] vertx; + /** + * 邻接矩阵 + */ + int[][] matrix; + + public MGraph(char[] vertx, int[][] matrix, int edgeNum) { + this.edgeNum = edgeNum; + this.vertx = vertx.clone(); + this.matrix = matrix.clone(); + } + } + + private static class Data implements Comparable{ + /** + * 边的起点 + */ + char start; + /** + * 边的终点 + */ + char end; + /** + * 边的权重 + */ + int weight; + + public Data(char start, char end, int weight) { + this.start = start; + this.end = end; + this.weight = weight; + } + + @Override + public String toString() { + return "Data{" + + "start=" + start + + ", end=" + end + + ", weight=" + weight + + '}'; + } + + @Override + public int compareTo(Object o) { + Data data = (Data) o; + return this.weight - data.weight; + } + } + + public static void main(String[] args) { + char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'}; + int[][] weight = { + /*A*//*B*//*C*//*D*//*E*//*F*//*G*/ + /*A*/ { 0, 12, INF, INF, INF, 16, 14}, + /*B*/ { 12, 0, 10, INF, INF, 7, INF}, + /*C*/ { INF, 10, 0, 3, 5, 6, INF}, + /*D*/ { INF, INF, 3, 0, 4, INF, INF}, + /*E*/ { INF, INF, 5, 4, 0, 2, 8}, + /*F*/ { 16, 7, 6, INF, 2, 0, 9}, + /*G*/ { 14, INF, INF, INF, 8, 9, 0}}; + + System.out.println("最终结果如下:\n" + kruskal(vertex, weight)); + } + + /** + * 封装kruskal算法 + * @param vertex + * @param weight + * @return + */ + public static List kruskal(char[] vertex, int[][] weight) { + List dataList = getSort(vertex, weight); + System.out.println("排序好的路径为:\n" + dataList); + MGraph mGraph = new MGraph(vertex, weight, dataList.size()); + System.out.println("矩阵图如下:"); + showGraph(mGraph); + return kruskalConnect(mGraph, dataList); + } + + /** + * kruskal实现 + * @param mGraph 图对象 + * @param dataList 排序好的路径 + * @return 封装修路的链表 + */ + public static List kruskalConnect(MGraph mGraph, List dataList) { + char[] vertx = mGraph.vertx; + int edgeNum = mGraph.edgeNum; + // 表示最后结果数组的索引 + int index = 0; + int[] connections = new int[mGraph.edgeNum]; + // 创建结果数组 + List result = new ArrayList<>(); + + // 遍历dataList,将边添加到最小生成树中,判断是否生成回路 + for (Data data : dataList) { + // 获取第data条边的起点 + int p1 = getPosition(vertx, data.start); + // 获取第data条边的终点 + int p2 = getPosition(vertx, data.end); + + // 获取p1这个顶点在最小数的终点 + int end1 = getEnd(connections, p1); + // 获取p2这个顶点在最小数的终点 + int end2 = getEnd(connections, p2); + + // 判断是否生成回路 + if (end1 != end2) { + // 说明没有构成回路 + connections[end1] = end2; + result.add(data); + } + } + return result; + } + + /** + * 算法核心点,获取下标顶点为i的顶点的终点 + * @param connections 连接点的集合 + * @param i + * @return + */ + public static int getEnd(int[] connections, int i) { + while (connections[i] != 0) { + i = connections[i]; + } + return i; + } + + /** + * 获得某个点对应的数组下标 + * @param vertex + * @param value + * @return + */ + public static int getPosition(char[] vertex, char value) { + if (vertex == null) { + return -1; + } + for (int i = 0; i < vertex.length; i++) { + if (vertex[i] == value) { + return i; + } + } + return -1; + } + + public static List getSort(char[] vertex, int[][] weight) { + List dataList = new ArrayList<>(); + for (int i = 0; i < weight.length; i++) { + for (int j = i + 1; j < weight.length; j++) { + if (weight[i][j] != INF) { + Data data = new Data(vertex[i], vertex[j], weight[i][j]); + dataList.add(data); + } + } + } + // 进行排序 + Collections.sort(dataList); + return dataList; + } + + public static void showGraph(MGraph mGraph) { + for (int[] link : mGraph.matrix) { + for (int x : link) { + System.out.printf("%10s\t", x); + } + System.out.println(); + } + } +} +``` + +#### 输出结果 + +``` +排序好的路径为: +[Data{start=E, end=F, weight=2}, Data{start=C, end=D, weight=3}, Data{start=D, end=E, weight=4}, Data{start=C, end=E, weight=5}, Data{start=C, end=F, weight=6}, Data{start=B, end=F, weight=7}, Data{start=E, end=G, weight=8}, Data{start=F, end=G, weight=9}, Data{start=B, end=C, weight=10}, Data{start=A, end=B, weight=12}, Data{start=A, end=G, weight=14}, Data{start=A, end=F, weight=16}] +矩阵图如下: + 0 12 2147483647 2147483647 2147483647 16 14 + 12 0 10 2147483647 2147483647 7 2147483647 +2147483647 10 0 3 5 6 2147483647 +2147483647 2147483647 3 0 4 2147483647 2147483647 +2147483647 2147483647 5 4 0 2 8 + 16 7 6 2147483647 2 0 9 + 14 2147483647 2147483647 2147483647 8 9 0 +最终结果如下: +[Data{start=E, end=F, weight=2}, Data{start=C, end=D, weight=3}, Data{start=D, end=E, weight=4}, Data{start=B, end=F, weight=7}, Data{start=E, end=G, weight=8}, Data{start=A, end=B, weight=12}] +``` + +## 迪杰斯特拉算法 + +> 应用场景-最短路径问题 + +![image-20220114114603754](数据结构与算法/image-20220114114603754.png) + +- 战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄 +- 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里 +- 问:如何计算出G村庄到 其它各个村庄的最短距离? +- 如果从其它点出发到各个点的最短距离又是多少? + +### 迪杰斯特拉(Dijkstra)算法介绍 + +迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止 + +> 迪杰斯特拉(Dijkstra)算法过程 + +设置出发顶点为v,顶点集合V{v1,v2,vi...},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di...},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di) + +- 从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径 + +- 更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的) +- 重复执行两步骤,**直到最短路径顶点为目标顶点即可结束** + +### 算法分析 + +链接:[docs-lemon](https://docs-lemon.view6view.club/#/src/Algorithm/105?id=%e8%bf%aa%e6%9d%b0%e6%96%af%e7%89%b9%e6%8b%89%e7%ae%97%e6%b3%95dijkstra) + +> 基本思想 + +通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。 + +此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。 + +初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是"起点s到该顶点的路径"。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 ... 重复该操作,直到遍历完所有顶点。 + +> 操作步骤 + +- 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为"起点s到该顶点的距离"[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞] +- U中选出"距离最短的顶点k",并将顶点k加入到S中;同时,从U中移除顶点k +- 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离 +- 重复步骤(2)和(3),直到遍历完所有顶点 + +> 迪杰斯特拉算法图解 + +![img](数据结构与算法/1117043-20170407105053816-427306966.png) + +以上图G4为例,来对迪杰斯特拉进行算法演示(以第4个顶点D为起点): + +![img](数据结构与算法/1117043-20170407105111300-518814658.png) + +### 最佳实践-最短路径 + +#### 代码实现 + +```java +/** + * @author wuyou + */ +public class DijkstraAlgorithm { + private final static int N = 10000; + + private static class Graph { + /** + * 顶点数组 + */ + char[] vertex; + /** + * 邻接矩阵 + */ + int[][] matrix; + + public Graph(char[] vertex, int[][] matrix) { + this.vertex = vertex.clone(); + this.matrix = matrix.clone(); + } + + } + + public static void showGraph(Graph graph) { + for (int[] link : graph.matrix) { + for (int x : link) { + System.out.printf("%5s\t", x); + } + System.out.println(); + } + } + + /** + * dij算法 + * @param graph + * @param start + * @return + */ + public static int[] dij(Graph graph, int start) { + // 权值数组 + int[][] weight = graph.matrix.clone(); + // 顶点数组 + char[] names = graph.vertex; + // 顶点个数 + int n = names.length; + // 标记当前顶点的最短路径是否已经求出,1表示已经求出 + int[] visited = new int[n]; + // 保存start到其他各点的最短路径 + int[] shortPath = new int[n]; + + // 保存start到其他各点最短路径的字符串表示 + String[] path = new String[n]; + for (int i = 0; i < n; i++) { + path[i] = names[start] + " => " + names[i]; + } + + // 初始化第一个顶点 + shortPath[start] = 0; + visited[start] = 1; + + // 加入剩下的节点 + for (int count = 1; count < n; count++) { + // 标记距离初始顶点start最近的未标记的节点 + int minIndex = -1; + int minWeight = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + if (visited[i] == 0 && weight[start][i] <= minWeight) { + minIndex = i; + minWeight = weight[start][i]; + } + } + + // 将新选出的顶点标记为已求出最短路径,且到start 最短路径就是minWeight + visited[minIndex] = 1; + shortPath[minIndex] = minWeight; + + // 以k为中间点,修正从start到未访问各点的距离 + for (int i = 0; i < n; i++) { + // 代码核心!!! + // 如果'起始点到当前点距离' + '当前点到某点距离' < '起始点到某点距离', 则更新 + if (visited[i] ==0 && weight[start][minIndex] + weight[minIndex][i] < weight[start][i]) { + weight[start][i] = weight[start][minIndex] + weight[minIndex][i]; + path[i] = path[minIndex] + " => " + names[i]; + } + } + } + for (int i = 0; i < n; i++) { + System.out.println("从" + names[start] + "出发到" + names[i] + "的最短路径为:" + path[i]); + } + return shortPath; + } + + public static void main(String[] args) { + char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'}; + int[][] weight = { + /*A*B*C*D*E*F*G*/ + /*A*/ {N,5,7,N,N,N,2}, + /*B*/ {5,N,N,9,N,N,3}, + /*C*/ {7,N,N,N,8,N,N}, + /*D*/ {N,9,N,N,N,4,N}, + /*E*/ {N,N,8,N,N,5,4}, + /*F*/ {N,N,N,4,5,N,6}, + /*G*/ {2,3,N,N,4,6,N}}; + Graph graph = new Graph(vertex, weight); + showGraph(graph); + int start = 0; + int[] dij = dij(graph, 0); + for (int i = 0; i < dij.length; i++) { + System.out.println("从" + graph.vertex[start] + "出发到" + graph.vertex[i] + "的最短距离为:" + dij[i]); + } + } +} +``` + +#### 输出结果 + +``` +10000 5 7 10000 10000 10000 2 + 5 10000 10000 9 10000 10000 3 + 7 10000 10000 10000 8 10000 10000 +10000 9 10000 10000 10000 4 10000 +10000 10000 8 10000 10000 5 4 +10000 10000 10000 4 5 10000 6 + 2 3 10000 10000 4 6 10000 +从A出发到A的最短路径为:A => A +从A出发到B的最短路径为:A => B +从A出发到C的最短路径为:A => C +从A出发到D的最短路径为:A => G => F => D +从A出发到E的最短路径为:A => G => E +从A出发到F的最短路径为:A => G => F +从A出发到G的最短路径为:A => G +从A出发到A的最短距离为:0 +从A出发到B的最短距离为:5 +从A出发到C的最短距离为:7 +从A出发到D的最短距离为:12 +从A出发到E的最短距离为:6 +从A出发到F的最短距离为:8 +从A出发到G的最短距离为:2 +``` + +## 弗洛伊德算法 + +### 弗洛伊德(Floyd)算法介绍 + +- 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授**罗伯特·弗洛伊德**命名 +- 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径 +- 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。 +- 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;**弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径** + +### 最佳实践-最短路径 + +#### 代码实现 + +```java +import java.util.Arrays; + +/** + * @author wuyou + */ +public class FloydAlgorithm { + private final static int N = 65535; + private static class Graph { + /** + * 存放顶点的数组 + */ + char[] vertex; + /** + * 保存从各个顶点出发到其他顶点的距离 + */ + int[][] dis; + /** + * 保存到目标顶点的前驱节点 + */ + int[][] pre; + + public Graph(char[] vertex, int[][] matrix) { + this.vertex = vertex.clone(); + this.dis = matrix.clone(); + this.pre = new int[vertex.length][vertex.length]; + // 对pre数组初始化 + for (int i = 0; i < vertex.length; i++) { + Arrays.fill(pre[i], i); + } + } + } + + public static void showGraph(Graph graph) { + for (int i = 0; i < graph.vertex.length; i++) { + System.out.printf("%8s", graph.vertex[i] + "的前驱节点为:"); + for (int j = 0; j < graph.vertex.length; j++) { + System.out.printf("%5s\t", graph.pre[i][j]); + } + System.out.println(); + System.out.printf("%8s", graph.vertex[i] + "的各点权值为:"); + for (int j = 0; j < graph.vertex.length; j++) { + System.out.printf("%5s\t", graph.dis[i][j]); + } + System.out.println(); + } + } + + /** + * 弗洛伊德算法 + * @param graph + */ + public static void floyd(Graph graph) { + int length = graph.vertex.length; + int[][] dis = graph.dis; + int[][] pre = graph.pre; + // 变量保存距离 + int len = 0; + for (int i = 0; i < length; i++) { + // 从j顶点出发 + for (int j = 0; j < length; j++) { + for (int k = 0; k < length; k++) { + // 从i顶点出发,经过k中间顶点,到大j + len = dis[j][i] + dis[i][k]; + if (len < dis[j][k]) { + // 更新距离 + dis[j][k] = len; + // 更新前驱 + pre[j][k] = pre[i][k]; + } + } + } + } + } + + public static void main(String[] args) { + char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'}; + int[][] weight = { + /*A*B*C*D*E*F*G*/ + /*A*/ {0,5,7,N,N,N,2}, + /*B*/ {5,0,N,9,N,N,3}, + /*C*/ {7,N,0,N,8,N,N}, + /*D*/ {N,9,N,0,N,4,N}, + /*E*/ {N,N,8,N,0,5,4}, + /*F*/ {N,N,N,4,5,0,6}, + /*G*/ {2,3,N,N,4,6,0}}; + Graph graph = new Graph(vertex, weight); + showGraph(graph); + floyd(graph); + System.out.println(); + showGraph(graph); + } +} +``` + +#### 输出结果 + +``` +A的前驱节点为: 0 0 0 0 0 0 0 +A的各点权值为: 0 5 7 65535 65535 65535 2 +B的前驱节点为: 1 1 1 1 1 1 1 +B的各点权值为: 5 0 65535 9 65535 65535 3 +C的前驱节点为: 2 2 2 2 2 2 2 +C的各点权值为: 7 65535 0 65535 8 65535 65535 +D的前驱节点为: 3 3 3 3 3 3 3 +D的各点权值为:65535 9 65535 0 65535 4 65535 +E的前驱节点为: 4 4 4 4 4 4 4 +E的各点权值为:65535 65535 8 65535 0 5 4 +F的前驱节点为: 5 5 5 5 5 5 5 +F的各点权值为:65535 65535 65535 4 5 0 6 +G的前驱节点为: 6 6 6 6 6 6 6 +G的各点权值为: 2 3 65535 65535 4 6 0 + +A的前驱节点为: 0 0 0 5 6 6 0 +A的各点权值为: 0 5 7 12 6 8 2 +B的前驱节点为: 1 1 0 1 6 6 1 +B的各点权值为: 5 0 12 9 7 9 3 +C的前驱节点为: 2 0 2 5 2 4 0 +C的各点权值为: 7 12 0 17 8 13 9 +D的前驱节点为: 6 3 4 3 5 3 5 +D的各点权值为: 12 9 17 0 9 4 10 +E的前驱节点为: 6 6 4 5 4 4 4 +E的各点权值为: 6 7 8 9 0 5 4 +F的前驱节点为: 6 6 4 5 5 5 5 +F的各点权值为: 8 9 13 4 5 0 6 +G的前驱节点为: 6 6 0 5 6 6 6 +G的各点权值为: 2 3 9 10 4 6 0 +``` + +## 回溯算法 + +### 马踏棋盘介绍 + +> 马踏棋盘算法介绍和游戏演示 + +- 马踏棋盘算法也被称为骑士周游问题 +- 将马随机放在国际象棋的8×8棋盘`Board[0~7][0~7]`的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格 +- 游戏演示: http://www.4399.com/flash/146267_2.htm + +![image-20220115125713618](C:\Users\22130\AppData\Roaming\Typora\typora-user-images\image-20220115125713618.png) + +> 马踏棋盘游戏代码实现 + +- 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用 +- 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,如图:走到了第53个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯……,思路分析+代码实现 +- 分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化,解决马踏棋盘问题. + +> 骑士周游问题的解决步骤和思路 + +1. 创建棋盘 chessBoard , 是一个二维数组 +2. 将当前位置设置为已经访问,然后根据当前位置,计算马儿还能走哪些位置,并放入到一个集合中(ArrayList),最多有8个位置, 每走一步,就使用step+1 +3. 遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续;走不通,就**回溯** +4. 判断马儿是否完成了任务,使用 step 和应该走的步数比较 , 如果没有达到数量,则表示没有完成任务,将整个棋盘置0 + + +注意:马儿不同的走法(策略),会得到不同的结果,效率也会有影响(优化) + +```java +//创建一个Point +Point p1 = new Point(); +if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) { +ps.add(new Point(p1)); +} +``` + +> 使用贪心算法对原来的算法优化 + +1. 我们获取当前位置,可以走的下一个位置的集合 + + ```java + //获取当前位置可以走的下一个位置的集合 + ArrayList ps = next(new Point(column, row)); + ``` + +2. 我们需要对 ps 中所有的Point 的下一步的所有集合的数目,进行非递减排序,就ok + +``` +// 递减排序 +9,7,6,5,3,2,1 +// 递增排序 +1,2,3,4,5,6,10 +// 非递减 +1,2,2,2,3,3,4,5,6 +// 非递增 +9,7,6,6,6,5,5,3,2,1 +``` + +### 最佳实践—骑士周游问题解决 + +#### 代码实现 + +```java +import java.awt.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * @author wuyou + */ +public class HorseChessBoard { + private static class ChessBoard { + /** + * 棋盘的行数 + */ + int y; + /** + * 棋盘的列数 + */ + int x; + /** + * 标记每个数组是否被访问过 + */ + boolean[][] visited; + /** + * 标记是否所有点都被访问过了 + */ + boolean finish; + public ChessBoard(int x, int y) { + this.x = x; + this.y = y; + this.visited = new boolean[x][y]; + this.finish = false; + } + } + + /** + * 根据当前的位置,计算马尔还能走哪些位置,并放入到一个集合中(List),最多有8个位置 + * @param chessBoard + * @param chessPoint + * @return + */ + public static List getAround(ChessBoard chessBoard, Point chessPoint) { + int X = chessBoard.x; + int Y = chessBoard.y; + int left = chessPoint.x - 2; + int right = chessPoint.x + 2; + int top = chessPoint.y + 2; + int bottom = chessPoint.y - 2; + List result = new ArrayList<>(); + if (left > 0) { + if (top - 1 <= X) { + result.add(new Point(left, top - 1)); + } + if (bottom + 1 > 0) { + result.add(new Point(left, bottom + 1)); + } + } + if (left + 1 > 0) { + if (top <= X) { + result.add(new Point(left + 1, top)); + } + if (bottom > 0) { + result.add(new Point(left + 1, bottom)); + } + } + if (right - 1 <= Y) { + if (top <= X) { + result.add(new Point(right - 1, top)); + } + if (bottom > 0) { + result.add(new Point(right - 1, bottom)); + } + } + if (right <= Y) { + if (top - 1 <= X) { + result.add(new Point(right, top - 1)); + } + if (bottom + 1 > 0) { + result.add(new Point(right, bottom + 1)); + } + } + return result; + } + + /** + * 算法核心,不断回溯 + * 需要注意:Point和数组下标的关系是,比如8x8的棋盘,Point就是[1,8],数组就是[0,7] + * @param chessBoard 棋盘对象 + * @param array 棋盘数组 + * @param row 按照数组索引的行索引 + * @param col 按照数组索引的列索引 + * @param step 步数 + */ + public static void travelChessBoard(ChessBoard chessBoard, int[][] array, int row, int col, int step) { + // 标记位置已经转换 + array[row][col] = step; + chessBoard.visited[row][col] = true; + List around = getAround(chessBoard, new Point(col + 1, row + 1)); + // 对around进行排序 + sort(chessBoard, around); + while (!around.isEmpty()) { + // 取出一个可以走的位置 + Point remove = around.remove(0); + int tempCol = remove.x - 1; + int tempRow = remove.y - 1; + // 判断该点是否已经访问过 + if (!chessBoard.visited[tempRow][tempCol]) { + // 说明没有访问过 + travelChessBoard(chessBoard, array, tempRow, tempCol, step + 1); + } + } + + if (step < chessBoard.x * chessBoard.y && !chessBoard.finish) { + array[row][col] = 0; + chessBoard.visited[row][col] = false; + } else { + chessBoard.finish = true; + } + } + + /** + * 贪心的思想:根据当前这个所有的下一步选择位置,进行递减排序,减少回溯可能 + * @param chessBoard + * @param pointList + */ + public static void sort(ChessBoard chessBoard, List pointList) { + pointList.sort(new Comparator() { + @Override + public int compare(Point o1, Point o2) { + // 获取到o1的下一步的所有位置个数 + int count1 = getAround(chessBoard, o1).size(); + // 获取到o2的下一步的所有位置个数 + int count2 = getAround(chessBoard, o2).size(); + return count1 - count2; + } + }); + } + + public static void main(String[] args) { + int x = 8; + int y = 8; + int row = 1; + int col = 1; + ChessBoard chessBoard = new ChessBoard(x, y); + int[][] array = new int[x][y]; + long start = System.currentTimeMillis(); + travelChessBoard(chessBoard, array, row - 1, col - 1, 1); + long end = System.currentTimeMillis(); + System.out.println("花费时间" + (end - start) + "毫秒"); + for (int[] rows : array) { + for (int step : rows) { + System.out.printf("%2s\t", step); + } + System.out.println(); + } + } +} +``` + +#### 输出结果 + +``` +花费时间85毫秒 + 1 46 15 30 35 54 13 28 +16 31 52 61 14 29 36 55 +45 2 47 34 53 62 27 12 +32 17 60 51 58 49 56 37 + 3 44 33 48 63 38 11 26 +18 21 64 59 50 57 8 39 +43 4 23 20 41 6 25 10 +22 19 42 5 24 9 40 7 +``` + +`◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◆◆◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◆◆◆◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◆◆◆◇◇◇◇◇◇◇◇◇◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◆◆◇◇◇◇◇◇◇◇◇◇◇◇◇◇◆◆◆◇◇◇ +◇◇◆◆◇◆◆◆◇◇◇◇◇◇◇◆◆◇◆◆◆◇◇◇ +◇◇◇◇◇◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇◇◇◇◇ +◇◇◇◇◇◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◆◆◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◆◆◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◆◆◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◆◆◆◇◇◇◆◆◇◇◇◇◆◇◇◇◇◇ +◇◇◇◇◇◆◆◆◆◇◇◇◆◆◇◇◇◇◆◆◆◇◇◇ +◇◇◇◇◆◆◆◆◇◇◇◇◆◆◆◇◇◆◆◆◆◇◇◇ +◇◆◆◆◆◆◆◇◇◇◇◇◆◆◆◆◆◆◆◆◇◇◇◇ +◇◆◆◆◆◆◇◇◇◇◇◇◆◆◆◆◆◆◆◆◇◇◇◇ +◇◇◆◆◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◆◇◇◇◇◇◇◇◇◆◆◆◇◇◇◇◇◇◇ +◇◇◇◇◆◆◆◇◇◇◇◇◇◇◆◆◆◇◇◇◇◇◇◇ +◇◇◇◇◆◆◆◇◇◇◇◇◇◇◆◆◆◇◇◇◇◇◇◇ +◇◇◇◆◆◆◇◇◇◆◆◇◇◇◆◆◇◇◇◆◆◆◇◇ +◇◇◇◆◆◆◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇ +◇◇◆◆◆◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇ +◇◆◆◆◆◆◆◆◆◆◇◇◇◇◆◆◇◇◇◇◇◇◇◇ +◇◆◆◆◆◆◆◆◆◇◆◆◇◇◆◆◇◇◇◆◆◇◇◇ +◇◆◆◆◆◆◆◆◇◇◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◇◇◆◆◆◇◇◇◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◇◇◆◆◆◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◆◆◆◇◇◆◇◇◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◆◆◆◆◆◆◆◆◇◇◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◆◆◆◆◆◆◆◆◇◇◆◆◇◇◇◇◇◆◆◇◇◇◇ +◇◇◆◆◆◇◇◇◇◇◇◆◆◇◇◇◇◇◆◆◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◆◆◇◇◇◇◇◆◆◇◇◇◇ +◇◇◆◆◆◆◆◆◆◆◇◆◆◆◆◆◆◆◆◆◇◇◇◇ +◇◆◆◆◆◆◆◆◆◆◇◆◆◆◆◆◆◆◆◆◇◇◇◇ +◇◆◆◆◆◆◇◇◇◇◇◆◆◇◇◇◇◇◆◆◆◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◆◆◇◇◇◇◇◆◆◆◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◆◆◇◇◇◆◆◆◆◆◆◇◇◆◆◇◇◇◇◇ +◇◇◇◇◆◆◆◇◇◆◆◆◆◆◆◇◇◆◆◇◇◇◇◇ +◇◇◇◇◆◆◆◇◇◆◆◇◆◆◇◇◆◆◆◇◇◇◇◇ +◇◇◇◇◆◆◇◇◆◆◆◆◆◆◆◇◆◆◆◇◇◇◇◇ +◇◇◆◆◆◆◇◆◆◆◆◆◆◆◆◇◆◆◆◆◆◆◇◇ +◇◇◆◆◆◆◆◆◇◆◆◇◆◆◇◇◆◆◆◆◆◆◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◆◆◆◇◇ +◇◇◇◇◆◆◇◆◆◆◆◆◆◆◆◆◆◆◇◆◆◇◇◇ +◇◇◇◇◆◆◇◇◆◆◆◆◆◆◆◆◆◆◇◆◆◇◇◇ +◇◇◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◆◆◇◇◇ +◇◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◆◆◇◇◇ +◇◇◆◆◆◆◆◆◆◆◇◇◇◆◆◇◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◇◇◆◆◆◆◆◆◆◇◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◇◇◆◆◆◆◆◆◆◇◆◆◆◆◇◇◇◇ +◇◇◇◇◆◆◇◇◆◆◇◇◇◆◆◇◇◆◆◆◇◇◇◇ +◇◇◇◇◆◆◇◇◆◆◆◆◆◆◆◇◇◆◆◆◇◇◇◇ +◇◇◇◇◆◆◇◇◆◆◆◆◆◆◆◇◆◆◆◆◇◇◇◇ +◇◇◇◇◆◆◇◇◆◆◇◇◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◇◇◆◆◇◆◆◆◆◆◆◆◇◆◆◆◇◇ +◇◇◇◆◆◆◇◇◆◆◇◇◆◆◆◆◆◇◇◆◆◆◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◆◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◇◆◆◆◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◇◆◆◆◇◇◇◇◇◇◇◇ +◇◇◆◆◇◇◇◆◆◆◇◇◇◆◆◇◇◇◇◆◆◇◇◇ +◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◇◆◆◆◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◇◆◆◆◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◆◆◆◇◇◆◆◆◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◆◆◆◆◇◇◆◆◆◇◇◆◆◆◇◇◇◇ +◇◇◇◇◇◆◆◆◆◇◇◇◆◆◇◇◆◆◆◆◇◇◇◇ +◇◇◇◆◆◆◆◆◇◇◇◇◆◆◇◆◆◆◆◇◇◇◇◇ +◇◇◆◆◆◆◆◆◇◇◇◇◆◆◆◆◆◆◇◇◇◇◇◇ +◇◆◆◆◆◇◆◆◇◇◇◇◆◆◆◆◆◇◇◇◇◇◇◇ +◇◇◆◆◇◇◆◆◇◇◆◆◆◆◆◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◆◆◆◆◆◆◆◆◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◆◆◆◆◆◆◆◆◇◇◇◇◇◆◇◇◇◇ +◇◇◇◇◇◇◆◆◇◆◇◇◆◆◇◇◇◇◇◆◆◆◇◇ +◇◇◇◇◇◇◆◆◇◇◇◇◆◆◆◇◇◇◆◆◆◇◇◇ +◇◇◇◇◇◇◆◆◇◇◇◇◆◆◆◆◆◆◆◆◆◇◇◇ +◇◇◇◇◇◇◆◆◇◇◇◇◇◆◆◆◆◆◆◆◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇ +◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇` + diff --git a/src/study/2.md b/src/study/2.md new file mode 100644 index 0000000..9605237 --- /dev/null +++ b/src/study/2.md @@ -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问题为:设编号为1,2,3,....n的n个人围坐一圈,约定编号为K(1<=k<=n)的人从1开始报数,数到m的那个人出列,他的下一位又从1开始报数,数到m的那个人又出列,依次类推,知道所有人出列为止,由此产生一个出队编号的序列。 + +提示:用一个不带头节点的循环列表来处理Josephu问题:先构成一个由n个结点的单循环链表(单向循环列表),然后由k结点从1开始计数。计到m时,对应结点从链表中删除,然后再从被删除结点的下一个删除结点又开始从1开始技术,直到最后一个结点从链表中删除算法结束。 + +- 其它常见问题 + +修路问题 —> 最小生成树(数+ 普利姆算法) + +最短路径问题 —> 图 + 弗洛伊德算法 + +汉诺塔问题 —> 分治算法 + +八皇后问题 —>回溯法 + +## 线性结构和非线性结构 + +> 线性结构 + +- 线性结构作为最常用的数据结构,其特点是**数据元素之间存在一对一**的线性关系。 +- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储结构的线性表称为顺序表,顺序表中的存储元素是连续的。 +- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。 +- 线性结构常见的有:数组、队列、链表和栈,后面我会相信讲解。 + +> 非线性结构 + +非线性结构包括:二维数组,多维数组,广义表,树结构,图结构 diff --git a/src/study/3.md b/src/study/3.md new file mode 100644 index 0000000..52c3ef2 --- /dev/null +++ b/src/study/3.md @@ -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() { + // 例如最大容量为5,rear是指向队列尾部数据,所以rear为4(maxSize - 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]; + } +} +``` + diff --git a/src/study/4.md b/src/study/4.md new file mode 100644 index 0000000..1db0eb8 --- /dev/null +++ b/src/study/4.md @@ -0,0 +1,1134 @@ +![image-20211130120246042](数据结构与算法/image-20211130120246042.png) + +## 单链表 + +链表是有序的列表,但是它在内存中的存储如下: + +![image-20211128161904800](数据结构与算法/image-20211128161904800.png) + +- 链表是以节点的方式来存储, 是**链式存储** +- 每个节点包含 data 域(存储数据),next 域(指向下一个节点) +- **链表的各个节点不一定是连续存储** +- 链表分为**带头节点的链表和 没有头节点的链表**,根据实际的需求来确定 + +> 单链表的应用实例 + +使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作, 注: 删除和修改,查找 +可以考虑学员独立完成,也可带学员完成 +1) 第一种方法在添加英雄时,直接添加到链表的尾部 + +![image-20211128162803435](数据结构与算法/image-20211128162803435.png) + +2) 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示) + +![image-20211128163006566](数据结构与算法/image-20211128163006566.png) + +> 代码 + +```java +public class SingleLinkedListDemo { + public static void main(String[] args) { + // 先创建节点 + HeroNode her1 = new HeroNode(1, "宋江", "及时雨"); + HeroNode her2 = new HeroNode(2, "卢俊义", "玉麒麟"); + HeroNode her3 = new HeroNode(3, "吴用", "智多星"); + HeroNode her4 = new HeroNode(4, "林冲", "豹子头"); + + // 创建一个链表 + SingleLinkedList singleLinkedList = new SingleLinkedList(); + // 无顺序加入节点 +// singleLinkedList.add(her1); +// singleLinkedList.add(her2); +// singleLinkedList.add(her3); +// singleLinkedList.add(her4); + // singleLinkedList.add(her4); (不考虑排序)如果重复添加一个对象就会死循环,因为第一次添加到队尾的时候next还为空,再次添加next就为自己本身就死循环了 + + // 按照编号顺序加入节点 + singleLinkedList.addByOrder(her1); + singleLinkedList.addByOrder(her4); + singleLinkedList.addByOrder(her2); + singleLinkedList.addByOrder(her3); + singleLinkedList.addByOrder(her3); // 不能重复插入 + + // 显示 + singleLinkedList.list(); + + // 测试修改节点 + HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~"); + singleLinkedList.update(newHeroNode); + System.out.println("修改之后的链表存储情况:"); + singleLinkedList.list(); + + // 删除节点 + singleLinkedList.delete(1); + singleLinkedList.delete(4); + singleLinkedList.delete(3); + singleLinkedList.delete(2); + System.out.println("删除之后的链表存储情况:"); + singleLinkedList.list(); + } +} + +// 定义SingleLinkedList 管理我们的英雄 +class SingleLinkedList { + // 初始化一个头节点,头节点不动,不存放具体的数据 + private HeroNode head = new HeroNode(0, "", ""); + // 初始化一个尾节点,指向最后一个元素,默认等于head + private HeroNode tail = head; + + // 添加节点到单向链表 + // 不考虑编号顺序时 + // 1. 找到当前链表的最后节点 + // 2. 将最后这个节点的next指向新的节点 + public void add(HeroNode heroNode) { + tail.setNext(heroNode); + tail = heroNode; + } + + // 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示) + public void addByOrder(HeroNode heroNode) { + // 因为头节点不能动,需要一个辅助指针(变量)来帮助涨找到添加的位置 + // 因为时单链表,所以我们找的temp是位于添加位置的前一个节点,否则加入不了 + HeroNode temp = head; + boolean flag = false; // 添加的编号是否存在,默认为false + while(true) { + if(temp.getNext() == null) { // 说明链表已经在链表的最后 + break; + } + if(temp.getNext().getNo() > heroNode.getNo()) { // 位置已经找到,应该在temp和temp.getNext()之间 + break; + } else if (temp.getNext().getNo() == heroNode.getNo()) { // 说明希望添加heroNode的编号已经存在 + flag = true; // 说明编号存在 + break; + } + temp = temp.getNext(); // 后移,遍历当前列表 + } + // 判断flag的值 + if(flag) { // 不能添加,说明编号存在 + System.out.printf("准备插入的英雄编号%d已经存在了\n", heroNode.getNo()); + } else { + // 插入到链表中 + heroNode.setNext(temp.getNext()); + temp.setNext(heroNode); + } + } + + // 修改节点的信息,根据编号来修改 + public void update(HeroNode newHeroNode) { + // 判断是否为空 + if (head.getNext() == null){ + System.out.println("链表为空~~"); + return; + } + // 找到需要修改的节点 + // 定义一个辅助变量 + HeroNode temp = head.getNext(); + boolean flag = false; // 表示是否找到这个节点 + while (true) { + if (temp == null) { + break; // 已经遍历完了链表 + } + // 如果no是Integer服装类型,不能使用 == ,而应该用 equals + if (temp.getNo() == newHeroNode.getNo()) { + // 找到节点 + flag = true; + break; + } + temp = temp.getNext(); + } + // 根据flag判断是否找到要修改的节点 + if (flag) { + temp.setName(newHeroNode.getName()); + temp.setNickname(newHeroNode.getNickname()); + } else { // 没有找到 + System.out.printf("没有找到编号为 %d 的节点,不能修改\n", newHeroNode.getNo()); + } + } + + // 删除节点 + public void delete(int no) { + HeroNode temp = head; + boolean flag = false; // 标志是否找到待删除节点 + while (true) { + if (temp.getNext() == null) { // 已经到节点的最后 + break; + } + if (temp.getNext().getNo() == no) { + // 找到待删除节点的前一个节点temp + flag = true; + break; + } + temp = temp.getNext(); + } + // 判断flag + if (flag) { // 找到 + // 可以删除 + temp.setNext(temp.getNext().getNext()); + } else { + System.out.printf("要删除的 %d 节点不存在\n", no); + } + } + + // 显示链表 + public void list() { + if (head.getNext() == null) { + System.out.println("链表为空"); + return; + } + // 因为头节点,不能动。因此我们需要一个辅助遍历来遍历 + HeroNode temp = head.getNext(); + while (temp != null){ + System.out.println(temp); + temp = temp.getNext(); + } + } +} +// 定义HeroNode,每个HeroNode对象就是一个节点 +class HeroNode { + private int no; + private String name; + private String nickname; + private HeroNode next; // 指向下一个节点 + // 构造器 + public HeroNode(int no, String name, String nickname){ + this.no = no; + this.name = name; + this.nickname = nickname; + } + + // 为了显示方便,我们重写一下toString + @Override + public String toString() { + return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]"; + } + + 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 String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public HeroNode getNext() { + return next; + } + + public void setNext(HeroNode next) { + this.next = next; + } +} +``` + +## 单链表面试题 + +> 求单链表中有效节点的个数 + +```java +/** + * 方法:获取单链表的节点个数(如果是带头结点的链表,需求不统计头节点) + * @param head 链表的头节点 + * @return 返回的就是有效节点的个数 + */ +public static int getLength(HeroNode head) { + if (head.getNext() == null) { // 空链表 + return 0; + } + int length = 0; + // 定义一个辅助变量 + HeroNode temp = head.getNext(); + while (temp != null) { + length++; + temp = temp.getNext(); // 遍历 + } + return length; +} +``` + +>查找单链表的倒数第k个节点【新浪面试题】 + +```java + /** + * 查找单链表的倒数第k个节点【新浪面试题】 + * 两次遍历方法 + * @param head 链表的 头节点 + * @param index 倒数位置索引 + * @return 倒数index的节点 + */ + public static HeroNode findLastIndexNode1(HeroNode head, int index) { + // 如果链表为空,返回null + if (head.getNext() == null) { + return null; // 没有找到 + } + // 第一个遍历得到链表的长度(节点个数) + int size = getLength(head); + // 第二次遍历 size - index 位置,就是我们倒数的第k个节点 + // 先做一个index的校验 + if (index <= 0 || index > size){ + return null; + } + // 这里需要获得倒数第index节点,所以这里遍历从head.getNext()起,与findFirstIndexNode从head起不一致 + HeroNode temp = head.getNext(); + for (int i = 0; i < size - index; i++) { + temp = temp.getNext(); + } + return temp; + } + + /** + * 快慢指针方法 + * @param head 链表的 头节点 + * @param index 倒数位置索引 + * @return 倒数index的节点 + */ + public static HeroNode findLastIndexNode2(HeroNode head, int index) { + // 调用方法获取正数第index个节点 + HeroNode quick = findFirstIndexNode(head, index); + if(quick == null) { + // 如果为空,说明链表长度太短,直接返回 + return null; + } + HeroNode slow = head.getNext(); + while (quick.getNext() != null) { + quick = quick.getNext(); + slow = slow.getNext(); + } + return slow; + } + + /** + * @param head 链表的 头节点 + * @param index 正数位置索引 + * @return 正数index的节点 + */ + public static HeroNode findFirstIndexNode(HeroNode head, int index) { + if(index <= 0){ + return null; + } + if(head.getNext() == null) { + return null; + } + // 这里需要获得正数第index节点,所以这里遍历从head.getNext()起,与findFirstIndexNode从head起不一致 + HeroNode temp = head; + for (int i = 0; i < index; i++) { + if(temp == null) { + return null; + } + temp = temp.getNext(); + } + return temp; + } +``` + +> 单链表的反转【腾讯面试题】 + +```java + /** + * 将单链表反转 + */ + public static void listReverse(HeroNode head) { + // 如果当前链表为空,或者只有一个节点,则无需反转,直接返回 + if(head.getNext() == null || head.getNext().getNext() == null) { + return; + } + // 定义一个辅助的指针(变量),帮助我们遍历原来的链表 + HeroNode temp = head.getNext(); + // 定义当前节点temp的下一个节点 + HeroNode next = null; + HeroNode reverseHead = new HeroNode(0, "", ""); + // 遍历原来的链表,每遍历一个节点,将其取出,放在reserveHead的最前端 + while (temp != null) { + // 先暂时保存当前节点的下一个节点,方便后面使用 + next = temp.getNext(); + // 将temp的下一个节点指向新链表的最前端 + temp.setNext(reverseHead.getNext()); + // 将temp插入到新链表的最前端 + reverseHead.setNext(temp); + // 然后temp后移 + temp = next; + } + // 将head的next指向reverseHead的next + head.setNext(reverseHead.getNext()); + } +``` + +> 从头到尾打印单链表【方式1:反向遍历,方式2:Stack栈】 + +```java + /** + * 利用栈这个数据结构,将各个节点压入栈中,实现逆序打印的效果,没有改变链表的结构 + */ + public static void reversePrint(HeroNode head) { + if (head.getNext() == null) { + // 空链表,不能打印 + return; + } + // 创建一个栈,将各个节点压入栈 + Stack stack = new Stack(); + HeroNode temp = head.getNext(); + // 将链表的所有节点压入栈 + while(temp != null) { + stack.push(temp); + temp = temp.getNext(); + } + // 将栈中节点进行打印 + while (stack.size() > 0) { + System.out.println(stack.pop()); // 先进后出 + } + } +``` + +> 合并两个有序的单链表,合并之后的链表依然是有序的 + +两种实现方法: + +```java + /** + * 以其中一个链表为主,依次向这个链表中插入另一个链表的元素 + * @param head1 + * @param head2 + * @return 合并之后的链表 + */ + public static HeroNode mergeList1(HeroNode head1, HeroNode head2) { + HeroNode newHead = head1; + HeroNode temp = head2.getNext(); + while(temp != null) { + HeroNode next = temp.getNext(); + temp.setNext(null); + addByOrder(newHead, temp); + temp = next; + } + return newHead; + } + + /** + * 将节点按照顺序新增到链表之中 + * @param head 链表的头节点 + * @param heroNode 代新增的头节点 + */ + public static void addByOrder(HeroNode head, HeroNode heroNode) { + // 因为头节点不能动,需要一个辅助指针(变量)来帮助涨找到添加的位置 + // 因为时单链表,所以我们找的temp是位于添加位置的前一个节点,否则加入不了 + HeroNode temp = head; + boolean flag = false; // 添加的编号是否存在,默认为false + while(true) { + if(temp.getNext() == null) { // 说明链表已经在链表的最后 + break; + } + if(temp.getNext().getNo() > heroNode.getNo()) { // 位置已经找到,应该在temp和temp.getNext()之间 + break; + } else if (temp.getNext().getNo() == heroNode.getNo()) { // 说明希望添加heroNode的编号已经存在 + flag = true; // 说明编号存在 + break; + } + temp = temp.getNext(); // 后移,遍历当前列表 + } + // 判断flag的值 + if(flag) { // 不能添加,说明编号存在 + System.out.printf("准备插入的英雄编号【%d】已经存在了\n", heroNode.getNo()); + } else { + // 插入到链表中 + heroNode.setNext(temp.getNext()); + temp.setNext(heroNode); + } + } + + /** + * 直接将两个有序链表合并成一个新的有序链表 + * @param head1 有序链表1 + * @param head2 有序链表2 + * @return 合并之后的链表 + */ + public static HeroNode mergeList2(HeroNode head1, HeroNode head2) { + HeroNode newHead = new HeroNode(0,"",""); + HeroNode tail = newHead; + newHead.setNext(tail); + HeroNode temp1 = head1.getNext(); + HeroNode temp2 = head2.getNext(); + while (temp1 != null && temp2 != null) { + Integer flag = compareNode(temp1, temp2); + if(flag.equals(1)) { // 说明节点1大于节点2 + HeroNode next = temp2.getNext(); // 暂时存储temp2的下一个节点 + temp2.setNext(null); // 将temp2的next设为null + tail.setNext(temp2); // 将temp2加入新的链表中 + tail = temp2; // tail变成了新增的节点temp2 + temp2 = next; // temp2后移 + } else if (flag.equals(0)) { // 说明节点1小于节点2 + HeroNode next = temp1.getNext(); // 暂时存储temp1的下一个节点 + temp1.setNext(null); // 将temp1的next设为null + tail.setNext(temp1); // 将temp1加入到新的链表中 + tail = temp1; // tail变成了新增的节点temp1 + temp1 = next; // temp1后移 + } else { // 说明节点1和节点2的no相等,新增其中一个就行了 + HeroNode next1 = temp1.getNext(); + HeroNode next2 = temp2.getNext(); + tail.setNext(temp1); + tail = temp1; + temp1 = next1; + temp2 = next2; + } + } + if(temp1 == null) { // 说明是链表1先遍历到最后一个 + tail.setNext(temp2); // 不用考虑tail的值会和temp2的值相等,上面相等的话同时后移 + } else { // 说明是链表2先遍历到最后一个 + tail.setNext(temp1); // 不用考虑tail的值会和temp2的值相等,上面相等的话同时后移 + } + return newHead; + } + + /** + * 比较两个节点的no值,-1表示相等,1表示节点1大于节点2,0表示节点1小于节点2 + * @param node1 节点1 + * @param node2 节点2 + * @return + */ + public static Integer compareNode(HeroNode node1, HeroNode node2) { + if(node1.getNo() == node2.getNo()) { + return -1; + } + return node1.getNo() > node2.getNo() ? 1 : 0; + } +``` + +测试: + +```java +public static void main(String[] args) { + HeroNode her1 = new HeroNode(1, "宋江", "及时雨"); + HeroNode her2 = new HeroNode(2, "卢俊义", "玉麒麟"); + HeroNode her3 = new HeroNode(3, "吴用", "智多星"); + HeroNode her4 = new HeroNode(4, "林冲", "豹子头"); + // 链表1 + SingleLinkedList singleLinkedList = new SingleLinkedList(); + // 按照编号顺序加入节点 + singleLinkedList.addByOrder(her1); + singleLinkedList.addByOrder(her4); + singleLinkedList.addByOrder(her2); + singleLinkedList.addByOrder(her3); + singleLinkedList.addByOrder(her3); // 不能重复插入 + + HeroNode her5 = new HeroNode(5, "宋江1", "及时雨"); + HeroNode her6 = new HeroNode(6, "卢俊义1", "玉麒麟"); + HeroNode her7 = new HeroNode(7, "吴用1", "智多星"); + HeroNode her8 = new HeroNode(8, "林冲1", "豹子头"); + // 链表2 + SingleLinkedList singleLinkedList1 = new SingleLinkedList(); + singleLinkedList1.addByOrder(new HeroNode(2, "卢俊义", "玉麒麟")); + singleLinkedList1.addByOrder(new HeroNode(4, "林冲", "豹子头")); + singleLinkedList1.addByOrder(her5); + singleLinkedList1.addByOrder(her6); + singleLinkedList1.addByOrder(her7); + singleLinkedList1.addByOrder(her8); + + System.out.println("合并之前链表1:"); + singleLinkedList.list(); + System.out.println("合并之前链表2:"); + singleLinkedList1.list(); + + System.out.println("开始合并两个链表:"); +// HeroNode heroNode = mergeList1(singleLinkedList.getHead(), singleLinkedList1.getHead()); + HeroNode heroNode = mergeList1(singleLinkedList1.getHead(), singleLinkedList.getHead()); +// HeroNode heroNode = mergeList2(singleLinkedList.getHead(), singleLinkedList1.getHead()); +// HeroNode heroNode = mergeList2(singleLinkedList1.getHead(), singleLinkedList.getHead()); + + System.out.println("合并之后的有序链表:"); + HeroNode temp = heroNode.getNext(); + while (temp != null){ + System.out.println(temp); + temp = temp.getNext(); + } + } +``` + +输出结果: + +``` +合并之前链表1: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=小卢, nickname=玉麒麟~~] +HeroNode [no=3, name=吴用, nickname=智多星] +HeroNode [no=4, name=林冲, nickname=豹子头] +合并之前链表2: +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=4, name=林冲, nickname=豹子头] +HeroNode [no=5, name=宋江1, nickname=及时雨] +HeroNode [no=6, name=卢俊义1, nickname=玉麒麟] +HeroNode [no=7, name=吴用1, nickname=智多星] +HeroNode [no=8, name=林冲1, nickname=豹子头] +开始合并两个链表: +准备插入的英雄编号【2】已经存在了 +准备插入的英雄编号【4】已经存在了 +合并之后的有序链表: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=3, name=吴用, nickname=智多星] +HeroNode [no=4, name=林冲, nickname=豹子头] +HeroNode [no=5, name=宋江1, nickname=及时雨] +HeroNode [no=6, name=卢俊义1, nickname=玉麒麟] +HeroNode [no=7, name=吴用1, nickname=智多星] +HeroNode [no=8, name=林冲1, nickname=豹子头] +``` + +## 双向链表 + +> 单向链表的缺点分析 + +- 单向链表,**查找的方向只能是一个方向**,而双向链 + 表可以向前或者向后查找 +- 单向链表不能自我删除,需要靠辅助节点 ,而双向 + 链表,则可以**自我删除**,所以前面我们单链表删除 + 时节点,总是找到temp,temp是待删除节点的前一 + 个节点 + +> 双向链表分析 + +![image-20211130120123494](数据结构与算法/image-20211130120123494.png) + +分析 双向链表的遍历,添加,修改,删除的操作思路 + +- **遍历**方法和单链表一样,只是可以向前,也可以向后查找 +- **添加** (默认添加到双向链表的最后) + - 先找到双向链表的最后这个节点 + - temp.next = newHeroNode + - newHeroNode.pre = temp +- **修改**思路和 原来的单向链表一样 +- **删除** + - 因为是双向链表,因此可以实现自我删除某个节点 + - 直接找到要删除的这个节点,比如 temp + - temp.pre.next = temp.next + - temp.next.pre = temp.pre + +> 代码 + +```java +/** + * 双向链表 + * @author wuyou + */ +public class DoubleLinkedListDemo { + public static void main(String[] args) { + System.out.println("双向链表的测试:"); + // 创建节点 + HeroNode2 her1 = new HeroNode2(1, "宋江", "及时雨"); + HeroNode2 her2 = new HeroNode2(2, "卢俊义", "玉麒麟"); + HeroNode2 her3 = new HeroNode2(3, "吴用", "智多星"); + HeroNode2 her4 = new HeroNode2(4, "林冲", "豹子头"); + // 创建一个双向链表对象 + DoubleLinkedList doubleLinkedList = new DoubleLinkedList(); + doubleLinkedList.add(her1); + doubleLinkedList.add(her2); + doubleLinkedList.add(her3); + doubleLinkedList.add(her4); + doubleLinkedList.list(); + + // 修改 + HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙"); + doubleLinkedList.update(newHeroNode); + System.out.println("修改后的链表情况:"); + doubleLinkedList.list(); + + // 删除 + doubleLinkedList.delete(3); + System.out.println("删除之后的链表情况:"); + doubleLinkedList.list(); + + // 测试有序新增 + DoubleLinkedList doubleLinkedList1 = new DoubleLinkedList(); + doubleLinkedList1.addByOrder(her3); + doubleLinkedList1.addByOrder(her2); + doubleLinkedList1.addByOrder(her2); + doubleLinkedList1.addByOrder(her4); + doubleLinkedList1.addByOrder(her4); + doubleLinkedList1.addByOrder(her2); + doubleLinkedList1.addByOrder(her1); + System.out.println("测试有序增加链表:"); + doubleLinkedList1.list(); + + } +} + +/** + * 创建一个双向链表的类 + */ +class DoubleLinkedList { + /** + * 初始化一个头节点,头节点不动,不存放具体的数据 + */ + private HeroNode2 head = new HeroNode2(0, "", ""); + /** + * 初始化一个尾节点,指向最后一个元素,默认等于head + */ + private HeroNode2 tail = head; + + public HeroNode2 getHead() { + return head; + } + + /** + * 修改一个节点的内容 + * @param newHeroNode 修改节点对象 + */ + public void update(HeroNode2 newHeroNode) { + // 判断是否为空 + if (head.getNext() == null){ + System.out.println("链表为空~~"); + return; + } + // 找到需要修改的节点 + // 定义一个辅助变量 + HeroNode2 temp = head.getNext(); + // 表示是否找到这个节点 + boolean flag = false; + while (true) { + // 已经遍历完了链表 + if (temp == null) { + break; + } + // 如果no是Integer服装类型,不能使用 == ,而应该用 equals + if (temp.getNo() == newHeroNode.getNo()) { + // 找到节点 + flag = true; + break; + } + temp = temp.getNext(); + } + // 根据flag判断是否找到要修改的节点 + if (flag) { + temp.setName(newHeroNode.getName()); + temp.setNickname(newHeroNode.getNickname()); + } else { // 没有找到 + System.out.printf("没有找到编号为 %d 的节点,不能修改\n", newHeroNode.getNo()); + } + } + + /** + * 双向链表删除节点 + * 对应双向链表,我们可以直接找到要删除的这个节点,直接删除即可 + * @param no + */ + public void delete(int no) { + // 判断当前链表是否为空 + if(head.getNext() == null) { + System.out.println("链表为空,无法删除"); + return; + } + // 辅助变量 + HeroNode2 temp = head; + // 标志是否找到删除节点 + boolean flag = false; + while (true) { + // 已经找到链表的最后 + if (temp == null) { + break; + } + if (temp.getNo() == no) { + // 找到待删除节点 + flag = true; + break; + } + // temp后移,遍历 + temp = temp.getNext(); + } + // 判断flag,此时找到要删除的节点就是temp + if (flag) { + // 可以删除,将【temp的pre的next域】设置为【temp的next域】 + temp.getPre().setNext(temp.getNext()); + // 如果是最后一个节点,就不需要指向下面这句话,否则会出现空指针 temp.getNext().setPre(null.getPre()) + if (temp.getNext() != null) { + temp.getNext().setPre(temp.getPre()); + } + } + } + + /** + * 直接新增节点 + * @param heroNode 新增的节点 + */ + public void add(HeroNode2 heroNode) { + tail.setNext(heroNode); + heroNode.setPre(tail); + tail = heroNode; + } + + /** + * 有序新增节点 + * @param heroNode 新增的节点 + */ + public void addByOrder(HeroNode2 heroNode) { + HeroNode2 temp = head; + // 标记添加的编号是否已经存在 + boolean flag = false; + while (true) { + if (temp.getNext() == null) { + break; + } + // 位置已经找到,应该在temp和temp.getNext()之间 + if (temp.getNext().getNo() > heroNode.getNo()) { + break; + } + if (temp.getNext().getNo() == heroNode.getNo()) { + flag = true; + } + temp = temp.getNext(); + } + // 判断flag + if (flag) { + System.out.printf("准备插入的英雄编号【%d】已经存在了\n", heroNode.getNo()); + } else { + // 插入到链表中 + // 1、将【heroNode的next】设置为【temp的next】 + heroNode.setNext(temp.getNext()); + // 判断是不是加在链表最后 + if (temp.getNext() != null) { + // 2、将【temp的next的pre】设为为【heroNode】 + temp.getNext().setPre(heroNode); + } + // 3、将【temp的next】设置为【heroNode】 + temp.setNext(heroNode); + // 4、将【heroNode的pre】设置为【temp】 + heroNode.setPre(temp); + } + } + + /** + * 遍历打印双向链表的方法 + */ + public void list() { + if (head.getNext() == null) { + System.out.println("链表为空"); + return; + } + // 因为头节点,不能动。因此我们需要一个辅助遍历来遍历 + HeroNode2 temp = head.getNext(); + while (temp != null){ + System.out.println(temp); + temp = temp.getNext(); + } + } +} + +/** + * 定义HeroNode,每个HeroNode对象就是一个节点 + */ +class HeroNode2 { + private int no; + private String name; + private String nickname; + /** + * 指向下一个节点,默认为null + */ + private HeroNode2 next; + /** + * 指向前一个节点,默认为null + */ + public HeroNode2 pre; + + /** + * 构造器 + * @param no + * @param name + * @param nickname + */ + public HeroNode2(int no, String name, String nickname){ + this.no = no; + this.name = name; + this.nickname = nickname; + } + + @Override + public String toString() { + return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]"; + } + + 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 String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public HeroNode2 getNext() { + return next; + } + + public void setNext(HeroNode2 next) { + this.next = next; + } + + public HeroNode2 getPre() { + return pre; + } + + public void setPre(HeroNode2 pre) { + this.pre = pre; + } +} +``` + +输出结果: + +``` +双向链表的测试: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=3, name=吴用, nickname=智多星] +HeroNode [no=4, name=林冲, nickname=豹子头] +修改后的链表情况: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=3, name=吴用, nickname=智多星] +HeroNode [no=4, name=公孙胜, nickname=入云龙] +删除之后的链表情况: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=4, name=公孙胜, nickname=入云龙] +准备插入的英雄编号【2】已经存在了 +准备插入的英雄编号【4】已经存在了 +准备插入的英雄编号【2】已经存在了 +测试有序增加链表: +HeroNode [no=1, name=宋江, nickname=及时雨] +HeroNode [no=2, name=卢俊义, nickname=玉麒麟] +HeroNode [no=3, name=吴用, nickname=智多星] +HeroNode [no=4, name=公孙胜, nickname=入云龙] +``` + +## 单向环形链表 + +> 单向环形链表应用场景 + +![img](数据结构与算法/E@J{NBX[D_MQT92ADNIN187.png) + +Josephu(约瑟夫、约瑟夫环) 问题 Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数 到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由 此产生一个出队编号的序列。 + +提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结 点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直 到最后一个结点从链表中删除算法结束 + +> 算法分析 + +- 根据用户的输入,生成一个小孩出圈的顺序 + - n=7,即有7个人 + - k=1,从第1个人开始报数 + - m=3,每次数3下 +- 需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这一个节点 +- 小孩报数前,先让first和helper指针同时移动m-1此 +- 当这个小孩报数的时候,让first和hepler指针同时的移动m-1次 + - `first = first.getNext()` + - `helper.setNext(first)` +- 原来的first指向的节点就没有了任何引用(计数无法解决循环引用的问题,现在JVM都不使用引用计数算法了,不过这样确实会通过可达性分析算法被回收,是不可达对象) + +> 代码 + +```java +/** + * 解决约瑟夫问题 + * @author 22130 + */ +public class Josepfu { + public static void main(String[] args) { + // 测试构建环形链表和遍历 + CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); + // 加入7个小孩 + circleSingleLinkedList.addBoy(7); + circleSingleLinkedList.showBoy(); + + // 测试小孩出圈 3->6->2->7->5->1->4 + circleSingleLinkedList.countBoy(1, 3, 7); + } +} + +/** + * 创建一个环形的单向链表 + */ +class CircleSingleLinkedList { + /** + * 创建一个first节点,当前没有编号 + */ + private Boy first = null; + + /** + * 添加小孩节点,构建一个环形链表 + * @param nums + */ + public void addBoy(int nums) { + // nums做一个数据校验 + if (nums < 1) { + System.out.println("nums的值不正确"); + } + // 辅助指针,帮助构建环形链表 + Boy temp = null; + // 使用for来创建我们的环形链表 + for (int i = 1; i <= nums; i++) { + // 根据编号,创建小孩节点 + Boy boy = new Boy(i); + // 如果是第一个小孩 + if (i == 1) { + first = boy; + // 构成环 + first.setNext(first); + // 让temp指向第一个小孩 + temp = first; + } else { + temp.setNext(boy); + boy.setNext(first); + temp = boy; + } + } + } + + /** + * 遍历当前的环形链表 + */ + public void showBoy() { + // 判断链表是否为空 + if (first == null) { + System.out.println("没有任何小孩~"); + return; + } + // 因为first不能动,所以我们还需要使用一个辅助指针完成遍历 + Boy temp = first; + while (true) { + System.out.printf("小孩的编号%d \n", temp.getNo()); + // 说明遍历完毕 + if (temp.getNext() == first){ + break; + } + // temp后移 + temp = temp.getNext(); + } + } + + /** + * 根据用户的输入,计算出小孩出圈的顺序 + * @param firstNo 表示小孩从第几个小孩开始报数 + * @param countNum 表示一次数几下 + * @param nums 表示最初有多少个小孩在圈中 + */ + public void countBoy(int firstNo, int countNum, int nums) { + // 先对数据进行校验 + if (first == null || firstNo < 1 || firstNo > nums) { + System.out.println("参数输入有误,请重新输入"); + return; + } + // 创建辅助指针 + Boy helper = first; + // 需求创建一个辅助指针变量helper,事先应该指向环形链表的最后这个节点 + while (true) { + // 说明helper指向了最后小孩节点 + if (helper.getNext() == first) { + break; + } + helper = helper.getNext(); + } + // 小孩报数前,先让first和helper移动firstNo-1次 + for (int j = 0; j < firstNo - 1; j++) { + first = first.getNext(); + helper = helper.getNext(); + } + // 当小孩报数时,,让first和helper指针同时移动m-1此,然后出圈 + while (true) { + // 说明圈中只有一节点 + if (helper == first) { + break; + } + // 让first和helper指针同时的移动countNum-1 + for (int j = 0; j < countNum - 1; j++) { + first = first.getNext(); + helper = helper.getNext(); + } + // 这时first指向的节点,就是小孩要出圈的节点 + System.out.printf("小孩%d出圈\n", first.getNo()); + // 这是将first指向的小孩出圈 + first = first.getNext(); + helper.setNext(first); + } + System.out.printf("最后留在圈中的小孩编号%d\n", first.getNo()); + } + +} + +/** + * 创建一个Boy类,表示一个节点 + */ +class Boy { + /** + * 编号 + */ + private int no; + /** + * 指向下一个节点,默认为null + */ + private Boy next; + + public Boy(int no) { + this.no = no; + } + + public int getNo() { + return no; + } + + public void setNo(int no) { + this.no = no; + } + + public Boy getNext() { + return next; + } + + public void setNext(Boy next) { + this.next = next; + } +} +``` + +输出结果: + +``` +小孩的编号1 +小孩的编号2 +小孩的编号3 +小孩的编号4 +小孩的编号5 +小孩的编号6 +小孩的编号7 +小孩3出圈 +小孩6出圈 +小孩2出圈 +小孩7出圈 +小孩5出圈 +小孩1出圈 +最后留在圈中的小孩编号4 +``` + diff --git a/src/study/5.md b/src/study/5.md new file mode 100644 index 0000000..7f448ee --- /dev/null +++ b/src/study/5.md @@ -0,0 +1,1630 @@ +## 介绍 + +> 表达式计算 + +输入一个表达式`7*2*2-5+1-5+3-3` ,对于计算机而言,它接收到的就是一个字符串,通过栈的思想可以获得表达式的计算结果 + +> 栈的介绍 + +- 栈的英文为(stack) +- 栈是一个**先入后出(FIFO——First In Last Out)**的有序列表 +- 栈(stack)是限制线性表中元素的插入和删除**只能在线性表的同一端**进行的一种特殊的线性表。允许插入和删除的一端,为变化的一端,称为**栈顶(Top)**,另一端为固定的一端,称为**栈底(Bottom)** +- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶;而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除 + +![See the source image](数据结构与算法/OIP-C.JWEDGRBWyujV5QkxXo4E1QHaDG) + +> 栈的应用场景 + +- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中 +- 处理参数递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数、区域变量等数据存入堆栈中 +- 表达式的转换【中缀表达式转后缀表达式】和求值(实际解决) +- 二叉树的遍历 +- 图形的深度优先(depth——first)搜索法 + +## 快速入门 + +### 代码实现(数组) + +用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容, 下面我们就用数组模拟栈的出栈,入栈等操作。 + +```java +import java.util.Scanner; + +/** + * @author wuyou + */ +public class ArrayStackDemo { + public static void main(String[] args) { + // 测试 + // 先创建一个ArrayStack ——> 表示栈 + ArrayStack stack = new ArrayStack(4); + String key = ""; + // 控制是否退出菜单 + boolean loop = true; + Scanner scanner = new Scanner(System.in);System.out.println("show:表示显示栈"); + System.out.println("exit:表示退出程序"); + System.out.println("push:表示添加数据到栈(入栈)"); + System.out.println("pop:表示从栈取出数据(出栈)"); + while (loop) { + System.out.println("请输入你的选择:"); + key = scanner.next(); + switch (key) { + case "show": + stack.list(); + break; + case "push": + System.out.println("请输入一个数字:"); + int value = scanner.nextInt(); + stack.push(value); + break; + case "pop": + try { + int res = stack.pop(); + System.out.printf("出栈的数据是%d\n", res); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + break; + case "exit": + scanner.close(); + loop = false; + break; + default: + break; + } + } + System.out.println("程序退出~~"); + } +} + +/** + * 定义一个ArrayStack表示栈 + */ +class ArrayStack{ + /** + * 栈的大小 + */ + private int maxSize; + /** + * 数组,数组模拟栈,数组就放在该数组 + */ + private int[] stack; + /** + * top表示栈顶,初始化为-1 + */ + private int top = -1; + + /** + * 构造器 + * @param maxSize 栈的最大容量 + */ + public ArrayStack(int maxSize) { + this.maxSize = maxSize; + stack = new int[maxSize]; + } + + /** + * 栈满 + * @return 是否栈满 + */ + public boolean isFull() { + return top == maxSize - 1; + } + + /** + * 栈空 + * @return 是否为空 + */ + public boolean isEmpty() { + return top == -1; + } + + /** + * 入栈 + * @param value 入栈的值 + */ + public void push(int value) { + // 先判断栈是否满 + if (isFull()) { + System.out.println("栈满"); + return; + } + top++; + stack[top] = value; + } + + /** + * 出栈 + * @return int 出栈的值 + */ + public int pop() { + // 先判断栈是否为空 + if (isEmpty()) { + // 抛出异常 + throw new RuntimeException("栈空"); + } + int value = stack[top]; + top--; + return value; + } + + /** + * 显示栈的情况,遍历时需要从栈顶开始显示数据 + */ + public void list() { + if (isEmpty()) { + System.out.println("栈空,没有数据~"); + return; + } + for (int i = top; i >= 0; i--) { + System.out.printf("stack【%d】 = %d\n", i, stack[i]); + } + } +} +``` + +输出结果: + +``` +show:表示显示栈 +exit:表示退出程序 +push:表示添加数据到栈(入栈) +pop:表示从栈取出数据(出栈) +请输入你的选择: +show +栈空,没有数据~ +请输入你的选择: +pop +栈空 +请输入你的选择: +push +请输入一个数字: +10 +请输入你的选择: +push +请输入一个数字: +20 +请输入你的选择: +push +请输入一个数字: +30 +请输入你的选择: +pop +出栈的数据是30 +请输入你的选择: +show +stack【1】 = 20 +stack【0】 = 10 +请输入你的选择: +exit +程序退出~~ +``` + +### 代码实现(链表) + +核心思想就是通过链表的头插法插入节点,弹出元素直接将头节点header后移一位就行,同时保留elementCount属性记录节点数和size记录栈的大小 + +```java +/** + * @author wuyou + */ +public class LinkedListStackDemo { + public static void main(String[] args) throws InterruptedException { + // 测试 + // 初始化栈 + ListStack listStack = new ListStack(5); + System.out.println("初始化栈,栈大小为:【" + listStack.getSize() + "】"); + + listStack.push("节点1"); + System.out.println("push`节点1`,栈顶元素为:【" + listStack.peek() + "】"); + listStack.push("节点2"); + System.out.println("push`节点2`,栈顶元素为:【" + listStack.peek() + "】"); + listStack.push("节点3"); + System.out.println("push`节点3`,栈顶元素为:【" + listStack.peek() + "】"); + System.out.println("当前链表的的元素个数为:" + listStack.getElementCount()); + + listStack.push("节点4"); + System.out.println("push`节点4`,栈顶元素为:【" + listStack.peek() + "】"); + listStack.push("节点5"); + System.out.println("push`节点5`,栈顶元素为:【" + listStack.peek() + "】"); + try { + listStack.push("节点6"); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("push`节点6`失败,栈顶元素为:【" + listStack.peek() + "】"); + } + + Thread.sleep(1000); + System.out.println("当前链表的的元素个数为:" + listStack.getElementCount()); + + System.out.println("pop元素为:【" + listStack.pop() +"】,当前栈顶元素为:【" + listStack.peek() + "】"); + System.out.println("pop元素为:【" + listStack.pop() +"】,当前栈顶元素为:【" + listStack.peek() + "】"); + System.out.println("当前链表的的元素个数为:" + listStack.getElementCount()); + System.out.println("pop元素为:【" + listStack.pop() +"】,当前栈顶元素为:【" + listStack.peek() + "】"); + System.out.println("pop元素为:【" + listStack.pop() +"】,当前栈顶元素为:【" + listStack.peek() + "】"); + System.out.println("pop元素为:【" + listStack.pop() +"】"); + System.out.println("当前链表的的元素个数为:" + listStack.getElementCount()); + System.out.println("当前栈顶元素为:"); + listStack.peek(); + } +} + +/** + * 表示链表的一个节点 + */ +class Node{ + Object element; + Node next; + + public Node(Object element) { + this(element, null); + } + + /** + * 头插法插入节点 + * @param element 新增节点的value + * @param n 原来的头节点 + */ + public Node(Object element, Node n) { + this.element = element; + next = n; + } + + public Object getElement() { + return element; + } + + public void setElement(Object element) { + this.element = element; + } + + public Node getNext() { + return next; + } + + public void setNext(Node next) { + this.next = next; + } +} + +/** + * 用链表实现堆栈 + */ +class ListStack { + /** + * 栈顶元素 + */ + Node header; + /** + * 栈内元素个数 + */ + int elementCount; + /** + * 栈的大小 + */ + int size; + + /** + * 构造函数,构造一个空的堆栈 + */ + public ListStack() { + header = null; + elementCount = 0; + size = 0; + } + + /** + * 通过构造器 自定义栈的大小 + * @param size 栈的大小 + */ + public ListStack(int size) { + header = null; + elementCount = 0; + this.size = size; + } + + /** + * 设置堆栈大小 + * @param size 堆栈大小 + */ + public void setSize(int size) { + this.size = size; + } + + /** + * 设置栈顶元素 + * @param header 栈顶元素 + */ + public void setHeader(Node header) { + this.header = header; + } + + /** + * 获取堆栈长度 + * @return 堆栈长度 + */ + public int getSize() { + return size; + } + + /** + * 返回栈中元素的个数 + * @return 栈中元素的个数 + */ + public int getElementCount() { + return elementCount; + } + + /** + * 判断栈是否为空 + * @return 如果栈是空的,返回真,否则,返回假 + */ + public boolean isEmpty() { + if (elementCount == 0) { + return true; + } + return false; + } + + /** + * 判断栈满 + * @return 如果栈是满的,返回真,否则,返回假 + */ + public boolean isFull() { + if (elementCount == size) { + return true; + } + return false; + } + + /** + * 把对象入栈 + * @param value 对象 + */ + public void push(Object value) { + if (this.isFull()) { + throw new RuntimeException("Stack is Full"); + } + header = new Node(value, header); + elementCount++; + } + + /** + * 出栈,并返回被出栈的元素 + * @return 被出栈的元素 + */ + public Object pop() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + Object obj = header.getElement(); + header = header.getNext(); + elementCount--; + return obj; + } + + /** + * 返回栈顶元素 + * @return 栈顶元素 + */ + public Object peek() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + return header.getElement(); + } +} +``` + +## 栈实现计算器(中缀) + +> 思路分析 + +![image-20211210145944818](数据结构与算法/image-20211210145944818.png) + +1. 初始化两个栈,一个【数栈】和一个【符号栈】 +1. 通过一个index值(索引),来遍历我们的表达式 +2. 如果发现是一个数字,就直接入【数栈】 +3. 如果发现是一个运算符,就分情况讨论 + 1. 如果当前的【符号栈】为空,那就直接入栈 + 2. 如果当前的【符号栈】有操作符,就进行比较 + 1. 【当前的操作符】的优先级 <= 【符号栈中操作符】的优先级,就需要从【数栈】中pop出两个数,再从【符号栈】中pop出一个符号,然后进行运算,将得到结果入【数栈】,再递归调用步骤3 + 2. 【当前的操作符】的优先级 > 【符号栈中操作符】的优先级,就直接入符号栈 +4. 当表达式扫描完毕,就顺序的从【数栈】和【符号栈】中pop相应的数和符号,并运行 +5. 最从【数栈】中只有一个数字就是表达式的结果 + + + +> 代码流程图 + +![计算机流程图](数据结构与算法/计算机流程图.png) + +> 代码实现 + +```java +/** + * @author wuyou + */ +public class Calculator { + public static void main(String[] args) { + // 测试运算 + String expression1 = "33+2+2*6-2"; + String expression2 = "7*22*2-5+1-5+3-4"; + String expression3 = "4/2*3-4*2-3-99"; + String expression4 = "1*1*1*3*2/3"; + String expression5 = "11*1*1*3*2/3"; + String expression6 = "1000*23"; + + // 创建两个栈:数栈、符号栈 + ListStack1 numStack = new ListStack1(10); + ListStack1 operationStack = new ListStack1(10); + + test(expression1, numStack, operationStack); + test(expression2, numStack, operationStack); + test(expression3, numStack, operationStack); + test(expression4, numStack, operationStack); + test(expression5, numStack, operationStack); + test(expression6, numStack, operationStack); + } + + /** + * 测试方法,测试表达式的结果,并且打印结果 + * @param expression 表达式 + * @param numStack 数字栈 + * @param operationStack 符号栈 + */ + public static void test(String expression, ListStack1 numStack, ListStack1 operationStack) { + // 用于扫描 + int index = 0; + // 将每次扫描得到的char保存到ch + char ch = ' '; + + // 开始while循环的扫描expression + while (true) { + // 依次得到expression的每一个字符 + ch = getCharByIndex(expression, index); + // 判断ch是什么,然后做相应的处理 + if (isOperation(ch)) { + // 运用管道过滤器风格,处理运算符 + operationSolve1(ch, numStack, operationStack); + } else { + // 数直接入数栈,对值为ASCII值-48 + // 当处理多位数时候,不能立即入栈,可能是多位数,调用过滤器处理多位数 + index = numSolve1(expression, index, numStack); + } + // 让index+1,并判断是否扫描到expression最后 + index++; + if (index >= expression.length()) { + break; + } + } + // 最后只剩下两个数和一个运算符 + int res = cal((int) numStack.pop(), (int) numStack.pop(), (char) operationStack.pop()); + System.out.printf("表达式: %s = %d\n", expression, res); + } + + /** + * 获取表达式的下标位置为index的字符 + * @param expression 表达式 + * @param index 下标 + * @return + */ + public static char getCharByIndex(String expression, int index) { + return expression.charAt(index); + } + + /** + * 处理数字入栈的情况,包含处理多位数的情况,并且返回到操作表达式当前的下标 + * @param expression 表达式 + * @param index 下标 + * @param numStack 数字栈 + * @return 新的下标 + */ + public static int numSolve1(String expression, Integer index, ListStack1 numStack) { + int end = index + 1; + for (; end < expression.length(); end++) { + char ch = getCharByIndex(expression, end); + // 判断是不是数字 + if (!isOperation(ch)) { + continue; + } else { + break; + } + } + String numStr = expression.substring(index, end); + // 数据入栈 + numStack.push(Integer.valueOf(numStr)); + // 因为test函数进行了+1,所以这里进行-1,避免给重复添加 + return end - 1; + } + + /** + * 符号过滤器1,判断当前是否具有字符 + * @param ch 运算符 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve1(char ch, ListStack1 numStack, ListStack1 operationStack) { + // 判断当前符号栈是否具有操作符 + if (!operationStack.isEmpty()) { + operationSolve2(ch, numStack, operationStack); + return; + } else { + operationStack.push(ch); + return; + } + } + + /** + * 符号过滤器2,处理字符优先级,递归调用过滤器1 + * @param ch 运算符 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve2(char ch, ListStack1 numStack, ListStack1 operationStack) { + // 比较优先级 + if (priority(ch) <= priority((Character) operationStack.peek())) { + // 调用过滤器3进行计算 + operationSolve3(numStack,operationStack); + // 递归调用过滤器1,不能递归调用过滤器2,因为可能存在当前运算符栈为空的情况 + operationSolve1(ch, numStack, operationStack); + return; + } else { + // 直接将运算符加入到运算符栈中 + operationStack.push(ch); + return; + } + } + + /** + * 符号过滤器3,进行运算 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve3(ListStack1 numStack, ListStack1 operationStack) { + // 定义相关变量 + int num1 = (int) numStack.pop(); + int num2 = (int) numStack.pop(); + char operation = (char) operationStack.pop(); + int res = cal(num1, num2, operation); + // 把运算结果加到数栈 + numStack.push(res); + return; + } + + /** + * 返回运算符的优先级,数字越大,运算符越高 + * @param operation 运算符 + * @return + */ + public static int priority(char operation) { + if (operation == '*' || operation == '/') { + return 1; + } else if (operation == '+' || operation == '-') { + return 0; + } else { + // 假设目前的表达式只有 + - * / + return -1; + } + } + + /** + * 判断是不是运算符 + * @param val 字符 + * @return 是不是运算符 + */ + public static boolean isOperation(char val) { + return val == '+' || val == '-' || val =='*' || val == '/'; + } + + /** + * 计算结果 + * @param num1 操作数1,先出栈的数 + * @param num2 操作数2,后出栈的数 + * @param operation 操作符 + * @return 计算结果 + */ + public static int cal(int num1, int num2, char operation) { + // 用于存放运算的结果 + int res = 0; + switch (operation) { + case '+': + res = num1 + num2; + break; + case '-': + // num1是先弹出来的数,为被减数 + res = num2 - num1; + break; + case '*': + res = num1 * num2; + break; + case '/': + // num1是先弹出来的数,为被除数 + res = num2 / num1; + default: + break; + } + return res; + } +} + +/** + * 表示链表的一个节点 + */ +class Node1{ + Object element; + Node1 next; + + public Node1(Object element) { + this(element, null); + } + + /** + * 头插法插入节点 + * @param element 新增节点的value + * @param n 原来的头节点 + */ + public Node1(Object element, Node1 n) { + this.element = element; + next = n; + } + + public Object getElement() { + return element; + } + + public void setElement(Object element) { + this.element = element; + } + + public Node1 getNext() { + return next; + } + + public void setNext(Node1 next) { + this.next = next; + } +} + +/** + * 用链表实现堆栈 + */ +class ListStack1 { + /** + * 栈顶元素 + */ + Node1 header; + /** + * 栈内元素个数 + */ + int elementCount; + /** + * 栈的大小 + */ + int size; + + /** + * 构造函数,构造一个空的堆栈 + */ + public ListStack1() { + header = null; + elementCount = 0; + size = 0; + } + + /** + * 通过构造器 自定义栈的大小 + * @param size 栈的大小 + */ + public ListStack1(int size) { + header = null; + elementCount = 0; + this.size = size; + } + + /** + * 设置堆栈大小 + * @param size 堆栈大小 + */ + public void setSize(int size) { + this.size = size; + } + + /** + * 设置栈顶元素 + * @param header 栈顶元素 + */ + public void setHeader(Node1 header) { + this.header = header; + } + + /** + * 获取堆栈长度 + * @return 堆栈长度 + */ + public int getSize() { + return size; + } + + /** + * 返回栈中元素的个数 + * @return 栈中元素的个数 + */ + public int getElementCount() { + return elementCount; + } + + /** + * 判断栈是否为空 + * @return 如果栈是空的,返回真,否则,返回假 + */ + public boolean isEmpty() { + if (elementCount == 0) { + return true; + } + return false; + } + + /** + * 判断栈满 + * @return 如果栈是满的,返回真,否则,返回假 + */ + public boolean isFull() { + if (elementCount == size) { + return true; + } + return false; + } + + /** + * 把对象入栈 + * @param value 对象 + */ + public void push(Object value) { + if (this.isFull()) { + throw new RuntimeException("Stack is Full"); + } + header = new Node1(value, header); + elementCount++; + } + + /** + * 出栈,并返回被出栈的元素 + * @return 被出栈的元素 + */ + public Object pop() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + Object obj = header.getElement(); + header = header.getNext(); + elementCount--; + return obj; + } + + /** + * 返回栈顶元素 + * @return 栈顶元素 + */ + public Object peek() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + return header.getElement(); + } +} +``` + +输出结果: + +``` +表达式: 33+2+2*6-2 = 45 +表达式: 7*22*2-5+1-5+3-4 = 298 +表达式: 4/2*3-4*2-3-99 = -104 +表达式: 1*1*1*3*2/3 = 2 +表达式: 11*1*1*3*2/3 = 22 +表达式: 1000*23 = 23000 +``` + +## 前缀、中缀、后缀表达式 + +### 前缀表达式 + +> 前缀表达式(波兰表达式) + +- 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前 +- 举例说明:(3+4)*5-6 对应的前缀表达式就是 - * + 3 4 5 6 + +> 前缀表达式的计算机求值 + +从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果 + + + +例如 (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下: + +- 从**右至左扫描**,将6、5、4、3压入堆栈 +- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈 +- 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈 +- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果 + +### 中缀表达式 + +> 中缀表达式 + +- 中缀表达式就是**常见的运算表达式**,如(3+4)*5-6 +- 中缀表达式的求值是平常最为熟悉的,但是对计算机说却不好操作(前面我们将的案例就能看出这个问题,)因此,在计算结束时,往往会将中缀表达式转成其它表达式来操作(一般是转成后缀表达式) + +### 后缀表达式 + +> 后缀表达式 + +- 后缀表达式又称**逆波兰表达式**,与前缀表达式相似,只是运算符位于操作数之后 +- 中举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 – + +| 正常的表达式 | 逆波兰表达式 | +| ------------ | ------------- | +| a+b | a b + | +| a+(b-c) | a b c - + | +| a+(b-c)*d | a b c – d * + | +| a+d*(b-c) | a d b c - * + | +| a=1+3 | a 1 3 + = | + +> 后缀表达式的计算机求值 + +从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果 + + + +例如: (3+4)×5-6 对应的后缀表达式就是 **3 4 + 5 × 6 -** **,** 针对后缀表达式求值步骤如下: + +- 从左至右扫描,将3和4压入堆栈 + +- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈 + +- 将5入栈 + +- 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈 + +- 将6入栈 +- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果 + +## 逆波兰计算器 + +> 我们完成一个逆波兰计算器,要求完成如下任务: + +- 输入一个逆波兰表达式(后缀表达式),使用栈(Stack), **计算其结果** + +- 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。 + +- 思路分析 + +- 代码完成 + +```java +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * @author wuyou + */ +public class PolandNotation { + public static void main(String[] args) { + // 定义一个逆波兰表达式 + // (3+4)*5-6 => 3 4 + 5 * 6 - + String suffixExpression = "3 4 + 5 * 6 -"; + // 先将3 4 + 5 * 6 - 放入一个链表,配合栈完成计算 + List listString = getListString(suffixExpression); + System.out.println(calculate(listString)); + } + + /** + * 将逆波兰表达式的数据和运算符依次放到ArrayList中 + * @param suffixExpression 逆波兰表达式 + * @return 链表 + */ + public static List getListString(String suffixExpression) { + // 将suffixExpression分割 + String[] split = suffixExpression.split(" "); + List list = new ArrayList<>(); + for (String element : split) { + list.add(element); + } + return list; + } + + /** + * 计算逆波兰表达式最终结果 + * @param stringList 数据和运算符链表 + * @return 计算结果 + */ + public static int calculate(List stringList) { + // 创建一个栈即可 + Stack stack = new Stack<>(); + // 遍历链表 + for (String element : stringList){ + // 使用正则表达式来取出数,匹配多位数 + if (element.matches("\\d+")) { + // 入栈 + stack.push(element); + } else { + // pop出两个数,并进行运算再入栈 + int num2 = Integer.parseInt(stack.pop()); + int num1 = Integer.parseInt(stack.pop()); + int res = 0; + if (element.equals("+")) { + res = num1 + num2; + } else if (element.equals("-")) { + res = num1 - num2; + } else if (element.equals("*")) { + res = num1 * num2; + } else if (element.equals("/")) { + res = num1 / num2; + } else { + throw new RuntimeException("运算符有误"); + } + // 把res 入栈 + stack.push(res + ""); + } + } + return Integer.valueOf(stack.pop()); + } +} +``` + +## 中缀表达式转换为后缀表达式 + +后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。 + +> 具体步骤如下: + +- 初始化两个栈:运算符栈s1和储存中间结果的栈s2 + +- 从左至右扫描中缀表达式 + +- 遇到操作数时,将其压s2 + +- 遇到运算符时,比较其与s1栈顶运算符的优先级: + - 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈 + - 否则,若优先级比栈顶运算符的高,也将运算符压入s1 + - 否则,将s1栈顶的运算符弹出并压入到s2中,再次与s1中新的栈顶运算符相比 +- 遇到括号时: + - 如果是左括号“(”,则直接压入s1 + - 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃 + +- ........ +- 重复步骤2至5,直到表达式的最右边 +- 将s1中剩余的运算符依次弹出并压入s2 +- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式 + +> 代码实现 + +```java +/** + * @author wuyou + */ +public class Calculator { + public static void main(String[] args) { + // 测试运算 + String expression1 = "33+2+2*6-2"; + String expression2 = "7*22*2-5+1-5+3-4"; + String expression3 = "4/2*3-4*2-3-99"; + String expression4 = "1*1*1*3*2/3"; + String expression5 = "11*1*1*3*2/3"; + String expression6 = "1000*23"; + + // 创建两个栈:数栈、符号栈 + ListStack1 numStack = new ListStack1(10); + ListStack1 operationStack = new ListStack1(10); + + test(expression1, numStack, operationStack); + test(expression2, numStack, operationStack); + test(expression3, numStack, operationStack); + test(expression4, numStack, operationStack); + test(expression5, numStack, operationStack); + test(expression6, numStack, operationStack); + } + + /** + * 测试方法,测试表达式的结果,并且打印结果 + * @param expression 表达式 + * @param numStack 数字栈 + * @param operationStack 符号栈 + */ + public static void test(String expression, ListStack1 numStack, ListStack1 operationStack) { + // 用于扫描 + int index = 0; + // 将每次扫描得到的char保存到ch + char ch = ' '; + + // 开始while循环的扫描expression + while (true) { + // 依次得到expression的每一个字符 + ch = getCharByIndex(expression, index); + // 判断ch是什么,然后做相应的处理 + if (isOperation(ch)) { + // 运用管道过滤器风格,处理运算符 + operationSolve1(ch, numStack, operationStack); + } else { + // 数直接入数栈,对值为ASCII值-48 + // 当处理多位数时候,不能立即入栈,可能是多位数,调用过滤器处理多位数 + index = numSolve1(expression, index, numStack); + } + // 让index+1,并判断是否扫描到expression最后 + index++; + if (index >= expression.length()) { + break; + } + } + // 最后只剩下两个数和一个运算符 + int res = cal((int) numStack.pop(), (int) numStack.pop(), (char) operationStack.pop()); + System.out.printf("表达式: %s = %d\n", expression, res); + } + + /** + * 获取表达式的下标位置为index的字符 + * @param expression 表达式 + * @param index 下标 + * @return + */ + public static char getCharByIndex(String expression, int index) { + return expression.charAt(index); + } + + /** + * 处理数字入栈的情况,包含处理多位数的情况,并且返回到操作表达式当前的下标 + * @param expression 表达式 + * @param index 下标 + * @param numStack 数字栈 + * @return 新的下标 + */ + public static int numSolve1(String expression, Integer index, ListStack1 numStack) { + int end = index + 1; + for (; end < expression.length(); end++) { + char ch = getCharByIndex(expression, end); + // 判断是不是数字 + if (!isOperation(ch)) { + continue; + } else { + break; + } + } + String numStr = expression.substring(index, end); + // 数据入栈 + numStack.push(Integer.valueOf(numStr)); + // 因为test函数进行了+1,所以这里进行-1,避免给重复添加 + return end - 1; + } + + /** + * 符号过滤器1,判断当前是否具有字符 + * @param ch 运算符 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve1(char ch, ListStack1 numStack, ListStack1 operationStack) { + // 判断当前符号栈是否具有操作符 + if (!operationStack.isEmpty()) { + operationSolve2(ch, numStack, operationStack); + return; + } else { + operationStack.push(ch); + return; + } + } + + /** + * 符号过滤器2,处理字符优先级,递归调用过滤器1 + * @param ch 运算符 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve2(char ch, ListStack1 numStack, ListStack1 operationStack) { + // 比较优先级 + if (priority(ch) <= priority((Character) operationStack.peek())) { + // 调用过滤器3进行计算 + operationSolve3(numStack,operationStack); + // 递归调用过滤器1,不能递归调用过滤器2,因为可能存在当前运算符栈为空的情况 + operationSolve1(ch, numStack, operationStack); + return; + } else { + // 直接将运算符加入到运算符栈中 + operationStack.push(ch); + return; + } + } + + /** + * 符号过滤器3,进行运算 + * @param numStack 数字栈 + * @param operationStack 运算符栈 + */ + public static void operationSolve3(ListStack1 numStack, ListStack1 operationStack) { + // 定义相关变量 + int num1 = (int) numStack.pop(); + int num2 = (int) numStack.pop(); + char operation = (char) operationStack.pop(); + int res = cal(num1, num2, operation); + // 把运算结果加到数栈 + numStack.push(res); + return; + } + + /** + * 返回运算符的优先级,数字越大,运算符越高 + * @param operation 运算符 + * @return + */ + public static int priority(char operation) { + if (operation == '*' || operation == '/') { + return 1; + } else if (operation == '+' || operation == '-') { + return 0; + } else { + // 假设目前的表达式只有 + - * / + return -1; + } + } + + /** + * 判断是不是运算符 + * @param val 字符 + * @return 是不是运算符 + */ + public static boolean isOperation(char val) { + return val == '+' || val == '-' || val =='*' || val == '/'; + } + + /** + * 计算结果 + * @param num1 操作数1,先出栈的数 + * @param num2 操作数2,后出栈的数 + * @param operation 操作符 + * @return 计算结果 + */ + public static int cal(int num1, int num2, char operation) { + // 用于存放运算的结果 + int res = 0; + switch (operation) { + case '+': + res = num1 + num2; + break; + case '-': + // num1是先弹出来的数,为被减数 + res = num2 - num1; + break; + case '*': + res = num1 * num2; + break; + case '/': + // num1是先弹出来的数,为被除数 + res = num2 / num1; + default: + break; + } + return res; + } +} + +/** + * 表示链表的一个节点 + */ +class Node1{ + Object element; + Node1 next; + + public Node1(Object element) { + this(element, null); + } + + /** + * 头插法插入节点 + * @param element 新增节点的value + * @param n 原来的头节点 + */ + public Node1(Object element, Node1 n) { + this.element = element; + next = n; + } + + public Object getElement() { + return element; + } + + public void setElement(Object element) { + this.element = element; + } + + public Node1 getNext() { + return next; + } + + public void setNext(Node1 next) { + this.next = next; + } +} + +/** + * 用链表实现堆栈 + */ +class ListStack1 { + /** + * 栈顶元素 + */ + Node1 header; + /** + * 栈内元素个数 + */ + int elementCount; + /** + * 栈的大小 + */ + int size; + + /** + * 构造函数,构造一个空的堆栈 + */ + public ListStack1() { + header = null; + elementCount = 0; + size = 0; + } + + /** + * 通过构造器 自定义栈的大小 + * @param size 栈的大小 + */ + public ListStack1(int size) { + header = null; + elementCount = 0; + this.size = size; + } + + /** + * 设置堆栈大小 + * @param size 堆栈大小 + */ + public void setSize(int size) { + this.size = size; + } + + /** + * 设置栈顶元素 + * @param header 栈顶元素 + */ + public void setHeader(Node1 header) { + this.header = header; + } + + /** + * 获取堆栈长度 + * @return 堆栈长度 + */ + public int getSize() { + return size; + } + + /** + * 返回栈中元素的个数 + * @return 栈中元素的个数 + */ + public int getElementCount() { + return elementCount; + } + + /** + * 判断栈是否为空 + * @return 如果栈是空的,返回真,否则,返回假 + */ + public boolean isEmpty() { + if (elementCount == 0) { + return true; + } + return false; + } + + /** + * 判断栈满 + * @return 如果栈是满的,返回真,否则,返回假 + */ + public boolean isFull() { + if (elementCount == size) { + return true; + } + return false; + } + + /** + * 把对象入栈 + * @param value 对象 + */ + public void push(Object value) { + if (this.isFull()) { + throw new RuntimeException("Stack is Full"); + } + header = new Node1(value, header); + elementCount++; + } + + /** + * 出栈,并返回被出栈的元素 + * @return 被出栈的元素 + */ + public Object pop() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + Object obj = header.getElement(); + header = header.getNext(); + elementCount--; + return obj; + } + + /** + * 返回栈顶元素 + * @return 栈顶元素 + */ + public Object peek() { + if (this.isEmpty()) { + throw new RuntimeException("Stack is empty"); + } + return header.getElement(); + } +} +``` + +输出结果: + +``` +后缀表达式字符串为:3 4 + 5 * 6 - +读取得到的后缀表达式链表为:[3, 4, +, 5, *, 6, -] +最终计算结果为:29 + +中缀表达式字符串为:1+((2+3)*4)-5 +读取得到的中缀表达式链表为:[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5] +转换得到的后缀表达式链表为:[1, 2, 3, +, 4, *, +, 5, -] +最终计算结果为:1+((2+3)*4)-5=16 +``` + +## 逆波兰表达式计算器完整版 + +> 说明 + +完整版的逆波兰计算器,功能包括如下: + +- 支持 + - * / ( ) +- 多位数,支持小数 +- 兼容处理, 过滤任何空白字符,包括空格、制表符、换页符 + +> 代码实现 + +```java +package com.atguigu.reversepolishcal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; +import java.util.regex.Pattern; + +public class ReversePolishMultiCalc { + + /** + * 匹配 + - * / ( ) 运算符 + */ + static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)"; + + static final String LEFT = "("; + static final String RIGHT = ")"; + static final String ADD = "+"; + static final String MINUS = "-"; + static final String TIMES = "*"; + static final String DIVISION = "/"; + + /** + * 加減 + - + */ + static final int LEVEL_01 = 1; + /** + * 乘除 * / + */ + static final int LEVEL_02 = 2; + + /** + * 括号 + */ + static final int LEVEL_HIGH = Integer.MAX_VALUE; + + + static Stack stack = new Stack<>(); + static List data = Collections.synchronizedList(new ArrayList()); + + /** + * 去除所有空白符 + * + * @param s + * @return + */ + public static String replaceAllBlank(String s) { + // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v] + return s.replaceAll("\\s+", ""); + } + + /** + * 判断是不是数字 int double long float + * + * @param s + * @return + */ + public static boolean isNumber(String s) { + Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$"); + return pattern.matcher(s).matches(); + } + + /** + * 判断是不是运算符 + * + * @param s + * @return + */ + public static boolean isSymbol(String s) { + return s.matches(SYMBOL); + } + + /** + * 匹配运算等级 + * + * @param s + * @return + */ + public static int calcLevel(String s) { + if ("+".equals(s) || "-".equals(s)) { + return LEVEL_01; + } else if ("*".equals(s) || "/".equals(s)) { + return LEVEL_02; + } + return LEVEL_HIGH; + } + + /** + * 匹配 + * + * @param s + * @throws Exception + */ + public static List doMatch(String s) throws Exception { + if (s == null || "".equals(s.trim())) throw new RuntimeException("data is empty"); + if (!isNumber(s.charAt(0) + "")) throw new RuntimeException("data illeagle,start not with a number"); + + s = replaceAllBlank(s); + + String each; + int start = 0; + + for (int i = 0; i < s.length(); i++) { + if (isSymbol(s.charAt(i) + "")) { + each = s.charAt(i) + ""; + // 栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈 + if (stack.isEmpty() || LEFT.equals(each) + || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)) { + stack.push(each); + } else if (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) { + // 栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈 + while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) { + if (calcLevel(stack.peek()) == LEVEL_HIGH) { + break; + } + data.add(stack.pop()); + } + stack.push(each); + } else if (RIGHT.equals(each)) { + // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈 + while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())) { + if (LEVEL_HIGH == calcLevel(stack.peek())) { + stack.pop(); + break; + } + data.add(stack.pop()); + } + } + start = i; // 前一个运算符的位置 + } else if (i == s.length() - 1 || isSymbol(s.charAt(i + 1) + "")) { + each = start == 0 ? s.substring(start, i + 1) : s.substring(start + 1, i + 1); + if (isNumber(each)) { + data.add(each); + continue; + } + throw new RuntimeException("data not match number"); + } + } + // 如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列 + Collections.reverse(stack); + data.addAll(new ArrayList<>(stack)); + + System.out.println(data); + return data; + } + + /** + * 算出结果 + * + * @param list + * @return + */ + public static Double doCalc(List list) { + Double d = 0d; + if (list == null || list.isEmpty()) { + return null; + } + if (list.size() == 1) { + System.out.println(list); + d = Double.valueOf(list.get(0)); + return d; + } + ArrayList list1 = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + list1.add(list.get(i)); + if (isSymbol(list.get(i))) { + Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i)); + list1.remove(i); + list1.remove(i - 1); + list1.set(i - 2, d1 + ""); + list1.addAll(list.subList(i + 1, list.size())); + break; + } + } + doCalc(list1); + return d; + } + + /** + * 运算 + * + * @param s1 + * @param s2 + * @param symbol + * @return + */ + public static Double doTheMath(String s1, String s2, String symbol) { + Double result; + switch (symbol) { + case ADD: + result = Double.valueOf(s1) + Double.valueOf(s2); + break; + case MINUS: + result = Double.valueOf(s1) - Double.valueOf(s2); + break; + case TIMES: + result = Double.valueOf(s1) * Double.valueOf(s2); + break; + case DIVISION: + result = Double.valueOf(s1) / Double.valueOf(s2); + break; + default: + result = null; + } + return result; + + } + + public static void main(String[] args) { + // String math = "9+(3-1)*3+10/2"; + String math = "12.8 + (2 - 3.55)*4+10/5.0"; + try { + doCalc(doMatch(math)); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} +``` + +输出结果: + +``` +[12.8, 2, 3.55, -, 4, *, +, 10, 5.0, /, +] +[8.600000000000001] +``` + diff --git a/src/study/6.md b/src/study/6.md new file mode 100644 index 0000000..501ca5f --- /dev/null +++ b/src/study/6.md @@ -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 3,2的出现是因为迷宫问题不会走重复路,不然会绕圈 + 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 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】次判断冲突的次数 +``` + diff --git a/src/study/7.md b/src/study/7.md new file mode 100644 index 0000000..4853d0e --- /dev/null +++ b/src/study/7.md @@ -0,0 +1,2780 @@ +## 概述 + +### 介绍 + +> 定义 + +排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列 + 的过程。 + +> 排序的分类: + +- 内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序。 +- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。 + +- 常见的排序算法分类如图: + +![image-20211214122555591](数据结构与算法/image-20211214122555591.png) + +> 算法的时间复杂度: + +度量一个程序(算法)执行时间的两种方法 + +- 事后统计的方法 + + 这种方法可行, 但是有两个问题: + + 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序 + + 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素 + + **这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快** + +- 事前估算的方法 + 通过分析某个算法的**时间复杂度**来判断哪个算法更优 + +> 时间频度 + +**时间频度**:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。**一个算法中的语句执行次数称为语句频度或时间频度**。记为T(n)。 + + + +比如设计计算1-100所有数字之和,我们设计两种算法: + +![image-20211214130955180](数据结构与算法/image-20211214130955180.png) + +此算法的时间频度为T(n) = n + 1 = 101 + + + +![image-20211214131142241](数据结构与算法/image-20211214131142241.png) + +此算法的时间频度为T(n) = 1 + + + +### 特点 + +> 举例说明—忽略常数项 + +| | T(n)=2n+20 | T(n)=2*n | T(n)=3n+10 | T(n)=3n | +| ---- | ---------- | -------- | ---------- | ------- | +| 1 | 22 | 2 | 13 | 3 | +| 2 | 24 | 4 | 16 | 6 | +| 5 | 30 | 10 | 25 | 15 | +| 8 | 36 | 16 | 34 | 24 | +| 15 | 50 | 30 | 55 | 45 | +| 30 | 80 | 60 | 100 | 90 | +| 100 | 220 | 200 | 310 | 300 | +| 300 | 620 | 600 | 910 | 900 | + +![image-20211214131548016](数据结构与算法/image-20211214131548016.png) + +结论: + +- 2n+20 和 2n 随着n 变大,执行曲线无限接近,20可以忽略 + +- 3n+10 和 3n 随着n 变大,执行曲线无限接近,10可以忽略 + + + +> 举例说明—忽略低次项 + +| | T(n)=2n^2+3n+10 | T(n)=2n^2 | T(n)=n^2+5n+20 | T(n)=n^2 | +| ---- | --------------- | --------- | -------------- | -------- | +| 1 | 15 | 2 | 26 | 1 | +| 2 | 24 | 8 | 34 | 4 | +| 5 | 75 | 50 | 70 | 25 | +| 8 | 162 | 128 | 124 | 64 | +| 15 | 505 | 450 | 320 | 225 | +| 30 | 1900 | 1800 | 1070 | 900 | +| 100 | 20310 | 20000 | 10520 | 10000 | + +![image-20211214132024072](数据结构与算法/image-20211214132024072.png) + +结论(类比洛必达法则): + +- 2n^2+3n+10 和 2n^2 随着n 变大,执行曲线无限接近,可以忽略 3n+10 +- n^2+5n+20 和 n^2 随着n 变大,执行曲线无限接近,可以忽略 5n+20 + + + +> 举例说明—忽略系数 + +| | T(n)=3n^2+2n | T(n)=5n^2+7n | T(n)=n^3+5n | T(n)=6n^3+4n | +| ---- | ------------ | ------------ | ----------- | ------------ | +| 1 | 5 | 12 | 6 | 10 | +| 2 | 16 | 34 | 18 | 56 | +| 5 | 85 | 160 | 150 | 770 | +| 8 | 208 | 376 | 552 | 3104 | +| 15 | 705 | 1230 | 3450 | 20310 | +| 30 | 2760 | 4710 | 27150 | 162120 | +| 100 | 30200 | 50700 | 1000500 | 6000400 | + +![image-20211214132233628](数据结构与算法/image-20211214132233628.png) + +结论(类比洛必达法则): + +- 随着n值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合,说明 这种情况下,5和3可以忽略 +- 而n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键 + +## 时间复杂度 + +- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。 + +- T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。 + +- 计算时间复杂度的方法: + - 用常数1代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1 + - 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n² + - 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²) + +> 常见的时间复杂度 + +- 常数阶O(1) + +- 对数阶O(log2n) + +- 线性阶O(n) + +- 线性对数阶O(nlog2n) + +- 平方阶O(n^2) + +- 立方阶O(n^3) + +- k次方阶O(n^k) + +- 指数阶O(2^n) + +![image-20211214133929119](数据结构与算法/image-20211214133929119.png) + +说明: + +- 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n^2)<Ο(n^3)< Ο(n^k) <Ο(2^n) < O(n!),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低 +- 从图中可见,我们应该尽可能避免使用指数阶的算法 + +### 常数阶O(1) + +- 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1) + +```java +int i = 1; +int j = 2; +++i; +j++; +int m = i + j; +``` + +- 上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。 + +### 对数阶O(log2n) + +```java +int n = 100; +int i = 1; +while (i < n) { + i = i * 2; +} +``` + + +说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n) + +![image-20211214134712046](数据结构与算法/image-20211214134712046.png) + +### 线性阶O(n) + +```java +int n = 100; +int j = 0; +for (int i = 1; i <= n; i++) { + j = i; + j++; +} +``` + + +说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度 + +### 线性对数阶O(nlogN) + +```java +int n = 100; +int m; +int i; +for (m = 1; m < n; m++) { + i = 1; + while (i < n) { + i = i * 2; + } +} +``` + + +说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n*O(log2N),也就是了O(nlog2N) + +### 平方阶O(n²) + +```java +int x; +int j; +int i = 0; +int n = 100; +for (x = 1; i <= n; x++) { + for (i = 1; i <= n; i++) { + j = i; + j ++; + } +} +``` + + +说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n) + +### 立方阶O(n³)、K次方阶O(n^k) + +说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似 + +### 平均时间复杂度和最坏时间复杂度 + +- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。 +- 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入 +- 实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。平均时间复杂度和最坏时间复杂度是否一致,和算法有关,如图: + +![See the source image](数据结构与算法/v2-d7f37e654f8b13555d2fbf5fe18eb6a6_r.jpg) + +## 空间复杂度 + +- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数 +- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况 +- 在做算法分析时,**主要讨论的是时间复杂度**。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis、memcache)和算法(基数排序)本质就是用空间换时间 + +## 冒泡排序 + +### 基本介绍 + +- 冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。 + +- 因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行) +- 遍历1次过后,可以指定已经排好序数据的下标flag,那么下1次排序排到flag就可以了,flag不断向前移动,移动到第1位之后排序就结束了 + +> 冒泡过程的例子(图解) + +![冒泡排序](数据结构与算法/冒泡排序.jpg) + + + +![冒泡排序](数据结构与算法/冒泡排序.gif) + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class BubbleSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{10, 3, -1, 9, 20}; + int[] array2 = array1.clone(); + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + System.out.println("array2排序之前的数据:" + Arrays.toString(array2)); + System.out.println("没有加flag的冒泡法array1排序次数为:" + bubbleSort1(array1)); + System.out.println("加了flag的冒泡法array2排序次数为:" + bubbleSort2(array2)); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + System.out.println("array2排序之后的数据:" + Arrays.toString(array2)); + + // 测试一下冒泡排序的速度O(n^2),给80000个数据,测试 + // 创建80000个随机的数组 + Random random = new Random(); + int[] array3 = new int[80000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4和array5 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + + // 测试 + System.out.println("\n测试80000条数据使用冒泡法进行排序:\n"); + + // 测试没有加上flag的冒泡排序 + long startTime1 = System.currentTimeMillis(); + int array3Count1 = bubbleSort1(array3); + long endTime1 = System.currentTimeMillis(); + System.out.println("没有加flag的冒泡法array3花费时间为:【" + (endTime1 - startTime1) + "】毫秒"); + System.out.println("没有加flag的冒泡法array3排序次数为:【" + array3Count1 + "】\n"); + + // 测试加上了flag的冒泡排序 + long startTime2 = System.currentTimeMillis(); + int array4Count = bubbleSort2(array4); + long endTime2 = System.currentTimeMillis(); + System.out.println("加上了flag的冒泡法array4花费时间为:【" + (endTime2 - startTime2) + "】毫秒"); + System.out.println("加上了flag的冒泡法array4排序次数为:【" + array4Count + "】\n"); + + // 测试没有返回值count的冒泡排序 + long startTime3 = System.currentTimeMillis(); + bubbleSort3(array5); + long endTime3 = System.currentTimeMillis(); + System.out.println("加上了flag、没有count返回值的冒泡法array5花费时间为:【" + (endTime3 - startTime3) + "】毫秒"); + } + + /** + * 冒泡法排序,没有加上flag + * @param array 数组 + */ + public static int bubbleSort1(int[] array) { + // 临时变量 + int temp = 0; + // 遍历的次数 + int count = 0; + // 因为接下来会使用 i + 1,为了避免数组越界,所以这里遍历的范围是 [0,array.length - 1) + for (int i = 0; i < array.length - 1; i++) { + count++; + // 上次遍历之后倒数第 i 位已经是有序的了,所以这次遍历的范围是 [0,array.length - 1 - i) + for (int j = 0; j < array.length - 1 - i; j++) { + // 如果前面的数比后面的数大,则交换 + if (array[j] > array[j + 1]) { + temp = array[j]; + array[j] = array[j + 1]; + array[j + 1] = temp; + } + } + } + return count; + } + + /** + * 冒泡法排序,加上flag判断是否进行了交换 + * @param array 数组 + */ + public static int bubbleSort2(int[] array) { + // 临时变量 + int temp = 0; + // 遍历的次数 + int count = 0; + // 标识变量,表示是否进行过交换 + // 在数量较小的情况下,事实上加了这个标识,虽然遍历的次数会变少,但所耗费的时间会变长。相比起判断赋值逻辑所耗费的时间,多遍历的时间几乎可以忽略不计 + boolean flag = false; + // 因为接下来会使用 i + 1,为了避免数组越界,所以这里遍历的范围是 [0,array.length - 1) + for (int i = 0; i < array.length - 1; i++) { + count++; + // 上次遍历之后倒数第 i 位已经是有序的了,所以这次遍历的范围是 [0,array.length - 1 - i) + for (int j = 0; j < array.length - 1 - i; j++) { + // 如果前面的数比后面的数大,则交换 + if (array[j] > array[j + 1]) { + flag = true; + temp = array[j]; + array[j] = array[j + 1]; + array[j + 1] = temp; + } + } + // 说明在一趟排序中依次都没有进行过交换 + if (flag == false) { + break; + } else { + flag = false; + } + } + return count; + } + + /** + * 冒泡法排序,加上flag判断是否进行了交换,不返回count次数 + * @param array 数组 + */ + public static void bubbleSort3(int[] array) { + // 临时变量 + int temp = 0; + // 标识变量,表示是否进行过交换 + // 在数量较小的情况下,事实上加了这个标识,虽然遍历的次数会变少,但所耗费的时间会变长。相比起判断赋值逻辑所耗费的时间,多遍历的时间几乎可以忽略不计 + boolean flag = false; + // 因为接下来会使用 i + 1,为了避免数组越界,所以这里遍历的范围是 [0,array.length - 1) + for (int i = 0; i < array.length - 1; i++) { + // 上次遍历之后倒数第 i 位已经是有序的了,所以这次遍历的范围是 [0,array.length - 1 - i) + for (int j = 0; j < array.length - 1 - i; j++) { + // 如果前面的数比后面的数大,则交换 + if (array[j] > array[j + 1]) { + flag = true; + temp = array[j]; + array[j] = array[j + 1]; + array[j + 1] = temp; + } + } + // 说明在一趟排序中依次都没有进行过交换 + if (flag == false) { + break; + } else { + flag = false; + } + } + } + + /** + * 打印数组 + * @param array 数组 + */ + public static void printArray(int[] array) { + for (int i = 0; i < array.length; i++) { + System.out.print(array[i] + " "); + } + } +} +``` + +运行结果: + +``` +array1排序之前的数据:[10, 3, -1, 9, 20] +array2排序之前的数据:[10, 3, -1, 9, 20] +没有加flag的冒泡法array1排序次数为:4 +加了flag的冒泡法array2排序次数为:3 +array1排序之后的数据:[-1, 3, 9, 10, 20] +array2排序之后的数据:[-1, 3, 9, 10, 20] + +测试80000条数据使用冒泡法进行排序: + +没有加flag的冒泡法array3花费时间为:【9003】毫秒 +没有加flag的冒泡法array3排序次数为:【79999】 + +加上了flag的冒泡法array4花费时间为:【8670】毫秒 +加上了flag的冒泡法array4排序次数为:【79330】 + +加上了flag、没有count返回值的冒泡法array5花费时间为:【8676】毫秒 +``` + +- 去掉了返回值count的统计,花费的时间反而变大了(多次测试普遍情况) +- 加上了flag,排序次数变小,但是排序时间不一定变小(多次测试普遍情况) +- 不同处理器和JDK版本都会有影响 + +## 选择排序 + +### 基本介绍 + +选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。 + +> 选择排序思想 + +选择排序(select sorting)也是一种简单的排序方法。 + +它的基本思想是: + +- 第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换 + +- 第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换 + +- 第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换 + +- … + +- 第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换 + +- … + +- 第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换 + +- 总共通过n-1次,得到一个按排序码从小到大排列的有序序列。 + +> 选择排序的例子(图解) + +![选择排序](数据结构与算法/选择排序.jpg) + + + +![选择排序](数据结构与算法/选择排序.gif) + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class SelectSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{101, 34, 119, 1, -1, 90, 123}; + int[] array2 = array1.clone(); + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + System.out.println("array2排序之前的数据:" + Arrays.toString(array2)); + selectSort2(array1); + System.out.println("从小到大排序,array1排序之后的数据:" + Arrays.toString(array1)); + selectSort3(array2); + System.out.println("从大到小排序,array2排序之后的数据:" + Arrays.toString(array2)); + + // 创建80000个随机的数组 + Random random = new Random(); + int[] array3 = new int[80000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4和array5 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + + // 测试 + System.out.println("\n测试80000条数据使用选择法进行排序:\n"); + + // 测试没有使用swap函数,从小到大 + long startTime1 = System.currentTimeMillis(); + selectSort1(array3); + long endTime1 = System.currentTimeMillis(); + System.out.println("没有使用swap方法,从小到大快速排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 测试使用了swap函数,从小到大 + long startTime2 = System.currentTimeMillis(); + selectSort2(array4); + long endTime2 = System.currentTimeMillis(); + System.out.println("使用了swap方法,从小到大快速排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 测试使用了swap函数,从大到小 + long startTime3 = System.currentTimeMillis(); + selectSort3(array5); + long endTime3 = System.currentTimeMillis(); + System.out.println("使用了swap方法,从小到大快速排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒"); + } + + /** + * 从小到大选择排序法,不使用swap函数 + * @param array 数组 + */ + public static void selectSort1(int[] array) { + int temp; + for (int i = 0; i < array.length; i++) { + // 使用下标来表示最小值 + int min = i; + for (int j = i + 1; j < array.length; j++) { + if (array[j] < array[min]) { + min = j; + } + } + // 如果min值发生了变化,则进行交换 + if (min != i) { + temp = array[min]; + array[min] = array[i]; + array[i] = temp; + } + } + } + + /** + * 从小到大选择排序法 + * @param array 数组 + */ + public static void selectSort2(int[] array) { + for (int i = 0; i < array.length; i++) { + // 使用下标来表示最小值 + int min = i; + for (int j = i + 1; j < array.length; j++) { + if (array[j] < array[min]) { + min = j; + } + } + // 如果min值发生了变化,则进行交换 + if (min != i) { + swap(array, i, min); + } + } + } + + /** + * 从大到小选择法排序 + * @param array 数组 + */ + public static void selectSort3(int[] array) { + for (int i = 0; i < array.length; i++) { + // 使用下标来表示最小值 + int min = i; + for (int j = i + 1; j < array.length; j++) { + if (array[j] > array[min]) { + min = j; + } + } + // 如果min值发生了变化,则进行交换 + if (min != i) { + swap(array, i, min); + } + } + } + + + /** + * 完成数组件两个元素的交换 + * @param array + * @param a + * @param b + */ + public static void swap(int[] array, int a, int b) { + int temp = array[a]; + array[a] = array[b]; + array[b] = temp; + } +} +``` + +输出结果: + +``` +array1排序之前的数据:[101, 34, 119, 1, -1, 90, 123] +array2排序之前的数据:[101, 34, 119, 1, -1, 90, 123] +从小到大排序,array1排序之后的数据:[-1, 1, 34, 90, 101, 119, 123] +从大到小排序,array2排序之后的数据:[123, 119, 101, 90, 34, 1, -1] + +测试80000条数据使用选择法进行排序: + +没有使用swap方法,从小到大快速排序法花费时间为:【2083】毫秒 + +使用了swap方法,从小到大快速排序法花费时间为:【1975】毫秒 + +使用了swap方法,从小到大快速排序法花费时间为:【2029】毫秒 +``` + +- 没有把数据交换封装成一个swap方法的情况下更慢(多次测试全是这样),**因为调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。** +- 从小到大比从大到小快一点点(普遍情况),可能和数据的偶然性有关吧 + +## 插入排序 + +### 基本介绍 + +插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。 + +> 插入排序法思想 + +插入排序(Insertion Sorting)的基本思想是: + +- 把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。 +- 插入排序由于操作不尽相同,可分为 `直接插入排序`、`折半插入排序`(又称二分插入排序)、`链表插入排序`、`希尔排序` 。 +- + +> 插入排序的例子(图解) + +![插入排序](数据结构与算法/插入排序.jpg) + + + +![插入排序](数据结构与算法/插入排序.gif) + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class InsertSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{101, 34, 119, 1, -1, 89}; + int[] array2 = array1.clone(); + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + System.out.println("array2排序之前的数据:" + Arrays.toString(array2)); + myInsertSort1(array1, array1.length); + System.out.println("从小到大,array1排序之后的数据:" + Arrays.toString(array1)); + myInsertSort2(array2, array2.length); + System.out.println("从大到小,array2排序之后的数据:" + Arrays.toString(array2)); + + // 创建80000个随机的数组 + Random random = new Random(); + int[] array3 = new int[80000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4和array5 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + + // 测试 + System.out.println("\n测试80000条数据使用插入法进行排序:\n"); + + // 书上的实现,从小到大 + long startTime1 = System.currentTimeMillis(); + insertionSort(array3, array3.length); + long endTime1 = System.currentTimeMillis(); + System.out.println("书上的方法,从小到大插入排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 我的实现,从小到大 + long startTime2 = System.currentTimeMillis(); + myInsertSort1(array4, array4.length); + long endTime2 = System.currentTimeMillis(); + System.out.println("我写的方法,从小到大插入排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 我的实现,从大到小 + long startTime3 = System.currentTimeMillis(); + myInsertSort2(array5, array5.length); + long endTime3 = System.currentTimeMillis(); + System.out.println("我写的方法,从大到小插入排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒\n"); + } + + /** + * 《数据结构与算法之美》书上的插入排序实现,插入排序最好不使用temp中间变量 + * @param array 数组 + * @param n 数组大小 + */ + public static void insertionSort(int[] array, int n) { + if (n <= 1) { + return; + } + // 从第2个元素开始遍历 + for (int i = 1; i < n; ++i) { + // value为当前要用来插入的变量 + int value = array[i]; + + // 遍历的范围为[0, i - 1),从大到小倒着遍历 + int j = i - 1; + // 查找插入的位置,依次判断 i 之前的第1个、第2个....是否比value大,如果大就要后移 + for (; j >= 0; --j) { + if (array[j] > value) { + // 数据移动 + array[j+1] = array[j]; + } else { + // 说明value比前面[0, j]个元素都大或者等于第j个元素,就应该插在j + 1的位置 + break; + } + } + // 插入数据,如果前面的都比value小,那么就array[j+1] = value就没有发生变化 + array[j+1] = value; + } + } + + /** + * 理解之后自己写的插入排序,从小到大 + * @param array + * @param n + */ + public static void myInsertSort1(int[] array, int n) { + if (n <= 1) { + return; + } + // 从第2个元素开始遍历 + for (int i = 1; i < n; i++) { + // value为当前要用来插入的变量 + int value = array[i]; + + // 遍历的范围为[0, i - 1),从大到小倒着遍历 + int j = i -1; + // 查找插入的值 + while (j >= 0) { + if (array[j] > value) { + array[j + 1] = array[j]; + j--; + } else { + // 说明value比前面[0, j]个元素都大或者等于第j个元素,就应该插在j + 1的位置 + break; + } + } + // 插入数据,如果前面的都比value小,那么就array[j+1] = value就没有发生变化 + array[j + 1] = value; + } + } + + /** + * 理解之后自己写的插入排序,从大到小 + * @param array + * @param n + */ + public static void myInsertSort2(int[] array, int n) { + if (n <= 1) { + return; + } + // 从第2个元素开始遍历 + for (int i = 1; i < n; i++) { + // value为当前要用来插入的变量 + int value = array[i]; + + // 遍历的范围为[0, i - 1),从大到小倒着遍历 + int j = i -1; + // 查找插入的值 + while (j >= 0) { + if (array[j] < value) { + array[j + 1] = array[j]; + j--; + } else { + // 说明value比前面[0, j]个元素都大或者等于第j个元素,就应该插在j + 1的位置 + break; + } + } + // 插入数据,如果前面的都比value小,那么就array[j+1] = value就没有发生变化 + array[j + 1] = value; + } + } +} +``` + +输出结果: + +``` +array1排序之前的数据:[101, 34, 119, 1, -1, 89] +array2排序之前的数据:[101, 34, 119, 1, -1, 89] +从小到大,array1排序之后的数据:[-1, 1, 34, 89, 101, 119] +从大到小,array2排序之后的数据:[119, 101, 89, 34, 1, -1] + +测试80000条数据使用插入法进行排序: + +书上的方法,从小到大插入排序法花费时间为:【455】毫秒 + +我写的方法,从小到大插入排序法花费时间为:【466】毫秒 + +我写的方法,从大到小插入排序法花费时间为:【403】毫秒 +``` + +- 插入排序可以不使用temp中间变量实现 +- 虽然插入排序的平均时间复杂度和冒泡排序、选择排序一样都是O(n^2),但是平均情况下,插入排序比选择排序快 + - 插入排序和选择排序比较: + - 比较开销:`选择排序`的比较开销是固定的n(n-1)/2,而`插入排序`平均下来是n(n-1)/4 + - 交换开销:`选择排序`最多只需要执行2*(n-1)次交换,而`插入排序`平均的交换开销也是n(n-1)/4 + - 这就取决于单次比较和交换的开销之比。如果是一个量级的,则插入排序优于选择排序,如果交换开销远大于插入开销,则插入排序可能比选择排序慢 + +## 希尔排序 + +### 基本介绍 + +简单插入排序存在的问题: + +数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小),这样的过程是: + +``` +{2,3,4,5,6,6} +{2,3,4,5,5,6} +{2,3,4,4,5,6} +{2,3,3,4,5,6} +{2,2,3,4,5,6} +{1,2,3,4,5,6} +``` + +结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响 + +希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种**插入排序**,它是简单插入排序经过改进之后的一个**更高效的版本**,也称为**缩小增量排序**。 + +> 希尔排序法基本思想 + +希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序; + +随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止 + +> 希尔排序的例子(图解) + +![image-20211214172906558](数据结构与算法/image-20211214172906558.png) + + + + + +![shell_sort](数据结构与算法/shell_sort.gif) + +### 分布解析说明 + +- 希尔排序时, 对有序序列在插入时采用**交换法**,并测试排序速度. +- 希尔排序时, 对有序序列在插入时采用**移动法**, 并测试排序速度 +- 交换法就是左边比最右边小就交换,然后左移一个步长再进行比较决定是否交换,这样不断交换;移动法是比最右边小的就后移一个步长,直到找到合适的位置插入最右边的元素;两种方式都实现了在每个区间最合适的位置插入最右边的元素 + + + +```java + /** + * 《数据结构与算法之美》书上的希尔排序实现 + * @param array 数组 + */ + public static void shellSort(int[] array) { + // step:步长 + int step = array.length / 2; + for (; step > 0; step /= 2) { + // 对一个步长区间进行比较 [step,array.length) + for (int i = step; i < array.length; i++) { + int value = array[i]; + int j; + + // 对步长区间中具体的元素进行比较 + for (j = i - step; j >= 0 && array[j] > value; j -= step) { + // j为左区间的取值,j + step为右区间与左区间的对应值。 + array[j + step] = array[j]; + } + // 此时step为一个负数,[j + step]为左区间上的初始交换值 + array[j + step] = value; + // 打印每一次交换的情况 + System.out.println(Arrays.toString(array)); + } + } + } +``` + +测试数组为`[8, 9, 1, 7, 2, 3, 5, 4, 6, 0]`,共10条数据进行分析: + +![image-20211214172906558](数据结构与算法/image-20211214172906558.png) + +所以代码执行流程如下: + +``` +array1排序之前的数据:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0] +*******************************增量变化step = array.length / 2,此时step = 2******************************* +// array【0】后移到array【5】,原本的array【5】插入到array【0】 +[3, 9, 1, 7, 2, 8, 5, 4, 6, 0] + +// array【1】后移到array【6】,原本的array【6】插入到array【1】 +[3, 5, 1, 7, 2, 8, 9, 4, 6, 0] + +// array【2】不后移,原本的array【7】插入到array【7】 +[3, 5, 1, 7, 2, 8, 9, 4, 6, 0] + +// array【3】后移到array【8】,原本的array【8】插入到array【3】 +[3, 5, 1, 6, 2, 8, 9, 4, 7, 0] + +// array【4】后移到array【9】,原本的array【9】插入到array【4】 +[3, 5, 1, 6, 0, 8, 9, 4, 7, 2] + +*****************************************增量变化step /= 2,此时step = 2*********************************** +// array【0】后移到array【2】,原本的array【2】插入到array【0】 +[1, 5, 3, 6, 0, 8, 9, 4, 7, 2] + +// array【1】不后移,原本的array【3】插入到array【3】 +[1, 5, 3, 6, 0, 8, 9, 4, 7, 2] + +// array【2】后移到array【4】,array【0】后移到array【2】,原本的array【4】插入到array【0】 +[0, 5, 1, 6, 3, 8, 9, 4, 7, 2] + +// array【3】不后移,原本的array【5】插入到array【5】 +[0, 5, 1, 6, 3, 8, 9, 4, 7, 2] + +// array【4】不后移,原本的array【6】插入到array【6】 +[0, 5, 1, 6, 3, 8, 9, 4, 7, 2] + +// array【5】后移到array【7】,array【3】后移到array【5】,array【1】后移到array【3】,原本的array【7】插入到array【1】 +[0, 4, 1, 5, 3, 6, 9, 8, 7, 2] + +// array【6】后移到array【8】,原本的array【8】插入到array【6】 +[0, 4, 1, 5, 3, 6, 7, 8, 9, 2] + +// array【7】后移到array【9】,array【5】后移到array【7】,array【3】后移到array【5】,array【1】后移到array【3】,原本的array【9】插入到array【1】 +[0, 2, 1, 4, 3, 5, 7, 6, 9, 8] + +*****************************************增量变化step /= 2,此时step = 1*********************************** +// array【0】不后移,原本的array【1】插入到array【1】 +[0, 2, 1, 4, 3, 5, 7, 6, 9, 8] + +// array【1】后移到array【2】,原本的array【2】插入到array【1】 +[0, 1, 2, 4, 3, 5, 7, 6, 9, 8] + +// array【2】不后移,原本的array【3】插入到array【3】 +[0, 1, 2, 4, 3, 5, 7, 6, 9, 8] + +// array【3】后移到array【4】,原本的array【4】插入到array【3】 +[0, 1, 2, 3, 4, 5, 7, 6, 9, 8] + +// array【4】不后移,原本的array【5】插入到array【5】 +[0, 1, 2, 3, 4, 5, 7, 6, 9, 8] + +// array【5】不后移,原本的array【6】插入到array【6】 +[0, 1, 2, 3, 4, 5, 7, 6, 9, 8] + +// array【6】后移到array【7】,原本的array【7】插入到array【6】 +[0, 1, 2, 3, 4, 5, 6, 7, 9, 8] + +// array【7】不后移,原本的array【8】插入到array【8】 +[0, 1, 2, 3, 4, 5, 6, 7, 9, 8] + +// array【8】后移到array【9】,原本的array【9】插入到array【8】 +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +array1排序之后的数据:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +### 代码实现 + +```java +/** + * @author wuyou + */ +public class ShellSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{8, 9, 1, 7, 2, 3, 5, 4, 6, 0, 10, 12, 11}; + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + myShellSort4(array1); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4和array5和array6和array7 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + int[] array6 = array3.clone(); + int[] array7 = array3.clone(); + + // 测试 + System.out.println("\n测试8000000条数据使用希尔排序法进行排序:\n"); + + // 移动法,书上的实现,从小到大 + long startTime1 = System.currentTimeMillis(); + shellSort(array3); + long endTime1 = System.currentTimeMillis(); + System.out.println("移动法,书上的方法,从小到大希尔排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 移动法,我的实现,从小到大 + long startTime2 = System.currentTimeMillis(); + myShellSort1(array4); + long endTime2 = System.currentTimeMillis(); + System.out.println("移动法,我写的方法,从小到大希尔排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 移动法,我的实现,从大到小 + long startTime3 = System.currentTimeMillis(); + myShellSort2(array5); + long endTime3 = System.currentTimeMillis(); + System.out.println("移动法,我写的方法,从大到小希尔排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒\n"); + + // 交换法,我的实现,从小到大 + long startTime4 = System.currentTimeMillis(); + myShellSort3(array6); + long endTime4 = System.currentTimeMillis(); + System.out.println("交换法,我写的方法,从小到大希尔排序法花费时间为:【" + (endTime4 - startTime4) + "】毫秒\n"); + + // 移动法,我的实现,步长基数从2变成3,从小到大 + long startTime5 = System.currentTimeMillis(); + myShellSort4(array7); + long endTime5 = System.currentTimeMillis(); + System.out.println("交换法,我写的方法,步长基数从2变成3,从小到大希尔排序法花费时间为:【" + (endTime5 - startTime5) + "】毫秒\n"); + + } + + /** + * 《数据结构与算法之美》书上的希尔排序实现,移动法 + * @param array 数组 + */ + public static void shellSort(int[] array) { + // step:步长 + int step = array.length / 2; + for (; step > 0; step /= 2) { + // 按照step步长划分为若干个区间,依次对这些区间进行插入排序 + for (int i = step; i < array.length; i++) { + // 暂时通过value保存要插入的值 + int value = array[i]; + int j; + + // 对步长区间中具体的元素进行比较,比value要大的都往后移一个step,直到区间最前面或者找到value可以插入的位置 + for (j = i - step; j >= 0 && array[j] > value; j -= step) { + // 满足j >= 0 && array[j] > value,说明依次将左区间满足的元素后移一个step + array[j + step] = array[j]; + } + // 此时j + step为可以插入的位置:有这两种情况 + // 如果上面遍历区间的array[j]都小于value,说明直接在原来的位置插入value就行了,没有发生变化 + // 如果上面遍历区间存在array[j] > value的元素,那么这些元素经过array[j + step] = array[j]都向后移了一个step,value就在它们空出来的位置插入即可 + array[j + step] = value; + } + } + } + + /** + * 理解之后自己写希尔排序,从小到大,移动法 + * @param array 数组 + */ + public static void myShellSort1(int[] array) { + // 步长,第一次肯定为array.length / 2 + int step = array.length / 2; + while (step > 0) { + // 按照step步长划分为若干个区间,依次对这些区间进行插入排序 + for (int i = step; i < array.length; i++) { + // 暂时通value来保存要插入的值 + int value = array[i]; + int j; + // 遍历当前区间的左边的元素,比value要小的,都向右边移动一个step,j要>=0,因为数组下标是从0开始的,要考虑下标为0的位置 + for (j = i - step; j >= 0 && array[j] > value; j -= step) { + array[j + step] = array[j]; + } + // 经过遍历之后,已经找出了可以插入的位置,在这个位置插入value + array[j + step] = value; + } + // 步长除以2 + step /= 2; + } + } + + /** + * 理解之后自己写希尔排序,从大到小,移动法 + * @param array 数组 + */ + public static void myShellSort2(int[] array) { + // 步长,第一次肯定为array.length / 2 + int step = array.length / 2; + while (step > 0) { + // 按照step步长划分为若干个区间,依次对这些区间进行插入排序 + for (int i = step; i < array.length; i++) { + // 暂时通value来保存要插入的值 + int value = array[i]; + int j; + // 遍历当前区间的左边的元素,比value要小的,都向右边移动一个step,j要>=0,因为数组下标是从0开始的,要考虑下标为0的位置 + for (j = i - step; j >= 0 && array[j] < value; j -= step) { + array[j + step] = array[j]; + } + // 经过遍历之后,已经找出了可以插入的位置,在这个位置插入value + array[j + step] = value; + } + // 步长除以2 + step /= 2; + } + } + + /** + * 理解之后自己写希尔排序,从小到大,交换法 + * @param array 数组 + */ + public static void myShellSort3(int[] array) { + // 步长,第一次肯定为array.length / 2 + int step = array.length / 2; + while (step > 0) { + // 按照step步长划分为若干个区间,依次对这些区间进行插入排序 + for (int i = step; i < array.length; i++) { + // 暂时通value来保存要插入的值 + int value = array[i]; + int j; + // 遍历当前区间的左边的元素,比value要小的,和value进行交换,这样value的值就不断向前移动 + for (j = i - step; j >= 0 && array[j] > value; j -= step) { + array[j + step] = array[j]; + array[j] = value; + } + } + // 步长除以2 + step /= 2; + } + } + + /** + * 理解之后自己写希尔排序,从小到大,步长基数从2变成3,移动法 + * @param array 数组 + */ + public static void myShellSort4(int[] array) { + // 步长,第一次肯定为array.length / 3 + int step = array.length / 3; + while (step > 0) { + // 按照step步长划分为若干个区间,依次对这些区间进行插入排序 + for (int i = step; i < array.length; i++) { + // 暂时通value来保存要插入的值 + int value = array[i]; + int j; + // 遍历当前区间的左边的元素,比value要小的,都向右边移动一个step,j要>=0,因为数组下标是从0开始的,要考虑下标为0的位置 + for (j = i - step; j >= 0 && array[j] > value; j -= step) { + array[j + step] = array[j]; + } + // 经过遍历之后,已经找出了可以插入的位置,在这个位置插入value + array[j + step] = value; + } + // 步长除以3 + step /= 3; + } + } +} +``` + +输出结果: + +``` +array1排序之前的数据:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0, 10, 12, 11] +array1排序之后的数据:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + +测试8000000条数据使用希尔排序法进行排序: + +移动法,书上的方法,从小到大希尔排序法花费时间为:【1874】毫秒 + +移动法,我写的方法,从小到大希尔排序法花费时间为:【1826】毫秒 + +移动法,我写的方法,从大到小希尔排序法花费时间为:【1827】毫秒 + +交换法,我写的方法,从小到大希尔排序法花费时间为:【1626】毫秒 + +交换法,我写的方法,步长基数从2变成3,从小到大希尔排序法花费时间为:【1698】毫秒 +``` + +- 希尔排序比冒泡排序、选择排序、插入排序效率高多了!即使800w条数据也只需要不到1秒 +- 交换法和移动法效率基本上差不多,交换法相当于移动一次插一次,移动法是移动到合适位置然后插一次就行了 +- 基数从2提升到3,花费时间会少一点点(多次测试普遍情况),但是提升不明显 ,但是如果基数变成了4排序就会出错了 + +## 快速排序 + +### 基本介绍 + +快速排序(Quicksort)是对冒泡排序的一种改进,借用了分治的思想,由C. A. R. Hoare在1962年提出。 + +> 基本思想 + +通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列 + +> 快速排序的例子(图解) + +![image-20211215105036842](数据结构与算法/image-20211215105036842.png) + + + +![快速排序](数据结构与算法/快速排序.gif) + +### 分布解析说明 + +- 每次以最后一个元素`array[high]`为中心元素`pivot` +- `pointer`用于指向比中心元素大的元素的下标,从low开始 +- `i`用于遍历要和中心元素进行比较的元素,所以`i`的取值范围为[low, high)左开右闭 +- `for (int i = low; i < high; i++)`依次遍历,如果遇到`array[i] <= pivot`,说明当前遍历的这个元素比`pivot`小,array[i] 和 array[pointer]进行交换,然后因为`pointer`用于指向比中心元素大的元素,所以pointer++(如果i = pointer时,array[i] <= pivot,就是自己和自己交换,i和 pointer都进行++) +- 最后将中心元素和pointer指向的元素进行交换,此时就满足pointer右边都小于中心元素,左边都大于中心元素,然后返回pointer +- 根据返回的position = pointer,对原本数组划分为左右两个数组,再对左右数组递归执行以上步骤 quickSort(array, low, position -1);quickSort(array, position + 1, high); +- 最终递归结束就排好序了 + +```java + /** + * + * @param array + * @param low + * @param high + * @return + */ + public static int partition(int[] array, int low, int high) { + // 取最后一个元素作为中心元素 + int pivot = array[high]; + // 定义指向比中心元素大的指针,首先指向第一个元素 + int pointer = low; + // 定义中间变量 + int temp; + // 遍历数组中的所有元素,将比中心元素大的放在右边,比中心元素小的放在左边 + System.out.println("\n当前遍历区间为[" + low + ", " + high + ")" + ", 中心元素pivot为:array【" + high+ "】 = " + pivot); + for (int i = low; i < high; i++) { + System.out.println("当前指针pointer为:" + pointer); + if (array[i] <= pivot) { + // 将比中心元素小的元素和指针指向的元素交换位置 + // 如果第1个元素比中心元素小,这里就是自己和自己交换位置,指针和索引都向下一位移动 + // 如果元素比中心元素大,索引i继续移动,指针pointer指向这个较大的元素,直到找到比中心元素小的元素,并交换位置,指针向下移动 + temp = array[i]; + array[i] = array[pointer]; + array[pointer] = temp; + pointer++; + } + System.out.println(Arrays.toString(array)); + } + // 将中心元素和指针指向的元素交换位置 + temp = array[pointer]; + array[pointer] = array[high]; + array[high] = temp; + return pointer; + } + + /** + * + * @param array + * @param low + * @param high + */ + public static void quickSort(int[] array, int low, int high) { + if (low < high) { + // 获取划分子数组的位置 + int position = partition(array, low, high); + // 左子数组递归调用 + quickSort(array, low, position -1); + // 右子数组递归调用 + quickSort(array, position + 1, high); + } + } +``` + +测试数组为`[2, 10, 8, 22, 34, 5, 12, 28, 21, 11]`,共10条数据进行分析: + +![image-20211215105036842](数据结构与算法/image-20211215105036842.png) + +所以代码执行流程如下: + +``` +array1排序之前的数据:[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] + +当前遍历区间为[0, 9), 中心元素pivot为:array【9】 = 11 +当前指针pointer为:0 +[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +当前指针pointer为:1 +[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +当前指针pointer为:2 +[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +当前指针pointer为:3 +[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +当前指针pointer为:3 +[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +当前指针pointer为:3 +[2, 10, 8, 5, 34, 22, 12, 28, 21, 11] +当前指针pointer为:4 +[2, 10, 8, 5, 34, 22, 12, 28, 21, 11] +当前指针pointer为:4 +[2, 10, 8, 5, 34, 22, 12, 28, 21, 11] +当前指针pointer为:4 +[2, 10, 8, 5, 34, 22, 12, 28, 21, 11] + +当前遍历区间为[0, 3), 中心元素pivot为:array【3】 = 5 +当前指针pointer为:0 +[2, 10, 8, 5, 11, 22, 12, 28, 21, 34] +当前指针pointer为:1 +[2, 10, 8, 5, 11, 22, 12, 28, 21, 34] +当前指针pointer为:1 +[2, 10, 8, 5, 11, 22, 12, 28, 21, 34] + +当前遍历区间为[2, 3), 中心元素pivot为:array【3】 = 10 +当前指针pointer为:2 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] + +当前遍历区间为[5, 9), 中心元素pivot为:array【9】 = 34 +当前指针pointer为:5 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] +当前指针pointer为:6 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] +当前指针pointer为:7 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] +当前指针pointer为:8 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] + +当前遍历区间为[5, 8), 中心元素pivot为:array【8】 = 21 +当前指针pointer为:5 +[2, 5, 8, 10, 11, 22, 12, 28, 21, 34] +当前指针pointer为:5 +[2, 5, 8, 10, 11, 12, 22, 28, 21, 34] +当前指针pointer为:6 +[2, 5, 8, 10, 11, 12, 22, 28, 21, 34] + +当前遍历区间为[7, 8), 中心元素pivot为:array【8】 = 22 +当前指针pointer为:7 +[2, 5, 8, 10, 11, 12, 21, 28, 22, 34] +array1排序之后的数据:[2, 5, 8, 10, 11, 12, 21, 22, 28, 34] +``` + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class QuickSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{2, 10, 8, 22, 34, 5, 12, 28, 21, 11}; + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + quickSort3(array1, 0, array1.length - 1); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4、array5、array6 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + int[] array6 = array3.clone(); + + // 测试 + System.out.println("\n测试8000000条数据使用快速排序法进行排序:\n"); + + // 加上 i == pointer判断,从小到大 + long startTime1 = System.currentTimeMillis(); + quickSort1(array3, 0, array3.length - 1); + long endTime1 = System.currentTimeMillis(); + System.out.println("加上 i == pointer判断,从小到大快速排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 不加上 i == pointer判断,从小到大 + long startTime2 = System.currentTimeMillis(); + quickSort2(array4, 0, array4.length - 1); + long endTime2 = System.currentTimeMillis(); + System.out.println("不加上 i == pointer判断,从小到大快速排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 不加上 i == pointer判断,从大到小 + long startTime3 = System.currentTimeMillis(); + quickSort3(array5, 0, array5.length - 1); + long endTime3 = System.currentTimeMillis(); + System.out.println("不加上 i == pointer判断,从大到小快速排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒\n"); + + // 使用Arrays.sort()进行排序 + long startTime4 = System.currentTimeMillis(); + Arrays.sort(array6, 0, array6.length - 1); + long endTime4 = System.currentTimeMillis(); + System.out.println("使用Arrays.sort()进行排序花费时间为:【" + (endTime4 - startTime4) + "】毫秒\n"); + } + + /** + * 快速排序法,以最后一个数为基准,加上 i == pointer判断,从小到大 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + * @return + */ + public static int partition1(int[] array, int low, int high) { + // 取最后一个元素作为中心元素 + int pivot = array[high]; + // 定义指向比中心元素大的指针,首先指向第一个元素 + int pointer = low; + // 定义中间变量 + int temp; + // 遍历数组中的所有元素,将比中心元素大的放在右边,比中心元素小的放在左边 + for (int i = low; i < high; i++) { + if (array[i] <= pivot) { + // 将比中心元素小的元素和指针指向的元素交换位置 + // 如果第1个元素比中心元素小,这里就是自己和自己交换位置,指针和索引都向下一位移动 + // 如果元素比中心元素大,索引i继续移动,指针pointer指向这个较大的元素,直到找到比中心元素小的元素,并交换位置,指针向下移动 + if (i == pointer) { + pointer++; + continue; + } else { + temp = array[i]; + array[i] = array[pointer]; + array[pointer] = temp; + pointer++; + } + } + } + // 将中心元素和指针指向的元素交换位置 + temp = array[pointer]; + array[pointer] = array[high]; + array[high] = temp; + return pointer; + } + + /** + * 递归调用快速排序法 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + */ + public static void quickSort1(int[] array, int low, int high) { + if (low < high) { + // 获取划分子数组的位置 + int position = partition1(array, low, high); + // 左子数组递归调用 + quickSort1(array, low, position -1); + // 右子数组递归调用 + quickSort1(array, position + 1, high); + } + } + + /** + * 快速排序法,以最后一个数为基准,不加上 i == pointer判断,从小到大 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + * @return + */ + public static int partition2(int[] array, int low, int high) { + // 取最后一个元素作为中心元素 + int pivot = array[high]; + // 定义指向比中心元素大的指针,首先指向第一个元素 + int pointer = low; + // 定义中间变量 + int temp; + // 遍历数组中的所有元素,将比中心元素大的放在右边,比中心元素小的放在左边 + for (int i = low; i < high; i++) { + if (array[i] <= pivot) { + // 将比中心元素小的元素和指针指向的元素交换位置 + // 如果第1个元素比中心元素小,这里就是自己和自己交换位置,指针和索引都向下一位移动 + // 如果元素比中心元素大,索引i继续移动,指针pointer指向这个较大的元素,直到找到比中心元素小的元素,并交换位置,指针向下移动 + temp = array[i]; + array[i] = array[pointer]; + array[pointer] = temp; + pointer++; + } + } + // 将中心元素和指针指向的元素交换位置 + temp = array[pointer]; + array[pointer] = array[high]; + array[high] = temp; + return pointer; + } + + /** + * 递归调用快速排序法 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + */ + public static void quickSort2(int[] array, int low, int high) { + if (low < high) { + // 获取划分子数组的位置 + int position = partition2(array, low, high); + // 左子数组递归调用 + quickSort2(array, low, position -1); + // 右子数组递归调用 + quickSort2(array, position + 1, high); + } + } + + /** + * 快速排序法,以最后一个数为基准,不加上 i == pointer判断,从大到小 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + * @return + */ + public static int partition3(int[] array, int low, int high) { + // 取最后一个元素作为中心元素 + int pivot = array[high]; + // 定义指向比中心元素小的指针,首先指向第一个元素 + int pointer = low; + // 定义中间变量 + int temp; + // 遍历数组中的所有元素,将比中心元素大的放在右边,比中心元素小的放在左边 + for (int i = low; i < high; i++) { + if (array[i] >= pivot) { + // 将比中心元素小的元素和指针指向的元素交换位置 + // 如果第1个元素比中心元素小,这里就是自己和自己交换位置,指针和索引都向下一位移动 + // 如果元素比中心元素大,索引i继续移动,指针pointer指向这个较大的元素,直到找到比中心元素小的元素,并交换位置,指针向下移动 + temp = array[i]; + array[i] = array[pointer]; + array[pointer] = temp; + pointer++; + } + } + // 将中心元素和指针指向的元素交换位置 + temp = array[pointer]; + array[pointer] = array[high]; + array[high] = temp; + return pointer; + } + + /** + * 递归调用快速排序法 + * @param array 数组 + * @param low 最低元素的下标 + * @param high 最高元素的下标 + */ + public static void quickSort3(int[] array, int low, int high) { + if (low < high) { + // 获取划分子数组的位置 + int position = partition3(array, low, high); + // 左子数组递归调用 + quickSort3(array, low, position -1); + // 右子数组递归调用 + quickSort3(array, position + 1, high); + } + } +} +``` + +输出结果: + +```java +array1排序之前的数据:[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +array1排序之后的数据:[34, 28, 22, 21, 12, 11, 10, 8, 5, 2] + +测试8000000条数据使用快速排序法进行排序: + +加上 i == pointer判断,从小到大快速排序法花费时间为:【746】毫秒 + +不加上 i == pointer判断,从小到大快速排序法花费时间为:【640】毫秒 + +不加上 i == pointer判断,从大到小快速排序法花费时间为:【617】毫秒 + +使用Arrays.sort()进行排序花费时间为:【581】毫秒 +``` + +- 加上了 i == pointer 判断是否需要交换花费时间比不加上判断时间多以一点点(多次测试普遍情况),但是差距不大 +- 使用Arrays.sort()库进行排序比自己写的快速排序快一点点(多次测试普遍情况),但是差距不大,底层是DualPivotQuicksort双轴快速排序法 + +## 归并排序 + +### 基本介绍 + +归并排序(MERGE-SORT)是利用**归并**的思想实现的排序方法,该算法采用经典的**分治(divide-and-conquer)**策略(分治法将问题**分(divide)**成一些小的问题然后递归求解,而**治(conquer)**的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。 + +> 基本思想 + +- 基本思想 + +![image-20211215201916076](数据结构与算法/image-20211215201916076.png) + +可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。**分**阶段可以理解为就是递归拆分子序列的过程。 + +- 合并相邻有序子序列 + +再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤 + +![image-20211215202215151](数据结构与算法/image-20211215202215151.png) + +![image-20211215202221175](数据结构与算法/image-20211215202221175.png) + +> 归并排序例子(图解) + +![归并排序](数据结构与算法/归并排序.jpg) + + + +![归并排序](数据结构与算法/归并排序.gif) + +### 分布解析说明 + +- 归并排序的思想主要是利用栈的特性,递归进行分解操作,肯定是从大数组向小数组进行分解,递归进行合并操作,肯定是从小数组向大数组进行合并 +- 以数组元素为8个进行举例分析:等分解成4个子数组的排序好之后,然后对左边2个子数组排序合并,右边的2个子数组排序合并,这样就从4个排好序的子数组变成了2个排好序的子数组,然后对这2个子数组进行排序合并,最终就得到了排好序的8个元素的数组 +- 8条数据要归并7次,80000条数据要归并79999次 + +```java + /** + * 归并排序(递归) + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort(int[] arr) { + return mergeSort(arr, 0, arr.length - 1); + } + + /** + * 分解数组,并将分解的数组调用合并 + * @param array + * @param low + * @param high + * @return + */ + private static int[] mergeSort(int[] array, int low, int high) { + int center = (high + low) / 2; + // 分析栈的特性,分解数组的操作肯定是先分解整个数组,然后分解成小的数组.....最终整个数组都被分解成最小 + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort(array, low, center); + mergeSort(array, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort(array, low, center, high); + } + return array; + } + + /** + * 合并数组,并进行排序 + * @param array + * @param low + * @param mid + * @param high + */ + private static void mergeSort(int[] array, int low, int mid, int high) { + // 创建一个临时数组,大小为high - low + 1 + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较小的数先移到新数组中 + while (i <= mid && j <= high) { + if (array[i] < array[j]) { + // 先赋值 temp[k] = array[i],再自增 k++,i++ + temp[k++] = array[i++]; + } else { + // 先赋值 temp[k] = array[j],再自增 k++,j++ + temp[k++] = array[j++]; + } + } + + // 因为跳出来上面的while循环 i <= mid && j <= high + // 所以 i <= mid 和 j <= high必有一个为false + // 所以接下来的两个while只会执行一个,把较大的数加入到temp数组的后面 + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = array[j++]; + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = array[i++]; + } + + // 把新数组中的数覆盖array数组 + // 分析栈的特性,合并数组的操作肯定是先合并小的数组,然后逐渐合并大的数组.....最终合并整个数组 + for (int x = 0; x < temp.length; x++) { + array[x + low] = temp[x]; + } + } +``` + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class MergeSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{2, 10, 8, 22, 34, 5, 12, 28, 21, 11}; + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + mergeSort1(array1); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4 + int[] array4 = array3.clone(); + + // 测试 + System.out.println("\n测试8000000条数据使用归并排序法进行排序:\n"); + + // 归并排序,从小到大 + long startTime1 = System.currentTimeMillis(); + mergeSort(array3, 0, array3.length - 1); + long endTime1 = System.currentTimeMillis(); + System.out.println("从小到大归并排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 归并排序,从大到小 + long startTime2 = System.currentTimeMillis(); + mergeSort1(array4, 0, array4.length - 1); + long endTime2 = System.currentTimeMillis(); + System.out.println("从大到小归并排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + } + + /** + * 归并排序(递归) + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort(int[] arr) { + return mergeSort(arr, 0, arr.length - 1); + } + + /** + * 分解数组,并将分解的数组调用合并 + * @param array + * @param low + * @param high + * @return + */ + private static int[] mergeSort(int[] array, int low, int high) { + int center = (high + low) / 2; + // 分析栈的特性,分解数组的操作肯定是先分解整个数组,然后分解成小的数组.....最终整个数组都被分解成最小 + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort(array, low, center); + mergeSort(array, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort(array, low, center, high); + } + return array; + } + + /** + * 合并数组,并进行排序 + * @param array + * @param low + * @param mid + * @param high + */ + private static void mergeSort(int[] array, int low, int mid, int high) { + // 创建一个临时数组,大小为high - low + 1 + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较小的数先移到新数组中 + while (i <= mid && j <= high) { + if (array[i] < array[j]) { + // 先赋值 temp[k] = array[i],再自增 k++,i++ + temp[k++] = array[i++]; + } else { + // 先赋值 temp[k] = array[j],再自增 k++,j++ + temp[k++] = array[j++]; + } + } + + // 因为跳出来上面的while循环 i <= mid && j <= high + // 所以 i <= mid 和 j <= high必有一个为false + // 所以接下来的两个while只会执行一个,把较大的数加入到temp数组的后面 + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = array[j++]; + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = array[i++]; + } + + // 把新数组中的数覆盖array数组 + // 分析栈的特性,合并数组的操作肯定是先合并小的数组,然后逐渐合并大的数组.....最终合并整个数组 + for (int x = 0; x < temp.length; x++) { + array[x + low] = temp[x]; + } + } + + /** + * 归并排序(递归),从大到小 + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort1(int[] arr) { + return mergeSort1(arr, 0, arr.length - 1); + } + + /** + * 分解数组,并将分解的数组调用合并,从大到小 + * @param array + * @param low + * @param high + * @return + */ + private static int[] mergeSort1(int[] array, int low, int high) { + int center = (high + low) / 2; + // 分析栈的特性,分解数组的操作肯定是先分解整个数组,然后分解成小的数组.....最终整个数组都被分解成最小 + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort1(array, low, center); + mergeSort1(array, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort1(array, low, center, high); + } + return array; + } + + /** + * 合并数组,并进行排序,从大到小 + * @param array + * @param low + * @param mid + * @param high + */ + private static void mergeSort1(int[] array, int low, int mid, int high) { + // 创建一个临时数组,大小为high - low + 1 + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较大的数先移到新数组中 + while (i <= mid && j <= high) { + if (array[i] > array[j]) { + // 先赋值 temp[k] = array[i],再自增 k++,i++ + temp[k++] = array[i++]; + } else { + // 先赋值 temp[k] = array[j],再自增 k++,j++ + temp[k++] = array[j++]; + } + } + + // 因为跳出来上面的while循环 i <= mid && j <= high + // 所以 i <= mid 和 j <= high必有一个为false + // 所以接下来的两个while只会执行一个,把较大的数加入到temp数组的后面 + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = array[j++]; + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = array[i++]; + } + + // 把新数组中的数覆盖array数组 + // 分析栈的特性,合并数组的操作肯定是先合并小的数组,然后逐渐合并大的数组.....最终合并整个数组 + for (int x = 0; x < temp.length; x++) { + array[x + low] = temp[x]; + } + } +} +``` + +输出结果: + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class MergeSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{2, 10, 8, 22, 34, 5, 12, 28, 21, 11}; + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + mergeSort1(array1); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4 + int[] array4 = array3.clone(); + + // 测试 + System.out.println("\n测试8000000条数据使用归并排序法进行排序:\n"); + + // 归并排序,从小到大 + long startTime1 = System.currentTimeMillis(); + mergeSort(array3, 0, array3.length - 1); + long endTime1 = System.currentTimeMillis(); + System.out.println("从小到大归并排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 归并排序,从大到小 + long startTime2 = System.currentTimeMillis(); + mergeSort1(array4, 0, array4.length - 1); + long endTime2 = System.currentTimeMillis(); + System.out.println("从大到小归并排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + } + + /** + * 归并排序(递归) + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort(int[] arr) { + return mergeSort(arr, 0, arr.length - 1); + } + + /** + * 分解数组,并将分解的数组调用合并 + * @param array + * @param low + * @param high + * @return + */ + private static int[] mergeSort(int[] array, int low, int high) { + int center = (high + low) / 2; + // 分析栈的特性,分解数组的操作肯定是先分解整个数组,然后分解成小的数组.....最终整个数组都被分解成最小 + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort(array, low, center); + mergeSort(array, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort(array, low, center, high); + } + return array; + } + + /** + * 合并数组,并进行排序 + * @param array + * @param low + * @param mid + * @param high + */ + private static void mergeSort(int[] array, int low, int mid, int high) { + // 创建一个临时数组,大小为high - low + 1 + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较小的数先移到新数组中 + while (i <= mid && j <= high) { + if (array[i] < array[j]) { + // 先赋值 temp[k] = array[i],再自增 k++,i++ + temp[k++] = array[i++]; + } else { + // 先赋值 temp[k] = array[j],再自增 k++,j++ + temp[k++] = array[j++]; + } + } + + // 因为跳出来上面的while循环 i <= mid && j <= high + // 所以 i <= mid 和 j <= high必有一个为false + // 所以接下来的两个while只会执行一个,把较大的数加入到temp数组的后面 + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = array[j++]; + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = array[i++]; + } + + // 把新数组中的数覆盖array数组 + // 分析栈的特性,合并数组的操作肯定是先合并小的数组,然后逐渐合并大的数组.....最终合并整个数组 + for (int x = 0; x < temp.length; x++) { + array[x + low] = temp[x]; + } + } + + /** + * 归并排序(递归),从大到小 + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort1(int[] arr) { + return mergeSort1(arr, 0, arr.length - 1); + } + + /** + * 分解数组,并将分解的数组调用合并,从大到小 + * @param array + * @param low + * @param high + * @return + */ + private static int[] mergeSort1(int[] array, int low, int high) { + int center = (high + low) / 2; + // 分析栈的特性,分解数组的操作肯定是先分解整个数组,然后分解成小的数组.....最终整个数组都被分解成最小 + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort1(array, low, center); + mergeSort1(array, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort1(array, low, center, high); + } + return array; + } + + /** + * 合并数组,并进行排序,从大到小 + * @param array + * @param low + * @param mid + * @param high + */ + private static void mergeSort1(int[] array, int low, int mid, int high) { + // 创建一个临时数组,大小为high - low + 1 + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较大的数先移到新数组中 + while (i <= mid && j <= high) { + if (array[i] > array[j]) { + // 先赋值 temp[k] = array[i],再自增 k++,i++ + temp[k++] = array[i++]; + } else { + // 先赋值 temp[k] = array[j],再自增 k++,j++ + temp[k++] = array[j++]; + } + } + + // 因为跳出来上面的while循环 i <= mid && j <= high + // 所以 i <= mid 和 j <= high必有一个为false + // 所以接下来的两个while只会执行一个,把较大的数加入到temp数组的后面 + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = array[j++]; + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = array[i++]; + } + + // 把新数组中的数覆盖array数组 + // 分析栈的特性,合并数组的操作肯定是先合并小的数组,然后逐渐合并大的数组.....最终合并整个数组 + for (int x = 0; x < temp.length; x++) { + array[x + low] = temp[x]; + } + } +} +``` + +输出结果: + +``` +array1排序之前的数据:[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +array1排序之后的数据:[34, 28, 22, 21, 12, 11, 10, 8, 5, 2] + +测试8000000条数据使用归并排序法进行排序: + +从小到大归并排序法花费时间为:【1274】毫秒 + +从大到小归并排序法花费时间为:【1173】毫秒 +``` + +- 归并排序和快速排序的时间复杂度都是O(nlogn),不过归并排序的递归调用对栈的消耗更大 + +## 基数排序 + +### 基本介绍 + +- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用 +- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法 +- 基数排序(Radix Sort)是桶排序的扩展 +- 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较 + +> 基数排序基本思想 + +- 基数排序是按照低位先排序,然后收集 +- 再按照高位排序,然后再收集 +- 依次类推,直到最高位 +- 有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序 +- 最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 + +> 基数排序例子(图解) + +![基数排序](数据结构与算法/基数排序.gif) + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class RadixSortDemo { + public static void main(String[] args) { + int[] array1 = new int[]{2, 10, 8, 22, 34, 5, 12, 28, 21, 11}; + System.out.println("array1排序之前的数据:" + Arrays.toString(array1)); + radixSort2(array1); + System.out.println("array1排序之后的数据:" + Arrays.toString(array1)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4、array5、array6 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + int[] array6 = array3.clone(); + // 使得array6包含负数 + for (int i = 0; i < array6.length; i++) { + array6[i] -= 30000; + } + + // 测试 + System.out.println("\n测试8000000条数据使用基数排序法进行排序:\n"); + + // 基数排序(LSD 从低位开始),教材上的实现 + long startTime1 = System.currentTimeMillis(); + radixSort(array3); + long endTime1 = System.currentTimeMillis(); + System.out.println("教材上的实现,从小到大基数排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 我的的实现,从小到大 + long startTime2 = System.currentTimeMillis(); + radixSort1(array4); + long endTime2 = System.currentTimeMillis(); + System.out.println("我的实现,从小到大基数排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 我的的实现,从大到小 + long startTime3 = System.currentTimeMillis(); + radixSort2(array5); + long endTime3 = System.currentTimeMillis(); + System.out.println("我的实现,从小到大基数排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒\n"); + + // 我的的实现,包含负数从小到大 + long startTime4 = System.currentTimeMillis(); + radixSort3_1(array6); + long endTime4 = System.currentTimeMillis(); + System.out.println("我的实现,包含负数从小到大基数排序法花费时间为:【" + (endTime4 - startTime4) + "】毫秒\n"); + } + + /** + * 基数排序(LSD 从低位开始),教材上的实现 + *

+ * 基数排序适用于: + * (1)数据范围较小,建议在小于1000 + * (2)每个数值都要大于等于0 + *

+ * ①. 取得数组中的最大数,并取得位数; + * ②. arr为原始数组,从最低位开始取每个位组成radix数组; + * ③. 对radix进行计数排序(利用计数排序适用于小范围数的特点); + * + * @param array 待排序数组 + */ + public static int[] radixSort(int[] array) { + // 取得数组中的最大数,并取得位数 + int max = 0; + for (int item : array) { + if (max < item) { + max = item; + } + } + // 最大位数 + int maxDigit = 1; + while (max / 10 > 0) { + maxDigit++; + max = max / 10; + } + + // 申请一个桶空间,总共有10个,对应0~9个位数,每个桶的长度为array.length + int[][] buckets = new int[10][array.length]; + // 对base取余,首先从10开始获取个位数 + int base = 10; + + // 从低位到高位,对每一位遍历,将所有元素分配到桶中 + for (int i = 0; i < maxDigit; i++) { + // 存储各个桶中存储元素的数量,记录0~9数字桶存储的元素个数,作为每次最外层循环的临时数组 + int[] bktLen = new int[10]; + + // 分配:将所有元素分配到桶中 + for (int value : array) { + // 先对base取余获取低位,再除以(base / 10)获取低位的单个数字 + int whichBucket = (value % base) / (base / 10); + // 在二维数组中加入这个数,加入的位置就是低位数对应的桶序号,在每个桶的位置就是bktLen[whichBucket] + buckets[whichBucket][bktLen[whichBucket]] = value; + // 每个桶加入了一个数,对应桶的元素个数就要增加 bktLen[whichBucket]++ + bktLen[whichBucket]++; + } + + // 收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞 + int k = 0; + for (int b = 0; b < buckets.length; b++) { + for (int p = 0; p < bktLen[b]; p++) { + array[k++] = buckets[b][p]; + } + } + // 该低位数处理完毕,需要处理高一位,所以base *= 10 + base *= 10; + } + return array; + } + + /** + * 基数排序,自己理解之后写的,从小到大 + * + * @param array 待排序数组 + * @return + */ + public static int[] radixSort1(int[] array) { + // 获取元素中的最大数 + int max = 0; + for (int item : array) { + if (item > max) { + max = item; + } + } + + // 获取最大位数,通过强转成字符串来计算 + int maxDigit = (max + "").length(); + + // 创建桶 + int[][] bucket = new int[10][array.length]; + // 对base取余,首先从10开始获取个位数 + int base = 10; + + for (int i = 0; i < maxDigit; i++) { + // 存储各个桶中的元素个数,局部变量 + int[] bktLen = new int[10]; + + // 将所有元素分配到桶中 + for (int value : array) { + // 获取低位的数字是0~9中的哪一个 + int whichBucket = (value % base) / (base / 10); + bucket[whichBucket][bktLen[whichBucket]] = value; + bktLen[whichBucket]++; + } + + // 将桶中数据依次放入到原本的数组中,从低位桶开始 + int k = 0; + for (int m = 0; m < bucket.length; m++) { + for (int n = 0; n < bktLen[m]; n++) { + array[k++] = bucket[m][n]; + } + } + // 该低位数处理完毕,需要处理高一位,所以base *= 10 + base *= 10; + } + return array; + } + + /** + * 基数排序,自己理解之后写的,从大到小 + * + * @param array 待排序数组 + * @return + */ + public static int[] radixSort2(int[] array) { + // 首先获取最大数 + int max = 0; + for (int item : array) { + if (item > max) { + max = item; + } + } + + // 获取最大位数 + int maxDigit = (max + "").length(); + + // 创建桶 + int[][] bucket = new int[10][array.length]; + // 需要对低位数字进行提取,接触base变量,第一次提取个位数,base从10开始 + int base = 10; + + for (int i = 0; i < maxDigit; i++) { + // 创建局部变量,用于记录每个桶的元素元素 + int[] bktLen = new int[10]; + + // 遍历数据,将每个数装入到对应的桶 + for (int value : array) { + // 提取低位数的数字,决定要放到哪个桶 + int witchBucket = (value % base) / (base / 10); + bucket[witchBucket][bktLen[witchBucket]] = value; + bktLen[witchBucket]++; + } + + // 收集:从大到小,从高位桶将不同桶里面的数挨个数取出来 + int k = 0; + for (int m = bucket.length - 1; m >= 0; m--) { + for (int n = 0; n < bktLen[m]; n++) { + array[k++] = bucket[m][n]; + } + } + // 该低位数处理完毕,需要处理高一位,所以base *= 10 + base *= 10; + } + return array; + } + + /** + * 基数排序3_1,自己理解之后写的,从大到小,实现负数排序 + *

+ * 如果最小值小于0,所有数都减去最小数使得最小数为0,然后进行排序,最后所有数再加上最小数重新变回原样 + * @param array 待排序数组 + * @return + */ + public static void radixSort3_1(int[] array) { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + for (int item : array) { + if (item < min) { + min = item; + } + if (item > max) { + max = item; + } + } + + // int的size为4字节32位,所以最大值为2147483647(2^32-1) + if ((long) max - min > Integer.MAX_VALUE) { + // 超出范围,抛出异常 + throw new RuntimeException("待排序的的数组的最大值超过了int的范围"); + } + + // 如果最小值小于0,就对所有数都减去最小值,使得最小值为0 + if (min < 0) { + for (int i = 0; i < array.length; i++) { + array[i] -= min; + } + // max也要减去min + max -= min; + } + + // 调用radixSort3_2进行排序 + radixSort3_2(array, max); + + // 如果最小值小于0,就对所有数都加上最小值,所有数都变成原来的 + if (min < 0) { + for (int i = 0; i < array.length; i++) { + array[i] += min; + } + } + } + + /** + * 基数排序3_2 + * @param array 待排序数组 + * @param max 数组最大值 + */ + public static void radixSort3_2(int[] array, int max) { + // 通过强转成string获取最大有几位数 + int maxDigit = (max + "").length(); + + // 申请一个桶空间,总共有10个,对应0~9个位数,每个桶的长度为array.length + int[][] buckets = new int[10][array.length]; + // 对base取余,首先从10开始获取个位数 + int base = 10; + + // 从低位到高位,对每一位遍历,将所有元素分配到桶中 + for (int i = 0; i < maxDigit; i++) { + // 存储各个桶中存储元素的数量,记录0~9数字桶存储的元素个数,作为每次最外层循环的临时数组 + int[] bktLen = new int[10]; + + // 分配:将所有元素分配到桶中 + for (int value : array) { + // 先对base取余获取低位,再除以(base / 10)获取低位的单个数字 + int whichBucket = (value % base) / (base / 10); + // 在二维数组中加入这个数,加入的位置就是低位数对应的桶序号,在每个桶的位置就是bktLen[whichBucket] + buckets[whichBucket][bktLen[whichBucket]] = value; + // 每个桶加入了一个数,对应桶的元素个数就要增加 bktLen[whichBucket]++ + bktLen[whichBucket]++; + } + + // 收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞 + int k = 0; + for (int b = 0; b < buckets.length; b++) { + for (int p = 0; p < bktLen[b]; p++) { + array[k++] = buckets[b][p]; + } + } + // 该低位数处理完毕,需要处理高一位,所以base *= 10 + base *= 10; + } + } +} +``` + +输出结果: + +``` +array1排序之前的数据:[2, 10, 8, 22, 34, 5, 12, 28, 21, 11] +array1排序之后的数据:[34, 28, 22, 21, 12, 11, 10, 8, 5, 2] + +测试8000000条数据使用基数排序法进行排序: + +教材上的实现,从小到大基数排序法花费时间为:【339】毫秒 + +我的实现,从小到大基数排序法花费时间为:【305】毫秒 + +我的实现,从小到大基数排序法花费时间为:【276】毫秒 + +我的实现,包含负数从小到大基数排序法花费时间为:【289】毫秒 +``` + +- 800w条数据只需要300毫秒左右,本地电脑上测试提升到8000w条就OOM了,需要修改虚拟机的参数 +- 通过将max转换为字符串获取length来得到最大数的位数,这样确实效率更高一点(多次测试普遍结论) + +## 堆排序 + +### 基本介绍 + +- 堆排序是利用**堆**这种数据结构而设计的一种排序算法,堆排序是一种**选择排序**,它的最坏/最好平均时间复杂度均为O(nlogn),它也是不稳定排序 + +- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,**注意** : 没有要求结点的左孩子的值和右孩子的值的大小关系 + +- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆 + +- 大顶堆举例说明 + + ![image-20211221112727452](数据结构与算法/image-20211221112727452.png) + + 我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子: + + ![image-20211221112748270](数据结构与算法/image-20211221112748270.png) + + 大顶堆特点:`arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]` // i 对应第几个节点,i从0开始编号 + +- 小顶堆举例说明 + + ![image-20211221112918889](数据结构与算法/image-20211221112918889.png) + +​ 小顶堆特点:`arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]` // i 对应第几个节点,i从0开始编号 + +- 一般升序采用大顶堆,降序采用小顶堆 + +> 实例 + +![堆排序动画演示](数据结构与算法/1469176-20190329000555410-1254067522.gif) + + + +![堆排序](数据结构与算法/堆排序.gif) + +### 分布解析说明 + +实现堆排序需要解决两个问题: + +- 如何由一个无序序列建成一个堆? +- 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆? + +假设给定一个组无序数列{100,5,3,11,6,8,7},带着问题,我们对其进行堆排序操作进行分步操作说明。 + +![img](数据结构与算法/v2-acbdde7cf6f0426e693187c4899716e7_720w.png) + +#### 创建最大堆 + +①首先我们将数组我们将数组从上至下按顺序排列,转换成二叉树:一个无序堆。每一个三角关系都是一个堆,上面是父节点,下面两个分叉是子节点,两个子节点俗称左孩子、右孩子; + +![img](数据结构与算法/v2-6db33bd4ddb7937ca5946283ef2acc5d_720w.jpg) + +②转换成无序堆之后,我们要努力让这个无序堆变成最大堆(或是最小堆),即每个堆里都实现父节点的值都大于任何一个子节点的值。 + +![img](数据结构与算法/v2-6db33bd4ddb7937ca5946283ef2acc5d_720w.jpg) + +③从最后一个堆开始,即左下角那个没有右孩子的那个堆开始;首先对比左右孩子,由于这个堆没有右孩子,所以只能用左孩子,左孩子的值比父节点的值小所以不需要交换。如果发生交换,要检测子节点是否为其他堆的父节点,如果是,递归进行同样的操作。 + +④第二次对比红色三角形内的堆,取较大的子节点,右孩子8胜出,和父节点比较,右孩子8大于父节点3,升级做父节点,与3交换位置,3的位置没有子节点,这个堆建成最大堆。 + +![img](数据结构与算法/v2-29c3af6ba60e66f1d328c164d09b4adc_720w.jpg) + +⑤对黄色三角形内堆进行排序,过程和上面一样,最终是右孩子33升为父节点,被交换的右孩子下面也没有子节点,所以直接结束对比。 + +⑥最顶部绿色的堆,堆顶100比左右孩子都大,所以不用交换,至此最大堆创建完成。 + +![img](数据结构与算法/v2-cf88501a8092e7b0c4712aa81a875f03_720w.jpg) + +#### 堆排序(最大堆调整) + +①首先将堆顶元素100交换至最底部7的位置,7升至堆顶,100所在的底部位置即为有序区,有序区不参与之后的任何对比。 + +![img](数据结构与算法/v2-e96b570c470785e19936abceee95c8ca_720w.jpg) + +②在7升至顶部之后,对顶部重新做最大堆调整,左孩子33代替7的位置。 + +![img](数据结构与算法/v2-5bbfec3cb200b9fa7efcf29fe71fc7dd_720w.jpg) + +③在7被交换下来后,下面还有子节点,所以需要继续与子节点对比,左孩子11比7大,所以11与7交换位置,交换位置后7下面为有序区,不参与对比,所以本轮结束,无序区再次形成一个最大堆。 + +![img](数据结构与算法/v2-1f490e927a5d7d5e97e9609f7e99b6e5_720w.jpg) + +④将最大堆堆顶33交换至堆末尾,扩大有序区; + +![img](数据结构与算法/v2-d77c2cf77a7b81041fba5871979f3910_720w.jpg) + +⑤不断建立最大堆,并且扩大有序区,最终全部有序。 + +![img](数据结构与算法/v2-724e54aaff73bd4c0bf5e5352fc673ce_720w.jpg) + +### 代码实现 + +```java +import java.util.Arrays; +import java.util.Random; + +/** + * @author wuyou + */ +public class HeapSortDemo { + public static void main(String[] args) { + int[] array = {7,6,7,11,5,12,3,0,1}; + System.out.println("排序前:" + Arrays.toString(array)); + heapSort3(array); + System.out.println("排序前:" + Arrays.toString(array)); + + // 创建8000000个随机的数组 + Random random = new Random(); + int[] array3 = new int[8000000]; + for (int i = 0; i < array3.length; i++) { + array3[i] = random.nextInt(800000); + } + + // 克隆一个array4、array5 + int[] array4 = array3.clone(); + int[] array5 = array3.clone(); + + // 测试 + System.out.println("\n测试8000000条数据使用快速排序法进行排序:\n"); + + // 堆排序法1,从小到大 + long startTime1 = System.currentTimeMillis(); + heapSort1(array3); + long endTime1 = System.currentTimeMillis(); + System.out.println("堆排序法1,从小到大堆排序法花费时间为:【" + (endTime1 - startTime1) + "】毫秒\n"); + + // 堆排序法2,从小到大 + long startTime2 = System.currentTimeMillis(); + heapSort2(array4); + long endTime2 = System.currentTimeMillis(); + System.out.println("堆排序法2,从小到大堆排序法花费时间为:【" + (endTime2 - startTime2) + "】毫秒\n"); + + // 堆排序法3,从大到小 + long startTime3 = System.currentTimeMillis(); + heapSort3(array5); + long endTime3 = System.currentTimeMillis(); + System.out.println("堆排序法3,从大到小堆排序法花费时间为:【" + (endTime3 - startTime3) + "】毫秒\n"); + } + + /** + * 堆排序1,从小到大 + * @param array + */ + public static void heapSort1(int[] array) { + // 构建大顶堆 + for (int i = array.length/2 - 1; i >= 0; i--) { + // 从第一个非叶子节点从下到上,从右至左调整结构 + siftDown1(array, i, array.length); + } + // 调整堆结构,交换堆顶元素与末尾元素 + for (int i = array.length - 1; i > 0; i--) { + // 将堆顶元素与末尾元素进行交换 + swap(array, 0, i); + // 重新对堆进行调整 + siftDown1(array, 0, i); + } + } + + /** + * 将以 i 对应的非叶子节点的树调整为大顶堆 + * @param array + * @param i + * @param length + */ + public static void siftDown1(int[] array, int i, int length) { + // 取出当前元素i + int temp = array[i]; + // 从i节点的左子节点开始,也就是2*i+1处开始 + for (int j = i*2 + 1; j < length; j = j*2 + 1) { + // 如果左子节点小于右子节点,j指向右子节点,因为需要构建大顶堆 + if (j + 1 < length && array[j] < array[j + 1]) { + j++; + } + // 如果子节点大于父节点,将子节点的值赋值父节点,不用交换 + if (array[j] > temp) { + array[i] = array[j]; + i = j; + } else { + break; + } + } + array[i] = temp; + } + + /** + * 堆排序2,从小到大 + * @param arr 待排序数组 + */ + public static void heapSort2(int[] arr) { + int n = arr.length; + if (n <= 1) { + return; + } + // 将给定数组变为大顶堆 + for (int i = (n - 1 - 1) / 2; i >= 0; i--) { + siftDown2(arr, n, i); + } + // 将大顶堆的最大元素取出,放在最终位置 + for (int i = n - 1; i >= 0; i--) { + swap(arr, i, 0); + siftDown2(arr, i, 0); + } + } + + /** + * 将给定下标的元素移动到正确的位置(元素下沉) + * @param arr 给定数组 + * @param n 给定数组的长度 + * @param index 待移动元素的下标 + */ + private static void siftDown2(int[] arr, int n, int index) { + // 判断当前节点是否为最后一个非叶子节点 + while (index * 2 + 1 < n) { + int i = index * 2 + 1; + // 判断当前节点是否存在右子树 + if (i + 1 < n) { + if (arr[i] < arr[i+1]) { + i++; + } + } + // 判断当前节点的值是否小于较大节点的值 + if (arr[index] > arr[i]) { + return; + } + swap(arr, index, i); + index = i; + } + } + + /** + * 堆排序3,从大到小 + * @param array + */ + public static void heapSort3(int[] array) { + // 构建小顶堆 + for (int i = array.length/2 - 1; i >= 0; i--) { + // 从第一个非叶子节点从下到上,从右至左调整结构 + siftDown3(array, i, array.length); + } + // 调整堆结构,交换堆顶元素与末尾元素 + for (int i = array.length - 1; i > 0; i--) { + // 将堆顶元素与末尾元素进行交换 + swap(array, 0, i); + // 重新对堆进行调整 + siftDown3(array, 0, i); + } + } + + /** + * 将以 i 对应的非叶子节点的树调整为小顶堆 + * @param array + * @param i + * @param length + */ + public static void siftDown3(int[] array, int i, int length) { + // 取出当前元素i + int temp = array[i]; + // 从i节点的左子节点开始,也就是2*i+1处开始 + for (int j = i*2 + 1; j < length; j = j*2 + 1) { + // 如果左子节点大于右子节点,j指向右子节点,因为需要构建小顶堆 + if (j + 1 < length && array[j] > array[j + 1]) { + j++; + } + // 如果子节点小于父节点,将子节点的值赋值父节点,不用交换 + if (array[j] < temp) { + array[i] = array[j]; + i = j; + } else { + break; + } + } + array[i] = temp; + } + + /** + * 交换元素 + * @param array + * @param a + * @param b + */ + public static void swap(int []array,int a ,int b){ + int temp=array[a]; + array[a] = array[b]; + array[b] = temp; + } +} + +``` + +输出结果: + +``` +排序前:[7, 6, 7, 11, 5, 12, 3, 0, 1] +排序前:[12, 11, 7, 7, 6, 5, 3, 1, 0] + +测试8000000条数据使用快速排序法进行排序: + +堆排序法1,从小到大堆排序法花费时间为:【1936】毫秒 + +堆排序法2,从小到大堆排序法花费时间为:【1766】毫秒 + +堆排序法3,从大到小堆排序法花费时间为:【1787】毫秒 +``` + + + +## 总结 + +![See the source image](数据结构与算法/v2-d7f37e654f8b13555d2fbf5fe18eb6a6_r.jpg) + +相关术语解释 + +- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面 +- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面 +- 内排序:所有排序操作都在内存中完成 +- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行 + +- 时间复杂度: 一个算法执行所耗费的时间 +- 空间复杂度:运行完一个程序所需内存的大小 +- n: 数据规模 +- k: “桶”的个数 +- In-place: 不占用额外内存 +- Out-place: 占用额外内存 diff --git a/src/study/8.md b/src/study/8.md new file mode 100644 index 0000000..fc84d7e --- /dev/null +++ b/src/study/8.md @@ -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 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 result = new ArrayList<>(); + for (int i = left + 1; i < right; i++) { + result.add(i); + } + return result; + } +} +``` + +输出结果: + +``` +原本的数组为:[1, 8, 10, 10, 10, 89, 1000, 1234] +测试递归二分法查找数字1000:6 +测试循环二分法查找数字1000:6 +测试批量查找数字10:[2, 3, 4] +``` + + + +## 插值查找 + +插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。将折半查找中的求mid索引的公式,low表示左边索引left,high表示右边索引right,key就是前面我们讲的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,需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列 + *

+ * 非递归方法得到一个斐波那契数列 + * + * @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; + } + + /** + * 编写斐波那契查找算法 + *

+ * 使用非递归的方式编写算法 + * + * @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 +``` + diff --git a/src/study/9.md b/src/study/9.md new file mode 100644 index 0000000..0a3613f --- /dev/null +++ b/src/study/9.md @@ -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 +- 也可以使用幂的连乘,比如`abc,1*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 +``` + diff --git a/src/study/README.md b/src/study/README.md new file mode 100644 index 0000000..68c634c --- /dev/null +++ b/src/study/README.md @@ -0,0 +1,5 @@ +

Study
+ +Introduction:自主学习笔记! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/study/_sidebar.md b/src/study/_sidebar.md new file mode 100644 index 0000000..ae6fdea --- /dev/null +++ b/src/study/_sidebar.md @@ -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 "十大算法") diff --git a/src/study/数据结构与算法/0-1577411144.gif b/src/study/数据结构与算法/0-1577411144.gif new file mode 100644 index 0000000..8d90d15 Binary files /dev/null and b/src/study/数据结构与算法/0-1577411144.gif differ diff --git a/src/study/数据结构与算法/09563QS5-1.png b/src/study/数据结构与算法/09563QS5-1.png new file mode 100644 index 0000000..5a82dc7 Binary files /dev/null and b/src/study/数据结构与算法/09563QS5-1.png differ diff --git a/src/study/数据结构与算法/1117043-20170407105053816-427306966.png b/src/study/数据结构与算法/1117043-20170407105053816-427306966.png new file mode 100644 index 0000000..916946d Binary files /dev/null and b/src/study/数据结构与算法/1117043-20170407105053816-427306966.png differ diff --git a/src/study/数据结构与算法/1117043-20170407105111300-518814658.png b/src/study/数据结构与算法/1117043-20170407105111300-518814658.png new file mode 100644 index 0000000..22c3859 Binary files /dev/null and b/src/study/数据结构与算法/1117043-20170407105111300-518814658.png differ diff --git a/src/study/数据结构与算法/1469176-20190329000555410-1254067522.gif b/src/study/数据结构与算法/1469176-20190329000555410-1254067522.gif new file mode 100644 index 0000000..83b2ebb Binary files /dev/null and b/src/study/数据结构与算法/1469176-20190329000555410-1254067522.gif differ diff --git a/src/study/数据结构与算法/20140725232020608.jpeg b/src/study/数据结构与算法/20140725232020608.jpeg new file mode 100644 index 0000000..1926b79 Binary files /dev/null and b/src/study/数据结构与算法/20140725232020608.jpeg differ diff --git a/src/study/数据结构与算法/20150818212028853.png b/src/study/数据结构与算法/20150818212028853.png new file mode 100644 index 0000000..eee7d96 Binary files /dev/null and b/src/study/数据结构与算法/20150818212028853.png differ diff --git a/src/study/数据结构与算法/20150818215441436.png b/src/study/数据结构与算法/20150818215441436.png new file mode 100644 index 0000000..0a41c04 Binary files /dev/null and b/src/study/数据结构与算法/20150818215441436.png differ diff --git a/src/study/数据结构与算法/20150818220942825.png b/src/study/数据结构与算法/20150818220942825.png new file mode 100644 index 0000000..d4483d5 Binary files /dev/null and b/src/study/数据结构与算法/20150818220942825.png differ diff --git a/src/study/数据结构与算法/20150818221513880.png b/src/study/数据结构与算法/20150818221513880.png new file mode 100644 index 0000000..d8070ae Binary files /dev/null and b/src/study/数据结构与算法/20150818221513880.png differ diff --git a/src/study/数据结构与算法/20150818222514855.png b/src/study/数据结构与算法/20150818222514855.png new file mode 100644 index 0000000..3502672 Binary files /dev/null and b/src/study/数据结构与算法/20150818222514855.png differ diff --git a/src/study/数据结构与算法/20150818224419149.png b/src/study/数据结构与算法/20150818224419149.png new file mode 100644 index 0000000..0e21a1c Binary files /dev/null and b/src/study/数据结构与算法/20150818224419149.png differ diff --git a/src/study/数据结构与算法/20150818224940731.png b/src/study/数据结构与算法/20150818224940731.png new file mode 100644 index 0000000..8f341e8 Binary files /dev/null and b/src/study/数据结构与算法/20150818224940731.png differ diff --git a/src/study/数据结构与算法/20150818230041580.png b/src/study/数据结构与算法/20150818230041580.png new file mode 100644 index 0000000..64f14f9 Binary files /dev/null and b/src/study/数据结构与算法/20150818230041580.png differ diff --git a/src/study/数据结构与算法/20180816194335560.png b/src/study/数据结构与算法/20180816194335560.png new file mode 100644 index 0000000..ab2d151 Binary files /dev/null and b/src/study/数据结构与算法/20180816194335560.png differ diff --git a/src/study/数据结构与算法/20180816194715558.png b/src/study/数据结构与算法/20180816194715558.png new file mode 100644 index 0000000..a5d40df Binary files /dev/null and b/src/study/数据结构与算法/20180816194715558.png differ diff --git a/src/study/数据结构与算法/20180816195733482.png b/src/study/数据结构与算法/20180816195733482.png new file mode 100644 index 0000000..2af56fd Binary files /dev/null and b/src/study/数据结构与算法/20180816195733482.png differ diff --git a/src/study/数据结构与算法/20180816200720830.png b/src/study/数据结构与算法/20180816200720830.png new file mode 100644 index 0000000..621ecf7 Binary files /dev/null and b/src/study/数据结构与算法/20180816200720830.png differ diff --git a/src/study/数据结构与算法/211727_72229.png b/src/study/数据结构与算法/211727_72229.png new file mode 100644 index 0000000..14cac50 Binary files /dev/null and b/src/study/数据结构与算法/211727_72229.png differ diff --git a/src/study/数据结构与算法/70-16397070745812.png b/src/study/数据结构与算法/70-16397070745812.png new file mode 100644 index 0000000..14608ea Binary files /dev/null and b/src/study/数据结构与算法/70-16397070745812.png differ diff --git a/src/study/数据结构与算法/70-16397070895984.png b/src/study/数据结构与算法/70-16397070895984.png new file mode 100644 index 0000000..1b25437 Binary files /dev/null and b/src/study/数据结构与算法/70-16397070895984.png differ diff --git a/src/study/数据结构与算法/70.png b/src/study/数据结构与算法/70.png new file mode 100644 index 0000000..3e236d4 Binary files /dev/null and b/src/study/数据结构与算法/70.png differ diff --git a/src/study/数据结构与算法/E@J{NBX[D_MQT92ADNIN187.png b/src/study/数据结构与算法/E@J{NBX[D_MQT92ADNIN187.png new file mode 100644 index 0000000..0625324 Binary files /dev/null and b/src/study/数据结构与算法/E@J{NBX[D_MQT92ADNIN187.png differ diff --git a/src/study/数据结构与算法/Hash-table-Representation.png b/src/study/数据结构与算法/Hash-table-Representation.png new file mode 100644 index 0000000..c2f877a Binary files /dev/null and b/src/study/数据结构与算法/Hash-table-Representation.png differ diff --git a/src/study/数据结构与算法/J@HZIQS0B_BPA99GK$IX_6J.png b/src/study/数据结构与算法/J@HZIQS0B_BPA99GK$IX_6J.png new file mode 100644 index 0000000..ad2e0f1 Binary files /dev/null and b/src/study/数据结构与算法/J@HZIQS0B_BPA99GK$IX_6J.png differ diff --git a/src/study/数据结构与算法/OIP-C.JWEDGRBWyujV5QkxXo4E1QHaDG b/src/study/数据结构与算法/OIP-C.JWEDGRBWyujV5QkxXo4E1QHaDG new file mode 100644 index 0000000..30bb777 Binary files /dev/null and b/src/study/数据结构与算法/OIP-C.JWEDGRBWyujV5QkxXo4E1QHaDG differ diff --git a/src/study/数据结构与算法/OIP-C.wflLyS9o2yrQwIBi8z9z1QHaCt b/src/study/数据结构与算法/OIP-C.wflLyS9o2yrQwIBi8z9z1QHaCt new file mode 100644 index 0000000..1a9cf8a Binary files /dev/null and b/src/study/数据结构与算法/OIP-C.wflLyS9o2yrQwIBi8z9z1QHaCt differ diff --git a/src/study/数据结构与算法/image-20211128095127128.png b/src/study/数据结构与算法/image-20211128095127128.png new file mode 100644 index 0000000..635ae06 Binary files /dev/null and b/src/study/数据结构与算法/image-20211128095127128.png differ diff --git a/src/study/数据结构与算法/image-20211128120651256.png b/src/study/数据结构与算法/image-20211128120651256.png new file mode 100644 index 0000000..2efc9fa Binary files /dev/null and b/src/study/数据结构与算法/image-20211128120651256.png differ diff --git a/src/study/数据结构与算法/image-20211128121059816.png b/src/study/数据结构与算法/image-20211128121059816.png new file mode 100644 index 0000000..1f2b92b Binary files /dev/null and b/src/study/数据结构与算法/image-20211128121059816.png differ diff --git a/src/study/数据结构与算法/image-20211128140347935.png b/src/study/数据结构与算法/image-20211128140347935.png new file mode 100644 index 0000000..5b6a32d Binary files /dev/null and b/src/study/数据结构与算法/image-20211128140347935.png differ diff --git a/src/study/数据结构与算法/image-20211128153106723.png b/src/study/数据结构与算法/image-20211128153106723.png new file mode 100644 index 0000000..1a01e92 Binary files /dev/null and b/src/study/数据结构与算法/image-20211128153106723.png differ diff --git a/src/study/数据结构与算法/image-20211128153235225.png b/src/study/数据结构与算法/image-20211128153235225.png new file mode 100644 index 0000000..600e43a Binary files /dev/null and b/src/study/数据结构与算法/image-20211128153235225.png differ diff --git a/src/study/数据结构与算法/image-20211128153539290.png b/src/study/数据结构与算法/image-20211128153539290.png new file mode 100644 index 0000000..27dc2a0 Binary files /dev/null and b/src/study/数据结构与算法/image-20211128153539290.png differ diff --git a/src/study/数据结构与算法/image-20211128161904800.png b/src/study/数据结构与算法/image-20211128161904800.png new file mode 100644 index 0000000..38e0d6a Binary files /dev/null and b/src/study/数据结构与算法/image-20211128161904800.png differ diff --git a/src/study/数据结构与算法/image-20211128162803435.png b/src/study/数据结构与算法/image-20211128162803435.png new file mode 100644 index 0000000..df1d22e Binary files /dev/null and b/src/study/数据结构与算法/image-20211128162803435.png differ diff --git a/src/study/数据结构与算法/image-20211128163006566.png b/src/study/数据结构与算法/image-20211128163006566.png new file mode 100644 index 0000000..473ba6c Binary files /dev/null and b/src/study/数据结构与算法/image-20211128163006566.png differ diff --git a/src/study/数据结构与算法/image-20211130120123494.png b/src/study/数据结构与算法/image-20211130120123494.png new file mode 100644 index 0000000..a3b4b00 Binary files /dev/null and b/src/study/数据结构与算法/image-20211130120123494.png differ diff --git a/src/study/数据结构与算法/image-20211130120246042.png b/src/study/数据结构与算法/image-20211130120246042.png new file mode 100644 index 0000000..bed0510 Binary files /dev/null and b/src/study/数据结构与算法/image-20211130120246042.png differ diff --git a/src/study/数据结构与算法/image-20211210145944818.png b/src/study/数据结构与算法/image-20211210145944818.png new file mode 100644 index 0000000..b7fe77a Binary files /dev/null and b/src/study/数据结构与算法/image-20211210145944818.png differ diff --git a/src/study/数据结构与算法/image-20211214103633902.png b/src/study/数据结构与算法/image-20211214103633902.png new file mode 100644 index 0000000..0b2bdb4 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103633902.png differ diff --git a/src/study/数据结构与算法/image-20211214103714508.png b/src/study/数据结构与算法/image-20211214103714508.png new file mode 100644 index 0000000..9a85894 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103714508.png differ diff --git a/src/study/数据结构与算法/image-20211214103745652.png b/src/study/数据结构与算法/image-20211214103745652.png new file mode 100644 index 0000000..4dcf918 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103745652.png differ diff --git a/src/study/数据结构与算法/image-20211214103828272.png b/src/study/数据结构与算法/image-20211214103828272.png new file mode 100644 index 0000000..d79cf92 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103828272.png differ diff --git a/src/study/数据结构与算法/image-20211214103910207.png b/src/study/数据结构与算法/image-20211214103910207.png new file mode 100644 index 0000000..51d9523 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103910207.png differ diff --git a/src/study/数据结构与算法/image-20211214103952075.png b/src/study/数据结构与算法/image-20211214103952075.png new file mode 100644 index 0000000..25c242b Binary files /dev/null and b/src/study/数据结构与算法/image-20211214103952075.png differ diff --git a/src/study/数据结构与算法/image-20211214105020926.png b/src/study/数据结构与算法/image-20211214105020926.png new file mode 100644 index 0000000..95fb074 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214105020926.png differ diff --git a/src/study/数据结构与算法/image-20211214122555591.png b/src/study/数据结构与算法/image-20211214122555591.png new file mode 100644 index 0000000..d8bd160 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214122555591.png differ diff --git a/src/study/数据结构与算法/image-20211214130955180.png b/src/study/数据结构与算法/image-20211214130955180.png new file mode 100644 index 0000000..08d81b5 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214130955180.png differ diff --git a/src/study/数据结构与算法/image-20211214131142241.png b/src/study/数据结构与算法/image-20211214131142241.png new file mode 100644 index 0000000..cd51b4f Binary files /dev/null and b/src/study/数据结构与算法/image-20211214131142241.png differ diff --git a/src/study/数据结构与算法/image-20211214131548016.png b/src/study/数据结构与算法/image-20211214131548016.png new file mode 100644 index 0000000..3d8b980 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214131548016.png differ diff --git a/src/study/数据结构与算法/image-20211214132024072.png b/src/study/数据结构与算法/image-20211214132024072.png new file mode 100644 index 0000000..9de7e5a Binary files /dev/null and b/src/study/数据结构与算法/image-20211214132024072.png differ diff --git a/src/study/数据结构与算法/image-20211214132233628.png b/src/study/数据结构与算法/image-20211214132233628.png new file mode 100644 index 0000000..5ba701b Binary files /dev/null and b/src/study/数据结构与算法/image-20211214132233628.png differ diff --git a/src/study/数据结构与算法/image-20211214133929119.png b/src/study/数据结构与算法/image-20211214133929119.png new file mode 100644 index 0000000..ab4d939 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214133929119.png differ diff --git a/src/study/数据结构与算法/image-20211214134712046.png b/src/study/数据结构与算法/image-20211214134712046.png new file mode 100644 index 0000000..0ee85b1 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214134712046.png differ diff --git a/src/study/数据结构与算法/image-20211214172906558.png b/src/study/数据结构与算法/image-20211214172906558.png new file mode 100644 index 0000000..9fc7449 Binary files /dev/null and b/src/study/数据结构与算法/image-20211214172906558.png differ diff --git a/src/study/数据结构与算法/image-20211215105036842.png b/src/study/数据结构与算法/image-20211215105036842.png new file mode 100644 index 0000000..6708c12 Binary files /dev/null and b/src/study/数据结构与算法/image-20211215105036842.png differ diff --git a/src/study/数据结构与算法/image-20211215201916076.png b/src/study/数据结构与算法/image-20211215201916076.png new file mode 100644 index 0000000..569fe1c Binary files /dev/null and b/src/study/数据结构与算法/image-20211215201916076.png differ diff --git a/src/study/数据结构与算法/image-20211215202215151.png b/src/study/数据结构与算法/image-20211215202215151.png new file mode 100644 index 0000000..3f97ea7 Binary files /dev/null and b/src/study/数据结构与算法/image-20211215202215151.png differ diff --git a/src/study/数据结构与算法/image-20211215202221175.png b/src/study/数据结构与算法/image-20211215202221175.png new file mode 100644 index 0000000..f99c94a Binary files /dev/null and b/src/study/数据结构与算法/image-20211215202221175.png differ diff --git a/src/study/数据结构与算法/image-20211217104036824.png b/src/study/数据结构与算法/image-20211217104036824.png new file mode 100644 index 0000000..a46be62 Binary files /dev/null and b/src/study/数据结构与算法/image-20211217104036824.png differ diff --git a/src/study/数据结构与算法/image-20211217104357883.png b/src/study/数据结构与算法/image-20211217104357883.png new file mode 100644 index 0000000..928b957 Binary files /dev/null and b/src/study/数据结构与算法/image-20211217104357883.png differ diff --git a/src/study/数据结构与算法/image-20211217104705066.png b/src/study/数据结构与算法/image-20211217104705066.png new file mode 100644 index 0000000..c55eb34 Binary files /dev/null and b/src/study/数据结构与算法/image-20211217104705066.png differ diff --git a/src/study/数据结构与算法/image-20211217105020257.png b/src/study/数据结构与算法/image-20211217105020257.png new file mode 100644 index 0000000..b9dad62 Binary files /dev/null and b/src/study/数据结构与算法/image-20211217105020257.png differ diff --git a/src/study/数据结构与算法/image-20211217114710063.png b/src/study/数据结构与算法/image-20211217114710063.png new file mode 100644 index 0000000..2181bc7 Binary files /dev/null and b/src/study/数据结构与算法/image-20211217114710063.png differ diff --git a/src/study/数据结构与算法/image-20211220181256216.png b/src/study/数据结构与算法/image-20211220181256216.png new file mode 100644 index 0000000..3812e40 Binary files /dev/null and b/src/study/数据结构与算法/image-20211220181256216.png differ diff --git a/src/study/数据结构与算法/image-20211220191257563.png b/src/study/数据结构与算法/image-20211220191257563.png new file mode 100644 index 0000000..970188c Binary files /dev/null and b/src/study/数据结构与算法/image-20211220191257563.png differ diff --git a/src/study/数据结构与算法/image-20211221112727452.png b/src/study/数据结构与算法/image-20211221112727452.png new file mode 100644 index 0000000..2d3bbc3 Binary files /dev/null and b/src/study/数据结构与算法/image-20211221112727452.png differ diff --git a/src/study/数据结构与算法/image-20211221112748270.png b/src/study/数据结构与算法/image-20211221112748270.png new file mode 100644 index 0000000..92c2445 Binary files /dev/null and b/src/study/数据结构与算法/image-20211221112748270.png differ diff --git a/src/study/数据结构与算法/image-20211221112918889.png b/src/study/数据结构与算法/image-20211221112918889.png new file mode 100644 index 0000000..4063f1e Binary files /dev/null and b/src/study/数据结构与算法/image-20211221112918889.png differ diff --git a/src/study/数据结构与算法/image-20211222104348969.png b/src/study/数据结构与算法/image-20211222104348969.png new file mode 100644 index 0000000..d327b63 Binary files /dev/null and b/src/study/数据结构与算法/image-20211222104348969.png differ diff --git a/src/study/数据结构与算法/image-20211222133209602.png b/src/study/数据结构与算法/image-20211222133209602.png new file mode 100644 index 0000000..fb76a96 Binary files /dev/null and b/src/study/数据结构与算法/image-20211222133209602.png differ diff --git a/src/study/数据结构与算法/image-20211222134244269.png b/src/study/数据结构与算法/image-20211222134244269.png new file mode 100644 index 0000000..4c76720 Binary files /dev/null and b/src/study/数据结构与算法/image-20211222134244269.png differ diff --git a/src/study/数据结构与算法/image-20211231194359684.png b/src/study/数据结构与算法/image-20211231194359684.png new file mode 100644 index 0000000..2b1d747 Binary files /dev/null and b/src/study/数据结构与算法/image-20211231194359684.png differ diff --git a/src/study/数据结构与算法/image-20211231200015376.png b/src/study/数据结构与算法/image-20211231200015376.png new file mode 100644 index 0000000..8ff75c5 Binary files /dev/null and b/src/study/数据结构与算法/image-20211231200015376.png differ diff --git a/src/study/数据结构与算法/image-20211231201625846.png b/src/study/数据结构与算法/image-20211231201625846.png new file mode 100644 index 0000000..1d494d2 Binary files /dev/null and b/src/study/数据结构与算法/image-20211231201625846.png differ diff --git a/src/study/数据结构与算法/image-20211231202151469.png b/src/study/数据结构与算法/image-20211231202151469.png new file mode 100644 index 0000000..71c9037 Binary files /dev/null and b/src/study/数据结构与算法/image-20211231202151469.png differ diff --git a/src/study/数据结构与算法/image-20211231225803400.png b/src/study/数据结构与算法/image-20211231225803400.png new file mode 100644 index 0000000..4855a34 Binary files /dev/null and b/src/study/数据结构与算法/image-20211231225803400.png differ diff --git a/src/study/数据结构与算法/image-20220105172309410.png b/src/study/数据结构与算法/image-20220105172309410.png new file mode 100644 index 0000000..c6ecfef Binary files /dev/null and b/src/study/数据结构与算法/image-20220105172309410.png differ diff --git a/src/study/数据结构与算法/image-20220105172537892.png b/src/study/数据结构与算法/image-20220105172537892.png new file mode 100644 index 0000000..a00e8c2 Binary files /dev/null and b/src/study/数据结构与算法/image-20220105172537892.png differ diff --git a/src/study/数据结构与算法/image-20220106104432336.png b/src/study/数据结构与算法/image-20220106104432336.png new file mode 100644 index 0000000..9c9905c Binary files /dev/null and b/src/study/数据结构与算法/image-20220106104432336.png differ diff --git a/src/study/数据结构与算法/image-20220106104834722.png b/src/study/数据结构与算法/image-20220106104834722.png new file mode 100644 index 0000000..4326a5d Binary files /dev/null and b/src/study/数据结构与算法/image-20220106104834722.png differ diff --git a/src/study/数据结构与算法/image-20220106105104540.png b/src/study/数据结构与算法/image-20220106105104540.png new file mode 100644 index 0000000..2914069 Binary files /dev/null and b/src/study/数据结构与算法/image-20220106105104540.png differ diff --git a/src/study/数据结构与算法/image-20220106105643012.png b/src/study/数据结构与算法/image-20220106105643012.png new file mode 100644 index 0000000..db9b830 Binary files /dev/null and b/src/study/数据结构与算法/image-20220106105643012.png differ diff --git a/src/study/数据结构与算法/image-20220112141918931.png b/src/study/数据结构与算法/image-20220112141918931.png new file mode 100644 index 0000000..6f7f27c Binary files /dev/null and b/src/study/数据结构与算法/image-20220112141918931.png differ diff --git a/src/study/数据结构与算法/image-20220112142359262.png b/src/study/数据结构与算法/image-20220112142359262.png new file mode 100644 index 0000000..0849d12 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112142359262.png differ diff --git a/src/study/数据结构与算法/image-20220112143133153.png b/src/study/数据结构与算法/image-20220112143133153.png new file mode 100644 index 0000000..d099558 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112143133153.png differ diff --git a/src/study/数据结构与算法/image-20220112144107978.png b/src/study/数据结构与算法/image-20220112144107978.png new file mode 100644 index 0000000..8fa0389 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112144107978.png differ diff --git a/src/study/数据结构与算法/image-20220112153125409.png b/src/study/数据结构与算法/image-20220112153125409.png new file mode 100644 index 0000000..e8e3a26 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112153125409.png differ diff --git a/src/study/数据结构与算法/image-20220112153338401.png b/src/study/数据结构与算法/image-20220112153338401.png new file mode 100644 index 0000000..e1cca9d Binary files /dev/null and b/src/study/数据结构与算法/image-20220112153338401.png differ diff --git a/src/study/数据结构与算法/image-20220112153600907.png b/src/study/数据结构与算法/image-20220112153600907.png new file mode 100644 index 0000000..2374d16 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112153600907.png differ diff --git a/src/study/数据结构与算法/image-20220112153640279.png b/src/study/数据结构与算法/image-20220112153640279.png new file mode 100644 index 0000000..e92056a Binary files /dev/null and b/src/study/数据结构与算法/image-20220112153640279.png differ diff --git a/src/study/数据结构与算法/image-20220112154031167.png b/src/study/数据结构与算法/image-20220112154031167.png new file mode 100644 index 0000000..574214a Binary files /dev/null and b/src/study/数据结构与算法/image-20220112154031167.png differ diff --git a/src/study/数据结构与算法/image-20220112165003746.png b/src/study/数据结构与算法/image-20220112165003746.png new file mode 100644 index 0000000..0922f80 Binary files /dev/null and b/src/study/数据结构与算法/image-20220112165003746.png differ diff --git a/src/study/数据结构与算法/image-20220112165336530.png b/src/study/数据结构与算法/image-20220112165336530.png new file mode 100644 index 0000000..7cffabb Binary files /dev/null and b/src/study/数据结构与算法/image-20220112165336530.png differ diff --git a/src/study/数据结构与算法/image-20220113221220475.png b/src/study/数据结构与算法/image-20220113221220475.png new file mode 100644 index 0000000..56ff364 Binary files /dev/null and b/src/study/数据结构与算法/image-20220113221220475.png differ diff --git a/src/study/数据结构与算法/image-20220113222305120.png b/src/study/数据结构与算法/image-20220113222305120.png new file mode 100644 index 0000000..48a85f0 Binary files /dev/null and b/src/study/数据结构与算法/image-20220113222305120.png differ diff --git a/src/study/数据结构与算法/image-20220114100735575.png b/src/study/数据结构与算法/image-20220114100735575.png new file mode 100644 index 0000000..713965c Binary files /dev/null and b/src/study/数据结构与算法/image-20220114100735575.png differ diff --git a/src/study/数据结构与算法/image-20220114114603754.png b/src/study/数据结构与算法/image-20220114114603754.png new file mode 100644 index 0000000..8fa6a8c Binary files /dev/null and b/src/study/数据结构与算法/image-20220114114603754.png differ diff --git a/src/study/数据结构与算法/shell_sort-163947429146713.gif b/src/study/数据结构与算法/shell_sort-163947429146713.gif new file mode 100644 index 0000000..101569b Binary files /dev/null and b/src/study/数据结构与算法/shell_sort-163947429146713.gif differ diff --git a/src/study/数据结构与算法/shell_sort-163947434694014.gif b/src/study/数据结构与算法/shell_sort-163947434694014.gif new file mode 100644 index 0000000..101569b Binary files /dev/null and b/src/study/数据结构与算法/shell_sort-163947434694014.gif differ diff --git a/src/study/数据结构与算法/shell_sort-163947435085215.gif b/src/study/数据结构与算法/shell_sort-163947435085215.gif new file mode 100644 index 0000000..101569b Binary files /dev/null and b/src/study/数据结构与算法/shell_sort-163947435085215.gif differ diff --git a/src/study/数据结构与算法/shell_sort.gif b/src/study/数据结构与算法/shell_sort.gif new file mode 100644 index 0000000..101569b Binary files /dev/null and b/src/study/数据结构与算法/shell_sort.gif differ diff --git a/src/study/数据结构与算法/v2-1f490e927a5d7d5e97e9609f7e99b6e5_720w.jpg b/src/study/数据结构与算法/v2-1f490e927a5d7d5e97e9609f7e99b6e5_720w.jpg new file mode 100644 index 0000000..4713902 Binary files /dev/null and b/src/study/数据结构与算法/v2-1f490e927a5d7d5e97e9609f7e99b6e5_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-29c3af6ba60e66f1d328c164d09b4adc_720w.jpg b/src/study/数据结构与算法/v2-29c3af6ba60e66f1d328c164d09b4adc_720w.jpg new file mode 100644 index 0000000..266d6bf Binary files /dev/null and b/src/study/数据结构与算法/v2-29c3af6ba60e66f1d328c164d09b4adc_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-36b0cfb3e79fd37d3589ea2a6ab50c35_b.jpg b/src/study/数据结构与算法/v2-36b0cfb3e79fd37d3589ea2a6ab50c35_b.jpg new file mode 100644 index 0000000..d4546f3 Binary files /dev/null and b/src/study/数据结构与算法/v2-36b0cfb3e79fd37d3589ea2a6ab50c35_b.jpg differ diff --git a/src/study/数据结构与算法/v2-5bbfec3cb200b9fa7efcf29fe71fc7dd_720w.jpg b/src/study/数据结构与算法/v2-5bbfec3cb200b9fa7efcf29fe71fc7dd_720w.jpg new file mode 100644 index 0000000..627adff Binary files /dev/null and b/src/study/数据结构与算法/v2-5bbfec3cb200b9fa7efcf29fe71fc7dd_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-6db33bd4ddb7937ca5946283ef2acc5d_720w.jpg b/src/study/数据结构与算法/v2-6db33bd4ddb7937ca5946283ef2acc5d_720w.jpg new file mode 100644 index 0000000..82fa9b8 Binary files /dev/null and b/src/study/数据结构与算法/v2-6db33bd4ddb7937ca5946283ef2acc5d_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-724e54aaff73bd4c0bf5e5352fc673ce_720w.jpg b/src/study/数据结构与算法/v2-724e54aaff73bd4c0bf5e5352fc673ce_720w.jpg new file mode 100644 index 0000000..3c1ae27 Binary files /dev/null and b/src/study/数据结构与算法/v2-724e54aaff73bd4c0bf5e5352fc673ce_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-acbdde7cf6f0426e693187c4899716e7_720w.png b/src/study/数据结构与算法/v2-acbdde7cf6f0426e693187c4899716e7_720w.png new file mode 100644 index 0000000..c2b26f2 Binary files /dev/null and b/src/study/数据结构与算法/v2-acbdde7cf6f0426e693187c4899716e7_720w.png differ diff --git a/src/study/数据结构与算法/v2-cf88501a8092e7b0c4712aa81a875f03_720w.jpg b/src/study/数据结构与算法/v2-cf88501a8092e7b0c4712aa81a875f03_720w.jpg new file mode 100644 index 0000000..0317cd4 Binary files /dev/null and b/src/study/数据结构与算法/v2-cf88501a8092e7b0c4712aa81a875f03_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-d77c2cf77a7b81041fba5871979f3910_720w.jpg b/src/study/数据结构与算法/v2-d77c2cf77a7b81041fba5871979f3910_720w.jpg new file mode 100644 index 0000000..2cdf792 Binary files /dev/null and b/src/study/数据结构与算法/v2-d77c2cf77a7b81041fba5871979f3910_720w.jpg differ diff --git a/src/study/数据结构与算法/v2-d7f37e654f8b13555d2fbf5fe18eb6a6_r.jpg b/src/study/数据结构与算法/v2-d7f37e654f8b13555d2fbf5fe18eb6a6_r.jpg new file mode 100644 index 0000000..4b87a2e Binary files /dev/null and b/src/study/数据结构与算法/v2-d7f37e654f8b13555d2fbf5fe18eb6a6_r.jpg differ diff --git a/src/study/数据结构与算法/v2-e96b570c470785e19936abceee95c8ca_720w.jpg b/src/study/数据结构与算法/v2-e96b570c470785e19936abceee95c8ca_720w.jpg new file mode 100644 index 0000000..24c2e23 Binary files /dev/null and b/src/study/数据结构与算法/v2-e96b570c470785e19936abceee95c8ca_720w.jpg differ diff --git a/src/study/数据结构与算法/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpc2hhbmxlaWxpeGlu,size_16,color_FFFFFF,t_70.png b/src/study/数据结构与算法/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpc2hhbmxlaWxpeGlu,size_16,color_FFFFFF,t_70.png new file mode 100644 index 0000000..a44a0c2 Binary files /dev/null and b/src/study/数据结构与算法/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpc2hhbmxlaWxpeGlu,size_16,color_FFFFFF,t_70.png differ diff --git a/src/study/数据结构与算法/中序遍历.gif b/src/study/数据结构与算法/中序遍历.gif new file mode 100644 index 0000000..89358b3 Binary files /dev/null and b/src/study/数据结构与算法/中序遍历.gif differ diff --git a/src/study/数据结构与算法/二分查找.gif b/src/study/数据结构与算法/二分查找.gif new file mode 100644 index 0000000..f1c00aa Binary files /dev/null and b/src/study/数据结构与算法/二分查找.gif differ diff --git a/src/study/数据结构与算法/二分查找mid.png b/src/study/数据结构与算法/二分查找mid.png new file mode 100644 index 0000000..cef8668 Binary files /dev/null and b/src/study/数据结构与算法/二分查找mid.png differ diff --git a/src/study/数据结构与算法/冒泡排序.gif b/src/study/数据结构与算法/冒泡排序.gif new file mode 100644 index 0000000..9dd0ed4 Binary files /dev/null and b/src/study/数据结构与算法/冒泡排序.gif differ diff --git a/src/study/数据结构与算法/冒泡排序.jpg b/src/study/数据结构与算法/冒泡排序.jpg new file mode 100644 index 0000000..7f47a26 Binary files /dev/null and b/src/study/数据结构与算法/冒泡排序.jpg differ diff --git a/src/study/数据结构与算法/前序遍历.gif b/src/study/数据结构与算法/前序遍历.gif new file mode 100644 index 0000000..a4ccb18 Binary files /dev/null and b/src/study/数据结构与算法/前序遍历.gif differ diff --git a/src/study/数据结构与算法/后序遍历.gif b/src/study/数据结构与算法/后序遍历.gif new file mode 100644 index 0000000..6bbf2b4 Binary files /dev/null and b/src/study/数据结构与算法/后序遍历.gif differ diff --git a/src/study/数据结构与算法/基数排序.gif b/src/study/数据结构与算法/基数排序.gif new file mode 100644 index 0000000..2a55695 Binary files /dev/null and b/src/study/数据结构与算法/基数排序.gif differ diff --git a/src/study/数据结构与算法/堆排序.gif b/src/study/数据结构与算法/堆排序.gif new file mode 100644 index 0000000..783010a Binary files /dev/null and b/src/study/数据结构与算法/堆排序.gif differ diff --git a/src/study/数据结构与算法/广度优先搜索.jpg b/src/study/数据结构与算法/广度优先搜索.jpg new file mode 100644 index 0000000..39f7c2c Binary files /dev/null and b/src/study/数据结构与算法/广度优先搜索.jpg differ diff --git a/src/study/数据结构与算法/归并排序.gif b/src/study/数据结构与算法/归并排序.gif new file mode 100644 index 0000000..a29ca19 Binary files /dev/null and b/src/study/数据结构与算法/归并排序.gif differ diff --git a/src/study/数据结构与算法/归并排序.jpg b/src/study/数据结构与算法/归并排序.jpg new file mode 100644 index 0000000..8787d2c Binary files /dev/null and b/src/study/数据结构与算法/归并排序.jpg differ diff --git a/src/study/数据结构与算法/快速排序.gif b/src/study/数据结构与算法/快速排序.gif new file mode 100644 index 0000000..ad88d35 Binary files /dev/null and b/src/study/数据结构与算法/快速排序.gif differ diff --git a/src/study/数据结构与算法/插值查找mid.png b/src/study/数据结构与算法/插值查找mid.png new file mode 100644 index 0000000..a21efd4 Binary files /dev/null and b/src/study/数据结构与算法/插值查找mid.png differ diff --git a/src/study/数据结构与算法/插入排序.gif b/src/study/数据结构与算法/插入排序.gif new file mode 100644 index 0000000..2702b14 Binary files /dev/null and b/src/study/数据结构与算法/插入排序.gif differ diff --git a/src/study/数据结构与算法/插入排序.jpg b/src/study/数据结构与算法/插入排序.jpg new file mode 100644 index 0000000..d2df145 Binary files /dev/null and b/src/study/数据结构与算法/插入排序.jpg differ diff --git a/src/study/数据结构与算法/斐波那契查找.png b/src/study/数据结构与算法/斐波那契查找.png new file mode 100644 index 0000000..d6d3ca0 Binary files /dev/null and b/src/study/数据结构与算法/斐波那契查找.png differ diff --git a/src/study/数据结构与算法/深度优先搜索.jpg b/src/study/数据结构与算法/深度优先搜索.jpg new file mode 100644 index 0000000..39f7c2c Binary files /dev/null and b/src/study/数据结构与算法/深度优先搜索.jpg differ diff --git a/src/study/数据结构与算法/计算机流程图.png b/src/study/数据结构与算法/计算机流程图.png new file mode 100644 index 0000000..9d03f52 Binary files /dev/null and b/src/study/数据结构与算法/计算机流程图.png differ diff --git a/src/study/数据结构与算法/选择排序.gif b/src/study/数据结构与算法/选择排序.gif new file mode 100644 index 0000000..353459b Binary files /dev/null and b/src/study/数据结构与算法/选择排序.gif differ diff --git a/src/study/数据结构与算法/选择排序.jpg b/src/study/数据结构与算法/选择排序.jpg new file mode 100644 index 0000000..ea7ed3e Binary files /dev/null and b/src/study/数据结构与算法/选择排序.jpg differ