算法.md 65.1 KB
Newer Older
C
CyC2018 已提交
1
[⭐️ 面试进阶专栏 ⭐️](https://xiaozhuanlan.com/CyC2018)
C
CyC2018 已提交
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
<!-- GFM-TOC -->
* [一、前言](#一前言)
* [二、算法分析](#二算法分析)
    * [数学模型](#数学模型)
    * [注意事项](#注意事项)
    * [ThreeSum](#threesum)
    * [倍率实验](#倍率实验)
* [三、排序](#三排序)
    * [选择排序](#选择排序)
    * [冒泡排序](#冒泡排序)
    * [插入排序](#插入排序)
    * [希尔排序](#希尔排序)
    * [归并排序](#归并排序)
    * [快速排序](#快速排序)
    * [堆排序](#堆排序)
    * [小结](#小结)
* [四、并查集](#四并查集)
    * [Quick Find](#quick-find)
    * [Quick Union](#quick-union)
    * [加权 Quick Union](#加权-quick-union)
    * [路径压缩的加权 Quick Union](#路径压缩的加权-quick-union)
    * [比较](#比较)
* [五、栈和队列](#五栈和队列)
    * [](#栈)
    * [队列](#队列)
* [六、符号表](#六符号表)
    * [初级实现](#初级实现)
    * [二叉查找树](#二叉查找树)
    * [2-3 查找树](#2-3-查找树)
    * [红黑树](#红黑树)
    * [散列表](#散列表)
    * [小结](#小结)
* [七、其它](#七其它)
    * [汉诺塔](#汉诺塔)
    * [哈夫曼编码](#哈夫曼编码)
* [参考资料](#参考资料)
<!-- GFM-TOC -->


# 一、前言

- 实现代码:[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 已提交
59 60 61

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

C
CyC2018 已提交
62
### 4. 成本模型
C
CyC2018 已提交
63 64 65

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

C
CyC2018 已提交
66
## 注意事项
C
CyC2018 已提交
67

C
CyC2018 已提交
68
### 1. 大常数
C
CyC2018 已提交
69 70 71

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

C
CyC2018 已提交
72
### 2. 缓存
C
CyC2018 已提交
73 74 75

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

C
CyC2018 已提交
76
### 3. 对最坏情况下的性能的保证
C
CyC2018 已提交
77 78 79

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

C
CyC2018 已提交
80
### 4. 随机化算法
C
CyC2018 已提交
81 82 83

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

C
CyC2018 已提交
84
### 5. 均摊分析
C
CyC2018 已提交
85

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

C
CyC2018 已提交
88
## ThreeSum
C
CyC2018 已提交
89

C
CyC2018 已提交
90
ThreeSum 用于统计一个数组中和为 0 的三元组数量。
C
CyC2018 已提交
91 92

```java
C
CyC2018 已提交
93 94
public interface ThreeSum {
    int count(int[] nums);
C
CyC2018 已提交
95 96 97
}
```

C
CyC2018 已提交
98
### 1. ThreeSumSlow
C
CyC2018 已提交
99

C
CyC2018 已提交
100
该算法的内循环为 `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 已提交
101

C
CyC2018 已提交
102
```java
C
CyC2018 已提交
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
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 已提交
119 120 121
}
```

C
CyC2018 已提交
122
### 2. ThreeSumBinarySearch
C
CyC2018 已提交
123

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

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

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

```java
C
CyC2018 已提交
131
public class ThreeSumBinarySearch implements ThreeSum {
C
CyC2018 已提交
132

C
CyC2018 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
    @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 已提交
150 151
}
```
C
CyC2018 已提交
152

C
CyC2018 已提交
153
```java
C
CyC2018 已提交
154
public class BinarySearch {
C
CyC2018 已提交
155

C
CyC2018 已提交
156 157 158 159 160 161 162 163 164 165 166 167 168 169
    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 已提交
170 171 172
}
```

C
CyC2018 已提交
173
### 3. ThreeSumTwoPointer
C
CyC2018 已提交
174

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

```java
C
CyC2018 已提交
178
public class ThreeSumTwoPointer implements ThreeSum {
C
CyC2018 已提交
179

C
CyC2018 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    @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 已提交
205 206 207
}
```

C
CyC2018 已提交
208
## 倍率实验
C
CyC2018 已提交
209

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

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

C
CyC2018 已提交
214 215 216 217 218 219 220 221
| 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 已提交
222

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

C
CyC2018 已提交
225
```java
C
CyC2018 已提交
226
public class RatioTest {
C
CyC2018 已提交
227

C
CyC2018 已提交
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
    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 已提交
245 246 247
}
```

C
CyC2018 已提交
248
```java
C
CyC2018 已提交
249
public class StopWatch {
C
CyC2018 已提交
250

C
CyC2018 已提交
251
    private static long start;
C
CyC2018 已提交
252 253


C
CyC2018 已提交
254 255 256
    public static void start() {
        start = System.currentTimeMillis();
    }
C
CyC2018 已提交
257 258


C
CyC2018 已提交
259 260 261 262
    public static double elapsedTime() {
        long now = System.currentTimeMillis();
        return (now - start) / 1000.0;
    }
C
CyC2018 已提交
263 264 265
}
```

C
CyC2018 已提交
266
# 三、排序
C
CyC2018 已提交
267

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

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

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

C
CyC2018 已提交
274
```java
C
CyC2018 已提交
275
public abstract class Sort<T extends Comparable<T>> {
C
CyC2018 已提交
276

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

C
CyC2018 已提交
279 280 281
    protected boolean less(T v, T w) {
        return v.compareTo(w) < 0;
    }
C
CyC2018 已提交
282

C
CyC2018 已提交
283 284 285 286 287
    protected void swap(T[] a, int i, int j) {
        T t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
C
CyC2018 已提交
288 289 290
}
```

C
CyC2018 已提交
291
## 选择排序
C
CyC2018 已提交
292 293 294

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

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

C
CyC2018 已提交
297
<div align="center"> <img src="pics/37e79a32-95a9-4503-bdb1-159527e628b8.png" width="250"/> </div><br>
C
CyC2018 已提交
298 299

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

C
CyC2018 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314
    @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 已提交
315 316
}
```
C
CyC2018 已提交
317

C
CyC2018 已提交
318
## 冒泡排序
C
CyC2018 已提交
319

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

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

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

C
CyC2018 已提交
326
<div align="center"> <img src="pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png" width="250"/> </div><br>
C
CyC2018 已提交
327

C
CyC2018 已提交
328
```java
C
CyC2018 已提交
329
public class Bubble<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
330

C
CyC2018 已提交
331 332 333 334 335 336 337 338 339 340 341 342 343 344
    @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 已提交
345 346 347
}
```

C
CyC2018 已提交
348
## 插入排序
C
CyC2018 已提交
349

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

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

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

C
CyC2018 已提交
356 357 358
- 平均情况下插入排序需要 \~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 已提交
359

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

C
CyC2018 已提交
362
<div align="center"> <img src="pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png" width="250"/> </div><br>
C
CyC2018 已提交
363

C
CyC2018 已提交
364
```java
C
CyC2018 已提交
365
public class Insertion<T extends Comparable<T>> extends Sort<T> {
C
CyC2018 已提交
366

C
CyC2018 已提交
367 368 369 370 371 372 373 374 375
    @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 已提交
376
}
C
CyC2018 已提交
377 378
```

C
CyC2018 已提交
379
## 希尔排序
C
CyC2018 已提交
380

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

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

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

C
CyC2018 已提交
387
<div align="center"> <img src="pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png" width="500"/> </div><br>
C
CyC2018 已提交
388 389

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

C
CyC2018 已提交
392 393
    @Override
    public void sort(T[] nums) {
C
CyC2018 已提交
394

C
CyC2018 已提交
395 396
        int N = nums.length;
        int h = 1;
C
CyC2018 已提交
397

C
CyC2018 已提交
398 399 400
        while (h < N / 3) {
            h = 3 * h + 1; // 1, 4, 13, 40, ...
        }
C
CyC2018 已提交
401

C
CyC2018 已提交
402 403 404 405 406 407 408 409 410
        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 已提交
411
}
C
CyC2018 已提交
412

C
CyC2018 已提交
413 414
```

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

C
CyC2018 已提交
417
## 归并排序
C
CyC2018 已提交
418 419 420

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

C
CyC2018 已提交
421
<div align="center"> <img src="pics/220790c6-4377-4a2e-8686-58398afc8a18.png" width="350"/> </div><br>
C
CyC2018 已提交
422

C
CyC2018 已提交
423
### 1. 归并方法
C
CyC2018 已提交
424 425 426 427

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

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

C
CyC2018 已提交
430
    protected T[] aux;
C
CyC2018 已提交
431

C
CyC2018 已提交
432

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

C
CyC2018 已提交
435
        int i = l, j = m + 1;
C
CyC2018 已提交
436

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

C
CyC2018 已提交
441 442 443
        for (int k = l; k <= h; k++) {
            if (i > m) {
                nums[k] = aux[j++];
C
CyC2018 已提交
444

C
CyC2018 已提交
445 446
            } else if (j > h) {
                nums[k] = aux[i++];
C
CyC2018 已提交
447

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

C
CyC2018 已提交
451 452 453 454 455
            } else {
                nums[k] = aux[j++];
            }
        }
    }
C
CyC2018 已提交
456 457 458
}
```

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

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

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

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

C
CyC2018 已提交
468 469 470 471 472
    @Override
    public void sort(T[] nums) {
        aux = (T[]) new Comparable[nums.length];
        sort(nums, 0, nums.length - 1);
    }
C
CyC2018 已提交
473

C
CyC2018 已提交
474 475 476 477 478 479 480 481 482
    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 已提交
483 484 485
}
```

C
CyC2018 已提交
486
### 3. 自底向上归并排序
C
CyC2018 已提交
487 488 489 490

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

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

C
CyC2018 已提交
493 494
    @Override
    public void sort(T[] nums) {
C
CyC2018 已提交
495

C
CyC2018 已提交
496 497
        int N = nums.length;
        aux = (T[]) new Comparable[N];
C
CyC2018 已提交
498

C
CyC2018 已提交
499 500 501 502 503 504
        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 已提交
505
}
C
CyC2018 已提交
506

C
CyC2018 已提交
507 508
```

C
CyC2018 已提交
509
## 快速排序
C
CyC2018 已提交
510

C
CyC2018 已提交
511
### 1. 基本算法
C
CyC2018 已提交
512

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

C
CyC2018 已提交
516
<div align="center"> <img src="pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png" width="500"/> </div><br>
C
CyC2018 已提交
517 518

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

C
CyC2018 已提交
521 522 523 524 525
    @Override
    public void sort(T[] nums) {
        shuffle(nums);
        sort(nums, 0, nums.length - 1);
    }
C
CyC2018 已提交
526

C
CyC2018 已提交
527 528 529 530 531 532 533
    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 已提交
534

C
CyC2018 已提交
535 536 537 538 539
    private void shuffle(T[] nums) {
        List<Comparable> list = Arrays.asList(nums);
        Collections.shuffle(list);
        list.toArray(nums);
    }
C
CyC2018 已提交
540 541 542
}
```

C
CyC2018 已提交
543
### 2. 切分
C
CyC2018 已提交
544

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

C
CyC2018 已提交
547
<div align="center"> <img src="pics/766aedd0-1b00-4065-aa2b-7d31138df84f.png" width="400"/> </div><br>
C
CyC2018 已提交
548 549

```java
C
CyC2018 已提交
550 551 552 553 554 555 556 557 558 559 560 561
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 已提交
562 563 564
}
```

C
CyC2018 已提交
565
### 3. 性能分析
C
CyC2018 已提交
566 567 568

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

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

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

C
CyC2018 已提交
573
### 4. 算法改进
C
CyC2018 已提交
574

C
CyC2018 已提交
575
#### 4.1 切换到插入排序
C
CyC2018 已提交
576 577 578

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

C
CyC2018 已提交
579
#### 4.2 三数取中
C
CyC2018 已提交
580

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

C
CyC2018 已提交
583
#### 4.3 三向切分
C
CyC2018 已提交
584 585 586

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

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

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

C
CyC2018 已提交
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
    @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 已提交
612 613 614
}
```

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

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

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

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

C
CyC2018 已提交
623
```java
C
CyC2018 已提交
624 625 626 627
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 已提交
628

C
CyC2018 已提交
629 630
        if (j == k) {
            return nums[k];
C
CyC2018 已提交
631

C
CyC2018 已提交
632 633
        } else if (j > k) {
            h = j - 1;
C
CyC2018 已提交
634

C
CyC2018 已提交
635 636 637 638 639
        } else {
            l = j + 1;
        }
    }
    return nums[k];
C
CyC2018 已提交
640 641 642
}
```

C
CyC2018 已提交
643
## 堆排序
C
CyC2018 已提交
644

C
CyC2018 已提交
645
### 1. 堆
C
CyC2018 已提交
646

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

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

C
CyC2018 已提交
651
<div align="center"> <img src="pics/f3080f83-6239-459b-8e9c-03b6641f7815.png" width="200"/> </div><br>
C
CyC2018 已提交
652 653

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

C
CyC2018 已提交
656 657
    private T[] heap;
    private int N = 0;
C
CyC2018 已提交
658

C
CyC2018 已提交
659 660 661
    public Heap(int maxN) {
        this.heap = (T[]) new Comparable[maxN + 1];
    }
C
CyC2018 已提交
662

C
CyC2018 已提交
663 664 665
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
666

C
CyC2018 已提交
667 668 669
    public int size() {
        return N;
    }
C
CyC2018 已提交
670

C
CyC2018 已提交
671 672 673
    private boolean less(int i, int j) {
        return heap[i].compareTo(heap[j]) < 0;
    }
C
CyC2018 已提交
674

C
CyC2018 已提交
675 676 677 678 679
    private void swap(int i, int j) {
        T t = heap[i];
        heap[i] = heap[j];
        heap[j] = t;
    }
C
CyC2018 已提交
680 681 682
}
```

C
CyC2018 已提交
683
### 2. 上浮和下沉
C
CyC2018 已提交
684 685 686

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

C
CyC2018 已提交
687
<div align="center"> <img src="pics/33ac2b23-cb85-4e99-bc41-b7b7199fad1c.png" width="400"/> </div><br>
C
CyC2018 已提交
688 689

```java
C
CyC2018 已提交
690 691 692 693 694
private void swim(int k) {
    while (k > 1 && less(k / 2, k)) {
        swap(k / 2, k);
        k = k / 2;
    }
C
CyC2018 已提交
695 696 697
}
```

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

C
CyC2018 已提交
700
<div align="center"> <img src="pics/72f0ff69-138d-4e54-b7ac-ebe025d978dc.png" width="400"/> </div><br>
C
CyC2018 已提交
701 702

```java
C
CyC2018 已提交
703 704 705 706 707 708 709 710 711 712
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 已提交
713 714 715
}
```

C
CyC2018 已提交
716
### 3. 插入元素
C
CyC2018 已提交
717 718 719 720

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

```java
C
CyC2018 已提交
721 722 723
public void insert(Comparable v) {
    heap[++N] = v;
    swim(N);
C
CyC2018 已提交
724 725 726
}
```

C
CyC2018 已提交
727
### 4. 删除最大元素
C
CyC2018 已提交
728 729 730 731

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

```java
C
CyC2018 已提交
732 733 734 735 736 737
public T delMax() {
    T max = heap[1];
    swap(1, N--);
    heap[N + 1] = null;
    sink(1);
    return max;
C
CyC2018 已提交
738 739 740
}
```

C
CyC2018 已提交
741
### 5. 堆排序
C
CyC2018 已提交
742

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

C
CyC2018 已提交
745
#### 5.1 构建堆
C
CyC2018 已提交
746

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

C
CyC2018 已提交
749
<div align="center"> <img src="pics/b84ba6fb-312b-4e69-8c77-fb6eb6fb38d4.png" width="300"/> </div><br>
C
CyC2018 已提交
750

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

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

C
CyC2018 已提交
755
<div align="center"> <img src="pics/51fb761d-8ce0-4472-92ff-2f227ac7888a.png" width="270"/> </div><br>
C
CyC2018 已提交
756

C
CyC2018 已提交
757
<div align="center"> <img src="pics/b20a3466-44b4-445e-87c7-dd4fb9ef44b2.png" width="350"/> </div><br>
C
CyC2018 已提交
758

C
CyC2018 已提交
759
```java
C
CyC2018 已提交
760 761 762 763 764 765 766 767 768
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 已提交
769

C
CyC2018 已提交
770 771 772 773 774
        while (N > 1) {
            swap(nums, 1, N--);
            sink(nums, 1, N);
        }
    }
C
CyC2018 已提交
775

C
CyC2018 已提交
776 777 778 779 780 781 782 783 784 785 786
    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 已提交
787

C
CyC2018 已提交
788 789 790
    private boolean less(T[] nums, int i, int j) {
        return nums[i].compareTo(nums[j]) < 0;
    }
C
CyC2018 已提交
791 792 793
}
```

C
CyC2018 已提交
794
### 6. 分析
C
CyC2018 已提交
795

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

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

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

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

C
CyC2018 已提交
804
## 小结
C
CyC2018 已提交
805

C
CyC2018 已提交
806
### 1. 排序算法的比较
C
CyC2018 已提交
807

C
CyC2018 已提交
808 809 810 811 812 813 814 815 816 817
| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 |
| :---: | :---: |:---: | :---: | :---: |
| 选择排序 | × | 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 已提交
818

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

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

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

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

C
CyC2018 已提交
827
# 四、并查集
C
CyC2018 已提交
828 829 830

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

C
CyC2018 已提交
831
<div align="center"> <img src="pics/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png" width="400"/> </div><br>
C
CyC2018 已提交
832

C
CyC2018 已提交
833 834 835 836 837 838
| 方法 | 描述 |
| :---: | :---: |
| 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 已提交
839 840

```java
C
CyC2018 已提交
841
public abstract class UF {
C
CyC2018 已提交
842

C
CyC2018 已提交
843
    protected int[] id;
C
CyC2018 已提交
844

C
CyC2018 已提交
845 846 847 848 849 850
    public UF(int N) {
        id = new int[N];
        for (int i = 0; i < N; i++) {
            id[i] = i;
        }
    }
C
CyC2018 已提交
851

C
CyC2018 已提交
852 853 854
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }
C
CyC2018 已提交
855

C
CyC2018 已提交
856
    public abstract int find(int p);
C
CyC2018 已提交
857

C
CyC2018 已提交
858
    public abstract void union(int p, int q);
C
CyC2018 已提交
859 860 861
}
```

C
CyC2018 已提交
862
## Quick Find
C
CyC2018 已提交
863

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

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

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

C
CyC2018 已提交
870
<div align="center"> <img src="pics/8f0cc500-5994-4c7a-91a9-62885d658662.png" width="350"/> </div><br>
C
CyC2018 已提交
871 872

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

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

C
CyC2018 已提交
879

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

C
CyC2018 已提交
885

C
CyC2018 已提交
886 887 888 889
    @Override
    public void union(int p, int q) {
        int pID = find(p);
        int qID = find(q);
C
CyC2018 已提交
890

C
CyC2018 已提交
891 892 893
        if (pID == qID) {
            return;
        }
C
CyC2018 已提交
894

C
CyC2018 已提交
895 896 897 898 899 900
        for (int i = 0; i < id.length; i++) {
            if (id[i] == pID) {
                id[i] = qID;
            }
        }
    }
C
CyC2018 已提交
901 902 903
}
```

C
CyC2018 已提交
904
## Quick Union
C
CyC2018 已提交
905

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

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

C
CyC2018 已提交
910
<div align="center"> <img src="pics/5d4a5181-65fb-4bf2-a9c6-899cab534b44.png" width="350"/> </div><br>
C
CyC2018 已提交
911 912

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

C
CyC2018 已提交
915 916 917
    public QuickUnionUF(int N) {
        super(N);
    }
C
CyC2018 已提交
918

C
CyC2018 已提交
919

C
CyC2018 已提交
920 921 922 923 924 925 926
    @Override
    public int find(int p) {
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }
C
CyC2018 已提交
927

C
CyC2018 已提交
928

C
CyC2018 已提交
929 930 931 932
    @Override
    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
C
CyC2018 已提交
933

C
CyC2018 已提交
934 935 936 937
        if (pRoot != qRoot) {
            id[pRoot] = qRoot;
        }
    }
C
CyC2018 已提交
938 939 940
}
```

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

C
CyC2018 已提交
943
<div align="center"> <img src="pics/bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width="130"/> </div><br>
C
CyC2018 已提交
944

C
CyC2018 已提交
945
## 加权 Quick Union
C
CyC2018 已提交
946

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

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

C
CyC2018 已提交
951
<div align="center"> <img src="pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png" width="170"/> </div><br>
C
CyC2018 已提交
952

C
CyC2018 已提交
953
```java
C
CyC2018 已提交
954
public class WeightedQuickUnionUF extends UF {
C
CyC2018 已提交
955

C
CyC2018 已提交
956 957
    // 保存节点的数量信息
    private int[] sz;
C
CyC2018 已提交
958 959


C
CyC2018 已提交
960 961 962 963 964 965 966
    public WeightedQuickUnionUF(int N) {
        super(N);
        this.sz = new int[N];
        for (int i = 0; i < N; i++) {
            this.sz[i] = 1;
        }
    }
C
CyC2018 已提交
967 968


C
CyC2018 已提交
969 970 971 972 973 974 975
    @Override
    public int find(int p) {
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }
C
CyC2018 已提交
976 977


C
CyC2018 已提交
978 979
    @Override
    public void union(int p, int q) {
C
CyC2018 已提交
980

C
CyC2018 已提交
981 982
        int i = find(p);
        int j = find(q);
C
CyC2018 已提交
983

C
CyC2018 已提交
984
        if (i == j) return;
C
CyC2018 已提交
985

C
CyC2018 已提交
986 987 988 989 990 991 992 993
        if (sz[i] < sz[j]) {
            id[i] = j;
            sz[j] += sz[i];
        } else {
            id[j] = i;
            sz[i] += sz[j];
        }
    }
C
CyC2018 已提交
994 995 996
}
```

C
CyC2018 已提交
997
## 路径压缩的加权 Quick Union
C
CyC2018 已提交
998

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

C
CyC2018 已提交
1001
## 比较
C
CyC2018 已提交
1002

C
CyC2018 已提交
1003 1004 1005 1006 1007 1008
| 算法 | union | find |
| :---: | :---: | :---: |
| Quick Find | N | 1 |
| Quick Union | 树高 | 树高 |
| 加权 Quick Union | logN | logN |
| 路径压缩的加权 Quick Union | 非常接近 1 | 非常接近 1 |
C
CyC2018 已提交
1009

C
CyC2018 已提交
1010
# 五、栈和队列
C
CyC2018 已提交
1011

C
CyC2018 已提交
1012
## 栈
C
CyC2018 已提交
1013 1014

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

C
CyC2018 已提交
1017
    MyStack<Item> push(Item item);
C
CyC2018 已提交
1018

C
CyC2018 已提交
1019
    Item pop() throws Exception;
C
CyC2018 已提交
1020

C
CyC2018 已提交
1021
    boolean isEmpty();
C
CyC2018 已提交
1022

C
CyC2018 已提交
1023
    int size();
C
CyC2018 已提交
1024

C
CyC2018 已提交
1025 1026 1027
}
```

C
CyC2018 已提交
1028
### 1. 数组实现
C
CyC2018 已提交
1029 1030

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

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

C
CyC2018 已提交
1036 1037
    // 元素数量
    private int N = 0;
C
CyC2018 已提交
1038 1039


C
CyC2018 已提交
1040 1041 1042 1043 1044 1045
    @Override
    public MyStack<Item> push(Item item) {
        check();
        a[N++] = item;
        return this;
    }
C
CyC2018 已提交
1046 1047


C
CyC2018 已提交
1048 1049
    @Override
    public Item pop() throws Exception {
C
CyC2018 已提交
1050

C
CyC2018 已提交
1051 1052 1053
        if (isEmpty()) {
            throw new Exception("stack is empty");
        }
C
CyC2018 已提交
1054

C
CyC2018 已提交
1055
        Item item = a[--N];
C
CyC2018 已提交
1056

C
CyC2018 已提交
1057
        check();
C
CyC2018 已提交
1058

C
CyC2018 已提交
1059 1060
        // 避免对象游离
        a[N] = null;
C
CyC2018 已提交
1061

C
CyC2018 已提交
1062 1063
        return item;
    }
C
CyC2018 已提交
1064

C
CyC2018 已提交
1065

C
CyC2018 已提交
1066
    private void check() {
C
CyC2018 已提交
1067

C
CyC2018 已提交
1068 1069
        if (N >= a.length) {
            resize(2 * a.length);
C
CyC2018 已提交
1070

C
CyC2018 已提交
1071 1072 1073 1074
        } else if (N > 0 && N <= a.length / 4) {
            resize(a.length / 2);
        }
    }
C
CyC2018 已提交
1075 1076


C
CyC2018 已提交
1077 1078 1079 1080
    /**
     * 调整数组大小,使得栈具有伸缩性
     */
    private void resize(int size) {
C
CyC2018 已提交
1081

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

C
CyC2018 已提交
1084 1085 1086
        for (int i = 0; i < N; i++) {
            tmp[i] = a[i];
        }
C
CyC2018 已提交
1087

C
CyC2018 已提交
1088 1089
        a = tmp;
    }
C
CyC2018 已提交
1090 1091


C
CyC2018 已提交
1092 1093 1094 1095
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1096 1097


C
CyC2018 已提交
1098 1099 1100 1101
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1102 1103


C
CyC2018 已提交
1104 1105
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1106

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

C
CyC2018 已提交
1110
            private int i = N;
C
CyC2018 已提交
1111

C
CyC2018 已提交
1112 1113 1114 1115
            @Override
            public boolean hasNext() {
                return i > 0;
            }
C
CyC2018 已提交
1116

C
CyC2018 已提交
1117 1118 1119 1120 1121
            @Override
            public Item next() {
                return a[--i];
            }
        };
C
CyC2018 已提交
1122

C
CyC2018 已提交
1123
    }
C
CyC2018 已提交
1124 1125
}
```
C
CyC2018 已提交
1126

C
CyC2018 已提交
1127
### 2. 链表实现
C
CyC2018 已提交
1128

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

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

C
CyC2018 已提交
1134 1135
    private Node top = null;
    private int N = 0;
C
CyC2018 已提交
1136 1137


C
CyC2018 已提交
1138 1139 1140 1141
    private class Node {
        Item item;
        Node next;
    }
C
CyC2018 已提交
1142 1143


C
CyC2018 已提交
1144 1145
    @Override
    public MyStack<Item> push(Item item) {
C
CyC2018 已提交
1146

C
CyC2018 已提交
1147
        Node newTop = new Node();
C
CyC2018 已提交
1148

C
CyC2018 已提交
1149 1150
        newTop.item = item;
        newTop.next = top;
C
CyC2018 已提交
1151

C
CyC2018 已提交
1152
        top = newTop;
C
CyC2018 已提交
1153

C
CyC2018 已提交
1154
        N++;
C
CyC2018 已提交
1155

C
CyC2018 已提交
1156 1157
        return this;
    }
C
CyC2018 已提交
1158 1159


C
CyC2018 已提交
1160 1161
    @Override
    public Item pop() throws Exception {
C
CyC2018 已提交
1162

C
CyC2018 已提交
1163 1164 1165
        if (isEmpty()) {
            throw new Exception("stack is empty");
        }
C
CyC2018 已提交
1166

C
CyC2018 已提交
1167
        Item item = top.item;
C
CyC2018 已提交
1168

C
CyC2018 已提交
1169 1170
        top = top.next;
        N--;
C
CyC2018 已提交
1171

C
CyC2018 已提交
1172 1173
        return item;
    }
C
CyC2018 已提交
1174

C
CyC2018 已提交
1175

C
CyC2018 已提交
1176 1177 1178 1179
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1180

C
CyC2018 已提交
1181

C
CyC2018 已提交
1182 1183 1184 1185
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1186 1187


C
CyC2018 已提交
1188 1189
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1190

C
CyC2018 已提交
1191
        return new Iterator<Item>() {
C
CyC2018 已提交
1192

C
CyC2018 已提交
1193
            private Node cur = top;
C
CyC2018 已提交
1194 1195


C
CyC2018 已提交
1196 1197 1198 1199
            @Override
            public boolean hasNext() {
                return cur != null;
            }
C
CyC2018 已提交
1200 1201


C
CyC2018 已提交
1202 1203 1204 1205 1206 1207 1208
            @Override
            public Item next() {
                Item item = cur.item;
                cur = cur.next;
                return item;
            }
        };
C
CyC2018 已提交
1209

C
CyC2018 已提交
1210
    }
C
CyC2018 已提交
1211 1212 1213
}
```

C
CyC2018 已提交
1214
## 队列
C
CyC2018 已提交
1215

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

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

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

C
CyC2018 已提交
1223
    int size();
C
CyC2018 已提交
1224

C
CyC2018 已提交
1225
    boolean isEmpty();
C
CyC2018 已提交
1226

C
CyC2018 已提交
1227
    MyQueue<Item> add(Item item);
C
CyC2018 已提交
1228

C
CyC2018 已提交
1229
    Item remove() throws Exception;
C
CyC2018 已提交
1230 1231 1232
}
```

C
CyC2018 已提交
1233
```java
C
CyC2018 已提交
1234
public class ListQueue<Item> implements MyQueue<Item> {
C
CyC2018 已提交
1235

C
CyC2018 已提交
1236 1237 1238
    private Node first;
    private Node last;
    int N = 0;
C
CyC2018 已提交
1239 1240


C
CyC2018 已提交
1241 1242 1243 1244
    private class Node {
        Item item;
        Node next;
    }
C
CyC2018 已提交
1245 1246


C
CyC2018 已提交
1247 1248 1249 1250
    @Override
    public boolean isEmpty() {
        return N == 0;
    }
C
CyC2018 已提交
1251 1252


C
CyC2018 已提交
1253 1254 1255 1256
    @Override
    public int size() {
        return N;
    }
C
CyC2018 已提交
1257 1258


C
CyC2018 已提交
1259 1260
    @Override
    public MyQueue<Item> add(Item item) {
C
CyC2018 已提交
1261

C
CyC2018 已提交
1262 1263 1264
        Node newNode = new Node();
        newNode.item = item;
        newNode.next = null;
C
CyC2018 已提交
1265

C
CyC2018 已提交
1266 1267 1268 1269 1270 1271 1272
        if (isEmpty()) {
            last = newNode;
            first = newNode;
        } else {
            last.next = newNode;
            last = newNode;
        }
C
CyC2018 已提交
1273

C
CyC2018 已提交
1274 1275 1276
        N++;
        return this;
    }
C
CyC2018 已提交
1277

C
CyC2018 已提交
1278

C
CyC2018 已提交
1279 1280
    @Override
    public Item remove() throws Exception {
C
CyC2018 已提交
1281

C
CyC2018 已提交
1282 1283 1284
        if (isEmpty()) {
            throw new Exception("queue is empty");
        }
C
CyC2018 已提交
1285

C
CyC2018 已提交
1286 1287 1288
        Node node = first;
        first = first.next;
        N--;
C
CyC2018 已提交
1289

C
CyC2018 已提交
1290 1291 1292
        if (isEmpty()) {
            last = null;
        }
C
CyC2018 已提交
1293

C
CyC2018 已提交
1294 1295
        return node.item;
    }
C
CyC2018 已提交
1296 1297


C
CyC2018 已提交
1298 1299
    @Override
    public Iterator<Item> iterator() {
C
CyC2018 已提交
1300

C
CyC2018 已提交
1301
        return new Iterator<Item>() {
C
CyC2018 已提交
1302

C
CyC2018 已提交
1303
            Node cur = first;
C
CyC2018 已提交
1304 1305


C
CyC2018 已提交
1306 1307 1308 1309
            @Override
            public boolean hasNext() {
                return cur != null;
            }
C
CyC2018 已提交
1310

C
CyC2018 已提交
1311

C
CyC2018 已提交
1312 1313 1314 1315 1316 1317 1318 1319
            @Override
            public Item next() {
                Item item = cur.item;
                cur = cur.next;
                return item;
            }
        };
    }
C
CyC2018 已提交
1320 1321 1322
}
```

C
CyC2018 已提交
1323 1324 1325 1326




C
CyC2018 已提交
1327
# 六、符号表
C
CyC2018 已提交
1328

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

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

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

C
CyC2018 已提交
1335
```java
C
CyC2018 已提交
1336
public interface UnorderedST<Key, Value> {
C
CyC2018 已提交
1337

C
CyC2018 已提交
1338
    int size();
C
CyC2018 已提交
1339

C
CyC2018 已提交
1340
    Value get(Key key);
C
CyC2018 已提交
1341

C
CyC2018 已提交
1342
    void put(Key key, Value value);
C
CyC2018 已提交
1343

C
CyC2018 已提交
1344
    void delete(Key key);
C
CyC2018 已提交
1345 1346 1347 1348
}
```

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

C
CyC2018 已提交
1351
    int size();
C
CyC2018 已提交
1352

C
CyC2018 已提交
1353
    void put(Key key, Value value);
C
CyC2018 已提交
1354

C
CyC2018 已提交
1355
    Value get(Key key);
C
CyC2018 已提交
1356

C
CyC2018 已提交
1357
    Key min();
C
CyC2018 已提交
1358

C
CyC2018 已提交
1359
    Key max();
C
CyC2018 已提交
1360

C
CyC2018 已提交
1361
    int rank(Key key);
C
CyC2018 已提交
1362

C
CyC2018 已提交
1363
    List<Key> keys(Key l, Key h);
C
CyC2018 已提交
1364 1365 1366
}
```

C
CyC2018 已提交
1367
## 初级实现
C
CyC2018 已提交
1368

C
CyC2018 已提交
1369
### 1. 链表实现无序符号表
C
CyC2018 已提交
1370 1371

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

C
CyC2018 已提交
1374
    private Node first;
C
CyC2018 已提交
1375

C
CyC2018 已提交
1376 1377 1378 1379
    private class Node {
        Key key;
        Value value;
        Node next;
C
CyC2018 已提交
1380

C
CyC2018 已提交
1381 1382 1383 1384 1385 1386
        Node(Key key, Value value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
C
CyC2018 已提交
1387

C
CyC2018 已提交
1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
    @Override
    public int size() {
        int cnt = 0;
        Node cur = first;
        while (cur != null) {
            cnt++;
            cur = cur.next;
        }
        return cnt;
    }
C
CyC2018 已提交
1398

C
CyC2018 已提交
1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412
    @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 已提交
1413

C
CyC2018 已提交
1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429
    @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 已提交
1430

C
CyC2018 已提交
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440
    @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 已提交
1441 1442 1443
}
```

C
CyC2018 已提交
1444
### 2. 二分查找实现有序符号表
C
CyC2018 已提交
1445 1446 1447

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

C
CyC2018 已提交
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 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546
二分查找的 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];
    }
}
```

## 二叉查找树

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

<div align="center"> <img src="pics/f9f9f993-8ece-4da7-b848-af9b438fad76.png" width="200"/> </div><br>

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

<div align="center"> <img src="pics/8ae4550b-f0cb-4e4d-9e2b-c550538bf230.png" width="200"/> </div><br>

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

<div align="center"> <img src="pics/fbe54203-c005-48f0-8883-b05e564a3173.png" width="200"/> </div><br>
C
CyC2018 已提交
1547 1548 1549 1550

基本数据结构:

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

C
CyC2018 已提交
1553
    protected Node root;
C
CyC2018 已提交
1554

C
CyC2018 已提交
1555 1556 1557 1558 1559 1560 1561 1562 1563
    protected class Node {
        Key key;
        Value val;
        Node left;
        Node right;
        // 以该节点为根的子树节点总数
        int N;
        // 红黑树中使用
        boolean color;
C
CyC2018 已提交
1564

C
CyC2018 已提交
1565 1566 1567 1568 1569 1570
        Node(Key key, Value val, int N) {
            this.key = key;
            this.val = val;
            this.N = N;
        }
    }
C
CyC2018 已提交
1571

C
CyC2018 已提交
1572 1573 1574 1575
    @Override
    public int size() {
        return size(root);
    }
C
CyC2018 已提交
1576

C
CyC2018 已提交
1577 1578 1579 1580 1581
    private int size(Node x) {
        if (x == null)
            return 0;
        return x.N;
    }
C
CyC2018 已提交
1582

C
CyC2018 已提交
1583 1584 1585
    protected void recalculateSize(Node x) {
        x.N = size(x.left) + size(x.right) + 1;
    }
C
CyC2018 已提交
1586 1587 1588
}
```

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

C
CyC2018 已提交
1591
### 1. get()
C
CyC2018 已提交
1592

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

```java
C
CyC2018 已提交
1598
@Override
C
CyC2018 已提交
1599 1600
public Value get(Key key) {
    return get(root, key);
C
CyC2018 已提交
1601
}
C
CyC2018 已提交
1602

C
CyC2018 已提交
1603 1604 1605 1606 1607 1608 1609 1610 1611 1612
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 已提交
1613 1614 1615
}
```

C
CyC2018 已提交
1616
### 2. put()
C
CyC2018 已提交
1617

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

C
CyC2018 已提交
1620
<div align="center"> <img src="pics/107a6a2b-f15b-4cad-bced-b7fb95258c9c.png" width="200"/> </div><br>
C
CyC2018 已提交
1621 1622

```java
C
CyC2018 已提交
1623 1624 1625
 @Override
public void put(Key key, Value value) {
    root = put(root, key, value);
C
CyC2018 已提交
1626
}
C
CyC2018 已提交
1627

C
CyC2018 已提交
1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639
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 已提交
1640 1641 1642
}
```

C
CyC2018 已提交
1643
### 3. 分析
C
CyC2018 已提交
1644

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

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

C
CyC2018 已提交
1649
<div align="center"> <img src="pics/4d741402-344d-4b7c-be01-e57184bcad0e.png" width="200"/> </div><br>
C
CyC2018 已提交
1650

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

C
CyC2018 已提交
1653
<div align="center"> <img src="pics/be7dca03-12ec-456b-8b54-b1b3161f5531.png" width="200"/> </div><br>
C
CyC2018 已提交
1654

C
CyC2018 已提交
1655
### 4. floor()
C
CyC2018 已提交
1656 1657 1658

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

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

```java
C
CyC2018 已提交
1663 1664 1665 1666 1667
public Key floor(Key key) {
    Node x = floor(root, key);
    if (x == null)
        return null;
    return x.key;
C
CyC2018 已提交
1668
}
C
CyC2018 已提交
1669

C
CyC2018 已提交
1670 1671 1672 1673 1674 1675 1676 1677 1678 1679
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 已提交
1680 1681 1682
}
```

C
CyC2018 已提交
1683
### 5. rank()
C
CyC2018 已提交
1684

C
CyC2018 已提交
1685
rank(key) 返回 key 的排名。
C
CyC2018 已提交
1686

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

```java
C
CyC2018 已提交
1692
@Override
C
CyC2018 已提交
1693 1694
public int rank(Key key) {
    return rank(key, root);
C
CyC2018 已提交
1695
}
C
CyC2018 已提交
1696

C
CyC2018 已提交
1697 1698 1699 1700 1701 1702 1703 1704 1705 1706
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 已提交
1707 1708 1709
}
```

C
CyC2018 已提交
1710
### 6. min()
C
CyC2018 已提交
1711 1712

```java
C
CyC2018 已提交
1713
@Override
C
CyC2018 已提交
1714 1715
public Key min() {
    return min(root).key;
C
CyC2018 已提交
1716 1717
}

C
CyC2018 已提交
1718 1719 1720 1721 1722 1723
private Node min(Node x) {
    if (x == null)
        return null;
    if (x.left == null)
        return x;
    return min(x.left);
C
CyC2018 已提交
1724 1725 1726
}
```

C
CyC2018 已提交
1727
### 7. deleteMin()
C
CyC2018 已提交
1728 1729 1730

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

C
CyC2018 已提交
1731
<div align="center"> <img src="pics/dd15a984-e977-4644-b127-708cddb8ca99.png" width="500"/> </div><br>
C
CyC2018 已提交
1732 1733

```java
C
CyC2018 已提交
1734 1735
public void deleteMin() {
    root = deleteMin(root);
C
CyC2018 已提交
1736
}
C
CyC2018 已提交
1737

C
CyC2018 已提交
1738 1739 1740 1741 1742 1743
public Node deleteMin(Node x) {
    if (x.left == null)
        return x.right;
    x.left = deleteMin(x.left);
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1744 1745 1746
}
```

C
CyC2018 已提交
1747
### 8. delete()
C
CyC2018 已提交
1748

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

C
CyC2018 已提交
1752
<div align="center"> <img src="pics/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png" width="400"/> </div><br>
C
CyC2018 已提交
1753 1754

```java
C
CyC2018 已提交
1755 1756
public void delete(Key key) {
    root = delete(root, key);
C
CyC2018 已提交
1757
}
C
CyC2018 已提交
1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777
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 已提交
1778 1779 1780
}
```

C
CyC2018 已提交
1781
### 9. keys()
C
CyC2018 已提交
1782 1783 1784 1785

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

```java
C
CyC2018 已提交
1786
@Override
C
CyC2018 已提交
1787 1788
public List<Key> keys(Key l, Key h) {
    return keys(root, l, h);
C
CyC2018 已提交
1789 1790
}

C
CyC2018 已提交
1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803
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 已提交
1804 1805 1806
}
```

C
CyC2018 已提交
1807
### 10. 分析
C
CyC2018 已提交
1808

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

C
CyC2018 已提交
1811
## 2-3 查找树
C
CyC2018 已提交
1812

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

C
CyC2018 已提交
1815
<div align="center"> <img src="pics/ff396233-1bb1-4e74-8bc2-d7c90146f5dd.png" width="250"/> </div><br>
C
CyC2018 已提交
1816

C
CyC2018 已提交
1817
### 1. 插入操作
C
CyC2018 已提交
1818

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

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

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

C
CyC2018 已提交
1825
<div align="center"> <img src="pics/7f38a583-2f2e-4738-97af-510e6fb403a7.png" width="400"/> </div><br>
C
CyC2018 已提交
1826

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

C
CyC2018 已提交
1829
<div align="center"> <img src="pics/ef280699-da36-4b38-9735-9b048a3c7fe0.png" width="500"/> </div><br>
C
CyC2018 已提交
1830

C
CyC2018 已提交
1831
### 2. 性质
C
CyC2018 已提交
1832

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

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

C
CyC2018 已提交
1837
## 红黑树
C
CyC2018 已提交
1838

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

C
CyC2018 已提交
1841
<div align="center"> <img src="pics/4f48e806-f90b-4c09-a55f-ac0cd641c047.png" width="250"/> </div><br>
C
CyC2018 已提交
1842 1843 1844

红黑树具有以下性质:

C
CyC2018 已提交
1845 1846
- 红链接都为左链接;
- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。
C
CyC2018 已提交
1847 1848 1849

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

C
CyC2018 已提交
1850
<div align="center"> <img src="pics/3086c248-b552-499e-b101-9cffe5c2773e.png" width="300"/> </div><br>
C
CyC2018 已提交
1851 1852

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

C
CyC2018 已提交
1855 1856
    private static final boolean RED = true;
    private static final boolean BLACK = false;
C
CyC2018 已提交
1857

C
CyC2018 已提交
1858 1859 1860 1861 1862
    private boolean isRed(Node x) {
        if (x == null)
            return false;
        return x.color == RED;
    }
C
CyC2018 已提交
1863 1864 1865
}
```

C
CyC2018 已提交
1866
### 1. 左旋转
C
CyC2018 已提交
1867 1868 1869

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

C
CyC2018 已提交
1870
<div align="center"> <img src="pics/9110c1a0-8a54-4145-a814-2477d0128114.png" width="450"/> </div><br>
C
CyC2018 已提交
1871 1872

```java
C
CyC2018 已提交
1873 1874 1875 1876 1877 1878 1879 1880 1881
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 已提交
1882 1883 1884
}
```

C
CyC2018 已提交
1885
### 2. 右旋转
C
CyC2018 已提交
1886 1887 1888

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

C
CyC2018 已提交
1889
<div align="center"> <img src="pics/e2f0d889-2330-424c-8193-198edebecff7.png" width="450"/> </div><br>
C
CyC2018 已提交
1890 1891

```java
C
CyC2018 已提交
1892 1893 1894 1895 1896 1897 1898 1899
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 已提交
1900 1901 1902
}
```

C
CyC2018 已提交
1903
### 3. 颜色转换
C
CyC2018 已提交
1904

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

C
CyC2018 已提交
1907
<div align="center"> <img src="pics/af4639f9-af54-4400-aaf5-4e261d96ace7.png" width="300"/> </div><br>
C
CyC2018 已提交
1908 1909

```java
C
CyC2018 已提交
1910 1911 1912 1913
void flipColors(Node h) {
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
C
CyC2018 已提交
1914 1915 1916
}
```

C
CyC2018 已提交
1917
### 4. 插入
C
CyC2018 已提交
1918 1919 1920

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

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

C
CyC2018 已提交
1925
<div align="center"> <img src="pics/08427d38-8df1-49a1-8990-e0ce5ee36ca2.png" width="400"/> </div><br>
C
CyC2018 已提交
1926 1927

```java
C
CyC2018 已提交
1928
@Override
C
CyC2018 已提交
1929 1930 1931
public void put(Key key, Value value) {
    root = put(root, key, value);
    root.color = BLACK;
C
CyC2018 已提交
1932 1933
}

C
CyC2018 已提交
1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946
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 已提交
1947

C
CyC2018 已提交
1948 1949 1950 1951 1952 1953
    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 已提交
1954

C
CyC2018 已提交
1955 1956
    recalculateSize(x);
    return x;
C
CyC2018 已提交
1957 1958 1959 1960 1961
}
```

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

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

C
CyC2018 已提交
1964
### 5. 分析
C
CyC2018 已提交
1965

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

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

C
CyC2018 已提交
1970
## 散列表
C
CyC2018 已提交
1971 1972 1973 1974 1975

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

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

C
CyC2018 已提交
1976
### 1. 散列函数
C
CyC2018 已提交
1977

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

C
CyC2018 已提交
1980
散列表存在冲突,也就是两个不同的键可能有相同的 hash 值。
C
CyC2018 已提交
1981 1982 1983

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

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

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

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

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

C
CyC2018 已提交
1994
例如,字符串的散列函数实现如下:
C
CyC2018 已提交
1995 1996

```java
C
CyC2018 已提交
1997 1998 1999
int hash = 0;
for (int i = 0; i < s.length(); i++)
    hash = (R * hash + s.charAt(i)) % M;
C
CyC2018 已提交
2000 2001 2002 2003 2004
```

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

```java
C
CyC2018 已提交
2005
int hash = (((day * R + month) % M) * R + year) % M;
C
CyC2018 已提交
2006 2007
```

C
CyC2018 已提交
2008
R 通常取 31。
C
CyC2018 已提交
2009

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

```java
C
CyC2018 已提交
2013
int hash = (x.hashCode() & 0x7fffffff) % M;
C
CyC2018 已提交
2014 2015
```

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

```java
C
CyC2018 已提交
2019
public class Transaction {
C
CyC2018 已提交
2020

C
CyC2018 已提交
2021 2022 2023
    private final String who;
    private final Date when;
    private final double amount;
C
CyC2018 已提交
2024

C
CyC2018 已提交
2025 2026 2027 2028 2029
    public Transaction(String who, Date when, double amount) {
        this.who = who;
        this.when = when;
        this.amount = amount;
    }
C
CyC2018 已提交
2030

C
CyC2018 已提交
2031 2032 2033 2034 2035 2036 2037 2038
    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 已提交
2039 2040 2041
}
```

C
CyC2018 已提交
2042
### 2. 拉链法
C
CyC2018 已提交
2043

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

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

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

C
CyC2018 已提交
2050
<div align="center"> <img src="pics/b4252c85-6fb0-4995-9a68-a1a5925fbdb1.png" width="300"/> </div><br>
C
CyC2018 已提交
2051

C
CyC2018 已提交
2052
### 3. 线性探测法
C
CyC2018 已提交
2053

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

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

C
CyC2018 已提交
2058
<div align="center"> <img src="pics/dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png" width="400"/> </div><br>
C
CyC2018 已提交
2059 2060

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

C
CyC2018 已提交
2063 2064 2065 2066
    private int N = 0;
    private int M = 16;
    private Key[] keys;
    private Value[] values;
C
CyC2018 已提交
2067

C
CyC2018 已提交
2068 2069 2070
    public LinearProbingHashST() {
        init();
    }
C
CyC2018 已提交
2071

C
CyC2018 已提交
2072 2073 2074 2075
    public LinearProbingHashST(int M) {
        this.M = M;
        init();
    }
C
CyC2018 已提交
2076

C
CyC2018 已提交
2077 2078 2079 2080
    private void init() {
        keys = (Key[]) new Object[M];
        values = (Value[]) new Object[M];
    }
C
CyC2018 已提交
2081

C
CyC2018 已提交
2082 2083 2084
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }
C
CyC2018 已提交
2085 2086 2087
}
```

C
CyC2018 已提交
2088
#### 3.1 查找
C
CyC2018 已提交
2089 2090

```java
C
CyC2018 已提交
2091 2092 2093 2094
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 已提交
2095

C
CyC2018 已提交
2096
    return null;
C
CyC2018 已提交
2097 2098 2099
}
```

C
CyC2018 已提交
2100
#### 3.2 插入
C
CyC2018 已提交
2101 2102

```java
C
CyC2018 已提交
2103 2104 2105
public void put(Key key, Value value) {
    resize();
    putInternal(key, value);
C
CyC2018 已提交
2106 2107
}

C
CyC2018 已提交
2108 2109 2110 2111 2112 2113 2114
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 已提交
2115

C
CyC2018 已提交
2116 2117 2118
    keys[i] = key;
    values[i] = value;
    N++;
C
CyC2018 已提交
2119 2120 2121
}
```

C
CyC2018 已提交
2122
#### 3.3 删除
C
CyC2018 已提交
2123 2124 2125 2126

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

```java
C
CyC2018 已提交
2127 2128 2129 2130
public void delete(Key key) {
    int i = hash(key);
    while (keys[i] != null && !key.equals(keys[i]))
        i = (i + 1) % M;
C
CyC2018 已提交
2131

C
CyC2018 已提交
2132 2133 2134
    // 不存在,直接返回
    if (keys[i] == null)
        return;
C
CyC2018 已提交
2135

C
CyC2018 已提交
2136 2137
    keys[i] = null;
    values[i] = null;
C
CyC2018 已提交
2138

C
CyC2018 已提交
2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151
    // 将之后相连的键值对重新插入
    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 已提交
2152 2153 2154
}
```

C
CyC2018 已提交
2155
#### 3.5 调整数组大小
C
CyC2018 已提交
2156

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

C
CyC2018 已提交
2159
<div align="center"> <img src="pics/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png" width="400"/> </div><br>
C
CyC2018 已提交
2160

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

```java
C
CyC2018 已提交
2164 2165 2166 2167 2168
private void resize() {
    if (N >= M / 2)
        resize(2 * M);
    else if (N <= M / 8)
        resize(M / 2);
C
CyC2018 已提交
2169 2170
}

C
CyC2018 已提交
2171 2172 2173 2174 2175
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 已提交
2176

C
CyC2018 已提交
2177 2178 2179
    keys = t.keys;
    values = t.values;
    M = t.M;
C
CyC2018 已提交
2180 2181 2182
}
```

C
CyC2018 已提交
2183
## 小结
C
CyC2018 已提交
2184

C
CyC2018 已提交
2185
### 1. 符号表算法比较
C
CyC2018 已提交
2186

C
CyC2018 已提交
2187 2188 2189 2190 2191 2192 2193 2194
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 链表实现的无序符号表 | N | N | yes |
| 二分查找实现的有序符号表 | N | logN | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 拉链法实现的散列表 | N/M | N/M | no |
| 线性探测法实现的散列表 | 1 | 1 | no |
C
CyC2018 已提交
2195 2196 2197

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

C
CyC2018 已提交
2198
### 2. Java 的符号表实现
C
CyC2018 已提交
2199

C
CyC2018 已提交
2200 2201
- java.util.TreeMap:红黑树
- java.util.HashMap:拉链法的散列表
C
CyC2018 已提交
2202

C
CyC2018 已提交
2203
### 3. 稀疏向量乘法
C
CyC2018 已提交
2204

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

```java
C
CyC2018 已提交
2208 2209
public class SparseVector {
    private HashMap<Integer, Double> hashMap;
C
CyC2018 已提交
2210

C
CyC2018 已提交
2211 2212 2213 2214 2215 2216
    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 已提交
2217

C
CyC2018 已提交
2218 2219 2220
    public double get(int i) {
        return hashMap.getOrDefault(i, 0.0);
    }
C
CyC2018 已提交
2221

C
CyC2018 已提交
2222 2223 2224 2225 2226 2227
    public double dot(SparseVector other) {
        double sum = 0;
        for (int i : hashMap.keySet())
            sum += this.get(i) * other.get(i);
        return sum;
    }
C
CyC2018 已提交
2228 2229 2230
}
```

C
CyC2018 已提交
2231
# 七、其它
C
CyC2018 已提交
2232

C
CyC2018 已提交
2233
## 汉诺塔
C
CyC2018 已提交
2234

C
CyC2018 已提交
2235
<div align="center"> <img src="pics/54f1e052-0596-4b5e-833c-e80d75bf3f9b.png" width="300"/> </div><br>
C
CyC2018 已提交
2236

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

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

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

C
CyC2018 已提交
2243
<div align="center"> <img src="pics/8587132a-021d-4f1f-a8ec-5a9daa7157a7.png" width="300"/> </div><br>
C
CyC2018 已提交
2244

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

C
CyC2018 已提交
2247
<div align="center"> <img src="pics/2861e923-4862-4526-881c-15529279d49c.png" width="300"/> </div><br>
C
CyC2018 已提交
2248

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

C
CyC2018 已提交
2251
<div align="center"> <img src="pics/1c4e8185-8153-46b6-bd5a-288b15feeae6.png" width="300"/> </div><br>
C
CyC2018 已提交
2252

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

C
CyC2018 已提交
2255
从上面的讨论可以知道,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 已提交
2256

C
CyC2018 已提交
2257
```java
C
CyC2018 已提交
2258 2259 2260 2261 2262 2263 2264 2265 2266 2267
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 已提交
2268

C
CyC2018 已提交
2269 2270 2271
    public static void main(String[] args) {
        Hanoi.move(3, "H1", "H2", "H3");
    }
C
CyC2018 已提交
2272 2273 2274 2275
}
```

```html
C
CyC2018 已提交
2276 2277 2278 2279 2280 2281 2282
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 已提交
2283 2284
```

C
CyC2018 已提交
2285
## 哈夫曼编码
C
CyC2018 已提交
2286

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

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

C
CyC2018 已提交
2291 2292 2293 2294
- a : 10
- b : 20
- c : 40
- d : 80
C
CyC2018 已提交
2295

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

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

C
CyC2018 已提交
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
生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。

<div align="center"> <img src="pics/3ff4f00a-2321-48fd-95f4-ce6001332151.png" width="400"/> </div><br>

```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.