算法.md 77.0 KB
Newer Older
C
CyC2018 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# 一、前言

- 实现代码:[Algorithm](https://github.com/CyC2018/Algorithm)
- 绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727)

# 二、算法分析

## 数学模型

### 1. 近似

N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 ~ N<sup>3</sup>/6。使用 ~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数。

### 2. 增长数量级

N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 的增长数量级为 O(N<sup>3</sup>)。增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 O(N<sup>3</sup>) 与它是否用 Java 实现,是否运行于特定计算机上无关。

### 3. 内循环
C
CyC2018 已提交
19 20 21

执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。

C
CyC2018 已提交
22
### 4. 成本模型
C
CyC2018 已提交
23 24 25

使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。

C
CyC2018 已提交
26
## 注意事项
C
CyC2018 已提交
27

C
CyC2018 已提交
28
### 1. 大常数
C
CyC2018 已提交
29 30 31

在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。

C
CyC2018 已提交
32
### 2. 缓存
C
CyC2018 已提交
33 34 35

计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。

C
CyC2018 已提交
36
### 3. 对最坏情况下的性能的保证
C
CyC2018 已提交
37 38 39

在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。

C
CyC2018 已提交
40
### 4. 随机化算法
C
CyC2018 已提交
41 42 43

通过打乱输入,去除算法对输入的依赖。

C
CyC2018 已提交
44
### 5. 均摊分析
C
CyC2018 已提交
45

C
CyC2018 已提交
46
将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的次数为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素的操作次数,其余的都是调整数组大小时进行复制需要的访问数组次数),均摊后访问数组的平均次数为常数。
C
CyC2018 已提交
47

C
CyC2018 已提交
48
## ThreeSum
C
CyC2018 已提交
49

C
CyC2018 已提交
50
ThreeSum 用于统计一个数组中和为 0 的三元组数量。
C
CyC2018 已提交
51 52

```java
C
CyC2018 已提交
53 54
public interface ThreeSum {
    int count(int[] nums);
C
CyC2018 已提交
55 56 57
}
```

C
CyC2018 已提交
58
### 1. ThreeSumSlow
C
CyC2018 已提交
59

C
CyC2018 已提交
60
该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N<sup>3</sup>/6-N<sup>2</sup>/2+N/3,因此它的近似执行次数为 ~N<sup>3</sup>/6,增长数量级为 O(N<sup>3</sup>)。
C
CyC2018 已提交
61

C
CyC2018 已提交
62
```java
C
CyC2018 已提交
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
public class ThreeSumSlow implements ThreeSum {
    @Override
    public int count(int[] nums) {
        int N = nums.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 (nums[i] + nums[j] + nums[k] == 0) {
                        cnt++;
                    }
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
79 80 81
}
```

C
CyC2018 已提交
82
### 2. ThreeSumBinarySearch
C
CyC2018 已提交
83

C
CyC2018 已提交
84
通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。
C
CyC2018 已提交
85

C
CyC2018 已提交
86 87
应该注意的是,只有数组不含有相同元素才能使用这种解法,否则二分查找的结果会出错。

C
CyC2018 已提交
88
该方法可以将 ThreeSum 算法增长数量级降低为 O(N<sup>2</sup>logN)。
C
CyC2018 已提交
89 90

```java
C
CyC2018 已提交
91
public class ThreeSumBinarySearch implements ThreeSum {
C
CyC2018 已提交
92

C
CyC2018 已提交
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
    @Override
    public int count(int[] nums) {
        Arrays.sort(nums);
        int N = nums.length;
        int cnt = 0;
        for (int i = 0; i < N; i++) {
            for (int j = i + 1; j < N; j++) {
                int target = -nums[i] - nums[j];
                int index = BinarySearch.search(nums, target);
                // 应该注意这里的下标必须大于 j,否则会重复统计。
                if (index > j) {
                    cnt++;
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
110 111
}
```
C
CyC2018 已提交
112

C
CyC2018 已提交
113
```java
C
CyC2018 已提交
114
public class BinarySearch {
C
CyC2018 已提交
115

C
CyC2018 已提交
116 117 118 119 120 121 122 123 124 125 126 127 128 129
    public static int search(int[] nums, int target) {
        int l = 0, h = nums.length - 1;
        while (l <= h) {
            int m = l + (h - l) / 2;
            if (target == nums[m]) {
                return m;
            } else if (target > nums[m]) {
                l = m + 1;
            } else {
                h = m - 1;
            }
        }
        return -1;
    }
C
CyC2018 已提交
130 131 132
}
```

C
CyC2018 已提交
133
### 3. ThreeSumTwoPointer
C
CyC2018 已提交
134

C
CyC2018 已提交
135
更有效的方法是先将数组排序,然后使用双指针进行查找,时间复杂度为 O(N<sup>2</sup>)。
C
CyC2018 已提交
136 137

```java
C
CyC2018 已提交
138
public class ThreeSumTwoPointer implements ThreeSum {
C
CyC2018 已提交
139

C
CyC2018 已提交
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    @Override
    public int count(int[] nums) {
        int N = nums.length;
        int cnt = 0;
        Arrays.sort(nums);
        for (int i = 0; i < N - 2; i++) {
            int l = i + 1, h = N - 1, target = -nums[i];
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            while (l < h) {
                int sum = nums[l] + nums[h];
                if (sum == target) {
                    cnt++;
                    while (l < h && nums[l] == nums[l + 1]) l++;
                    while (l < h && nums[h] == nums[h - 1]) h--;
                    l++;
                    h--;
                } else if (sum < target) {
                    l++;
                } else {
                    h--;
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
165 166 167
}
```

C
CyC2018 已提交
168
## 倍率实验
C
CyC2018 已提交
169

C
CyC2018 已提交
170
如果 T(N) ~ aN<sup>b</sup>logN,那么 T(2N)/T(N) ~ 2<sup>b</sup>
C
CyC2018 已提交
171

C
CyC2018 已提交
172
例如对于暴力的 ThreeSum 算法,近似时间为 ~N<sup>3</sup>/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果:
C
CyC2018 已提交
173

C
CyC2018 已提交
174 175 176 177 178 179 180 181
| N | Time(ms) | Ratio |
| :---: | :---: | :---: |
| 500 | 48 | / |
| 1000 | 320 | 6.7 |
| 2000 | 555 | 1.7 |
| 4000 | 4105 | 7.4 |
| 8000 | 33575 | 8.2 |
| 16000 | 268909 | 8.0 |
C
CyC2018 已提交
182

C
CyC2018 已提交
183
可以看到,T(2N)/T(N) ~ 2<sup>3</sup>,因此可以确定 T(N) ~ aN<sup>3</sup>logN。
C
CyC2018 已提交
184

C
CyC2018 已提交
185
```java
C
CyC2018 已提交
186
public class RatioTest {
C
CyC2018 已提交
187

C
CyC2018 已提交
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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;
        }
    }
C
CyC2018 已提交
205 206 207
}
```

C
CyC2018 已提交
208
```java
C
CyC2018 已提交
209
public class StopWatch {
C
CyC2018 已提交
210

C
CyC2018 已提交
211
    private static long start;
C
CyC2018 已提交
212 213


C
CyC2018 已提交
214 215 216
    public static void start() {
        start = System.currentTimeMillis();
    }
C
CyC2018 已提交
217 218


C
CyC2018 已提交
219 220 221 222
    public static double elapsedTime() {
        long now = System.currentTimeMillis();
        return (now - start) / 1000.0;
    }
C
CyC2018 已提交
223 224 225
}
```

C
CyC2018 已提交
226
# 三、排序
C
CyC2018 已提交
227

C
CyC2018 已提交
228
待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。
C
CyC2018 已提交
229

C
CyC2018 已提交
230
研究排序算法的成本模型时,统计的是比较和交换的次数。
C
CyC2018 已提交
231

C
CyC2018 已提交
232
使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。
C
CyC2018 已提交
233

C
CyC2018 已提交
234
```java
C
CyC2018 已提交
235
public abstract class Sort<T extends Comparable<T>> {
C
CyC2018 已提交
236

C
CyC2018 已提交
237
    public abstract void sort(T[] nums);
C
CyC2018 已提交
238

C
CyC2018 已提交
239 240 241
    protected boolean less(T v, T w) {
        return v.compareTo(w) < 0;
    }
C
CyC2018 已提交
242

C
CyC2018 已提交
243 244 245 246 247
    protected void swap(T[] a, int i, int j) {
        T t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
C
CyC2018 已提交
248 249 250
}
```

C
CyC2018 已提交
251
## 选择排序
C
CyC2018 已提交
252 253 254

选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。

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

C
CyC2018 已提交
257
<img src="index_files/37e79a32-95a9-4503-bdb1-159527e628b8.png" width="250"/>
C
CyC2018 已提交
258 259

```java
C
CyC2018 已提交
260
public class Selection<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
261

C
CyC2018 已提交
262 263 264 265 266 267 268 269 270 271 272 273 274
    @Override
    public void sort(T[] nums) {
        int N = nums.length;
        for (int i = 0; i < N - 1; i++) {
            int min = i;
            for (int j = i + 1; j < N; j++) {
                if (less(nums[j], nums[min])) {
                    min = j;
                }
            }
            swap(nums, i, min);
        }
    }
C
CyC2018 已提交
275 276
}
```
C
CyC2018 已提交
277

C
CyC2018 已提交
278
## 冒泡排序
C
CyC2018 已提交
279

C
CyC2018 已提交
280
从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。
C
CyC2018 已提交
281

C
CyC2018 已提交
282
在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。
C
CyC2018 已提交
283

C
CyC2018 已提交
284
以下演示了在一轮循环中,将最大的元素 5 上浮到最右侧。
C
CyC2018 已提交
285

C
CyC2018 已提交
286
<img src="index_files/1a2f2998-d0da-41c8-8222-1fd95083a66b.png" width="250"/>
C
CyC2018 已提交
287

C
CyC2018 已提交
288
```java
C
CyC2018 已提交
289
public class Bubble<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
290

C
CyC2018 已提交
291 292 293 294 295 296 297 298 299 300 301 302 303 304
    @Override
    public void sort(T[] nums) {
        int N = nums.length;
        boolean hasSorted = false;
        for (int i = N - 1; i > 0 && !hasSorted; i--) {
            hasSorted = true;
            for (int j = 0; j < i; j++) {
                if (less(nums[j + 1], nums[j])) {
                    hasSorted = false;
                    swap(nums, j, j + 1);
                }
            }
        }
    }
C
CyC2018 已提交
305 306 307
}
```

C
CyC2018 已提交
308
## 插入排序
C
CyC2018 已提交
309

C
CyC2018 已提交
310
每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。
C
CyC2018 已提交
311

C
CyC2018 已提交
312
对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。
C
CyC2018 已提交
313

C
CyC2018 已提交
314
插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,逆序较少,那么插入排序会很快。
C
CyC2018 已提交
315

C
CyC2018 已提交
316 317 318
- 平均情况下插入排序需要 ~N<sup>2</sup>/4 比较以及 ~N<sup>2</sup>/4 次交换;
- 最坏的情况下需要 ~N<sup>2</sup>/2 比较以及 ~N<sup>2</sup>/2 次交换,最坏的情况是数组是倒序的;
- 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。
C
CyC2018 已提交
319

