diff --git a/notes/算法.md b/notes/算法.md index cb5d4dbe..e404a3c8 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -2,19 +2,10 @@ * [一、前言](#一前言) * [二、算法分析](#二算法分析) * [数学模型](#数学模型) + * [注意事项](#注意事项) * [ThreeSum](#threesum) * [倍率实验](#倍率实验) - * [注意事项](#注意事项) -* [三、栈和队列](#三栈和队列) - * [栈](#栈) - * [队列](#队列) -* [四、并查集](#四并查集) - * [quick-find](#quick-find) - * [quick-union](#quick-union) - * [加权 quick-union](#加权-quick-union) - * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) - * [各种 union-find 算法的比较](#各种-union-find-算法的比较) -* [五、排序](#五排序) +* [三、排序](#三排序) * [选择排序](#选择排序) * [冒泡排序](#冒泡排序) * [插入排序](#插入排序) @@ -23,6 +14,15 @@ * [快速排序](#快速排序) * [堆排序](#堆排序) * [小结](#小结) +* [四、并查集](#四并查集) + * [Quick Find](#quick-find) + * [Quick Union](#quick-union) + * [加权 Quick Union](#加权-quick-union) + * [路径压缩的加权 Quick Union](#路径压缩的加权-quick-union) + * [比较](#比较) +* [五、栈和队列](#五栈和队列) + * [栈](#栈) + * [队列](#队列) * [六、查找](#六查找) * [初级实现](#初级实现) * [二叉查找树](#二叉查找树) @@ -39,7 +39,8 @@ # 一、前言 -本文实现代码以及测试代码放在 [Algorithm](https://github.com/CyC2018/Algorithm) +- 实现代码:[Algorithm](https://github.com/CyC2018/Algorithm) +- 绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727) # 二、算法分析 @@ -61,6 +62,28 @@ N3/6-N2/2+N/3 的增长数量级为 O(N3)。增 使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 +## 注意事项 + +### 1. 大常数 + +在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 + +### 2. 缓存 + +计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 + +### 3. 对最坏情况下的性能的保证 + +在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 + +### 4. 随机化算法 + +通过打乱输入,去除算法对输入的依赖。 + +### 5. 均摊分析 + +将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 + ## ThreeSum ThreeSum 用于统计一个数组中和为 0 的三元组数量。 @@ -71,6 +94,10 @@ public interface ThreeSum { } ``` +### 1. ThreeSumSlow + +该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 + ```java public class ThreeSumSlow implements ThreeSum { @Override @@ -87,9 +114,7 @@ public class ThreeSumSlow implements ThreeSum { } ``` -该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 - - **改进**
+### 2. ThreeSumFast 通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 @@ -153,21 +178,31 @@ public class BinarySearch { ```java public class RatioTest { + public static void main(String[] args) { + int N = 500; int loopTimes = 7; double preTime = -1; + while (loopTimes-- > 0) { + int[] nums = new int[N]; + StopWatch.start(); + ThreeSum threeSum = new ThreeSumSlow(); + int cnt = threeSum.count(nums); System.out.println(cnt); + double elapsedTime = StopWatch.elapsedTime(); double ratio = preTime == -1 ? 0 : elapsedTime / preTime; System.out.println(N + " " + elapsedTime + " " + ratio); + preTime = elapsedTime; N *= 2; + } } } @@ -175,12 +210,15 @@ public class RatioTest { ```java public class StopWatch { + private static long start; - - public static void start(){ + + + public static void start() { start = System.currentTimeMillis(); } - + + public static double elapsedTime() { long now = System.currentTimeMillis(); return (now - start) / 1000.0; @@ -188,438 +226,7 @@ public class StopWatch { } ``` -## 注意事项 - -### 1. 大常数 - -在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 - -### 2. 缓存 - -计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 - -### 3. 对最坏情况下的性能的保证 - -在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 - -### 4. 随机化算法 - -通过打乱输入,去除算法对输入的依赖。 - -### 5. 均摊分析 - -将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 - -# 三、栈和队列 - -## 栈 - -First-In-Last-Out - -```java -public interface MyStack extends Iterable { - MyStack push(Item item); - - Item pop() throws Exception; - - boolean isEmpty(); - - int size(); -} -``` - -### 1. 数组实现 - -```java -public class ArrayStack implements MyStack { - // 栈元素数组,只能通过转型来创建泛型数组 - private Item[] a = (Item[]) new Object[1]; - // 元素数量 - private int N = 0; - - @Override - public MyStack push(Item item) { - check(); - a[N++] = item; - return this; - } - - @Override - public Item pop() throws Exception { - if (isEmpty()) - throw new Exception("stack is empty"); - - Item item = a[--N]; - check(); - a[N] = null; // 避免对象游离 - return item; - } - - private void check() { - if (N >= a.length) - resize(2 * a.length); - else if (N > 0 && N <= a.length / 4) - resize(a.length / 2); - } - - /** - * 调整数组大小,使得栈具有伸缩性 - */ - private void resize(int size) { - Item[] tmp = (Item[]) new Object[size]; - for (int i = 0; i < N; i++) - tmp[i] = a[i]; - a = tmp; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public Iterator iterator() { - // 返回逆序遍历的迭代器 - return new Iterator() { - private int i = N; - - @Override - public boolean hasNext() { - return i > 0; - } - - @Override - public Item next() { - return a[--i]; - } - }; - } -} -``` - -### 2. 链表实现 - -需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 - -```java -public class ListStack implements MyStack { - private Node top = null; - private int N = 0; - - private class Node { - Item item; - Node next; - } - - @Override - public MyStack push(Item item) { - Node newTop = new Node(); - newTop.item = item; - newTop.next = top; - top = newTop; - N++; - return this; - } - - @Override - public Item pop() throws Exception { - if (isEmpty()) - throw new Exception("stack is empty"); - Item item = top.item; - top = top.next; - N--; - return item; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public Iterator iterator() { - return new Iterator() { - private Node cur = top; - - @Override - public boolean hasNext() { - return cur != null; - } - - @Override - public Item next() { - Item item = cur.item; - cur = cur.next; - return item; - } - }; - } -} -``` - -## 队列 - -First-In-First-Out - -下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 - -这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 - -```java -public interface MyQueue extends Iterable { - int size(); - - boolean isEmpty(); - - MyQueue add(Item item); - - Item remove() throws Exception; -} -``` - -```java -public class ListQueue implements MyQueue { - private Node first; - private Node last; - int N = 0; - - private class Node { - Item item; - Node next; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public MyQueue add(Item item) { - Node newNode = new Node(); - newNode.item = item; - newNode.next = null; - if (isEmpty()) { - last = newNode; - first = newNode; - } else { - last.next = newNode; - last = newNode; - } - N++; - return this; - } - - @Override - public Item remove() throws Exception { - if (isEmpty()) - throw new Exception("queue is empty"); - Node node = first; - first = first.next; - N--; - if (isEmpty()) - last = null; - return node.item; - } - - @Override - public Iterator iterator() { - return new Iterator() { - Node cur = first; - - @Override - public boolean hasNext() { - return cur != null; - } - - @Override - public Item next() { - Item item = cur.item; - cur = cur.next; - return item; - } - }; - } -} -``` - -# 四、并查集 - -用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 - -

- -| 方法 | 描述 | -| :---: | :---: | -| UF(int N) | 构造一个大小为 N 的并查集 | -| void union(int p, int q) | 连接 p 和 q 节点 | -| int find(int p) | 查找 p 所在的连通分量 | -| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | - -```java -public abstract class UF { - protected int[] id; - - public UF(int N) { - id = new int[N]; - for (int i = 0; i < N; i++) - id[i] = i; - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - public abstract int find(int p); - - public abstract void union(int p, int q); -} -``` - -## quick-find - -可以快速进行 find 操作,即可以快速判断两个节点是否连通。 - -同一连通分量的所有节点的 id 值相等。 - -但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 - -

- -```java -public class QuickFindUF extends UF { - public QuickFindUF(int N) { - super(N); - } - - @Override - public int find(int p) { - return id[p]; - } - - @Override - public void union(int p, int q) { - int pID = find(p); - int qID = find(q); - - if (pID == qID) - return; - - for (int i = 0; i < id.length; i++) - if (id[i] == pID) - id[i] = qID; - } -} - -``` - -## quick-union - -可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 - -但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 - -

- -```java -public class QuickUnionUF extends UF { - public QuickUnionUF(int N) { - super(N); - } - - @Override - public int find(int p) { - while (p != id[p]) - p = id[p]; - return p; - } - - @Override - public void union(int p, int q) { - int pRoot = find(p); - int qRoot = find(q); - if (pRoot != qRoot) - id[pRoot] = qRoot; - } -} -``` - -这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 - -

- -## 加权 quick-union - -为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 - -理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 - -

- -```java -public class WeightedQuickUnionUF extends UF { - - // 保存节点的数量信息 - private int[] sz; - - public WeightedQuickUnionUF(int N) { - super(N); - this.sz = new int[N]; - for (int i = 0; i < N; i++) - this.sz[i] = 1; - } - - @Override - public int find(int p) { - while (p != id[p]) - p = id[p]; - return p; - } - - @Override - public void union(int p, int q) { - int i = find(p); - int j = find(q); - if (i == j) return; - if (sz[i] < sz[j]) { - id[i] = j; - sz[j] += sz[i]; - } else { - id[j] = i; - sz[i] += sz[j]; - } - } -} -``` - -## 路径压缩的加权 quick-union - -在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 - -## 各种 union-find 算法的比较 - -| 算法 | union | find | -| :---: | :---: | :---: | -| quick-find | N | 1 | -| quick-union | 树高 | 树高 | -| 加权 quick-union | logN | logN | -| 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 | - -# 五、排序 +# 三、排序 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 @@ -648,41 +255,49 @@ public abstract class Sort> { 选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 -

+选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 + +

```java public class Selection> extends Sort { + @Override public void sort(T[] nums) { int N = nums.length; for (int i = 0; i < N; i++) { int min = i; - for (int j = i + 1; j < N; j++) - if (less(nums[j], nums[min])) + for (int j = i + 1; j < N; j++) { + if (less(nums[j], nums[min])) { min = j; + } + } swap(nums, i, min); } } } ``` -选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 - ## 冒泡排序 -通过从左到右不断交换相邻逆序的相邻元素,在一轮的交换之后,可以让未排序的元素上浮到右侧。 +从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。 在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 +以下演示了在一轮循环中,将最大的元素 5 上浮到最右侧。 + +

+ ```java public class Bubble> extends Sort { + @Override public void sort(T[] nums) { int N = nums.length; boolean hasSorted = false; - for (int i = 0; i < N && !hasSorted; i++) { + for (int i = N - 1; i > 0 && !hasSorted; i--) { hasSorted = true; - for (int j = 0; j < N - i - 1; j++) { + for (int j = 0; j < i; j++) { if (less(nums[j + 1], nums[j])) { hasSorted = false; swap(nums, j, j + 1); @@ -695,23 +310,7 @@ public class Bubble> extends Sort { ## 插入排序 -插入排序从左到右进行,每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左部数组依然有序。 - -第 j 元素是通过不断向左比较并交换来实现插入过程:当第 j 元素小于第 j - 1 元素,就将它们的位置交换,然后令 j 指针向左移动一个位置,不断进行以上操作。 - -

- -```java -public class Insertion> extends Sort { - @Override - public void sort(T[] nums) { - int N = nums.length; - for (int i = 1; i < N; i++) - for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) - swap(nums, j, j - 1); - } -} -``` +每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。 对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。 @@ -721,6 +320,25 @@ public class Insertion> extends Sort { - 最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是倒序的; - 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 +以下演示了在一轮循环中,将元素 2 插入到左侧已经排序的数组中。 + +

+ +```java +public class Insertion> extends Sort { + + @Override + public void sort(T[] nums) { + int N = nums.length; + for (int i = 1; i < N; i++) { + for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) { + swap(nums, j, j - 1); + } + } + } +} +``` + ## 希尔排序 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。 @@ -1137,6 +755,511 @@ public class HeapSort> extends Sort { Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 +# 四、并查集 + +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 + +

+ +| 方法 | 描述 | +| :---: | :---: | +| UF(int N) | 构造一个大小为 N 的并查集 | +| void union(int p, int q) | 连接 p 和 q 节点 | +| int find(int p) | 查找 p 所在的连通分量 | +| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | + +```java +public abstract class UF { + + protected int[] id; + + public UF(int N) { + id = new int[N]; + for (int i = 0; i < N; i++) { + id[i] = i; + } + } + + public boolean connected(int p, int q) { + return find(p) == find(q); + } + + public abstract int find(int p); + + public abstract void union(int p, int q); +} +``` + +## Quick Find + +可以快速进行 find 操作,即可以快速判断两个节点是否连通。 + +需要保证同一连通分量的所有节点的 id 值相等。 + +但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 + +

+ +```java +public class QuickFindUF extends UF { + + public QuickFindUF(int N) { + super(N); + } + + + @Override + public int find(int p) { + return id[p]; + } + + + @Override + public void union(int p, int q) { + + int pID = find(p); + int qID = find(q); + + if (pID == qID) { + return; + } + + for (int i = 0; i < id.length; i++) { + if (id[i] == pID) { + id[i] = qID; + } + } + } +} +``` + +## Quick Union + +可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 + +但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 + +

+ +```java +public class QuickUnionUF extends UF { + + public QuickUnionUF(int N) { + super(N); + } + + + @Override + public int find(int p) { + + while (p != id[p]) { + p = id[p]; + } + return p; + } + + + @Override + public void union(int p, int q) { + + int pRoot = find(p); + int qRoot = find(q); + + if (pRoot != qRoot) { + id[pRoot] = qRoot; + } + } +} +``` + +这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 + +

+ +## 加权 Quick Union + +为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 + +理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 + +

+ +```java +public class WeightedQuickUnionUF extends UF { + + // 保存节点的数量信息 + private int[] sz; + + + public WeightedQuickUnionUF(int N) { + super(N); + this.sz = new int[N]; + for (int i = 0; i < N; i++) { + this.sz[i] = 1; + } + } + + + @Override + public int find(int p) { + while (p != id[p]) { + p = id[p]; + } + return p; + } + + + @Override + public void union(int p, int q) { + + int i = find(p); + int j = find(q); + + if (i == j) return; + + if (sz[i] < sz[j]) { + id[i] = j; + sz[j] += sz[i]; + } else { + id[j] = i; + sz[i] += sz[j]; + } + } +} +``` + +## 路径压缩的加权 Quick Union + +在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 + +## 比较 + +| 算法 | union | find | +| :---: | :---: | :---: | +| Quick Find | N | 1 | +| Quick Union | 树高 | 树高 | +| 加权 Quick Union | logN | logN | +| 路径压缩的加权 Quick Union | 非常接近 1 | 非常接近 1 | + +# 五、栈和队列 + +## 栈 + +```java +public interface MyStack extends Iterable { + + MyStack push(Item item); + + Item pop() throws Exception; + + boolean isEmpty(); + + int size(); + +} +``` + +### 1. 数组实现 + +```java +public class ArrayStack implements MyStack { + + // 栈元素数组,只能通过转型来创建泛型数组 + private Item[] a = (Item[]) new Object[1]; + + // 元素数量 + private int N = 0; + + + @Override + public MyStack push(Item item) { + check(); + a[N++] = item; + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = a[--N]; + + check(); + + // 避免对象游离 + a[N] = null; + + return item; + } + + + private void check() { + + if (N >= a.length) { + resize(2 * a.length); + + } else if (N > 0 && N <= a.length / 4) { + resize(a.length / 2); + } + } + + + /** + * 调整数组大小,使得栈具有伸缩性 + */ + private void resize(int size) { + + Item[] tmp = (Item[]) new Object[size]; + + for (int i = 0; i < N; i++) { + tmp[i] = a[i]; + } + + a = tmp; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + // 返回逆序遍历的迭代器 + return new Iterator() { + + private int i = N; + + @Override + public boolean hasNext() { + return i > 0; + } + + @Override + public Item next() { + return a[--i]; + } + }; + + } +} +``` + +### 2. 链表实现 + +需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 + +```java +public class ListStack implements MyStack { + + private Node top = null; + private int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public MyStack push(Item item) { + + Node newTop = new Node(); + + newTop.item = item; + newTop.next = top; + + top = newTop; + + N++; + + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = top.item; + + top = top.next; + N--; + + return item; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + private Node cur = top; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + + } +} +``` + +## 队列 + +First-In-First-Out + +下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 + +这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 + +```java +public interface MyQueue extends Iterable { + + int size(); + + boolean isEmpty(); + + MyQueue add(Item item); + + Item remove() throws Exception; +} +``` + +```java +public class ListQueue implements MyQueue { + + private Node first; + private Node last; + int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public MyQueue add(Item item) { + + Node newNode = new Node(); + newNode.item = item; + newNode.next = null; + + if (isEmpty()) { + last = newNode; + first = newNode; + } else { + last.next = newNode; + last = newNode; + } + + N++; + return this; + } + + + @Override + public Item remove() throws Exception { + + if (isEmpty()) { + throw new Exception("queue is empty"); + } + + Node node = first; + first = first.next; + N--; + + if (isEmpty()) { + last = null; + } + + return node.item; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + Node cur = first; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + } +} +``` + + + + + # 六、查找 符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。 diff --git a/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png new file mode 100644 index 00000000..c4592305 Binary files /dev/null and b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png differ diff --git a/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png new file mode 100644 index 00000000..a97e49a6 Binary files /dev/null and b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png differ diff --git a/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png new file mode 100644 index 00000000..3b05b25b Binary files /dev/null and b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png differ