From 1229e8a008871ab30e8e4a43ad9c6dee6072908d Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Sat, 17 Mar 2018 22:37:36 +0800 Subject: [PATCH] auto commit --- notes/算法.md | 324 ++++++++++++++++++++++++-------------------------- 1 file changed, 155 insertions(+), 169 deletions(-) diff --git a/notes/算法.md b/notes/算法.md index 5bbffb5c..bed2c994 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -1,80 +1,67 @@ -* [算法分析](#算法分析) - * [1. 函数转换](#1-函数转换) - * [2. 数学模型](#2-数学模型) - * [3. ThreeSum](#3-threesum) - * [4. 倍率实验](#4-倍率实验) - * [5. 注意事项](#5-注意事项) -* [栈和队列](#栈和队列) - * [1. 栈](#1-栈) +* [一、算法分析](#一算法分析) + * [函数转换](#函数转换) + * [数学模型](#数学模型) + * [ThreeSum](#threesum) + * [倍率实验](#倍率实验) + * [注意事项](#注意事项) +* [二、栈和队列](#二栈和队列) + * [栈](#栈) + * [1. 数组实现](#1-数组实现) + * [2. 链表实现](#2-链表实现) * [2. 队列](#2-队列) -* [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-初级排序算法) - * [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-算法改进) - * [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.2 红黑二叉查找树](#32-红黑二叉查找树) - * [4. 散列表](#4-散列表) - * [4.1 散列函数](#41-散列函数) - * [4.2 基于拉链法的散列表](#42-基于拉链法的散列表) - * [4.3 基于线性探测法的散列表](#43-基于线性探测法的散列表) - * [5. 应用](#5-应用) - * [5.1 各种符号表实现的比较](#51-各种符号表实现的比较) - * [5.2 Java 的符号表实现](#52-java-的符号表实现) - * [5.3 集合类型](#53-集合类型) - * [5.4 稀疏向量乘法](#54-稀疏向量乘法) +* [三、union-find](#三union-find) + * [quick-find](#quick-find) + * [quick-union](#quick-union) + * [加权 quick-union](#加权-quick-union) + * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) + * [各种 union-find 算法的比较](#各种-union-find-算法的比较) +* [三、排序](#三排序) + * [初级排序算法](#初级排序算法) + * [1. 选择排序](#1-选择排序) + * [2. 插入排序](#2-插入排序) + * [3. 希尔排序](#3-希尔排序) + * [归并排序](#归并排序) + * [1. 归并方法](#1-归并方法) + * [2. 自顶向下归并排序](#2-自顶向下归并排序) + * [3. 自底向上归并排序](#3-自底向上归并排序) + * [快速排序](#快速排序) + * [1. 基本算法](#1-基本算法) + * [2. 切分](#2-切分) + * [3. 性能分析](#3-性能分析) + * [4. 算法改进](#4-算法改进) + * [优先队列](#优先队列) + * [1. 堆](#1-堆) + * [2. 上浮和下沉](#2-上浮和下沉) + * [3. 插入元素](#3-插入元素) + * [4. 删除最大元素](#4-删除最大元素) + * [5. 堆排序](#5-堆排序) + * [6. 分析](#6-分析) + * [应用](#应用) + * [1. 排序算法的比较](#1-排序算法的比较) + * [2. Java 的排序算法实现](#2-java-的排序算法实现) + * [3. 基于切分的快速选择算法](#3-基于切分的快速选择算法) +* [四、查找](#四查找) + * [符号表](#符号表) + * [二叉查找树](#二叉查找树) + * [平衡查找树](#平衡查找树) + * [1. 2-3 查找树](#1-2-3-查找树) + * [2. 红黑二叉查找树](#2-红黑二叉查找树) + * [散列表](#散列表) + * [1. 散列函数](#1-散列函数) + * [2. 基于拉链法的散列表](#2-基于拉链法的散列表) + * [3. 基于线性探测法的散列表](#3-基于线性探测法的散列表) + * [应用](#应用) + * [1. 各种符号表实现的比较](#1-各种符号表实现的比较) + * [2. Java 的符号表实现](#2-java-的符号表实现) + * [3. 集合类型](#3-集合类型) + * [4. 稀疏向量乘法](#4-稀疏向量乘法) -# 算法分析 +# 一、算法分析 -## 1. 函数转换 +## 函数转换 指数函数可以转换为线性函数,从而在函数图像上显示的更直观。例如 @@ -86,33 +73,32 @@

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

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

-**内循环** +#### 3. 内循环 执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 -**成本模型** +#### 4. 成本模型 使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 -## 3. ThreeSum +## ThreeSum ThreeSum 用于统计一个数组中三元组的和为 0 的数量。 - ```java public class ThreeSum { public static int count(int[] a) { @@ -160,7 +146,7 @@ public class ThreeSumFast { } ``` -## 4. 倍率实验 +## 倍率实验 如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 @@ -170,37 +156,37 @@ public class ThreeSumFast { 可以看到,T(2N)/T(N)\~23,因此可以确定 T(N) \~ aN2logN。 -## 5. 注意事项 +## 注意事项 -**大常数** +#### 1. 大常数 在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 -**缓存** +#### 2. 缓存 计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 -**对最坏情况下的性能的保证** +#### 3. 对最坏情况下的性能的保证 在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 -**随机化算法** +#### 4. 随机化算法 通过打乱输入,去除算法对输入的依赖。 -**均摊分析** +#### 5. 均摊分析 将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 -# 栈和队列 +# 二、栈和队列 -## 1. 栈 +## 栈 first-in-last-out(FILO)

-**数组实现** +### 1. 数组实现 ```java public class ResizeArrayStack implements Iterable { @@ -267,7 +253,7 @@ public class ResizeArrayStack implements Iterable { Item[] arr = (Item[]) new Object[N]; ``` -**链表实现** +### 2. 链表实现 需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以让前一个压入栈的元素称为栈顶元素。 @@ -360,19 +346,19 @@ public class Queue { } ``` -# union-find +# 三、union-find -**概览** + **概览**
用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连接。

-**API** + **API**

-**基本数据结构** + **基本数据结构**
```java public class UF { @@ -392,7 +378,7 @@ public class UF { } ``` -## 1. quick-find 算法 +## quick-find 保证在同一连通分量的所有触点的 id 值相等。 @@ -413,7 +399,7 @@ public class UF { } ``` -## 2. quick-union 算法 +## quick-union 在 union 时只将触点的 id 值指向另一个触点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,根节点需要指向自己。在进行查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id 值。 @@ -437,7 +423,7 @@ public class UF {

-## 3. 加权 quick-union 算法 +## 加权 quick-union 为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 @@ -484,19 +470,17 @@ public class WeightedQuickUnionUF { } ``` -## 4. 路径压缩的加权 quick-union 算法 +## 路径压缩的加权 quick-union 在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 -## 5. 各种 union-find 算法的比较 +## 各种 union-find 算法的比较

-# 排序 +# 三、排序 -## 1. 初级排序算法 - -### 1.1 约定 + **约定**
待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法。 @@ -516,7 +500,9 @@ private void exch(Comparable[] a, int i, int j){ } ``` -### 1.2 选择排序 +## 初级排序算法 + +### 1. 选择排序 找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 @@ -539,7 +525,7 @@ public class Selection { 选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 -### 1.3 插入排序 +### 2. 插入排序 入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。 @@ -562,11 +548,11 @@ public class Insertion { 插入排序对于部分有序数组和小规模数组特别高效。 -### 1.4 选择排序和插入排序的比较 +**选择排序和插入排序的比较** 对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 -### 1.5 希尔排序 +### 3. 希尔排序 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 @@ -598,13 +584,13 @@ public class Shell { 希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 -## 2 归并排序 +## 归并排序 归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。

-### 2.1 归并方法 +### 1. 归并方法 归并方法将数组中两个已经排序的部分归并成一个。 @@ -629,7 +615,7 @@ public class MergeSort { } ``` -### 2.2 自顶向下归并排序 +### 2. 自顶向下归并排序 ```java public static void sort(Comparable[] a) { @@ -654,7 +640,7 @@ private static void sort(Comparable[] a, int lo, int hi) { 因为小数组的递归操作会过于频繁,因此使用插入排序来处理小数组将会获得更高的性能。 -### 2.3 自底向上归并排序 +### 3. 自底向上归并排序 先归并那些微型数组,然后成对归并得到的子数组。 @@ -672,9 +658,9 @@ public static void busort(Comparable[] a) { } ``` -## 3. 快速排序 +## 快速排序 -### 3.1 基本算法 +### 1. 基本算法 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 @@ -696,7 +682,7 @@ public class QuickSort { } ``` -### 3.2 切分 +### 2. 切分 取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断继续这个过程,就可以保证左指针的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 @@ -717,7 +703,7 @@ private static int partition(Comparable[] a, int lo, int hi) { } ``` -### 3.3 性能分析 +### 3. 性能分析 快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 @@ -725,17 +711,17 @@ private static int partition(Comparable[] a, int lo, int hi) { 最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 -### 3.4 算法改进 +### 4. 算法改进 -#### 3.4.1 切换到插入排序 +#### 4.1 切换到插入排序 因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -#### 3.4.2 三取样 +#### 4.2 三取样 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 -#### 3.4.3 三向切分 +#### 4.3 三向切分 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 @@ -761,11 +747,11 @@ public class Quick3Way { } ``` -## 4. 优先队列 +## 优先队列 优先队列主要用于处理最大元素。 -### 4.1 堆 +### 1. 堆 定义:一颗二叉树的每个节点都大于等于它的两个子节点。 @@ -802,7 +788,7 @@ public class MaxPQ { } ``` -### 4.2 上浮和下沉 +### 2. 上浮和下沉 在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 @@ -829,7 +815,7 @@ private void sink(int k) { } ``` -### 4.3 插入元素 +### 3. 插入元素 将新元素放到数组末尾,然后上浮到合适的位置。 @@ -840,7 +826,7 @@ public void insert(Key v) { } ``` -### 4.4 删除最大元素 +### 4. 删除最大元素 从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 @@ -854,7 +840,7 @@ public Key delMax() { } ``` -### 4.5 堆排序 +### 5. 堆排序 由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 @@ -877,7 +863,7 @@ public static void sort(Comparable[] a){ } ``` -### 4.6 分析 +### 6. 分析 一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。 @@ -887,19 +873,19 @@ public static void sort(Comparable[] a){ 现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 -## 5. 应用 +## 应用 -### 5.1 排序算法的比较 +### 1. 排序算法的比较

快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 -### 5.2 Java 的排序算法实现 +### 2. Java 的排序算法实现 Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 -### 5.3 基于切分的快速选择算法 +### 3. 基于切分的快速选择算法 快速排序的 partition() 方法,会返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 @@ -920,17 +906,17 @@ public static Comparable select(Comparable[] a, int k) { 该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 -# 查找 +# 四、查找 本章使用三种经典的数据结构来实现高效的符号表:二叉查找树、红黑树和散列表。 -## 1. 符号表 +## 符号表 -### 1.1 无序符号表 +#### 1. 无序符号表

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

@@ -938,7 +924,7 @@ public static Comparable select(Comparable[] a, int k) { 查找的成本模型:键的比较次数,在不进行比较时使用数组的访问次数。 -### 1.3 二分查找实现有序符号表 +#### 3. 二分查找实现有序符号表 使用一对平行数组,一个存储键一个存储值。 @@ -1005,7 +991,7 @@ public class BinarySearchST, Value> { } ``` -## 2. 二叉查找树 +## 二叉查找树 **二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 @@ -1044,7 +1030,7 @@ public class BST, Value> { } ``` -### 2.1 get() +#### 1. get() 如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中,否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 @@ -1061,7 +1047,7 @@ private Value get(Node x, Key key) { } ``` -### 2.2 put() +#### 2. put() 当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。 @@ -1080,7 +1066,7 @@ private Node put(Node x, Key key, Value val) { } ``` -### 2.3 分析 +#### 3. 分析 二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。在最坏的情况下,树的高度为 N。 @@ -1088,7 +1074,7 @@ private Node put(Node x, Key key, Value val) { 复杂度:查找和插入操作都为对数级别。 -### 2.4 floor() +#### 4. floor() 如果 key 小于根节点的 key,那么小于等于 key 的最大键节点一定在左子树中;如果 key 大于根节点的 key,只有当根节点右子树中存在小于等于 key 的节点,小于等于 key 的最大键节点才在右子树中,否则根节点就是小于等于 key 的最大键节点。 @@ -1112,7 +1098,7 @@ private Node floor(Node x, Key key) { } ``` -### 2.5 rank() +#### 5. rank() ```java public int rank(Key key) { @@ -1127,7 +1113,7 @@ private int rank(Key key, Node x) { } ``` -### 2.6 min() +#### 6. min() ```java private Node min(Node x) { @@ -1136,7 +1122,7 @@ private Node min(Node x) { } ``` -### 2.7 deleteMin() +#### 7. deleteMin() 令指向最小节点的链接指向最小节点的右子树。 @@ -1154,7 +1140,7 @@ public Node deleteMin(Node x) { } ``` -### 2.8 delete() +#### 8. delete() 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。 @@ -1182,7 +1168,7 @@ private Node delete(Node x, Key key) { } ``` -### 2.9 keys() +#### 9. keys() 利用二叉查找树中序遍历的结果为有序序列的特点。 @@ -1202,31 +1188,31 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { } ``` -### 2.10 性能分析 +#### 10. 性能分析 复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 -## 3. 平衡查找树 +## 平衡查找树 -### 3.1 2-3 查找树 +### 1. 2-3 查找树

一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 -#### 3.1.1 插入操作 +#### 1.1 插入操作 当插入之后产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。

-#### 3.1.2 性质 +#### 1.2 性质 2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 2-3 查找树的查找和插入操作复杂度和插入顺序 **无关** ,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 -### 3.2 红黑二叉查找树 +### 2. 红黑二叉查找树 2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 @@ -1269,7 +1255,7 @@ public class RedBlackBST, Value> { } ``` -#### 3.2.1 左旋转 +#### 2.1 左旋转 因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 @@ -1290,7 +1276,7 @@ public Node rotateLeft(Node h) { } ``` -#### 3.2.2 右旋转 +#### 2.2 右旋转 进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 @@ -1310,7 +1296,7 @@ public Node rotateRight(Node h) { } ``` -#### 3.2.3 颜色转换 +#### 2.3 颜色转换 一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 @@ -1326,7 +1312,7 @@ void flipColors(Node h){ } ``` -#### 3.2.4 插入 +#### 2.4 插入 先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: @@ -1362,7 +1348,7 @@ private Node put(Node x, Key key, Value val) { 根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. -#### 3.2.5 删除最小键 +#### 2.5 删除最小键 如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: @@ -1376,19 +1362,19 @@ private Node put(Node x, Key key, Value val) {

-#### 3.2.6 分析 +#### 2.6 分析 一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树中构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 红黑树大多数的操作所需要的时间都是对数级别的。 -## 4. 散列表 +## 散列表 散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入的符号表。 由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 -### 4.1 散列函数 +### 1. 散列函数 对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 @@ -1446,7 +1432,7 @@ public class Transaction{ } ``` -### 4.2 基于拉链法的散列表 +### 2. 基于拉链法的散列表 拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 @@ -1454,7 +1440,7 @@ public class Transaction{ 对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 -### 4.3 基于线性探测法的散列表 +### 3. 基于线性探测法的散列表 线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 @@ -1487,7 +1473,7 @@ public class LinearProbingHashST { } ``` -#### 4.3.1 查找 +#### 3.1 查找 ```java public Value get(Key key) { @@ -1500,7 +1486,7 @@ public Value get(Key key) { } ``` -#### 4.3.2 插入 +#### 3.2 插入 ```java public void put(Key key, Value val) { @@ -1518,7 +1504,7 @@ public void put(Key key, Value val) { } ``` -#### 4.3.3 删除 +#### 3.3 删除 删除操作应当将右侧所有相邻的键值重新插入散列表中。 @@ -1546,7 +1532,7 @@ public void delete(Key key) { } ``` -#### 4.3.4 调整数组大小 +#### 3.4 调整数组大小 线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 @@ -1579,23 +1565,23 @@ private void resize(int cap) {

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

应当优先考虑散列表,当需要有序性操作时使用红黑树。 -### 5.2 Java 的符号表实现 +### 2. Java 的符号表实现 Java 的 java.util.TreeMap 和 java.util.HashMap 分别是基于红黑树和拉链法的散列表的符号表实现。 -### 5.3 集合类型 +### 3. 集合类型 除了符号表,集合类型也经常使用,它只有键没有值,可以用集合类型来存储一系列的键然后判断一个键是否在集合中。 -### 5.4 稀疏向量乘法 +### 4. 稀疏向量乘法 当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。