C
CyC2018 已提交
320
以下演示了在一轮循环中,将元素 2 插入到左侧已经排序的数组中。
C
CyC2018 已提交
321

C
CyC2018 已提交
322
<img src="index_files/2a8e1442-2381-4439-a83f-0312c8678b1f.png" width="250"/>
C
CyC2018 已提交
323

C
CyC2018 已提交
324
```java
C
CyC2018 已提交
325
public class Insertion<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
326

C
CyC2018 已提交
327 328 329 330 331 332 333 334 335
    @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);
            }
        }
    }
C
CyC2018 已提交
336
}
C
CyC2018 已提交
337 338
```

C
CyC2018 已提交
339
## 希尔排序
C
CyC2018 已提交
340

C
CyC2018 已提交
341
对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。
C
CyC2018 已提交
342

C
CyC2018 已提交
343
希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。
C
CyC2018 已提交
344

C
CyC2018 已提交
345
希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。
C
CyC2018 已提交
346

C
CyC2018 已提交
347
<img src="index_files/0157d362-98dd-4e51-ac26-00ecb76beb3e.png" width="500"/>
C
CyC2018 已提交
348 349

```java
C
CyC2018 已提交
350
public class Shell<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
351

C
CyC2018 已提交
352 353
    @Override
    public void sort(T[] nums) {
C
CyC2018 已提交
354

C
CyC2018 已提交
355 356
        int N = nums.length;
        int h = 1;
C
CyC2018 已提交
357

C
CyC2018 已提交
358 359 360
        while (h < N / 3) {
            h = 3 * h + 1; // 1, 4, 13, 40, ...
        }
C
CyC2018 已提交
361

C
CyC2018 已提交
362 363 364 365 366 367 368 369 370
        while (h >= 1) {
            for (int i = h; i < N; i++) {
                for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) {
                    swap(nums, j, j - h);
                }
            }
            h = h / 3;
        }
    }
C
CyC2018 已提交
371
}
C
CyC2018 已提交
372

C
CyC2018 已提交
373 374
```

C
CyC2018 已提交
375
希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ...  的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。
C
CyC2018 已提交
376

C
CyC2018 已提交
377
## 归并排序
C
CyC2018 已提交
378 379 380

归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。

C
CyC2018 已提交
381
<img src="index_files/220790c6-4377-4a2e-8686-58398afc8a18.png" width="350"/>
C
CyC2018 已提交
382

C
CyC2018 已提交
383
### 1. 归并方法
C
CyC2018 已提交
384 385 386 387

归并方法将数组中两个已经排序的部分归并成一个。

```java
C
CyC2018 已提交
388
public abstract class MergeSort<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
389

C
CyC2018 已提交
390
    protected T[] aux;
C
CyC2018 已提交
391

C
CyC2018 已提交
392

C
CyC2018 已提交
393
    protected void merge(T[] nums, int l, int m, int h) {
C
CyC2018 已提交
394

C
CyC2018 已提交
395
        int i = l, j = m + 1;
C
CyC2018 已提交
396

C
CyC2018 已提交
397 398 399
        for (int k = l; k <= h; k++) {
            aux[k] = nums[k]; // 将数据复制到辅助数组
        }
C
CyC2018 已提交
400

C
CyC2018 已提交
401 402 403
        for (int k = l; k <= h; k++) {
            if (i > m) {
                nums[k] = aux[j++];
C
CyC2018 已提交
404

C
CyC2018 已提交
405 406
            } else if (j > h) {
                nums[k] = aux[i++];
C
CyC2018 已提交
407

C
CyC2018 已提交
408 409
            } else if (aux[i].compareTo(nums[j]) <= 0) {
                nums[k] = aux[i++]; // 先进行这一步,保证稳定性
C
CyC2018 已提交
410

C
CyC2018 已提交
411 412 413 414 415
            } else {
                nums[k] = aux[j++];
            }
        }
    }
C
CyC2018 已提交
416 417 418
}
```

C
CyC2018 已提交
419
### 2. 自顶向下归并排序
C
CyC2018 已提交
420

C
CyC2018 已提交
421 422
将一个大数组分成两个小数组去求解。

C
CyC2018 已提交
423
因为每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(NlogN)。
C
CyC2018 已提交
424 425

```java
C
CyC2018 已提交
426
public class Up2DownMergeSort<T extends Comparable<T>> extends MergeSort<T> {
C
CyC2018 已提交
427

C
CyC2018 已提交
428 429 430 431 432
    @Override
    public void sort(T[] nums) {
        aux = (T[]) new Comparable[nums.length];
        sort(nums, 0, nums.length - 1);
    }
C
CyC2018 已提交
433

C
CyC2018 已提交
434 435 436 437 438 439 440 441 442
    private void sort(T[] nums, int l, int h) {
        if (h <= l) {
            return;
        }
        int mid = l + (h - l) / 2;
        sort(nums, l, mid);
        sort(nums, mid + 1, h);
        merge(nums, l, mid, h);
    }
C
CyC2018 已提交
443 444 445
}
```

C
CyC2018 已提交
446
### 3. 自底向上归并排序
C
CyC2018 已提交
447 448 449 450

先归并那些微型数组,然后成对归并得到的微型数组。

```java
C
CyC2018 已提交
451
public class Down2UpMergeSort<T extends Comparable<T>> extends MergeSort<T> {
C
CyC2018 已提交
452

C
CyC2018 已提交
453 454
    @Override
    public void sort(T[] nums) {
C
CyC2018 已提交
455

C
CyC2018 已提交
456 457
        int N = nums.length;
        aux = (T[]) new Comparable[N];
C
CyC2018 已提交
458

C
CyC2018 已提交
459 460 461 462 463 464
        for (int sz = 1; sz < N; sz += sz) {
            for (int lo = 0; lo < N - sz; lo += sz + sz) {
                merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
            }
        }
    }
C
CyC2018 已提交
465
}
C
CyC2018 已提交
466

C
CyC2018 已提交
467 468
```

C
CyC2018 已提交
469
## 快速排序
C
CyC2018 已提交
470

C
CyC2018 已提交
471
### 1. 基本算法
C
CyC2018 已提交
472

C
CyC2018 已提交
473 474
- 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;
- 快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。
C
CyC2018 已提交
475

C
CyC2018 已提交
476
<img src="index_files/f8047846-efd4-42be-b6b7-27a7c4998b51.png" width="500"/>
C
CyC2018 已提交
477 478

```java
C
CyC2018 已提交
479
public class QuickSort<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
480

C
CyC2018 已提交
481 482 483 484 485
    @Override
    public void sort(T[] nums) {
        shuffle(nums);
        sort(nums, 0, nums.length - 1);
    }
C
CyC2018 已提交
486

C
CyC2018 已提交
487 488 489 490 491 492 493
    private void sort(T[] nums, int l, int h) {
        if (h <= l)
            return;
        int j = partition(nums, l, h);
        sort(nums, l, j - 1);
        sort(nums, j + 1, h);
    }
C
CyC2018 已提交
494

C
CyC2018 已提交
495 496 497 498 499
    private void shuffle(T[] nums) {
        List<Comparable> list = Arrays.asList(nums);
        Collections.shuffle(list);
        list.toArray(nums);
    }
C
CyC2018 已提交
500 501 502
}
```

C
CyC2018 已提交
503
### 2. 切分
C
CyC2018 已提交
504

C
CyC2018 已提交
505
取 a[l] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于它的元素,交换这两个元素。不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[l] 和 a[j] 交换位置。
C
CyC2018 已提交
506

C
CyC2018 已提交
507
<img src="index_files/766aedd0-1b00-4065-aa2b-7d31138df84f.png" width="400"/>
C
CyC2018 已提交
508 509

```java
C
CyC2018 已提交
510 511 512 513 514 515 516 517 518 519 520 521
private int partition(T[] nums, int l, int h) {
    int i = l, j = h + 1;
    T v = nums[l];
    while (true) {
        while (less(nums[++i], v) && i != h) ;
        while (less(v, nums[--j]) && j != l) ;
        if (i >= j)
            break;
        swap(nums, i, j);
    }
    swap(nums, l, j);
    return j;
C
CyC2018 已提交
522 523 524
}
```

C
CyC2018 已提交
525
### 3. 性能分析
C
CyC2018 已提交
526 527 528

快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。

C
CyC2018 已提交
529
快速排序最好的情况下是每次都正好将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 C<sub>N</sub>=2C<sub>N/2</sub>+N,复杂度为 O(NlogN)。
C
CyC2018 已提交
530

C
CyC2018 已提交
531
最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N<sup>2</sup>/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。
C
CyC2018 已提交
532

C
CyC2018 已提交
533
### 4. 算法改进
C
CyC2018 已提交
534

C
CyC2018 已提交
535
#### 4.1 切换到插入排序
C
CyC2018 已提交
536 537 538

因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。

C
CyC2018 已提交
539
#### 4.2 三数取中
C
CyC2018 已提交
540

C
CyC2018 已提交
541
最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。一种折中方法是取 3 个元素,并将大小居中的元素作为切分元素。
C
CyC2018 已提交
542

C
CyC2018 已提交
543
#### 4.3 三向切分
C
CyC2018 已提交
544 545 546

对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。

C
CyC2018 已提交
547
三向切分快速排序对于有大量重复元素的随机数组可以在线性时间内完成排序。
C
CyC2018 已提交
548 549

```java
C
CyC2018 已提交
550
public class ThreeWayQuickSort<T extends Comparable<T>> extends QuickSort<T> {
C
CyC2018 已提交
551

C
CyC2018 已提交
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
    @Override
    protected void sort(T[] nums, int l, int h) {
        if (h <= l) {
            return;
        }
        int lt = l, i = l + 1, gt = h;
        T v = nums[l];
        while (i <= gt) {
            int cmp = nums[i].compareTo(v);
            if (cmp < 0) {
                swap(nums, lt++, i++);
            } else if (cmp > 0) {
                swap(nums, i, gt--);
            } else {
                i++;
            }
        }
        sort(nums, l, lt - 1);
        sort(nums, gt + 1, h);
    }
C
CyC2018 已提交
572 573 574
}
```

C
CyC2018 已提交
575
### 5. 基于切分的快速选择算法
C
CyC2018 已提交
576

C
CyC2018 已提交
577
快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。
C
CyC2018 已提交
578

C
CyC2018 已提交
579
可以利用这个特性找出数组的第 k 个元素。
C
CyC2018 已提交
580

C
CyC2018 已提交
581
该算法是线性级别的,假设每次能将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。
C
CyC2018 已提交
582

C
CyC2018 已提交
583
```java
C
CyC2018 已提交
584 585 586 587
public T select(T[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (h > l) {
        int j = partition(nums, l, h);
C
CyC2018 已提交
588

C
CyC2018 已提交
589 590
        if (j == k) {
            return nums[k];
C
CyC2018 已提交
591

C
CyC2018 已提交
592 593
        } else if (j > k) {
            h = j - 1;
C
CyC2018 已提交
594

C
CyC2018 已提交
595 596 597 598 599
        } else {
            l = j + 1;
        }
    }
    return nums[k];
C
CyC2018 已提交
600 601 602
}
```

