From 61b93e53fdfb8a39789ebf1d880ec157fb999583 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Thu, 1 Mar 2018 09:31:51 +0800 Subject: [PATCH] auto commit --- notes/算法.md | 604 +++++++++++++------------------------------------- 1 file changed, 152 insertions(+), 452 deletions(-) diff --git a/notes/算法.md b/notes/算法.md index 0a3613af..bdad52f6 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -1,255 +1,86 @@ -* [第一章 基础](#第一章-基础) - * [栈](#栈) - * [队列](#队列) - * [算法分析](#算法分析) - * [1. 函数转换](#1-函数转换) - * [2. 数学模型](#2-数学模型) - * [3. ThreeSum](#3-threesum) - * [4. 倍率实验](#4-倍率实验) - * [5. 注意事项](#5-注意事项) - * [union-find](#union-find) - * [1. quick-find 算法](#1-quick-find-算法) - * [2. quick-union 算法](#2-quick-union-算法) - * [3. 加权 quick-union 算法](#3-加权-quick-union-算法) - * [4. 路径压缩的加权 quick-union 算法](#4-路径压缩的加权-quick-union-算法) - * [5. 各种 union-find 算法的比较](#5-各种-union-find-算法的比较) -* [第二章 排序](#第二章-排序) - * [初级排序算法](#初级排序算法) - * [1. 约定](#1-约定) - * [2. 选择排序](#2-选择排序) - * [3. 插入排序](#3-插入排序) - * [4. 选择排序和插入排序的比较](#4-选择排序和插入排序的比较) - * [5. 希尔排序](#5-希尔排序) - * [归并排序](#归并排序) - * [1. 归并方法](#1-归并方法) - * [2. 自顶向下归并排序](#2-自顶向下归并排序) - * [3. 自底向上归并排序](#3-自底向上归并排序) - * [快速排序](#快速排序) - * [1. 基本算法](#1-基本算法) - * [2. 切分](#2-切分) - * [3. 性能分析](#3-性能分析) - * [4. 算法改进](#4-算法改进) - * [4.1 切换到插入排序](#41-切换到插入排序) - * [4.2 三取样](#42-三取样) - * [4.3 三向切分](#43-三向切分) - * [优先队列](#优先队列) - * [1. 堆](#1-堆) - * [2. 上浮和下沉](#2-上浮和下沉) - * [3. 插入元素](#3-插入元素) - * [4. 删除最大元素](#4-删除最大元素) - * [5. 堆排序](#5-堆排序) - * [6. 分析](#6-分析) - * [应用](#应用) - * [1. 排序算法的比较](#1-排序算法的比较) - * [2. Java 的排序算法实现](#2-java-的排序算法实现) - * [3. 基于切分的快速选择算法](#3-基于切分的快速选择算法) -* [第三章 查找](#第三章-查找) - * [符号表](#符号表) - * [1. 无序符号表](#1-无序符号表) - * [2. 有序符号表](#2-有序符号表) - * [3. 二分查找实现有序符号表](#3-二分查找实现有序符号表) - * [二叉查找树](#二叉查找树) - * [1. get()](#1-get) - * [2. put()](#2-put) - * [3. 分析](#3-分析) - * [4. floor()](#4-floor) - * [5. rank()](#5-rank) - * [6. min()](#6-min) - * [7. deleteMin()](#7-deletemin) - * [8. delete()](#8-delete) - * [9. keys()](#9-keys) - * [10. 性能分析](#10-性能分析) - * [平衡查找树](#平衡查找树) - * [2-3 查找树](#2-3-查找树) - * [1. 插入操作](#1-插入操作) - * [2. 性质](#2-性质) - * [红黑二叉查找树](#红黑二叉查找树) - * [1. 左旋转](#1-左旋转) - * [2. 右旋转](#2-右旋转) - * [3. 颜色转换](#3-颜色转换) - * [4. 插入](#4-插入) - * [5. 删除最小键](#5-删除最小键) - * [6. 分析](#6-分析) - * [散列表](#散列表) - * [1. 散列函数](#1-散列函数) - * [2. 基于拉链法的散列表](#2-基于拉链法的散列表) - * [3. 基于线性探测法的散列表](#3-基于线性探测法的散列表) - * [3.1 查找](#31-查找) - * [3.2 插入](#32-插入) - * [3.3 删除](#33-删除) - * [3.4 调整数组大小](#34-调整数组大小) - * [应用](#应用) - * [1. 各种符号表实现的比较](#1-各种符号表实现的比较) - * [2. Java 的符号表实现](#2-java-的符号表实现) - * [3. 集合类型](#3-集合类型) - * [4. 稀疏向量乘法](#4-稀疏向量乘法) +* [算法分析](#算法分析) + * [1. 函数转换](#1-函数转换) + * [2. 数学模型](#2-数学模型) + * [3. ThreeSum](#3-threesum) + * [4. 倍率实验](#4-倍率实验) + * [5. 注意事项](#5-注意事项) +* [排序](#排序) + * [1. 初级排序算法](#1-初级排序算法) + * [1.1 约定](#11-约定) + * [1.2 选择排序](#12-选择排序) + * [1.3 插入排序](#13-插入排序) + * [1.4 选择排序和插入排序的比较](#14-选择排序和插入排序的比较) + * [1.5 希尔排序](#15-希尔排序) + * [2 归并排序](#2-归并排序) + * [2.1 归并方法](#21-归并方法) + * [2.2 自顶向下归并排序](#22-自顶向下归并排序) + * [2.3 自底向上归并排序](#23-自底向上归并排序) + * [3.快速排序](#3快速排序) + * [3.1 基本算法](#31-基本算法) + * [3.2 切分](#32-切分) + * [3.3 性能分析](#33-性能分析) + * [3.4 算法改进](#34-算法改进) + * [3.4.1 切换到插入排序](#341-切换到插入排序) + * [3.4.2 三取样](#342-三取样) + * [3.4.3 三向切分](#343-三向切分) + * [4. 优先队列](#4-优先队列) + * [4.1 堆](#41-堆) + * [4.2 上浮和下沉](#42-上浮和下沉) + * [4.3 插入元素](#43-插入元素) + * [4.4 删除最大元素](#44-删除最大元素) + * [4.5 堆排序](#45-堆排序) + * [4.6 分析](#46-分析) + * [5. 应用](#5-应用) + * [5.1 排序算法的比较](#51-排序算法的比较) + * [5.2 Java 的排序算法实现](#52-java-的排序算法实现) + * [5.3 基于切分的快速选择算法](#53-基于切分的快速选择算法) +* [查找](#查找) + * [1. 符号表](#1-符号表) + * [1.1 无序符号表](#11-无序符号表) + * [1.2 有序符号表](#12-有序符号表) + * [1.3 二分查找实现有序符号表](#13-二分查找实现有序符号表) + * [2. 二叉查找树](#2-二叉查找树) + * [2.1 get()](#21-get) + * [2.2 put()](#22-put) + * [2.3 分析](#23-分析) + * [2.4 floor()](#24-floor) + * [2.5 rank()](#25-rank) + * [2.6 min()](#26-min) + * [2.7 deleteMin()](#27-deletemin) + * [2.8 delete()](#28-delete) + * [2.9 keys()](#29-keys) + * [2.10 性能分析](#210-性能分析) + * [3. 平衡查找树](#3-平衡查找树) + * [3.1 2-3 查找树](#31-2-3-查找树) + * [3.1.1 插入操作](#311-插入操作) + * [3.1.2 性质](#312-性质) + * [3.2 红黑二叉查找树](#32-红黑二叉查找树) + * [3.2.1 左旋转](#321-左旋转) + * [3.2.2 右旋转](#322-右旋转) + * [3.2.3 颜色转换](#323-颜色转换) + * [3.2.4 插入](#324-插入) + * [3.2.5 删除最小键](#325-删除最小键) + * [3.2.6 分析](#326-分析) + * [4. 散列表](#4-散列表) + * [4.1 散列函数](#41-散列函数) + * [4.2 基于拉链法的散列表](#42-基于拉链法的散列表) + * [4.3 基于线性探测法的散列表](#43-基于线性探测法的散列表) + * [4.3.1 查找](#431-查找) + * [4.3.2 插入](#432-插入) + * [4.3.3 删除](#433-删除) + * [4.3.4 调整数组大小](#434-调整数组大小) + * [5. 应用](#5-应用) + * [5.1 各种符号表实现的比较](#51-各种符号表实现的比较) + * [5.2 Java 的符号表实现](#52-java-的符号表实现) + * [5.3 集合类型](#53-集合类型) + * [5.4 稀疏向量乘法](#54-稀疏向量乘法) -# 第一章 基础 +# 算法分析 -## 栈 - -**数组实现** - -```java -public class ResizeArrayStack implements Iterable { - private Item[] a = (Item[]) new Object[1]; - private int N = 0; - - public void push(Item item) { - if (N >= a.length) { - resize(2 * a.length); - } - a[N++] = item; - } - - public Item pop() { - Item item = a[--N]; - if (N <= a.length / 4) { - resize(a.length / 2); - } - return item; - } - - // 调整数组大小,使得栈具有伸缩性 - private void resize(int size) { - Item[] tmp = (Item[]) new Object[size]; - for (int i = 0; i < N; i++) { - tmp[i] = a[i]; - } - a = tmp; - } - - public boolean isEmpty() { - return N == 0; - } - - public int size() { - return N; - } - - @Override - public Iterator iterator() { - // 需要返回逆序遍历的迭代器 - return new ReverseArrayIterator(); - } - - private class ReverseArrayIterator implements Iterator { - private int i = N; - - @Override - public boolean hasNext() { - return i > 0; - } - - @Override - public Item next() { - return a[--i]; - } - } -} -``` - -上面实现使用了泛型,Java 不能直接创建泛型数组,只能使用转型来创建。 - -```java -Item[] arr = (Item[]) new Object[N]; -``` - -**链表实现** - -需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以让前一个压入栈的元素称为栈顶元素。 - -```java -public class Stack { - - private Node top = null; - private int N = 0; - - private class Node { - Item item; - Node next; - } - - public boolean isEmpty() { - return N == 0; - } - - public int size() { - return N; - } - - public void push(Item item) { - Node newTop = new Node(); - newTop.item = item; - newTop.next = top; - top = newTop; - N++; - } - - public Item pop() { - Item item = top.item; - top = top.next; - N--; - return item; - } -} -``` - -## 队列 - -下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 - -这里需要考虑让哪个指针指针链表头部节点,哪个指针指向链表尾部节点。因为出队列操作需要让队首元素的下一个元素成为队首,就需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此让队首指针 first 指针链表的开头。 - -```java -public class Queue { - private Node first; - private Node last; - int N = 0; - private class Node{ - Item item; - Node next; - } - - public boolean isEmpty(){ - return N == 0; - } - - public int size(){ - return N; - } - - // 入队列 - public void enqueue(Item item){ - Node newNode = new Node(); - newNode.item = item; - newNode.next = null; - if(isEmpty()){ - last = newNode; - first = newNode; - } else{ - last.next = newNode; - last = newNode; - } - N++; - } - - // 出队列 - public Item dequeue(){ - Node node = first; - first = first.next; - N--; - return node.item; - } -} -``` - -## 算法分析 - -### 1. 函数转换 +## 1. 函数转换 指数函数可以转换为线性函数,从而在函数图像上显示的更直观。 @@ -257,7 +88,7 @@ T(N)=aN3 转换为 lg(T(N))=3lgN+lga ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5510045a-8f32-487f-a756-463e51a6dab0.png) -### 2. 数学模型 +## 2. 数学模型 **近似** @@ -279,7 +110,7 @@ T(N)=aN3 转换为 lg(T(N))=3lgN+lga 使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 -### 3. ThreeSum +## 3. ThreeSum ThreeSum 程序用于统计一个数组中三元组的和为 0 的数量。 @@ -331,7 +162,7 @@ public class ThreeSumFast { } ``` -### 4. 倍率实验 +## 4. 倍率实验 如果 T(N) \~ aNblgN,那么 T(2N)/T(N) \~ 2b,例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N3/6,对它进行倍率实验得到如下结果: @@ -339,7 +170,7 @@ public class ThreeSumFast { 可见 T(2N)/T(N)\~23,也就是 b 为 3。 -### 5. 注意事项 +## 5. 注意事项 **大常数** @@ -361,143 +192,12 @@ public class ThreeSumFast { 将所有操作的总成本所以操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 -## union-find -**概览** +# 排序 -用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连接。 +## 1. 初级排序算法 -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5d387d02-6f96-44d6-b5d0-4538349f868e.png) - -**API** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a9b91b7d-65d7-4aa3-8ef6-21876b05ad16.png) - -**基本数据结构** - -```java -public class UF { - // 使用 id 数组来保存点的连通信息 - private int[] id; - - public UF(int N) { - id = new int[N]; - for (int i = 0; i < N; i++) { - id[i] = i; - } - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } -} -``` - -### 1. quick-find 算法 - -保证在同一连通分量的所有触点的 id 值相等。 - -这种方法可以快速取得一个触点的 id 值,并且判断两个触点是否连通,但是 union 的操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 - -```java - public int find(int p) { - return id[p]; - } - public void union(int p, int q) { - int pID = find(p); - int qID = find(q); - - if (pID == qID) return; - for (int i = 0; i < id.length; i++) { - if (id[i] == pID) id[i] = qID; - } - } -``` - -### 2. quick-union 算法 - -在 union 时只将触点的 id 值指向另一个触点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,根节点需要指向自己。在进行查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id值。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9192dc0a-a7cd-4030-8df6-e388600644cf.jpg) - -```java - public int find(int p) { - while (p != id[p]) p = id[p]; - return p; - } - - public void union(int p, int q) { - int pRoot = find(p); - int qRoot = find(q); - if (pRoot == qRoot) return; - id[pRoot] = qRoot; - } -``` - -这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d206d090-d911-4263-a1fe-d6f63f5d1776.png) - -### 3. 加权 quick-union 算法 - -为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 - -理论研究证明,加权 quick-union 算法构造的树深度最多不超过 lgN。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8d6af5ac-74eb-4e07-99aa-654b9f21f1d3.jpg) - -```java -public class WeightedQuickUnionUF { - private int[] id; - // 保存节点的数量信息 - private int[] sz; - - public WeightedQuickUnionUF(int N) { - id = new int[N]; - sz = new int[N]; - for (int i = 0; i < N; i++) { - id[i] = i; - sz[i] = 1; - } - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - public int find(int p) { - while (p != id[p]) p = id[p]; - return p; - } - - public void union(int p, int q) { - int i = find(p); - int j = find(q); - if (i == j) return; - if (sz[i] < sz[j]) { - id[i] = j; - sz[j] += sz[i]; - } else { - id[j] = i; - sz[i] += sz[j]; - } - } -} -``` - -### 4. 路径压缩的加权 quick-union 算法 - -在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 - -### 5. 各种 union-find 算法的比较 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e5baeb38-0ec9-4ad7-8374-1cdb0dba74a6.jpg) - -# 第二章 排序 - -## 初级排序算法 - -### 1. 约定 +### 1.1 约定 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法。 @@ -517,7 +217,7 @@ private void exch(Comparable[] a, int i, int j){ } ``` -### 2. 选择排序 +### 1.2 选择排序 找到数组中的最小元素,然后将它与数组的第一个元素交换位置。然后再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 @@ -540,7 +240,7 @@ public class Selection { 选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 -### 3. 插入排序 +### 1.3 插入排序 将一个元素插入到已排序的数组中,使得插入之后的数组也是有序的。插入排序从左到右插入每个元素,每次插入之后左部的子数组是有序的。 @@ -563,11 +263,11 @@ public class Insertion { 插入排序对于部分有序数组和小规模数组特别高效。 -### 4. 选择排序和插入排序的比较 +### 1.4 选择排序和插入排序的比较 对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 -### 5. 希尔排序 +### 1.5 希尔排序 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 @@ -599,13 +299,13 @@ public class Shell { 希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 -## 归并排序 +## 2 归并排序 归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dcf265ad-fe35-424d-b4b7-d149cdf239f4.png) -### 1. 归并方法 +### 2.1 归并方法 ```java public class MergeSort { @@ -628,7 +328,7 @@ public class MergeSort { } ``` -### 2. 自顶向下归并排序 +### 2.2 自顶向下归并排序 ```java public static void sort(Comparable[] a) { @@ -653,7 +353,7 @@ private static void sort(Comparable[] a, int lo, int hi) { 因为小数组的递归操作会过于频繁,因此使用插入排序来处理小数组将会获得更高的性能。 -### 3. 自底向上归并排序 +### 2.3 自底向上归并排序 先归并那些微型数组,然后成对归并得到的子数组。 @@ -671,9 +371,9 @@ public static void busort(Comparable[] a) { } ``` -## 快速排序 +## 3.快速排序 -### 1. 基本算法 +### 3.1 基本算法 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 @@ -695,7 +395,7 @@ public class QuickSort { } ``` -### 2. 切分 +### 3.2 切分 取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断继续这个过程,就可以保证左指针的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 @@ -716,7 +416,7 @@ private static int partition(Comparable[] a, int lo, int hi) { } ``` -### 3. 性能分析 +### 3.3 性能分析 快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 @@ -724,17 +424,17 @@ private static int partition(Comparable[] a, int lo, int hi) { 最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 -### 4. 算法改进 +### 3.4 算法改进 -#### 4.1 切换到插入排序 +#### 3.4.1 切换到插入排序 因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -#### 4.2 三取样 +#### 3.4.2 三取样 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 -#### 4.3 三向切分 +#### 3.4.3 三向切分 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 @@ -760,11 +460,11 @@ public class Quick3Way { } ``` -## 优先队列 +## 4. 优先队列 优先队列主要用于处理最大元素。 -### 1. 堆 +### 4.1 堆 定义:一颗二叉树的每个节点都大于等于它的两个子节点。 @@ -801,7 +501,7 @@ public class MaxPQ { } ``` -### 2. 上浮和下沉 +### 4.2 上浮和下沉 在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 @@ -828,7 +528,7 @@ private void sink(int k) { } ``` -### 3. 插入元素 +### 4.3 插入元素 将新元素放到数组末尾,然后上浮到合适的位置。 @@ -839,7 +539,7 @@ public void insert(Key v) { } ``` -### 4. 删除最大元素 +### 4.4 删除最大元素 从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 @@ -853,7 +553,7 @@ public Key delMax() { } ``` -### 5. 堆排序 +### 4.5 堆排序 由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 @@ -876,7 +576,7 @@ public static void sort(Comparable[] a){ } ``` -### 6. 分析 +### 4.6 分析 一个堆的高度为 lgN,因此在堆中插入元素和删除最大元素的复杂度都为 lgN。 @@ -886,19 +586,19 @@ public static void sort(Comparable[] a){ 现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 -## 应用 +## 5. 应用 -### 1. 排序算法的比较 +### 5.1 排序算法的比较 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//be53c00b-2534-4dc6-ad03-c55995c47db9.jpg) 快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlgN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 -### 2. Java 的排序算法实现 +### 5.2 Java 的排序算法实现 Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 -### 3. 基于切分的快速选择算法 +### 5.3 基于切分的快速选择算法 快速排序的 partition() 方法,会将数组的 a[lo] 至 a[hi] 重新排序并返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j]。那么如果 j=k,a[j] 就是第 k 个数。 @@ -917,17 +617,17 @@ public static Comparable select(Comparable[] a, int k) { } ``` -# 第三章 查找 +# 查找 本章使用三种经典的数据类型来实现高效的符号表:二叉查找树、红黑树和散列表。 -## 符号表 +## 1. 符号表 -### 1. 无序符号表 +### 1.1 无序符号表 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b69d7184-ab62-4957-ba29-fb4fa25f9b65.jpg) -### 2. 有序符号表 +### 1.2 有序符号表 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ba6ae411-82da-4d86-a434-6776d1731e8e.jpg) @@ -935,7 +635,7 @@ public static Comparable select(Comparable[] a, int k) { 查找的成本模型:键的比较次数,在不进行比较时使用数组的访问次数。 -### 3. 二分查找实现有序符号表 +### 1.3 二分查找实现有序符号表 使用一对平行数组,一个存储键一个存储值。 @@ -1002,7 +702,7 @@ public class BinarySearchST, Value> { } ``` -## 二叉查找树 +## 2. 二叉查找树 **二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 @@ -1041,7 +741,7 @@ public class BST, Value> { } ``` -### 1. get() +### 2.1 get() 如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中,否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 @@ -1058,7 +758,7 @@ private Value get(Node x, Key key) { } ``` -### 2. put() +### 2.2 put() 当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。 @@ -1077,7 +777,7 @@ private Node put(Node x, Key key, Value val) { } ``` -### 3. 分析 +### 2.3 分析 二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 lgN。在最坏的情况下,树的高度为 N。 @@ -1085,7 +785,7 @@ private Node put(Node x, Key key, Value val) { 复杂度:查找和插入操作都为对数级别。 -### 4. floor() +### 2.4 floor() 如果 key 小于根节点的 key,那么小于等于 key 的最大键节点一定在左子树中;如果 key 大于根节点的 key,只有当根节点右子树中存在小于等于 key 的节点,小于等于 key 的最大键节点才在右子树中,否则根节点就是小于等于 key 的最大键节点。 @@ -1109,7 +809,7 @@ private Node floor(Node x, Key key) { } ``` -### 5. rank() +### 2.5 rank() ```java public int rank(Key key) { @@ -1124,7 +824,7 @@ private int rank(Key key, Node x) { } ``` -### 6. min() +### 2.6 min() ```java private Node min(Node x) { @@ -1133,7 +833,7 @@ private Node min(Node x) { } ``` -### 7. deleteMin() +### 2.7 deleteMin() 令指向最小节点的链接指向最小节点的右子树。 @@ -1151,7 +851,7 @@ public Node deleteMin(Node x) { } ``` -### 8. delete() +### 2.8 delete() 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。 @@ -1179,7 +879,7 @@ private Node delete(Node x, Key key) { } ``` -### 9. keys() +### 2.9 keys() 利用二叉查找树中序遍历的结果为有序序列的特点。 @@ -1199,31 +899,31 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { } ``` -### 10. 性能分析 +### 2.10 性能分析 复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 -## 平衡查找树 +## 3. 平衡查找树 -### 2-3 查找树 +### 3.1 2-3 查找树 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2548f2ec-7b00-4ec7-b286-20fc3022e084.jpg) 一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 -#### 1. 插入操作 +#### 3.1.1 插入操作 当插入之后产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//912174d8-0786-4222-b7ef-a611d36e5db9.jpg) -#### 2. 性质 +#### 3.1.2 性质 2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 2-3 查找树的查找和插入操作复杂度和插入顺序 **无关**,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 -### 红黑二叉查找树 +### 3.2 红黑二叉查找树 2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 @@ -1266,7 +966,7 @@ public class RedBlackBST, Value> { } ``` -#### 1. 左旋转 +#### 3.2.1 左旋转 因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 @@ -1287,7 +987,7 @@ public Node rotateLeft(Node h) { } ``` -#### 2. 右旋转 +#### 3.2.2 右旋转 进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 @@ -1307,7 +1007,7 @@ public Node rotateRight(Node h) { } ``` -#### 3. 颜色转换 +#### 3.2.3 颜色转换 一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 @@ -1323,7 +1023,7 @@ void flipColors(Node h){ } ``` -#### 4. 插入 +#### 3.2.4 插入 先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: @@ -1359,7 +1059,7 @@ private Node put(Node x, Key key, Value val) { 根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. -#### 5. 删除最小键 +#### 3.2.5 删除最小键 如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: @@ -1373,19 +1073,19 @@ private Node put(Node x, Key key, Value val) { ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//70b66757-755c-4e17-a7b7-5ce808023643.png) -#### 6. 分析 +#### 3.2.6 分析 一颗大小为 N 的红黑树的高度不会超过 2lgN。最坏的情况下是它所对应的 2-3 树中构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 红黑树大多数的操作所需要的时间都是对数级别的。 -## 散列表 +## 4. 散列表 散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入的符号表。 由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 -### 1. 散列函数 +### 4.1 散列函数 对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 @@ -1443,7 +1143,7 @@ public class Transaction{ } ``` -### 2. 基于拉链法的散列表 +### 4.2 基于拉链法的散列表 拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 @@ -1451,7 +1151,7 @@ public class Transaction{ 对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 -### 3. 基于线性探测法的散列表 +### 4.3 基于线性探测法的散列表 线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 @@ -1484,7 +1184,7 @@ public class LinearProbingHashST { } ``` -#### 3.1 查找 +#### 4.3.1 查找 ```java public Value get(Key key) { @@ -1497,7 +1197,7 @@ public Value get(Key key) { } ``` -#### 3.2 插入 +#### 4.3.2 插入 ```java public void put(Key key, Value val) { @@ -1515,7 +1215,7 @@ public void put(Key key, Value val) { } ``` -#### 3.3 删除 +#### 4.3.3 删除 删除操作应当将右侧所有相邻的键值重新插入散列表中。 @@ -1543,7 +1243,7 @@ public void delete(Key key) { } ``` -#### 3.4 调整数组大小 +#### 4.3.4 调整数组大小 线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 @@ -1576,23 +1276,23 @@ private void resize(int cap) { ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//01658047-0d86-4a7a-a8ca-7ea20fa1fdde.png) -## 应用 +## 5. 应用 -### 1. 各种符号表实现的比较 +### 5.1 各种符号表实现的比较 ![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9ee83c8c-1165-476c-85a6-e6e434e5307a.jpg) 应当优先考虑散列表,当需要有序性操作时使用红黑树。 -### 2. Java 的符号表实现 +### 5.2 Java 的符号表实现 Java 的 java.util.TreeMap 和 java.util.HashMap 分别是基于红黑树和拉链法的散列表的符号表实现。 -### 3. 集合类型 +### 5.3 集合类型 除了符号表,集合类型也经常使用,它只有键没有值,可以用集合类型来存储一系列的键然后判断一个键是否在集合中。 -### 4. 稀疏向量乘法 +### 5.4 稀疏向量乘法 当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。