From d9e41ff083328a186562970a07d3e7a06938fb63 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Wed, 28 Mar 2018 14:26:12 +0800 Subject: [PATCH] auto commit --- notes/算法.md | 1621 ++++++++++++++++++++++++++++++++++++++++ notes/设计模式.md | 1800 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 3421 insertions(+) create mode 100644 notes/算法.md create mode 100644 notes/设计模式.md diff --git a/notes/算法.md b/notes/算法.md new file mode 100644 index 00000000..b4d8893a --- /dev/null +++ b/notes/算法.md @@ -0,0 +1,1621 @@ + +* [一、算法分析](#一算法分析) + * [函数转换](#函数转换) + * [数学模型](#数学模型) + * [ThreeSum](#threesum) + * [倍率实验](#倍率实验) + * [注意事项](#注意事项) +* [二、栈和队列](#二栈和队列) + * [栈](#栈) + * [队列](#队列) +* [三、union-find](#三union-find) + * [quick-find](#quick-find) + * [quick-union](#quick-union) + * [加权 quick-union](#加权-quick-union) + * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) + * [各种 union-find 算法的比较](#各种-union-find-算法的比较) +* [四、排序](#四排序) + * [选择排序](#选择排序) + * [插入排序](#插入排序) + * [希尔排序](#希尔排序) + * [归并排序](#归并排序) + * [快速排序](#快速排序) + * [优先队列](#优先队列) + * [应用](#应用) +* [五、查找](#五查找) + * [符号表](#符号表) + * [二叉查找树](#二叉查找树) + * [2-3 查找树](#2-3-查找树) + * [红黑二叉查找树](#红黑二叉查找树) + * [散列表](#散列表) + * [应用](#应用) + + + +# 一、算法分析 + +## 函数转换 + +指数函数可以转换为线性函数,从而在函数图像上显示的更直观。例如 + +

+ +可以在其两端同时取对数,得到: + +

+ +

+ +## 数学模型 + +### 1. 近似 + +使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数,例如 N3/6-N2/2+N/3 \~ N3/6。 + +

+ +### 2. 增长数量级 + +增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 N3 与它是否用 Java 实现,是否运行于特定计算机上无关。 + +

+ +### 3. 内循环 + +执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 + +### 4. 成本模型 + +使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 + +## ThreeSum + +ThreeSum 用于统计一个数组中三元组的和为 0 的数量。 + +```java +public class ThreeSum { + public static int count(int[] a) { + int N = a.length; + int cnt = 0; + for (int i = 0; i < N; i++) { + for (int j = i + 1; j < N; j++) { + for (int k = j + 1; k < N; k++) { + if (a[i] + a[j] + a[k] == 0) { + cnt++; + } + } + } + } + return cnt; + } +} +``` + +该算法的内循环为`if (a[i] + a[j] + a[k] == 0)`语句,总共执行的次数为 N(N-1)(N-2) = N3/6 - N2/2 + N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 N3。 + + **改进**
+ +通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 + +该方法可以将 ThreeSum 算法增长数量级降低为 N2logN。 + +```java +public class ThreeSumFast { + public static int count(int[] a) { + Arrays.sort(a); + int N = a.length; + int cnt = 0; + for (int i = 0; i < N; i++) { + for (int j = i + 1; j < N; j++) { + // rank() 方法返回元素在数组中的下标,如果元素不存在,这里会返回 -1。 + // 应该注意这里的下标必须大于 j,否则会重复统计。 + if (BinarySearch.rank(-a[i] - a[j], a) > j) { + cnt++; + } + } + } + return cnt; + } +} +``` + +## 倍率实验 + +如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 + +例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: + +

+ +可以看到,T(2N)/T(N) \~ 23,因此可以确定 T(N) \~ aN3logN。 + +## 注意事项 + +### 1. 大常数 + +在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 + +### 2. 缓存 + +计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 + +### 3. 对最坏情况下的性能的保证 + +在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 + +### 4. 随机化算法 + +通过打乱输入,去除算法对输入的依赖。 + +### 5. 均摊分析 + +将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 + +# 二、栈和队列 + +## 栈 + +first-in-last-out(FILO) + +

+ + **1. 数组实现**
+ +```java +public class ResizeArrayStack implements Iterable { + private Item[] a = (Item[]) new Object[1]; + private int N = 0; + + public void push(Item item) { + if (N >= a.length) { + resize(2 * a.length); + } + a[N++] = item; + } + + public Item pop() { + Item item = a[--N]; + if (N <= a.length / 4) { + resize(a.length / 2); + } + return item; + } + + // 调整数组大小,使得栈具有伸缩性 + private void resize(int size) { + Item[] tmp = (Item[]) new Object[size]; + for (int i = 0; i < N; i++) { + tmp[i] = a[i]; + } + a = tmp; + } + + public boolean isEmpty() { + return N == 0; + } + + public int size() { + return N; + } + + @Override + public Iterator iterator() { + // 需要返回逆序遍历的迭代器 + return new ReverseArrayIterator(); + } + + private class ReverseArrayIterator implements Iterator { + private int i = N; + + @Override + public boolean hasNext() { + return i > 0; + } + + @Override + public Item next() { + return a[--i]; + } + } +} +``` + +上面实现使用了泛型,Java 不能直接创建泛型数组,只能使用转型来创建。 + +```java +Item[] arr = (Item[]) new Object[N]; +``` + + **2. 链表实现**
+ +需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 + +```java +public class Stack { + + private Node top = null; + private int N = 0; + + private class Node { + Item item; + Node next; + } + + public boolean isEmpty() { + return N == 0; + } + + public int size() { + return N; + } + + public void push(Item item) { + Node newTop = new Node(); + newTop.item = item; + newTop.next = top; + top = newTop; + N++; + } + + public Item pop() { + Item item = top.item; + top = top.next; + N--; + return item; + } +} +``` +## 队列 + +first-in-first-out(FIFO) + +

+ +下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 + +这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 + +```java +public class Queue { + private Node first; + private Node last; + int N = 0; + private class Node{ + Item item; + Node next; + } + + public boolean isEmpty(){ + return N == 0; + } + + public int size(){ + return N; + } + + // 入队列 + public void enqueue(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++; + } + + // 出队列 + public Item dequeue(){ + Node node = first; + first = first.next; + N--; + return node.item; + } +} +``` + +# 三、union-find + + **概览**
+ +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 + +

+ + **API**
+ +

+ + **基本数据结构**
+ +```java +public class UF { + // 使用 id 数组来保存点的连通信息 + private 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); + } +} +``` + +## quick-find + +保证在同一连通分量的所有节点的 id 值相等。 + +这种方法可以快速取得一个节点的 id 值,并且判断两个节点是否连通。但是 union 的操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 + +```java + public int find(int p) { + return id[p]; + } + 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 值指向另一个节点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,应该注意的是根节点需要指向自己。查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id 值。 + +

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

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

+ +```java +public class WeightedQuickUnionUF { + private int[] id; + // 保存节点的数量信息 + private int[] sz; + + public WeightedQuickUnionUF(int N) { + id = new int[N]; + sz = new int[N]; + for (int i = 0; i < N; i++) { + id[i] = i; + sz[i] = 1; + } + } + + public boolean connected(int p, int q) { + return find(p) == find(q); + } + + public int find(int p) { + while (p != id[p]) p = id[p]; + return p; + } + + 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 算法的比较 + +

+ +# 四、排序 + + **约定**
+ +待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 + +研究排序算法的成本模型时,计算的是比较和交换的次数。 + +使用辅助函数 less() 和 exch() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 + +```java +private boolean less(Comparable v, Comparable w){ + return v.compareTo(w) < 0; +} + +private void exch(Comparable[] a, int i, int j){ + Comparable t = a[i]; + a[i] = a[j]; + a[j] = t; +} +``` + +## 选择排序 + +找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 + +

+ +```java +public class Selection { + public static void sort(Comparable[] a) { + int N = a.length; + for (int i = 0; i < N; i++) { + int min = i; + for (int j = i + 1; j < N; j++) { + if (less(a[j], a[min])) min = j; + } + exch(a, i, min); + } + } +} +``` + +选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 + +## 插入排序 + +插入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。 + +

+ +```java +public class Insertion { + public static void sort(Comparable[] a) { + int N = a.length; + for (int i = 1; i < N; i++) { + for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) { + exch(a, j, j - 1); + } + } + } +} +``` + +插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么插入排序会很快。平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换,最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是逆序的;而最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 + +插入排序对于部分有序数组和小规模数组特别高效。 + + **选择排序和插入排序的比较**
+ +对于随机排列的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 + +## 希尔排序 + +对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 + +希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,使得元素更快的移到正确的位置上。 + +希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 + +

+ +

+ +```java +public class Shell { + public static void sort(Comparable[] a) { + int N = a.length; + int h = 1; + while (h < N / 3) { + h = 3 * h + 1; // 1, 4, 13, 40, ... + } + while (h >= 1) { + for (int i = h; i < N; i++) { + for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) { + exch(a, j, j - h); + } + } + h = h / 3; + } + } +} +``` + +希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 + +## 归并排序 + +归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 + +

+ +

+ +### 1. 归并方法 + +归并方法将数组中两个已经排序的部分归并成一个。 + +```java +public class MergeSort { + private static Comparable[] aux; + + private static void merge(Comparable[] a, int lo, int mid, int hi) { + int i = lo, j = mid + 1; + + for (int k = lo; k <= hi; k++) { + aux[k] = a[k]; // 将数据复制到辅助数组 + } + + for (int k = lo; k <= hi; k++) { + if (i > mid) a[k] = aux[j++]; + else if (j > hi) a[k] = aux[i++]; + else if (aux[i].compareTo(a[j]) < 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 + else a[k] = aux[j++]; + } + } +} +``` + +### 2. 自顶向下归并排序 + +

+ +```java +public static void sort(Comparable[] a) { + aux = new Comparable[a.length]; + sort(a, 0, a.length - 1); +} + +private static void sort(Comparable[] a, int lo, int hi) { + if (hi <= lo) return; + int mid = lo + (hi - lo) / 2; + sort(a, lo, mid); + sort(a, mid + 1, hi); + merge(a, lo, mid, hi); +} +``` + +因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。 + +小数组的递归操作会过于频繁,可以在数组过小时切换到插入排序来提高性能。 + +### 3. 自底向上归并排序 + +先归并那些微型数组,然后成对归并得到的子数组。 + +

+ +```java +public static void busort(Comparable[] a) { + int N = a.length; + aux = new Comparable[N]; + for (int sz = 1; sz < N; sz += sz) { + for (int lo = 0; lo < N - sz; lo += sz + sz) { + merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); + } + } +} +``` + +## 快速排序 + +### 1. 基本算法 + +归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 + +

+ +```java +public class QuickSort { + public static void sort(Comparable[] a) { + shuffle(a); + sort(a, 0, a.length - 1); + } + + private static void sort(Comparable[] a, int lo, int hi) { + if (hi <= lo) return; + int j = partition(a, lo, hi); + sort(a, lo, j - 1); + sort(a, j + 1, hi); + } +} +``` + +### 2. 切分 + +取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 + +

+ +```java +private static int partition(Comparable[] a, int lo, int hi) { + int i = lo, j = hi + 1; + Comparable v = a[lo]; + while (true) { + while (less(a[++i], v)) if (i == hi) break; + while (less(v, a[--j])) if (j == lo) break; + if (i >= j) break; + exch(a, i, j); + } + exch(a, lo, j); + return j; +} +``` + +### 3. 性能分析 + +快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 + +快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。 + +最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 + +### 4. 算法改进 + +**(一)切换到插入排序** + +因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 + +**(二)三取样** + +最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 + +**(三)三向切分** + +对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 + +三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。 + +

+ +```java +public class Quick3Way { + public static void sort(Comparable[] a, int lo, int hi) { + if (hi <= lo) return; + int lt = lo, i = lo + 1, gt = hi; + Comparable v = a[lo]; + while (i <= gt) { + int cmp = a[i].compareTo(v); + if (cmp < 0) exch(a, lt++, i++); + else if (cmp > 0) exch(a, i, gt--); + else i++; + } + sort(a, lo, lt - 1); + sort(a, gt + 1, hi); + } +} +``` + +## 优先队列 + +优先队列主要用于处理最大元素。 + +### 1. 堆 + +定义:一颗二叉树的每个节点都大于等于它的两个子节点。 + +堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。 + +

+ +```java +public class MaxPQ { + private Key[] pq; + private int N = 0; + + public MaxPQ(int maxN) { + pq = (Key[]) new Comparable[maxN + 1]; + } + + public boolean isEmpty() { + return N == 0; + } + + public int size() { + return N; + } + + private boolean less(int i, int j) { + return pq[i].compareTo(pq[j]) < 0; + } + + private void exch(int i, int j) { + Key t = pq[i]; + pq[i] = pq[j]; + pq[j] = t; + } +} +``` + +### 2. 上浮和下沉 + +在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 + +

+ +```java +private void swim(int k) { + while (k > 1 && less(k / 2, k)) { + exch(k / 2, k); + k = k / 2; + } +} +``` + +类似地,当一个节点比子节点来得小,也需要不断的向下比较和交换操作,把这种操作称为下沉。一个节点有两个子节点,应当与两个子节点中最大那么节点进行交换。 + +

+ +```java +private void sink(int k) { + while (2 * k <= N) { + int j = 2 * k; + if (j < N && less(j, j + 1)) j++; + if (!less(k, j)) break; + exch(k, j); + k = j; + } +} +``` + +### 3. 插入元素 + +将新元素放到数组末尾,然后上浮到合适的位置。 + +```java +public void insert(Key v) { + pq[++N] = v; + swim(N); +} +``` + +### 4. 删除最大元素 + +从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 + +```java +public Key delMax() { + Key max = pq[1]; + exch(1, N--); + pq[N + 1] = null; + sink(1); + return max; +} +``` + +### 5. 堆排序 + +由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 + +堆排序要分两个阶段,第一个阶段是把无序数组建立一个堆;第二个阶段是交换最大元素和当前堆的数组最后一个元素,并且进行下沉操作维持堆的有序状态。 + +无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 + +

+ +```java +public static void sort(Comparable[] a){ + int N = a.length; + for(int k = N/2; k >= 1; k--){ + sink(a, k, N); + } + while(N > 1){ + exch(a, 1, N--); + sink(a, 1, N); + } +} +``` + +### 6. 分析 + +一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。 + +对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。 + +堆排序时一种原地排序,没有利用额外的空间。 + +现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 + +## 应用 + +### 1. 排序算法的比较 + +

+ +快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 + +### 2. Java 的排序算法实现 + +Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 + +### 3. 基于切分的快速选择算法 + +快速排序的 partition() 方法,会返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 + +可以利用这个特性找出数组的第 k 个元素。 + +```java +public static Comparable select(Comparable[] a, int k) { + int lo = 0, hi = a.length - 1; + while (hi > lo) { + int j = partion(a, lo, hi); + if (j == k) return a[k]; + else if (j > k) hi = j - 1; + else lo = j + 1; + } + return a[k]; +} +``` + +该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 + +# 五、查找 + +符号表是一种存储键值对的数据结构,支持两种操作:插入一个新的键值对;根据给定键得到值。 + +## 符号表 + +### 1. 无序符号表 + +

+ +### 2. 有序符号表 + +

+ +有序指的是支持 min() max() 等根据键的大小关系来实现的操作。 + +有序符号表的键需要实现 Comparable 接口。 + +### 3. 二分查找实现有序符号表 + +使用一对平行数组,一个存储键一个存储值。其中键的数组为 Comparable 数组,值的数组为 Object 数组。 + +rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 + +复杂度:二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 + +```java +public class BinarySearchST, Value> { + private Key[] keys; + private Value[] values; + private int N; + + public BinarySearchST(int capacity) { + keys = (Key[]) new Comparable[capacity]; + values = (Value[]) new Object[capacity]; + } + + public int size() { + return N; + } + + public Value get(Key key) { + int i = rank(key); + if (i < N && keys[i].compareTo(key) == 0) { + return values[i]; + } + return null; + } + + public int rank(Key key) { + int lo = 0, hi = N - 1; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int cmp = key.compareTo(keys[mid]); + if (cmp == 0) return mid; + else if (cmp < 0) hi = mid - 1; + else lo = mid + 1; + } + return lo; + } + + public void put(Key key, Value value) { + int i = rank(key); + if (i < N && keys[i].compareTo(key) == 0) { + values[i] = value; + return; + } + for (int j = N; j > i; j--) { + keys[j] = keys[j - 1]; + values[j] = values[j - 1]; + } + keys[i] = key; + values[i] = value; + N++; + } + + public Key ceiling(Key key){ + int i = rank(key); + return keys[i]; + } +} +``` + +## 二叉查找树 + +**二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 + +

+ +**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于其左子树中的所有节点的值而小于右子树的所有节点的值。 + +

+ +BST 有一个重要性质,就是它的前序遍历结果递增排序。 + +

+ +基本数据结构: + +```java +public class BST, Value> { + private Node root; + + private class Node { + private Key key; + private Value val; + private Node left, right; + // 以该节点为根的子树中节点总数 + private int N; + + public Node(Key key, Value val, int N) { + this.key = key; + this.val = val; + this.N = N; + } + } + + public int size() { + return size(root); + } + + private int size(Node x) { + if (x == null) return 0; + return x.N; + } +} +``` + +### 1. get() + +- 如果树是空的,则查找未命中; +- 如果被查找的键和根节点的键相等,查找命中; +- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 + +BST 的查找操作每次递归都会让区间减少一半,和二分查找类似,因此查找的复杂度为 O(logN)。 + +```java +public Value get(Key key) { + return get(root, key); +} +private Value get(Node x, Key key) { + if (x == null) return null; + int cmp = key.compareTo(x.key); + if (cmp == 0) return x.val; + else if (cmp < 0) return get(x.left, key); + else return get(x.right, key); +} +``` + +### 2. put() + +当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。 + +

+ +```java +public void put(Key key, Value val) { + root = put(root, key, val); +} +private Node put(Node x, Key key, Value val) { + if (x == null) return new Node(key, val, 1); + int cmp = key.compareTo(x.key); + if (cmp == 0) x.val = val; + else if (cmp < 0) x.left = put(x.left, key, val); + else x.right = put(x.right, key, val); + x.N = size(x.left) + size(x.right) + 1; + return x; +} +``` + +### 3. 分析 + +二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。在最坏的情况下,树的高度为 N。 + +

+ +### 4. floor() + +floor(key):小于等于键的最大键 + +- 如果键小于根节点的键,那么 floor(key) 一定在左子树中; +- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就找到,否则根节点就是 floor(key)。 + +

+ +```java +public Key floor(Key key) { + Node x = floor(root, key); + if (x == null) return null; + return x.key; +} +private Node floor(Node x, Key key) { + if (x == null) return null; + int cmp = key.compareTo(x.key); + if (cmp == 0) return x; + if (cmp < 0) return floor(x.left, key); + Node t = floor(x.right, key); + if (t != null) { + return t; + } else { + return x; + } +} +``` + +### 5. rank() + +rank(key) 返回 key 的排名。 + +- 如果键和根节点的键相等,返回左子树的节点数; +- 如果小于,递归计算在左子树中的排名; +- 如果大于,递归计算在右子树中的排名,并加上左子树的节点数,再加上 1(根节点)。 + +```java +public int rank(Key key) { + return rank(key, root); +} +private int rank(Key key, Node x) { + if (x == null) return 0; + int cmp = key.compareTo(x.key); + if (cmp == 0) return size(x.left); + else if (cmp < 0) return rank(key, x.left); + else return 1 + size(x.left) + rank(key, x.right); +} +``` + +### 6. min() + +```java +private Node min(Node x) { + if (x.left == null) return x; + return min(x.left); +} +``` + +### 7. deleteMin() + +令指向最小节点的链接指向最小节点的右子树。 + +

+ +```java +public void deleteMin() { + root = deleteMin(root); +} +public Node deleteMin(Node x) { + if (x.left == null) return x.right; + x.left = deleteMin(x.left); + x.N = size(x.left) + size(x.right) + 1; + return x; +} +``` + +### 8. delete() + +- 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可; +- 否则,让右子树的最小节点替换该节点。 + +

+ +```java +public void delete(Key key) { + root = delete(root, key); +} +private Node delete(Node x, Key key) { + if (x == null) return null; + int cmp = key.compareTo(x.key); + if (cmp < 0) x.left = delete(x.left, key); + else if (cmp > 0) x.right = delete(x.right, key); + else { + if (x.right == null) return x.left; + if (x.left == null) return x.right; + Node t = x; + x = min(t.right); + x.right = deleteMin(t.right); + x.left = t.left; + } + x.N = size(x.left) + size(x.right) + 1; + return x; +} +``` + +### 9. keys() + +利用二叉查找树中序遍历的结果为递增的特点。 + +```java +public Iterable keys(Key lo, Key hi) { + Queue queue = new LinkedList<>(); + keys(root, queue, lo, hi); + return queue; +} +private void keys(Node x, Queue queue, Key lo, Key hi) { + if (x == null) return; + int cmpLo = lo.compareTo(x.key); + int cmpHi = hi.compareTo(x.key); + if (cmpLo < 0) keys(x.left, queue, lo, hi); + if (cmpLo <= 0 && cmpHi >= 0) queue.add(x.key); + if (cmpHi > 0) keys(x.right, queue, lo, hi); +} +``` + +### 10. 性能分析 + +复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 + +## 2-3 查找树 + +

+ +2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树更平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 + +### 1. 插入操作 + +插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。 + +根据叶子节点的类型不同,有不同的处理方式。 + +插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。 + +

+ +如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 + +

+ +### 2. 性质 + +2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 + +2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 + +

+ +## 红黑二叉查找树 + +2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 + +

+ +红黑树具有以下性质: + +1. 红链接都为左链接; +2. 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。 + +画红黑树时可以将红链接画平。 + +

+ +```java +public class RedBlackBST, Value> { + private Node root; + private static final boolean RED = true; + private static final boolean BLACK = false; + + private class Node { + Key key; + Value val; + Node left, right; + int N; + boolean color; + + Node(Key key, Value val, int n, boolean color) { + this.key = key; + this.val = val; + N = n; + this.color = color; + } + } + + private boolean isRed(Node x) { + if (x == null) return false; + return x.color == RED; + } +} +``` + +### 1. 左旋转 + +因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 + +

+ +

+ +```java +public Node rotateLeft(Node h) { + Node x = h.right; + h.right = x.left; + x.left = h; + x.color = h.color; + h.color = RED; + x.N = h.N; + h.N = 1 + size(h.left) + size(h.right); + return x; +} +``` + +### 2. 右旋转 + +进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 + +

+ +

+ +```java +public Node rotateRight(Node h) { + Node x = h.left; + h.left = x.right; + x.color = h.color; + h.color = RED; + x.N = h.N; + h.N = 1 + size(h.left) + size(h.right); + return x; +} +``` + +### 3. 颜色转换 + +一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 + +

+ +

+ +```java +void flipColors(Node h){ + h.color = RED; + h.left.color = BLACK; + h.right.color = BLACK; +} +``` + +### 4. 插入 + +先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: + +- 如果右子节点是红色的而左子节点是黑色的,进行左旋转; +- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转; +- 如果左右子节点均为红色的,进行颜色转换。 + +

+ +```java +public void put(Key key, Value val) { + root = put(root, key, val); + root.color = BLACK; +} + +private Node put(Node x, Key key, Value val) { + if (x == null) return new Node(key, val, 1, RED); + int cmp = key.compareTo(x.key); + if (cmp == 0) x.val = val; + else if (cmp < 0) x.left = put(x.left, key, val); + else x.right = put(x.right, key, val); + + if (isRed(x.right) && !isRed(x.left)) x = rotateLeft(x); + if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x); + if (isRed(x.left) && isRed(x.right)) flipColors(x); + + x.N = size(x.left) + size(x.right) + 1; + return x; +} +``` + +可以看到该插入操作和二叉查找树的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。 + +根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. + +### 5. 删除最小键 + +如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。 + +将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: + +1. 如果当前节点的左子节点不是 2- 节点,完成; +2. 如果当前节点的左子节点是 2- 节点而它的兄弟节点不是 2- 节点,向兄弟节点拿一个 key 过来; +3. 如果当前节点的左子节点和它的兄弟节点都是 2- 节点,将左子节点、父节点中的最小键和最近的兄弟节点合并为一个 4- 节点。 + +

+ +最后得到一个含有最小键的 3- 节点或者 4- 节点,直接从中删除。然后再从头分解所有临时的 4- 节点。 + +

+ +### 6. 分析 + +一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 + +红黑树大多数的操作所需要的时间都是对数级别的。 + +## 散列表 + +散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。 + +由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 + +### 1. 散列函数 + +对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 + +散列表有冲突的存在,也就是两个不同的键可能有相同的 hash 值。 + +散列函数应该满足以下三个条件: + +1. 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。 +2. 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。 +3. 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,这个条件至关重要,直接影响到散列表的性能。 + +除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 必须是一个素数,否则无法利用键包含的所有信息。例如 M 为 10k,那么只能利用键的后 k 位。 + +对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其表示成二进制形式,然后使用二进制形式的整数值进行除留余数法。 + +对于有多部分组合的键,每部分都需要计算 hash 值,并且最后合并时需要让每部分 hash 值都具有同等重要的地位。可以将该键看成 R 进制的整数,键中每部分都具有不同的权值。 + +例如,字符串的散列函数实现如下 + +```java +int hash = 0; +for(int i = 0; i < s.length(); i++) + hash = (R * hash + s.charAt(i)) % M; +``` + +再比如,拥有多个成员的自定义类的哈希函数如下: + +```java +int hash = (((day * R + month) % M) * R + year) % M; +``` + +R 通常取 31。 + +Java 中的 hashCode() 实现了 hash 函数,但是默认使用对象的内存地址值。在使用 hashCode() 函数时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 + +```java +int hash = (x.hashCode() & 0x7fffffff) % M; +``` + +使用 Java 自带的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode()。 + +```java +public class Transaction{ + private final String who; + private final Date when; + private final double amount; + + public int hashCode(){ + int hash = 17; + hash = 31 * hash + who.hashCode(); + hash = 31 * hash + when.hashCode(); + hash = 31 * hash + ((Double) amount).hashCode(); + return hash; + } +} +``` + +### 2. 基于拉链法的散列表 + +拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 + +

+ +对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 + +### 3. 基于线性探测法的散列表 + +线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 + +

+ +```java +public class LinearProbingHashST { + private int N; + private int M = 16; + private Key[] keys; + private Value[] vals; + + public LinearProbingHashST() { + init(); + } + + public LinearProbingHashST(int M) { + this.M = M; + init(); + } + + private void init() { + keys = (Key[]) new Object[M]; + vals = (Value[]) new Object[M]; + } + + private int hash(Key key) { + return (key.hashCode() & 0x7fffffff) % M; + } +} +``` + +**(一)查找** + +```java +public Value get(Key key) { + for (int i = hash(key); keys[i] != null; i = (i + 1) % M) { + if (keys[i].equals(key)) { + return vals[i]; + } + } + return null; +} +``` + +**(二)插入** + +```java +public void put(Key key, Value val) { + int i; + for (i = hash(key); keys[i] != null; i = (i + 1) % M) { + if (keys[i].equals(key)) { + vals[i] = val; + return; + } + } + keys[i] = key; + vals[i] = val; + N++; + resize(); +} +``` + +**(三)删除** + +删除操作应当将右侧所有相邻的键值对重新插入散列表中。 + +```java +public void delete(Key key) { + if (!contains(key)) return; + int i = hash(key); + while (!key.equals(keys[i])) { + i = (i + 1) % M; + } + keys[i] = null; + vals[i] = null; + i = (i + 1) % M; + while (keys[i] != null) { + Key keyToRedo = keys[i]; + Value valToRedo = vals[i]; + keys[i] = null; + vals[i] = null; + N--; + put(keyToRedo, valToRedo); + i = (i + 1) % M; + } + N--; + resize(); +} +``` + +**(四)调整数组大小** + +线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 + +α = N/M,把 α 称为利用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。 + +

+ +为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 + +```java +private void resize() { + if (N >= M / 2) resize(2 * M); + else if (N <= M / 8) resize(M / 2); +} + +private void resize(int cap) { + LinearProbingHashST t = new LinearProbingHashST<>(cap); + for (int i = 0; i < M; i++) { + if (keys[i] != null) { + t.put(keys[i], vals[i]); + } + } + keys = t.keys; + vals = t.vals; + M = t.M; +} +``` + +虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1,这是因为散列表中每个键都需要重新计算散列值。随后平均值会下降。 + +

+ +## 应用 + +### 1. 各种符号表实现的比较 + +

+ +应当优先考虑散列表,当需要有序性操作时使用红黑树。 + +### 2. Java 的符号表实现 + +- java.util.TreeMap:红黑树 +- java.util.HashMap:拉链法的散列表 + +### 3. 集合类型 + +除了符号表,集合类型也经常使用,它只有键没有值,可以用集合类型来存储一系列的键然后判断一个键是否在集合中。 + +### 4. 稀疏向量乘法 + +当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。 + +```java +public class SparseVector { + private HashMap hashMap; + + public SparseVector(double[] vector) { + hashMap = new HashMap<>(); + for (int i = 0; i < vector.length; i++) { + if (vector[i] != 0) { + hashMap.put(i, vector[i]); + } + } + } + + public double get(int i) { + return hashMap.getOrDefault(i, 0.0); + } + + public double dot(SparseVector other) { + double sum = 0; + for (int i : hashMap.keySet()) { + sum += this.get(i) * other.get(i); + } + return sum; + } +} +``` + diff --git a/notes/设计模式.md b/notes/设计模式.md new file mode 100644 index 00000000..35ba2440 --- /dev/null +++ b/notes/设计模式.md @@ -0,0 +1,1800 @@ + +* [一、前言](#一前言) +* [二、设计模式概念](#二设计模式概念) +* [三、策略模式](#三策略模式) +* [三、观察者模式](#三观察者模式) +* [四、装饰模式](#四装饰模式) +* [五、简单工厂](#五简单工厂) +* [六、工厂方法模式](#六工厂方法模式) +* [七、抽象工厂模式](#七抽象工厂模式) +* [八、单例模式](#八单例模式) +* [九、命令模式](#九命令模式) +* [十、适配器模式](#十适配器模式) +* [十、外观模式](#十外观模式) +* [十一、模板方法模式](#十一模板方法模式) +* [十二、迭代器模式](#十二迭代器模式) +* [十三、组合模式](#十三组合模式) +* [十四、状态模式](#十四状态模式) +* [十五、代理模式](#十五代理模式) +* [十六、MVC](#十六mvc) +* [十七、与设计模式相处](#十七与设计模式相处) + + + +# 一、前言 + +文中涉及一些 UML 类图,为了更好地理解,可以先阅读 [UML 类图](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E7%AC%AC%E4%B8%89%E7%AB%A0-uml)。 + +需要说明的一点是,文中的 UML 类图和规范的 UML 类图不大相同,其中组合关系使用以下箭头表示: + +

+ +# 二、设计模式概念 + +设计模式不是代码,而是解决问题的方案,学习现有的设计模式可以做到经验复用。 + +拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。 + +# 三、策略模式 + +## 模式定义 + +定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 + +## 问题描述 + +设计不同种类的鸭子拥有不同的叫声和飞行方式。 + +## 简单实现方案 + +使用继承的解决方案如下,这种方案代码无法复用,如果两个鸭子类拥有同样的飞行方式,就有两份重复的代码。 + +

+ +## 设计原则 + +**封装变化** :在这里变化的是鸭子叫和飞行的行为方式。 + +**针对接口编程,而不是针对实现编程** :变量声明的类型为父类,而不是具体的某个子类。父类中的方法实现不在父类,而是在各个子类。程序在运行时可以动态改变变量所指向的子类类型。 + +运用这一原则,将叫和飞行的行为抽象出来,实现多种不同的叫和飞行的子类,让子类去实现具体的叫和飞行方式。 + +

+ +**多用组合,少用继承** :组合也就是 HAS-A 关系,通过组合,可以在运行时动态改变实现,只要通过改变父类对象具体指向哪个子类即可。而继承就不能做到这些,继承体系在创建类时就已经确定。 + +运用这一原则,在 Duck 类中组合 FlyBehavior 和 QuackBehavior 类,performQuack() 和 performFly() 方法委托给这两个类去处理。通过这种方式,一个 Duck 子类可以根据需要去初始化 FlyBehavior 和 QuackBehavior 的子类对象,并且也可以动态地进行改变。 + +

+ +## 问题的解决方案类图 + +(可放大网页查看) + +

+ +## 代码实现 + +```java +public abstract class Duck { + FlyBehavior flyBehavior; + QuackBehavior quackBehavior; + + public Duck(){ + } + + public void performFly(){ + flyBehavior.fly(); + } + + public void setFlyBehavior(FlyBehavior fb){ + flyBehavior = fb; + } + + public void performQuack(){ + quackBehavior.quack(); + } + + public void setQuackBehavior(QuackBehavior qb){ + quackBehavior = qb; + } +} +``` +```java +public class MallardDuck extends Duck{ + public MallardDuck(){ + flyBehavior = new FlyWithWings(); + quackBehavior = new Quack(); + } +} +``` +```java +public interface FlyBehavior { + void fly(); +} +``` +```java +public class FlyNoWay implements FlyBehavior{ + @Override + public void fly() { + System.out.println("FlyBehavior.FlyNoWay"); + } +} +``` +```java +public class FlyWithWings implements FlyBehavior{ + @Override + public void fly() { + System.out.println("FlyBehavior.FlyWithWings"); + } +} +``` +```java +public interface QuackBehavior { + void quack(); +} +``` +```java +public class Quack implements QuackBehavior{ + @Override + public void quack() { + System.out.println("QuackBehavior.Quack"); + } +} +``` +```java +public class MuteQuack implements QuackBehavior{ + @Override + public void quack() { + System.out.println("QuackBehavior.MuteQuack"); + } +} +``` +```java +public class Squeak implements QuackBehavior{ + @Override + public void quack() { + System.out.println("QuackBehavior.Squeak"); + } +} +``` +```java +public class MiniDuckSimulator { + public static void main(String[] args) { + Duck mallardDuck = new MallardDuck(); + mallardDuck.performQuack(); + mallardDuck.performFly(); + mallardDuck.setFlyBehavior(new FlyNoWay()); + mallardDuck.performFly(); + } +} +``` +执行结果 +```html +QuackBehavior.Quack +FlyBehavior.FlyWithWings +FlyBehavior.FlyNoWay +``` + +# 三、观察者模式 + +## 模式定义 + +定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。 + +

+ +## 模式类图 + +主题具有注册和移除观察者、并通知所有注册者的功能,主题是通过维护一张观察者列表来实现这些操作的。 + +观察者拥有一个主题对象的引用,因为注册、移除观察者功能,还有数据都在主题当中,必须通过操作主题才能完成相应操作。 + +

+ +## 问题描述 + +天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 + +## 问题的解决方案类图 + +

+ +## 设计原则 + +为交互对象之间的松耦合设计而努力:当两个对象之间松耦合,它们依然可以交互,但是不清楚彼此的细节。由于松耦合的两个对象之间互相依赖程度很低,因此系统具有弹性,能够应对变化。 + +## 代码实现 + +```java +public interface Subject { + public void resisterObserver(Observer o); + public void removeObserver(Observer o); + public void notifyObserver(); +} +``` +```java + +public class WeatherData implements Subject { + private List observers; + private float temperature; + private float humidity; + private float pressure; + + public WeatherData() { + observers = new ArrayList<>(); + } + + @Override + public void resisterObserver(Observer o) { + observers.add(o); + } + + @Override + public void removeObserver(Observer o) { + int i = observers.indexOf(o); + if (i >= 0) { + observers.remove(i); + } + } + + @Override + public void notifyObserver() { + for (Observer o : observers) { + o.update(temperature, humidity, pressure); + } + } + + public void setMeasurements(float temperature, float humidity, float pressure) { + this.temperature = temperature; + this.humidity = humidity; + this.pressure = pressure; + notifyObserver(); + } +} +``` +```java +public interface Observer { + public void update(float temp, float humidity, float pressure); +} +``` +```java +public class CurrentConditionsDisplay implements Observer { + private Subject weatherData; + + public CurrentConditionsDisplay(Subject weatherData) { + this.weatherData = weatherData; + weatherData.resisterObserver(this); + } + + @Override + public void update(float temp, float humidity, float pressure) { + System.out.println("CurrentConditionsDisplay.update:" + temp + " " + humidity + " " + pressure); + } +} +``` +```java +public class StatisticsDisplay implements Observer { + private Subject weatherData; + + public StatisticsDisplay(Subject weatherData) { + this.weatherData = weatherData; + weatherData.resisterObserver(this); + } + + @Override + public void update(float temp, float humidity, float pressure) { + System.out.println("StatisticsDisplay.update:" + temp + " " + humidity + " " + pressure); + } +} +``` +```java +public class WeatherStation { + public static void main(String[] args) { + WeatherData weatherData = new WeatherData(); + CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData); + StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData); + + weatherData.setMeasurements(0, 0, 0); + weatherData.setMeasurements(1, 1, 1); + } +} +``` +执行结果 +```html +CurrentConditionsDisplay.update:0.0 0.0 0.0 +StatisticsDisplay.update:0.0 0.0 0.0 +CurrentConditionsDisplay.update:1.0 1.0 1.0 +StatisticsDisplay.update:1.0 1.0 1.0 +``` + +# 四、装饰模式 + +## 问题描述 + +设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。 + +## 模式定义 + +动态地将责任附加到对象上。在扩展功能上,装饰者提供了比继承更有弹性的替代方案。 + +下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。 + +

+ +## 模式类图 + +装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。 + +

+ +## 问题的解决方案类图 + +

+ +## 设计原则 + +类应该对扩展开放,对修改关闭:也就是添加新功能时不需要修改代码。在本章问题中该原则体现在,饮料可以动态添加新的配料,而不需要去修改饮料的代码。观察者模式也符合这个原则。不可能把所有的类设计成都满足这一原则,应当把该原则应用于最有可能发生改变的地方。 + +## Java I/O 中的装饰者模式 + +

+ +## 代码实现 + +```java +public interface Beverage { + public double cost(); +} +``` +```java +public class HouseBlend implements Beverage{ + @Override + public double cost() { + return 1; + } +} +``` +```java +public class DarkRoast implements Beverage{ + @Override + public double cost() { + return 1; + } +} +``` +```java +public abstract class CondimentDecorator implements Beverage{ + protected Beverage beverage; +} +``` +```java +public class Mocha extends CondimentDecorator { + + public Mocha(Beverage beverage) { + this.beverage = beverage; + } + + @Override + public double cost() { + return 1 + beverage.cost(); + } +} +``` +```java +public class Milk extends CondimentDecorator { + + public Milk(Beverage beverage) { + this.beverage = beverage; + } + + @Override + public double cost() { + return 1 + beverage.cost(); + } +} +``` +```java +public class StartbuzzCoffee { + public static void main(String[] args) { + Beverage beverage = new HouseBlend(); + beverage = new Mocha(beverage); + beverage = new Milk(beverage); + System.out.println(beverage.cost()); + } +} +``` + +输出 + +```html +3.0 +``` + +# 五、简单工厂 + +## 问题描述 + +Pizza 类有很多子类,要求根据不同的情况用不同的子类实例化一个 Pizza 对象。 + +## 模式定义 + +简单工厂不是设计模式,更像是一种编程习惯。它把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个子类来实例化。 + +这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。 + +

+ +## 问题的解决方案类图 + +

+ +## 代码实现 + +```java +public interface Pizza { + public void make(); +} +``` + +```java +public class CheesePizza implements Pizza{ + @Override + public void make() { + System.out.println("CheesePizza"); + } +} +``` + +```java +public class GreekPizza implements Pizza{ + @Override + public void make() { + System.out.println("GreekPizza"); + } +} +``` + +```java +public class SimplePizzaFactory { + public Pizza createPizza(String type) { + if (type.equals("cheese")) { + return new CheesePizza(); + } else if (type.equals("greek")) { + return new GreekPizza(); + } else { + throw new UnsupportedOperationException(); + } + } +} +``` + +```java +public class PizzaStore { + public static void main(String[] args) { + SimplePizzaFactory simplePizzaFactory = new SimplePizzaFactory(); + Pizza pizza = simplePizzaFactory.createPizza("cheese"); + pizza.make(); + } +} +``` + +运行结果 + +```java +CheesePizza +``` + +# 六、工厂方法模式 + +## 问题描述 + +每个地区的 PizzaStore 卖的 Pizza 虽然种类相同,但是都有自己的风味。一个客户点了纽约的 cheese 种类的 Pizza 和在芝加哥点的相同种类的 Pizza 是不同的。要求设计出满足条件的 PizzaStore。 + +## 模式定义 + +定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化推迟到子类。 + +## 模式类图 + +在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。 + +下图中,Creator 有一个 anOperation() 方法,这个方法需要用到一组产品对象,这组产品对象由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。 + +

+ +## 问题的解决方案类图 + +PizzaStore 有 orderPizza() 方法,顾客可以用它来下单。下单之后需要先使用 createPizza() 来制作 Pizza,这里的 createPizza() 就是 factoryMethod(),不同的 PizzaStore 子类实现了不同的 createPizza()。 + +

+ +## 设计原则 + +依赖倒置原则:要依赖抽象,不要依赖具体类。听起来像是针对接口编程,不针对实现编程,但是这个原则说明了:不能让高层组件依赖底层组件,而且,不管高层或底层组件,两者都应该依赖于抽象。例如,下图中 Pizza 是抽象类,PizzaStore 和 Pizza 子类都依赖于 Pizza 这个抽象类。 + +

+ +## 代码实现 + +```java +public interface Pizza { + public void make(); +} +``` +```java +public interface PizzaStore { + public Pizza orderPizza(String item); +} +``` +```java +public class NYStyleCheesePizza implements Pizza{ + @Override + public void make() { + System.out.println("NYStyleCheesePizza is making.."); + } +} +``` +```java +public class NYStyleVeggiePizza implements Pizza { + @Override + public void make() { + System.out.println("NYStyleVeggiePizza is making.."); + } +} +``` +```java +public class ChicagoStyleCheesePizza implements Pizza{ + @Override + public void make() { + System.out.println("ChicagoStyleCheesePizza is making.."); + } +} +``` +```java +public class ChicagoStyleVeggiePizza implements Pizza{ + @Override + public void make() { + System.out.println("ChicagoStyleVeggiePizza is making.."); + } +} +``` +```java +public class NYPizzaStore implements PizzaStore { + @Override + public Pizza orderPizza(String item) { + Pizza pizza = null; + if (item.equals("cheese")) { + pizza = new NYStyleCheesePizza(); + } else if (item.equals("veggie")) { + pizza = new NYStyleVeggiePizza(); + } else { + throw new UnsupportedOperationException(); + } + pizza.make(); + return pizza; + } +} +``` +```java +public class ChicagoPizzaStore implements PizzaStore { + @Override + public Pizza orderPizza(String item) { + Pizza pizza = null; + if (item.equals("cheese")) { + pizza = new ChicagoStyleCheesePizza(); + } else if (item.equals("veggie")) { + pizza = new ChicagoStyleVeggiePizza(); + } else { + throw new UnsupportedOperationException(); + } + pizza.make(); + return pizza; + } +} +``` +```java +public class PizzaTestDrive { + public static void main(String[] args) { + PizzaStore nyStore = new NYPizzaStore(); + nyStore.orderPizza("cheese"); + PizzaStore chicagoStore = new ChicagoPizzaStore(); + chicagoStore.orderPizza("cheese"); + } +} +``` + +运行结果 + +```html +NYStyleCheesePizza is making.. +ChicagoStyleCheesePizza is making.. +``` + +# 七、抽象工厂模式 + +## 模式定义 + +提供一个接口,用于创建 **相关的对象家族** 。 + +## 模式类图 + +抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 + +抽象工厂模式用到了工厂模式来创建单一对象,在类图左部,AbstractFactory 中的 CreateProductA 和 CreateProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。 + +至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。 + +从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂模式使用了继承。 + +

+ +## 解决方案类图 + +

+ +## 代码实现 + +```java +public interface Dough { + public String doughType(); +} +``` +```java +public class ThickCrustDough implements Dough{ + + @Override + public String doughType() { + return "ThickCrustDough"; + } +} +``` +```java +public class ThinCrustDough implements Dough { + @Override + public String doughType() { + return "ThinCrustDough"; + } +} +``` +```java +public interface Sauce { + public String sauceType(); +} +``` +```java +public class MarinaraSauce implements Sauce { + @Override + public String sauceType() { + return "MarinaraSauce"; + } +} +``` +```java +public class PlumTomatoSauce implements Sauce { + @Override + public String sauceType() { + return "PlumTomatoSauce"; + } +} +``` +```java +public interface PizzaIngredientFactory { + public Dough createDough(); + public Sauce createSauce(); +} +``` +```java +public class NYPizzaIngredientFactory implements PizzaIngredientFactory{ + @Override + public Dough createDough() { + return new ThickCrustDough(); + } + + @Override + public Sauce createSauce() { + return new MarinaraSauce(); + } +} +``` +```java +public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory{ + @Override + public Dough createDough() { + return new ThinCrustDough(); + } + + @Override + public Sauce createSauce() { + return new PlumTomatoSauce(); + } +} +``` +```java +public class NYPizzaStore { + private PizzaIngredientFactory ingredientFactory; + + public NYPizzaStore() { + ingredientFactory = new NYPizzaIngredientFactory(); + } + + public void makePizza() { + Dough dough = ingredientFactory.createDough(); + Sauce sauce = ingredientFactory.createSauce(); + System.out.println(dough.doughType()); + System.out.println(sauce.sauceType()); + } +} +``` +```java +public class NYPizzaStoreTestDrive { + public static void main(String[] args) { + NYPizzaStore nyPizzaStore = new NYPizzaStore(); + nyPizzaStore.makePizza(); + } +} +``` + +运行结果 + +```html +ThickCrustDough +MarinaraSauce +``` + +# 八、单例模式 + +## 模式定义 + +确保一个类只有一个实例,并提供了一个全局访问点。 + +## 模式类图 + +使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。 + +私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。 + +

+ +## 懒汉式-线程不安全 + +以下实现中,私有静态变量 uniqueInstance 被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 + +这个实现在多线程环境下是不安全的,如果多个线程能够同时进入`if(uniqueInstance == null)` ,那么就会多次实例化 uniqueInstance。 + +```java +public class Singleton { + + private static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + return uniqueInstance; + } +} +``` + +## 懒汉式-线程安全 + +只需要对 `getUniqueInstance()` 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了对 uniqueInstance 进行多次实例化的问题。 + +但是这样有一个问题,就是当一个线程进入该方法之后,其它线程试图进入该方法都必须等待,因此性能上有一定的损耗。 + +```java +public static synchronized Singleton getUniqueInstance() { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + return uniqueInstance; +} +``` + +## 饿汉式-线程安全 + +线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。 + +```java +private static Singleton uniqueInstance = new Singleton(); +``` + +## 双重校验锁-线程安全 + +uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行。也就是说,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 + +双重校验锁先判断 uniqueInstance 是否已经被初始化了,如果没有被实例化,那么才对实例化语句进行加锁。 + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + if (uniqueInstance == null) { + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行`uniqueInstance = new Singleton();`这条语句,只是早晚的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 判断。 + +```java +if (uniqueInstance == null) { + synchronized (Singleton.class) { + uniqueInstance = new Singleton(); + } +} +``` + +# 九、命令模式 + +## 问题描述 + +设计一个遥控器,它有很多按钮,每个按钮可以发起一个命令,命令会让一个家电完成相应操作。 + +

+ +有非常多的家电,并且之后会增加家电。 + +

+ +## 模式定义 + +将命令封装成对象,以便使用不同的命令来参数化其它对象。 + +## 问题的解决方案类图 + +- RemoteControl 是遥控器,它可以为每个按钮设置命令对象,并且执行命令。 + +- Command 是命令对象。 + +- Light(电灯)是命令真正的执行者。 + +- RemoteLoader 是客户端,应该注意它与 RemoteControl 的区别。因为 RemoteControl 不能主动地调用自身的方法,因此也就不能当成是客户端。客户端好比人,只有人才能去真正去使用遥控器。 + +

+ +## 模式类图 + +

+ +## 代码实现 + +```java +public interface Command { + public void execute(); +} +``` + +```java +public class Light { + + public void on() { + System.out.println("Light is on!"); + } + + public void off() { + System.out.println("Light is off!"); + } +} +``` + +```java +public class LightOnCommand implements Command{ + Light light; + + public LightOnCommand(Light light) { + this.light = light; + } + + @Override + public void execute() { + light.on(); + } +} +``` + +```java +/** + * 遥控器类 + */ +public class SimpleRemoteControl { + Command slot; + + public SimpleRemoteControl() { + + } + + public void setCommand(Command command) { + this.slot = command; + } + + public void buttonWasPressed() { + slot.execute(); + } + +} +``` + +```java +/** + * 客户端 + */ +public class RemoteLoader { + public static void main(String[] args) { + SimpleRemoteControl remote = new SimpleRemoteControl(); + Light light = new Light(); + LightOnCommand lightOnCommand = new LightOnCommand(light); + remote.setCommand(lightOnCommand); + remote.buttonWasPressed(); + } +} +``` + +输出 + +```html +Light is on! +``` + +# 十、适配器模式 + +## 模式定义 + +将一个类的接口,转换为客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。 + +

+ +## 模式类图 + +适配器(Adapter)组合一个适配者(Adaptee),Adapter 把操作委托给 Adaptee。 + +

+ +## 问题描述 + +鸭子(Duck)和火鸡(Turkey)拥有不同的叫声,Duck 的叫声调用 quack() 方法,而 Turkey 调用 gobble() 方法。 + +要求将 Turkey 的 gobble() 方法适配成 Duck 的 quack() 方法,从而让火鸡冒充鸭子! + +## 问题的解决方案类图 + +

+ +## 代码实现 + +```java +public interface Duck { + public void quack(); +} +``` + +```java +public interface Turkey { + public void gobble(); +} +``` + +```java +public class WildTurkey implements Turkey{ + @Override + public void gobble() { + System.out.println("gobble!"); + } +} +``` + +```java +public class TurkeyAdapter implements Duck{ + Turkey turkey; + + public TurkeyAdapter(Turkey turkey) { + this.turkey = turkey; + } + + @Override + public void quack() { + turkey.gobble(); + } +} +``` + +```java +public class DuckTestDrive { + public static void main(String[] args) { + Turkey turkey = new WildTurkey(); + Duck duck = new TurkeyAdapter(turkey); + duck.quack(); + } +} +``` + +运行结果 + +```html +gobble! +``` + +# 十、外观模式 + +## 模式定义 + +提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。 + +## 模式类图 + +

+ +## 问题描述 + +家庭影院中有众多电器,当要进行观看电影时需要对很多电器进行操作。要求简化这些操作,使得家庭影院类只提供一个简化的接口,例如提供一个看电影相关的接口。 + +

+ +## 解决方案类图 + +

+ +## 设计原则 + +**最少知识原则** :只和你的密友谈话。也就是客户对象所需要交互的对象应当尽可能少。 + +## 代码实现 + +过于简单,无实现。 + +# 十一、模板方法模式 + +## 模式定义 + +在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。 + +模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 + +## 模式类图 + +模板方法 templateMethod() 定义了算法的骨架,确定了 primitiveOperation1() 和 primitiveOperation2() 方法执行的顺序,而 primitiveOperation1() 和 primitiveOperation2() 让子类去实现。 + +

+ +## 问题描述 + +冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。 + +

+ +## 问题的解决方案类图 + +prepareRecipe() 方法就是模板方法,它确定了其它四个方法的具体执行步骤。其中 brew() 和 addCondiments() 方法在子类中实现。 + +

+ +## 设计原则 + +**好莱坞原则** :别调用(打电话给)我们,我们会调用(打电话给)你。这一原则可以防止依赖腐败,即防止高层组件依赖底层组件,底层组件又依赖高层组件。该原则在模板方法的体现为,只有父类会调用子类,子类不会调用父类。 + +## 钩子 + +某些步骤在不同实现中可有可无,可以先定义一个什么都不做的方法,把它加到模板方法中,如果子类需要它就覆盖默认实现并加上自己的实现。 + +## 代码实现 + +```java +public abstract class CaffeineBeverage { + + final void prepareRecipe(){ + boilWater(); + brew(); + pourInCup(); + addCondiments(); + } + + abstract void brew(); + + abstract void addCondiments(); + + void boilWater(){ + System.out.println("boilWater"); + } + + void pourInCup(){ + System.out.println("pourInCup"); + } +} +``` + +```java +public class Coffee extends CaffeineBeverage{ + @Override + void brew() { + System.out.println("Coffee.brew"); + } + + @Override + void addCondiments() { + System.out.println("Coffee.addCondiments"); + } +} +``` + +```java +public class Tea extends CaffeineBeverage{ + @Override + void brew() { + System.out.println("Tea.brew"); + } + + @Override + void addCondiments() { + System.out.println("Tea.addCondiments"); + } +} +``` + +```java +public class CaffeineBeverageTestDrive { + public static void main(String[] args) { + CaffeineBeverage caffeineBeverage = new Coffee(); + caffeineBeverage.prepareRecipe(); + System.out.println("-----------"); + caffeineBeverage = new Tea(); + caffeineBeverage.prepareRecipe(); + } +} +``` + +运行结果 + +```html +boilWater +Coffee.brew +pourInCup +Coffee.addCondiments +----------- +boilWater +Tea.brew +pourInCup +Tea.addCondiments +``` + +# 十二、迭代器模式 + +## 模式定义 + +提供顺序访问一个聚合对象中的各个元素的方法,而又不暴露聚合对象内部的表示。 + +## 模式类图 + +- Aggregate 是聚合类,其中 createIterator() 方法可以产生一个 Iterator; + +- Iterator 主要定义了 hasNext() 和 next() 方法。 + +- Client 组合了 Aggregate,为了迭代遍历 Aggregate,也需要组合 Iterator。 + +

+ +## 代码实现 + +```java +public class Aggregate { + + private int[] items; + + public Aggregate() { + items = new int[10]; + for (int i = 0; i < items.length; i++) { + items[i] = i; + } + } + + public Iterator createIterator() { + return new ConcreteIterator(items); + } + +} +``` + +```java +public interface Iterator { + boolean hasNext(); + int next(); +} +``` + +```java +public class ConcreteIterator implements Iterator { + + private int[] items; + private int position = 0; + + public ConcreteIterator(int[] items) { + this.items = items; + } + + @Override + public boolean hasNext() { + return position < items.length; + } + + @Override + public int next() { + return items[position++]; + } +} +``` +```java +public class Client { + public static void main(String[] args) { + Aggregate aggregate = new Aggregate(); + Iterator iterator = aggregate.createIterator(); + while(iterator.hasNext()){ + System.out.println(iterator.next()); + } + } +} +``` +运行结果 +```html +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +``` + +## Java 内置的迭代器 + +需要让聚合类实现 Iterable 接口,该接口有一个 iterator() 方法会返回一个 Iterator 对象。 + +可以使用 foreach 循环来顺序访问聚合对象中的每个元素。 + +Java 中的集合类基本都实现了 Iterable 接口。 + +```java +import java.util.Iterator; + +public class Aggregate implements Iterable{ + + private int[] items; + + public Aggregate() { + items = new int[10]; + for (int i = 0; i < items.length; i++) { + items[i] = i; + } + } + + @Override + public Iterator iterator() { + return new ConcreteIterator(items); + } +} +``` +```java +import java.util.Iterator; + +public class ConcreteIterator implements Iterator { + + private int[] items; + private int position = 0; + + public ConcreteIterator(int[] items) { + this.items = items; + } + + @Override + public boolean hasNext() { + return position < items.length; + } + + @Override + public Integer next() { + return items[position++]; + } +} +``` +```java +public class Client { + public static void main(String[] args) { + Aggregate aggregate = new Aggregate(); + for (int item : aggregate) { + System.out.println(item); + } + } +} +``` + +# 十三、组合模式 + +## 设计原则 + +一个类应该只有一个引起它改变的原因。 + +## 模式定义 + +允许将对象组合成树形结构来表现“整体/部分”关系。 + +组合能让客户以一致的方式处理个别对象以及组合对象。 + +## 模式类图 + +组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。 + +组合对象拥有一个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。 + +

+ +## 代码实现 + +```java +public abstract class Component { + protected String name; + + public Component(String name) { + this.name = name; + } + + abstract public void addChild(Component component); + + public void print() { + print(0); + } + + abstract protected void print(int level); +} +``` + +```java +public class Leaf extends Component { + public Leaf(String name) { + super(name); + } + + @Override + public void addChild(Component component) { + throw new UnsupportedOperationException(); // 牺牲透明性换取单一职责原则,这样就不用考虑是叶子节点还是组合节点 + } + + @Override + protected void print(int level) { + for (int i = 0; i < level; i++) { + System.out.print("--"); + } + System.out.println("left:" + name); + } +} +``` + +```java +public class Composite extends Component { + + private List childs; + + public Composite(String name) { + super(name); + childs = new ArrayList<>(); + } + + @Override + public void addChild(Component component) { + childs.add(component); + } + + @Override + protected void print(int level) { + for (int i = 0; i < level; i++) { + System.out.print("--"); + } + System.out.println("Composite:" + name); + for (Component component : childs) { + component.print(level + 1); + } + } +} +``` + +```java +public class Client { + public static void main(String[] args) { + Composite root = new Composite("root"); + Component node1 = new Leaf("1"); + Component node2 = new Composite("2"); + Component node3 = new Leaf("3"); + root.addChild(node1); + root.addChild(node2); + root.addChild(node3); + Component node21 = new Leaf("21"); + Component node22 = new Composite("22"); + node2.addChild(node21); + node2.addChild(node22); + Component node221 = new Leaf("221"); + node22.addChild(node221); + root.print(); + } +} +``` +运行结果 + +```html +Composite:root +--left:1 +--Composite:2 +----left:21 +----Composite:22 +------left:221 +--left:3 +``` + +# 十四、状态模式 + +## 模式定义 + +允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。 + +## 模式类图 + +Context 的 request() 方法委托给 State 对象去处理。当 Context 组合的 State 对象发生改变时,它的行为也就发生了改变。 + +

+ +## 与策略模式的比较 + +状态模式的类图和策略模式一样,并且都是能够动态改变对象的行为。 + +但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。 + +所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。 + +状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。 + +## 问题描述 + +糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。 + +

+ +## 直接解决方案 + +在糖果机的每个操作函数里面,判断当前的状态,根据不同的状态进行不同的处理,并且发生不同的状态转移。 + +这种解决方案在需要增加状态的时候,必须对每个操作的代码都进行修改。 + +

+ +## 代码实现 + +糖果销售机即 Context。 + +下面的实现中每个 State 都组合了 Context 对象,这是因为状态转移的操作在 State 对象中,而状态转移过程又必须改变 Context 对象的 state 对象,因此 State 必须组合 Context 对象。 + +```java +public interface State { + /** + * 投入 25 分钱 + */ + void insertQuarter(); + + /** + * 退回 25 分钱 + */ + void ejectQuarter(); + + /** + * 转动曲柄 + */ + void turnCrank(); + + /** + * 发放糖果 + */ + void dispense(); +} +``` +```java +public class HasQuarterState implements State{ + + private GumballMachine gumballMachine; + + public HasQuarterState(GumballMachine gumballMachine){ + this.gumballMachine = gumballMachine; + } + + @Override + public void insertQuarter() { + System.out.println("You can't insert another quarter"); + } + + @Override + public void ejectQuarter() { + System.out.println("Quarter returned"); + gumballMachine.setState(gumballMachine.getNoQuarterState()); + } + + @Override + public void turnCrank() { + System.out.println("You turned..."); + gumballMachine.setState(gumballMachine.getSoldState()); + } + + @Override + public void dispense() { + System.out.println("No gumball dispensed"); + } +} +``` +```java +public class NoQuarterState implements State { + + GumballMachine gumballMachine; + + public NoQuarterState(GumballMachine gumballMachine) { + this.gumballMachine = gumballMachine; + } + + @Override + public void insertQuarter() { + System.out.println("You insert a quarter"); + gumballMachine.setState(gumballMachine.getHasQuarterState()); + } + + @Override + public void ejectQuarter() { + System.out.println("You haven't insert a quarter"); + } + + @Override + public void turnCrank() { + System.out.println("You turned, but there's no quarter"); + } + + @Override + public void dispense() { + System.out.println("You need to pay first"); + } +} +``` +```java +public class SoldOutState implements State { + + GumballMachine gumballMachine; + + public SoldOutState(GumballMachine gumballMachine) { + this.gumballMachine = gumballMachine; + } + + @Override + public void insertQuarter() { + System.out.println("You can't insert a quarter, the machine is sold out"); + } + + @Override + public void ejectQuarter() { + System.out.println("You can't eject, you haven't inserted a quarter yet"); + } + + @Override + public void turnCrank() { + System.out.println("You turned, but there are no gumballs"); + } + + @Override + public void dispense() { + System.out.println("No gumball dispensed"); + } +} +``` +```java +public class SoldState implements State { + + GumballMachine gumballMachine; + + public SoldState(GumballMachine gumballMachine) { + this.gumballMachine = gumballMachine; + } + + @Override + public void insertQuarter() { + System.out.println("Please wait, we're already giving you a gumball"); + } + + @Override + public void ejectQuarter() { + System.out.println("Sorry, you already turned the crank"); + } + + @Override + public void turnCrank() { + System.out.println("Turning twice doesn't get you another gumball!"); + } + + @Override + public void dispense() { + gumballMachine.releaseBall(); + if(gumballMachine.getCount()>0){ + gumballMachine.setState(gumballMachine.getNoQuarterState()); + } else{ + System.out.println("Oops, out of gumballs"); + gumballMachine.setState(gumballMachine.getSoldOutState()); + } + } +} +``` +```java +public class GumballMachine { + + private State soldOutState; + private State noQuarterState; + private State hasQuarterState; + private State soldState; + + private State state; + private int count = 0; + + public GumballMachine(int numberGumballs) { + count = numberGumballs; + soldOutState = new SoldOutState(this); + noQuarterState = new NoQuarterState(this); + hasQuarterState = new HasQuarterState(this); + soldState = new SoldState(this); + + if (numberGumballs > 0) { + state = noQuarterState; + } else { + state = soldOutState; + } + } + + public void insertQuarter() { + state.insertQuarter(); + } + + public void ejectQuarter() { + state.ejectQuarter(); + } + + public void turnCrank() { + state.turnCrank(); + state.dispense(); + } + + public void setState(State state) { + this.state = state; + } + + public void releaseBall() { + System.out.println("A gumball comes rolling out the slot..."); + if (count != 0) { + count -= 1; + } + } + + public State getSoldOutState() { + return soldOutState; + } + + public State getNoQuarterState() { + return noQuarterState; + } + + public State getHasQuarterState() { + return hasQuarterState; + } + + public State getSoldState() { + return soldState; + } + + public int getCount() { + return count; + } +} +``` +```java +public class GumballMachineTestDrive { + + public static void main(String[] args) { + GumballMachine gumballMachine = new GumballMachine(5); + + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + + gumballMachine.insertQuarter(); + gumballMachine.ejectQuarter(); + gumballMachine.turnCrank(); + + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + gumballMachine.ejectQuarter(); + + gumballMachine.insertQuarter(); + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + gumballMachine.insertQuarter(); + gumballMachine.turnCrank(); + } +} +``` +运行结果 +```html +You insert a quarter +You turned... +A gumball comes rolling out the slot... +You insert a quarter +Quarter returned +You turned, but there's no quarter +You need to pay first +You insert a quarter +You turned... +A gumball comes rolling out the slot... +You insert a quarter +You turned... +A gumball comes rolling out the slot... +You haven't insert a quarter +You insert a quarter +You can't insert another quarter +You turned... +A gumball comes rolling out the slot... +You insert a quarter +You turned... +A gumball comes rolling out the slot... +Oops, out of gumballs +You can't insert a quarter, the machine is sold out +You turned, but there are no gumballs +No gumball dispensed +``` + +# 十五、代理模式 + +# 十六、MVC + +## 传统 MVC + +视图使用组合模式,模型使用了观察者模式,控制器使用了策略模式。 + +

+ +## Web 中的 MVC + +模式不再使用观察者模式。 + +

+ +# 十七、与设计模式相处 + +## 定义 + +在某情境下,针对某问题的某种解决方案。 + +## 何时使用 + +过度使用设计模式可能导致代码被过度工程化,应该总是用最简单的解决方案完成工作,并在真正需要模式的地方才使用它。 + +## 反模式 + +不好的解决方案来解决一个问题。主要作用是为了警告人们不要使用这些解决方案。 + +## 模式分类 + +