C
CyC2018 已提交
603
## 堆排序
C
CyC2018 已提交
604

C
CyC2018 已提交
605
### 1. 堆
C
CyC2018 已提交
606

C
CyC2018 已提交
607
堆中某个节点的值总是大于等于其子节点的值,并且堆是一颗完全二叉树。
C
CyC2018 已提交
608

C
CyC2018 已提交
609
堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
C
CyC2018 已提交
610

C
CyC2018 已提交
611
<img src="index_files/f3080f83-6239-459b-8e9c-03b6641f7815.png" width="200"/>
C
CyC2018 已提交
612 613

```java
C
CyC2018 已提交
614
public class Heap<T extends Comparable<T>> {
C
CyC2018 已提交
615

C
CyC2018 已提交
616 617
    private T[] heap;
    private int N = 0;
C
CyC2018 已提交
618

C
CyC2018 已提交
619 620 621
    public Heap(int maxN) {
        this.heap = (T[]) new Comparable[maxN + 1];
    }
C
CyC2018 已提交
622

C
CyC2018 已提交
623 624 625
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
626

C
CyC2018 已提交
627 628 629
    public int size() {
        return N;
    }
C
CyC2018 已提交
630

C
CyC2018 已提交
631 632 633
    private boolean less(int i, int j) {
        return heap[i].compareTo(heap[j]) < 0;
    }
C
CyC2018 已提交
634

C
CyC2018 已提交
635 636 637 638 639
    private void swap(int i, int j) {
        T t = heap[i];
        heap[i] = heap[j];
        heap[j] = t;
    }
C
CyC2018 已提交
640 641 642
}
```

C
CyC2018 已提交
643
### 2. 上浮和下沉
C
CyC2018 已提交
644 645 646

在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮。

C
CyC2018 已提交
647
<img src="index_files/33ac2b23-cb85-4e99-bc41-b7b7199fad1c.png" width="400"/>
C
CyC2018 已提交
648 649

```java
C
CyC2018 已提交
650 651 652 653 654
private void swim(int k) {
    while (k > 1 && less(k / 2, k)) {
        swap(k / 2, k);
        k = k / 2;
    }
C
CyC2018 已提交
655 656 657
}
```

C
CyC2018 已提交
658
类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉。一个节点如果有两个子节点,应当与两个子节点中最大那个节点进行交换。
C
CyC2018 已提交
659

C
CyC2018 已提交
660
<img src="index_files/72f0ff69-138d-4e54-b7ac-ebe025d978dc.png" width="400"/>
C
CyC2018 已提交
661 662

```java
C
CyC2018 已提交
663 664 665 666 667 668 669 670 671 672
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;
        swap(k, j);
        k = j;
    }
C
CyC2018 已提交
673 674 675
}
```

C
CyC2018 已提交
676
### 3. 插入元素
C
CyC2018 已提交
677 678 679 680

将新元素放到数组末尾,然后上浮到合适的位置。

```java
C
CyC2018 已提交
681 682 683
public void insert(Comparable v) {
    heap[++N] = v;
    swim(N);
C
CyC2018 已提交
684 685 686
}
```

C
CyC2018 已提交
687
### 4. 删除最大元素
C
CyC2018 已提交
688 689 690 691

从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。

```java
C
CyC2018 已提交
692 693 694 695 696 697
public T delMax() {
    T max = heap[1];
    swap(1, N--);
    heap[N + 1] = null;
    sink(1);
    return max;
C
CyC2018 已提交
698 699 700
}
```

C
CyC2018 已提交
701
### 5. 堆排序
C
CyC2018 已提交
702

C
CyC2018 已提交
703
把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列,这就是堆排序。
C
CyC2018 已提交
704

C
CyC2018 已提交
705
#### 5.1 构建堆
C
CyC2018 已提交
706

C
CyC2018 已提交
707
无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
C
CyC2018 已提交
708

C
CyC2018 已提交
709
<img src="index_files/b84ba6fb-312b-4e69-8c77-fb6eb6fb38d4.png" width="300"/>
C
CyC2018 已提交
710

C
CyC2018 已提交
711
#### 5.2 交换堆顶元素与最后一个元素
C
CyC2018 已提交
712

C
CyC2018 已提交
713
交换之后需要进行下沉操作维持堆的有序状态。
C
CyC2018 已提交
714

C
CyC2018 已提交
715
<img src="index_files/51fb761d-8ce0-4472-92ff-2f227ac7888a.png" width="270"/>
C
CyC2018 已提交
716

C
CyC2018 已提交
717
<img src="index_files/b20a3466-44b4-445e-87c7-dd4fb9ef44b2.png" width="350"/>
C
CyC2018 已提交
718

C
CyC2018 已提交
719
```java
C
CyC2018 已提交
720 721 722 723 724 725 726 727 728
public class HeapSort<T extends Comparable<T>> extends Sort<T> {
    /**
     * 数组第 0 个位置不能有元素
     */
    @Override
    public void sort(T[] nums) {
        int N = nums.length - 1;
        for (int k = N / 2; k >= 1; k--)
            sink(nums, k, N);
C
CyC2018 已提交
729

C
CyC2018 已提交
730 731 732 733 734
        while (N > 1) {
            swap(nums, 1, N--);
            sink(nums, 1, N);
        }
    }
C
CyC2018 已提交
735

C
CyC2018 已提交
736 737 738 739 740 741 742 743 744 745 746
    private void sink(T[] nums, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && less(nums, j, j + 1))
                j++;
            if (!less(nums, k, j))
                break;
            swap(nums, k, j);
            k = j;
        }
    }
C
CyC2018 已提交
747

C
CyC2018 已提交
748 749 750
    private boolean less(T[] nums, int i, int j) {
        return nums[i].compareTo(nums[j]) < 0;
    }
C
CyC2018 已提交
751 752 753
}
```

C
CyC2018 已提交
754
### 6. 分析
C
CyC2018 已提交
755

C
CyC2018 已提交
756
一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。
C
CyC2018 已提交
757

C
CyC2018 已提交
758
对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。
C
CyC2018 已提交
759

C
CyC2018 已提交
760
堆排序是一种原地排序,没有利用额外的空间。
C
CyC2018 已提交
761

C
CyC2018 已提交
762
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。
C
CyC2018 已提交
763

C
CyC2018 已提交
764
## 小结
C
CyC2018 已提交
765

C
CyC2018 已提交
766
### 1. 排序算法的比较
C
CyC2018 已提交
767

C
CyC2018 已提交
768 769 770 771 772 773 774 775 776 777
| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 |
| :---: | :---: |:---: | :---: | :---: |
| 选择排序 | × | N<sup>2</sup> | 1 | |
| 冒泡排序 | √ |  N<sup>2</sup> | 1 | |
| 插入排序 | √ |  N ~ N<sup>2</sup> | 1 | 时间复杂度和初始顺序有关 |
| 希尔排序 | ×  |  N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 |
| 快速排序 | ×  | NlogN | logN | |
| 三向切分快速排序 | ×  |  N ~ NlogN | logN | 适用于有大量重复主键|
| 归并排序 | √ |  NlogN | N | |
| 堆排序 | ×  |  NlogN | 1 | 无法利用局部性原理|
C
CyC2018 已提交
778

C
CyC2018 已提交
779
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 ~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。
C
CyC2018 已提交
780 781

使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
C
CyC2018 已提交
782

C
CyC2018 已提交
783
### 2. Java 的排序算法实现
C
CyC2018 已提交
784

C
CyC2018 已提交
785
Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。
C
CyC2018 已提交
786

C
CyC2018 已提交
787
# 四、并查集
C
CyC2018 已提交
788 789 790

用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。

C
CyC2018 已提交
791
<img src="index_files/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png" width="400"/>
C
CyC2018 已提交
792

C
CyC2018 已提交
793 794 795 796 797 798
| 方法 | 描述 |
| :---: | :---: |
| 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 节点是否连通 |
C
CyC2018 已提交
799 800

```java
C
CyC2018 已提交
801
public abstract class UF {
C
CyC2018 已提交
802

C
CyC2018 已提交
803
    protected int[] id;
C
CyC2018 已提交
804

C
CyC2018 已提交
805 806 807 808 809 810
    public UF(int N) {
        id = new int[N];
        for (int i = 0; i < N; i++) {
            id[i] = i;
        }
    }
C
CyC2018 已提交
811

C
CyC2018 已提交
812 813 814
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }
C
CyC2018 已提交
815

C
CyC2018 已提交
816
    public abstract int find(int p);
C
CyC2018 已提交
817

C
CyC2018 已提交
818
    public abstract void union(int p, int q);
C
CyC2018 已提交
819 820 821
}
```

C
CyC2018 已提交
822
## Quick Find
C
CyC2018 已提交
823

C
CyC2018 已提交
824
可以快速进行 find 操作,也就是可以快速判断两个节点是否连通。
C
CyC2018 已提交
825

C
CyC2018 已提交
826
需要保证同一连通分量的所有节点的 id 值相等。
C
CyC2018 已提交
827

C
CyC2018 已提交
828
但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。
C
CyC2018 已提交
829

C
CyC2018 已提交
830
<img src="index_files/8f0cc500-5994-4c7a-91a9-62885d658662.png" width="350"/>
C
CyC2018 已提交
831 832

```java
C
CyC2018 已提交
833
public class QuickFindUF extends UF {
C
CyC2018 已提交
834

C
CyC2018 已提交
835 836 837
    public QuickFindUF(int N) {
        super(N);
    }
C
CyC2018 已提交
838

C
CyC2018 已提交
839

C
CyC2018 已提交
840 841 842 843
    @Override
    public int find(int p) {
        return id[p];
    }
C
CyC2018 已提交
844

C
CyC2018 已提交
845

C
CyC2018 已提交
846 847 848 849
    @Override
    public void union(int p, int q) {
        int pID = find(p);
        int qID = find(q);
C
CyC2018 已提交
850

C
CyC2018 已提交
851 852 853
        if (pID == qID) {
            return;
        }
C
CyC2018 已提交
854

C
CyC2018 已提交
855 856 857 858 859 860
        for (int i = 0; i < id.length; i++) {
            if (id[i] == pID) {
                id[i] = qID;
            }
        }
    }
C
CyC2018 已提交
861 862 863
}
```

C
CyC2018 已提交
864
## Quick Union
C
CyC2018 已提交
865

C
CyC2018 已提交
866
可以快速进行 union 操作,只需要修改一个节点的 id 值即可。
C
CyC2018 已提交
867

C
CyC2018 已提交
868
但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。
C
CyC2018 已提交
869

C
CyC2018 已提交
870
<img src="index_files/5d4a5181-65fb-4bf2-a9c6-899cab534b44.png" width="350"/>
C
CyC2018 已提交
871 872

```java
C
CyC2018 已提交
873
public class QuickUnionUF extends UF {
C
CyC2018 已提交
874

C
CyC2018 已提交
875 876 877
    public QuickUnionUF(int N) {
        super(N);
    }
C
CyC2018 已提交
878

C
CyC2018 已提交
879

C
CyC2018 已提交
880 881 882 883 884 885 886
    @Override
    public int find(int p) {
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }
C
CyC2018 已提交
887

C
CyC2018 已提交
888

C
CyC2018 已提交
889 890 891 892
    @Override
    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
C
CyC2018 已提交
893

C
CyC2018 已提交
894 895 896 897
        if (pRoot != qRoot) {
            id[pRoot] = qRoot;
        }
    }
C
CyC2018 已提交
898 899 900
}
```

C
CyC2018 已提交
901
这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为节点的数目。
C
CyC2018 已提交
902

C
CyC2018 已提交
903
<img src="index_files/bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width="130"/>
C
CyC2018 已提交
904

C
CyC2018 已提交
905
## 加权 Quick Union
C
CyC2018 已提交
906

C
CyC2018 已提交
907
为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。
C
CyC2018 已提交
908

C
CyC2018 已提交
909
理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。
C
CyC2018 已提交
910

C
CyC2018 已提交
911
<img src="index_files/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png" width="170"/>
C
CyC2018 已提交
912

C
CyC2018 已提交
913
```java
C
CyC2018 已提交
914
public class WeightedQuickUnionUF extends UF {
C
CyC2018 已提交
915

C
CyC2018 已提交
916 917
    // 保存节点的数量信息
    private int[] sz;
C
CyC2018 已提交
918 919


C
CyC2018 已提交
920 921 922 923 924 925 926
    public WeightedQuickUnionUF(int N) {
        super(N);
        this.sz = new int[N];
        for (int i = 0; i < N; i++) {
            this.sz[i] = 1;
        }
    }
C
CyC2018 已提交
927 928


C
CyC2018 已提交
929 930 931 932 933 934 935
    @Override
    public int find(int p) {
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }
C
CyC2018 已提交
936 937


C
CyC2018 已提交
938 939
    @Override
    public void union(int p, int q) {
C
CyC2018 已提交
940

C
CyC2018 已提交
941 942
        int i = find(p);
        int j = find(q);
C
CyC2018 已提交
943

C
CyC2018 已提交
944
        if (i == j) return;
C
CyC2018 已提交
945

C
CyC2018 已提交
946 947 948 949 950 951 952 953
        if (sz[i] < sz[j]) {
            id[i] = j;
            sz[j] += sz[i];
        } else {
            id[j] = i;
            sz[i] += sz[j];
        }
    }
C
CyC2018 已提交
954 955 956
}
```

C
CyC2018 已提交
957
## 路径压缩的加权 Quick Union
C
CyC2018 已提交
958

C
CyC2018 已提交
959
在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。
C
CyC2018 已提交
960

C
CyC2018 已提交
961
## 比较
C
CyC2018 已提交
962

C
CyC2018 已提交
963 964 965 966 967 968
| 算法 | union | find |
| :---: | :---: | :---: |
| Quick Find | N | 1 |
| Quick Union | 树高 | 树高 |
| 加权 Quick Union | logN | logN |
| 路径压缩的加权 Quick Union | 非常接近 1 | 非常接近 1 |
C
CyC2018 已提交
969

C
CyC2018 已提交
970
# 五、栈和队列
C
CyC2018 已提交
971

C
CyC2018 已提交
972
## 栈
C
CyC2018 已提交
973 974

```java
C
CyC2018 已提交
975
public interface MyStack<Item> extends Iterable<Item> {
C
CyC2018 已提交
976

C
CyC2018 已提交
977
    MyStack<Item> push(Item item);
C
CyC2018 已提交
978

C
CyC2018 已提交
979
    Item pop() throws Exception;
C
CyC2018 已提交
980

C
CyC2018 已提交
981
    boolean isEmpty();
C
CyC2018 已提交
982

C
CyC2018 已提交
983
    int size();
C
CyC2018 已提交
984

C
CyC2018 已提交
985 986 987
}
```

C
CyC2018 已提交
988
### 1. 数组实现
C
CyC2018 已提交
989 990

```java
C
CyC2018 已提交
991
public class ArrayStack<Item> implements MyStack<Item> {
C
CyC2018 已提交
992

C
CyC2018 已提交
993 994
    // 栈元素数组,只能通过转型来创建泛型数组
    private Item[] a = (Item[]) new Object[1];
C
CyC2018 已提交
995

C
CyC2018 已提交
996 997
    // 元素数量
    private int N = 0;
C
CyC2018 已提交
998 999


C
CyC2018 已提交
1000 1001 1002 1003 1004 1005
    @Override
    public MyStack<Item> push(Item item) {
        check();
        a[N++] = item;
        return this;
    }
C
CyC2018 已提交
1006 1007


C
CyC2018 已提交
1008 1009
    @Override
    public Item pop() throws Exception {
C
CyC2018 已提交
1010

C
CyC2018 已提交
1011 1012 1013
        if (isEmpty()) {
            throw new Exception("stack is empty");
        }
C
CyC2018 已提交
1014

C
CyC2018 已提交
1015
        Item item = a[--N];
C
CyC2018 已提交
1016

C
CyC2018 已提交
1017
        check();
C
CyC2018 已提交
1018

C
CyC2018 已提交
1019 1020
        // 避免对象游离
        a[N] = null;
C
CyC2018 已提交
1021

C
CyC2018 已提交
1022 1023
        return item;
    }
C
CyC2018 已提交
1024

C
CyC2018 已提交
1025

C
CyC2018 已提交
1026
    private void check() {
C
CyC2018 已提交
1027

C
CyC2018 已提交
1028 1029
        if (N >= a.length) {
            resize(2 * a.length);
C
CyC2018 已提交
1030

C
CyC2018 已提交
1031 1032 1033 1034
        } else if (N > 0 && N <= a.length / 4) {
            resize(a.length / 2);
        }
    }
C
CyC2018 已提交
1035 1036


C
CyC2018 已提交
1037 1038 1039 1040
    /**
     * 调整数组大小,使得栈具有伸缩性
     */
    private void resize(int size) {
C
CyC2018 已提交
1041

C
CyC2018 已提交
1042
        Item[] tmp = (Item[]) new Object[size];
C
CyC2018 已提交
1043

C
CyC2018 已提交
1044 1045 1046
        for (int i = 0; i < N; i++) {
            tmp[i] = a[i];
        }
C
CyC2018 已提交
1047

C
CyC2018 已提交
1048 1049
        a = tmp;
    }
C
CyC2018 已提交
1050 1051


C
CyC2018 已提交
1052 1053 1054 1055
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1056 1057


C
CyC2018 已提交
1058 1059 1060 1061
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1062 1063


C
CyC2018 已提交
1064 1065
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1066

C
CyC2018 已提交
1067 1068
        // 返回逆序遍历的迭代器
        return new Iterator<Item>() {
C
CyC2018 已提交
1069

C
CyC2018 已提交
1070
            private int i = N;
C
CyC2018 已提交
1071

C
CyC2018 已提交
1072 1073 1074 1075
            @Override
            public boolean hasNext() {
                return i > 0;
            }
C
CyC2018 已提交
1076

C
CyC2018 已提交
1077 1078 1079 1080 1081
            @Override
            public Item next() {
                return a[--i];
            }
        };
C
CyC2018 已提交
1082

C
CyC2018 已提交
1083
    }
C
CyC2018 已提交
1084 1085
}
```
C
CyC2018 已提交
1086

C
CyC2018 已提交
1087
### 2. 链表实现
C
CyC2018 已提交
1088

C
CyC2018 已提交
1089
需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素成为新的栈顶元素。
C
CyC2018 已提交
1090 1091

```java
C
CyC2018 已提交
1092
public class ListStack<Item> implements MyStack<Item> {
C
CyC2018 已提交
1093

C
CyC2018 已提交
1094 1095
    private Node top = null;
    private int N = 0;
C
CyC2018 已提交
1096 1097


C
CyC2018 已提交
1098 1099 1100 1101
    private class Node {
        Item item;
        Node next;
    }
C
CyC2018 已提交
1102 1103


C
CyC2018 已提交
1104 1105
    @Override
    public MyStack<Item> push(Item item) {
C
CyC2018 已提交
1106

C
CyC2018 已提交
1107
        Node newTop = new Node();
C
CyC2018 已提交
1108

C
CyC2018 已提交
1109 1110
        newTop.item = item;
        newTop.next = top;
C
CyC2018 已提交
1111

C
CyC2018 已提交
1112
        top = newTop;
C
CyC2018 已提交
1113

C
CyC2018 已提交
1114
        N++;
C
CyC2018 已提交
1115

C
CyC2018 已提交
1116 1117
        return this;
    }
C
CyC2018 已提交
1118 1119


C
CyC2018 已提交
1120 1121
    @Override
    public Item pop() throws Exception {
C
CyC2018 已提交
1122

C
CyC2018 已提交
1123 1124 1125
        if (isEmpty()) {
            throw new Exception("stack is empty");
        }
C
CyC2018 已提交
1126

C
CyC2018 已提交
1127
        Item item = top.item;
C
CyC2018 已提交
1128

C
CyC2018 已提交
1129 1130
        top = top.next;
        N--;
C
CyC2018 已提交
1131

C
CyC2018 已提交
1132 1133
        return item;
    }
C
CyC2018 已提交
1134

C
CyC2018 已提交
1135

C
CyC2018 已提交
1136 1137 1138 1139
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1140

C
CyC2018 已提交
1141

C
CyC2018 已提交
1142 1143 1144 1145
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1146 1147


C
CyC2018 已提交
1148 1149
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1150

C
CyC2018 已提交
1151
        return new Iterator<Item>() {
C
CyC2018 已提交
1152

C
CyC2018 已提交
1153
            private Node cur = top;
C
CyC2018 已提交
1154 1155


C
CyC2018 已提交
1156 1157 1158 1159
            @Override
            public boolean hasNext() {
                return cur != null;
            }
C
CyC2018 已提交
1160 1161


C
CyC2018 已提交
1162 1163 1164 1165 1166 1167 1168
            @Override
            public Item next() {
                Item item = cur.item;
                cur = cur.next;
                return item;
            }
        };
C
CyC2018 已提交
1169

C
CyC2018 已提交
1170
    }
C
CyC2018 已提交
1171 1172 1173
}
```

C
CyC2018 已提交
1174
## 队列
C
CyC2018 已提交
1175

C
CyC2018 已提交
1176
下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。
C
CyC2018 已提交
1177

C
CyC2018 已提交
1178
这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。
C
CyC2018 已提交
1179 1180

```java
C
CyC2018 已提交
1181
public interface MyQueue<Item> extends Iterable<Item> {
C
CyC2018 已提交
1182

C
CyC2018 已提交
1183
    int size();
C
CyC2018 已提交
1184

C
CyC2018 已提交
1185
    boolean isEmpty();
C
CyC2018 已提交
1186

C
CyC2018 已提交
1187
    MyQueue<Item> add(Item item);
C
CyC2018 已提交
1188

C
CyC2018 已提交
1189
    Item remove() throws Exception;
C
CyC2018 已提交
1190 1191 1192
}
```

C
CyC2018 已提交
1193
```java
C
CyC2018 已提交
1194
public class ListQueue<Item> implements MyQueue<Item> {
C
CyC2018 已提交
1195

C
CyC2018 已提交
1196 1197 1198
    private Node first;
    private Node last;
    int N = 0;
C
CyC2018 已提交
1199 1200


C
CyC2018 已提交
1201 1202 1203 1204
    private class Node {
        Item item;
        Node next;
    }
C
CyC2018 已提交
1205 1206


C
CyC2018 已提交
1207 1208 1209 1210
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1211 1212


C
CyC2018 已提交
1213 1214 1215 1216
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1217 1218


C
CyC2018 已提交
1219 1220
    @Override
    public MyQueue<Item> add(Item item) {
C
CyC2018 已提交
1221

C
CyC2018 已提交
1222 1223 1224
        Node newNode = new Node();
        newNode.item = item;
        newNode.next = null;
C
CyC2018 已提交
1225

C
CyC2018 已提交
1226 1227 1228 1229 1230 1231 1232
        if (isEmpty()) {
            last = newNode;
            first = newNode;
        } else {
            last.next = newNode;
            last = newNode;
        }
C
CyC2018 已提交
1233

C
CyC2018 已提交
1234 1235 1236
        N++;
        return this;
    }
C
CyC2018 已提交
1237

C
CyC2018 已提交
1238

C
CyC2018 已提交
1239 1240
    @Override
    public Item remove() throws Exception {
C
CyC2018 已提交
1241

C
CyC2018 已提交
1242 1243 1244
        if (isEmpty()) {
            throw new Exception("queue is empty");
        }
C
CyC2018 已提交
1245

C
CyC2018 已提交
1246 1247 1248
        Node node = first;
        first = first.next;
        N--;
C
CyC2018 已提交
1249

C
CyC2018 已提交
1250 1251 1252
        if (isEmpty()) {
            last = null;
        }
C
CyC2018 已提交
1253

C
CyC2018 已提交
1254 1255
        return node.item;
    }
C
CyC2018 已提交
1256 1257


C
CyC2018 已提交
1258 1259
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1260

C
CyC2018 已提交
1261
        return new Iterator<Item>() {
C
CyC2018 已提交
1262

C
CyC2018 已提交
1263
            Node cur = first;
C
CyC2018 已提交
1264 1265


C
CyC2018 已提交
1266 1267 1268 1269
            @Override
            public boolean hasNext() {
                return cur != null;
            }
C
CyC2018 已提交
1270

C
CyC2018 已提交
1271

C
CyC2018 已提交
1272 1273 1274 1275 1276 1277 1278 1279
            @Override
            public Item next() {
                Item item = cur.item;
                cur = cur.next;
                return item;
            }
        };
    }
C
CyC2018 已提交
1280 1281 1282
}
```

C
CyC2018 已提交
1283 1284 1285 1286




C
CyC2018 已提交
1287
# 六、符号表
C
CyC2018 已提交
1288

C
CyC2018 已提交
1289
符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。
C
CyC2018 已提交
1290

C
CyC2018 已提交
1291
符号表分为有序和无序两种,有序符号表主要指支持 min()、max() 等根据键的大小关系来实现的操作。
C
CyC2018 已提交
1292

C
CyC2018 已提交
1293
有序符号表的键需要实现 Comparable 接口。
C
CyC2018 已提交
1294

C
CyC2018 已提交
1295
```java
C
CyC2018 已提交
1296
public interface UnorderedST<Key, Value> {
C
CyC2018 已提交
1297

C
CyC2018 已提交
1298
    int size();
C
CyC2018 已提交
1299

C
CyC2018 已提交
1300
    Value get(Key key);
C
CyC2018 已提交
1301

C
CyC2018 已提交
1302
    void put(Key key, Value value);
C
CyC2018 已提交
1303

C
CyC2018 已提交
1304
    void delete(Key key);
C
CyC2018 已提交
1305 1306 1307 1308
}
```

```java
C
CyC2018 已提交
1309
public interface OrderedST<Key extends Comparable<Key>, Value> {
C
CyC2018 已提交
1310

C
CyC2018 已提交
1311
    int size();
C
CyC2018 已提交
1312

C
CyC2018 已提交
1313
    void put(Key key, Value value);
C
CyC2018 已提交
1314

C
CyC2018 已提交
1315
    Value get(Key key);
C
CyC2018 已提交
1316

C
CyC2018 已提交
1317
    Key min();
C
CyC2018 已提交
1318

C
CyC2018 已提交
1319
    Key max();
C
CyC2018 已提交
1320

C
CyC2018 已提交
1321
    int rank(Key key);
C
CyC2018 已提交
1322

C
CyC2018 已提交
1323
    List<Key> keys(Key l, Key h);
C
CyC2018 已提交
1324 1325 1326
}
```

C
CyC2018 已提交
1327
## 初级实现
C
CyC2018 已提交
1328

C
CyC2018 已提交
1329
### 1. 链表实现无序符号表
C
CyC2018 已提交
1330 1331

```java
C
CyC2018 已提交
1332
public class ListUnorderedST<Key, Value> implements UnorderedST<Key, Value> {
C
CyC2018 已提交
1333

C
CyC2018 已提交
1334
    private Node first;
C
CyC2018 已提交
1335

C
CyC2018 已提交
1336 1337 1338 1339
    private class Node {
        Key key;
        Value value;
        Node next;
C
CyC2018 已提交
1340

C
CyC2018 已提交
1341 1342 1343 1344 1345 1346
        Node(Key key, Value value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
C
CyC2018 已提交
1347

C
CyC2018 已提交
1348 1349 1350 1351 1352 1353 1354 1355 1356 1357
    @Override
    public int size() {
        int cnt = 0;
        Node cur = first;
        while (cur != null) {
            cnt++;
            cur = cur.next;
        }
        return cnt;
    }
C
CyC2018 已提交
1358

C
CyC2018 已提交
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372
    @Override
    public void put(Key key, Value value) {
        Node cur = first;
        // 如果在链表中找到节点的键等于 key 就更新这个节点的值为 value
        while (cur != null) {
            if (cur.key.equals(key)) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        // 否则使用头插法插入一个新节点
        first = new Node(key, value, first);
    }
C
CyC2018 已提交
1373

C
CyC2018 已提交
1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389
    @Override
    public void delete(Key key) {
        if (first == null)
            return;
        if (first.key.equals(key))
            first = first.next;
        Node pre = first, cur = first.next;
        while (cur != null) {
            if (cur.key.equals(key)) {
                pre.next = cur.next;
                return;
            }
            pre = pre.next;
            cur = cur.next;
        }
    }
C
CyC2018 已提交
1390

C
CyC2018 已提交
1391 1392 1393 1394 1395 1396 1397 1398 1399 1400
    @Override
    public Value get(Key key) {
        Node cur = first;
        while (cur != null) {
            if (cur.key.equals(key))
                return cur.value;
            cur = cur.next;
        }
        return null;
    }
C
CyC2018 已提交
1401 1402 1403
}
```

C
CyC2018 已提交
1404
### 2. 二分查找实现有序符号表
C
CyC2018 已提交
1405 1406 1407

使用一对平行数组,一个存储键一个存储值。

C
CyC2018 已提交
1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506
二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。

二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。

```java
public class BinarySearchOrderedST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {

    private Key[] keys;
    private Value[] values;
    private int N = 0;

    public BinarySearchOrderedST(int capacity) {
        keys = (Key[]) new Comparable[capacity];
        values = (Value[]) new Object[capacity];
    }

    @Override
    public int size() {
        return N;
    }

    @Override
    public int rank(Key key) {
        int l = 0, h = N - 1;
        while (l <= h) {
            int m = l + (h - l) / 2;
            int cmp = key.compareTo(keys[m]);
            if (cmp == 0)
                return m;
            else if (cmp < 0)
                h = m - 1;
            else
                l = m + 1;
        }
        return l;
    }

    @Override
    public List<Key> keys(Key l, Key h) {
        int index = rank(l);
        List<Key> list = new ArrayList<>();
        while (keys[index].compareTo(h) <= 0) {
            list.add(keys[index]);
            index++;
        }
        return list;
    }

    @Override
    public void put(Key key, Value value) {
        int index = rank(key);
        // 如果找到已经存在的节点键为 key,就更新这个节点的值为 value
        if (index < N && keys[index].compareTo(key) == 0) {
            values[index] = value;
            return;
        }
        // 否则在数组中插入新的节点,需要先将插入位置之后的元素都向后移动一个位置
        for (int j = N; j > index; j--) {
            keys[j] = keys[j - 1];
            values[j] = values[j - 1];
        }
        keys[index] = key;
        values[index] = value;
        N++;
    }

    @Override
    public Value get(Key key) {
        int index = rank(key);
        if (index < N && keys[index].compareTo(key) == 0)
            return values[index];
        return null;
    }

    @Override
    public Key min() {
        return keys[0];
    }

    @Override
    public Key max() {
        return keys[N - 1];
    }
}
```

## 二叉查找树

**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。

<img src="index_files/f9f9f993-8ece-4da7-b848-af9b438fad76.png" width="200"/>

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

<img src="index_files/8ae4550b-f0cb-4e4d-9e2b-c550538bf230.png" width="200"/>

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

<img src="index_files/fbe54203-c005-48f0-8883-b05e564a3173.png" width="200"/>
C
CyC2018 已提交
1507 1508 1509 1510

基本数据结构:

```java
C
CyC2018 已提交
1511
public class BST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {
C
CyC2018 已提交
1512

C
CyC2018 已提交
1513
    protected Node root;
C
CyC2018 已提交
1514

C
CyC2018 已提交
1515 1516 1517 1518 1519 1520 1521 1522 1523
    protected class Node {
        Key key;
        Value val;
        Node left;
        Node right;
        // 以该节点为根的子树节点总数
        int N;
        // 红黑树中使用
        boolean color;
C
CyC2018 已提交
1524

C
CyC2018 已提交
1525 1526 1527 1528 1529 1530
        Node(Key key, Value val, int N) {
            this.key = key;
            this.val = val;
            this.N = N;
        }
    }
C
CyC2018 已提交
1531

C
CyC2018 已提交
1532 1533 1534 1535
    @Override
    public int size() {
        return size(root);
    }
C
CyC2018 已提交
1536

C
CyC2018 已提交
1537 1538 1539 1540 1541
    private int size(Node x) {
        if (x == null)
            return 0;
        return x.N;
    }
C
CyC2018 已提交
1542

C
CyC2018 已提交
1543 1544 1545
    protected void recalculateSize(Node x) {
        x.N = size(x.left) + size(x.right) + 1;
    }
C
CyC2018 已提交
1546 1547 1548
}
```

C
CyC2018 已提交
1549
为了方便绘图,下文中二叉树的空链接不画出来。
C
CyC2018 已提交
1550

C
CyC2018 已提交
1551
### 1. get()
C
CyC2018 已提交
1552

C
CyC2018 已提交
1553 1554 1555
- 如果树是空的,则查找未命中;
- 如果被查找的键和根节点的键相等,查找命中;
- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。
C
CyC2018 已提交
1556 1557

```java
C
CyC2018 已提交
1558
@Override
C
CyC2018 已提交
1559 1560
public Value get(Key key) {
    return get(root, key);
C
CyC2018 已提交
1561
}
C
CyC2018 已提交
1562

C
CyC2018 已提交
1563 1564 1565 1566 1567 1568 1569 1570 1571 1572
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);
C
CyC2018 已提交
1573 1574 1575
}
```

C
CyC2018 已提交
1576
### 2. put()
C
CyC2018 已提交
1577

C
CyC2018 已提交
1578
当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接指向该节点,使得该节点正确地链接到树中。
C
CyC2018 已提交
1579

C
CyC2018 已提交
1580
<img src="index_files/107a6a2b-f15b-4cad-bced-b7fb95258c9c.png" width="200"/>
C
CyC2018 已提交
1581 1582

```java
C
CyC2018 已提交
1583 1584 1585
 @Override
public void put(Key key, Value value) {
    root = put(root, key, value);
C
CyC2018 已提交
1586
}
C
CyC2018 已提交
1587

C
CyC2018 已提交
1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
private Node put(Node x, Key key, Value value) {
    if (x == null)
        return new Node(key, value, 1);
    int cmp = key.compareTo(x.key);
    if (cmp == 0)
        x.val = value;
    else if (cmp < 0)
        x.left = put(x.left, key, value);
    else
        x.right = put(x.right, key, value);
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1600 1601 1602
}
```

C
CyC2018 已提交
1603
### 3. 分析
C
CyC2018 已提交
1604

C
CyC2018 已提交
1605 1606
二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。

C
CyC2018 已提交
1607
最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。
C
CyC2018 已提交
1608

C
CyC2018 已提交
1609
<img src="index_files/4d741402-344d-4b7c-be01-e57184bcad0e.png" width="200"/>
C
CyC2018 已提交
1610

C
CyC2018 已提交
1611
在最坏的情况下,树的高度为 N。
C
CyC2018 已提交
1612

C
CyC2018 已提交
1613
<img src="index_files/be7dca03-12ec-456b-8b54-b1b3161f5531.png" width="200"/>
C
CyC2018 已提交
1614

C
CyC2018 已提交
1615
### 4. floor()
C
CyC2018 已提交
1616 1617 1618

floor(key):小于等于键的最大键

C
CyC2018 已提交
1619 1620
- 如果键小于根节点的键,那么 floor(key) 一定在左子树中;
- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就返回,否则根节点就是 floor(key)。
C
CyC2018 已提交
1621 1622

```java
C
CyC2018 已提交
1623 1624 1625 1626 1627
public Key floor(Key key) {
    Node x = floor(root, key);
    if (x == null)
        return null;
    return x.key;
C
CyC2018 已提交
1628
}
C
CyC2018 已提交
1629

C
CyC2018 已提交
1630 1631 1632 1633 1634 1635 1636 1637 1638 1639
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);
    return t != null ? t : x;
C
CyC2018 已提交
1640 1641 1642
}
```

C
CyC2018 已提交
1643
### 5. rank()
C
CyC2018 已提交
1644

C
CyC2018 已提交
1645
rank(key) 返回 key 的排名。
C
CyC2018 已提交
1646

C
CyC2018 已提交
1647 1648 1649
- 如果键和根节点的键相等,返回左子树的节点数;
- 如果小于,递归计算在左子树中的排名;
- 如果大于,递归计算在右子树中的排名,加上左子树的节点数,再加上 1(根节点)。
C
CyC2018 已提交
1650 1651

```java
C
CyC2018 已提交
1652
@Override
C
CyC2018 已提交
1653 1654
public int rank(Key key) {
    return rank(key, root);
C
CyC2018 已提交
1655
}
C
CyC2018 已提交
1656

C
CyC2018 已提交
1657 1658 1659 1660 1661 1662 1663 1664 1665 1666
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);
C
CyC2018 已提交
1667 1668 1669
}
```

C
CyC2018 已提交
1670
### 6. min()
C
CyC2018 已提交
1671 1672

```java
C
CyC2018 已提交
1673
@Override
C
CyC2018 已提交
1674 1675
public Key min() {
    return min(root).key;
C
CyC2018 已提交
1676 1677
}

C
CyC2018 已提交
1678 1679 1680 1681 1682 1683
private Node min(Node x) {
    if (x == null)
        return null;
    if (x.left == null)
        return x;
    return min(x.left);
C
CyC2018 已提交
1684 1685 1686
}
```

C
CyC2018 已提交
1687
### 7. deleteMin()
C
CyC2018 已提交
1688 1689 1690

令指向最小节点的链接指向最小节点的右子树。

C
CyC2018 已提交
1691
<img src="index_files/dd15a984-e977-4644-b127-708cddb8ca99.png" width="500"/>
C
CyC2018 已提交
1692 1693

```java
C
CyC2018 已提交
1694 1695
public void deleteMin() {
    root = deleteMin(root);
C
CyC2018 已提交
1696
}
C
CyC2018 已提交
1697

C
CyC2018 已提交
1698 1699 1700 1701 1702 1703
public Node deleteMin(Node x) {
    if (x.left == null)
        return x.right;
    x.left = deleteMin(x.left);
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1704 1705 1706
}
```

C
CyC2018 已提交
1707
### 8. delete()
C
CyC2018 已提交
1708

C
CyC2018 已提交
1709 1710
- 如果待删除的节点只有一个子树,  那么只需要让指向待删除节点的链接指向唯一的子树即可;
- 否则,让右子树的最小节点替换该节点。
C
CyC2018 已提交
1711

C
CyC2018 已提交
1712
<img src="index_files/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png" width="400"/>
C
CyC2018 已提交
1713 1714

```java
C
CyC2018 已提交
1715 1716
public void delete(Key key) {
    root = delete(root, key);
C
CyC2018 已提交
1717
}
C
CyC2018 已提交
1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737
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;
    }
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1738 1739 1740
}
```

C
CyC2018 已提交
1741
### 9. keys()
C
CyC2018 已提交
1742 1743 1744 1745

利用二叉查找树中序遍历的结果为递增的特点。

```java
C
CyC2018 已提交
1746
@Override
C
CyC2018 已提交
1747 1748
public List<Key> keys(Key l, Key h) {
    return keys(root, l, h);
C
CyC2018 已提交
1749 1750
}

C
CyC2018 已提交
1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763
private List<Key> keys(Node x, Key l, Key h) {
    List<Key> list = new ArrayList<>();
    if (x == null)
        return list;
    int cmpL = l.compareTo(x.key);
    int cmpH = h.compareTo(x.key);
    if (cmpL < 0)
        list.addAll(keys(x.left, l, h));
    if (cmpL <= 0 && cmpH >= 0)
        list.add(x.key);
    if (cmpH > 0)
        list.addAll(keys(x.right, l, h));
    return list;
C
CyC2018 已提交
1764 1765 1766
}
```

C
CyC2018 已提交
1767
### 10. 分析
C
CyC2018 已提交
1768

C
CyC2018 已提交
1769
二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。
C
CyC2018 已提交
1770

C
CyC2018 已提交
1771
## 2-3 查找树
C
CyC2018 已提交
1772

C
CyC2018 已提交
1773
2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。
C
CyC2018 已提交
1774

C
CyC2018 已提交
1775
<img src="index_files/ff396233-1bb1-4e74-8bc2-d7c90146f5dd.png" width="250"/>
C
CyC2018 已提交
1776

C
CyC2018 已提交
1777
### 1. 插入操作
C
CyC2018 已提交
1778

C
CyC2018 已提交
1779
插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。
C
CyC2018 已提交
1780

C
CyC2018 已提交
1781
根据叶子节点的类型不同,有不同的处理方式:
C
CyC2018 已提交
1782

C
CyC2018 已提交
1783
- 如果插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。
C
CyC2018 已提交
1784

C
CyC2018 已提交
1785
<img src="index_files/7f38a583-2f2e-4738-97af-510e6fb403a7.png" width="400"/>
C
CyC2018 已提交
1786

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

C
CyC2018 已提交
1789
<img src="index_files/ef280699-da36-4b38-9735-9b048a3c7fe0.png" width="500"/>
C
CyC2018 已提交
1790

C
CyC2018 已提交
1791
### 2. 性质
C
CyC2018 已提交
1792

C
CyC2018 已提交
1793
2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。
C
CyC2018 已提交
1794

C
CyC2018 已提交
1795
2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。
C
CyC2018 已提交
1796

C
CyC2018 已提交
1797
## 红黑树
C
CyC2018 已提交
1798

C
CyC2018 已提交
1799
红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
C
CyC2018 已提交
1800

C
CyC2018 已提交
1801
<img src="index_files/4f48e806-f90b-4c09-a55f-ac0cd641c047.png" width="250"/>
C
CyC2018 已提交
1802 1803 1804

红黑树具有以下性质:

C
CyC2018 已提交
1805 1806
- 红链接都为左链接;
- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。
C
CyC2018 已提交
1807 1808 1809

画红黑树时可以将红链接画平。

C
CyC2018 已提交
1810
<img src="index_files/3086c248-b552-499e-b101-9cffe5c2773e.png" width="300"/>
C
CyC2018 已提交
1811 1812

```java
C
CyC2018 已提交
1813
public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Value> {
C
CyC2018 已提交
1814

C
CyC2018 已提交
1815 1816
    private static final boolean RED = true;
    private static final boolean BLACK = false;
C
CyC2018 已提交
1817

C
CyC2018 已提交
1818 1819 1820 1821 1822
    private boolean isRed(Node x) {
        if (x == null)
            return false;
        return x.color == RED;
    }
C
CyC2018 已提交
1823 1824 1825
}
```

C
CyC2018 已提交
1826
### 1. 左旋转
C
CyC2018 已提交
1827 1828 1829

因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。

C
CyC2018 已提交
1830
<img src="index_files/9110c1a0-8a54-4145-a814-2477d0128114.png" width="450"/>
C
CyC2018 已提交
1831 1832

```java
C
CyC2018 已提交
1833 1834 1835 1836 1837 1838 1839 1840 1841
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;
    recalculateSize(h);
    return x;
C
CyC2018 已提交
1842 1843 1844
}
```

C
CyC2018 已提交
1845
### 2. 右旋转
C
CyC2018 已提交
1846 1847 1848

进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。

C
CyC2018 已提交
1849
<img src="index_files/e2f0d889-2330-424c-8193-198edebecff7.png" width="450"/>
C
CyC2018 已提交
1850 1851

```java
C
CyC2018 已提交
1852 1853 1854 1855 1856 1857 1858 1859
public Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    recalculateSize(h);
    return x;
C
CyC2018 已提交
1860 1861 1862
}
```

C
CyC2018 已提交
1863
### 3. 颜色转换
C
CyC2018 已提交
1864

C
CyC2018 已提交
1865
一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。
C
CyC2018 已提交
1866

C
CyC2018 已提交
1867
<img src="index_files/af4639f9-af54-4400-aaf5-4e261d96ace7.png" width="300"/>
C
CyC2018 已提交
1868 1869

```java
C
CyC2018 已提交
1870 1871 1872 1873
void flipColors(Node h) {
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
C
CyC2018 已提交
1874 1875 1876
}
```

C
CyC2018 已提交
1877
### 4. 插入
C
CyC2018 已提交
1878 1879 1880

先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作:

C
CyC2018 已提交
1881 1882 1883
- 如果右子节点是红色的而左子节点是黑色的,进行左旋转;
- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转;
- 如果左右子节点均为红色的,进行颜色转换。
C
CyC2018 已提交
1884

C
CyC2018 已提交
1885
<img src="index_files/08427d38-8df1-49a1-8990-e0ce5ee36ca2.png" width="400"/>
C
CyC2018 已提交
1886 1887

```java
C
CyC2018 已提交
1888
@Override
C
CyC2018 已提交
1889 1890 1891
public void put(Key key, Value value) {
    root = put(root, key, value);
    root.color = BLACK;
C
CyC2018 已提交
1892 1893
}

C
CyC2018 已提交
1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906
private Node put(Node x, Key key, Value value) {
    if (x == null) {
        Node node = new Node(key, value, 1);
        node.color = RED;
        return node;
    }
    int cmp = key.compareTo(x.key);
    if (cmp == 0)
        x.val = value;
    else if (cmp < 0)
        x.left = put(x.left, key, value);
    else
        x.right = put(x.right, key, value);
C
CyC2018 已提交
1907

C
CyC2018 已提交
1908 1909 1910 1911 1912 1913
    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);
C
CyC2018 已提交
1914

C
CyC2018 已提交
1915 1916
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1917 1918 1919 1920 1921
}
```

可以看到该插入操作和二叉查找树的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。

C
CyC2018 已提交
1922
根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1.
C
CyC2018 已提交
1923

C
CyC2018 已提交
1924
### 5. 分析
C
CyC2018 已提交
1925

C
CyC2018 已提交
1926
一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。
C
CyC2018 已提交
1927 1928 1929

红黑树大多数的操作所需要的时间都是对数级别的。

C
CyC2018 已提交
1930
## 散列表
C
CyC2018 已提交
1931 1932 1933 1934 1935

散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。

由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。

C
CyC2018 已提交
1936
### 1. 散列函数
C
CyC2018 已提交
1937

C
CyC2018 已提交
1938
对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。
C
CyC2018 已提交
1939

C
CyC2018 已提交
1940
散列表存在冲突,也就是两个不同的键可能有相同的 hash 值。
C
CyC2018 已提交
1941 1942 1943

散列函数应该满足以下三个条件:

C
CyC2018 已提交
1944 1945 1946
- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。
- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。
- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致散列表的性能下降。
C
CyC2018 已提交
1947

C
CyC2018 已提交
1948
除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 必须是一个素数,否则无法利用键包含的所有信息。例如 M 为 10<sup>k</sup>,那么只能利用键的后 k 位。
C
CyC2018 已提交
1949

C
CyC2018 已提交
1950
对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其的二进制形式转换成整数。
C
CyC2018 已提交
1951

C
CyC2018 已提交
1952
对于多部分组合的类型,每个部分都需要计算 hash 值,这些 hash 值都具有同等重要的地位。为了达到这个目的,可以将该类型看成 R 进制的整数,每个部分都具有不同的权值。
C
CyC2018 已提交
1953

C
CyC2018 已提交
1954
例如,字符串的散列函数实现如下:
C
CyC2018 已提交
1955 1956

```java
C
CyC2018 已提交
1957 1958 1959
int hash = 0;
for (int i = 0; i < s.length(); i++)
    hash = (R * hash + s.charAt(i)) % M;
C
CyC2018 已提交
1960 1961 1962 1963 1964
```

再比如,拥有多个成员的自定义类的哈希函数如下:

```java
C
CyC2018 已提交
1965
int hash = (((day * R + month) % M) * R + year) % M;
C
CyC2018 已提交
1966 1967
```

C
CyC2018 已提交
1968
R 通常取 31。
C
CyC2018 已提交
1969

C
CyC2018 已提交
1970
Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。
C
CyC2018 已提交
1971 1972

```java
C
CyC2018 已提交
1973
int hash = (x.hashCode() & 0x7fffffff) % M;
C
CyC2018 已提交
1974 1975
```

C
CyC2018 已提交
1976
使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode():
C
CyC2018 已提交
1977 1978

```java
C
CyC2018 已提交
1979
public class Transaction {
C
CyC2018 已提交
1980

C
CyC2018 已提交
1981 1982 1983
    private final String who;
    private final Date when;
    private final double amount;
C
CyC2018 已提交
1984

C
CyC2018 已提交
1985 1986 1987 1988 1989
    public Transaction(String who, Date when, double amount) {
        this.who = who;
        this.when = when;
        this.amount = amount;
    }
C
CyC2018 已提交
1990

C
CyC2018 已提交
1991 1992 1993 1994 1995 1996 1997 1998
    public int hashCode() {
        int hash = 17;
        int R = 31;
        hash = R * hash + who.hashCode();
        hash = R * hash + when.hashCode();
        hash = R * hash + ((Double) amount).hashCode();
        return hash;
    }
C
CyC2018 已提交
1999 2000 2001
}
```

C
CyC2018 已提交
2002
### 2. 拉链法
C
CyC2018 已提交
2003

C
CyC2018 已提交
2004
拉链法使用链表来存储 hash 值相同的键,从而解决冲突。
C
CyC2018 已提交
2005

C
CyC2018 已提交
2006
查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。
C
CyC2018 已提交
2007

C
CyC2018 已提交
2008
对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 ~N/M。
C
CyC2018 已提交
2009

C
CyC2018 已提交
2010
<img src="index_files/b4252c85-6fb0-4995-9a68-a1a5925fbdb1.png" width="300"/>
C
CyC2018 已提交
2011

C
CyC2018 已提交
2012
### 3. 线性探测法
C
CyC2018 已提交
2013

C
CyC2018 已提交
2014 2015
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。

C
CyC2018 已提交
2016
使用线性探测法,数组的大小 M 应当大于键的个数 N(M>N)。
C
CyC2018 已提交
2017

C
CyC2018 已提交
2018
<img src="index_files/dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png" width="400"/>
C
CyC2018 已提交
2019 2020

```java
C
CyC2018 已提交
2021
public class LinearProbingHashST<Key, Value> implements UnorderedST<Key, Value> {
C
CyC2018 已提交
2022

C
CyC2018 已提交
2023 2024 2025 2026
    private int N = 0;
    private int M = 16;
    private Key[] keys;
    private Value[] values;
C
CyC2018 已提交
2027

C
CyC2018 已提交
2028 2029 2030
    public LinearProbingHashST() {
        init();
    }
C
CyC2018 已提交
2031

C
CyC2018 已提交
2032 2033 2034 2035
    public LinearProbingHashST(int M) {
        this.M = M;
        init();
    }
C
CyC2018 已提交
2036

C
CyC2018 已提交
2037 2038 2039 2040
    private void init() {
        keys = (Key[]) new Object[M];
        values = (Value[]) new Object[M];
    }
C
CyC2018 已提交
2041

C
CyC2018 已提交
2042 2043 2044
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }
C
CyC2018 已提交
2045 2046 2047
}
```

C
CyC2018 已提交
2048
#### 3.1 查找
C
CyC2018 已提交
2049 2050

```java
C
CyC2018 已提交
2051 2052 2053 2054
public Value get(Key key) {
    for (int i = hash(key); keys[i] != null; i = (i + 1) % M)
        if (keys[i].equals(key))
            return values[i];
C
CyC2018 已提交
2055

C
CyC2018 已提交
2056
    return null;
C
CyC2018 已提交
2057 2058 2059
}
```

C
CyC2018 已提交
2060
#### 3.2 插入
C
CyC2018 已提交
2061 2062

```java
C
CyC2018 已提交
2063 2064 2065
public void put(Key key, Value value) {
    resize();
    putInternal(key, value);
C
CyC2018 已提交
2066 2067
}

C
CyC2018 已提交
2068 2069 2070 2071 2072 2073 2074
private void putInternal(Key key, Value value) {
    int i;
    for (i = hash(key); keys[i] != null; i = (i + 1) % M)
        if (keys[i].equals(key)) {
            values[i] = value;
            return;
        }
C
CyC2018 已提交
2075

C
CyC2018 已提交
2076 2077 2078
    keys[i] = key;
    values[i] = value;
    N++;
C
CyC2018 已提交
2079 2080 2081
}
```

C
CyC2018 已提交
2082
#### 3.3 删除
C
CyC2018 已提交
2083 2084 2085 2086

删除操作应当将右侧所有相邻的键值对重新插入散列表中。

```java
C
CyC2018 已提交
2087 2088 2089 2090
public void delete(Key key) {
    int i = hash(key);
    while (keys[i] != null && !key.equals(keys[i]))
        i = (i + 1) % M;
C
CyC2018 已提交
2091

C
CyC2018 已提交
2092 2093 2094
    // 不存在,直接返回
    if (keys[i] == null)
        return;
C
CyC2018 已提交
2095

C
CyC2018 已提交
2096 2097
    keys[i] = null;
    values[i] = null;
C
CyC2018 已提交
2098

C
CyC2018 已提交
2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111
    // 将之后相连的键值对重新插入
    i = (i + 1) % M;
    while (keys[i] != null) {
        Key keyToRedo = keys[i];
        Value valToRedo = values[i];
        keys[i] = null;
        values[i] = null;
        N--;
        putInternal(keyToRedo, valToRedo);
        i = (i + 1) % M;
    }
    N--;
    resize();
C
CyC2018 已提交
2112 2113 2114
}
```

C
CyC2018 已提交
2115
#### 3.5 调整数组大小
C
CyC2018 已提交
2116

C
CyC2018 已提交
2117
线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2~5 位置就是一个聚簇。
C
CyC2018 已提交
2118

C
CyC2018 已提交
2119
<img src="index_files/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png" width="400"/>
C
CyC2018 已提交
2120

C
CyC2018 已提交
2121
α = N/M,把 α 称为使用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。
C
CyC2018 已提交
2122 2123

```java
C
CyC2018 已提交
2124 2125 2126 2127 2128
private void resize() {
    if (N >= M / 2)
        resize(2 * M);
    else if (N <= M / 8)
        resize(M / 2);
C
CyC2018 已提交
2129 2130
}

C
CyC2018 已提交
2131 2132 2133 2134 2135
private void resize(int cap) {
    LinearProbingHashST<Key, Value> t = new LinearProbingHashST<Key, Value>(cap);
    for (int i = 0; i < M; i++)
        if (keys[i] != null)
            t.putInternal(keys[i], values[i]);
C
CyC2018 已提交
2136

C
CyC2018 已提交
2137 2138 2139
    keys = t.keys;
    values = t.values;
    M = t.M;
C
CyC2018 已提交
2140 2141 2142
}
```

C
CyC2018 已提交
2143
## 小结
C
CyC2018 已提交
2144

C
CyC2018 已提交
2145
### 1. 符号表算法比较
C
CyC2018 已提交
2146

C
CyC2018 已提交
2147 2148 2149 2150 2151 2152 2153 2154
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 链表实现的无序符号表 | N | N | yes |
| 二分查找实现的有序符号表 | N | logN | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 拉链法实现的散列表 | N/M | N/M | no |
| 线性探测法实现的散列表 | 1 | 1 | no |
C
CyC2018 已提交
2155 2156 2157

应当优先考虑散列表,当需要有序性操作时使用红黑树。

C
CyC2018 已提交
2158
### 2. Java 的符号表实现
C
CyC2018 已提交
2159

C
CyC2018 已提交
2160 2161
- java.util.TreeMap:红黑树
- java.util.HashMap:拉链法的散列表
C
CyC2018 已提交
2162

C
CyC2018 已提交
2163
### 3. 稀疏向量乘法
C
CyC2018 已提交
2164

C
CyC2018 已提交
2165
当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。
C
CyC2018 已提交
2166 2167

```java
C
CyC2018 已提交
2168 2169
public class SparseVector {
    private HashMap<Integer, Double> hashMap;
C
CyC2018 已提交
2170

C
CyC2018 已提交
2171 2172 2173 2174 2175 2176
    public SparseVector(double[] vector) {
        hashMap = new HashMap<>();
        for (int i = 0; i < vector.length; i++)
            if (vector[i] != 0)
                hashMap.put(i, vector[i]);
    }
C
CyC2018 已提交
2177

C
CyC2018 已提交
2178 2179 2180
    public double get(int i) {
        return hashMap.getOrDefault(i, 0.0);
    }
C
CyC2018 已提交
2181

C
CyC2018 已提交
2182 2183 2184 2185 2186 2187
    public double dot(SparseVector other) {
        double sum = 0;
        for (int i : hashMap.keySet())
            sum += this.get(i) * other.get(i);
        return sum;
    }
C
CyC2018 已提交
2188 2189 2190
}
```

C
CyC2018 已提交
2191
# 七、其它
C
CyC2018 已提交
2192

C
CyC2018 已提交
2193
## 汉诺塔
C
CyC2018 已提交
2194

C
CyC2018 已提交
2195
<img src="index_files/54f1e052-0596-4b5e-833c-e80d75bf3f9b.png" width="300"/>
C
CyC2018 已提交
2196

C
CyC2018 已提交
2197
有三个柱子,分别为 from、buffer、to。需要将 from 上的圆盘全部移动到 to 上,并且要保证小圆盘始终在大圆盘上。
C
CyC2018 已提交
2198

C
CyC2018 已提交
2199
这是一个经典的递归问题,分为三步求解:
C
CyC2018 已提交
2200

C
CyC2018 已提交
2201
① 将 n-1 个圆盘从 from -> buffer
C
CyC2018 已提交
2202

C
CyC2018 已提交
2203
<img src="index_files/8587132a-021d-4f1f-a8ec-5a9daa7157a7.png" width="300"/>
C
CyC2018 已提交
2204

C
CyC2018 已提交
2205
② 将 1 个圆盘从 from -> to
C
CyC2018 已提交
2206

C
CyC2018 已提交
2207
<img src="index_files/2861e923-4862-4526-881c-15529279d49c.png" width="300"/>
C
CyC2018 已提交
2208

C
CyC2018 已提交
2209
③ 将 n-1 个圆盘从 buffer -> to
C
CyC2018 已提交
2210

C
CyC2018 已提交
2211
<img src="index_files/1c4e8185-8153-46b6-bd5a-288b15feeae6.png" width="300"/>
C
CyC2018 已提交
2212

C
CyC2018 已提交
2213 2214
如果只有一个圆盘,那么只需要进行一次移动操作。

C
CyC2018 已提交
2215
从上面的讨论可以知道,a<sub>n</sub> = 2 * a<sub>n-1</sub> + 1,显然 a<sub>n</sub> = 2<sup>n</sup> - 1,n 个圆盘需要移动 2<sup>n</sup> - 1 次。
C
CyC2018 已提交
2216

C
CyC2018 已提交
2217
```java
C
CyC2018 已提交
2218 2219 2220 2221 2222 2223 2224 2225 2226 2227
public class Hanoi {
    public static void move(int n, String from, String buffer, String to) {
        if (n == 1) {
            System.out.println("from " + from + " to " + to);
            return;
        }
        move(n - 1, from, to, buffer);
        move(1, from, buffer, to);
        move(n - 1, buffer, from, to);
    }
C
CyC2018 已提交
2228

C
CyC2018 已提交
2229 2230 2231
    public static void main(String[] args) {
        Hanoi.move(3, "H1", "H2", "H3");
    }
C
CyC2018 已提交
2232 2233 2234 2235
}
```

```html
C
CyC2018 已提交
2236 2237 2238 2239 2240 2241 2242
from H1 to H3
from H1 to H2
from H3 to H2
from H1 to H3
from H2 to H1
from H2 to H3
from H1 to H3
C
CyC2018 已提交
2243 2244
```

C
CyC2018 已提交
2245
## 哈夫曼编码
C
CyC2018 已提交
2246

C
CyC2018 已提交
2247
根据数据出现的频率对数据进行编码,从而压缩原始数据。
C
CyC2018 已提交
2248

C
CyC2018 已提交
2249
例如对于一个文本文件,其中各种字符出现的次数如下:
C
CyC2018 已提交
2250

C
CyC2018 已提交
2251 2252 2253 2254
- a : 10
- b : 20
- c : 40
- d : 80
C
CyC2018 已提交
2255

C
CyC2018 已提交
2256
可以将每种字符转换成二进制编码,例如将 a 转换为 00,b 转换为 01,c 转换为 10,d 转换为 11。这是最简单的一种编码方式,没有考虑各个字符的权值(出现频率)。而哈夫曼编码采用了贪心策略,使出现频率最高的字符的编码最短,从而保证整体的编码长度最短。
C
CyC2018 已提交
2257

C
CyC2018 已提交
2258
首先生成一颗哈夫曼树,每次生成过程中选取频率最少的两个节点,生成一个新节点作为它们的父节点,并且新节点的频率为两个节点的和。选取频率最少的原因是,生成过程使得先选取的节点位于树的更低层,那么需要的编码长度更长,频率更少可以使得总编码长度更少。
C
CyC2018 已提交
2259

C
CyC2018 已提交
2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370
生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。

<img src="index_files/3ff4f00a-2321-48fd-95f4-ce6001332151.png" width="400"/>

```java
public class Huffman {

    private class Node implements Comparable<Node> {
        char ch;
        int freq;
        boolean isLeaf;
        Node left, right;

        public Node(char ch, int freq) {
            this.ch = ch;
            this.freq = freq;
            isLeaf = true;
        }

        public Node(Node left, Node right, int freq) {
            this.left = left;
            this.right = right;
            this.freq = freq;
            isLeaf = false;
        }

        @Override
        public int compareTo(Node o) {
            return this.freq - o.freq;
        }
    }

    public Map<Character, String> encode(Map<Character, Integer> frequencyForChar) {
        PriorityQueue<Node> priorityQueue = new PriorityQueue<>();
        for (Character c : frequencyForChar.keySet()) {
            priorityQueue.add(new Node(c, frequencyForChar.get(c)));
        }
        while (priorityQueue.size() != 1) {
            Node node1 = priorityQueue.poll();
            Node node2 = priorityQueue.poll();
            priorityQueue.add(new Node(node1, node2, node1.freq + node2.freq));
        }
        return encode(priorityQueue.poll());
    }

    private Map<Character, String> encode(Node root) {
        Map<Character, String> encodingForChar = new HashMap<>();
        encode(root, "", encodingForChar);
        return encodingForChar;
    }

    private void encode(Node node, String encoding, Map<Character, String> encodingForChar) {
        if (node.isLeaf) {
            encodingForChar.put(node.ch, encoding);
            return;
        }
        encode(node.left, encoding + '0', encodingForChar);
        encode(node.right, encoding + '1', encodingForChar);
    }
}
```

# 参考资料

- Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011.

---bottom---CyC---
![](index_files/766aedd0-1b00-4065-aa2b-7d31138df84f.png)
![](index_files/37e79a32-95a9-4503-bdb1-159527e628b8.png)
![](index_files/1a2f2998-d0da-41c8-8222-1fd95083a66b.png)
![](index_files/2a8e1442-2381-4439-a83f-0312c8678b1f.png)
![](index_files/0157d362-98dd-4e51-ac26-00ecb76beb3e.png)
![](index_files/220790c6-4377-4a2e-8686-58398afc8a18.png)
![](index_files/f8047846-efd4-42be-b6b7-27a7c4998b51.png)
![](index_files/864bfa7d-1149-420c-a752-f9b3d4e782ec.png)
![](index_files/f3080f83-6239-459b-8e9c-03b6641f7815.png)
![](index_files/33ac2b23-cb85-4e99-bc41-b7b7199fad1c.png)
![](index_files/72f0ff69-138d-4e54-b7ac-ebe025d978dc.png)
![](index_files/b84ba6fb-312b-4e69-8c77-fb6eb6fb38d4.png)
![](index_files/51fb761d-8ce0-4472-92ff-2f227ac7888a.png)
![](index_files/b20a3466-44b4-445e-87c7-dd4fb9ef44b2.png)
![](index_files/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png)
![](index_files/8f0cc500-5994-4c7a-91a9-62885d658662.png)
![](index_files/5d4a5181-65fb-4bf2-a9c6-899cab534b44.png)
![](index_files/bfbb11e2-d208-4efa-b97b-24cd40467cd8.png)
![](index_files/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png)
![](index_files/f9f9f993-8ece-4da7-b848-af9b438fad76.png)
![](index_files/8ae4550b-f0cb-4e4d-9e2b-c550538bf230.png)
![](index_files/fbe54203-c005-48f0-8883-b05e564a3173.png)
![](index_files/107a6a2b-f15b-4cad-bced-b7fb95258c9c.png)
![](index_files/4d741402-344d-4b7c-be01-e57184bcad0e.png)
![](index_files/be7dca03-12ec-456b-8b54-b1b3161f5531.png)
![](index_files/dd15a984-e977-4644-b127-708cddb8ca99.png)
![](index_files/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png)
![](index_files/ff396233-1bb1-4e74-8bc2-d7c90146f5dd.png)
![](index_files/7f38a583-2f2e-4738-97af-510e6fb403a7.png)
![](index_files/ef280699-da36-4b38-9735-9b048a3c7fe0.png)
![](index_files/4f48e806-f90b-4c09-a55f-ac0cd641c047.png)
![](index_files/3086c248-b552-499e-b101-9cffe5c2773e.png)
![](index_files/9110c1a0-8a54-4145-a814-2477d0128114.png)
![](index_files/e2f0d889-2330-424c-8193-198edebecff7.png)
![](index_files/af4639f9-af54-4400-aaf5-4e261d96ace7.png)
![](index_files/08427d38-8df1-49a1-8990-e0ce5ee36ca2.png)
![](index_files/b4252c85-6fb0-4995-9a68-a1a5925fbdb1.png)
![](index_files/dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png)
![](index_files/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png)
![](index_files/54f1e052-0596-4b5e-833c-e80d75bf3f9b.png)
![](index_files/8587132a-021d-4f1f-a8ec-5a9daa7157a7.png)
![](index_files/2861e923-4862-4526-881c-15529279d49c.png)
![](index_files/1c4e8185-8153-46b6-bd5a-288b15feeae6.png)
![](index_files/3ff4f00a-2321-48fd-95f4-ce6001332151.png)