├── 175-BFPRT算法(TOP-K问题).md ├── 176-算法复杂度分析概要.md ├── 179-单链表的各种操作(面试必备).md ├── 180-表达式求值.md ├── 181-Manacher算法.md ├── 183-KMP算法(1):如何理解KMP.md ├── 184-size_t,微尘下的 bug.md ├── 186-扩展KMP算法.md ├── 187-二分查找(面试必备).md ├── 188-01背包问题.md ├── 190-线索二叉树.md ├── 191-二叉树的各种操作(面试必备).md ├── 192-KMP算法(2):其细微之处.md ├── 193-排序(1):直接插入排序,二分查找插入排序,希尔排序.md ├── 194-单源最短路径(1):Dijkstra算法.md ├── 195-单源最短路径(2):Bellman_Ford算法.md ├── 196-单源最短路径(3):SPFA算法.md ├── 197-全排列问题(1).md ├── 198-全排列问题(2).md ├── 199-Prim算法(1).md ├── 200-单源最短路径(4):总结.md ├── 201~300 ├── 201-排序(2):快速排序.md ├── 202-排序(3):堆排序.md ├── 203-排序(4):归并排序.md ├── 204-排序(5):基数排序.md ├── 205-排序(6):总结.md ├── 206-Dijkstra算法与Prim算法的区别.md ├── 208-递归(1):基础.md ├── 209-你被欺骗了很久:前缀和真前缀.md ├── 210-Boyer-Moore算法.md ├── 211-递归(2):高级.md ├── 212-BFS和DFS.md ├── 213-Sunday算法.md ├── 214-Sparse Table算法.md ├── 215-字典树(1):字典树入门.md ├── 216-Aho-Corasick算法.md ├── 217-二叉查找树.md ├── 218-AVL树.md ├── 219-红黑树.md ├── 221-AVL树与红黑树的对比.md ├── 222-跳跃表.md ├── 224-B-树(1):定义及其代码实现.md ├── 225-B-树(2):应用及其拓展.md ├── 226-位运算总结.md ├── 227-向量积与线段相交问题.md └── 228-凸包问题.md └── README.md /175-BFPRT算法(TOP-K问题).md: -------------------------------------------------------------------------------- 1 | ## 一:背景介绍 2 | 在一堆数中求其前k大或前k小的问题,简称TOP-K问题。而目前解决TOP-K问题最有效的算法即是"BFPRT算法",又称为"中位数的中位数算法",该算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最坏时间复杂度为$O(n)$。 3 | 4 | 在首次接触TOP-K问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前k即可,但是这么做有两个问题: 5 | 6 | * 快速排序的平均复杂度为$O(nlogn)$,但最坏时间复杂度为$O(n^2)$,不能始终保证较好的复杂度。 7 | * 我们只需要前k大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。 8 | 9 | 除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为k的堆,时间复杂度为$O(nlogk)$。 10 | 11 | 那是否还存在更有效的方法呢?BFPRT算法的做法就是**在快速排序的基础上**,通过判断主元位置与k的大小使递归的规模变小,其次通过修改快速排序中**主元的选取方法**来降低快速排序在**最坏情况下的时间复杂度**。 12 | 13 | 下面先来简单回顾下快速排序的过程,以升序为例: 14 | 15 | (1):选取主元(数组中随机一个元素); 16 | (2):以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边; 17 | (3):分别对左边和右边进行递归,重复上述过程。 18 | 19 | 20 | 21 | 22 | 23 | ## 二:BFPRT算法过程及代码 24 | 25 | BFPRT算法步骤如下: 26 | 27 | (1):选取主元; 28 |   (1.1):将n个元素划分为$⌊\frac n5⌋$个组,每组5个元素,若有剩余,舍去; 29 |   (1.2):使用插入排序找到$⌊\frac n5⌋$个组中每一组的中位数; 30 |   (1.3):对于(1.2)中找到的所有中位数,调用BFPRT算法求出它们的中位数,作为主元; 31 | (2):以(1.3)选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边; 32 | (3):判断主元的位置与k的大小,有选择的对左边或右边递归。 33 | 34 | 上面的描述可能并不易理解,先看下面这幅图: 35 | 36 | ![](https://61mon.com/images/illustrations/BFPRT/1.png) 37 | 38 | BFPRT()调用GetPivotIndex()和Partition()来求解第k小,在这过程中,GetPivotIndex()也调用了BFPRT(),即GetPivotIndex)和BFPRT()为互递归的关系。 39 | 40 | 下面为代码实现,其所求为**前K小的数**: 41 | 42 | ```c++ 43 | /** 44 | * BFPRT算法(前K小数问题) 45 | * 46 | * author : 刘毅(Limer) 47 | * date : 2017-01-25 48 | * mode : C++ 49 | */ 50 | 51 | #include 52 | #include 53 | 54 | using namespace std; 55 | 56 | /* 插入排序,返回中位数下标 */ 57 | int InsertSort(int array[], int left, int right) 58 | { 59 | int temp; 60 | int j; 61 | 62 | for (int i = left + 1; i <= right; i++) 63 | { 64 | temp = array[i]; 65 | j = i - 1; 66 | while (j >= left && array[j] > temp) 67 | array[j + 1] = array[j--]; 68 | array[j + 1] = temp; 69 | } 70 | 71 | return ((right - left) >> 1) + left; 72 | } 73 | 74 | /* 返回中位数的中位数下标 */ 75 | 76 | int BFPRT(int array[], int left, int right, const int & k); 77 | 78 | int GetPivotIndex(int array[], int left, int right) 79 | { 80 | if (right - left < 5) 81 | return InsertSort(array, left, right); 82 | 83 | int sub_right = left - 1; 84 | 85 | for (int i = left; i + 4 <= right; i += 5) 86 | { 87 | int index = InsertSort(array, i, i + 4); // 找到五个元素的中位数的下标 88 | swap(array[++sub_right], array[index]); // 依次放在左侧 89 | } 90 | 91 | return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1); 92 | } 93 | 94 | /* 利用中位数的中位数的下标进行划分,返回分界线下标 */ 95 | int Partition(int array[], int left, int right, int pivot_index) 96 | { 97 | swap(array[pivot_index], array[right]); // 把主元放置于末尾 98 | int divide_index = left; // 跟踪划分的分界线 99 | 100 | for (int i = left; i < right; i++) 101 | { 102 | if (array[i] < array[right]) 103 | swap(array[divide_index++], array[i]); // 比主元小的都放在左侧 104 | } 105 | 106 | swap(array[divide_index], array[right]); // 最后把主元换回来 107 | 108 | return divide_index; 109 | } 110 | 111 | int BFPRT(int array[], int left, int right, const int & k) 112 | { 113 | int pivot_index = GetPivotIndex(array, left, right); // 得到中位数的中位数下标 114 | int divide_index = Partition(array, left, right, pivot_index); // 进行划分,返回划分边界 115 | int num = divide_index - left + 1; 116 | 117 | if (num == k) 118 | return divide_index; 119 | else if (num > k) 120 | return BFPRT(array, left, divide_index - 1, k); 121 | else 122 | return BFPRT(array, divide_index + 1, right, k - num); 123 | } 124 | 125 | int main() 126 | { 127 | int k = 5; 128 | int array[10] = { 1,1,2,3,1,5,-1,7,8,-10 }; 129 | 130 | cout << "原数组:"; 131 | for (int i = 0; i < 10; i++) 132 | cout << array[i] << " "; 133 | cout << endl; 134 | 135 | cout << "第" << k << "小值为:" << array[BFPRT(array, 0, 9, k)] << endl; 136 | 137 | cout << "变换后的数组:"; 138 | for (int i = 0; i < 10; i++) 139 | cout << array[i] << " "; 140 | cout << endl; 141 | 142 | return 0; 143 | } 144 | ``` 145 | 146 | 运行如下: 147 | 148 | ![](https://61mon.com/images/illustrations/BFPRT/2.PNG) 149 | 150 | ## 三:时间复杂度分析 151 | 152 | BFPRT算法在最坏情况下的时间复杂度是$O(n)$,下面予以证明。令$T(n)$为所求的时间复杂度,则有: 153 | 154 | $$ 155 | T(n)≤T(\frac n 5)+T(\frac {7n}{10})+c⋅n\tag{c为一个正常数} 156 | $$ 157 | 158 | 其中: 159 | 160 | - $T(\frac n 5)$来自GetPivotIndex(),n个元素,5个一组,共有$⌊\frac n5⌋$个中位数; 161 | - $T(\frac {7n}{10})$来自BFPRT(),在$⌊\frac n5⌋$个中位数中,主元x大于其中 $\frac 12⋅\frac n5=\frac n{10}$的中位数,而每个中位数在其本来的5个数的小组中又大于或等于其中的3个数,所以主元x至少大于所有数中的$\frac n{10}⋅3=\frac {3n}{10}$个。即划分之后,任意一边的长度至少为$\frac 3{10}$,在最坏情况下,每次选择都选到了$\frac 7{10}$的那一部分。 162 | - $c⋅n$来自其它操作,比如InsertSort(),以及GetPivotIndex()和Partition()里所需的一些额外操作。 163 | 164 | 设$T(n)=t⋅n$,其中t为未知,它可以是一个正常数,也可以是一个关于n的函数,代入上式: 165 | 166 | $$ 167 | \begin{align} 168 | t⋅n&≤\frac {t⋅n}5+\frac{7t⋅n}{10}+c⋅n \tag{两边消去n}\\ 169 | t&≤\frac t 5+\frac {7t}{10}+c \tag{再化简}\\ 170 | t&≤10c \tag{c为一个正常数} 171 | \end{align} 172 | $$ 173 | 174 | 其中c为一个正常数,故t也是一个正常数,即$T(n)≤10c⋅n$,因此$T(n)=O(n)$,至此证明结束。 175 | 176 | 接下来我们再来探讨下BFPRT算法为何选5作为分组主元,而不是2, 3, 7, 9呢? 177 | 178 | 首先排除偶数,对于偶数我们很难取舍其中位数,而奇数很容易。再者对于3而言,会有$T(n)≤T(\frac n 3)+T(\frac {2n}3)+c⋅n$,它本身还是操作了n个元素,与以5为主元的$\frac {9n}{10}$相比,其复杂度并没有减少。对于7,9,...而言,上式中的10c,其整体都会增加,所以与5相比,5更适合。 179 | 180 | ## 四:参考文献 181 | 182 | - 算法导论(第3版). Page 120. 183 | -------------------------------------------------------------------------------- /176-算法复杂度分析概要.md: -------------------------------------------------------------------------------- 1 | ## 一:渐近符号 2 | ### 1.1 符号的辨析 3 | #### 1.1.1 符号$O$ 4 | $O$,读作“大O”,非正式来说,$O(g(n))$是增长次数小于等于$g(n)$及其常数倍($n$趋向于无穷大)的函数集合。 5 | 6 | **定义** 如果函数$f(n)$包含在$O(g(n))$中,记作$f(n)∈O(g(n))$(平时使用为了方便书写,我们通常使用$f(n)=O(g(n))$代替)。它的成立条件是:对于所有足够大的$n$,$f(n)$的上界由$g(n)$的常数倍所确定,也就是说,存在大于$0$的常数$c$和非负整数$n_0$,使得对于所有的$n≥n_0$来说,$f(n)≤c⋅g(n)$。 7 | 8 | 下图说明了这个定义: 9 | 10 | ![](https://61mon.com/images/illustrations/ComplexityAnalysis/1.png) 11 | 12 | 13 | 14 | 15 | 16 | 下面给出几个例子: 17 | 18 | $$ 19 | \begin{align} 20 | n&=O(n^2)\tag{1.1.1.1}\\ 21 | 100n+5&=O(n^2)\tag{1.1.1.2}\\ 22 | \frac 12n(n-1)&=O(n^2)\tag{1.1.1.3}\\ 23 | n^3&≠O(n^2)\tag{1.1.1.4}\\ 24 | 0.00001n^3&≠O(n^2)\tag{1.1.1.5} 25 | \end{align} 26 | $$ 27 | 28 | #### 1.1.2 符号$Ω$ 29 | 30 | $Ω$,读作“omega”,$Ω(g(n))$是增长次数大于等于$g(n)$及其常数倍($n$趋向于无穷大)的函数集合。 31 | 32 | **定义** 如果函数$f(n)$包含在$Ω(g(n))$中,记作$f(n)=Ω(g(n))$。它的成立条件是:对于所有足够大的$n$,$f(n)$的下界由$g(n)$的常数倍所确定,也就是说,存在大于$0$的常数$c$和非负整数$n_0$,使得对于所有的$n≥n_0$来说,$f(n)≥c⋅g(n)$。 33 | 34 | 下图说明了这个定义: 35 | 36 | ![](https://61mon.com/images/illustrations/ComplexityAnalysis/2.png) 37 | 38 | 下面给出几个例子: 39 | 40 | $$ 41 | \begin{align} 42 | n^3&=Ω(n^2)\tag{1.1.2.1}\\ 43 | \frac 12n(n-1)&=Ω(n^2)\tag{1.1.2.2}\\ 44 | 100n+5&≠Ω(n^2)\tag{1.1.2.3} 45 | \end{align} 46 | $$ 47 | 48 | #### 1.1.3 符号$Θ$ 49 | 50 | 读作“theta”。 51 | 52 | **定义** 如果函数$f(n)$包含在$Θ(g(n))$中,记作$f(n)=Θ(g(n))$。它的成立条件是:对于所有足够大的$n$,$f(n)$的上界和下界由$g(n)$的常数倍所确定,也就是说,存在大于$0$的常数$c_1,c_2$和非负整数$n_0$,使得对于所有的$n≥n_0$来说,$c_1⋅g(n)≤f(n)≤c_2⋅g(n)$。 53 | 54 | 下图说明了这个定义: 55 | 56 | ![](https://61mon.com/images/illustrations/ComplexityAnalysis/3.png) 57 | 58 | 下面给出几个例子: 59 | 60 | $$ 61 | \begin{align} 62 | \frac 12n(n-1)&=Θ(n^2)\tag{$c_1=\frac 14,c_2=\frac 12,n_0=2$}\\ 63 | 6n^3&≠Θ(n^2)\tag{1.1.3.2} 64 | \end{align} 65 | $$ 66 | 67 | ### 1.2 符号的性质 68 | 69 | **定理** 如果$t_1(n)=O(g_1(n))$并且$t_2(n)=O(g_2(n))$,则 70 | $$ 71 | t_1(n)+t_2(n)=O(\max\{g_1(n),g_2(n)\}) 72 | $$ 73 | 74 | 对于$Ω$和$Θ$符号,同样成立。 75 | 76 | **证明** 增长次数的证明是基于以下简单事实:对于$4$个任意实数$a_1,b_1,a_2,b_2$,如果$a_1≤b_1$并且$a_2≤b_2$,则$a_1+a_2≤2\max\{b_1,b_2\}$。 77 | 78 | 因为$t_1(n)=O(g_1(n))$,且存在正常量$c_1$和非负整数$n_1$,使得对于所有的$n≥n_1$,有$t_1(n)≤c_1g_1(n)$。同理,因为$t_2(n)=O(g_2(n))$,对于所有的$n≥n_2$,亦有$t_2(n)≤c_2g_2(n)$。 79 | 80 | 假设$c_3=\max\{c_1,c_2\}$并且$n≥\max\{n_1,n_2\}$,就可以利用两个不等式的结论将其相加,得出以下结论: 81 | 82 | $$ 83 | \begin{align} 84 | t_1(n)+t_2(n)&≤c_1g_1(n)+c_2g_2(n)\tag{1.2.1}\\ 85 | &≤c_3g_1(n)+c_3g_2(n)=c_3[g_1(n)+g_2(n)]\tag{1.2.2}\\ 86 | &≤c_3⋅2\max\{g_1(n),g_2(n)\}\tag{1.2.3} 87 | \end{align} 88 | $$ 89 | 90 | 那么,对于**两个连续执行部分**组成的算法,应该如何应用这个特性呢?**它意味着该算法的整体效率是由具有较大增长次数的部分所决定的,即效率较差的部分决定**。 91 | 92 | ## 二:复杂度求解方法分析 93 | 94 | 对于一个普通函数(即非递归函数),其时间复杂度自然易求。下面我们主要谈谈如何求解递归函数的时间复杂度。 95 | 96 | 递归函数通常会有以下的方程式: 97 | 98 | $$ 99 | T(n)=aT(\frac nb)+f(n)\tag{2.1} 100 | $$ 101 | 其中,$a≥1,b>1$,且都是常数,$f(n)$是渐近正函数。递归式$(2.1)$描述的是这样一种算法的运行时间:它将规模为$n$的问题分解为$a$个子问题,每个子问题规模为$\frac nb$,其中$a≥1,b>1$。$a$个子问题递归地求解,每个花费时间$T(\frac nb)$。函数$f(n)$包含了问题分解和子问题解合并的代价。 102 | 103 | 解决上述方程式常用的方法有两种:**主定理**和**分治法**。 104 | 105 | ### 2.1 主定理(Master Theorem) 106 | 107 | **定理** 令$a≥1,b>1$,且都是常数,$f(n)$是一个函数,$T(n)$是定义在非负整数上的递归式,即: 108 | 109 | $$ 110 | T(n)=aT(\frac nb)+f(n) 111 | $$ 112 | 113 | 其中我们将$\frac nb​$解释为$⌊\frac nb⌋​$或$⌈\frac nb⌉​$。那么$T(n)​$有如下渐近界: 114 | 115 | (Case 1)如果$f(n)=O(n^c)$,其中$clog_ba$,并且对某个常数$k<1$和所有足够大的$n$有$af(\frac nb)≤kf(n)$,则: 128 | 129 | $$ 130 | T(n)=Θ(f(n)) 131 | $$ 132 | 133 | 算法导论已有关于这个定理的证明,故此处省略,有兴趣的读者可以去翻阅下。 134 | 135 | ### 2.2 分治法 136 | 137 | 分治法先把给定的实例划分为若干个较小的实例,对每个实例递归求解,然后如果有必要,再把较小实例的解合并成给定实例的一个解。假设所有较小实例的规模都为$\frac nb$,其中$a$个实例需要实际求解,对于$n=b^k,k=1,2,3...$,其中$a≥1,b>1$,得到以下结果: 138 | 139 | $$ 140 | \begin{align} 141 | T(n)&=aT(\frac nb)+f(n)\tag{原始递归方程式}\\ 142 | T(b^k)&=aT(b^{k-1})+f(b^k)\tag{2.2.2}\\ 143 | &=a[aT(b^{k-2})+f(b^{k-1})]+f(b^k)=a^2T(b^{k-2})+af(b^{k-1})+f(b^k)\tag{2.2.3}\\ 144 | &=a^3T(b^{k-3})+a^2f(b^{k-2})+af(b^{k-1})+f(b^k)\tag{2.2.4}\\ 145 | &=...\tag{2.2.5}\\ 146 | &=a^kT(1)+a^{k-1}f(b^1)+a^{k-2}f(b^2)+...+a^0f(b^k)\tag{2.2.6}\\ 147 | &=a^k[T(1)+\sum_{j=1}^{k} {\frac {f(b^j)}{a^j}}]\tag{2.2.7} 148 | \end{align} 149 | $$ 150 | 151 | 由于$a^k=a^{log_bn}=n^{log_ba}$,当$n=b^k$时,对于式$(2.2.7)$我们可以推出下式: 152 | 153 | $$ 154 | T(n)=n^{log_ba}[T(1)+\sum_{j=1}^{log_bn}{\frac {f(b^j)}{a^j}}]\tag{2.2.8} 155 | $$ 156 | 157 | 显然,**$T(n)$的增长次数取决于常数$a$和$b$的值以及函数$f(n)$的增长次数。** 158 | 159 | ## 三:无意发现的定理 160 | 161 | 以下是我自己在演算时无意发现的两条定理,并给出了证明。该定理依旧建立在递归方程式中,即: 162 | 163 | $$ 164 | T(n)=aT(\frac nb)+f(n)\tag{$a≥1,b>1$} 165 | $$ 166 | 167 | 根据以上的方程式有以下两个定理: 168 | 169 | ### 3.1 定理1 170 | 171 | **定理1** 对于递归方程式,若$a=1,b>1,f(n)=c,c$为某个常数,即: 172 | 173 | $$ 174 | T(n)=T(\frac nb)+c 175 | $$ 176 | 177 | 则: 178 | 179 | $$ 180 | T(n)=Θ(logn) 181 | $$ 182 | 183 | **证明** 应用主定理 Case 2,其中$c=log_ba=log_b1=0$,再使$k=0$,则$f(n)=Θ(n^clog^kn)=Θ(1)$,这里的$f(n)$即等于常数$c$,证明成立。 184 | 185 | ### 3.2 定理2 186 | 187 | **定理2** 对于递归方程式,若$a=1,b>1,f(n)=kn+p$,其中$k>0,p>0$且为某个常数(也就是$f(n)$是一个线性直线方程),即: 188 | 189 | $$ 190 | T(n)=T(\frac nb)+(kn+p)\tag{$b>1,k>0,p$为某个常数} 191 | $$ 192 | 193 | 则: 194 | 195 | $$ 196 | T(n)=Θ(n) 197 | $$ 198 | 199 | **证明** 应用分治法中式$(2.2.8)$: 200 | 201 | $$ 202 | \begin{align} 203 | T(n)&=n^{log_ba}[T(1)+\sum_{j=1}^{log_bn}{\frac {f(b^j)}{a^j}}]\tag{3.2.1}\\ 204 | &=\sum_{j=1}^{log_bn}{(kb^j+p)}\tag{3.2.2}\\ 205 | &=plog_bn+\frac {kb}{b-1}(n-1)\tag{$k>0,p>0,b>1$}\\ 206 | &0$且为某个常数}\\ 208 | &=Θ(n)\tag{3.2.5} 209 | \end{align} 210 | $$ 211 | 212 | 证明成立。 213 | 214 | ## 四:总结 215 | 216 | 第一部分辨析了$O,Ω,Θ$三种符号的区别以及它们的性质。 217 | 218 | 第二部分介绍了两种常用的计算时间复杂度方法,即主定理和分治法。 219 | 220 | 第三部分给出了个人在演算时发现的两个定理,并给出了证明,题外话,这两条定理比较实用,希望读者能够熟记。另外如果您在其它网站看到类似原创定理,纯属巧合。 221 | 222 | ## 五:参考文献 223 | 224 | - 算法设计与分析基础(第3版). Page 40-45,Page 376. 225 | - 维基百科. [主定理](https://zh.wikipedia.org/wiki/%E4%B8%BB%E5%AE%9A%E7%90%86). 226 | -------------------------------------------------------------------------------- /179-单链表的各种操作(面试必备).md: -------------------------------------------------------------------------------- 1 | # 一:前言 2 | 单链表经常为公司面试所提及,先不贬其过于简单,因为单链表确实是数据结构中最简单的一部分,但往往最简单的,人们越无法把握其细节。本文一共总结了单链表常被提及的各种操作,如下: 3 | (1)逆序构造单链表; 4 | (2)链表反转; 5 | (3)链表排序; 6 | (4)合并两个有序链表; 7 | (5)求出链表倒数第k个值; 8 | (6)判断链表是否有环,有环返回相遇结点; 9 | (7)在一个有环链表中找到环的入口; 10 | (8)删除当前结点; 11 | (9)找出链表的中间结点。 12 | 13 | 14 | 15 | 16 | 17 | 本文中的所有操作均针对带有头结点的单链表。请注意:头结点和第一结点是两个结点,百度百科解释为:为方便操作,在单链表的第一个结点之前附设一个结点,称之为头结点。本文中用header代替头结点。 18 | 19 | 在继续下文之前先约定下结点结构: 20 | 21 | ```c++ 22 | /* 定义结点结构 */ 23 | struct Node 24 | { 25 | int data; 26 | Node * next; 27 | Node() { data = 0; next = nullptr; } 28 | }; 29 | 30 | /* 定义头结点 */ 31 | Node * header = new Node; 32 | ``` 33 | # 二:具体分析与实现代码 34 | ## 2.1 逆序构造单链表 35 | 36 | 例如:输入数据:1 2 3 4 5 6,构造单链表:6->5->4->3->2->1。 37 | ```c++ 38 | /* 逆序构造单链表,-1 结束输入 */ 39 | void desc_construct(Node * header) 40 | { 41 | Node * pre = nullptr; // 前一个结点 42 | int x; 43 | 44 | while (cin >> x && x != -1) 45 | { 46 | Node * cur = new Node; 47 | cur->data = x; 48 | cur->next = pre; // 指向前一个结点 49 | pre = cur; // 保存当前结点 50 | } 51 | 52 | header->next = pre; // 头结点指向第一结点 53 | } 54 | ``` 55 | ## 2.2 链表反转 56 | 57 | 例如:假设现有链表:6->5->4->3->2->1,进行反转操作后,链表变成:1->2->3->4->5->6。 58 | ```c++ 59 | /* 反转链表 */ 60 | void reverse(Node * header) 61 | { 62 | if (!header->next || !header->next->next) // 如果是空链表或链表只有一个结点 63 | return; 64 | 65 | Node * cur = header->next; // 指向第一个结点 66 | Node * pre = nullptr; 67 | 68 | while (cur) 69 | { 70 | Node * temp = cur->next; // 保存下一个结点 71 | cur->next = pre; // 调整指向 72 | pre = cur; // pre 前进一步 73 | cur = temp; // cur 前进一步 74 | } 75 | 76 | header->next = pre; // 头结点指向反转后的第一结点 77 | } 78 | ``` 79 | ## 2.3 链表升序排序 80 | 81 | 我们希望用最小的时间复杂度来完成这个排序任务。归并排序是个不错的选择,平均时间复杂度$T(n)=O(nlogn)$,但是还有其他方法么? 82 | 83 | 我们想到经常出现的快排,快排是需要一个指针指向头,一个指针指向尾,然后两个指针相向运动并按一定规律交换值,最后使得支点左边小于支点,支点右边大于支点,但是对于单链表而言,指向结尾的指针很好办,但是这个指针如何往前,我们只有一个next(这并不是一个双向链表)。 84 | 85 | 如果是这样的话,对于单链表我们没有前驱指针,怎么能使得后面的那个指针往前移动呢?所以这种快排思路行不通,如果我们能使两个指针都往next方向移动并且也可以按相同规律交换值那就好了,怎么做呢? 86 | 87 | 接下来我们使用快排的另一种思路来解答。我们只需要两个指针i和j,这两个指针均往next方向移动,移动的过程中始终保持区间[1, i]的data都小于base(位置0是主元),区间[i+1, j)的data都大于等于base,那么当j走到末尾的时候便完成了一次支点的寻找。若以swap操作即if判断语句成立作为基本操作,其操作数和快速排序相同,故该方法的平均时间复杂度亦为$T(n)=O(nlogn)$。 88 | ```c++ 89 | /** 90 | * 链表升序排序 91 | * 92 | * begin 链表的第一个结点,即 header->next 93 | * end 链表的最后一个结点的 next 94 | */ 95 | void asc_sort(Node * begin, Node * end) 96 | { 97 | if (begin == end || begin->next == end) // 链表为空或只有一个结点 98 | return; 99 | 100 | int base = begin->data; // 设置主元 101 | Node * i = begin; // i 左边的小于 base 102 | Node * j = begin->next; // i 和 j 中间的大于 base 103 | 104 | while (j != end) 105 | { 106 | if (j->data < base) 107 | { 108 | i = i->next; 109 | swap(i->data, j->data); 110 | } 111 | j = j->next; 112 | } 113 | swap(i->data, begin->data); // 交换主元和 i 的值 114 | 115 | asc_sort(begin, i); // 递归左边 116 | asc_sort(i->next, end); // 递归右边 117 | } 118 | 119 | // how to use it? 120 | asc_sort(header->next, nullptr); 121 | ``` 122 | ## 2.4 合并两个有序的单链表 123 | 124 | 为简化问题,以下代码为合并两个升序链表。 125 | 126 | ```c++ 127 | /* 合并两个有序链表 */ 128 | void asc_merge(Node * header, Node * other_header) 129 | { 130 | asc_sort(header->next, nullptr); // 保证有序 131 | asc_sort(other_header->next, nullptr); 132 | 133 | if (!header->next) // 链表为空 134 | { 135 | header->next = other_header->next; // 合并后两个 header 指向第一个结点 136 | return; 137 | } 138 | if (!list->header->next) // 链表为空 139 | { 140 | other_header->next = header->next; // 合并后两个 header 指向第一个结点 141 | return; 142 | } 143 | 144 | Node * p = nullptr; // 还需一个指针,指向合并的结点 145 | Node * this_pointer = header->next; // 第一个结点 146 | Node * other_pointer = other_header->next; // 第一个结点 147 | 148 | // 单独考虑合并的第一个结点 149 | if (this_pointer->data < other_pointer->data) 150 | { 151 | other_header->next = p = this_pointer; // p 指向新合并的结点 152 | this_pointer = this_pointer->next; // 前进一步 153 | } 154 | else 155 | { 156 | header->next = p = other_pointer; // p 指向新合并的结点 157 | other_pointer = other_pointer->next; // 前进一步 158 | } 159 | 160 | while (this_pointer && other_pointer) 161 | { 162 | if (this_pointer->data < other_pointer->data) 163 | { 164 | p->next = this_pointer; // 合并新结点 165 | p = this_pointer; // p 前进一步指向新合并的结点 166 | this_pointer = this_pointer->next; 167 | } 168 | else 169 | { 170 | p->next = other_pointer; // 合并新结点 171 | p = other_pointer; // p 前进一步指向新合并的结点 172 | other_pointer = other_pointer->next; 173 | } 174 | } 175 | 176 | // 处理剩下的结点 177 | if (this_pointer) 178 | p->next = this_pointer; 179 | if (other_pointer) 180 | p->next = other_pointer; 181 | } 182 | ``` 183 | 184 | ## 2.5 返回链表倒数第k个值 185 | 186 | 例如,给定链表1->4->3->5->6->8,返回倒数第3个数,也就是5。要求,只给定链表,但并不知道链表长度,如何在最短时间内找出这个倒数第k个值。 187 | 188 | 其实思路很简单,假设k是小于等于链表长度,那么我们可以设置两个指针p和q,这两个指针在链表里的距离就是k,那么后面那个指针走到链表末尾的nullptr时,另一个指针肯定指向链表倒数第k个值。 189 | ```c++ 190 | /* 返回链表倒数第k个值 */ 191 | int kth_last(Node * header, int k) 192 | { 193 | Node * p = header->next; 194 | Node * q = p; 195 | 196 | for (int i = 0; i < k; i++) 197 | { 198 | if (!q) 199 | { 200 | cout << "链表长度小于k\n"; 201 | return -1; 202 | } 203 | q = q->next; 204 | } 205 | 206 | while (q) 207 | { 208 | q = q->next; 209 | p = p->next; 210 | } 211 | 212 | return p->data; 213 | } 214 | ``` 215 | ## 2.6 判断链表是否有环,有环返回相遇结点 216 | 有环是什么意思?一个单链表最后一个结点的位置的next应该是nullptr,标志着链表的结尾,但是如果现在这个next指向了链表里的某一个结点(可以是自身),那么这个链表就存在环。如下图: 217 | 218 | ![](https://61mon.com/images/illustrations/SinglyLinkedList/1.png) 219 | 220 | 因此我们只要找到两个结点,其地址相同(因为两个结点的data可能相同),即可断定有环。 221 | 222 | 我们的思路就是:设置两个快慢指针(快慢指针即两个指针起点相同,慢指针每次走一步,快指针走两步),让它们一直往下走,直到它们相等,说明有环;遇到nullptr,说明无环。下面简单证明:如上图,A为链表第一个结点,B为环与链表的交叉点,C为`slow_pointer`与`fast_pointer`相遇的位置。假设环的长度为r,则有 223 | 224 | $$ 225 | AB+BC+t_1r=\frac {AB+BC+t_2r}{2} \tag{左为慢指针,右为快指针} 226 | $$ 227 | 228 | 化简为: 229 | 230 | $$ 231 | AB+BC=(t_2-2t_1)r \tag{t1,t2为整数} 232 | $$ 233 | 234 | 在确定了AB和r后,只需调整BC,使AB+BC能整除r即可。 235 | ```c++ 236 | /* 判断链表是否有环,有环返回相遇结点 */ 237 | Node * is_loop(Node * header) 238 | { 239 | if (!header->next) // 空链表 240 | return nullptr; 241 | 242 | Node * slow_pointer = header; 243 | Node * fast_pointer = header; 244 | 245 | while (fast_pointer->next && fast_pointer->next->next && slow_pointer != fast_pointer) 246 | { 247 | slow_pointer = slow_pointer->next; // 慢指针走一步 248 | fast_pointer = fast_pointer->next->next; // 快指针走两步 249 | } 250 | 251 | if (slow_pointer == fast_pointer) 252 | return slow_pointer; 253 | 254 | return nullptr; 255 | } 256 | ``` 257 | ## 2.7 在一个有环链表中找到环的入口 258 | 259 | 参考2.6图,若存在环且找到了相遇点C,此时令一个指针start\_node从链表第一个结点处开始往后遍历,再令另一个指针meet_node从C处往后遍历,它们的相遇结点就是环的入口点。为什么呢? 260 | 261 | 2.6公式已经证明了:若快慢指针相遇在C点,则: 262 | 263 | 264 | $$ 265 | AB+BC=tr \tag{t是整数} 266 | $$ 267 | 268 | 进一步整理上式为: 269 | 270 | $$ 271 | AB=(r-BC)+(t-1)r \tag{其中r-BC的含义请对照2.6图} 272 | $$ 273 | 274 | 好了,至此,证明就已经很显然了。当start_node走了r-BC距离后,meet_node正好到达入口处B点,此时start_node还剩(t-1)r距离,显然两个指针继续走的话,一定会相遇在入口处B点。 275 | ```c++ 276 | /* 在一个有环链表中找到环的入口 */ 277 | Node * find_meet_node(Node * header) 278 | { 279 | Node * meet_node = is_loop(header); 280 | 281 | if (meet_node == nullptr) // 不存在环 282 | return nullptr; 283 | 284 | Node * start_node = header->next; 285 | while (start_node != meet_node) 286 | { 287 | start_node = start_node->next; 288 | meet_node = meet_node->next; 289 | } 290 | 291 | return start_node; 292 | } 293 | ``` 294 | 此外,我们也会遇到“判断两个链表是否相交”,“求出两个相交链表的交点”这样的问题,百变不离其宗,我们只需把链表尾接到其中一个链表头就转化为2.6和2.7的问题,所以在这里不作详述了。 295 | 296 | ![](https://61mon.com/images/illustrations/SinglyLinkedList/2.jpg) 297 | 298 | ## 2.8 删除当前结点 299 | 300 | 题意规定,给定要删除的结点和头结点,现要你删除这个结点,要求平均时间复杂度为$T(n)=O(1)$。 301 | 302 | 例如,现有这样的链表,1->2->3->4->5->6,需要删除4,我们的思路肯定是先找到4的前一个结点3,和4的后一个结点5,然后把3和5连起来,再把4删除。但是这样做的话,我们需要花费$O(n)$的时间来找到3和5,与题意要求的$O(1)$相距甚远。 303 | 304 | 我们之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除结点的前一个结点。我们试着换一种思路。如果我们要删除4,可以把4和5的数据交换下,然后删除5,再把4和6连接起来,如此其时间复杂度为$O(1)$。 305 | 306 | 上面的思路还有一个问题:如果删除的结点位于链表的尾部,没有下一个结点,怎么办?我们仍然从链表的头结点开始,顺便遍历得到给定结点的前序结点,并完成删除操作。这个时候时间复杂度是$O(n)$。那题目要求我们需要在$O(1)$时间完成删除操作,我们的算法是不是不符合要求?实际上,假设链表总共有n个结点,我们的算法在n-1个情况下,时间复杂度是$O(1)$,只有当给定的结点处于链表末尾的时候,时间复杂度为$O(n)$。因此其平均时间复杂度$\frac {(n-1)⋅O(1)+1⋅O(n)}n$,仍然为$O(1)$。 307 | ```c++ 308 | /* 删除当前结点 */ 309 | void del(Node * header, Node * position) 310 | { 311 | if (!position->next) // 要删除的是最后一个结点 312 | { 313 | Node * p = header; 314 | while (p->next != position) 315 | p = p->next; // 找到 position 的前一个结点 316 | p->next = nullptr; 317 | delete position; 318 | } 319 | else 320 | { 321 | Node * p = position->next; 322 | swap(p->data, position->data); 323 | position->next = p->next; 324 | delete p; 325 | } 326 | } 327 | ``` 328 | 329 | ## 2.9 找出单链表的中间结点 330 | 331 | 题意要求,给定链表头结点,在最小复杂度下输出该链表的中间结点。 332 | 如果只知链表的头结点,我们一般的思路就是先遍历链表得到链表长度,然后再遍历一遍得到中间结点,如此时间复杂度为$O(n)+O(\frac n2)$。 333 | 334 | 上面的思路似乎不太令人满意。我们又想到快慢指针,它有一个很重要的性质:慢指针走的长度等于快慢指针相距的程度。所以利用这个性质,当快指针走到链表尾时,慢指针正好在中间结点。 335 | ```c++ 336 | /* 找出单链表的中间结点 */ 337 | Node * find_middle(Node * header) 338 | { 339 | Node * slow_pointer = header; 340 | Node * fast_pointer = header; 341 | 342 | while (fast_pointer->next && fast_pointer->next->next) 343 | { 344 | slow_pointer = slow_pointer->next; // 慢指针走一步 345 | fast_pointer = fast_pointer->next->next; // 快指针走两步 346 | } 347 | 348 | return slow_pointer; 349 | } 350 | ``` 351 | -------------------------------------------------------------------------------- /180-表达式求值.md: -------------------------------------------------------------------------------- 1 | ## 一:前言 2 | 所谓表达式求值,就是写一个微型计算器。例如输入:(1+9)\* 2 / 2 - 1,输出计算结果9。对于这样的问题,我们一般利用栈,模拟数学运算来完成。为了简化问题,在继续下面的分析之前,先在此作个约定:本文只讨论`+-*/()`基本的四则运算,另外不对意外出现的符号(例如^)和不符合规范的数学表达式(例如2*-1)做异常处理。 3 | 4 | 5 | 6 | 7 | 8 | ## 二:思路及分析 9 | 我们用一个字符数组(即`char s[1000]`)来存储数学表达式,定义一个全局变量g_pos表示s[ ]的下标,下标从0开始。首先我们定义两个栈,optr和opnd,分别存储运算符和运算数,遇到运算数直接放进opnd;遇到运算符,分四种情况: 10 | (1):遇到负号; 11 | (2):遇到右括号; 12 | (3):遇到左括号; 13 | (4):遇到+-\*/; 14 | 15 | 前三种情况容易理解,这里就谈下第四种情况。我们都知道四则运算是遵循先乘除再加减的,因此对于每个运算符,我们都要和optr的栈顶符号等级比较,如果这个符号的等级比optr栈顶符号等级高,什么也不做,直接放进栈;如果小于等于,就需要把opnd的栈顶两个数字抽出来,进行计算。这里有个问题,在遇到第一个运算符时,此时optr为空,而我们又需要与optr栈顶运算符比较等级,这时候怎么办?为了解决这个问题,我们在初始化optr的时候,放进一个#,设置其等级为最低。 16 | 17 | 看到这里你可能还存有一个疑问,如何判定符号`-`是负号还是减号?例如-1+2 和 5-1+2。分析发现,出现负号只有两种情况: 18 | (1):左边是左括号,例如(-1+5\*3); 19 | (2):字符串的第一个字符,例如-5\*6-1。 20 | 21 | 为了方便代码书写,若出现负号,就在运算数栈加入一个数字0(这也就是下面代码里出现bool值is_minus的原因),即转化一下表达式,例如-1+2转化为0-1+2。 22 | 23 | ## 三:代码 24 | ```c++ 25 | /** 26 | * 27 | * author 刘毅(Limer) 28 | * date 2017-02-24 29 | * mode C++ 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | using namespace std; 39 | 40 | char s[1000]; 41 | int g_pos; // 字符数组的下标 42 | 43 | /* 字符转数字 */ 44 | double Translation(int & pos) 45 | { 46 | double integer = 0.0; // 整数部分 47 | double remainder = 0.0; // 余数部分 48 | 49 | while (s[pos] >= '0' && s[pos] <= '9') 50 | { 51 | integer *= 10; 52 | integer += (s[pos] - '0'); 53 | pos++; 54 | } 55 | 56 | if (s[pos] == '.') 57 | { 58 | pos++; 59 | int c = 1; 60 | while (s[pos] >= '0' && s[pos] <= '9') 61 | { 62 | double t = s[pos] - '0'; 63 | t *= pow(0.1, c); 64 | c++; 65 | remainder += t; 66 | pos++; 67 | } 68 | } 69 | 70 | return integer + remainder; 71 | } 72 | 73 | /* 返回运算符级别 */ 74 | int GetLevel(char ch) 75 | { 76 | switch (ch) 77 | { 78 | case '+': 79 | case '-': 80 | return 1; 81 | case '*': 82 | case '/': 83 | return 2; 84 | case '(': 85 | return 0; 86 | case '#': 87 | return -1; 88 | }; 89 | } 90 | 91 | /* 对两个数进行运算 */ 92 | double Operate(double a1, char op, double a2) 93 | { 94 | switch (op) 95 | { 96 | case '+': 97 | return a1 + a2; 98 | case '-': 99 | return a1 - a2; 100 | case '*': 101 | return a1 * a2; 102 | case '/': 103 | return a1 / a2; 104 | }; 105 | } 106 | 107 | /* 利用两个栈进行模拟计算 */ 108 | double Compute() 109 | { 110 | stack optr; // 操作符栈 111 | stack opnd; // 操作数栈 112 | 113 | optr.push('#'); 114 | int len = strlen(s); 115 | bool is_minus = true; // 判断'-'是减号还是负号 116 | 117 | for (g_pos = 0; g_pos < len;) 118 | { 119 | //1. 负号 120 | if (s[g_pos] == '-' && is_minus) // 是负号 121 | { 122 | opnd.push(0); 123 | optr.push('-'); 124 | g_pos++; 125 | } 126 | //2. 是右括号 ) 127 | else if (s[g_pos] == ')') 128 | { 129 | is_minus = false; 130 | g_pos++; 131 | 132 | while (optr.top() != '(') 133 | { 134 | double a2 = opnd.top(); 135 | opnd.pop(); 136 | double a1 = opnd.top(); 137 | opnd.pop(); 138 | char op = optr.top(); 139 | optr.pop(); 140 | 141 | double result = Operate(a1, op, a2); 142 | opnd.push(result); 143 | } 144 | 145 | optr.pop(); // 删除'(' 146 | } 147 | //3. 数字 148 | else if (s[g_pos] >= '0' && s[g_pos] <= '9') 149 | { 150 | is_minus = false; 151 | opnd.push(Translation(g_pos)); 152 | } 153 | //4. ( 左括号 154 | else if (s[g_pos] == '(') 155 | { 156 | is_minus = true; 157 | optr.push(s[g_pos]); 158 | g_pos++; 159 | } 160 | //5. + - * / 四种 161 | else 162 | { 163 | while (GetLevel(s[g_pos]) <= GetLevel(optr.top())) 164 | { 165 | double a2 = opnd.top(); 166 | opnd.pop(); 167 | double a1 = opnd.top(); 168 | opnd.pop(); 169 | char op = optr.top(); 170 | optr.pop(); 171 | 172 | double result = Operate(a1, op, a2); 173 | opnd.push(result); 174 | } 175 | 176 | optr.push(s[g_pos]); 177 | g_pos++; 178 | } 179 | } 180 | 181 | while (optr.top() != '#') 182 | { 183 | double a2 = opnd.top(); 184 | opnd.pop(); 185 | double a1 = opnd.top(); 186 | opnd.pop(); 187 | char op = optr.top(); 188 | optr.pop(); 189 | 190 | double result = Operate(a1, op, a2); 191 | opnd.push(result); 192 | } 193 | 194 | return opnd.top(); 195 | } 196 | 197 | int main() 198 | { 199 | while (cin >> s) 200 | cout << "结果为:" << Compute() << endl << endl; 201 | 202 | return 0; 203 | } 204 | ``` 205 | 数据测试如下图: 206 | 207 | ![](https://61mon.com/images/illustrations/ExpressionEvaluation/1.png) 208 | -------------------------------------------------------------------------------- /181-Manacher算法.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 给定一个字符串,求出其最长回文子串。例如: 3 | (1):s="abcd",最长回文长度为 1; 4 | (2):s="ababa",最长回文长度为 5; 5 | (3):s="abccb",最长回文长度为 4,即bccb。 6 | 7 | 以上问题的传统思路大概是,遍历每一个字符,以该字符为中心向两边查找。其时间复杂度为$O(n^2)$,效率很差。 8 | 9 | 1975年,一个叫Manacher的人发明了一个算法,Manacher算法(中文名:马拉车算法),该算法可以把时间复杂度提升到$O(n)$。下面来看看马拉车算法是如何工作的。 10 | 11 | 12 | 13 | 14 | 15 | ## 二:算法过程分析 16 | 由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,具体做法是:在字符串首尾,及各字符间各插入一个字符(前提这个字符未出现在串里)。 17 | 18 | 举个例子:`s="abbahopxpo"`,转换为`s_new="$#a#b#b#a#h#o#p#x#p#o#"`(这里的字符 $ 只是为了防止越界,下面代码会有说明),如此,s 里起初有一个偶回文`abba`和一个奇回文`opxpo`,被转换为`#a#b#b#a#`和`#o#p#x#p#o#`,长度都转换成了**奇数**。 19 | 20 | 定义一个辅助数组`int p[]`,其中`p[i]`表示以 i 为中心的最长回文的半径,例如: 21 | 22 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | | :------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 24 | | s_new[i] | $ | # | a | # | b | # | b | # | a | # | h | # | o | # | p | # | x | # | p | # | 25 | | p[i] | | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 26 | 27 | 可以看出,`p[i] - 1`正好是原字符串中最长回文串的长度。 28 | 29 | 接下来的重点就是求解 p 数组,如下图: 30 | ![](https://61mon.com/images/illustrations/Manacher/1.png) 31 | 设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是`mx = id + p[id]`。 32 | 33 | 假设我们现在求`p[i]`,也就是以 i 为中心的最长回文半径,如果`i < mx`,如上图,那么: 34 | 35 | ```c++ 36 | if (i < mx) 37 | p[i] = min(p[2 * id - i], mx - i); 38 | ``` 39 | `2 * id - i`为 i 关于 id 的对称点,即上图的 j 点,而**`p[j]`表示以 j 为中心的最长回文半径**,因此我们可以利用`p[j]`来加快查找。 40 | 41 | ## 三:代码 42 | ```c++ 43 | /** 44 | * 45 | * author 刘毅(Limer) 46 | * date 2017-02-25 47 | * mode C++ 48 | */ 49 | 50 | #include 51 | #include 52 | #include 53 | 54 | using namespace std; 55 | 56 | char s[1000]; 57 | char s_new[2000]; 58 | int p[2000]; 59 | 60 | int Init() 61 | { 62 | int len = strlen(s); 63 | s_new[0] = '$'; 64 | s_new[1] = '#'; 65 | int j = 2; 66 | 67 | for (int i = 0; i < len; i++) 68 | { 69 | s_new[j++] = s[i]; 70 | s_new[j++] = '#'; 71 | } 72 | 73 | s_new[j] = '\0'; // 别忘了哦 74 | 75 | return j; // 返回 s_new 的长度 76 | } 77 | 78 | int Manacher() 79 | { 80 | int len = Init(); // 取得新字符串长度并完成向 s_new 的转换 81 | int max_len = -1; // 最长回文长度 82 | 83 | int id; 84 | int mx = 0; 85 | 86 | for (int i = 1; i < len; i++) 87 | { 88 | if (i < mx) 89 | p[i] = min(p[2 * id - i], mx - i); // 需搞清楚上面那张图含义, mx 和 2*id-i 的含义 90 | else 91 | p[i] = 1; 92 | 93 | while (s_new[i - p[i]] == s_new[i + p[i]]) // 不需边界判断,因为左有'$',右有'\0' 94 | p[i]++; 95 | 96 | // 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,这样才能更有机会执行 if (i < mx)这句代码,从而提高效率 97 | if (mx < i + p[i]) 98 | { 99 | id = i; 100 | mx = i + p[i]; 101 | } 102 | 103 | max_len = max(max_len, p[i] - 1); 104 | } 105 | 106 | return max_len; 107 | } 108 | 109 | int main() 110 | { 111 | while (printf("请输入字符串:\n")) 112 | { 113 | scanf("%s", s); 114 | printf("最长回文长度为 %d\n\n", Manacher()); 115 | } 116 | return 0; 117 | } 118 | ``` 119 | 120 | ## 四:算法复杂度分析 121 | 文章开头已经提及,Manacher算法为线性算法,即使最差情况下其时间复杂度亦为$O(n)$,在进行证明之前,我们还需要更加深入地理解上述算法过程。 122 | 123 | 根据回文的性质,`p[i]`的值基于以下三种情况得出: 124 | 125 | (1)**j 的回文串有一部分在 id 的之外**,如下图: 126 | ![](https://61mon.com/images/illustrations/Manacher/2.png) 127 | 上图中,黑线为 id 的回文,i 与 j 关于 id 对称,红线为 j 的回文。那么根据代码此时`p[i] = mx - i`,即紫线。那么`p[i]`还可以更大么?答案是不可能!见下图: 128 | ![](https://61mon.com/images/illustrations/Manacher/3.png) 129 | 假设右侧新增的紫色部分是`p[i]`可以增加的部分,那么根据回文的性质,a 等于 d ,也就是说 id 的回文不仅仅是黑线,而是黑线+两条紫线,矛盾,所以假设不成立,故`p[i] = mx - i`,不可以再增加一分。 130 | 131 | (2)**j 回文串全部在 id 的内部**,如下图: 132 | ![](https://61mon.com/images/illustrations/Manacher/4.png) 133 | 根据代码,此时`p[i] = p[j]`,那么`p[i]`还可以更大么?答案亦是不可能!见下图: 134 | ![](https://61mon.com/images/illustrations/Manacher/5.png) 135 | 假设右侧新增的红色部分是`p[i]`可以增加的部分,那么根据回文的性质,a 等于 b ,也就是说 j 的回文应该再加上 a 和 b ,矛盾,所以假设不成立,故`p[i] = p[j]`,也不可以再增加一分。 136 | 137 | (3)**j 回文串左端正好与 id 的回文串左端重合**,见下图: 138 | ![](https://61mon.com/images/illustrations/Manacher/6.png) 139 | 根据代码,此时`p[i] = p[j]`或`p[i] = mx - i`,并且`p[i]`还可以继续增加,所以需要 140 | 141 | ```c++ 142 | while (s_new[i - p[i]] == s_new[i + p[i]]) 143 | p[i]++; 144 | ``` 145 | 根据(1)(2)(3),很容易推出Manacher算法的最坏情况,即为字符串内全是相同字符的时候。在这里我们重点研究Manacher()中的for语句,推算发现for语句内平均访问每个字符5次,即时间复杂度为:$T_{worst}(n)=O(n)$。 146 | 147 | 同理,我们也很容易知道最佳情况下的时间复杂度,即字符串内字符各不相同的时候。推算得平均访问每个字符4次,即时间复杂度为:$T_{best}(n)=O(n)$。 148 | 149 | 综上,**Manacher算法的时间复杂度为$O(n)$**。 150 | 151 | -------------------------------------------------------------------------------- /183-KMP算法(1):如何理解KMP.md: -------------------------------------------------------------------------------- 1 | >系列文章目录 2 | > 3 | >KMP 算法(1):如何理解 KMP 4 | >[KMP算法(2):其细微之处](https://61mon.com/index.php/archives/192/) 5 | 6 | ## 一:背景 7 | 8 | 给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题。 9 | 10 | Knuth-Morris-Pratt 算法(简称 KMP)是解决这一问题的常用算法之一,这个算法是由高德纳(Donald Ervin Knuth)和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终三人于1977年联合发表。 11 | 12 | 在继续下面的内容之前,有必要在这里介绍下两个概念:**真前缀** 和 **真后缀**。 13 | 14 | ![](https://61mon.com/images/illustrations/KMP/1.png) 15 | 16 | 由上图所得, "真前缀"指除了自身以外,一个字符串的全部头部组合;"真后缀"指除了自身以外,一个字符串的全部尾部组合。(网上很多博客,应该说是几乎所有的博客,也包括我以前写的,都是“前缀”。严格来说,“真前缀”和“前缀”是不同的,既然不同,还是不要混为一谈的好!) 17 | 18 | 19 | 20 | 21 | 22 | ## 二:朴素字符串匹配算法 23 | 24 | 初遇串的模式匹配问题,我们脑海中的第一反应,就是朴素字符串匹配(即所谓的暴力匹配),代码如下: 25 | ```c++ 26 | /* 字符串下标始于 0 */ 27 | int NaiveStringSearch(string S, string P) 28 | { 29 | int i = 0; // S 的下标 30 | int j = 0; // P 的下标 31 | int s_len = S.size(); 32 | int p_len = P.size(); 33 | 34 | while (i < s_len && j < p_len) 35 | { 36 | if (S[i] == P[j]) // 若相等,都前进一步 37 | { 38 | i++; 39 | j++; 40 | } 41 | else // 不相等 42 | { 43 | i = i - j + 1; 44 | j = 0; 45 | } 46 | } 47 | 48 | if (j == p_len) // 匹配成功 49 | return i - j; 50 | 51 | return -1; 52 | } 53 | ``` 54 | 暴力匹配的时间复杂度为 $O(nm)$,其中 $n$ 为 S 的长度,$m$ 为 P 的长度。很明显,这样的时间复杂度很难满足我们的需求。 55 | 56 | 接下来进入正题:时间复杂度为 $Θ(n+m)$ 的 KMP 算法。 57 | 58 | ## 三:KMP字符串匹配算法 59 | 60 | ### 3.1 算法流程 61 | 62 | 以下摘自阮一峰的[字符串匹配的KMP算法](http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html),并作稍微修改。 63 | 64 | (1) 65 | 66 | ![](https://61mon.com/images/illustrations/KMP/2.png) 67 | 68 | 首先,主串"BBC ABCDAB ABCDABCDABDE"的第一个字符与模式串"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以模式串后移一位。 69 | 70 | (2) 71 | 72 | ![](https://61mon.com/images/illustrations/KMP/3.png) 73 | 74 | 因为B与A又不匹配,模式串再往后移。 75 | 76 | (3) 77 | 78 | ![](https://61mon.com/images/illustrations/KMP/4.png) 79 | 80 | 就这样,直到主串有一个字符,与模式串的第一个字符相同为止。 81 | 82 | (4) 83 | 84 | ![](https://61mon.com/images/illustrations/KMP/5.png) 85 | 86 | 接着比较主串和模式串的下一个字符,还是相同。 87 | 88 | (5) 89 | 90 | ![](https://61mon.com/images/illustrations/KMP/6.png) 91 | 92 | 直到主串有一个字符,与模式串对应的字符不相同为止。 93 | 94 | (6) 95 | 96 | ![](https://61mon.com/images/illustrations/KMP/7.png) 97 | 98 | 这时,最自然的反应是,将模式串整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。 99 | 100 | (7) 101 | 102 | ![](https://61mon.com/images/illustrations/KMP/8.png) 103 | 104 | 一个基本事实是,当空格与D不匹配时,你其实是已经知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。 105 | 106 | (8) 107 | 108 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 109 | | :-----: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: | 110 | | 模式串 | A | B | C | D | A | B | D | '\\0' | 111 | | next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 112 | 113 | 怎么做到这一点呢?可以针对模式串,设置一个跳转数组`int next[]`,这个数组是怎么计算出来的,后面再介绍,这里只要会用就可以了。 114 | 115 | (9) 116 | 117 | ![](https://61mon.com/images/illustrations/KMP/9.png) 118 | 119 | 已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。根据跳转数组可知,不匹配处D的next值为2,因此接下来**从模式串下标为2的位置开始匹配**。 120 | 121 | (10) 122 | 123 | ![](https://61mon.com/images/illustrations/KMP/10.png) 124 | 125 | 因为空格与C不匹配,C处的next值为0,因此接下来模式串从下标为0处开始匹配。 126 | 127 | (11) 128 | 129 | ![](https://61mon.com/images/illustrations/KMP/11.png) 130 | 131 | 因为空格与A不匹配,此处next值为-1,表示模式串的第一个字符就不匹配,那么直接往后移一位。 132 | 133 | (12) 134 | 135 | ![](https://61mon.com/images/illustrations/KMP/12.png) 136 | 137 | 逐位比较,直到发现C与D不匹配。于是,下一步从下标为2的地方开始匹配。 138 | 139 | (13) 140 | 141 | ![](https://61mon.com/images/illustrations/KMP/13.png) 142 | 143 | 逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成。 144 | 145 | ### 3.2 next数组是如何求出的 146 | 147 | next数组的求解基于“真前缀”和“真后缀”,即`next[i]`等于`P[0]...P[i - 1]`最长的相同真前后缀的长度(请暂时忽视i等于0时的情况,下面会有解释)。我们依旧以上述的表格为例,为了方便阅读,我复制在下方了。 148 | 149 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 150 | | :-------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: | 151 | | 模式串 | A | B | C | D | A | B | D | '\\0' | 152 | | next[ i ] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 153 | 154 | (1):i = 0,对于模式串的首字符,我们统一为`next[0] = -1`; 155 | (2):i = 1,前面的字符串为`A`,其最长相同真前后缀长度为0,即`next[1] = 0`; 156 | (3):i = 2,前面的字符串为`AB`,其最长相同真前后缀长度为0,即`next[2] = 0`; 157 | (4):i = 3,前面的字符串为`ABC`,其最长相同真前后缀长度为0,即`next[3] = 0`; 158 | (5):i = 4,前面的字符串为`ABCD`,其最长相同真前后缀长度为0,即`next[4] = 0`; 159 | (6):i = 5,前面的字符串为`ABCDA`,其最长相同真前后缀为`A`,即`next[5] = 1`; 160 | (7):i = 6,前面的字符串为`ABCDAB`,其最长相同真前后缀为`AB`,即`next[6] = 2`; 161 | (8):i = 7,前面的字符串为`ABCDABD`,其最长相同真前后缀长度为0,即`next[7] = 0`。 162 | 163 | 那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?举个代表性的例子:假如`i = 6`时不匹配,此时我们是知道其位置前的字符串为`ABCDAB`,仔细观察这个字符串,首尾都有一个`AB`,既然在`i = 6`处的D不匹配,我们为何不直接把`i = 2`处的C拿过来继续比较呢,因为都有一个`AB`啊,而这个`AB`就是`ABCDAB`的最长相同真前后缀,其长度2正好是跳转的下标位置。 164 | 165 | 有的读者可能存在疑问,若在`i = 5`时匹配失败,按照我讲解的思路,此时应该把`i = 1`处的字符拿过来继续比较,但是这两个位置的字符是一样的啊,都是`B`,既然一样,拿过来比较不就是无用功了么?其实不是我讲解的有问题,也不是这个算法有问题,而是这个算法还未优化,关于这个问题在下面会详细说明,不过建议读者不要在这里纠结,跳过这个,下面你自然会恍然大悟。 166 | 167 | 思路如此简单,接下来就是代码实现了,如下: 168 | 169 | ```c++ 170 | /* P 为模式串,下标从 0 开始 */ 171 | void GetNext(string P, int next[]) 172 | { 173 | int p_len = P.size(); 174 | int i = 0; // P 的下标 175 | int j = -1; 176 | next[0] = -1; 177 | 178 | while (i < p_len) 179 | { 180 | if (j == -1 || P[i] == P[j]) 181 | { 182 | i++; 183 | j++; 184 | next[i] = j; 185 | } 186 | else 187 | j = next[j]; 188 | } 189 | } 190 | ``` 191 | 一脸懵逼,是不是。。。上述代码就是用来求解模式串中每个位置的`next[]`值。 192 | 193 | 下面具体分析,我把代码分为两部分来讲: 194 | 195 | **(1):i和j的作用是什么?** 196 | 197 | i和j就像是两个”指针“,一前一后,通过移动它们来找到最长的相同真前后缀。 198 | 199 | **(2):if...else...语句里做了什么?** 200 | 201 | ![](https://61mon.com/images/illustrations/KMP/14.png) 202 | 203 | 假设i和j的位置如上图,由`next[i] = j`得,也就是对于位置i来说,**区段[0, i - 1]的最长相同真前后缀分别是[0, j - 1]和[i - j, i - 1],即这两区段内容相同**。 204 | 205 | 按照算法流程,`if (P[i] == P[j])`,则`i++; j++; next[i] = j;`;若不等,则`j = next[j]`,见下图: 206 | 207 | ![](https://61mon.com/images/illustrations/KMP/15.png) 208 | 209 | `next[j]`代表[0, j - 1]区段中最长相同真前后缀的长度。如图,用左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以else语句就是利用第一个椭圆和第四个椭圆内容相同来加快得到[0, i - 1]区段的相同真前后缀的长度。 210 | 211 | 细心的朋友会问if语句中`j == -1`存在的意义是何?第一,程序刚运行时,j是被初始为-1,直接进行`P[i] == P[j]`判断无疑会边界溢出;第二,else语句中`j = next[j]`,j是不断后退的,若j在后退中被赋值为-1(也就是`j = next[0]`),在`P[i] == P[j]`判断也会边界溢出。综上两点,其意义就是为了特殊边界判断。 212 | 213 | ## 四:完整代码 214 | 215 | ```c++ 216 | /** 217 | * 218 | * author : 刘毅(Limer) 219 | * date : 2017-03-05 220 | * mode : C++ 221 | */ 222 | 223 | #include 224 | #include 225 | 226 | using namespace std; 227 | 228 | /* P 为模式串,下标从 0 开始 */ 229 | void GetNext(string P, int next[]) 230 | { 231 | int p_len = P.size(); 232 | int i = 0; // P 的下标 233 | int j = -1; 234 | next[0] = -1; 235 | 236 | while (i < p_len) 237 | { 238 | if (j == -1 || P[i] == P[j]) 239 | { 240 | i++; 241 | j++; 242 | next[i] = j; 243 | } 244 | else 245 | j = next[j]; 246 | } 247 | } 248 | 249 | /* 在 S 中找到 P 第一次出现的位置 */ 250 | int KMP(string S, string P, int next[]) 251 | { 252 | GetNext(P, next); 253 | 254 | int i = 0; // S 的下标 255 | int j = 0; // P 的下标 256 | int s_len = S.size(); 257 | int p_len = P.size(); 258 | 259 | while (i < s_len && j < p_len) 260 | { 261 | if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j] 262 | { 263 | i++; 264 | j++; 265 | } 266 | else 267 | j = next[j]; // 当前字符匹配失败,进行跳转 268 | } 269 | 270 | if (j == p_len) // 匹配成功 271 | return i - j; 272 | 273 | return -1; 274 | } 275 | 276 | int main() 277 | { 278 | int next[100] = { 0 }; 279 | 280 | cout << KMP("bbc abcdab abcdabcdabde", "abcdabd", next) << endl; // 15 281 | 282 | return 0; 283 | } 284 | ``` 285 | 286 | ## 五:算法复杂度分析 287 | 288 | 在`GetNext()`和`KMP()`中,我们观察`i` 的移动,一直往前不回溯,所以它们所耗的时间都是线性的,两者相加为$Θ(m+n)$。 289 | 290 | KMP算法的时间复杂度还是很稳定的。 291 | 292 | - 平均时间复杂度为$Θ(m+n)$。 293 | 294 | - 最好时间复杂度为$O(m+(n-m))=O(n)$。它发生在主串和模式串字符都不相同的情况下,例如,主串为`abcdefghijk`,模式串为`+-*/`。 295 | 296 | - 最差时间复杂度为$O(m+n)$。它发生在主串和模式串都为相同的字符的情况下,例如,主串为`aaaaaaaaaaaaaaaaaaaaa`,模式串为`aaaa`。 297 | 298 | ## 六:KMP优化 299 | 300 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 301 | | :-------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: | 302 | | 模式串 | A | B | C | D | A | B | D | '\\0' | 303 | | next[ i ] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 304 | 305 | 以3.2的表格为例(已复制在上方),若在`i = 5`时匹配失败,按照3.2的代码,此时应该把`i = 1`处的字符拿过来继续比较,但是这两个位置的字符是一样的,都是`B`,既然一样,拿过来比较不就是无用功了么?这我在3.2已经解释过,之所以会这样是因为KMP不够完美。那怎么改写代码就可以解决这个问题呢?很简单。 306 | ```c++ 307 | /* P 为模式串,下标从 0 开始 */ 308 | void GetNextval(string P, int nextval[]) 309 | { 310 | int p_len = P.size(); 311 | int i = 0; // P 的下标 312 | int j = -1; 313 | nextval[0] = -1; 314 | 315 | while (i < p_len) 316 | { 317 | if (j == -1 || P[i] == P[j]) 318 | { 319 | i++; 320 | j++; 321 | 322 | if (P[i] != P[j]) 323 | nextval[i] = j; 324 | else 325 | nextval[i] = nextval[j]; // 既然相同就继续往前找真前缀 326 | } 327 | else 328 | j = nextval[j]; 329 | } 330 | } 331 | ``` 332 | 在此也给各位读者提个醒,KMP算法严格来说分为KMP算法(未优化版)和KMP算法(优化版),所以建议读者在表述KMP算法时,最好告知你的版本,因为两者在某些情况下区别很大,这里简单说下。 333 | 334 | **KMP算法(未优化版):** next数组表示最长的相同真前后缀的长度,我们不仅可以利用next来解决模式串的匹配问题,也可以用来解决类似字符串重复问题等等,这类问题大家可以在各大OJ找到,这里不作过多表述。 335 | 336 | **KMP算法(优化版):** 根据代码很容易知道(名称也改为了nextval),优化后的next仅仅表示相同真前后缀的长度,但**不一定是最长**(我个人称之为“最优相同真前后缀”)。此时我们利用优化后的next可以在模式串匹配问题中以更快的速度得到我们的答案(相较于未优化版),但是上述所说的字符串重复问题,优化版本则束手无策。 337 | 338 | 所以,该采用哪个版本,取决于你在现实中遇到的实际问题。 339 | 340 | ## 七:参考文献 341 | 342 | - 严蔚敏. 数据结构(C语言版) 343 | - 阮一峰. [字符串匹配的KMP算法](http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html) 344 | -------------------------------------------------------------------------------- /184-size_t,微尘下的 bug.md: -------------------------------------------------------------------------------- 1 | 这个问题是我在写KMP时遇到的,考虑到并不是每位读者都了解这个算法,这里我简化下问题,请看下面的代码,并猜测其输出什么: 2 | ```c++ 3 | string str = "123456"; 4 | 5 | if (-1 < str.size()) 6 | cout << "win\n"; 7 | else 8 | cout << "lose\n"; 9 | ``` 10 | 你的答案是win,是么?那么很遗憾的告诉你,NO,答案是;lose!!! 11 | 12 | 为何?其实很简单的问题,类型不一致。 13 | 14 | -1默认为`int`,`size()`返回类型为`size_t`即`unsigned int`。 15 | 16 | 17 | 18 | 19 | 20 | ```c++ 21 | size_t x = 1; 22 | int y = -1; 23 | cout << x + y << endl; // 0 24 | cout << typeid(x+y).name() << endl; // unsigned int 25 | ``` 26 | 27 | 两种类型进行操作,int类型的-1会被自动转为`unsigned int`,即: 28 | 29 | ```c++ 30 | 1 (unsigned int) + -1 (int) 31 | 0000...0001(unsigned int) + 1111...1111(int) 32 | = 0000...0001(unsigned int) + 1111...1111(unsigned int) 33 | = 0000...0000(unsigned int) 34 | ``` 35 | 36 | 显而易见,int类型的-1转为unsigned int后,会变成一个非常大的正数。 37 | 38 | 就是因为这个问题,让我纠结了一个多小时,下面贴下代码纪念下: 39 | 40 | ```c++ 41 | int KMP(string S, string P, int next[]) 42 | { 43 | GetNext(P, next); 44 | 45 | int i = 0; 46 | int j = 0; 47 | 48 | while (i < S.size() && j < P.size()) // 除了上面提及的问题,这里的 size() 还有一个问题,你知道么? 49 | { 50 | if (j == -1 || S[i] == P[j]) 51 | { 52 | i++; 53 | j++; 54 | } 55 | else 56 | j = next[j]; 57 | } 58 | 59 | if (j == P.size()) 60 | return i - j; 61 | 62 | return -1; 63 | } 64 | ``` 65 | **所以,以后写代码一定要注意类型转换,因为一旦出现问题,很难发现它的bug。** -------------------------------------------------------------------------------- /186-扩展KMP算法.md: -------------------------------------------------------------------------------- 1 | 前文已经介绍了经典的[KMP算法](https://61mon.com/index.php/archives/183/),本文继续介绍KMP算法的扩展,即扩展KMP算法。 2 | 3 | **问题定义:**给定两个字符串S和T(长度分别为n和m),下标从0开始,定义`extend[i]`等于`S[i]...S[n-1]`与T的最长相同前缀的长度,求出所有的`extend[i]`。举个例子,看下表: 4 | 5 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 6 | | :-------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 7 | | S | a | a | a | a | a | b | b | b | 8 | | T | a | a | a | a | a | c | | | 9 | | extend[i] | 5 | 4 | 3 | 2 | 1 | 0 | 0 | 0 | 10 | 11 | 为什么说这是KMP算法的扩展呢?显然,如果在S的某个位置i有`extend[i]`等于m,则可知在S中找到了匹配串T,并且匹配的首位置是i。而且,扩展KMP算法可以找到S中所有T的匹配。接下来具体介绍下这个算法。 12 | 13 | 14 | 15 | 16 | 17 | ## 一:算法流程 18 | (1) 19 | 20 | ![](https://61mon.com/images/illustrations/ExtendedKMP/1.png) 21 | 22 | 如上图,假设当前遍历到S串位置i,即`extend[0]...extend[i - 1]`这i个位置的值已经计算得到。设置两个变量,a和p。p代表以a为起始位置的字符匹配成功的最右边界,也就是"p = 最后一个匹配成功位置 + 1"。相较于字符串T得出,**S[a...p)等于T[0...p-a)**。 23 | 24 | 再定义一个辅助数组`int next[]`,其中`next[i]`含义为:`T[i]...T[m - 1]`与T的最长相同前缀长度,m为串T的长度。举个例子: 25 | 26 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 27 | | :-----: | :--: | :--: | :--: | :--: | :--: | :--: | 28 | | T | a | a | a | a | a | c | 29 | | next[i] | 6 | 4 | 3 | 2 | 1 | 0 | 30 | 31 | (2) 32 | 33 | ![](https://61mon.com/images/illustrations/ExtendedKMP/2.png) 34 | 35 | `S[i]`对应`T[i - a]`,如果`i + next[i - a] < p`,如上图,三个椭圆长度相同,根据next数组的定义,此时`extend[i] = next[i - a]`。 36 | 37 | (3) 38 | 39 | ![](https://61mon.com/images/illustrations/ExtendedKMP/3.png) 40 | 41 | 如果`i + next[i - a] == p`呢?如上图,三个椭圆都是完全相同的,`S[p] != T[p - a]`且`T[p - i] != T[p - a]`,但`S[p]`有可能等于`T[p - i]`,所以我们可以直接从`S[p]`与`T[p - i]`开始往后匹配,加快了速度。 42 | 43 | (4) 44 | 45 | ![](https://61mon.com/images/illustrations/ExtendedKMP/4.png) 46 | 47 | 如果`i + next[i - a] > p`呢?那说明`S[i...p)`与`T[i-a...p-a)`相同,注意到`S[p] != T[p - a]`且`T[p - i] == T[p - a]`,也就是说`S[p] != T[p - i]`,所以就没有继续往下判断的必要了,我们可以直接将`extend[i]`赋值为`p - i`。 48 | 49 | (5)最后,就是求解next数组。我们再来看下`next[i]`与`extend[i]`的定义: 50 | 51 | - **next[i]**: `T[i]...T[m - 1]`与T的最长相同前缀长度; 52 | - **extend[i]**: `S[i]...S[n - 1]`与T的最长相同前缀长度。 53 | 54 | 恍然大悟,求解`next[i]`的过程不就是T自己和自己的一个匹配过程嘛,下面直接看代码。 55 | 56 | ## 二:代码 57 | ```c++ 58 | /** 59 | * 60 | * author : 刘毅(Limer) 61 | * date : 2017-03-12 62 | * mode : C++ 63 | */ 64 | 65 | #include 66 | #include 67 | 68 | using namespace std; 69 | 70 | /* 求解 T 中 next[],注释参考 GetExtend() */ 71 | void GetNext(string & T, int & m, int next[]) 72 | { 73 | int a = 0, p = 0; 74 | next[0] = m; 75 | 76 | for (int i = 1; i < m; i++) 77 | { 78 | if (i >= p || i + next[i - a] >= p) 79 | { 80 | if (i >= p) 81 | p = i; 82 | 83 | while (p < m && T[p] == T[p - i]) 84 | p++; 85 | 86 | next[i] = p - i; 87 | a = i; 88 | } 89 | else 90 | next[i] = next[i - a]; 91 | } 92 | } 93 | 94 | /* 求解 extend[] */ 95 | void GetExtend(string & S, int & n, string & T, int & m, int extend[], int next[]) 96 | { 97 | int a = 0, p = 0; 98 | GetNext(T, m, next); 99 | 100 | for (int i = 0; i < n; i++) 101 | { 102 | if (i >= p || i + next[i - a] >= p) // i >= p 的作用:举个典型例子,S 和 T 无一字符相同 103 | { 104 | if (i >= p) 105 | p = i; 106 | 107 | while (p < n && p - i < m && S[p] == T[p - i]) 108 | p++; 109 | 110 | extend[i] = p - i; 111 | a = i; 112 | } 113 | else 114 | extend[i] = next[i - a]; 115 | } 116 | } 117 | 118 | int main() 119 | { 120 | int next[100]; 121 | int extend[100]; 122 | string S, T; 123 | int n, m; 124 | 125 | while (cin >> S >> T) 126 | { 127 | n = S.size(); 128 | m = T.size(); 129 | GetExtend(S, n, T, m, extend, next); 130 | 131 | // 打印 next 132 | cout << "next: "; 133 | for (int i = 0; i < m; i++) 134 | cout << next[i] << " "; 135 | 136 | // 打印 extend 137 | cout << "\nextend: "; 138 | for (int i = 0; i < n; i++) 139 | cout << extend[i] << " "; 140 | 141 | cout << endl << endl; 142 | } 143 | return 0; 144 | } 145 | ``` 146 | 147 | 数据测试如下: 148 | 149 | ![](https://61mon.com/images/illustrations/ExtendedKMP/5.png) 150 | 151 | ## 三:时间复杂度 152 | 153 | 对比KMP算法,很容易发现时间复杂度为$Θ(n+m)$。 154 | -------------------------------------------------------------------------------- /187-二分查找(面试必备).md: -------------------------------------------------------------------------------- 1 | 在计算机科学中,二分搜索(binary search),也称折半搜索(half-interval search)、对数搜索(logarithmic search),是一种在**有序数组**中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。 2 | 3 | 4 | 5 | 6 | 7 | ## 问题1 8 | 给定一个有序的数组,查找value是否在数组中,不存在返回-1。 9 | 10 | 例如:{ 1, 2, 3, 4, 5 }找3,返回下标2(下标从0开始计算)。 11 | 12 | ```c++ 13 | /* 注意:题目保证数组不为空,且 n 大于等于 1 ,以下问题默认相同 */ 14 | int BinarySearch(int array[], int n, int value) 15 | { 16 | int left = 0; 17 | int right = n - 1; 18 | // 如果这里是 int right = n 的话,那么下面有两处地方需要修改,以保证一一对应: 19 | // 1、下面循环的条件则是 while(left < right) 20 | // 2、循环内当 array[middle] > value 的时候,right = middle 21 | 22 | while (left <= right) // 循环条件,适时而变 23 | { 24 | int middle = left + ((right - left) >> 1); // 防止溢出,移位也更高效。同时,每次循环都需要更新。 25 | if (array[middle] > value) 26 | right = middle - 1; // right 赋值,适时而变 27 | else if (array[middle] < value) 28 | left = middle + 1; 29 | else 30 | return middle; 31 | // 可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多 32 | // 如果每次循环都判断一下是否相等,将耗费时间 33 | } 34 | 35 | return -1; 36 | } 37 | ``` 38 | ## 问题2 39 | 给定一个有序的数组,查找第一个等于value的下标,找不到返回-1。 40 | 41 | 例如:{ 1, 2, 2, 2, 4 }找2,返回下标1(下标从0开始计算)。 42 | 43 | ```c++ 44 | int BinarySearch(int array[], int n, int value) 45 | { 46 | int left = 0; 47 | int right = n - 1; 48 | 49 | while (left <= right) 50 | { 51 | int middle = left + ((right - left) >> 1); 52 | 53 | if (array[middle] >= value) // 因为是找到最小的等值下标,所以等号放在这里 54 | right = middle - 1; 55 | else 56 | left = middle + 1; 57 | } 58 | 59 | if (left < n && array[left] == value) 60 | return left; 61 | 62 | return -1; 63 | } 64 | ``` 65 | 如果问题改为"查找value最后一个等于value的下标"呢?只需改动两个位置: 66 | 1. `if (array[middle] >= value)`中的等号去掉; 67 | 68 | 2. ```c++ 69 | if (left < n && array[left] == value) 70 | return left; 71 | ``` 72 | 改为 73 | 74 | ```c++ 75 | if (right >= 0 && array[right] == value) 76 | return right; 77 | ``` 78 | 79 | ## 问题3 80 | 给定一个有序的数组,查找第一个大于等于value的下标,都比value小则返回-1。 81 | 82 | 也就是说如果有等于value的返回第一个等于value的下标,如果没有则返回第一个大于value的下标。 83 | 84 | ```c++ 85 | int BinarySearch(int array[], int n, int value) 86 | { 87 | int left = 0; 88 | int right = n - 1; 89 | 90 | while (left <= right) 91 | { 92 | int middle = left + ((right - left) >> 1); 93 | 94 | if (array[middle] >= value) 95 | right = middle - 1; 96 | else 97 | left = middle + 1; 98 | } 99 | 100 | return (left < n) ? left : -1; 101 | } 102 | ``` 103 | 如果问题改为"查找最后一个小于等于value的下标"呢?只需改动两个位置: 104 | 105 | 1. `if (array[middle] >= value)`的等号去掉; 106 | 2. `return (left < n) ? left : -1`改为`return (right >= 0) ? right : -1`。 107 | 108 | ## 问题4 109 | 给定一个轮转后的有序数组(所谓转轮有序数组,比如:{ 2, 3, 4, 5, 1 },{ 5, 1, 2, 3, 4 }),查找value是否在数组中,不存在返回-1。 110 | ```c++ 111 | int BinarySearch(int array[], int n, int value) 112 | { 113 | int left = 0; 114 | int right = n - 1; 115 | 116 | while (left <= right) 117 | { 118 | int middle = left + ((right - left) >> 1); 119 | 120 | if (value < array[middle]) 121 | { 122 | if (array[middle] < array[right]) 123 | right = middle - 1; 124 | else 125 | { 126 | if (value < array[left]) 127 | left = middle + 1; 128 | else 129 | right = middle - 1; 130 | } 131 | } 132 | else if (value > array[middle]) 133 | { 134 | if (array[middle] > array[left]) 135 | left = middle + 1; 136 | else 137 | { 138 | if (value > array[right]) 139 | right = middle - 1; 140 | else 141 | left = middle + 1; 142 | } 143 | } 144 | else 145 | return middle; 146 | } 147 | 148 | return -1; 149 | } 150 | ``` 151 | 理解上面的代码很简单,只需从三个方面考虑: 152 | { 1, 2, 3, 4, 5 }, 153 | { 2, 3, 4, 5, 1 }, 154 | { 5, 1, 2, 3, 4 }。 155 | 156 | ## 总结 157 | 二分算法所操作的区间,是左闭右开,还是左闭右闭,需要在循环体跳出判断中,以及每次修改left,,right区间值这两个地方保持一致,否则就可能出错。 158 | 159 | 另外,我发现一个小技巧(个人观点,正确性未经考证,仅作参考): 160 | 161 | 当我们写好一个二分程序的时候,总希望找到一些数据来考证我们的程序的正确性,但差强人意的是,我们总会遗漏某个测试数据,从而导致程序依旧存在bug。而我想说的是,我的这个小技巧可以把针对二分的成千上万所有的测试数据都压缩成与之等价的屈指可数的几个测试数据。 162 | 163 | 我们知道二分的思想就是每次取一半,想象一下,不管给我们的数组有多长,每次取一半,最终都会被压缩成长度为1的数组,然后在这个长度为1的数组里判断并返回,所以我们可以直接用长度为1的数组来测试程序。以上述的"问题3:给定一个有序的数组,查找第一个大于等于value的下标,都比value小则返回-1"为例。 164 | 165 | - 若找不到。例如数组为{ 0 },value = 1,则left = 1,right = 0,left越界; 166 | 167 | - 若可以找到; 168 | - 找到等于value的。例如数组为{ 0 },value = 0,则left = 0,right = -1,right越界; 169 | - 找到大于value的。例如数组为{ 0 },value = -1,则left = 0,right = -1,right越界; 170 | 171 | 对比程序最后的返回语句`return (left < n) ? left : -1`,代码正确。 172 | 173 | ## 参考文献 174 | 175 | - 维基百科. [二分搜索算法](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2%E7%AE%97%E6%B3%95). 176 | - GitHub. [有序数组的查找](https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/04.01.md). 177 | - [二分查找](http://www.cnblogs.com/luoxn28/p/5767571.html). 178 | -------------------------------------------------------------------------------- /188-01背包问题.md: -------------------------------------------------------------------------------- 1 | ## 一:问题 2 | 3 | 有$N$件物品和一个容量为$V$的背包。第$i$件物品的体积是$C_i$,其价值是$W_i$。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。 4 | 5 | ## 二:基本思路 6 | 7 | 这是最基础的背包问题,特点是:每种物品仅有一件。 8 | 9 | **状态** $F[i,v]$表示前$i$件物品中选择若干件放在容量为$v$的背包中,可以取得的最大价值。 10 | 11 | **转移方程** 12 | 13 | $$ 14 | F[i,v]=\max \{F[i−1,v],F[i−1,v−C_i]+W_i\} 15 | $$ 16 | 17 | 对于第$i$件物品,有放与不放两种选择。若选择不放,$F[i,v]=F[i−1,v]$;若选择放,$v−C_i$确保有足够的空间,随之$F[i,v]=F[i−1,v−C_i]+W_i$。 18 | 19 | 20 | 21 | 22 | 23 | ## 三:代码 24 | ```c++ 25 | /** 26 | * 27 | * author 刘毅(Limer) 28 | * date 2017-03-17 29 | * mode C++ 30 | */ 31 | 32 | #include 33 | #include 34 | 35 | using namespace std; 36 | 37 | int main() 38 | { 39 | const int N = 6; // 物品个数 40 | const int V = 10; // 背包体积 41 | int C[N + 1] = { -1,5,6,5,1,19,7 }; // 第 i 个物品的体积(下标从 1 开始) 42 | int W[N + 1] = { -1,2,3,1,4,6,5 }; // 第 i 个物品的价值 43 | int F[N + 1][V + 1] = { 0 }; // 状态 44 | 45 | for (int i = 1; i <= N; i++) // 对于第 i 个物品 46 | for (int v = 0; v <= V; v++) 47 | { 48 | F[i][v] = F[i - 1][v]; //第 i 个不放 49 | if (v - C[i] >= 0 && F[i][v] < F[i - 1][v - C[i]] + W[i]) // 如果比它大,再放第 i 个 50 | F[i][v] = F[i - 1][v - C[i]] + W[i]; 51 | } 52 | 53 | cout << "最大价值是:" << F[N][V] << endl; // 9 54 | 55 | return 0; 56 | } 57 | ``` 58 | ## 四:空间复杂度优化 59 | 以上方法的时间和空间复杂度均为$O(VN)$,其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到$O(V)$。 60 | 61 | 先考虑上面讲的基本思路如何实现,肯定是有一个主循环`i ← 1 to N`,每次算出来二维数组$F[i,v]$的所有值。那么,如果只用一个数组$F[v]$能不能保证第$i$次循环结束后$F[v]$中表示的就是我们定义的状态$F[i,v]$呢? 62 | 63 | $F[i,v]$是由$F[i−1,v]$和$F[i−1,v−C_i]$两个子问题递推而来,能否保证在推$F[i,v]$时(也即在第$i$次主循环中推$F[v]$时)能够取用$F[i−1,v]$和$F[i−1,v−C_i]$的值呢? 64 | 65 | 事实上,这要求在每次主循环中我们以`v ← V to C[i]`的递减顺序计算$F[v]$,这样才能保证计算$F[v]$时$F[v−C_i]$保存的是状态$F[i−1,v−C_i]$的值。 66 | 67 | 优化后的代码如下: 68 | ```c++ 69 | /** 70 | * 71 | * author 刘毅(Limer) 72 | * date 2017-03-17 73 | * mode C++ 74 | */ 75 | 76 | #include 77 | #include 78 | 79 | using namespace std; 80 | 81 | int main() 82 | { 83 | const int N = 6; // 物品个数 84 | const int V = 10; // 背包体积 85 | int C[N + 1] = { -1,5,6,5,1,19,7 }; // 第 i 个物品的体积(下标从 1 开始) 86 | int W[N + 1] = { -1,2,3,1,4,6,5 }; // 第 i 个物品的价值 87 | int F[V + 1] = { 0 }; // 状态 88 | 89 | for (int i = 1; i <= N; i++) // 对于第 i 个物品 90 | for (int v = V; v >= C[i]; v--) 91 | F[v] = max(F[v], F[v - C[i]] + W[i]); 92 | 93 | cout << "最大价值是:" << F[V] << endl; // 9 94 | 95 | return 0; 96 | } 97 | ``` 98 | ## 五:初始化的细节问题 99 | 100 | 我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。这两种问法的实现方法只是在初始化的时候有所不同。 101 | 102 | 如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1]...F[V]均设为−∞,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0]...F[V]全部设为0。 103 | 104 | 这是为什么呢?可以这样理解:初始化的F数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。 105 | 106 | ## 六:参考文献 107 | 108 | - 背包九讲. 109 | -------------------------------------------------------------------------------- /190-线索二叉树.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 线索二叉树的定义为:**一个二叉树通过如下的方法“穿起来”:所有应该为空的右孩子指针指向该结点在中序序列中的后继,所有应该为空的左孩子指针指向该结点的中序序列的前驱。** 3 | 4 | 那么在有N个结点的二叉树中共有N+1个空指针,这些空指针就叫做“线索”。(提示:在一个有N个结点的二叉树中,每个结点有2个指针,所以一共有2N个指针,将这N个结点连接起来需要N-1条线,即使用了N-1个指针。所以剩下2N-(N-1)=N+1个空指针。) 5 | 6 | 那线索二叉树有何用处呢?由于巧妙地利用了空指针,所以它可以**快速地查找到二叉树中某结点的前驱和后继**。接下来具体介绍这个数据结构。 7 | 8 | 在进行下文之前,约定如下: 9 | 10 | 11 | 12 | 13 | 14 | ```c++ 15 | struct Node 16 | { 17 | bool left_thread; 18 | bool right_thread; 19 | char data; 20 | Node * left; 21 | Node * right; 22 | 23 | Node() 24 | { 25 | left_thread = right_thread = false; 26 | data = 0; 27 | left = right = nullptr; 28 | } 29 | }; 30 | 31 | class ThreadedBinaryTree 32 | { 33 | public: 34 | ThreadedBinaryTree(); 35 | Node * create(); 36 | void threaded(Node * cur, Node *& pre); // 线索化 37 | void formLoop(); // 构环 38 | Node * get_successor(Node * node); // 返回后继结点 39 | Node * get_precursor(Node * node); // 返回前驱结点 40 | void in_order_print(); // 中序遍历 41 | private: 42 | Node * header; // 头结点 43 | Node * root; // 二叉树根结点 44 | }; 45 | ``` 46 | 请注意,在决定left是指向左孩子还是前驱,right是指向右孩子还是后继,我们是需要一个区分标志的。因此我们在Node结构体中增设两个布尔变量left_thread和right_thread,其中: 47 | (1):left\_thread为true时指向前驱,为false时指向该结点的左子树; 48 | (2):right\_thread为true时指向后继,为false时指向该结点的右子树。 49 | ## 二:具体实现与代码分析 50 | ### 2.1 构造二叉树 51 | ```c++ 52 | ThreadedBinaryTree::ThreadedBinaryTree() 53 | { 54 | root = create(); 55 | } 56 | 57 | Node * ThreadedBinaryTree::create() 58 | { 59 | Node * p = nullptr; 60 | char ch; 61 | cin >> ch; 62 | 63 | if (ch == '.') // 结束输入 64 | p = nullptr; 65 | else 66 | { 67 | p = new Node; 68 | p->data = ch; 69 | p->left = create(); 70 | p->right = create(); 71 | } 72 | 73 | return p; 74 | } 75 | ``` 76 | 77 | ### 2.2 线索化及二叉树构环 78 | ```c++ 79 | void ThreadedBinaryTree::threaded(Node * cur, Node *& pre) 80 | { 81 | if (cur == nullptr) 82 | return; 83 | else 84 | { 85 | // 按照中序遍历方向,先处理左子树 86 | threaded(cur->left, pre); 87 | 88 | // 再处理当前结点 89 | if (cur->left == nullptr) 90 | { 91 | cur->left_thread = true; 92 | cur->left = pre; 93 | } 94 | if (cur->right == nullptr) 95 | cur->right_thread = true; 96 | if (pre->right_thread) 97 | pre->right = cur; 98 | pre = cur; 99 | 100 | // 最后处理右子树 101 | threaded(cur->right, pre); 102 | } 103 | } 104 | 105 | void ThreadedBinaryTree::formLoop() 106 | { 107 | header = new Node; // 创建头结点,并完成初始化 108 | header->left_thread = true; 109 | header->right_thread = true; 110 | header->left = header->right = header; 111 | 112 | Node * pre = header; // 记录中序遍历的前一个结点 113 | threaded(root, pre); // 进行线索化 114 | 115 | pre->right_thread = true; // 线索化完后,把中序遍历的最后一个结点即 pre,指向 header 116 | pre->right = header; 117 | header->left = pre; // 注意,header 的左指针指向中序遍历的最后一个 118 | } 119 | ``` 120 | header结点的作用就是把线索化后的二叉树串起来,形成一个环。header的左孩子指向**中序遍历序列的最后一个结点**,右孩子指向**中序遍历序列的第一个结点**,如下图: 121 | 122 | ![](https://61mon.com/images/illustrations/ThreadedBinaryTree/1.png) 123 | 124 | ### 2.3 后继和前驱 125 | ```c++ 126 | Node * ThreadedBinaryTree::get_successor(Node * node) 127 | { 128 | if (node->right_thread) 129 | return node->right; 130 | 131 | Node * p = node->right; 132 | while (p->left_thread == false) // 已线索化,故此处只能用 left_thread 来判断左子树的情况 133 | p = p->left; 134 | 135 | return p; 136 | } 137 | 138 | Node * ThreadedBinaryTree::get_precursor(Node * node) 139 | { 140 | if (node->left_thread) 141 | return node->left; 142 | 143 | Node * p = node->left; 144 | while (p->right_thread == false) // 已线索化,故此处只能用 right_thread 来判断右子树的情况 145 | p = p->right; 146 | 147 | return p; 148 | } 149 | ``` 150 | 151 | ### 2.4 中序遍历 152 | ```c++ 153 | void ThreadedBinaryTree::in_order_print() 154 | { 155 | cout << "中序遍历为:"; 156 | Node * p = header->right; // header 的右结点指向二叉树中序遍历的第一个结点 157 | 158 | while (p != header) 159 | { 160 | cout << p->data << " "; 161 | p = get_successor(p); 162 | } 163 | 164 | cout << endl; 165 | } 166 | ``` 167 | 168 | ## 三:完整代码 169 | ```c++ 170 | /** 171 | * 172 | * author : 刘毅(Limer) 173 | * date : 2017-03-26 174 | * mode : C++ 175 | */ 176 | 177 | #include 178 | 179 | using namespace std; 180 | 181 | struct Node 182 | { 183 | bool left_thread; 184 | bool right_thread; 185 | char data; 186 | Node * left; 187 | Node * right; 188 | 189 | Node() 190 | { 191 | left_thread = right_thread = false; 192 | data = 0; 193 | left = right = nullptr; 194 | } 195 | }; 196 | 197 | class ThreadedBinaryTree 198 | { 199 | public: 200 | ThreadedBinaryTree(); 201 | Node * create(); 202 | void threaded(Node * cur, Node *& pre); // 线索化 203 | void formLoop(); // 构环 204 | Node * get_successor(Node * node); // 返回后继结点 205 | Node * get_precursor(Node * node); // 返回前驱结点 206 | void in_order_print(); // 中序遍历 207 | private: 208 | Node * header; // 头结点 209 | Node * root; // 二叉树根结点 210 | }; 211 | 212 | int main() 213 | { 214 | 215 | ThreadedBinaryTree my_tree; 216 | my_tree.formLoop(); 217 | my_tree.in_order_print(); 218 | 219 | return 0; 220 | } 221 | 222 | ThreadedBinaryTree::ThreadedBinaryTree() 223 | { 224 | root = create(); 225 | } 226 | 227 | Node * ThreadedBinaryTree::create() 228 | { 229 | Node * p = nullptr; 230 | char ch; 231 | cin >> ch; 232 | 233 | if (ch == '.') // 结束输入 234 | p = nullptr; 235 | else 236 | { 237 | p = new Node; 238 | p->data = ch; 239 | p->left = create(); 240 | p->right = create(); 241 | } 242 | 243 | return p; 244 | } 245 | 246 | void ThreadedBinaryTree::threaded(Node * cur, Node *& pre) 247 | { 248 | if (cur == nullptr) 249 | return; 250 | else 251 | { 252 | // 按照中序遍历方向,先处理左子树 253 | threaded(cur->left, pre); 254 | 255 | // 再处理当前结点 256 | if (cur->left == nullptr) 257 | { 258 | cur->left_thread = true; 259 | cur->left = pre; 260 | } 261 | if (cur->right == nullptr) 262 | cur->right_thread = true; 263 | if (pre->right_thread) 264 | pre->right = cur; 265 | pre = cur; 266 | 267 | // 最后处理右子树 268 | threaded(cur->right, pre); 269 | } 270 | } 271 | 272 | void ThreadedBinaryTree::formLoop() 273 | { 274 | header = new Node; // 创建头结点,并完成初始化 275 | header->left_thread = true; 276 | header->right_thread = true; 277 | header->left = header->right = header; 278 | 279 | Node * pre = _header; // 记录中序遍历的前一个结点 280 | threaded(root, pre); // 进行线索化 281 | 282 | pre->right_thread = true; // 线索化完后,把中序遍历的最后一个结点即 pre,指向 header 283 | pre->right = header; 284 | header->left = pre; // 注意,header 的左指针指向中序遍历的最后一个 285 | } 286 | 287 | Node * ThreadedBinaryTree::get_successor(Node * node) 288 | { 289 | if (node->right_thread) 290 | return node->right; 291 | 292 | Node * p = node->right; 293 | while (p->left_thread == false) // 已线索化,故此处只能用 left_thread 来判断左子树的情况 294 | p = p->left; 295 | 296 | return p; 297 | } 298 | 299 | Node * ThreadedBinaryTree::get_precursor(Node * node) 300 | { 301 | if (node->left_thread) 302 | return node->left; 303 | 304 | Node * p = node->left; 305 | while (p->right_thread == false) // 已线索化,故此处只能用 right_thread 来判断右子树的情况 306 | p = p->right; 307 | 308 | return p; 309 | } 310 | 311 | void ThreadedBinaryTree::in_order_print() 312 | { 313 | cout << "中序遍历为:"; 314 | Node * p = header->right; // header 的右结点指向二叉树中序遍历的第一个结点 315 | 316 | while (p != header) 317 | { 318 | cout << p->data << " "; 319 | p = get_successor(p); 320 | } 321 | 322 | cout << endl; 323 | } 324 | ``` 325 | 以(2.2)中的图为例,输入数据及测试结果为: 326 | 327 | ![](https://61mon.com/images/illustrations/ThreadedBinaryTree/2.png) 328 | -------------------------------------------------------------------------------- /192-KMP算法(2):其细微之处.md: -------------------------------------------------------------------------------- 1 | >系列文章目录 2 | > 3 | >[KMP 算法(1):如何理解 KMP](https://61mon.com/index.php/archives/183/) 4 | >KMP算法(2):其细微之处 5 | 6 | 本篇来谈一谈KMP的一些细微之处,直接进入主题。 7 | 8 | 9 | 10 | 11 | 12 | ## 一:起始下标之“争”:0和1 13 | ```c++ 14 | /* P 为模式串,下标从 0 开始 */ 15 | void GetNext(string P, int next[]) 16 | { 17 | int p_len = P.size(); 18 | int i = 0; // P 的下标 19 | int j = -1; // 相同真前后缀的长度 20 | next[0] = -1; 21 | 22 | while (i < p_len) 23 | { 24 | if (j == -1 || P[i] == P[j]) 25 | { 26 | i++; 27 | j++; 28 | next[i] = j; 29 | } 30 | else 31 | j = next[j]; 32 | } 33 | } 34 | 35 | /* 在 S 中找到 P 第一次出现的位置 */ 36 | int KMP(string S, string P, int next[]) 37 | { 38 | GetNext(P, next); 39 | 40 | int i = 0; // S 的下标 41 | int j = 0; // P 的下标 42 | int s_len = S.size(); 43 | int p_len = P.size(); 44 | 45 | while (i < s_len && j < p_len) 46 | { 47 | if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j] 48 | { 49 | i++; 50 | j++; 51 | } 52 | else 53 | j = next[j]; // 当前字符匹配失败,进行跳转 54 | } 55 | 56 | if (j == p_len) // 匹配成功 57 | return i - j; 58 | 59 | return -1; 60 | } 61 | ``` 62 | 上述代码的起始下标都是从0开始的,但每个人对数组起始位置的编码习惯不同,分为两类:0和1。对于上面的代码,起始位置如果改为1的话又是怎样呢? 63 | 64 |
65 | 68 |
69 |
70 | 71 | 但它们的区别并不止如此。我们知道,KMP算法的next[i]表示最长的相同真前后缀,但这对起始位置为1的next[i]却不再适用。 72 | 73 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 74 | | :-------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: | 75 | | 模式串 | A | B | C | D | A | B | D | '\\0' | 76 | | next[ i ] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 77 | 78 | | i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 79 | | :-------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: | 80 | | 模式串 | A | B | C | D | A | B | D | '\\0' | 81 | | next[ i ] | 0 | 1 | 1 | 1 | 1 | 2 | 3 | 1 | 82 | 83 | 上面两个表格表展示的是:相同模式串下不同起始位置的next值对比。 84 | 85 | 相比之下,起始位置为1的next值比起始位置为0的next值多了1。**多1,不是巧合,而是必然。**这很容易证明。 86 | 87 | 在GetNext()中,j从0开始(起始位置为1),在走了相等步后停下依次赋值给next[i],因此相较于起始位置为0的next总是多1。这又引起了我们的思考,多了1后在模式匹配中,next还会正确的实现跳转么?当然会了,next多1,同时模式串的起始位置也多了1,这就好比数学中,从a=b转化为a+1=b+1,形式不同但完全等价。 88 | 89 | ## 二:next[i]里最不起眼处的妙用 90 | 先来看一个问题,在主串S中找到模式串P**所有可以完全匹配**的位置。 91 | 92 | 很简单,典型的KMP模式匹配。 93 | 94 | ![](https://61mon.com/images/illustrations/KMP/17.png) 95 | 96 | 假设起始位置都是从0开始,对于上图,若已找到主串的第一个完全匹配位置即0--4,那么请问接下来模式串如何移动? 97 | 98 | ![](https://61mon.com/images/illustrations/KMP/18.png) 99 | 100 | 不知道各位读者有没有注意过模式串最后末尾处的next值代表什么?(末尾即为字符串的结尾标志:'\\0') 101 | 102 | 它代表**整个模式串**的最长相同真前后缀。 103 | 104 | ![](https://61mon.com/images/illustrations/KMP/19.png) 105 | 106 | 利用这个next值,我们直接可以实现跳转,更快地找到下一个匹配点。 107 | -------------------------------------------------------------------------------- /193-排序(1):直接插入排序,二分查找插入排序,希尔排序.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):直接插入排序,二分查找插入排序,希尔排序 4 | > 排序(2):[快速排序](https://61mon.com/index.php/archives/201/) 5 | > 排序(3):[堆排序](https://61mon.com/index.php/archives/202/) 6 | > 排序(4):[归并排序](https://61mon.com/index.php/archives/203/) 7 | > 排序(5):[基数排序](https://61mon.com/index.php/archives/204/) 8 | > 排序(6):[总结](https://61mon.com/index.php/archives/205/) 9 | 10 | 直接插入排序(Insertion Sort)可以说是排序里最简单的了。为简化问题,我们下面只讨论升序排序。 11 | 12 | ![](https://61mon.com/images/illustrations/Sort/1.gif) 13 | 14 | 15 | 16 | 17 | 18 | ```c++ 19 | void InsertSort(int array[], int left, int right) 20 | { 21 | int temp; 22 | int j; 23 | 24 | for (int i = left + 1; i <= right; i++) 25 | { 26 | temp = array[i]; 27 | j = i - 1; 28 | 29 | while (j >= left && array[j] > temp) 30 | array[j + 1] = array[j--]; 31 | 32 | array[j + 1] = temp; 33 | } 34 | } 35 | ``` 36 | 那么它的算法复杂度如下(参考[维基百科](https://zh.wikipedia.org/wiki/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F#.E7.AE.97.E6.B3.95.E5.A4.8D.E6.9D.82.E5.BA.A6)): 37 | 38 | * 时间复杂度 39 | 40 | * 最好情况,序列是升序排列,在这种情况下,只需进行n-1比较,即$T_{best}(n)=O(n)$; 41 | * 最坏情况,序列是降序排列,那么此时需要进行的比较共有 $\frac 12n(n-1)$次,即$T_{worse}(n)=O(n^2)$; 42 | * 平均情况,为$T_{avg}(n)=O(n^2)$。 43 | 44 | * 空间复杂度 45 | 46 | * 由程序很容易得$S(n)=O(1)$。 47 | 48 | 直接插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如量级小于千,那么直接插入排序还是一个不错的选择,因此在STL的sort算法和stdlib的qsort算法中,都将直接插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。 49 | 50 | 此外直接插入排序有两个常用的优化:二分查找插入排序,希尔排序。下面分别介绍。 51 | 52 | ## 1 二分查找插入排序 53 | 54 | 因为在一个有序序列中查找一个插入位置,所以可使用二分查找,减少元素比较次数提高效率。 55 | 56 | ```c++ 57 | /* 给定一个有序的数组,查找第一个大于等于 value 的下标,不存在返回 -1 */ 58 | int BinarySearch(int array[], int n, int value) 59 | { 60 | int left = 0; 61 | int right = n - 1; 62 | 63 | while (left <= right) 64 | { 65 | int middle = left + ((right - left) >> 1); 66 | 67 | if (array[middle] >= value) 68 | right = middle - 1; 69 | else 70 | left = middle + 1; 71 | } 72 | 73 | return (left < n) ? left : -1; 74 | } 75 | 76 | void BinaryInsertSort(int array[], int left, int right) 77 | { 78 | for (int i = left + 1; i <= right; i++) 79 | { 80 | int insert_index = BinarySearch(array, i, array[i]); 81 | 82 | if (insert_index != -1) // 如果可以插入到前面的有序序列中 83 | { 84 | int temp = array[i]; 85 | int j = i - 1; 86 | 87 | while (j >= insert_index) 88 | { 89 | array[j + 1] = array[j]; 90 | j--; 91 | } 92 | 93 | array[j + 1] = temp; 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | 关于上面的二分查找,可以参考[二分查找-问题3](https://61mon.com/index.php/archives/187/#menu_index_3)。 100 | 101 | 最好情况下,即序列为升序时,时间复杂度为$\sum_{k=1}^{n-1}logk= O(logn)$。 102 | 103 | 其它情况下,除了找到插入点所需的操作数从$O(n)$降为$O(logn)$外,其它的操作并未减小,其时间复杂度依旧是$O(n^2)$。 104 | 105 | ## 2 希尔排序 106 | 107 | 希尔排序,也称递减增量排序算法,以其设计者希尔(Donald Shell)的名字命名,该算法由1959年公布。 108 | 109 | 我们举个例子来描述算法流程(以下摘自[维基百科](https://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F)): 110 | 111 | 假设有这样一组数{ 13, 14, 94, 33, 82, 25, 59, 94, 65, 23, 45, 27, 73, 25, 39, 10 },如果我们以步长为5开始进行排序: 112 | 113 | ``` 114 | 13 14 94 33 82 115 | 25 59 94 65 23 116 | 45 27 73 25 39 117 | 10 118 | ``` 119 | 120 | 然后我们对**每列**进行排序: 121 | 122 | ``` 123 | 10 14 73 25 23 124 | 13 27 94 33 39 125 | 25 59 94 65 82 126 | 45 127 | 128 | ``` 129 | 130 | 将上述四行数字,依序接在一起时我们得到:{ 10, 14, 73, 25, 23, 13, 27, 94, 33, 39, 25, 59, 94, 65, 82, 45 },然后再以3为步长: 131 | 132 | ``` 133 | 10 14 73 134 | 25 23 13 135 | 27 94 33 136 | 39 25 59 137 | 94 65 82 138 | 45 139 | ``` 140 | 141 | 排序之后变为: 142 | 143 | ``` 144 | 10 14 13 145 | 25 23 33 146 | 27 25 59 147 | 39 65 73 148 | 45 94 82 149 | 94 150 | ``` 151 | 152 | 最后以1为步长进行排序(此时就是简单的插入排序了)。 153 | 154 | 可想而知,步长的选择是希尔排序的重要部分。算法最开始以一定的步长进行排序,然后会继续以更小的步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为直接插入排序,这就保证了数据一定会被全部排序。 155 | 156 | Donald Shell最初建议步长选择为$\frac n 2$,并且对步长取半直到步长达到1。虽然这样取可以比$O(n^2)$类的算法(直接插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。 157 | 158 | | 步长序列 | 最坏情况下复杂度 | 159 | | :-------------: | :---------------: | 160 | | $\frac n {2^i}$ | $O(n^2)$ | 161 | | $2^i-1$ | $O(n^{\frac 32})$ | 162 | | $2^i3^i$ | $O(nlog^2n)$ | 163 | 164 | 165 | 已知的最好步长序列是由Sedgewick提出的{ 1, 5, 19, 41, 109, ... },该序列的项来自$9⋅4^i-9⋅2^i+1$和$2^{i+2}⋅(2^{i+2}-3)+1$这两个算式。这项研究也表明**比较**在希尔排序中是最主要的操作,而不是交换。用这样步长序列的希尔排序比插入排序要快,甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。 166 | 167 | 另一个在大数组中表现优异的步长序列是:{ 1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, … }(斐波那契数列除去0和1,将剩余的数以黄金分区比的两倍的幂进行运算得到的数列) 168 | 169 | 以下程序以$\frac n {2^i}$作为步长序列: 170 | 171 | ```c++ 172 | void ShellSort(int array[], int n) 173 | { 174 | for (int gap = n >> 1; gap > 0; gap >>= 1) 175 | { 176 | for (int i = gap; i < n; i++) 177 | { 178 | int temp = array[i]; 179 | int j = i - gap; 180 | 181 | while (j >= 0 && array[j] > temp) 182 | { 183 | array[j + gap] = array[j]; 184 | j -= gap; 185 | } 186 | 187 | array[j + gap] = temp; 188 | } 189 | } 190 | } 191 | ``` 192 | -------------------------------------------------------------------------------- /194-单源最短路径(1):Dijkstra算法.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 单源最短路径(1):Dijkstra算法 4 | > [单源最短路径(2):Bellman_Ford算法](https://61mon.com/index.php/archives/195/) 5 | > [单源最短路径(3):SPFA算法](https://61mon.com/index.php/archives/196/) 6 | > [单源最短路径(4):总结](https://61mon.com/index.php/archives/200/) 7 | 8 | ## 一:背景 9 | Dijkstra算法(中文名:迪杰斯特拉算法)是由荷兰计算机科学家Edsger Wybe Dijkstra提出。该算法常用于路由算法或者作为其他图算法的一个子模块。举例来说,如果图中的顶点表示城市,而边上的权重表示城市间开车行经的距离,该算法可以用来找到两个城市之间的最短路径。 10 | 11 | 12 | 13 | 14 | 15 | ## 二:算法过程 16 | 我们用一个例子来具体说明迪杰斯特拉算法的流程。 17 | 18 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/1.png) 19 | 20 | 定义源点为0,`dist[i]`为源点0到顶点i的最短路径。其过程描述如下: 21 | 22 | | 步骤 | dist[1] | dist[2] | dist[3] | dist[4] | 已找到的集合 | 23 | | :--: | :-----: | :-----: | :-----: | :-----: | :------------: | 24 | | 第1步 | 8 | 1 | 2 | +∞ | { 2 } | 25 | | 第2步 | 8 | × | 2 | 4 | { 2, 3 } | 26 | | 第3步 | 5 | × | × | 4 | { 2, 3, 4 } | 27 | | 第4步 | 5 | × | × | × | { 2, 3, 4, 1 } | 28 | | 第5步 | × | × | × | × | { 2, 3, 4, 1 } | 29 | 30 | 第1步:从源点0开始,找到与其邻接的点:1,2,3,更新`dist[]`数组,因0不与4邻接,故`dist[4]`为正无穷。在`dist[]`中找到最小值,其顶点为2,即此时已找到0到2的最短路。 31 | 32 | 第2步:从2开始,继续更新`dist[]`数组:2与1不邻接,不更新;2与3邻接,因`0→2→3`比`dist[3]`大,故不更新`dist[3]` ;2与4邻接,因`0→2→4`比`dist[4]`小,故更新`dist[4]`为4。在`dist[]`中找到最小值,其顶点为3,即此时又找到0到3的最短路。 33 | 34 | 第3步:从3开始,继续更新`dist[]`数组:3与1邻接,因`0→3→1`比`dist[1]`小,更新`dist[1]`为5;3与4邻接,因`0→3→4`比`dist[4]`大,故不更新。在`dist[]`中找到最小值,其顶点为4,即此时又找到0到4的最短路。 35 | 36 | 第4步:从4开始,继续更新`dist[]`数组:4与1不邻接,不更新。在`dist[]`中找到最小值,其顶点为1,即此时又找到0到1的最短路。 37 | 38 | 第5步:所有点都已找到,停止。 39 | 40 | 对于上述步骤,你可能存在以下的疑问: 41 | 42 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/2.png) 43 | 44 | 若A作为源点,与其邻接的只有B,C,D三点,其`dist[]`最小时顶点为C,即就可以确定`A→C`为A到C的最短路。但是我们存在疑问的是:**是否还存在另一条路径使A到C的距离更小?** 用反证法证明。 45 | 46 | 假设存在如上图的红色虚线路径,使`A→D→C`的距离更小,那么`A→D`作为`A→D→C`的子路径,其距离也比`A→C`小,这与前面所述“`dist[]`最小时顶点为C”矛盾,故假设不成立。因此这个疑问不存在。 47 | 48 | 根据上面的证明,我们可以推断出,**Dijkstra每次循环都可以确定一个顶点的最短路径,故程序需要循环n-1次。** 49 | 50 | ## 三:完整代码 51 | 52 | ```c++ 53 | /** 54 | * 55 | * author : 刘毅(Limer) 56 | * date : 2017-05-17 57 | * mode : C++ 58 | */ 59 | 60 | #include 61 | 62 | using namespace std; 63 | 64 | int matrix[100][100]; // 邻接矩阵 65 | bool visited[100]; // 标记数组 66 | int dist[100]; // 源点到顶点i的最短距离 67 | int path[100]; // 记录最短路的路径 68 | int source; // 源点 69 | int vertex_num; // 顶点数 70 | int edge_num; // 边数 71 | 72 | void Dijkstra(int source) 73 | { 74 | memset(visited, 0, sizeof(visited)); // 初始化标记数组 75 | visited[source] = true; 76 | for (int i = 0; i < vertex_num; i++) 77 | { 78 | dist[i] = matrix[source][i]; 79 | path[i] = source; 80 | } 81 | 82 | int min_cost; // 权值最小 83 | int min_cost_index; // 权值最小的下标 84 | 85 | for (int i = 1; i < vertex_num; i++) // 找到源点到另外 vertex_num-1 个点的最短路径 86 | { 87 | min_cost = INT_MAX; 88 | 89 | for (int j = 0; j < vertex_num; j++) 90 | { 91 | if (visited[j] == false && dist[j] < min_cost) // 找到权值最小 92 | { 93 | min_cost = dist[j]; 94 | min_cost_index = j; 95 | } 96 | } 97 | 98 | visited[min_cost_index] = true; // 该点已找到,进行标记 99 | 100 | for (int j = 0; j < vertex_num; j++) // 更新 dist 数组 101 | { 102 | if (visited[j] == false && 103 | matrix[min_cost_index][j] != INT_MAX && // 确保两点之间有边 104 | matrix[min_cost_index][j] + min_cost < dist[j]) 105 | { 106 | dist[j] = matrix[min_cost_index][j] + min_cost; 107 | path[j] = min_cost_index; 108 | } 109 | } 110 | } 111 | } 112 | 113 | int main() 114 | { 115 | cout << "请输入图的顶点数(<100):"; 116 | cin >> vertex_num; 117 | cout << "请输入图的边数:"; 118 | cin >> edge_num; 119 | 120 | for (int i = 0; i < vertex_num; i++) 121 | for (int j = 0; j < vertex_num; j++) 122 | matrix[i][j] = (i != j) ? INT_MAX : 0; // 初始化 matrix 数组 123 | 124 | cout << "请输入边的信息:\n"; 125 | int u, v, w; 126 | for (int i = 0; i < edge_num; i++) 127 | { 128 | cin >> u >> v >> w; 129 | matrix[u][v] = matrix[v][u] = w; 130 | } 131 | 132 | cout << "请输入源点(<" << vertex_num << "):"; 133 | cin >> source; 134 | Dijkstra(source); 135 | 136 | for (int i = 0; i < vertex_num; i++) 137 | { 138 | if (i != source) 139 | { 140 | cout << source << "到" << i << "最短距离是:" << dist[i] << ",路径是:" << i; 141 | int t = path[i]; 142 | while (t != source) 143 | { 144 | cout << "--" << t; 145 | t = path[t]; 146 | } 147 | cout << "--" << source << endl; 148 | } 149 | } 150 | 151 | return 0; 152 | } 153 | ``` 154 | 155 | 输入数据,结果为: 156 | 157 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/3.png) 158 | 159 | ## 四:时间复杂度 160 | 设图的边数为m,顶点数为n。 161 | 162 | Dijkstra算法最简单的实现方法是用一个数组来存储所有顶点的`dist[]`(即本程序采用的策略),所以搜索`dist[]`中最小元素的运算需要线性搜索$O(n)$。这样的话算法的运行时间是$O(n^2)$。 163 | 164 | 对于边数远少于$n^2$的稀疏图来说,我们可以用邻接表来更有效的实现该算法。同时需要将一个二叉堆或者斐波纳契堆用作优先队列来查找最小的顶点。当用到二叉堆的时候,算法所需的时间为 $O((m+n)logn)$,斐波纳契堆能稍微提高一些性能,让算法运行时间达到$O(m+nlogn)$。然而,使用斐波纳契堆进行编程,常常会由于算法常数过大而导致速度没有显著提高。 165 | 166 | 关于$O((m+n)logn)$的由来,我简单的证明了下(仅个人看法,不保证其正确性): 167 | 168 | * 把`dist[]`数组调整成最小堆,需要$O(n)$的时间; 169 | 170 | * 因为是最小堆,所以每次取出最小值只需$O(1)$的时间,接着把数组尾的数放置堆顶,并花费$O(logn)$的时间重新调整成最小堆; 171 | 172 | * 我们需要n-1次操作才可以找出剩下的n-1个点,在这期间,大约需要访问m次边,每次访问都可能造成`dist[]`的改变,因此还需要$O(logn)$的时间来进行最小堆的重新调整(从当前`dist[]`改变的位置往上调整)。 173 | 174 | 综上所述:总的时间复杂度为:$O(n)+O(nlogn)+O(mlogn)=O((m+n)logn)$ 175 | 176 | 最后简单说下Dijkstra优化时二叉堆的两种实现方式: 177 | 178 | * 优先队列,把每个顶点的序号和其`dist[]`压在一个结构体再放进队列里; 179 | * 自己建一个小顶堆`heap[]`,存储顶点序号,再用一个数组`pos[]`记录第i个顶点在堆中的位置。 180 | 181 | 相比之下,前者的编码难度较低,因此在平时编程甚至算法竞赛中,都是首选。 182 | 183 | ## 五:该算法的缺陷 184 | 185 | Dijkstra算法有个巨大的缺陷,请考虑下面这幅图: 186 | 187 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/4.png) 188 | 189 | `u→v`间存在一条负权回路(所谓的负权回路,维基和百科并未收录其名词,但从网上的一致态度来看,其含义为:如果存在一个环(从某个点出发又回到自己的路径),而且这个环上所有权值之和是负数,那这就是一个负权环,也叫负权回路),那么只要无限次地走这条负权回路,便可以无限制地减少它的最短路径权值,这就变相地说明最短路径不存在。一个不存在最短路径的图,Dijkstra算法无法检测出这个问题,其最后求解的`dist[]`也是错的。 190 | 191 | 那么对于上述的“一个不存在最短路径的图”,我们该用什么方法来解决呢?请接着看本系列第二篇文章。 192 | -------------------------------------------------------------------------------- /195-单源最短路径(2):Bellman_Ford算法.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > [单源最短路径(1):Dijkstra算法](https://61mon.com/index.php/archives/194/) 4 | > 单源最短路径(2):Bellman_Ford算法 5 | > [单源最短路径(3):SPFA算法](https://61mon.com/index.php/archives/196/) 6 | > [单源最短路径(4):总结](https://61mon.com/index.php/archives/200/) 7 | 8 | ## 一:背景 9 | Dijkstra算法是处理单源最短路径的有效算法,但它对存在负权回路的图就会失效。这时候,就需要使用其他的算法来应对这个问题,Bellman-Ford(中文名:贝尔曼-福特)算法就是其中一个。 10 | 11 | Bellman-Ford算法不仅可以求出最短路径,也可以检测负权回路的问题。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。 12 | 13 | 14 | 15 | 16 | 17 | ## 二:算法过程分析 18 | 对于一个不存在负权回路的图,Bellman-Ford算法求解最短路径的方法如下: 19 | 20 | 设其顶点数为n,边数为m。设其源点为source,数组`dist[i]`记录从源点source到顶点i的最短路径,除了`dist[source]`初始化为0外,其它`dist[]`皆初始化为MAX。以下操作循环执行n-1次: 21 | 22 | * 对于每一条边arc(u, v),如果dist[u] + w(u, v) < dist[v],则使dist[v] = dist[u] + w(u, v),其中w(u, v)为边arc(u, v)的权值。 23 | 24 | n-1次循环,Bellman-Ford算法就是利用已经找到的最短路径去更新其它点的`dist[]`。 25 | 26 | 接下来再看看Bellman-Ford算法是如何检测负权回路的? 27 | 28 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/5.png) 29 | 30 | 检测的方法很简单,只需在求解最短路径的n-1次循环基础上,再进行第n次循环: 31 | 32 | * 对于所有边,只要存在一条边arc(u, v)使得dist[u] + w(u, v) < dist[v],则该图存在负权回路,其中w(u, v)为边arc(u, v)的权值。 33 | 34 | | 循环次数 | dist[0] | dist[1] | dist[2] | 35 | | :--: | :-----: | :-----: | :-----: | 36 | | 第1次 | 0 | -5 | -3 | 37 | | 第2次 | -2 | -5 | -3 | 38 | | 第3次 | -2 | -7 | -5 | 39 | 40 | ## 三:完整代码 41 | 42 | ```c++ 43 | /** 44 | * 45 | * author : 刘毅(Limer) 46 | * date : 2017-05-21 47 | * mode : C++ 48 | */ 49 | 50 | #include 51 | #include 52 | 53 | using namespace std; 54 | 55 | #define MAX 10000 // 假设权值最大不超过 10000 56 | 57 | struct Edge 58 | { 59 | int u; 60 | int v; 61 | int w; 62 | }; 63 | 64 | Edge edge[10000]; // 记录所有边 65 | int dist[100]; // 源点到顶点 i 的最短距离 66 | int path[100]; // 记录最短路的路径 67 | int vertex_num; // 顶点数 68 | int edge_num; // 边数 69 | int source; // 源点 70 | 71 | bool BellmanFord() 72 | { 73 | // 初始化 74 | for (int i = 0; i < vertex_num; i++) 75 | dist[i] = (i == source) ? 0 : MAX; 76 | 77 | // n-1 次循环求最短路径 78 | for (int i = 1; i <= vertex_num - 1; i++) 79 | { 80 | for (int j = 0; j < edge_num; j++) 81 | { 82 | if (dist[edge[j].v] > dist[edge[j].u] + edge[j].w) 83 | { 84 | dist[edge[j].v] = dist[edge[j].u] + edge[j].w; 85 | path[edge[j].v] = edge[j].u; 86 | } 87 | } 88 | } 89 | 90 | bool flag = true; // 标记是否有负权回路 91 | 92 | // 第 n 次循环判断负权回路 93 | for (int i = 0; i < edge_num; i++) 94 | { 95 | if (dist[edge[i].v] > dist[edge[i].u] + edge[i].w) 96 | { 97 | flag = false; 98 | break; 99 | } 100 | } 101 | 102 | return flag; 103 | } 104 | 105 | void Print() 106 | { 107 | for (int i = 0; i < vertex_num; i++) 108 | { 109 | if (i != source) 110 | { 111 | int p = i; 112 | stack s; 113 | cout << "顶点 " << source << " 到顶点 " << p << " 的最短路径是: "; 114 | 115 | while (source != p) // 路径顺序是逆向的,所以先保存到栈 116 | { 117 | s.push(p); 118 | p = path[p]; 119 | } 120 | 121 | cout << source; 122 | while (!s.empty()) // 依次从栈中取出的才是正序路径 123 | { 124 | cout << "--" << s.top(); 125 | s.pop(); 126 | } 127 | cout << " 最短路径长度是:" << dist[i] << endl; 128 | } 129 | 130 | } 131 | } 132 | 133 | int main() 134 | { 135 | 136 | cout << "请输入图的顶点数,边数,源点:"; 137 | cin >> vertex_num >> edge_num >> source; 138 | 139 | cout << "请输入" << edge_num << "条边的信息:\n"; 140 | for (int i = 0; i < edge_num; i++) 141 | cin >> edge[i].u >> edge[i].v >> edge[i].w; 142 | 143 | if (BellmanFord()) 144 | Print(); 145 | else 146 | cout << "Sorry,it have negative circle!\n"; 147 | 148 | return 0; 149 | } 150 | ``` 151 | 152 | 运行截图: 153 | 154 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/6.jpg) 155 | 156 | 157 | ## 四:算法优化 158 | 159 | 以下除非特殊说明,否则都默认是不存在负权回路的。 160 | 161 | 先来看看Bellman-Ford算法为何需要循环n-1次来求解最短路径? 162 | 163 | 读者可以从Dijkstra算法来考虑,想一下,Dijkstra从源点开始,更新`dist[]`,找到最小值,再更新`dist[]` ,,,每次循环都可以确定一个点的最短路。Bellman-Ford算法同样也是这样,它的每次循环也可以确定一个点的最短路,只不过代价很大,因为Bellman-Ford每次循环都是操作所有边。 164 | 165 | 既然代价这么大,相比Dijkstra算法,Bellman-Ford算法还有啥用?因为后者可以检测负权回路啊。 166 | 167 | Bellman-Ford算法的时间复杂度为$O(nm)$,其中n为顶点数,m为边数。 168 | 169 | $O(nm)$的时间,大多数都浪费了。考虑一个随机图(点和边随机生成),除了已确定最短路的顶点与尚未确定最短路的顶点之间的边,其它的边所做的都是无用的,大致描述为下图(分割线以左为已确定最短路的顶点): 170 | 171 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/7.png) 172 | 173 | 其中红色部分为所做无用的边,蓝色部分为实际有用的边。 174 | 175 | 既然只需用到中间蓝色部分的边,那算法优化的方向就找到了,请接着看本系列第三篇文章:spfa算法。 176 | -------------------------------------------------------------------------------- /196-单源最短路径(3):SPFA算法.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > [单源最短路径(1):Dijkstra算法](https://61mon.com/index.php/archives/194/) 4 | > [单源最短路径(2):Bellman_Ford算法](https://61mon.com/index.php/archives/195/) 5 | > 单源最短路径(3):SPFA算法 6 | > [单源最短路径(4):总结](https://61mon.com/index.php/archives/200/) 7 | 8 | ## 一:背景 9 | SPFA(Shortest Path Faster Algorithm)算法,是西南交通大学段凡丁于1994年发表的,其在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## 二:算法过程 18 | 设立一个队列用来保存待优化的顶点,优化时每次取出队首顶点u,并且用u点当前的最短路径估计值`dist[u]`对与u点邻接的顶点v进行松弛操作,如果v点的最短路径估计值`dist[v]`可以更小,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出顶点来进行松弛操作,直至队列空为止。(所谓的松弛操作,简单来说,对于顶点i,把`dist[i]`调整更小或更大。更多解释请参考百科:[松弛操作](http://baike.baidu.com/item/%E6%9D%BE%E5%BC%9B%E6%93%8D%E4%BD%9C)) 19 | 20 | 而其检测负权回路的方法也很简单,**如果某个点进入队列的次数大于等于n,则存在负权回路,其中n为图的顶点数。** 21 | 22 | ## 三:代码 23 | 24 | ```c++ 25 | /** 26 | * 27 | * author : 刘毅(Limer) 28 | * date : 2017-05-28 29 | * mode : C++ 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | 36 | using namespace std; 37 | 38 | int matrix[100][100]; // 邻接矩阵 39 | bool visited[100]; // 标记数组 40 | int dist[100]; // 源点到顶点 i 的最短距离 41 | int path[100]; // 记录最短路的路径 42 | int enqueue_num[100]; // 记录入队次数 43 | int vertex_num; // 顶点数 44 | int edge_num; // 边数 45 | int source; // 源点 46 | 47 | bool SPFA() 48 | { 49 | memset(visited, 0, sizeof(visited)); 50 | memset(enqueue_num, 0, sizeof(enqueue_num)); 51 | for (int i = 0; i < vertex_num; i++) 52 | { 53 | dist[i] = INT_MAX; 54 | path[i] = source; 55 | } 56 | 57 | queue Q; 58 | Q.push(source); 59 | dist[source] = 0; 60 | visited[source] = 1; 61 | enqueue_num[source]++; 62 | 63 | while (!Q.empty()) 64 | { 65 | int u = Q.front(); 66 | Q.pop(); 67 | visited[u] = 0; 68 | 69 | for (int v = 0; v < vertex_num; v++) 70 | { 71 | if (matrix[u][v] != INT_MAX) // u 与 v 直接邻接 72 | { 73 | if (dist[u] + matrix[u][v] < dist[v]) 74 | { 75 | dist[v] = dist[u] + matrix[u][v]; 76 | path[v] = u; 77 | 78 | if (!visited[v]) 79 | { 80 | Q.push(v); 81 | enqueue_num[v]++; 82 | if (enqueue_num[v] >= vertex_num) 83 | return false; 84 | visited[v] = 1; 85 | } 86 | } 87 | } 88 | } 89 | } // while (!Q.empty()) 90 | 91 | return true; 92 | } 93 | 94 | void Print() 95 | { 96 | for (int i = 0; i < vertex_num; i++) 97 | { 98 | if (i != source) 99 | { 100 | int p = i; 101 | stack s; 102 | cout << "顶点 " << source << " 到顶点 " << p << " 的最短路径是: "; 103 | 104 | while (source != p) // 路径顺序是逆向的,所以先保存到栈 105 | { 106 | s.push(p); 107 | p = path[p]; 108 | } 109 | 110 | cout << source; 111 | while (!s.empty()) // 依次从栈中取出的才是正序路径 112 | { 113 | cout << "--" << s.top(); 114 | s.pop(); 115 | } 116 | cout << " 最短路径长度是:" << dist[i] << endl; 117 | } 118 | } 119 | } 120 | 121 | int main() 122 | { 123 | 124 | cout << "请输入图的顶点数,边数,源点:"; 125 | cin >> vertex_num >> edge_num >> source; 126 | 127 | for (int i = 0; i < vertex_num; i++) 128 | for (int j = 0; j < vertex_num; j++) 129 | matrix[i][j] = (i != j) ? INT_MAX : 0; // 初始化 matrix 数组 130 | 131 | cout << "请输入" << edge_num << "条边的信息:\n"; 132 | int u, v, w; 133 | for (int i = 0; i < edge_num; i++) 134 | { 135 | cin >> u >> v >> w; 136 | matrix[u][v] = w; 137 | } 138 | 139 | if (SPFA()) 140 | Print(); 141 | else 142 | cout << "Sorry,it have negative circle!\n"; 143 | 144 | return 0; 145 | } 146 | ``` 147 | 148 | 运行如下: 149 | 150 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/6.jpg) 151 | 152 | ## 四:判断负权回路的证明 153 | 154 | **如果某个点进入队列的次数大于等于n,则存在负权回路。**为什么偏偏是n? 155 | 156 | 对于一个不存在负权回路的图,设其顶点数为n,我们把图稍微“转换”下,如下图A: 157 | 158 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/8.png) 159 | 160 | * 与源点0邻接的点{ 1, 2, 3 }作为第一批次; 161 | * 与第一批次邻接的点{ 4, 5, 6, 7, 8, 9 }作为第二批次; 162 | * ...... 163 | * 与第k-1批次邻接的点{ ...... }作为第k批次。 164 | 165 | 其中k≤n-1,当k=n-1时,即为上图B。 166 | 167 | **每操作完一个批次的点,至少有一个点的最短路径被确定。 **这里读者只需从Dijkstra算法方面来考虑即可。Dijkstra每次循环都找出`dist[]`里的最小值,可以对应到这里的每个批次。 168 | 169 | **一个不存在负权回路的图,最多有n-1个批次,每做完一个批次至少有一个点的最短路径被确定,即一个点的入队次数不超过n-1。**因为若一个顶点要入队列,则必存在一条权值之和更小的路径,而在最多做完n-1个批次后,所有顶点的最短路径都被确定。(这里需要注意的是,如果一个批次中,有多条路径对某顶点进行更新,则该顶点只会被入队一次,这从代码就可以看出) 170 | 171 | 一个点入队n-1次是什么样的呢?见下图: 172 | 173 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/9.png) 174 | 175 | n个顶点,源点为0,其中: 176 | 177 | * 0到1,1到2,2到3,,,n-3到n-2的权值都为1; 178 | * 0到i的权值为1; 179 | * 1到i,2到i,3到i,,,n-2到i的权值从-1开始依次变小。 180 | 181 | 顶点i依次从0,1,2,3,,,n-2更新,共计n-1次,即入队n-1次。 182 | 183 | ## 五:时间复杂度 184 | 185 | 对于一个不存在负权回路的图,我们假设其顶点数为n,边数为m。 186 | 187 | 引自[SPFA](https://wenku.baidu.com/view/1d0afac05fbfc77da269b1ee.html)论文:考虑一个随机图,运用均摊分析的思想,每个点的平均出度为$O(\frac m n)$,而每个点的平均入队次数为2,因此时间复杂度为$O(n⋅\frac m n⋅2)=O(2m)=O(m)$。 188 | 189 | 关于上述的“平均入队次数为2”,2这个数字从何得来,我也找不到证明,从网上各位朋友对此的一致态度:尚待商榷。但是可以确定的是,SPFA算法在随机图中的平均性能是优于Bellman_Ford算法的。 190 | 191 | 接着再看下SPFA的最差时间复杂度,它发生在一个完全图中,如下图:(参考自 [xiazdong](http://blog.csdn.net/xiazdong/article/details/8193680)) 192 | 193 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/10.png) 194 | 195 | 为了突出重点,故其余边未画出。我们约定: 196 | 197 | * 从0分别到2,3,,,,n-2,n-1的边(即上半部分的曲线),满足0先更新k,待0更新完k-1后,k-1还可以更新k。 198 | 199 | 那么我们得出: 200 | 0点,入队1次,出度n-1次; 201 | 1点,入队1次,出度n-1次; 202 | 2点,入队2次,出度n-1次; 203 | 3点,入队3次,出度n-1次; 204 | . 205 | . 206 | . 207 | n-2点,入队n-2次,出度n-1次; 208 | n-1点,入队n-1次,出度n-1次; 209 | 210 | 因此时间复杂度为:$(n-1)⋅(1+1+2+3+...+(n-2)+(n-1))$,整理为$O(n^3)$,由于是完全图,也可以表达成$O(nm)$。 211 | 212 | ## 六:结后语 213 | 214 | 关于单源最短路径的算法:Dijkstra,Bellman_Ford,SPFA,讲到这里就全部结束了。但关于它们的适用点,彼此的联系以及由网上散出的一些错误概念,我觉得还需要进一步讲下,请看本系列的第四篇。 215 | -------------------------------------------------------------------------------- /197-全排列问题(1).md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 全排列算法(1) 4 | > [全排列算法(2)](https://61mon.com/index.php/archives/198/) 5 | 6 | 给定{ 1, 2, 3, , , n },其全排列为$n!$个,这是最基础的高中组合数学知识。我们以n=4为例,其全部排列如下图(以字典序树形式来呈现): 7 | 8 | ![](https://61mon.com/images/illustrations/FullPermutation/1.jpg) 9 | 10 | 11 | 12 | 13 | 14 | 我们很容易想到用递归来求出它的所有全排列。 15 | 16 | 仔细观察上图, 17 | 18 | * 以1开头,下面跟着{ 2, 3, 4 }的全排列; 19 | * 以2开头,下面跟着{ 1, 3, 4 }的全排列; 20 | * 以3开头,下面跟着{ 1, 2, 4 }的全排列; 21 | * 以4开头,下面跟着{ 1, 2, 3 }的全排列。 22 | 23 | 代码如下: 24 | 25 | ```c++ 26 | /** 27 | * 28 | * author : 刘毅(Limer) 29 | * date : 2017-05-31 30 | * mode : C++ 31 | */ 32 | 33 | #include 34 | #include 35 | 36 | using namespace std; 37 | 38 | void FullPermutation(int array[], int left, int right) 39 | { 40 | if (left == right) 41 | { 42 | for (int i = 0; i < 4; i++) 43 | cout << array[i] << " "; 44 | cout << endl; 45 | } 46 | else 47 | { 48 | for (int i = left; i <= right; i++) 49 | { 50 | swap(array[i], array[left]); 51 | FullPermutation(array, left + 1, right); 52 | swap(array[i], array[left]); 53 | } 54 | } 55 | } 56 | 57 | int main() 58 | { 59 | 60 | int array[4] = { 1,2,3,4 }; 61 | 62 | FullPermutation(array, 0, 3); 63 | 64 | return 0; 65 | } 66 | ``` 67 | 68 | 运行如下: 69 | 70 | ![](https://61mon.com/images/illustrations/FullPermutation/2.png) 71 | 72 | 咦~递归写出的全排列有点不完美,它并不严格遵循字典序。但是熟悉C++的朋友肯定知道另一种更简单,更完美的全排列方法。 73 | 74 | 定义于文件< algorithm >内的两个算法函数: 75 | 76 | * next_permutation,对于当前的排列,如果在字典序中还存在下一个排列,返回真,并且把当前排列调整为下一个排列;如果不存在,就把当前排列调整为字典序中的第一个排列(即递增排列),返回假。 77 | * prev_permutation,对于当前的排列,如果在字典序中还存在上一个排列,返回真,并且把当前排列调整为上一个排列;如果不存在,就把当前排列调整为字典序中的最后一个排列(即递减排列),返回假。 78 | 79 | ```c++ 80 | /** 81 | * 82 | * author : 刘毅(Limer) 83 | * date : 2017-05-31 84 | * mode : C++ 85 | */ 86 | 87 | #include 88 | #include 89 | 90 | using namespace std; 91 | 92 | void FullPermutation(int array[]) 93 | { 94 | do 95 | { 96 | for (int i = 0; i < 4; i++) 97 | cout << array[i] << " "; 98 | cout << endl; 99 | } while (next_permutation(array, array + 4)); 100 | } 101 | 102 | int main() 103 | { 104 | 105 | int array[4] = { 1,2,3,4 }; 106 | 107 | FullPermutation(array); 108 | 109 | return 0; 110 | } 111 | ``` 112 | 113 | 运行截图省略。输出结果正好符合字典序。 114 | 115 | 那这个“轮子”是怎么做的呢?(摘自侯捷的《STL源码剖析》) 116 | 117 | * next_permutation,首先,从最尾端开始往前寻找两个相邻元素,令第一元素为`*i`,第二元素为`*ii`,且满足`*i < *ii`,找到这样一组相邻元素后,再从最尾端开始往前检验,找出第一个大于`*i`的元素,令为`*j`,将i,j元素对调,再将ii之后的所有元素颠倒排列,此即所求之“下一个”排列组合。 118 | * prev_permutation,首先,从最尾端开始往前寻找两个相邻元素,令第一元素为`*i`,第二元素为`*ii`,且满足`*i > *ii`,找到这样一组相邻元素后,再从最尾端开始往前检验,找出第一个小于`*i`的元素,令为`*j`,将i,j元素对调,再将ii之后的所有元素颠倒排列,此即所求之“上一个”排列组合。 119 | 120 | 代码如下: 121 | 122 | ```c++ 123 | bool next_permutation(int * first, int * last) 124 | { 125 | if (first == last) return false; // 空区间 126 | int * i = first; 127 | ++i; 128 | if (i == last) return false; // 只有一个元素 129 | i = last; 130 | --i; 131 | 132 | for (;;) 133 | { 134 | int * ii = i; 135 | --i; 136 | if (*i < *ii) 137 | { 138 | int * j = last; 139 | while (!(*i < *--j)) // 由尾端往前找,直到遇上比 *i 大的元素 140 | ; 141 | swap(*i, *j); 142 | reverse(ii, last); 143 | return true; 144 | } 145 | } 146 | 147 | if (i == first) // 当前排列为字典序的最后一个排列 148 | { 149 | reverse(first, last); // 全部逆向排列,即为升序 150 | return false; 151 | } 152 | } 153 | 154 | bool prev_premutation(int * first, int * last) 155 | { 156 | if (first == last) return false; // 空区间 157 | int * i = first; 158 | ++i; 159 | if (i == last) return false; // 只有一个元素 160 | i = last; 161 | --i; 162 | 163 | for (;;) 164 | { 165 | int * ii = i; 166 | --i; 167 | if (*i > *ii) 168 | { 169 | int * j = last; 170 | while (!(*i > *--j)) // 由尾端往前找,直到遇上比 *i 大的元素 171 | ; 172 | swap(*i, *j); 173 | reverse(ii, last); 174 | return true; 175 | } 176 | } 177 | 178 | if (i == first) // 当前排列为字典序的第一个排列 179 | { 180 | reverse(first, last); // 全部逆向排列,即为降序 181 | return false; 182 | } 183 | } 184 | ``` 185 | 186 | ## 结后语 187 | 188 | 这篇文章主要介绍了解决不重复序列的全排列问题的两个方法:递归和字典序法。 189 | 190 | 那如果是重复序列的全排列呢?该如何求解? 191 | 192 | 请看本系列的第二篇文章。 193 | -------------------------------------------------------------------------------- /198-全排列问题(2).md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > [全排列算法(1)](https://61mon.com/index.php/archives/197/) 4 | > 全排列算法(2) 5 | 6 | 对于一个重复序列{ 1, 2, 2 },其全排列只有三个:{ 1, 2, 2 },{ 2, 1, 2 },{ 2, 2, 1 }。 7 | 8 | ## 递归 9 | 10 | 我们依旧先用递归来求解。 11 | 12 | 13 | 14 | 15 | 16 | ```c++ 17 | /** 18 | * 19 | * author : 刘毅(Limer) 20 | * date : 2017-06-02 21 | * mode : C++ 22 | */ 23 | 24 | #include 25 | #include 26 | 27 | using namespace std; 28 | 29 | bool IsEqual(int array[], int left, int right) 30 | { 31 | for (int i = left; i < right; i++) 32 | if (array[i] == array[right]) 33 | return true; 34 | 35 | return false; 36 | } 37 | 38 | void FullPermutation(int array[], int left, int right) 39 | { 40 | if (left == right) 41 | { 42 | for (int i = 0; i < 3; i++) 43 | cout << array[i] << " "; 44 | cout << endl; 45 | } 46 | else 47 | { 48 | for (int i = left; i <= right; i++) 49 | { 50 | if (!IsEqual(array, left, i)) 51 | { 52 | swap(array[i], array[left]); 53 | FullPermutation(array, left + 1, right); 54 | swap(array[i], array[left]); 55 | } 56 | } 57 | } 58 | } 59 | 60 | int main() 61 | { 62 | 63 | int array[4] = { 1,2,2 }; 64 | 65 | FullPermutation(array, 0, 2); 66 | 67 | return 0; 68 | } 69 | ``` 70 | 71 | 运行截图: 72 | 73 | ![](https://61mon.com/images/illustrations/FullPermutation/3.PNG) 74 | 75 | 简单说下`IsEqual()`为什么那么写。 76 | 77 | 考虑重复序列1abc2xyz2, 78 | 79 | * 交换1与第一个2,变成了2abc1xyz2,按照程序,接下来对**abc1xyz2**进行全排列; 80 | * 假若1与第二个2交换,变成了2abc2xyz1,按照程序,接下来对**abc2xyz1**进行全排列。 81 | 82 | 那么问题来了,注意我加粗的两个地方,这两个全排列进行的都是同样的工作,必然会造成重复输出。 83 | 84 | ## next_permutation 85 | 86 | 下面再来看下STL里的next_permutation和prev_permutation对重复序列的反应。 87 | 88 | ```c++ 89 | /** 90 | * 91 | * author : 刘毅(Limer) 92 | * date : 2017-06-02 93 | * mode : C++ 94 | */ 95 | 96 | #include 97 | #include 98 | 99 | using namespace std; 100 | 101 | void FullPermutation(int array[]) 102 | { 103 | do 104 | { 105 | for (int i = 0; i < 3; i++) 106 | cout << array[i] << " "; 107 | cout << endl; 108 | } while (next_permutation(array, array + 3)); 109 | } 110 | 111 | int main() 112 | { 113 | 114 | int array[3] = { 1,2,2 }; 115 | 116 | FullPermutation(array); 117 | 118 | return 0; 119 | } 120 | ``` 121 | 122 | 运行截图: 123 | 124 | ![](https://61mon.com/images/illustrations/FullPermutation/3.PNG) 125 | 126 | 从结果来看,next_permutation的适应性更强,不管是不重复序列还是重复序列,它都可以输出正确的结果。其实这很好理解,next_permutation的本质是字典序原理,而字典序是严格的大于或者小于,没有等于。 127 | 128 | ## 全排列拓展 129 | 130 | 最后再引申一个网友提出的全排列问题:对于序列{ 1, 2, 3, 4 },输出它所有长度的全排列,即: 131 | 1 132 | 2 133 | 3 134 | 4 135 | 1 2 136 | 1 3 137 | . . . 138 | 1 2 3 4 139 | 1 2 4 3 140 | . . . 141 | 4 3 2 1 142 | 143 | 这样的问题看起来有点复杂,其实很简单,再来回顾下{ 1, 2, 3, 4 }的字典序树。 144 | 145 | ![](https://61mon.com/images/illustrations/FullPermutation/1.jpg) 146 | 147 | 我们只需控制递归的深度即可。 148 | 149 | ```c++ 150 | /** 151 | * 152 | * author : 刘毅(Limer) 153 | * date : 2017-06-02 154 | * mode : C++ 155 | */ 156 | 157 | #include 158 | #include 159 | 160 | using namespace std; 161 | 162 | void FullPermutation(int array[], int left, int right, int len, int depth) 163 | { 164 | if (depth == 0) 165 | { 166 | for (int i = 0; i < len; i++) 167 | cout << array[i] << " "; 168 | cout << endl; 169 | } 170 | else 171 | { 172 | for (int i = left; i <= right; i++) 173 | { 174 | swap(array[i], array[left]); 175 | FullPermutation(array, left + 1, right, len, depth - 1); 176 | swap(array[i], array[left]); 177 | } 178 | } 179 | } 180 | 181 | int main() 182 | { 183 | 184 | int array[4] = { 1,2,3,4 }; 185 | 186 | for (int i = 1; i <= 4; i++) 187 | FullPermutation(array, 0, 3, i, i); 188 | 189 | return 0; 190 | } 191 | ``` 192 | -------------------------------------------------------------------------------- /199-Prim算法(1).md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | 假设要在n个城市之间通信联络网,则连通n个城市只需要n-1条线路。这时,自然会考虑这样一个问题,如何在最节省经费的前提下建立这个通信网。 4 | 5 | 上面的这个问题,就是最小生成树的问题。这篇文章就来介绍下解决这一问题的Prim算法(普里姆算法),该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克发现,并在1957年由美国计算机科学家罗伯特·普里姆独立发现,1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。 6 | 7 | 8 | 9 | 10 | 11 | ## 二:算法过程 12 | 13 | 我们用一个例子来具体说明普里姆算法的流程。 14 | 15 | ![](https://61mon.com/images/illustrations/Prim/1.png) 16 | 17 | 随机选一起点,假如为0,`low_cost[i]`表示以i为终点的边的权值。其过程描述如下: 18 | 19 | | 步骤 | low_cost[1] | low_cost[2] | low_cost[3] | low_cost[4] | 已找到的集合 | 20 | | :--: | :---------: | :---------: | :---------: | :---------: | :------------: | 21 | | 第1步 | 8 | 3 | 2 | +∞ | { 3 } | 22 | | 第2步 | 3 | 3 | × | 5 | { 3, 1 } | 23 | | 第3步 | × | 3 | × | 5 | { 3, 1, 2 } | 24 | | 第4步 | × | × | × | 1 | { 3, 1, 2, 4 } | 25 | | 第5步 | × | × | × | × | { 3, 1, 2, 4 } | 26 | 27 | 第1步:从起点0开始,找到与其邻接的点:1,2,3,更新`low_cost[]`数组,因0不与4邻接,故`low_cost[4]`为正无穷。在`low_cost[]`中找到最小值,其顶点为3,即此时已找到最小生成树中的一条边:`0→3`。 28 | 29 | 第2步:从3开始,继续更新`low_cost[]`数组:3与1邻接,因`3→1`比`low_cost[1]`小,故更新`low_cost[1]`为3;3与2邻接,因`3→2`比`low_cost[2]`大,故不更新`low_cost[2]` ;3与4邻接,因`3→4`比`low_cost[4]`小,故更新`low_cost[4]`为5。在`low_cost[]`中找到最小值,其顶点为1或者2,随便取一个即可,我们这里取1。即此时又找到一条边:`3→1` 。 30 | 31 | 第3步:从1开始,继续更新`low_cost[]`数组:因与1邻接的点都被放入最小生成树中,故不更新,直接在`low_cost[]`中找到最小值,其顶点为2,即此时又找到一条边:`0→2`。 32 | 33 | 第4步:从2开始,继续更新`low_cost[]`数组:2与4邻接,因`2→4`比`low_cost[4]`小,故更新`low_cost[4]`为1。在`low_cost[]`中找到最小值,其顶点为4,即此时又找到一条边:`2→4`。 34 | 35 | 第5步:最小生成树完成,停止。 36 | 37 | ## 三:代码 38 | 39 | ```c++ 40 | /** 41 | * 42 | * author : 刘毅(Limer) 43 | * date : 2017-05-29 44 | * mode : C++ 45 | */ 46 | 47 | #include 48 | 49 | using namespace std; 50 | 51 | int matrix[100][100]; // 邻接矩阵 52 | bool visited[100]; // 标记数组 53 | int low_cost[100]; // 边的权值 54 | int path[100]; // 记录生成树的路径 55 | int source; // 指定生成树的起点 56 | int vertex_num; // 顶点数 57 | int edge_num; // 边数 58 | int sum; // 生成树权和 59 | 60 | void Prim(int source) 61 | { 62 | memset(visited, 0, sizeof(visited)); 63 | visited[source] = true; 64 | for (int i = 0; i < vertex_num; i++) 65 | { 66 | low_cost[i] = matrix[source][i]; 67 | path[i] = source; 68 | } 69 | 70 | int min_cost; // 权值最小 71 | int min_cost_index; // 权值最小的下标 72 | sum = 0; 73 | for (int i = 1; i < vertex_num; i++) // 除去起点,还需要找到另外 vertex_num-1 个点 74 | { 75 | min_cost = INT_MAX; 76 | for (int j = 0; j < vertex_num; j++) 77 | { 78 | if (visited[j] == false && low_cost[j] < min_cost) // 找到权值最小 79 | { 80 | min_cost = low_cost[j]; 81 | min_cost_index = j; 82 | } 83 | } 84 | 85 | visited[min_cost_index] = true; // 该点已找到,进行标记 86 | sum += low_cost[min_cost_index]; // 更新生成树权和 87 | 88 | for (int j = 0; j < vertex_num; j++) // 从找到的最小下标更新 low_cost 数组 89 | { 90 | if (visited[j] == false && matrix[min_cost_index][j] < low_cost[j]) 91 | { 92 | low_cost[j] = matrix[min_cost_index][j]; 93 | path[j] = min_cost_index; 94 | } 95 | } 96 | } 97 | } 98 | 99 | int main() 100 | { 101 | cout << "请输入图的顶点数(<=100):"; 102 | cin >> vertex_num; 103 | cout << "请输入图的边数:"; 104 | cin >> edge_num; 105 | 106 | for (int i = 0; i < vertex_num; i++) 107 | for (int j = 0; j < vertex_num; j++) 108 | matrix[i][j] = INT_MAX; // 初始化 matrix 数组 109 | 110 | cout << "请输入边的信息:\n"; 111 | int u, v, w; 112 | for (int i = 0; i < edge_num; i++) 113 | { 114 | cin >> u >> v >> w; 115 | matrix[u][v] = matrix[v][u] = w; 116 | } 117 | 118 | cout << "请输入起点(<" << vertex_num << "):"; 119 | cin >> source; 120 | Prim(source); 121 | 122 | cout << "最小生成树权和为:" << sum << endl; 123 | cout << "最小生成树路径为:\n"; 124 | for (int i = 0; i < vertex_num; i++) 125 | if (i != source) 126 | cout << i << "----" << path[i] << endl; 127 | 128 | return 0; 129 | } 130 | ``` 131 | 132 | 运行截图: 133 | 134 | ![](https://61mon.com/images/illustrations/Prim/2.png) 135 | 136 | ## 四:算法的正确性证明 137 | 138 | 以下除非特别说明,否则都默认是连通图,即是存在最小生成树的。 139 | 140 | Prim算法利用了最小生成树(Minimu Spanning Tree,简称MST)性质,描述为: 141 | 142 | 假设$N=(V,\{E\})$是一个连通图($V$为顶点集合,$\{E\}$为边集合),$U$是已被加入生成树的顶点集合。若$(u,v)$是一条具有最小权值的边,其中$u∈U,v∈V-U$ (其中$V-U$就是未被加入生成树的顶点集合,如下图),则必存在一棵包含边$(u,v)$的最小生成树。 143 | 144 | ![](https://61mon.com/images/illustrations/Prim/3.png) 145 | 146 | 可以用反证法证明。见下图,假设图$N$的任何一棵最小生成树都不包含$(u,v)$。设$T$是$N$上的一棵最小生成树,当将边$(u,v)$加入到$T$时,由生成树的定义,$T$中必存在一条包含$(u,v)$的回路。另一方面,由于$T$是生成树,则在$T$上必存在另一条边$(u',v')$,其中$u'∈U,v'∈V-U$,且$u$与$u'$,$v$与$v'$之间均有路径相通。删去边$(u',v')$,便可消除上述回路,同时得到另一棵生成树$T'$。因为$(u,v)$的权值不大于$(u',v')$,故$T'$的权值和不大于$T$,$T'$是包含$(u,v)$的一棵最新小生成树。和假设矛盾。 147 | 148 | ![](https://61mon.com/images/illustrations/Prim/4.png) 149 | 150 | 上述所说的$(u',v')$即为$u→y→z→v$中的某条边。 151 | 152 | 对于初学者来说,把上述的MST性质和Prim算法联系起来还是有点困难的。读者可以这样去理解,Prim本质上就是利用了贪心思想,随机选取一顶点作为起点,加入到集合$U$,接着找到与其关联的最小权值的边,该边上的另一个点也加入到$U$,接着再从$U$中的两个点出发继续向外找最小权值的边,找到后再加入第三个点,就这样重复下去。因为每次都是找最小值,所以当所有点都被加入$U$时,最小生成树也就被确定了。 153 | 154 | ## 五:时间复杂度 155 | 156 | Prim算法和Dijkstra 算法的时间复杂度一样,读者可以[点击](http://www.61mon.com/index.php/archives/194/#menu_index_4)查看,所以这里就不详细陈述了,附上一张表即可,其中$m$为边数,$n$为顶点数。 157 | 158 | | 最小边,权的数据结构 | 时间复杂度 | 159 | | :-------------: | :------------: | 160 | | 邻接矩阵,搜索(即本程序所用) | $O(n^2)$ | 161 | | 二叉堆,邻接表 | $O((m+n)logn)$ | 162 | | 斐波那契堆,邻接表 | $O(m+nlogn)$ | 163 | -------------------------------------------------------------------------------- /200-单源最短路径(4):总结.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > [单源最短路径(1):Dijkstra算法](https://61mon.com/index.php/archives/194/) 4 | > [单源最短路径(2):Bellman_Ford算法](https://61mon.com/index.php/archives/195/) 5 | > [单源最短路径(3):SPFA算法](https://61mon.com/index.php/archives/196/) 6 | > 单源最短路径(4):总结 7 | 8 | 9 | 10 | 11 | 12 | ## 一:负权问题 13 | 14 | 在早些时候学习最短路径算法时,心里就一直有个疑问,如果一个图仅仅是存在负权,但不构成负权回路,又该如何?我们一个一个看。 15 | 16 | Dijkstra算法, 17 | 18 | ![](https://61mon.com/images/illustrations/SingleSourceShortestPaths/11.png) 19 | 20 | 观察上图,若A作为源点,在第一轮循环后,B被标记数组标记,但我们发现在第二轮循环中,B还可以通过C点继续进行更新。由此,可以得出结论:**Dijkstra算法不适用于负权图。** 21 | 22 | Bellman_Ford算法和SPFA算法, 23 | 24 | 我们先思考下上述“因B点被标记数组标记而导致无法通过C点再更新”的问题,归根结底是标记数组的锅。有人提议,不妨去掉标记数组,如果去掉,就会造成很严重的问题,首当其冲的是“无法求出`dist[]`的最小值”。 25 | 26 | 反观Bellman_Ford算法和SPFA算法,它们不存在标记数组的问题,**因此对于仅仅存在负权的图,它们都可以工作的很好。** 27 | 28 | 最后,读者需要注意的是,如果是无向图,只要存在一条负边,该图就存在负权回路,这不难理解,无向图的一条边相当于有向图的往返两条边。 29 | 30 | ## 二:Dijkstra,Bellman_Ford和SPFA,该用哪个? 31 | 32 | | 算法 | 时间复杂度 | 原理 | 33 | | :----------: | :------------: | :--: | 34 | | Dijkstra | $O((m+n)logn)$ | 贪心 | 35 | | Bellman_Ford | $O(nm)$ | 动态规划 | 36 | | SPFA | $≤O(n^3)$ | 贪心 | 37 | 38 | Bellman_Ford没什么好说的,能不用就不用。 39 | 40 | 国际上一般不承认SPFA算法,首先在SPFA算法论文中,对它的复杂度证明存在错误,其次Bellman_Ford的论文中早已提及这个队列优化,所以SPFA并不算是新创的优化算法。 41 | 42 | 另外,SPFA算法有两个优化策略:SLF和LLL。 43 | 44 | * SLF,Small Label First 策略,设要加入的节点是j,队首元素为i,若`dist[j] < dist[i]`,则将j插入队首,否则插入队尾; 45 | * LLL,Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若`dist[i] > x`则将i插入到队尾,查找下一元素,直到找到某一i使得`dist[i] <= x`,则将i出队进行松弛操作。 46 | 47 | SLF 可使速度提高 15 ~ 20%,SLF + LLL 可提高约 50%。 但这两个优化本身是需要额外的复杂度的,甚至可能需要重新设计一套数据结构,来高效完成队首与队尾的插入。 48 | 49 | 因此,个人建议,在实际的应用中尽量不要采用SPFA算法。其时间效率也不是很稳定,为了避免[最坏情况](https://61mon.com/index.php/archives/196/#menu_index_5)的出现,还是应该使用效率更加稳定的Dijkstra算法。 50 | 51 | 使用邻接表+二叉堆优化的Dijkstra算法,复杂度适宜,也稳定,就是有个缺陷,不能处理负权回路。 52 | 53 | 最后,我发现在算法竞赛中我们大多数的选择还是SPFA,[知乎](https://www.zhihu.com/question/37832084)了下,邻接表+二叉堆优化的Dijkstra写起来复杂,容易错,而SPFA代码简单,容易写,但可能会被题目卡数据。国内的赛事应该不会出现卡数据的现象,国际赛事就不一定了,鉴于我在这方面不是很了解,所以就不多说了。但还是友情提醒下各位ACMer,若某题使用SPFA被判定为超时,您应该考虑下该题的SPFA是否被卡数据了。 54 | 55 | **总结:首选Dijkstra。**具体采用哪种方法,视情况而定。 56 | -------------------------------------------------------------------------------- /201~300/201-排序(2):快速排序.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):[直接插入排序,二分查找插入排序,希尔排序](https://61mon.com/index.php/archives/193/) 4 | > 排序(2):快速排序 5 | > 排序(3):[堆排序](https://61mon.com/index.php/archives/202/) 6 | > 排序(4):[归并排序](https://61mon.com/index.php/archives/203/) 7 | > 排序(5):[基数排序](https://61mon.com/index.php/archives/204/) 8 | > 排序(6):[总结](https://61mon.com/index.php/archives/205/) 9 | 10 | 11 | 12 | 13 | 14 | 快速排序本身不难,对于初学者,难就难在递归的理解。 15 | 16 | 算法步骤: 17 | (1):选取主元(以下选取数组开头为主元); 18 | (2):小于等于主元的放左边,大于等于主元的放右边; 19 | (3):分别对左边,右边递归,即重复(1)(2)步。 20 | 21 | ```c++ 22 | void QuickSort(int array[], int left, int right) 23 | { 24 | int i = left; 25 | int j = right; 26 | int base = array[left]; 27 | 28 | while (i <= j) 29 | { 30 | while (array[i] < base) 31 | i++; 32 | while (array[j] > base) 33 | j--; 34 | 35 | if (i <= j) 36 | { 37 | swap(array[i], array[j]); 38 | i++; 39 | j--; 40 | } 41 | }; 42 | 43 | if (left < j) 44 | QuickSort(array, left, j); 45 | if (i < right) 46 | QuickSort(array, i, right); 47 | } 48 | ``` 49 | ## 一:算法复杂度 50 | 51 | 最好的情况,每次我们运行一次分区,我们会把一个数组分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数组。则会有关系式: 52 | 53 | 54 | $$ 55 | T(n)=2T(\frac n2)+O(n) 56 | $$ 57 | 58 | 解出$T_{best}(n)=O(nlogn)$。 59 | 60 | 最坏的情况,在分割后,两子数组总是拥有各为1和n-1长度的数组,则递归关系式变为: 61 | 62 | $$ 63 | T(n)=T(n-1)+O(n)+O(1)=T(n-1)+O(n) 64 | $$ 65 | 66 | 解出$T_{worst}(n)=O(n^2)$。 67 | 68 | ## 二:细节讨论 69 | 70 | **(1):如何选择主元** 71 | 72 | 本文的代码很简单,以数组首元素作为主元。但我们知道主元的大小直接决定快排的效率,因为数组的划分需要依靠主元,理想状态下,给定的主元正好可以把数组分为长度相等的两个子数组,但找到并确定这样的主元还需要耗费额外的时间,如此一来,得不偿失。 73 | 74 | 因此现实生活中,我们更多的采取"三点中值",即数组首元素,尾元素和中间元素这三个元素的中位数作为主元。 75 | 76 | **(2):等于主元的数如何放置** 77 | 78 | 左右扫描,如果遇到和主元相等的元素怎么办?是暂停扫描(然后交换)还是继续扫描? 79 | 80 | 首先,两个方向采取的策略应该是一样的,也就是要么都暂停(然后交换),要么都继续扫描。否则将导致两个子数组不平衡。 81 | 82 | 其次,为了更好分析这个问题,我们不妨考虑所有元素都相同的情形。如果我们遇到和主元相等的时候不停止,那么从左到右扫描时,两指针将相遇,此次过程结束。结果呢?什么都没做,却得到了两个大小极其不均衡的数组。算法时间复杂度为$O(n^2)$。如果我们选择遇到相等元素时停止扫描,然后交换,那么虽然看上去交换的次数变多了,但是我们将得到大小相等(或者差1)的两个子数组,算法的时间复杂度为$O(nlogn)$。 83 | 84 | 因此,遇到和主元相等的元素时候我们都暂停扫描,交换元素后继续,直到指针相遇或者交叉。(摘自[深入解析快速排序](http://www.yebangyu.org/blog/2016/03/09/quicksort/)) 85 | 86 | **(3):`i <= j `的等号可以去掉么** 87 | 88 | 不可以! 89 | 90 | 我们先来看下代码的结尾, 91 | 92 | ```c++ 93 | if (left < j) 94 | QuickSort(array, left, j); 95 | if (i < right) 96 | QuickSort(array, i, right); 97 | ``` 98 | 99 | 从上面的代码可以看出,当while循环结束,i需指向左子数组的尾元素,j需指向右子数组的首元素,但两者不能重合,因为一旦重合,子数组的递归就可能会打乱它们的排序。 100 | 101 | **(4):分割划分策略** 102 | 103 | 本文所采用的划分策略很简单,易于理解,实际应用中,要复杂的多,有兴趣的朋友可以参见[这里](https://algs4.cs.princeton.edu/lectures/23DemoPartitioning.pdf)。 104 | -------------------------------------------------------------------------------- /201~300/202-排序(3):堆排序.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):[直接插入排序,二分查找插入排序,希尔排序](https://61mon.com/index.php/archives/193/) 4 | > 排序(2):[快速排序](https://61mon.com/index.php/archives/201/) 5 | > 排序(3):堆排序 6 | > 排序(4):[归并排序](https://61mon.com/index.php/archives/203/) 7 | > 排序(5):[基数排序](https://61mon.com/index.php/archives/204/) 8 | > 排序(6):[总结](https://61mon.com/index.php/archives/205/) 9 | 10 | 堆排序是利用堆的性质进行的一种选择排序。下面先讨论一下堆。 11 | 12 | 堆实际上是一棵完全二叉树,其满足性质:任何一结点大于等于或者小于等于其左右子树结点。 13 | 14 | 堆分为大顶堆和小顶堆,满足“任何一结点大于等于其左右子树结点”的称为大顶堆,满足“任何一结点小于等于其左右子树结点”的称为小顶堆。由上述性质可知:大顶堆的堆顶肯定是最大的,小顶堆的堆顶是最小的。 15 | 16 | 下面举个例子(资源来自[堆排序-海子](http://www.cnblogs.com/dolphin0520/archive/2011/10/06/2199741.html))来说明堆排序的过程(以升序为例): 17 | 18 | (1) 19 | ![](https://61mon.com/images/illustrations/Sort/3.jpg) 20 | 21 | 给定整型数组:{ 16, 7, 3, 20, 17, 8 },根据该数组“构建”完全二叉树(并不是真的写代码去构建,只是把数组看成完全二叉树去操作)。 22 | 23 | 程序从最后一个非叶子结点开始,即3。判断其左右孩子:8,8比3大,把8调整上去。 24 | 25 | (2) 26 | ![](https://61mon.com/images/illustrations/Sort/4.jpg) 27 | 28 | 3结点下无孩子,判断结束。 29 | 30 | 继续往前一步,至7结点,判断其左右孩子:20和17,20是最大的,将其调整上去。 31 | 32 | (3) 33 | ![](https://61mon.com/images/illustrations/Sort/5.jpg) 34 | 35 | 7结点下无孩子,判断结束。 36 | 37 | 继续往前一步,至16结点,判断其左右孩子:20和8,20是最大的,将其调整上去。 38 | 39 | (4) 40 | ![](https://61mon.com/images/illustrations/Sort/6.jpg) 41 | 42 | 判断16结点下左右孩子:7和17,17是最大的,将其调整上去。 43 | 44 | (5) 45 | ![](https://61mon.com/images/illustrations/Sort/7.jpg) 46 | 47 | 16结点下无孩子,判断结束。 48 | 49 | 遍历已至头部,结束。 50 | 51 | (6)至此数组已经满足大顶堆的性质,接下来的操作就很简单了。 52 | ![](https://61mon.com/images/illustrations/Sort/8.jpg) 53 | 54 | 看完上面所述的流程你至少有两个疑问: 55 | * **如何确定最后一个非叶子结点?** 56 | 57 | 其实这是有一个公式的,设二叉树结点总数为n,则最后一个非叶子结点是第$⌊\frac n2⌋$个。 58 | 59 | * **数组当中如何确定当前结点的左右孩子位置?** 60 | 61 | 设当前结点下标是i,则其左孩子的下标是2i,右孩子的下标是2i+1。请注意:这是建立在数组下标从1开始的情况。若数组下标从0开始,则其左右孩子下标还各需多加一个1。 62 | 63 | 以下代码默认数组下标从1开始,请读者注意。 64 | 65 | 66 | ```c++ 67 | /* 已知 array[left]...array[right] 的值除 array[left] 之外均满足堆的定义, 68 | 本函数调整 array[left],使 array[left]...array[right] 成一个大顶堆 */ 69 | void HeapAdjust(int array[], int left, int right) 70 | { 71 | int index = left; 72 | 73 | for (int i = left * 2; i <= right; i = i * 2) 74 | { 75 | if (i < right && array[i] < array[i + 1]) // 找到孩子中较大者 76 | i++; 77 | if (array[index] > array[i]) 78 | return; 79 | swap(array[index], array[i]); 80 | index = i; 81 | } 82 | } 83 | 84 | void HeapSort(int array[], int left, int right) 85 | { 86 | int len = right - left + 1; 87 | 88 | for (int i = len / 2; i >= left; i--) // 把数组调整成大顶堆 89 | HeapAdjust(array, i, right); 90 | 91 | for (int i = right; i > left; i--) // 排序 92 | { 93 | swap(array[left], array[i]); 94 | HeapAdjust(array, left, i - 1); 95 | } 96 | } 97 | ``` 98 | **时间复杂度为$O(nlogn)$**,证明如下。 99 | 100 | 首先计算建堆的时间,也就是下面的代码, 101 | 102 | ```c++ 103 | for (int i = len / 2; i >= left; i--) // 把数组调整成大顶堆 104 | HeapAdjust(array, i, right); 105 | ``` 106 | 107 | n个结点,从第0层至第$log_2n$层。对于第i层的$2^i$个点如果需要往下走$log_2n-i$步,那么把走的所有步相加得, 108 | $$ 109 | \begin{align} 110 | T(n)&=\sum_{i=0}^{i=log_2n}{2^i(log_2n-i)}\\ 111 | &=2n-log_2n-2\\ 112 | &<2n\\ 113 | &=O(n) 114 | \end{align} 115 | $$ 116 | 117 | 接下来就是排序的时间,即下面的代码: 118 | ```c++ 119 | for (int i = right; i > left; i--) // 排序 120 | { 121 | swap(array[left], array[i]); 122 | HeapAdjust(array, left, i - 1); 123 | } 124 | ``` 125 | HeapAdjust()耗时$logn$,共n次,故排序时间为$O(nlogn)$。 126 | 127 | 综上所述,堆排序时间复杂度为$T(n)=O(n)+O(nlogn)=O(nlogn)$。 128 | -------------------------------------------------------------------------------- /201~300/203-排序(4):归并排序.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):[直接插入排序,二分查找插入排序,希尔排序](https://61mon.com/index.php/archives/193/) 4 | > 排序(2):[快速排序](https://61mon.com/index.php/archives/201/) 5 | > 排序(3):[堆排序](https://61mon.com/index.php/archives/202/) 6 | > 排序(4):归并排序 7 | > 排序(5):[基数排序](https://61mon.com/index.php/archives/204/) 8 | > 排序(6):[总结](https://61mon.com/index.php/archives/205/) 9 | 10 | 11 | 12 | 13 | 14 | ![](https://61mon.com/images/illustrations/Sort/9.png) 15 | 16 | 对于待排序数组:{ 5, 2, 4, 6, 1, 3, 2, 6 },归并算法的思路如上图,注意上图是**从下往上**看。 17 | ```c++ 18 | /* 把有序的 array[left]...array[mid] 和 array[mid+1]...array[right] 合并 */ 19 | void Merge(int array[], int temp[], int left, int mid, int right) 20 | { 21 | int i = left; 22 | int j = mid + 1; 23 | int k = left; 24 | 25 | while (i <= mid && j <= right) 26 | { 27 | if (array[i] < array[j]) 28 | temp[k++] = array[i++]; 29 | else 30 | temp[k++] = array[j++]; 31 | } 32 | 33 | while (i <= mid) 34 | temp[k++] = array[i++]; 35 | while (j <= right) 36 | temp[k++] = array[j++]; 37 | 38 | while (left <= right) 39 | array[left] = temp[left++]; 40 | } 41 | 42 | /* temp[] 数组起到一个中转的作用 */ 43 | void MergeSort(int array[], int temp[], int left, int right) 44 | { 45 | if (left >= right) 46 | return; 47 | 48 | int mid = ((right - left) >> 1) + left; 49 | 50 | MergeSort(array, temp, left, mid); 51 | MergeSort(array, temp, mid + 1, right); 52 | 53 | Merge(array, temp, left, mid, right); 54 | } 55 | ``` 56 | 57 | Merge()耗时$O(n)$,则归并排序的时间递归式为$T(n)=2T(\frac n2)+O(n)$,解得$T(n)=O(nlogn)$。 58 | -------------------------------------------------------------------------------- /201~300/204-排序(5):基数排序.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):[直接插入排序,二分查找插入排序,希尔排序](https://61mon.com/index.php/archives/193/) 4 | > 排序(2):[快速排序](https://61mon.com/index.php/archives/201/) 5 | > 排序(3):[堆排序](https://61mon.com/index.php/archives/202/) 6 | > 排序(4):[归并排序](https://61mon.com/index.php/archives/203/) 7 | > 排序(5):基数排序 8 | > 排序(6):[总结](https://61mon.com/index.php/archives/205/) 9 | 10 | 11 | 12 | 13 | 14 | 基数排序与前面所述的排序方法都不同,它不需要比较关键字的大小。 15 | 16 | 它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。 17 | 18 | 基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。 19 | 20 | 不妨通过一个具体的实例来展示一下基数排序是如何进行的。 设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。 21 | 22 | 我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以0\~9来表示的,所以我们不妨把0~9视为10个桶。 23 | 24 | 我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是0,将这个数存入编号为0的桶中。 25 | 26 | ![](https://61mon.com/images/illustrations/Sort/10.png) 27 | 28 | 分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。这时,得到的序列就是个位数上呈递增趋势的序列。 29 | 30 | 按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。 31 | 32 | 接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。 33 | 34 | 代码如下: 35 | 36 | ```c++ 37 | /* 求出数组中元素最大的位数 */ 38 | int MaxBit(int array[], int n) 39 | { 40 | // 数组最大值 41 | int max_data = array[0]; 42 | for (int i = 1; i < n; i++) 43 | { 44 | if (array[i] > max_data) 45 | max_data = array[i]; 46 | } 47 | 48 | // 数组最大值的位数 49 | int bits_num = 0; 50 | while (max_data) 51 | { 52 | bits_num++; 53 | max_data /= 10; 54 | } 55 | 56 | return bits_num; 57 | } 58 | 59 | /* 基数排序 */ 60 | void RadixSort(int array[], int n) 61 | { 62 | int bits_num = MaxBit(array, n); 63 | int radix = 1; 64 | int * temp = new int[n]; // 临时数组 65 | int * count = new int[10]; // 保存各个桶中的个数 66 | 67 | for (int i = 1; i <= bits_num; i++) 68 | { 69 | // 计数器清 0 70 | for (int j = 0; j < 10; j++) 71 | count[j] = 0; 72 | 73 | // 统计各个桶中的个数 74 | for (int j = 0; j < n; j++) 75 | { 76 | int k = (array[j] / radix) % 10; 77 | count[k]++; 78 | } 79 | 80 | // 索引重分配 81 | for (int j = 1; j < 10; j++) 82 | count[j] = count[j - 1] + count[j]; 83 | 84 | // 放入临时数组,从右往左扫描,保证排序稳定性 85 | for (int j = n - 1; j >= 0; j--) 86 | { 87 | int k = (array[j] / radix) % 10; 88 | temp[count[k] - 1] = array[j]; 89 | count[k]--; 90 | } 91 | 92 | // 临时数组复制到 array[] 中 93 | for (int j = 0; j < n; j++) 94 | array[j] = temp[j]; 95 | 96 | radix *= 10; 97 | } 98 | 99 | delete[] temp; 100 | delete[] count; 101 | } 102 | ``` 103 | 104 | 基数排序的时间复杂度是$O(k⋅n)$,其中$n$是排序元素个数,$k$是最大的数字位数。 105 | 106 | 那基数排序是否比基于比较的排序算法(如快速排序)更好呢?(以下摘自算法导论) 107 | 108 | $O(k⋅n)$与$O(nlogn)$这一结果看上去确实是基数排序更好一点。但是在这两个表达式中,隐藏在$O$符号背后的常数项因子是不同的。在处理的$n$个关键字时,尽管基数排序执行的循环轮数会比快速排序要少,但每一轮它所耗费的时间要长得多。哪一个排序算法更合适依赖于具体实现和底层硬件的特性(例如,快速排序通常可以比基数排序更有效地使用硬件的缓存),以及输入数据的特征。此外,利用计数排序作为中间稳定排序的基数排序不是原址排序,而很多$O(nlogn)$时间的比较排序是原址排序。因此,当主存的容量比较宝贵时,我们可能会倾向于像快速排序这样的原址排序算法。 109 | -------------------------------------------------------------------------------- /201~300/205-排序(6):总结.md: -------------------------------------------------------------------------------- 1 | > 系列文章目录 2 | > 3 | > 排序(1):[直接插入排序,二分查找插入排序,希尔排序](https://61mon.com/index.php/archives/193/) 4 | > 排序(2):[快速排序](https://61mon.com/index.php/archives/201/) 5 | > 排序(3):[堆排序](https://61mon.com/index.php/archives/202/) 6 | > 排序(4):[归并排序](https://61mon.com/index.php/archives/203/) 7 | > 排序(5):[基数排序](https://61mon.com/index.php/archives/204/) 8 | > 排序(6):总结 9 | 10 | 11 | 12 | 13 | 14 | | 排序方法 | 最差时间复杂度 | 最佳时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 | 15 | | :----: | :--------: | :--------: | :--------: | :-------: | :--: | 16 | | 直接插入排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | O(1) | 稳定 | 17 | | 快速排序 | $O(n^2)$ | $O(nlogn)$ | $O(nlogn)$ | $O(logn)$ | 不稳定 | 18 | | 堆排序 | $O(nlogn)$ | $O(nlogn)$ | $O(nlogn)$ | $O(1)$ | 不稳定 | 19 | | 归并排序 | $O(nlogn)$ | $O(nlogn)$ | $O(nlogn)$ | $O(n)$ | 稳定 | 20 | | 基数排序 | $×$ | $×$ | $O(k⋅n)$ | $O(n)$ | 稳定 | 21 | 22 | 所谓的稳定性,百度百科解释为:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri = rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。 23 | 24 | 我并未把前面几篇文章所涉及的所有排序算法都列入表格,因为在实际生活中,我们所用的无外乎上面的5种排序,所以我们只需关注这5种算法足矣。 25 | 26 | 当然,排序算法有很多种,有兴趣的读者可以[了解下](https://zh.wikipedia.org/wiki/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95)。 27 | 28 | 最后不得不说下STL之sort的实现技巧。STL之sort并非QuickSort,而是IntroSort。 29 | 30 | IntroSort,是David R.Musser于1996年提出的一种混合式排序算法:Introspective Sort(内省式排序),简称IntroSort。 31 | 32 | 代码如下(摘自Visual Studio 2017,代码并不完整): 33 | 34 | ```c++ 35 | const int _ISORT_MAX = 32; // maximum size for insertion sort 36 | 37 | template inline 38 | void _Sort_unchecked1(_RanIt _First, _RanIt _Last, _Diff _Ideal, _Pr& _Pred) 39 | { // order [_First, _Last), using _Pred 40 | _Diff _Count; 41 | while (_ISORT_MAX < (_Count = _Last - _First) && 0 < _Ideal) 42 | { // divide and conquer by quicksort 43 | pair<_RanIt, _RanIt> _Mid = 44 | _Partition_by_median_guess_unchecked(_First, _Last, _Pred); 45 | _Ideal /= 2, _Ideal += _Ideal / 2; // allow 1.5 log2(N) divisions 46 | 47 | if (_Mid.first - _First < _Last - _Mid.second) 48 | { // loop on second half 49 | _Sort_unchecked1(_First, _Mid.first, _Ideal, _Pred); 50 | _First = _Mid.second; 51 | } 52 | else 53 | { // loop on first half 54 | _Sort_unchecked1(_Mid.second, _Last, _Ideal, _Pred); 55 | _Last = _Mid.first; 56 | } 57 | } 58 | 59 | if (_ISORT_MAX < _Count) 60 | { // heap sort if too many divisions 61 | _Make_heap_unchecked(_First, _Last, _Pred); 62 | _Sort_heap_unchecked(_First, _Last, _Pred); 63 | } 64 | else if (2 <= _Count) 65 | _Insertion_sort_unchecked(_First, _Last, _Pred); // small 66 | } 67 | ``` 68 | 69 | 宏观来看,STL之sort是“快排+堆排+直接插入”三种混合排序的排序算法。当算法有恶化的倾向时,IntroSort能够自我检测,转而使用另外的排序算法,保证其时间复杂度,此即所谓的“扬长避短”。 70 | 71 | 微观来看,利用`_Ideal`来记录快速排序的分割次数,当大于$1.5log_2n$时,转而选择堆排或插入排序,二选其一的基准是此刻待排序元素个数是否大于$32$,这从代码就可以看出。 72 | 73 | 当然不同的STL版本采用不同的具体实现,比如[SGI STL](http://www.sgi.com/tech/stl/)也采用了IntroSort,但其实现却有较大区别,读者可以参考侯杰所著的《STL源码剖析》;RW STL则是纯粹地使用了QuickSort。 74 | -------------------------------------------------------------------------------- /201~300/206-Dijkstra算法与Prim算法的区别.md: -------------------------------------------------------------------------------- 1 | Dijkstra算法与Prim算法非常相似,甚至很多初学者觉得它们就是一样的。它们最直观的区别就是目的不同:前者求解最短路径,后者求解最小生成树。 2 | 3 | ![](https://61mon.com/images/illustrations/DijkstraAndPrim/1.png) 4 | 5 | 6 | 7 | 8 | 9 | 对比上图, 10 | 11 | * 最短路径 12 | 13 | ``` 14 | a->b @length = 2 15 | a->c @length = 3 16 | ``` 17 | 18 | * 最小生成树 19 | 20 | ``` 21 | a->b->c @sum = 4 22 | ``` 23 | 24 | 结论:两者不一样! 25 | 26 | 好,最后让我们回归代码。 27 | 28 | Dijkstra算法与Prim算法都有一个数组,不妨统一称为`R[]`,我们每次都是取`R[]`的最小值,接着更新`R[]`,再取其最小值,,,往复下去。而这两者的区别就发生在**更新**操作之中。 29 | 30 | * 最短路径 31 | 32 | ``` 33 | for the weight of edge(u->v) 34 | if R[v] > R[u] + weight 35 | R[v] = R[u] + weight 36 | ``` 37 | 38 | * 最小生成树 39 | 40 | ``` 41 | for the weight of edge(u->v) 42 | if R[v] > weight 43 | R[v] = weight 44 | ``` 45 | 46 | 其区别,从代码来看,显而易见。 47 | -------------------------------------------------------------------------------- /201~300/208-递归(1):基础.md: -------------------------------------------------------------------------------- 1 | >系列文章目录 2 | > 3 | >递归(1):基础 4 | >[递归(2):高级](https://61mon.com/index.php/archives/211/) 5 | 6 | 递归的学习绝对是一个持久战,没有人可以一蹴而就。一年两年的,很寻常。 7 | 8 | 问题的复杂,加上递归本身的细节,我们想要"学会","学好",再"用好",是需要一个漫长的过程的。所以还希望读者有足够的耐心。 9 | 10 | ![](https://61mon.com/images/illustrations/Recursion/1.jpg) 11 | 12 | 13 | 14 | 15 | 16 | ## 一:什么是递归 17 | 18 | 所谓递归,简单点来说,就是一个函数直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。 19 | 20 | 我们可以把”递归“比喻成“查字典“,当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词,可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。(摘自[知乎](https://www.zhihu.com/question/20507130)的一个回答) 21 | 22 | 我们以阶乘作为例子($0!=1, 1!=1, 3!=6$ ,...): 23 | 24 | ```c++ 25 | int Factorial(int n) 26 | { 27 | if (n == 0) 28 | return 1; 29 | 30 | return n * Factorial(n - 1); 31 | } 32 | ``` 33 | 34 | ## 二:递归与栈的关系 35 | 36 | 常常听到“递归的过程就是出入栈的过程”,这句话怎么理解?我们以上述代码为例,取$n=3$,则过程如下: 37 | 38 | ![](https://61mon.com/images/illustrations/Recursion/2.png) 39 | 40 | * 第1~4步,都是入栈过程,`Factorial(3)`调用了`Factorial(2)`,`Factorial(2)`又接着调用`Factorial(1)`,直到`Factorial(0)`; 41 | * 第5步,因0是递归结束条件,故不再入栈,此时栈高度为4,即为我们平时所说的递归深度; 42 | * 第6~9步,`Factorial(0)`做完,出栈,而`Factorial(0)`做完意味着`Factorial(1)`也做完,同样进行出栈,重复下去,直到所有的都出栈完毕,递归结束。 43 | 44 | **每一个递归程序都可以把它改写为非递归版本。**我们只需利用栈,通过入栈和出栈两个操作就可以模拟递归的过程,二叉树的遍历无疑是这方面的代表。 45 | 46 | **但是并不是每个递归程序都是那么容易被改写为非递归的。**某些递归程序比较复杂,其入栈和出栈非常繁琐,给编码带来了很大难度,而且易读性极差,所以条件允许的情况下,推荐使用递归。 47 | 48 | ## 三:如何思考递归 49 | 50 | 在初学递归的时候, 看到一个递归实现, 我们总是难免陷入不停的验证之中,比如上面提及的阶乘,求解`Factorial(n)`时,我们总会情不自禁的发问,`Factorial(n-1)`可以求出正确的答案么?接着我们就会再用`Factorial(n-2)`去验证,,,不停地往下验证直到`Factorial(0)`。 51 | 52 | 对递归这样的不适应,和我们平时习惯的思维方式有关。我们习惯的思维是:已知`Factorial(0)`,乘上1就等于`Factorial(1)`,再乘以2就等于`Factorial(2)`,,,直到乘到n。 53 | 54 | **而递归和我们的思维方式正好相反。** 55 | 56 | 那我们怎么判断这个递归计算是否是正确的呢?[Paul Graham](https://zh.wikipedia.org/wiki/%E4%BF%9D%E7%BD%97%C2%B7%E6%A0%BC%E9%9B%B7%E5%8E%84%E5%A7%86)提到一种方法,如下: 57 | 58 | > 如果下面这两点是成立的,我们就知道这个递归对于所有的n都是正确的。 59 | > 1. 当$n=0,1​$时,结果正确; 60 | > 2. 假设递归对于$n$是正确的,同时对于$n+1$也正确。 61 | 62 | 这种方法很像数学归纳法,也是递归正确的思考方式,上述的第1点称为基本情况,第2点称为通用情况。 63 | 64 | 在递归中,我们通常把第1点称为终止条件,因为这样更容易理解,其作用就是终止递归,防止递归无限地运行下去。 65 | 66 | 下面我们用两个例子来具体说明这种数学归纳法: 67 | 68 | ##### 例1 汉诺塔 69 | 70 | ![](https://61mon.com/images/illustrations/Recursion/3.png) 71 | 72 | 问题描述为:有三根杆子A,B,C。A杆上有N个穿孔圆盘,盘的尺寸由上到下依次变大,B,C杆为空。要求按下列规则将所有圆盘移至C杆: 73 | 74 | 1. 每次只能移动一个圆盘; 75 | 2. 大盘不能叠在小盘上面。 76 | 77 | 问:如何移?最少要移动多少次? 78 | 79 | 首先看下基本情况,即终止条件:N=1时,直接从A移到C。 80 | 81 | 再来看下通用情况:当有N个圆盘在A上,我们已经找到办法将其移到C杠上了,我们怎么移动N+1个圆盘到C杠上呢?很简单,我们首先用将N个圆盘移动到C上的方法将N个圆盘都移动到B上,然后再把第N+1个圆盘(最后一个)移动到C上,再用同样的方法将在B杠上的N个圆盘移动到C上,问题解决。 82 | 83 | 代码如下: 84 | 85 | ```c++ 86 | void Hanoi(int n, char a, char b, char c) 87 | { 88 | //终止条件 89 | if (n == 1) 90 | { 91 | cout << a << "-->" << c << endl; 92 | return; 93 | } 94 | 95 | //通用情况 96 | Hanoi(n - 1, a, c, b); 97 | Hanoi(1, a, b, c); 98 | Hanoi(n - 1, b, a, c); 99 | } 100 | ``` 101 | 102 | ##### 例2 求二叉树节点个数 103 | 104 | ![](https://61mon.com/images/illustrations/Recursion/4.png) 105 | 106 | 首先看下基本情况,即终止条件:当为空树时,节点数为0; 107 | 108 | 再来看下通用情况:当前节点的左,右子树节点数都被求出,则以当前结点为根的二叉树的节点总数就是“左子树+右子树+1”。 109 | 110 | 代码如下: 111 | 112 | ```c++ 113 | int GetNodes(Node * node) 114 | { 115 | //终止条件 116 | if (node == nullptr) 117 | return 0; 118 | 119 | //通用情况 120 | return GetNodes(node->left) + GetNode(node->right) + 1; 121 | } 122 | ``` 123 | 124 | ## 四:什么时候该用递归 125 | 126 | **当我们遇到一个问题时,我们是怎么判断该题用递归来解决的?** 127 | 128 | > 问题可用递归来解决需具备的条件: 129 | > 130 | > 1. 子问题需与原问题为同样的事,且规模更小; 131 | > 2. 程序停止条件。 132 | 133 | 概念说的很容易,但往往事情难做,所以接下里我更想给初学者一些学习的建议。 134 | 135 | 递归这个东西,没别的办法,要想搞定它,就三个字:敲代码! 136 | 137 | 剩下的问题就是该去哪里敲呢?这里我给几个途径: 138 | 139 | 1. 二叉树的各种面试笔试题目,链接为[https://61mon.com/index.php/archives/191/](https://61mon.com/index.php/archives/191/); 140 | 2. DFS搜索的题目可以很好地帮助我们理解递归,读者在OJ上就可以找到很多。 141 | 142 | 待你做了50题后,自然而然你就理解递归了。 143 | -------------------------------------------------------------------------------- /201~300/209-你被欺骗了很久:前缀和真前缀.md: -------------------------------------------------------------------------------- 1 | 相信很多读者都看过网上博客对KMP算法的讲解,其中必提及的一个名词就是:**前缀**。那么请问你心中理解的前缀的定义是什么呢? 2 | 3 | > 对于字符串“china”,其前缀为: 4 | > 5 | > china, chin, chi, ch, c 6 | 7 | 你的想法是不是和上面一样呢。但是我很遗憾地告诉你,KMP之前缀不是这样的,它是这样的: 8 | 9 | >chin, chi, ch, c 10 | 11 | ![](https://61mon.com/images/illustrations/PrefixesAndProperPrefixes/1.jpg) 12 | 13 | 14 | 15 | 16 | 17 | 难道是我们记错前缀的概念了?不!不是我们记错了,只是有人在指鹿为马而已。下面来揭晓真像吧。 18 | 19 | ![](https://61mon.com/images/illustrations/PrefixesAndProperPrefixes/2.png) 20 | 21 | 如此看来,KMP之前缀并非前缀,而是真前缀!而大多数(几乎所有)的博客都在**以“真前缀”去定义“前缀”**。 22 | 23 | **next数组**是KMP的一个核心概念,而**真前缀**又是next数组的核心。算法本属于一个很严谨的领域,这种在重要概念上却还指鹿为马的行为,是应该需要我们注意和避免的。 24 | 25 | 不知道大家有没有发现,你所看过的KMP博文无一提及真前缀的定义,除了阮一峰的[字符串匹配的KMP算法](http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html)。 26 | 27 | ![](https://61mon.com/images/illustrations/PrefixesAndProperPrefixes/3.png) 28 | 29 | 哈哈,阮老师太粗心了,在文章开头阮老师已经讲过,他是阅读了[Jake Boxer](http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/)的文章才明白KMP的,那原文是什么样的呢? 30 | 31 | ![](https://61mon.com/images/illustrations/PrefixesAndProperPrefixes/4.png) 32 | -------------------------------------------------------------------------------- /201~300/210-Boyer-Moore算法.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | 上一篇文章,我介绍了[KMP算法](https://61mon.com/index.php/archives/183/)。 4 | 5 | 但是,它并不是效率最高的算法,实际采用并不多。各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。Boyer-Moore算法不仅效率高,而且构思巧妙。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。 6 | 7 | 8 | 9 | 10 | 11 | ## 二:算法过程 12 | 13 | 以下摘自阮一峰的[字符串匹配的Boyer-Moore算法](http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html),并作稍微修改。 14 | 15 | (1) 16 | 17 | ![](https://61mon.com/images/illustrations/boyer_moore/1.png) 18 | 19 | 假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。 20 | 21 | (2) 22 | 23 | ![](https://61mon.com/images/illustrations/boyer_moore/2.png) 24 | 25 | 首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。 26 | 27 | 这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。 28 | 29 | 我们看到,"S"与"E"不匹配。这时,**"S"就被称为"坏字符"(bad character),即不匹配的字符**。我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。 30 | 31 | (3) 32 | 33 | ![](https://61mon.com/images/illustrations/boyer_moore/3.png) 34 | 35 | 依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。 36 | 37 | (4) 38 | 39 | ![](https://61mon.com/images/illustrations/boyer_moore/4.png) 40 | 41 | 我们由此总结出**"坏字符规则":后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置。** 42 | 43 | 44 | 我们约定,如果"坏字符"不包含在搜索词之中,则上一次出现位置为-1。 45 | 46 | 举个例子,以前面第三步的"P"为例,它作为"坏字符",出现在(相对)搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 - 4 = 2位。再以前面第二步的"S"为例,它出现在第6位(相对搜索词),上一次出现位置是-1(即未出现),则整个搜索词后移 6 - (-1) = 7位。 47 | 48 | (5) 49 | 50 | ![](https://61mon.com/images/illustrations/boyer_moore/5.png) 51 | 52 | 依然从尾部开始比较,"E"与"E"匹配。 53 | 54 | (6) 55 | 56 | ![](https://61mon.com/images/illustrations/boyer_moore/6.png) 57 | 58 | 比较前面一位,"LE"与"LE"匹配。 59 | 60 | (7) 61 | 62 | ![](https://61mon.com/images/illustrations/boyer_moore/7.png) 63 | 64 | 比较前面一位,"PLE"与"PLE"匹配。 65 | 66 | (8) 67 | 68 | ![](https://61mon.com/images/illustrations/boyer_moore/8.png) 69 | 70 | 比较前面一位,"MPLE"与"MPLE"匹配。**我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。**注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。 71 | 72 | 为了接下来方便讲解,在这里作一下区分:**我们统一称"MPLE"、"PLE"、"LE"、"E"为"好后缀",其中"MPLE"为"最长好后缀","PLE"、"LE"、"E"统一称为"真好后缀"。** 73 | 74 | (9) 75 | 76 | ![](https://61mon.com/images/illustrations/boyer_moore/9.png) 77 | 78 | 比较前一位,发现"I"与"A"不匹配。所以,"I"是"坏字符"。 79 | 80 | (10) 81 | 82 | ![](https://61mon.com/images/illustrations/boyer_moore/10.png) 83 | 84 | 根据"坏字符规则",此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法? 85 | 86 | (11) 87 | 88 | ![](https://61mon.com/images/illustrations/boyer_moore/11.png) 89 | 90 | 我们知道,此时存在"好后缀"。 91 | 92 | 所以,可以采用**"好后缀规则":后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置。** 93 | 94 | 这个规则有三个注意点: 95 | 96 | 1. "好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"好后缀"是"EF"、"F",则它的位置以"F"为准,即5(从0开始计算)。 97 | 2. 如果"好后缀"在搜索词中都只出现一次,则它们的上一次出现位置为 -1。比如,ABCDEF"的好后缀是"EF"、"F",但它们在"ABCDEF"之中都只出现一次,则它们的上一次出现位置为-1(即未出现)。 98 | 3. 如果有多个"好后缀"在搜索词中都出现多次,那我们应该选哪个作为上一次出现的位置呢?我们采取的策略是:先查看"最长好后缀"是否也出现多次,如果是,直接选用这个"最长好后缀"作为上一次出现的位置;如果不是,则在"真好后缀"中选取"长度最长且也属于前缀"的那个"好后缀"。 99 | 100 | 举几个例子来具体说明上述的3个注意点。 101 | 102 | 1. 如果"ABCDEF"的"好后缀"是"EF"、"F"。那么"好后缀"的位置是 5(从 0 开始计算,取最后的 "F" 的值),观察发现"好后缀"只出现一次,则它们的上一次出现位置为-1,则后移位数为5-(-1)=6; 103 | 2. 如果"ABEFCDEF"的"好后缀"是"EF"、"F"。那么"好后缀"的位置是7,观察发现这两个"好后缀"都出现多次,则先查看"最长好后缀(EF)"是否出现多次,观察发现出现2次,所以选用"EF"为上一次出现的位置,"EF"上一次出现的位置为3,则后移位数为7-3=4; 104 | 3. 如果"EFABCDEF"的"好后缀"是"CDEF"、"DEF"、"EF"、"F"。那么"好后缀"的位置是7,观察发现有两个"好后缀"出现多次,则先查看"最长好后缀(CDEF)"是否出现多次,观察发现不是,则在"真好后缀(DEF、EF、F)"中选取"长度最长且也属于前缀"的那个"好后缀",是"EF",其上一次出现的位置是1,则后移位数为7-1=6。 105 | 106 | 回到当前步骤的这个例子。此时,所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。 107 | 108 | (12) 109 | 110 | ![](https://61mon.com/images/illustrations/boyer_moore/12.png) 111 | 112 | 可以看到,"坏字符规则"只能移3位,"好后缀规则"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。 113 | 114 | 更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。 115 | 116 | (13) 117 | 118 | ![](https://61mon.com/images/illustrations/boyer_moore/13.png) 119 | 120 | 继续从尾部开始比较,"P"与"E"不匹配,因此"P"是"坏字符"。根据"坏字符规则",后移 6 - 4 = 2位。 121 | 122 | (14) 123 | 124 | ![](https://61mon.com/images/illustrations/boyer_moore/14.png) 125 | 126 | 从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据"好后缀规则",后移 6 - 0 = 6位,即头部的"E"移到尾部的"E"的位置。 127 | 128 | ## 三:如何求坏字符表和好后缀表 129 | 130 | ### 3.1 坏字符算法(Bad Character) 131 | 132 | 当出现一个坏字符时, BM算法向右移动搜索词,让搜索词中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法有两种情况。 133 | 134 | **(1)搜索词中有对应的坏字符时,让搜索词中最靠右的对应字符与坏字符相对。** 135 | 136 | ![](https://61mon.com/images/illustrations/boyer_moore/15.png) 137 | 138 | 第二个$x$即为移动后的位置。 139 | 140 | **(2)搜索词中不存在坏字符,那就直接右移整个搜索词长度这么大步数。** 141 | 142 | ![](https://61mon.com/images/illustrations/boyer_moore/16.png) 143 | 144 | 第二个$x$即为移动后的位置。 145 | 146 | **代码实现:** 147 | 148 | 利用哈希空间换时间的思想,使用字符作为下标而不是位置作为下标。这样只需要遍历一遍即可。如果是纯8位字符也只需要256个空间大小,而且对于大模式,可能本身长度就超过了256,所以这样做是值得的(这也是为什么数据越大,BM算法越高效的原因之一)。 149 | 150 | ```c++ 151 | #define MAX_CHAR 256 152 | 153 | /* 计算坏字符数组 */ 154 | void GetBC(string & p, int & m, int bc[]) 155 | { 156 | for (int i = 0; i < MAX_CHAR; i++) 157 | bc[i] = m; 158 | for (int i = 0; i < m; i++) 159 | bc[p[i]] = m - 1 - i; 160 | } 161 | ``` 162 | 163 | ### 3.2 好后缀算法(Good Suffix) 164 | 165 | 如果程序匹配了一个"好后缀",那就把前面的"好后缀"移动到当前"好后缀"位置。这样,好后缀算法有三种情况。 166 | 167 | **(1)搜索词中有子串和"最长好后缀"完全匹配,则将最靠右的那个完全匹配的子串移动到"最长好后缀"的位置继续进行匹配。** 168 | 169 | ![](https://61mon.com/images/illustrations/boyer_moore/17.png) 170 | 171 | 第二个$x$即为移动后的位置。 172 | 173 | **(2)如果不存在和"最长好后缀"完全匹配的子串,则选取长度最长且也属于前缀的那个"真好后缀"。** 174 | 175 | ![](https://61mon.com/images/illustrations/boyer_moore/18.png) 176 | 177 | 第二个$x$即为移动后的位置。 178 | 179 | **(3)如果完全不存在和"好后缀"匹配的子串,则右移整个搜索词。** 180 | 181 | ![](https://61mon.com/images/illustrations/boyer_moore/19.png) 182 | 183 | 第二个$x$即为移动后的位置。 184 | 185 | **代码实现:** 186 | 187 | ```c++ 188 | #define MAX_LENGTH 1000 // 假定字符串长度不超过1000 189 | 190 | /* 计算好后缀数组 */ 191 | void GetGS(string & p, int & m, int gs[]) 192 | { 193 | int suff[MAX_LENGTH]; 194 | Suffixes(p, m, suff); 195 | 196 | // 第三种情况 197 | for (int i = 0; i < m; i++) 198 | gs[i] = m; 199 | 200 | // 第二种情况 201 | int j = 0; 202 | for (int i = m - 2; i >= 0; i--) 203 | { 204 | if (suff[i] == i + 1) 205 | for (; j < m - 1 - i; j++) 206 | gs[j] = m - 1 - i; 207 | } 208 | 209 | // 第一种情况 210 | for (int i = 0; i <= m - 2; i++) 211 | gs[m - 1 - suff[i]] = m - 1 - i; 212 | } 213 | ``` 214 | 215 | 看到这段代码,你可能会有很多的疑问,接下来我一步一步地讲解。 216 | 217 | **疑问 1:Suffixes(p, m, suff) 的作用** 218 | 219 | 假定搜索词为p,长度为m,则数组`suff[i]`定义为:`p[0...i]`与p的最长公共后缀。举个例子: 220 | 221 | | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 222 | | :-----: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 223 | | p[i] | E | L | E | M | E | L | E | 224 | | suff[i] | 1 | 0 | 3 | 0 | 1 | 0 | 7 | 225 | 226 | 那该怎么去求解它呢? 227 | 228 | ![](https://61mon.com/images/illustrations/boyer_moore/20.png) 229 | 230 | 如上图,串p的长度为m,i是当前正准备计算`suff[]`值的那个位置。再设置两个变量,f和g。g代表以f为起始位置的字符匹配成功的最左边界,即"g = 最后一个成功匹配位置 - 1"。 231 | 232 | 当i处于f和g之间并且`suff[m - 1 - (f - i)] < i - g`,根据`suff[f]`,我们可以推断出`suff[i] = suff[m - 1 - (f - i)]`,图中椭圆的长度即为`suff[m - 1 - (f - i)]`。 233 | 234 | ```c++ 235 | /* 计算后缀数组 */ 236 | void Suffixes(string & p, int & m, int suff[]) 237 | { 238 | int f; 239 | int g = m - 1; // 这里 g 必须要大于 m - 2,因为必须保证 i 等于 m - 2 的时候执行 else 语句 240 | suff[m - 1] = m; 241 | for (int i = m - 2; i >= 0; i--) 242 | { 243 | if (i > g && suff[m - 1 - f + i] < i - g) 244 | suff[i] = suff[m - 1 - f + i]; 245 | else 246 | { 247 | if (i < g) 248 | g = i; 249 | f = i; 250 | while (g >= 0 && p[g] == p[m - 1 - f + g]) 251 | g--; 252 | suff[i] = f - g; 253 | } 254 | } 255 | } 256 | ``` 257 | 258 | **疑问 2:第二种情况的代码怎么理解** 259 | 260 | ```c++ 261 | // 第二种情况 262 | int j = 0; 263 | for (int i = m - 2; i >= 0; i--) 264 | { 265 | if (suff[i] == i + 1) 266 | for (; j < m - 1 - i; j++) 267 | gs[j] = m - 1 - i; 268 | } 269 | ``` 270 | 271 | 第二种情况下,即如果不存在和"最长好后缀"完全匹配的子串,则选取长度最长且也属于前缀的那个"真好后缀"。代码为何这么写? 272 | 273 | ![](https://61mon.com/images/illustrations/boyer_moore/21.png) 274 | 275 | 对于代码`if (suff[i] == i + 1)`,如上图,两个椭圆的长度都是`i + 1`,其所表示的字符都对应相等。正如下面浅黑色字所述,若有`if (suff[i] == i + 1)`,因为下标0前面没有字符,故[0, (m-1-i)-1]这部分的都只能找到"真好后缀"。 276 | 277 | 但如果是下面的情况呢? 278 | 279 | ![](https://61mon.com/images/illustrations/boyer_moore/22.png) 280 | 281 | 假设**[i, m-1]是"最长好后缀"**,但在整个串中只能找到"真好后缀"可以匹配,假设图中的两个椭圆互相匹配。那我们不是可以把区间[k, s]移到区间[j, m-1]的位置么?事实并非这么简单,我们观察`k-1`和`j-1`这两个位置的字符,它们是不等的,所以这样的移动毫无意义,因此第二种情况,只需利用`if (suff[i] == i + 1)`就可以找到所有的部分后缀匹配。 282 | 283 | **疑问3:代码的顺序问题** 284 | 285 | ```c++ 286 | // 第三种情况 287 | for (int i = 0; i < m; i++) 288 | gs[i] = m; 289 | 290 | // 第二种情况 291 | int j = 0; 292 | for (int i = m - 2; i >= 0; i--) 293 | { 294 | if (suff[i] == i + 1) 295 | for (; j < m - 1 - i; j++) 296 | gs[j] = m - 1 - i; 297 | } 298 | 299 | // 第一种情况 300 | for (int i = 0; i <= m - 2; i++) 301 | gs[m - 1 - suff[i]] = m - 1 - i; 302 | ``` 303 | 304 | 首先要明确我们的目标:获得最精准的`gs[]`。最精准也就是尽可能小,因为如果值稍大的话,搜索词在跳转的时候就可能遗漏一个完全匹配。 305 | 306 | **for循环中的运行顺序**和**三种情况代码的顺序**之所以按如上代码那样安排,都是为了**获得最精准的`gs[]`**。下面我就从这两个方面解释。 307 | 308 | - **for循环中的运行顺序** 309 | - 第三种情况,i从0到m。这很简单,不用解释; 310 | - 第二种情况,i从m-2到0。若$i_1m-1-i_2$,因此需要从后往前算; 311 | - 第一种情况,i从0到m-2。若$suff[i_1]==suff[i_2]$,其中$i_1m-1-i_2$。 312 | - **三种情况代码的顺序** 313 | - 第三种情况放在第一个位置,这很好理解; 314 | - 第二种情况和第一种情况,我们可以举个同时满足这两个情况的例子。假设搜索词为`a....ba......ba`, 读 者自己手算一遍就懂了。 315 | 316 | 317 | 不知道读者有没有发现,第一和第二种情况的代码都没有`i = m - 1`。考虑第二种情况的代码,我们把`i = m - 1`代进去计算,发现程序会直接跳过。再看第一种情况的代码,我们把`i = m - 1`代进去计算,发现`m - 1 - suff[i]`为负,此时下标会溢出。 318 | 319 | ## 四:完整代码 320 | 321 | ```c++ 322 | /** 323 | * 324 | * author : 刘毅(Limer) 325 | * date : 2017-07-26 326 | * mode : C++ 327 | */ 328 | 329 | #include 330 | #include 331 | #include 332 | 333 | #define MAX_CHAR 256 334 | #define MAX_LENGTH 1000 335 | 336 | using namespace std; 337 | 338 | /* 计算坏字符数组 */ 339 | void GetBC(string & p, int & m, int bc[]) 340 | { 341 | for (int i = 0; i < MAX_CHAR; i++) 342 | bc[i] = m; 343 | for (int i = 0; i < m; i++) 344 | bc[p[i]] = m - 1 - i; 345 | } 346 | 347 | /* 计算后缀数组 */ 348 | void Suffixes(string & p, int & m, int suff[]) 349 | { 350 | int f; 351 | int g = m - 1; // 这里 g 必须要大于 m - 2,因为必须保证 i 等于 m - 2 的时候执行 else 语句 352 | suff[m - 1] = m; 353 | for (int i = m - 2; i >= 0; i--) 354 | { 355 | if (i > g && suff[m - 1 - f + i] < i - g) 356 | suff[i] = suff[m - 1 - f + i]; 357 | else 358 | { 359 | if (i < g) 360 | g = i; 361 | f = i; 362 | while (g >= 0 && p[g] == p[m - 1 - f + g]) 363 | g--; 364 | suff[i] = f - g; 365 | } 366 | } 367 | } 368 | 369 | /* 计算好后缀数组 */ 370 | void GetGS(string & p, int & m, int gs[]) 371 | { 372 | int suff[MAX_LENGTH]; 373 | Suffixes(p, m, suff); 374 | 375 | // 第三种情况 376 | for (int i = 0; i < m; i++) 377 | gs[i] = m; 378 | 379 | // 第二种情况 380 | int j = 0; 381 | for (int i = m - 2; i >= 0; i--) 382 | { 383 | if (suff[i] == i + 1) 384 | for (; j < m - 1 - i; j++) 385 | gs[j] = m - 1 - i; 386 | } 387 | 388 | // 第一种情况 389 | for (int i = 0; i <= m - 2; i++) 390 | gs[m - 1 - suff[i]] = m - 1 - i; 391 | } 392 | 393 | void BM(string & s, int & n, string & p, int & m) 394 | { 395 | int bc[MAX_LENGTH]; 396 | int gs[MAX_LENGTH]; 397 | GetBC(p, m, bc); 398 | GetGS(p, m, gs); 399 | 400 | int j = 0, i; 401 | while (j <= n - m) 402 | { 403 | for (i = m - 1; i >= 0 && p[i] == s[i + j]; i--) 404 | ; 405 | if (i < 0) 406 | { 407 | cout << "在下标 " << j << " 位置找到匹配\n"; 408 | j += gs[0]; 409 | } 410 | else 411 | j += max(bc[s[i + j]] - m + 1 + i, gs[i]); 412 | } 413 | 414 | // PS: 匹配失败不作处理 415 | } 416 | 417 | int main() 418 | { 419 | string s, p; 420 | int n, m; 421 | 422 | while (cin >> s >> p) 423 | { 424 | n = s.size(); 425 | m = p.size(); 426 | BM(s, n, p, m); 427 | cout << endl; 428 | } 429 | 430 | return 0; 431 | } 432 | ``` 433 | 434 | 数据测试如下: 435 | 436 | ![](https://61mon.com/images/illustrations/boyer_moore/23.png) 437 | -------------------------------------------------------------------------------- /201~300/211-递归(2):高级.md: -------------------------------------------------------------------------------- 1 | >系列文章目录 2 | > 3 | >[递归(1):基础](https://61mon.com/index.php/archives/208/) 4 | >递归(2):高级 5 | 6 | ## 一:递归的方式 7 | 8 | 递归的方式分为两个:**自底向上和自顶向下**。 9 | 10 | 我们以打印单链表为例: 11 | 12 | 13 | 14 | 15 | 16 | ```c++ 17 | /* 自底向上 */ 18 | void Print(Node * node) 19 | { 20 | if (node == nullptr) 21 | return; 22 | Print(node->next); 23 | cout << node->data << " "; 24 | } 25 | 26 | /* 自顶向下 */ 27 | void Print(Node * node) 28 | { 29 | if (node == nullptr) 30 | return; 31 | cout << node->data << " "; 32 | Print(node->next); 33 | } 34 | ``` 35 | 36 | 前者逆序打印链表,后者正序打印链表。 37 | 38 | 形象点,我们可以用“吃冰糖葫芦”来描述上面的两种方式: 39 | 40 | ![](https://61mon.com/images/illustrations/Recursion/5.jpg) 41 | 42 | 自底向上就是从下面往上面吃;自顶向下就是从上面往下面吃。 43 | 44 | 那这两种方式有何不同呢? 45 | 46 | - **处理数据的顺序:**正如上面打印链表的例子,一个正序,一个逆序; 47 | 48 | - **空间内存的消耗:**这在接下来对“尾调用”的讲解会提及; 49 | 50 | - **运行时间的消耗:**这其实是由处理数据的顺序导致的,我能找到的一个例子就是[有序链表转化为平衡的二分查找树](https://61mon.com/index.php/archives/191/#menu_index_21)。 51 | 52 | 因此,在写递归程序的时候,**自底向上和自顶向下**这两个方式都是我们应该考虑在内的。 53 | 54 | ## 二:尾递归 55 | 56 | 在介绍**尾递归**前,需要先理解尾调用。(以下摘自阮一峰的[尾调用优化](http://www.ruanyifeng.com/blog/2015/04/tail-call.html),并作稍微修改) 57 | 58 | 它的概念非常简单,就是指某个函数的最后一步是调用另一个函数。 59 | 60 | ```c++ 61 | TypeName f() 62 | { 63 | return g(); 64 | } 65 | ``` 66 | 67 | 以下两种情况都不属于尾调用。 68 | 69 | ```c++ 70 | /* case 1 */ 71 | TypeName f() 72 | { 73 | TypeName t = g(); 74 | return t; 75 | } 76 | 77 | /* case 2 */ 78 | TypeName f() 79 | { 80 | return 1 + g(x); 81 | } 82 | ``` 83 | 84 | 尾调用不一定出现在函数尾部,只要是最后一步操作即可。 85 | 86 | ```c++ 87 | TypeName f() 88 | { 89 | if (x > 0) 90 | return g(); 91 | return h(); 92 | } 93 | ``` 94 | 95 | 尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 96 | 97 | 我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个["调用栈"](http://zh.wikipedia.org/wiki/%E8%B0%83%E7%94%A8%E6%A0%88)(call stack)。 98 | 99 | ![](https://61mon.com/images/illustrations/Recursion/6.png) 100 | 101 | 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。 102 | 103 | ```c++ 104 | TypeName f() 105 | { 106 | int m = 1; 107 | int n = 2; 108 | return g(m + n); 109 | } 110 | ``` 111 | 112 | 上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g() 的调用记录。 113 | 114 | 这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。 115 | 116 | 而**尾递归**就是函数尾调用自身。 117 | 118 | 递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。 119 | 120 | ```c++ 121 | int Factorial(int n) 122 | { 123 | if (n == 0) 124 | return 1; 125 | return n * Factorial(n - 1); 126 | } 127 | ``` 128 | 129 | 上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,空间复杂度 $O(n)$ 。 130 | 131 | 如果改写成尾递归,只保留一个调用记录,空间复杂度 $O(1)$ 。 132 | 133 | ```c++ 134 | int Factorial(int n, int total) 135 | { 136 | if (n == 0) 137 | return 1; 138 | return Factorial(n - 1, n * total); 139 | } 140 | ``` 141 | 142 | 由此可见,"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。 143 | 144 | 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1? 145 | 146 | 有个方法可以解决这个问题。就是在尾递归函数之外,再提供一个正常形式的函数。 147 | 148 | ```c++ 149 | int Factorial(int n, int total) 150 | { 151 | if (n == 0) 152 | return 1; 153 | return Factorial(n - 1, n * total); 154 | } 155 | 156 | int Factorial(int n) 157 | { 158 | return Factorial(n , 1); 159 | } 160 | 161 | /* you can use it like this */ 162 | Factorial(5); 163 | ``` 164 | -------------------------------------------------------------------------------- /201~300/212-BFS和DFS.md: -------------------------------------------------------------------------------- 1 | 我们首次接触BFS和DFS时,应该是在数据结构课上讲的“图的遍历”。它们的实现都很简单,这里我就不哆嗦去贴代码了。我觉得读者更想知道的是,这两者“遍历”的序列到底有何差别? 2 | 3 | 那本篇文章就单纯来讲讲它们的区别和各自的应用,不会涉及任何代码。 4 | 5 | **广度优先搜索算法(Breadth-First-Search,缩写为BFS)**,是一种利用**队列**实现的搜索算法。简单来说,其搜索过程和“湖面丢进一块石头激起层层涟漪”类似。 6 | 7 | **深度优先搜索算法(Depth-First-Search,缩写为DFS)**,是一种利用**递归**实现的搜索算法。简单来说,其搜索过程和“不撞南墙不回头”类似。 8 | 9 | **BFS的重点在于队列,而DFS的重点在于递归。这是它们的本质区别。** 10 | 11 | 举个典型例子,如下图,灰色代表墙壁,绿色代表起点,红色代表终点,规定每次只能走一步,且只能往下或右走。求一条绿色到红色的最短路径。 12 | 13 | 14 | 15 | 16 | 17 | ![](https://61mon.com/images/illustrations/bfs_and_dfs/1.png) 18 | 19 | 对于上面的问题,BFS和DFS都可以求出结果,它们的区别就是在复杂度上存在差异。我可以先告诉你,该题BFS是较佳算法。 20 | 21 | ### BFS 22 | 23 | ![](https://61mon.com/images/illustrations/bfs_and_dfs/2.gif) 24 | 25 | 如上图所示,从起点出发,对于每次出队列的点,都要遍历其四周的点。所以说BFS的搜索过程和“湖面丢进一块石头激起层层涟漪”很相似,此即“广度优先搜索算法”中“广度”的由来。 26 | 27 | ### DFS 28 | 29 | ![](https://61mon.com/images/illustrations/bfs_and_dfs/3.gif) 30 | 31 | 如上图所示,从起点出发,先把一个方向的点都遍历完才会改变方向......所以说,DFS的搜索过程和“不撞南墙不回头”很相似,此即“深度优先搜索算法”中“深度”的由来。 32 | 33 | ### 总结 34 | 35 | 现在,你不妨对照着图,再去看看你打印出的遍历序列,是不是一目了然呢? 36 | 37 | 最后我再说下它们的应用方向。 38 | 39 | BFS常用于找单一的最短路线,它的特点是"搜到就是最优解",而DFS用于找所有解的问题,它的空间效率高,而且找到的不一定是最优解,必须记录并完成整个搜索,故一般情况下,深搜需要非常高效的剪枝(剪枝的概念请百度)。 40 | 41 | PS:BFS和DFS是很重要的算法,仅凭一文就将其解释清楚,是不可能的,读者如果想要更深入地了解它们,建议去OJ上找一些相关赛题训练下,一定会给你一个别样的天地。 42 | -------------------------------------------------------------------------------- /201~300/213-Sunday算法.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | Sunday算法是Daniel M.Sunday于1990年提出的字符串模式匹配。其效率在匹配随机的字符串时比其他匹配算法还要更快。Sunday算法的实现可比KMP,BM的实现容易太多。 4 | 5 | 6 | 7 | 8 | 9 | ## 二:算法过程 10 | 11 | 假定主串为 "HERE IS A SIMPLE EXAMPLE",模式串为 "EXAMPLE"。 12 | 13 | (1) 14 | 15 | ![](https://61mon.com/images/illustrations/sunday/1.png) 16 | 17 | 从头部开始比较,发现不匹配。则Sunday算法要求如下:找到主串中位于模式串后面的第一个字符,即红色箭头所指的“空格”,再在模式串中从后往前找“空格”,没有找到,则直接把模式串移到“空格”的后面。 18 | 19 | (2) 20 | 21 | ![](https://61mon.com/images/illustrations/sunday/2.png) 22 | 23 | 依旧从头部开始比较,发现不匹配。找到主串中位于模式串后面的第一个字符“L”,模式串中存在“L”,则移动模式串使两个“L”对齐。 24 | 25 | 26 | (3) 27 | 28 | ![](https://61mon.com/images/illustrations/sunday/3.png) 29 | 30 | 找到匹配。 31 | 32 | ## 三:完整代码 33 | 34 | ```c++ 35 | /** 36 | * 37 | * author : 刘毅(Limer) 38 | * date : 2017-07-30 39 | * mode : C++ 40 | */ 41 | 42 | #include 43 | #include 44 | 45 | #define MAX_CHAR 256 46 | #define MAX_LENGTH 1000 47 | 48 | using namespace std; 49 | 50 | void GetNext(string & p, int & m, int next[]) 51 | { 52 | for (int i = 0; i < MAX_CHAR; i++) 53 | next[i] = -1; 54 | for (int i = 0; i < m; i++) 55 | next[p[i]] = i; 56 | } 57 | 58 | void Sunday(string & s, int & n, string & p, int & m) 59 | { 60 | int next[MAX_CHAR]; 61 | GetNext(p, m, next); 62 | 63 | int j; // s 的下标 64 | int k; // p 的下标 65 | int i = 0; 66 | while (i <= n - m) 67 | { 68 | j = i; 69 | k = 0; 70 | while (j < n && k < m && s[j] == p[k]) 71 | j++, k++; 72 | 73 | if (k == m) 74 | cout << "在" << i << " 处找到匹配\n"; 75 | 76 | if (i + m < n) 77 | i += (m - next[s[i + m]]); 78 | else 79 | return; 80 | } 81 | 82 | // PS: 匹配失败不作处理 83 | } 84 | 85 | int main() 86 | { 87 | string s, p; 88 | int n, m; 89 | 90 | while (cin >> s >> p) 91 | { 92 | n = s.size(); 93 | m = p.size(); 94 | Sunday(s, n, p, m); 95 | cout << endl; 96 | } 97 | 98 | return 0; 99 | } 100 | ``` 101 | 102 | 数据测试如下图: 103 | 104 | ![](https://61mon.com/images/illustrations/sunday/4.png) 105 | -------------------------------------------------------------------------------- /201~300/214-Sparse Table算法.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | Sparse Table算法(以下简称ST算法)是用来解决以下问题的, 4 | 5 | **区间最值查询(Range Minimum/Maximum Query,简称RMQ问题)**:对于长度为n的数组`array[]`,回答若干询问`RMQ(array, i, j)`$(0 ≤ i, j < n)$,返回数组`array[]`中下标在i,j之间的最小或最大值。 6 | 7 | 找一个区间最值,最简单的直接比较,复杂度是$O(n)$,所以如果查找次数很少,用ST算法没有意义。ST算法的应用场景就是要对一个数串查询多次的情况。 8 | 9 | 算法的基本思想就是对串中所有可能的区间组合的最值用二维数组保存,也就是所谓的预处理,查询时直接通过数组下标获取,$O(1)$的时间。下面采用动态规划来对数串进行预处理,也就是填充二维数组。 10 | 11 | 12 | 13 | 14 | 15 | ## 二:算法过程分析 16 | 17 | 我们以区间最大值查询为例。 18 | 19 | 设$f[i][j]$表示数组$array[]$在区间$[i,i+2^j-1]$的最大值。(此即DP的状态,区间长度正好为$2^j$) 20 | 21 | 很容易发现对于$0≤i 66 | #include 67 | #include 68 | 69 | using namespace std; 70 | 71 | const int ROW = 1000; 72 | const int COLUMN = 10; // log(1000)/log(2.0) ~ 9.96 73 | int f[ROW][COLUMN]; 74 | 75 | void RMQ(int array[], int n) 76 | { 77 | for (int i = 0; i < n; i++) 78 | f[i][0] = array[i]; 79 | 80 | int k = log(n) / log(2.0); 81 | for (int j = 1; j <= k; j++) 82 | for (int i = 0; i < n; i++) 83 | if (i + (1 << j) - 1 < n) 84 | f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); 85 | } 86 | 87 | int main() 88 | { 89 | int array[] = { 3,2,4,5,6,8,1,2,9,7 }; 90 | int n = sizeof(array) / sizeof(int); 91 | 92 | RMQ(array, n); 93 | 94 | cout << "数组的下标范围为:0 -- " << n - 1 << ","; 95 | cout << "请输入需要查询的区间: \n"; 96 | int i, j; 97 | while (cin >> i >> j) 98 | { 99 | int k = log(j - i + 1.0) / log(2.0); 100 | int max_ans = max(f[i][k], f[j - (1 << k) + 1][k]); 101 | cout << "最大值是: " << max_ans << endl << endl; 102 | } 103 | return 0; 104 | } 105 | ``` 106 | 107 | 运行截图如下: 108 | 109 | ![](https://61mon.com/images/illustrations/sparse_table/2.png) 110 | 111 | ## 四:总结 112 | 113 | 整体来说,ST算法的时间复杂度为$O(nlogn+Q)$,其中$Q$为查询次数。 114 | 115 | 当然对于RMQ问题,不止ST一个方法,线段树也可以,类似问题在ACM竞赛较为常见,有兴趣进一步深入的朋友可以去OJ找相关题源。 116 | -------------------------------------------------------------------------------- /201~300/215-字典树(1):字典树入门.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | 字典树,又称前缀树(英文名:Trie Tree),为Edward Fredkin发明。 4 | 5 | 举个例子,给出一些单词,(and,as,at,cn,com),则其字典树如下: 6 | 7 | ![](https://61mon.com/images/illustrations/trie_tree/1.png) 8 | 9 | 10 | 11 | 12 | 13 | 从上图可以发现,它有3个基本性质: 14 | 15 | 1. 根结点不包含字符,除根结点外每一个结点都只包含一个字符。 16 | 2. 从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串。 17 | 3. 每个结点的所有子结点包含的字符都不相同。 18 | 19 | 字典树是一个很重要的数据结构,其主要应用为: 20 | 21 | 1. **词频统计**:例如,给定一个由10万个单词组成的库,现要你判断一个单词是否有在库中出现,若出现,求出共出现多少次。 22 | 2. **前缀匹配**:以上图为例,如果想获取所有以"a"开头的字符串,那么从图中可以很明显的看到是:(and,as,at)。因此利用这个特性,可以巧妙地实现搜索提示功能,如输入一个网址,可以自动列出可能的选择。当没有完全匹配的搜索结果,可以列出前缀最相似的可能。 23 | 24 | ## 二:算法过程分析 25 | 26 | 共实现三个对外接口: 27 | 28 | 1. `void add(char * s);`:将字符串s添加至字典树; 29 | 2. `int query(char * s);`:查询字符串s是否存在。若存在,返回其存在的次数;若不存在,返回0; 30 | 3. `bool remove(char * s);`:删除字符串s。字符串s若存在,则直接删除,返回真;若不存在,则返回假。 31 | 32 | 结点结构如下: 33 | 34 | ```c++ 35 | #define TREE_WIDTH 26 36 | 37 | struct Node 38 | { 39 | int path; 40 | int end; 41 | char ch; 42 | Node * next[TREE_WIDTH]; 43 | Node(char ch = ' ') 44 | { 45 | this->ch = ch; 46 | this->path = this->end = 0; 47 | for (int i = 0; i < TREE_WIDTH; i++) 48 | this->next[i] = nullptr; 49 | } 50 | }; 51 | ``` 52 | 53 | 本文只讨论**小写26个英文字母**的字典集合,故`TREE_WIDTH`设为26。 54 | 55 | 变量`end`的作用,标记该结点是否是单词结尾。变量`path`则用来记录结点被路径覆盖的次数。 56 | 57 | 请注意,下述代码针对的是动态数据集,即一系列添加,查询,删除三种混合操作。因此对于已无路径覆盖的结点,我们并不会释放其内存,这也是引入`path`的原因(当`path = 0`,表示该结点已无路径覆盖)。 58 | 59 | 两个变量的具体使用如下图: 60 | 61 | ![](https://61mon.com/images/illustrations/trie_tree/2.png) 62 | 63 | ## 三:完整代码 64 | 65 | ```c++ 66 | /** 67 | * 68 | * author : 刘毅(Limer) 69 | * date : 2017-08-08 70 | * mode : C++ 71 | */ 72 | 73 | #include 74 | 75 | #define TREE_WIDTH 26 76 | 77 | using namespace std; 78 | 79 | struct Node 80 | { 81 | int path; 82 | int end; 83 | char ch; 84 | Node * next[TREE_WIDTH]; 85 | Node(char ch = ' ') 86 | { 87 | this->ch = ch; 88 | this->path = this->end = 0; 89 | for (int i = 0; i < TREE_WIDTH; i++) 90 | this->next[i] = nullptr; 91 | } 92 | }; 93 | 94 | class TrieTree 95 | { 96 | private: 97 | Node * root; 98 | public: 99 | TrieTree(); 100 | ~TrieTree(); 101 | void destroy(Node * t); 102 | void add(char * s); 103 | int query(char * s); 104 | bool remove(char * s); 105 | }; 106 | 107 | TrieTree::TrieTree() 108 | { 109 | root = new Node; 110 | } 111 | 112 | TrieTree::~TrieTree() 113 | { 114 | destroy(root); 115 | } 116 | 117 | void TrieTree::destroy(Node * t) 118 | { 119 | for (int i = 0; i < TREE_WIDTH; i++) 120 | if (t->next[i]) 121 | destroy(t->next[i]); 122 | delete t; 123 | } 124 | 125 | void TrieTree::add(char * s) 126 | { 127 | Node * t = root; 128 | while (*s) 129 | { 130 | if (t->next[*s - 'a'] == nullptr) 131 | t->next[*s - 'a'] = new Node(*s); 132 | t->next[*s - 'a']->path++; 133 | t = t->next[*s - 'a']; 134 | s++; 135 | } 136 | t->end++; 137 | } 138 | 139 | int TrieTree::query(char * s) 140 | { 141 | Node * t = root; 142 | while (*s) 143 | { 144 | if (t->next[*s - 'a'] == nullptr || t->next[*s - 'a']->path == 0) 145 | return 0; 146 | t = t->next[*s - 'a']; 147 | s++; 148 | } 149 | return t->end; 150 | } 151 | 152 | bool TrieTree::remove(char * s) 153 | { 154 | if (query(s)) 155 | { 156 | Node * t = root; 157 | while (*s) 158 | { 159 | t->next[*s - 'a']->path--; 160 | t = t->next[*s - 'a']; 161 | s++; 162 | } 163 | t->end--; 164 | return true; 165 | } 166 | 167 | return false; 168 | } 169 | 170 | int main() 171 | { 172 | TrieTree tree; 173 | 174 | tree.add("strawberry"); 175 | tree.add("grandfather"); 176 | tree.add("policeman"); 177 | tree.add("breakfast"); 178 | tree.add("mutton"); 179 | tree.add("bus"); 180 | tree.add("bus"); 181 | tree.add("bustop"); 182 | tree.add("computer"); 183 | 184 | // test "query" 185 | cout << tree.query("bud") << endl; // 0 186 | cout << tree.query("bus") << endl; // 2 187 | 188 | // test "remove" 189 | tree.remove("bustop"); 190 | cout << tree.query("bustop") << endl; // 0 191 | tree.remove("bus"); 192 | cout << tree.query("bus") << endl; // 1 193 | tree.remove("bus"); 194 | cout << tree.query("bus") << endl; // 0 195 | 196 | return 0; 197 | } 198 | ``` 199 | 200 | ## 四:不足及改进 201 | 202 | 我们发现,每个结点,其内部都有一个指针数组,在稀疏树下,大多空间被浪费。 203 | 204 | 因此针对上面问题,人们提出了两种改进结构:**二数组字典树**和**三数组字典树**。具体可阅本系列第二篇。 205 | -------------------------------------------------------------------------------- /201~300/216-Aho-Corasick算法.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | Aho–Corasick算法(也称AC算法,AC自动机)是由Alfred V. Aho和Margaret J.Corasick 发明的字符串搜索算法,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。 4 | 5 | 一个典型应用就是:给出$k$个单词,再给出一段文章(长度是$n$),让你找出有多少个单词在文章里出现过。 6 | 7 | 与其它模式串匹配不同,KMP算法是单模式串的匹配算法,AC算法是多模式串的匹配算法,匹配所需的时间复杂度是$O(n)$。 8 | 9 | AC算法建立在字典树基础上,如果您还不了解字典树,可以参考[字典树入门](https://61mon.com/index.php/archives/215/)。 10 | 11 | 12 | 13 | 14 | 15 | ## 二:算法过程分析 16 | 17 | 以上述所说的典型应用为例,现给定3个单词{"china", "hit", "use"},再给定一段文本"chitchat",求有多少个单词出现在文本中。 18 | 19 | (1) 20 | 21 | ![](https://61mon.com/images/illustrations/aho_corasick/1.png) 22 | 23 | 根据单词{"china", "hit", "use"}建立字典树。 24 | 25 | (2) 26 | 27 | ![](https://61mon.com/images/illustrations/aho_corasick/2.png) 28 | 29 | 根据所给文本“chitchat”依次匹配,图中所示“chi”为匹配成功的字符串。 30 | 31 | (3) 32 | 33 | ![](https://61mon.com/images/illustrations/aho_corasick/3.png) 34 | 35 | 当匹配到第四个字符时,“t”和“n”匹配失败。 36 | 37 | (4) 38 | 39 | ![](https://61mon.com/images/illustrations/aho_corasick/4.png) 40 | 41 | 我们此时是知道已匹配成功的字符串的,即“chi”。 42 | 43 | AC算法的核心就是**在所有给定的单词中,找到这样的一个单词,使其与已匹配成功字符串的相同前后缀最长,利用这个最长的相同前后缀实现搜索跳转。**如上图,单词“hit”与已匹配成功字符串“chi”的最长相同前后缀为“hi”,因此下一步从单词“hit”的“t”开始搜索。 44 | 45 | (4) 46 | 47 | ![](https://61mon.com/images/illustrations/aho_corasick/5.png) 48 | 49 | 此时“t”是匹配的,在文本“chitchat”中找到一个单词“hit”。 50 | 51 | 其实到这里,AC算法的思想已经基本呈现在大家面前了。剩下的问题就是如何解决第(3)步所述的“核心”。 52 | 53 | #### AC算法核心 54 | 55 | 在每个结点里设置一个指针(我们称之为fail指针),指向跳转的位置。 56 | 57 | ![](https://61mon.com/images/illustrations/aho_corasick/6.png) 58 | 59 | 对于跳转位置的选择,基于以下两点: 60 | 61 | 1. 对于根结点的所有儿子结点,它们的fail指针指向根结点; 62 | 2. 而对于其它结点,不妨设该结点上的字符为`ch`,沿着它的父亲结点的fail指针走,直到走到一个结点,它的儿子中也有字符为`ch`的结点,然后把该结点的fail指针指向那个字符为`ch`的结点。如果一直走到了根结点都没找到,那就把fail指针指向根结点。 63 | 64 | 对于第2点,初学者可能有点不理解,我这里稍微解释下。请仔细阅读上面的第(3)步过程,利用父亲结点的最长相同前后缀来找到儿子的最长相同前后缀。 65 | 66 | ## 三:完整代码 67 | 68 | ```c++ 69 | /** 70 | * 71 | * author : 刘毅(Limer) 72 | * date : 2017-08-10 73 | * mode : C++ 74 | */ 75 | 76 | #include 77 | #include 78 | 79 | #define TREE_WIDTH 26 80 | 81 | using namespace std; 82 | 83 | struct Node 84 | { 85 | int end; 86 | Node * fail; 87 | Node * next[TREE_WIDTH]; 88 | Node() 89 | { 90 | this->end = 0; 91 | this->fail = nullptr; 92 | for (int i = 0; i < TREE_WIDTH; i++) 93 | this->next[i] = nullptr; 94 | } 95 | }; 96 | 97 | class AC 98 | { 99 | private: 100 | Node * root; 101 | public: 102 | AC(); 103 | ~AC(); 104 | void destroy(Node * t); 105 | void add(char * s); 106 | void build_fail_pointer(); 107 | int ac_automaton(char * t); 108 | }; 109 | 110 | AC::AC() 111 | { 112 | root = new Node; 113 | } 114 | 115 | AC::~AC() 116 | { 117 | destroy(root); 118 | } 119 | 120 | void AC::destroy(Node * t) 121 | { 122 | for (int i = 0; i < TREE_WIDTH; i++) 123 | if (t->next[i]) 124 | destroy(t->next[i]); 125 | delete t; 126 | } 127 | 128 | void AC::add(char * s) 129 | { 130 | Node * t = root; 131 | while (*s) 132 | { 133 | if (t->next[*s - 'a'] == nullptr) 134 | t->next[*s - 'a'] = new Node; 135 | t = t->next[*s - 'a']; 136 | s++; 137 | } 138 | t->end++; // 假设单词可重复 139 | } 140 | 141 | void AC::build_fail_pointer() 142 | { 143 | queue Q; 144 | 145 | for (int i = 0; i < TREE_WIDTH; i++) 146 | { 147 | if (root->next[i]) 148 | { 149 | Q.push(root->next[i]); 150 | root->next[i]->fail = root; 151 | } 152 | } 153 | 154 | Node * parent = nullptr; 155 | Node * son = nullptr; 156 | Node * p = nullptr; 157 | while (!Q.empty()) 158 | { 159 | parent = Q.front(); 160 | Q.pop(); 161 | for (int i = 0; i < TREE_WIDTH; i++) 162 | { 163 | if (parent->next[i]) 164 | { 165 | Q.push(parent->next[i]); 166 | son = parent->next[i]; 167 | p = parent->fail; 168 | while (p) 169 | { 170 | if (p->next[i]) 171 | { 172 | son->fail = p->next[i]; 173 | break; 174 | } 175 | p = p->fail; 176 | } 177 | if (!p) son->fail = root; 178 | } 179 | } 180 | } 181 | } 182 | 183 | int AC::ac_automaton(char * t) 184 | { 185 | int ans = 0; 186 | 187 | int pos; 188 | Node * pre = root; 189 | Node * cur = nullptr; 190 | while (*t) 191 | { 192 | pos = *t - 'a'; 193 | if (pre->next[pos]) 194 | { 195 | cur = pre->next[pos]; 196 | while (cur != root) 197 | { 198 | if (cur->end >= 0) 199 | { 200 | ans += cur->end; 201 | cur->end = -1; // 避免重复查找 202 | } 203 | else 204 | break; // 等于 -1 说明以前这条路径已找过,现在无需再找 205 | cur = cur->fail; 206 | } 207 | pre = pre->next[pos]; 208 | t++; 209 | } 210 | else 211 | { 212 | if (pre == root) 213 | t++; 214 | else 215 | pre = pre->fail; 216 | } 217 | } 218 | return ans; 219 | } 220 | 221 | int main() 222 | { 223 | int n; 224 | char s[1000]; 225 | while (1) 226 | { 227 | cout << "请输入单词个数:"; 228 | cin >> n; 229 | 230 | AC tree; 231 | cout << "请输入" << n << "个单词:\n"; 232 | while (n--) 233 | { 234 | cin >> s; 235 | tree.add(s); 236 | } 237 | 238 | cout << "请输入搜索文本:"; 239 | cin >> s; 240 | 241 | tree.build_fail_pointer(); 242 | cout << "共有" << tree.ac_automaton(s) << "个单词匹配" << endl << endl; 243 | } 244 | return 0; 245 | } 246 | ``` 247 | 248 | 运行截图如下: 249 | 250 | ![](https://61mon.com/images/illustrations/aho_corasick/7.png) 251 | -------------------------------------------------------------------------------- /201~300/217-二叉查找树.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | 二叉查找树(Binary Search Tree,简称BST),也称二叉搜索树、有序二叉树、排序二叉树,是指一棵空树或者具有下列性质的二叉树: 4 | 5 | 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 6 | 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; 7 | 3. 任意节点的左、右子树也分别为二叉查找树; 8 | 4. 没有键值相等的节点。 9 | 10 | 11 | 12 | 13 | 14 | ![](https://61mon.com/images/illustrations/binary_search_tree/1.png) 15 | 16 | 二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低,为$O(logn)$。 17 | 18 | ## 二:具体实现与代码分析 19 | 20 | 首先大体看下结点和类的结构: 21 | 22 | ```c++ 23 | struct Node 24 | { 25 | int key; 26 | Node * left; 27 | Node * right; 28 | Node(int key) 29 | { 30 | this->key = key; 31 | this->left = this->right = nullptr; 32 | } 33 | }; 34 | 35 | class BST 36 | { 37 | private: 38 | Node * header; 39 | private: 40 | void destroy(Node * node); 41 | Node * insert_real(int key, Node *& node); 42 | Node *& find_real(int key, Node *& node); 43 | void in_order(Node * node); 44 | public: 45 | BST(); 46 | ~BST(); 47 | Node * insert(int key); 48 | Node * find(int key); 49 | void erase(int key); 50 | void print(); 51 | }; 52 | ``` 53 | 54 | 其中,`header`结点并非根结点,而是我在实现上的一个技巧,`header->left`指向的才是根结点。 55 | 56 | ### 2.1 插入操作 57 | 58 | 思路很简单,但有一点需要注意,在插入结点以后,需要把它父亲结点的孩子指针指向它,因此这里我们使用了指针引用。 59 | 60 | ```c++ 61 | Node * BST::insert_real(int key, Node *& node) 62 | { 63 | if (node == nullptr) 64 | return node = new Node(key); 65 | 66 | if (key < node->key) 67 | return insert_real(key, node->left); 68 | else if (key > node->key) 69 | return insert_real(key, node->right); 70 | else 71 | return nullptr; 72 | } 73 | 74 | Node * BST::insert(int key) 75 | { 76 | return insert_real(key, header->left); 77 | } 78 | ``` 79 | 80 | ### 2.2 查找 81 | 82 | 思路依旧很简单,只需注意的是`find_real()`返回的是指针引用,之所以这么做,是因为在接下来的删除操作中有妙用。 83 | 84 | ```c++ 85 | Node *& BST::find_real(int key, Node *& node) 86 | { 87 | if (node == nullptr) 88 | return node; 89 | 90 | if (key < node->key) 91 | return find_real(key, node->left); 92 | else if (key > node->key) 93 | return find_real(key, node->right); 94 | else 95 | return node; 96 | } 97 | 98 | Node * BST::find(int key) 99 | { 100 | return find_real(key, header->left); 101 | } 102 | ``` 103 | 104 | ### 2.3 删除 105 | 106 | 一个结点的删除有四种情况,如下图,其中红色结点为要被删除的结点: 107 | 108 | ![](https://61mon.com/images/illustrations/binary_search_tree/2.png) 109 | 110 | - 图一,要删除的结点存在左孩子,也存在右孩子; 111 | - 图二,要删除的结点仅存在左孩子; 112 | - 图三,要删除的结点仅存在右孩子; 113 | - 图四,要删除的结点没有孩子。 114 | 115 | 对于图二,图三,图四这三种情况,实现很容易。我们重点说下图一的情况。 116 | 117 | 首先我们要明确一条定理:**二叉查找树的中序遍历序列是升序序列。**因此对于图一的情况,我们采取的策略就是,找到要删除结点(在中序遍历中)的后继,用后继替换要删除的结点(当然用前驱去替换也是可以的)。详情见下图: 118 | 119 | ![](https://61mon.com/images/illustrations/binary_search_tree/3.png) 120 | 121 | 其中,`t`为要删除的结点,`x`为`t`的后继结点,`y`为`x`的父亲结点。 122 | 123 | ```c++ 124 | void BST::erase(int key) 125 | { 126 | Node *& p = find_real(key, header->left); 127 | if (p) 128 | { 129 | Node * t = p; 130 | if (t->left && t->right) 131 | { 132 | // 找到 t 的后继结点 133 | Node * y = t; 134 | Node * x = t->right; 135 | while (x->left) 136 | { 137 | y = x; 138 | x = x->left; 139 | } 140 | 141 | // 将后继结点的右子树接上它的父亲 142 | if (y == t) 143 | y->right = x->right; 144 | else 145 | y->left = x->right; 146 | 147 | // 用后继结点替换要删除的结点 t 148 | p = x; 149 | x->left = t->left; 150 | x->right = t->right; 151 | } 152 | else 153 | p = t->left ? t->left : t->right; 154 | 155 | delete t; 156 | } 157 | } 158 | ``` 159 | 160 | ## 三:完整代码 161 | 162 | ```c++ 163 | /** 164 | * 165 | * author : 刘毅(Limer) 166 | * date : 2017-08-15 167 | * mode : C++ 168 | */ 169 | 170 | #include 171 | #include 172 | #include 173 | 174 | using namespace std; 175 | 176 | struct Node 177 | { 178 | int key; 179 | Node * left; 180 | Node * right; 181 | Node(int key) 182 | { 183 | this->key = key; 184 | this->left = this->right = nullptr; 185 | } 186 | }; 187 | 188 | class BST 189 | { 190 | private: 191 | Node * header; 192 | private: 193 | void destroy(Node * node); 194 | Node * insert_real(int key, Node *& node); 195 | Node *& find_real(int key, Node *& node); 196 | void in_order(Node * node); 197 | public: 198 | BST(); 199 | ~BST(); 200 | Node * insert(int key); 201 | Node * find(int key); 202 | void erase(int key); 203 | void print(); 204 | }; 205 | 206 | BST::BST() 207 | { 208 | header = new Node(0); 209 | } 210 | 211 | BST::~BST() 212 | { 213 | destroy(header->left); 214 | delete header; 215 | header = nullptr; 216 | } 217 | 218 | void BST::destroy(Node * node) 219 | { 220 | if (node == nullptr) 221 | return; 222 | destroy(node->left); 223 | destroy(node->right); 224 | delete node; 225 | } 226 | 227 | Node * BST::insert_real(int key, Node *& node) 228 | { 229 | if (node == nullptr) 230 | return node = new Node(key); 231 | 232 | if (key < node->key) 233 | return insert_real(key, node->left); 234 | else if (key > node->key) 235 | return insert_real(key, node->right); 236 | else 237 | return nullptr; 238 | } 239 | 240 | Node *& BST::find_real(int key, Node *& node) 241 | { 242 | if (node == nullptr) 243 | return node; 244 | 245 | if (key < node->key) 246 | return find_real(key, node->left); 247 | else if (key > node->key) 248 | return find_real(key, node->right); 249 | else 250 | return node; 251 | } 252 | 253 | void BST::in_order(Node * node) 254 | { 255 | if (node == nullptr) 256 | return; 257 | 258 | in_order(node->left); 259 | cout << node->key << " "; 260 | in_order(node->right); 261 | } 262 | 263 | Node * BST::insert(int key) 264 | { 265 | return insert_real(key, header->left); 266 | } 267 | 268 | Node * BST::find(int key) 269 | { 270 | return find_real(key, header->left); 271 | } 272 | 273 | void BST::erase(int key) 274 | { 275 | Node *& p = find_real(key, header->left); 276 | if (p) 277 | { 278 | Node * t = p; 279 | if (t->left && t->right) 280 | { 281 | // 找到 t 的后继结点 282 | Node * y = t; 283 | Node * x = t->right; 284 | while (x->left) 285 | { 286 | y = x; 287 | x = x->left; 288 | } 289 | 290 | // 将后继结点的右子树接上它的父亲 291 | if (y == t) 292 | y->right = x->right; 293 | else 294 | y->left = x->right; 295 | 296 | // 用后继结点替换要删除的结点 t 297 | p = x; 298 | x->left = t->left; 299 | x->right = t->right; 300 | } 301 | else 302 | p = t->left ? t->left : t->right; 303 | 304 | delete t; 305 | } 306 | } 307 | 308 | void BST::print() 309 | { 310 | in_order(header->left); 311 | cout << endl; 312 | } 313 | 314 | int main() 315 | { 316 | BST bst; 317 | 318 | // test "insert" 319 | bst.insert(7); 320 | bst.insert(2); 321 | bst.insert(1); bst.insert(1); 322 | bst.insert(5); 323 | bst.insert(3); 324 | bst.insert(6); 325 | bst.insert(4); 326 | bst.insert(9); 327 | bst.insert(8); 328 | bst.insert(11); bst.insert(11); 329 | bst.insert(10); 330 | bst.insert(12); 331 | bst.print(); // 1 2 3 4 5 6 7 8 9 10 11 12 332 | 333 | // test "find" 334 | Node * p = nullptr; 335 | cout << ((p = bst.find(2)) ? p->key : -1) << endl; // 2 336 | cout << ((p = bst.find(100)) ? p->key : -1) << endl; // -1 337 | 338 | // test "erase" 339 | bst.erase(2); 340 | bst.print(); // 1 3 4 5 6 7 8 9 10 11 12 341 | bst.erase(10); 342 | bst.erase(9); 343 | bst.print(); // 1 3 4 5 6 7 8 11 12 344 | 345 | return 0; 346 | } 347 | ``` 348 | 349 | ## 四:时间复杂度 350 | 351 | 最好情况,二叉查找树同时也是一棵完全二叉树,此时时间复杂度为$O_{best}(logn)$; 352 | 353 | 最差情况,输入的数据正好是升序或降序序列,此时二叉查找树退化成单链表,时间复杂度变为$O_{worst}(n)$; 354 | 355 | 平均情况,时间复杂度为$O_{avg}(1.39logn)$,关于它的证明,读者可以参考[这里](http://cseweb.ucsd.edu/~kube/cls/100/Lectures/lec4/lec4.pdf)。 356 | -------------------------------------------------------------------------------- /201~300/218-AVL树.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | AVL树是一棵平衡的[二叉查找树](https://61mon.com/index.php/archives/217/),于1962年,G. M. Adelson-Velsky和E. M. Landis在他们的论文《An algorithm for the organization of information》中发表。 4 | 5 | 所谓的平衡之意,就是树中任意一个结点下左右两个子树的高度差不超过1。(本文对于树的高度约定为:空结点高度是0,叶子结点高度是1。) 6 | 7 | ![](https://61mon.com/images/illustrations/avl/1.png) 8 | 9 | 那AVL树和普通的二叉查找树有何区别呢?如图,如果我们插入的是一组有序上升或下降的数据,则一棵普通的二叉查找树必然会退化成一个单链表,其查找效率就降为$O(n)$。而AVL树因其平衡的限制,可以始终保持$O(logn)$的时间复杂度。 10 | 11 | 12 | 13 | 14 | 15 | ## 二:具体实现与代码分析 16 | 17 | 在我们进行完插入或删除操作后,很可能会导致某个结点失去平衡,那么我们就需要把失衡结点旋转一下,使其重新恢复平衡。 18 | 19 | 经过分析,不管是插入还是删除,它们都会有四种失衡的情况:左左失衡,右右失衡,左右失衡,右左失衡。因此每次遇到失衡时,我们只需判断一下是哪个失衡,再对其进行相对应的恢复平衡操作即可。 20 | 21 | 好,下面以插入操作为例,来看下这四种失衡的庐山真面目。(以下统一约定:**红色结点为新插入结点,y结点为失衡结点**) 22 | 23 | **1. 左左失衡** 24 | 25 | ![](https://61mon.com/images/illustrations/avl/2.png) 26 | 27 | 所谓的左左,即"失衡结点"的左子树比右子树高2,左孩子下的左子树比右子树高1。 28 | 29 | 我们只需对"以y为根的子树"进行"左左旋转(ll_rotate)"即可。一次旋转后,恢复平衡。 30 | 31 | ```c++ 32 | Node * AVL::ll_rotate(Node * y) 33 | { 34 | Node * x = y->left; 35 | y->left = x->right; 36 | x->right = y; 37 | 38 | y->height = max(get_height(y->left), get_height(y->right)) + 1; 39 | x->height = max(get_height(x->left), get_height(x->right)) + 1; 40 | 41 | return x; 42 | } 43 | ``` 44 | 45 | **2. 右右失衡** 46 | 47 | ![](https://61mon.com/images/illustrations/avl/3.png) 48 | 49 | 所谓的右右,即"失衡结点"的右子树比左子树高2,右孩子下的右子树比左子树高1。 50 | 51 | 我们只需对"以y为根的子树"进行"右右旋转(rr_rotate)"即可。一次旋转后,恢复平衡。 52 | 53 | ```c++ 54 | Node * AVL::rr_rotate(Node * y) 55 | { 56 | Node * x = y->right; 57 | y->right = x->left; 58 | x->left = y; 59 | 60 | y->height = max(get_height(y->left), get_height(y->right)) + 1; 61 | x->height = max(get_height(x->left), get_height(x->right)) + 1; 62 | 63 | return x; 64 | } 65 | ``` 66 | 67 | **3. 左右失衡** 68 | 69 | ![](https://61mon.com/images/illustrations/avl/4.png) 70 | 71 | 所谓的左右,即"失衡结点"的左子树比右子树高2,左孩子下的右子树比左子树高1。 72 | 73 | 观察发现,若先对"以x为根的子树"进行"右右旋转(rr_rotate)",此时"以y为根的子树"恰好符合"左左失衡",所以再进行一次"左左旋转(ll_rotate)"。两次旋转后,恢复平衡。 74 | 75 | ```c++ 76 | Node * AVL::lr_rotate(Node * y) 77 | { 78 | Node * x = y->left; 79 | y->left = rr_rotate(x); 80 | return ll_rotate(y); 81 | } 82 | ``` 83 | 84 | **4. 右左失衡** 85 | 86 | ![](https://61mon.com/images/illustrations/avl/5.png) 87 | 88 | 所谓的右左,即"失衡结点"的右子树比左子树高2,右孩子下的左子树比右子树高1。 89 | 90 | 观察发现,若先对"以x为根的子树"进行"左左旋转(ll_rotate)",此时"以y为根的子树"恰好符合"右右失衡",所以再进行一次"右右旋转(rr_rotate)"。两次旋转后,恢复平衡。 91 | 92 | ```c++ 93 | Node * AVL::rl_rotate(Node * y) 94 | { 95 | Node * x = y->right; 96 | y->right = ll_rotate(x); 97 | return rr_rotate(y); 98 | } 99 | ``` 100 | 101 | ### 2.1 插入操作 102 | 103 | 插入成功后,在递归回溯时依次对经过的结点判断是否失衡,若失衡就需要对其进行对应的旋转操作使其恢复平衡,在这期间,原先作为一棵子树的根结点就会因为旋转被替换,因此设置`insert_real()`返回的是新根结点,这样就可以实时更新根结点。 104 | 105 | 插入操作实现代码如下: 106 | 107 | ```c++ 108 | int AVL::get_height(Node * node) 109 | { 110 | if (node == nullptr) 111 | return 0; 112 | return node->height; 113 | } 114 | 115 | int AVL::get_balance(Node * node) 116 | { 117 | if (node == nullptr) 118 | return 0; 119 | return get_height(node->left) - get_height(node->right); 120 | } 121 | 122 | Node * AVL::insert_real(int key, Node * node) 123 | { 124 | if (node == nullptr) 125 | return new Node(key); 126 | 127 | if (key < node->key) 128 | node->left = insert_real(key, node->left); 129 | else if (key > node->key) 130 | node->right = insert_real(key, node->right); 131 | else 132 | return node; 133 | 134 | node->height = max(get_height(node->left), get_height(node->right)) + 1; 135 | 136 | int balance = get_balance(node); 137 | 138 | // 左左失衡 139 | if (balance > 1 && get_balance(node->left) > 0) 140 | return ll_rotate(node); 141 | 142 | // 右右失衡 143 | if (balance < -1 && get_balance(node->right) < 0) 144 | return rr_rotate(node); 145 | 146 | // 左右失衡 147 | if (balance > 1 && get_balance(node->left) < 0) 148 | return lr_rotate(node); 149 | 150 | // 右左失衡 151 | if (balance < -1 && get_balance(node->right) > 0) 152 | return rl_rotate(node); 153 | 154 | return node; 155 | } 156 | 157 | void AVL::insert(int key) 158 | { 159 | header->left = insert_real(key, header->left); 160 | } 161 | ``` 162 | 163 | ### 2.2 查找操作 164 | 165 | ```c++ 166 | Node * AVL::find_real(int key, Node * node) 167 | { 168 | if (node == nullptr) 169 | return nullptr; 170 | 171 | if (key < node->key) 172 | return find_real(key, node->left); 173 | else if (key > node->key) 174 | return find_real(key, node->right); 175 | else 176 | return node; 177 | } 178 | 179 | Node * AVL::find(int key) 180 | { 181 | return find_real(key, header->left); 182 | } 183 | ``` 184 | 185 | ### 2.3 删除操作 186 | 187 | 删除操作的四种失衡情况和插入操作一样,读者可以参考前文。下面是删除操作的实现代码: 188 | 189 | ```c++ 190 | Node * AVL::erase_real(int key, Node * node) 191 | { 192 | if (node == nullptr) 193 | return node; 194 | 195 | if (key < node->key) 196 | node->left = erase_real(key, node->left); 197 | else if (key > node->key) 198 | node->right = erase_real(key, node->right); 199 | else 200 | { 201 | if (node->left && node->right) 202 | { 203 | // 找到后继结点 204 | Node * x = node->right; 205 | while (x->left) 206 | x = x->left; 207 | 208 | // 后继直接复制 209 | node->key = x->key; 210 | 211 | // 转化为删除后继 212 | node->right = erase_real(x->key, node->right); 213 | } 214 | else 215 | { 216 | Node * t = node; 217 | node = node->left ? node->left : node->right; 218 | delete t; 219 | if (node == nullptr) 220 | return nullptr; 221 | } 222 | } 223 | 224 | node->height = max(get_height(node->left), get_height(node->right)) + 1; 225 | 226 | int balance = get_balance(node); 227 | 228 | // 左左失衡 229 | if (balance > 1 && get_balance(node->left) >= 0) // 需要加等号 230 | return ll_rotate(node); 231 | 232 | // 右右失衡 233 | if (balance < -1 && get_balance(node->right) <= 0) // 需要加等号 234 | return rr_rotate(node); 235 | 236 | // 左右失衡 237 | if (balance > 1 && get_balance(node->left) < 0) 238 | return lr_rotate(node); 239 | 240 | // 右左失衡 241 | if (balance < -1 && get_balance(node->right) > 0) 242 | return rl_rotate(node); 243 | 244 | return node; 245 | } 246 | 247 | void AVL::erase(int key) 248 | { 249 | header->left = erase_real(key, header->left); 250 | } 251 | ``` 252 | 253 | ## 三:完整代码 254 | 255 | ```c++ 256 | /** 257 | * 258 | * author : 刘毅(Limer) 259 | * date : 2017-08-17 260 | * mode : C++ 261 | */ 262 | 263 | #include 264 | #include 265 | 266 | using namespace std; 267 | 268 | struct Node 269 | { 270 | int key; 271 | int height; 272 | Node * left; 273 | Node * right; 274 | Node(int key = 0) 275 | { 276 | this->key = key; 277 | this->height = 1; 278 | this->left = this->right = nullptr; 279 | } 280 | }; 281 | 282 | class AVL 283 | { 284 | private: 285 | Node * header; 286 | private: 287 | Node * ll_rotate(Node * y); 288 | Node * rr_rotate(Node * y); 289 | Node * lr_rotate(Node * y); 290 | Node * rl_rotate(Node * y); 291 | void destroy(Node * node); 292 | int get_height(Node * node); 293 | int get_balance(Node * node); 294 | Node * insert_real(int key, Node * node); 295 | Node * find_real(int key, Node * node); 296 | Node * erase_real(int key, Node * node); 297 | void in_order(Node * node); 298 | public: 299 | AVL(); 300 | ~AVL(); 301 | void insert(int key); 302 | Node * find(int key); 303 | void erase(int key); 304 | void print(); 305 | }; 306 | 307 | Node * AVL::ll_rotate(Node * y) 308 | { 309 | Node * x = y->left; 310 | y->left = x->right; 311 | x->right = y; 312 | 313 | y->height = max(get_height(y->left), get_height(y->right)) + 1; 314 | x->height = max(get_height(x->left), get_height(x->right)) + 1; 315 | 316 | return x; 317 | } 318 | 319 | Node * AVL::rr_rotate(Node * y) 320 | { 321 | Node * x = y->right; 322 | y->right = x->left; 323 | x->left = y; 324 | 325 | y->height = max(get_height(y->left), get_height(y->right)) + 1; 326 | x->height = max(get_height(x->left), get_height(x->right)) + 1; 327 | 328 | return x; 329 | } 330 | 331 | Node * AVL::lr_rotate(Node * y) 332 | { 333 | Node * x = y->left; 334 | y->left = rr_rotate(x); 335 | return ll_rotate(y); 336 | } 337 | 338 | Node * AVL::rl_rotate(Node * y) 339 | { 340 | Node * x = y->right; 341 | y->right = ll_rotate(x); 342 | return rr_rotate(y); 343 | } 344 | 345 | void AVL::destroy(Node * node) 346 | { 347 | if (node == nullptr) 348 | return; 349 | destroy(node->left); 350 | destroy(node->right); 351 | delete node; 352 | } 353 | 354 | int AVL::get_height(Node * node) 355 | { 356 | if (node == nullptr) 357 | return 0; 358 | return node->height; 359 | } 360 | 361 | int AVL::get_balance(Node * node) 362 | { 363 | if (node == nullptr) 364 | return 0; 365 | return get_height(node->left) - get_height(node->right); 366 | } 367 | 368 | Node * AVL::insert_real(int key, Node * node) 369 | { 370 | if (node == nullptr) 371 | return new Node(key); 372 | 373 | if (key < node->key) 374 | node->left = insert_real(key, node->left); 375 | else if (key > node->key) 376 | node->right = insert_real(key, node->right); 377 | else 378 | return node; 379 | 380 | node->height = max(get_height(node->left), get_height(node->right)) + 1; 381 | 382 | int balance = get_balance(node); 383 | 384 | // 左左失衡 385 | if (balance > 1 && get_balance(node->left) > 0) 386 | return ll_rotate(node); 387 | 388 | // 右右失衡 389 | if (balance < -1 && get_balance(node->right) < 0) 390 | return rr_rotate(node); 391 | 392 | // 左右失衡 393 | if (balance > 1 && get_balance(node->left) < 0) 394 | return lr_rotate(node); 395 | 396 | // 右左失衡 397 | if (balance < -1 && get_balance(node->right) > 0) 398 | return rl_rotate(node); 399 | 400 | return node; 401 | } 402 | 403 | Node * AVL::find_real(int key, Node * node) 404 | { 405 | if (node == nullptr) 406 | return nullptr; 407 | 408 | if (key < node->key) 409 | return find_real(key, node->left); 410 | else if (key > node->key) 411 | return find_real(key, node->right); 412 | else 413 | return node; 414 | } 415 | 416 | Node * AVL::erase_real(int key, Node * node) 417 | { 418 | if (node == nullptr) 419 | return node; 420 | 421 | if (key < node->key) 422 | node->left = erase_real(key, node->left); 423 | else if (key > node->key) 424 | node->right = erase_real(key, node->right); 425 | else 426 | { 427 | if (node->left && node->right) 428 | { 429 | // 找到后继结点 430 | Node * x = node->right; 431 | while (x->left) 432 | x = x->left; 433 | 434 | // 后继直接复制 435 | node->key = x->key; 436 | 437 | // 转化为删除后继 438 | node->right = erase_real(x->key, node->right); 439 | } 440 | else 441 | { 442 | Node * t = node; 443 | node = node->left ? node->left : node->right; 444 | delete t; 445 | if (node == nullptr) 446 | return nullptr; 447 | } 448 | } 449 | 450 | node->height = max(get_height(node->left), get_height(node->right)) + 1; 451 | 452 | int balance = get_balance(node); 453 | 454 | // 左左失衡 455 | if (balance > 1 && get_balance(node->left) >= 0) // 需要加等号 456 | return ll_rotate(node); 457 | 458 | // 右右失衡 459 | if (balance < -1 && get_balance(node->right) <= 0) // 需要加等号 460 | return rr_rotate(node); 461 | 462 | // 左右失衡 463 | if (balance > 1 && get_balance(node->left) < 0) 464 | return lr_rotate(node); 465 | 466 | // 右左失衡 467 | if (balance < -1 && get_balance(node->right) > 0) 468 | return rl_rotate(node); 469 | 470 | return node; 471 | } 472 | 473 | void AVL::in_order(Node * node) 474 | { 475 | if (node == nullptr) 476 | return; 477 | 478 | in_order(node->left); 479 | cout << node->key << " "; 480 | in_order(node->right); 481 | } 482 | 483 | AVL::AVL() 484 | { 485 | header = new Node(0); 486 | } 487 | 488 | AVL::~AVL() 489 | { 490 | destroy(header->left); 491 | delete header; 492 | header = nullptr; 493 | } 494 | 495 | void AVL::insert(int key) 496 | { 497 | header->left = insert_real(key, header->left); 498 | } 499 | 500 | Node * AVL::find(int key) 501 | { 502 | return find_real(key, header->left); 503 | } 504 | 505 | void AVL::erase(int key) 506 | { 507 | header->left = erase_real(key, header->left); 508 | } 509 | 510 | void AVL::print() 511 | { 512 | in_order(header->left); 513 | cout << endl; 514 | } 515 | 516 | int main() 517 | { 518 | AVL avl; 519 | 520 | // test "insert" 521 | avl.insert(7); 522 | avl.insert(2); 523 | avl.insert(1); avl.insert(1); 524 | avl.insert(5); 525 | avl.insert(3); 526 | avl.insert(6); 527 | avl.insert(4); 528 | avl.insert(9); 529 | avl.insert(8); 530 | avl.insert(11); avl.insert(11); 531 | avl.insert(10); 532 | avl.insert(12); 533 | avl.print(); // 1 2 3 4 5 6 7 8 9 10 11 12 534 | 535 | // test "find" 536 | Node * p = nullptr; 537 | cout << ((p = avl.find(2)) ? p->key : -1) << endl; // 2 538 | cout << ((p = avl.find(100)) ? p->key : -1) << endl; // -1 539 | 540 | // test "erase" 541 | avl.erase(1); 542 | avl.print(); // 2 3 4 5 6 7 8 9 10 11 12 543 | avl.erase(9); 544 | avl.print(); // 2 3 4 5 6 7 8 10 11 12 545 | avl.erase(11); 546 | avl.print(); // 2 3 4 5 6 7 8 10 12 547 | 548 | return 0; 549 | } 550 | ``` 551 | 552 | 起初构造的AVL树为下图: 553 | 554 | ![](https://61mon.com/images/illustrations/avl/6.png) 555 | 556 | ## 四:总结 557 | 558 | 和二叉查找树相比,AVL树的特点是时间复杂度更稳定,但缺点也是很明显的。 559 | 560 | 插入操作中,**至多**需要一次恢复平衡操作,递归回溯的量级为$O(logn)​$。有一点需要我们注意,在对第一个失衡结点进行恢复平衡后,递归回溯就应该立即停止(因为失衡结点的父亲及其祖先们肯定都是处于平衡状态的),但让"递归的回溯"中途停止,不好实现,所以我上面的编码程序都不可避免的会继续回溯,直到整棵树的根结点,而这些回溯都是没有必要的。(谢谢[LLL](https://61mon.com/index.php/archives/218/comment-page-1#comment-448)的提醒,若在结点中增设父亲结点,就可以解决递归回溯的问题) 561 | 562 | 删除操作中,若存在失衡,则**至少**需要一次恢复平衡操作,递归回溯的量级亦为$O(logn)$。与插入操作不同,当对第一个失衡结点恢复平衡后,它的父亲或者是它的祖先们也可能是非平衡的(见下图,删除1),所以删除操作的回溯很有必要。 563 | 564 | ![](https://61mon.com/images/illustrations/avl/7.png) 565 | 566 | 没有参照物对比的探讨是没有意义的,所以此文就止于此吧,有兴趣的朋友可以看下我后面《红黑树》及《AVL树与红黑树的对比》的文章。 567 | 568 | ## 五:参考文献 569 | 570 | - 维基百科. [AVL树](https://zh.wikipedia.org/wiki/AVL%E6%A0%91). 571 | - GeeksforGeeks. [AVL Tree | Set 1 (Insertion)](http://www.geeksforgeeks.org/avl-tree-set-1-insertion/). 572 | - GeeksforGeeks. [AVL Tree | Set 2 (Deletion)](http://www.geeksforgeeks.org/avl-tree-set-2-deletion/). 573 | -------------------------------------------------------------------------------- /201~300/221-AVL树与红黑树的对比.md: -------------------------------------------------------------------------------- 1 | 前面已经分别介绍了两种平衡二叉树:[AVL树](https://61mon.com/index.php/archives/218/) 和 [红黑树](https://61mon.com/index.php/archives/219/),先让我们来简单回顾下。 2 | 3 | AVL树,规定其任一结点下左右子树高度不超过1。 4 | 5 | 红黑树,规定其必须满足四个性质: 6 | 7 | 1. 每个结点要么是红的,要么是黑的; 8 | 2. 根结点是黑色的; 9 | 3. 如果一个结点是红色的,则它的两个孩子都是黑色的; 10 | 4. 对于任意一个结点,其到叶子结点的每条路径上都包含相同数目的黑色结点。 11 | 12 | 对比之下,我们发现:AVL树可以说是完全平衡的平衡二叉树,因为它的硬性规定就是左右子树高度差不超过1;而红黑树,更准确地说,它应该是"几于平衡"的平衡二叉树,在最坏情况下,红黑相间的路径长度是全黑路径长度的2倍。 13 | 14 | 有趣的是,某些底层数据结构(如Linux, STL ......)选用的都是红黑树而非AVL树,这是为何? 15 | 16 | 1. 对于AVL树,在插入或删除操作后,都要利用递归的回溯,去维护从被删结点到根结点这条路径上的所有结点的平衡性,回溯的量级是需要$O(logn)$的,其中插入操作最多需要两次旋转,删除操作可能是1次、2次或2次以上。而红黑树在insert_rebalance的时候最多需要2次旋转,在erase_rebalance的时候最多也只需要3次旋转。 17 | 2. 其次,AVL树的结构相较红黑树来说更为平衡,故在插入和删除结点时更容易引起不平衡。因此在大量数据需要插入或者删除时,AVL树需要rebalance的频率会更高,相比之下,红黑树的效率会更高。 18 | 19 | 另外,读者需要注意的是,insert_rebalance操作也可能会有$O(logn)$量级的回溯,证明如下: 20 | 21 | 当程序进入`insert_rebalance()`的`while (x != root() && x->parent->color == red)`后,它只会有如下6种运行方式: 22 | 23 | 1. Case 1; 24 | 2. Case 1 ----> Case 1 ----> Case 1 ......; 25 | 3. Case 1 ----> ...... ----> Case 2 ----> Case 3; 26 | 4. Case 1 ----> ...... ----> Case 3; 27 | 5. Case 2 ----> Case 3; 28 | 6. Case 3; 29 | 30 | 而这回溯就发生在第2,3,4种,我们就以第2种的为例,如下图, 31 | 32 | ![](https://61mon.com/images/illustrations/the_difference_of_avl_and_rbtree/1.png) 33 | 34 | "结点1"为新插入结点,此时属于**Case 1:当前结点的父亲是红色,叔叔存在且也是红色**。那么我们的处理策略就是: 35 | 36 | - 将 "父亲结点" 改为黑色; 37 | - 将 "叔叔结点" 改为黑色; 38 | - 将 "祖父结点" 改为红色; 39 | - 将 "祖父结点" 设为 "当前结点",继续进行操作。 40 | 41 | 但处理完后,根据代码`while (x != root() && x->parent->color == red)`,我们发现"当前结点"又进入了Case 1。假设每次处理完后都会进入Case 1,那么这样的处理操作会直到根结点才会结束。 42 | 43 | erase_rebalance是否也存在同样的回溯呢?事实上,它并不存在。这很好证明。 44 | 45 | 当程序进入`while (x != root() && (x == nullptr || x->color == black))`后,它只会有如下6种运行方式: 46 | 47 | 1. Case 1 ----> Case 2; 48 | 2. Case 1 ----> Case 3 ----> Case 4; 49 | 3. Case 1 ----> Case 4; 50 | 4. Case 2; 51 | 5. Case 3 ----> Case 4; 52 | 6. Case 4; 53 | 54 | 因为4~6分别是1~3的后缀,所以我们只需考虑1~3即可。 55 | 56 | 读者可以自己脑补下1~3的运行流程就会发现,`while (x != root() && (x == nullptr || x->color == black))`语句只会被用到一次,就是最初进入程序的那次,之后便不再使用。 57 | 58 | 经过如上分析,我们可以对`insert_rebalance()`和`erase_rebalance()`作些微小的优化: 59 | 60 | ```c++ 61 | void RBTree::insert_rebalance(Node * x) 62 | { 63 | x->color = red; 64 | 65 | while (x != root() && x->parent->color == red) 66 | { 67 | if (x->parent == x->parent->parent->left) 68 | { 69 | Node * y = x->parent->parent->right; 70 | 71 | if (y && y->color == red) 72 | { 73 | x->parent->color = black; 74 | y->color = black; 75 | x->parent->parent->color = red; 76 | x = x->parent->parent; 77 | } 78 | else 79 | { 80 | if (x == x->parent->right) 81 | { 82 | x = x->parent; 83 | rotate_left(x); 84 | } 85 | 86 | x->parent->color = black; 87 | x->parent->parent->color = red; 88 | rotate_right(x->parent->parent); 89 | break; // add "break;" 90 | } 91 | } 92 | else 93 | { 94 | Node * y = x->parent->parent->left; 95 | 96 | if (y && y->color == red) 97 | { 98 | x->parent->color = black; 99 | y->color = black; 100 | x->parent->parent->color = red; 101 | x = x->parent->parent; 102 | } 103 | else 104 | { 105 | if (x == x->parent->left) 106 | { 107 | x = x->parent; 108 | rotate_right(x); 109 | } 110 | 111 | x->parent->color = black; 112 | x->parent->parent->color = red; 113 | rotate_left(x->parent->parent); 114 | break; // add "break;" 115 | } 116 | } 117 | } 118 | 119 | root()->color = black; 120 | } 121 | 122 | void RBTree::erase_rebalance(Node * z) 123 | { 124 | ...... 125 | ...... 126 | ...... 127 | 128 | if (y->color == black) 129 | { 130 | if (x != root() && (x == nullptr || x->color == black)) // "while" to "if" 131 | { 132 | if (x == x_parent->left) 133 | { 134 | Node * w = x_parent->right; 135 | 136 | if (w->color == red) 137 | { 138 | w->color = black; 139 | x_parent->color = red; 140 | rotate_left(x_parent); 141 | w = x_parent->right; 142 | } 143 | 144 | if ((w->left == nullptr || w->left->color == black) && 145 | (w->right == nullptr || w->right->color == black)) 146 | { 147 | w->color = red; 148 | x = x_parent; 149 | x_parent = x_parent->parent; 150 | } 151 | else 152 | { 153 | if (w->right == nullptr || w->right->color == black) 154 | { 155 | if (w->left) 156 | w->left->color = black; 157 | w->color = red; 158 | rotate_right(w); 159 | w = x_parent->right; 160 | } 161 | 162 | w->color = x_parent->color; 163 | x_parent->color = black; 164 | if (w->right) 165 | w->right->color = black; 166 | rotate_left(x_parent); 167 | // delete "break;" 168 | } 169 | } 170 | else 171 | { 172 | Node * w = x_parent->left; 173 | 174 | if (w->color == red) 175 | { 176 | w->color = black; 177 | x_parent->color = red; 178 | rotate_right(x_parent); 179 | w = x_parent->left; 180 | } 181 | 182 | if ((w->right == nullptr || w->right->color == black) && 183 | (w->left == nullptr || w->left->color == black)) 184 | { 185 | w->color = red; 186 | x = x_parent; 187 | x_parent = x_parent->parent; 188 | } 189 | else 190 | { 191 | if (w->left == nullptr || w->left->color == black) 192 | { 193 | if (w->right) 194 | w->right->color = black; 195 | w->color = red; 196 | rotate_left(w); 197 | w = x_parent->left; 198 | } 199 | 200 | w->color = x_parent->color; 201 | x_parent->color = black; 202 | if (w->left) 203 | w->left->color = black; 204 | rotate_right(x_parent); 205 | // delete "break;" 206 | } 207 | } 208 | } 209 | 210 | if (x) 211 | x->color = black; 212 | } 213 | } 214 | ``` 215 | -------------------------------------------------------------------------------- /201~300/222-跳跃表.md: -------------------------------------------------------------------------------- 1 | ## 一:背景 2 | 3 | 跳跃表(英文名:Skip List),于1990年William Pugh发明,是一个可以在有序元素中实现快速查询的数据结构,其插入,查找,删除操作的平均效率都为$O(logn)$。 4 | 5 | 跳跃表的整体性能可以和二叉查找树(AVL树,红黑树等)相媲美,其在[Redis](https://zh.wikipedia.org/wiki/Redis)和[LevelDB](https://zh.wikipedia.org/wiki/LevelDB)中都有广泛的应用。 6 | 7 | ![](https://61mon.com/images/illustrations/skip_list/1.png) 8 | 9 | 每个结点除了数据域,还有若干个指针指向下一个结点。 10 | 11 | 整体上看,Skip List就是带有层级结构的链表(结点都是排好序的),最下面一层(level 0)是所有结点组成的一个链表,依次往上,每一层也都是一个链表。不同的是,它们只包含一部分结点,并且越往上结点越少。仔细观察你会发现,通过增加层数,从当前结点可以直接访问更远的结点(这也就是Skip List的精髓所在),就像跳过去一样,所以取名叫Skip List(跳跃表)。 12 | 13 | ## 二:过程分析 14 | 15 | 先来看下跳跃表的整体代码结构: 16 | 17 | ```c++ 18 | #define P 0.25 19 | #define MAX_LEVEL 32 20 | 21 | struct Node 22 | { 23 | int key; 24 | Node ** forward; 25 | Node(int key = 0, int level = MAX_LEVEL) 26 | { 27 | this->key = key; 28 | forward = new Node*[level]; 29 | memset(forward, 0, level * sizeof(Node*)); 30 | } 31 | }; 32 | 33 | class SkipList 34 | { 35 | private: 36 | Node * header; 37 | int level; 38 | private: 39 | int random_level(); 40 | public: 41 | SkipList(); 42 | ~SkipList(); 43 | bool insert(int key); 44 | bool find(int key); 45 | bool erase(int key); 46 | void print(); 47 | }; 48 | ``` 49 | 50 | ### 2.1 插入 51 | 52 | ![](https://61mon.com/images/illustrations/skip_list/2.png) 53 | 54 | **首先**,我们要找到10在每一层应该被插入的位置,因此需要一个临时数组`update[]`来记录位置信息。 55 | 56 | **其次**,我们要确定结点10的层数(结点9的层数为2,结点12的层数为1)。 57 | 58 | 理想的跳跃表结构是:第一层有全部的结点,第二层有$\frac 1 2$的结点,且是均匀间隔的,第三层有$\frac 1 4$的结点,且也是均匀间隔的...,那么整个表的层数就是$logn$。每一次插入一个新结点时,最好的做法就是根据当前表的结构得到一个合适的层数,插入后可以让跳跃表尽量接近理想的结构,但这在实现上会非常困难。Pugh的论文中提出的方法是根据概率随机为新结点生成一个层数,具体的算法如下: 59 | 60 | 1. 给定一个概率p(p小于1),产生一个[0,1) 之间的随机数; 61 | 2. 如果这个随机数小于p,则层数加1; 62 | 3. 重复以上动作,直到随机数大于概率p(或层数大于程序给定的最大层数限制)。 63 | 64 | 虽然随机生成的层数会打破理想结构,但这种结构的期望复杂度依旧是$O(logn)$,稍后文尾会给出证明。 65 | 66 | **最后**,把结点10和它的前后结点连起来就行了。 67 | 68 | ```c++ 69 | int SkipList::random_level() 70 | { 71 | int level = 1; 72 | 73 | while ((rand() & 0xffff) < (P * 0xffff) && level < MAX_LEVEL) 74 | level++; 75 | 76 | return level; 77 | } 78 | 79 | bool SkipList::insert(int key) 80 | { 81 | Node * node = header; 82 | Node * update[MAX_LEVEL]; 83 | memset(update, 0, MAX_LEVEL * sizeof(Node*)); 84 | 85 | for (int i = level - 1; i >= 0; i--) 86 | { 87 | while (node->forward[i] && node->forward[i]->key < key) 88 | node = node->forward[i]; 89 | update[i] = node; 90 | } 91 | 92 | node = node->forward[0]; 93 | 94 | if (node == nullptr || node->key != key) 95 | { 96 | int new_level = random_level(); 97 | 98 | if (new_level > level) 99 | { 100 | for (int i = level; i < new_level; i++) 101 | update[i] = header; 102 | 103 | level = new_level; 104 | } 105 | 106 | Node * new_node = new Node(key, new_level); 107 | 108 | for (int i = 0; i < new_level; i++) 109 | { 110 | new_node->forward[i] = update[i]->forward[i]; 111 | update[i]->forward[i] = new_node; 112 | } 113 | 114 | return true; 115 | } 116 | 117 | return false; 118 | } 119 | ``` 120 | 121 | ### 2.2 查找 122 | 123 | ![](https://61mon.com/images/illustrations/skip_list/3.png) 124 | 125 | 查找操作很简单,例如上图,现要查找20, 126 | 127 | 1. 从最高层开始找,`17 < 20`,继续往后,发现是`NULL`,则往下一层继续查找; 128 | 2. `25 > 20`,则往下一层继续查找; 129 | 3. 找到20。 130 | 131 | ```c++ 132 | bool SkipList::find(int key) 133 | { 134 | Node * node = header; 135 | 136 | for (int i = level - 1; i >= 0; i--) 137 | { 138 | while (node->forward[i] && node->forward[i]->key <= key) 139 | node = node->forward[i]; 140 | 141 | if (node->key == key) 142 | return true; 143 | } 144 | 145 | return false; 146 | } 147 | ``` 148 | 149 | ### 2.3 删除 150 | 151 | ![](https://61mon.com/images/illustrations/skip_list/4.png) 152 | 153 | 删除操作跟插入操作类似。 首先找到我们要删除结点的位置,在查找时使用临时空间来记录结点在每一层的位置,接着就是逐层的链表删除操作。 **最后记住要释放空间。 删除结点之后,如果最高层没有结点存在,那么相应的,跳跃表的层数就应该降低**。 154 | 155 | ```c++ 156 | bool SkipList::erase(int key) 157 | { 158 | Node * node = header; 159 | Node * update[MAX_LEVEL]; 160 | fill(update, update + MAX_LEVEL, nullptr); 161 | 162 | for (int i = level - 1; i >= 0; i--) 163 | { 164 | while (node->forward[i] && node->forward[i]->key < key) 165 | node = node->forward[i]; 166 | update[i] = node; 167 | } 168 | 169 | node = node->forward[0]; 170 | 171 | if (node && node->key == key) 172 | { 173 | for (int i = 0; i < level; i++) 174 | if (update[i]->forward[i] == node) 175 | update[i]->forward[i] = node->forward[i]; 176 | 177 | delete node; 178 | 179 | for (int i = level - 1; i >= 0; i--) 180 | { 181 | if (header->forward[i] == nullptr) 182 | level--; 183 | else 184 | break; 185 | } 186 | } 187 | 188 | return false; 189 | } 190 | ``` 191 | 192 | ## 三:完整代码 193 | 194 | ```c++ 195 | /** 196 | * 197 | * author : 刘毅(Limer) 198 | * date : 2017-09-01 199 | * mode : C++ 200 | */ 201 | 202 | #include 203 | #include 204 | #include 205 | 206 | #define P 0.25 207 | #define MAX_LEVEL 32 208 | 209 | using namespace std; 210 | 211 | struct Node 212 | { 213 | int key; 214 | Node ** forward; 215 | Node(int key = 0, int level = MAX_LEVEL) 216 | { 217 | this->key = key; 218 | forward = new Node*[level]; 219 | memset(forward, 0, level * sizeof(Node*)); 220 | } 221 | }; 222 | 223 | class SkipList 224 | { 225 | private: 226 | Node * header; 227 | int level; 228 | private: 229 | int random_level(); 230 | public: 231 | SkipList(); 232 | ~SkipList(); 233 | bool insert(int key); 234 | bool find(int key); 235 | bool erase(int key); 236 | void print(); 237 | }; 238 | 239 | int SkipList::random_level() 240 | { 241 | int level = 1; 242 | 243 | while ((rand() & 0xffff) < (P * 0xffff) && level < MAX_LEVEL) 244 | level++; 245 | 246 | return level; 247 | } 248 | 249 | SkipList::SkipList() 250 | { 251 | header = new Node; 252 | level = 0; 253 | } 254 | 255 | SkipList::~SkipList() 256 | { 257 | Node * cur = header; 258 | Node * next = nullptr; 259 | 260 | while (cur) 261 | { 262 | next = cur->forward[0]; 263 | delete cur; 264 | cur = next; 265 | } 266 | 267 | header = nullptr; 268 | } 269 | 270 | bool SkipList::insert(int key) 271 | { 272 | Node * node = header; 273 | Node * update[MAX_LEVEL]; 274 | memset(update, 0, MAX_LEVEL * sizeof(Node*)); 275 | 276 | for (int i = level - 1; i >= 0; i--) 277 | { 278 | while (node->forward[i] && node->forward[i]->key < key) 279 | node = node->forward[i]; 280 | update[i] = node; 281 | } 282 | 283 | node = node->forward[0]; 284 | 285 | if (node == nullptr || node->key != key) 286 | { 287 | int new_level = random_level(); 288 | 289 | if (new_level > level) 290 | { 291 | for (int i = level; i < new_level; i++) 292 | update[i] = header; 293 | 294 | level = new_level; 295 | } 296 | 297 | Node * new_node = new Node(key, new_level); 298 | 299 | for (int i = 0; i < new_level; i++) 300 | { 301 | new_node->forward[i] = update[i]->forward[i]; 302 | update[i]->forward[i] = new_node; 303 | } 304 | 305 | return true; 306 | } 307 | 308 | return false; 309 | } 310 | 311 | bool SkipList::find(int key) 312 | { 313 | Node * node = header; 314 | 315 | for (int i = level - 1; i >= 0; i--) 316 | { 317 | while (node->forward[i] && node->forward[i]->key <= key) 318 | node = node->forward[i]; 319 | 320 | if (node->key == key) 321 | return true; 322 | } 323 | 324 | return false; 325 | } 326 | 327 | bool SkipList::erase(int key) 328 | { 329 | Node * node = header; 330 | Node * update[MAX_LEVEL]; 331 | fill(update, update + MAX_LEVEL, nullptr); 332 | 333 | for (int i = level - 1; i >= 0; i--) 334 | { 335 | while (node->forward[i] && node->forward[i]->key < key) 336 | node = node->forward[i]; 337 | update[i] = node; 338 | } 339 | 340 | node = node->forward[0]; 341 | 342 | if (node && node->key == key) 343 | { 344 | for (int i = 0; i < level; i++) 345 | if (update[i]->forward[i] == node) 346 | update[i]->forward[i] = node->forward[i]; 347 | 348 | delete node; 349 | 350 | for (int i = level - 1; i >= 0; i--) 351 | { 352 | if (header->forward[i] == nullptr) 353 | level--; 354 | else 355 | break; 356 | } 357 | } 358 | 359 | return false; 360 | } 361 | 362 | void SkipList::print() 363 | { 364 | Node * node = nullptr; 365 | 366 | for (int i = 0; i < level; i++) 367 | { 368 | node = header->forward[i]; 369 | cout << "Level " << i << " : "; 370 | while (node) 371 | { 372 | cout << node->key << " "; 373 | node = node->forward[i]; 374 | } 375 | cout << endl; 376 | } 377 | 378 | cout << endl; 379 | } 380 | 381 | int main() 382 | { 383 | SkipList sl; 384 | 385 | // test "insert" 386 | sl.insert(3); 387 | sl.insert(9); 388 | sl.insert(1); sl.insert(1); 389 | sl.insert(4); 390 | sl.insert(2); sl.insert(2); 391 | sl.insert(5); 392 | sl.insert(6); 393 | sl.insert(7); 394 | sl.insert(8); 395 | sl.insert(10); 396 | sl.insert(11); 397 | sl.insert(12); 398 | sl.print(); 399 | 400 | // test "find" 401 | cout << sl.find(50) << endl; 402 | cout << sl.find(2) << endl; 403 | cout << sl.find(7) << endl << endl; 404 | 405 | // test "erase" 406 | sl.erase(1); 407 | sl.print(); 408 | sl.erase(10); 409 | sl.print(); 410 | sl.erase(11); 411 | sl.print(); 412 | 413 | return 0; 414 | } 415 | ``` 416 | 417 | 运行如下(注意:结点层数采用的是随机值,故不同电脑可能会有不同的运行结果): 418 | 419 | ![](https://61mon.com/images/illustrations/skip_list/5.png) 420 | 421 | ## 四:效率分析与证明 422 | 423 | 首先回顾下插入操作中随机生成层数的函数: 424 | 425 | ```c++ 426 | #define P 0.25 427 | #define MAX_LEVEL 32 428 | 429 | int SkipList::random_level() 430 | { 431 | int level = 1; 432 | 433 | while ((rand() & 0xffff) < (P * 0xffff) && level < MAX_LEVEL) 434 | level++; 435 | 436 | return level; 437 | } 438 | ``` 439 | 440 | 下文中我们用小写的`p`来代替上述代码大写的常量`P`。 441 | 442 | **1.查找的期望时间复杂度** 443 | 444 | 设$T(n)$表示$n$个结点的跳跃表中查找的期望路径长度。 445 | 446 | 它分为三部分: 447 | 448 | 1. 第$1$层至最高层构成的跳跃表中查找的期望路径长度。此部分相当于一个期望规模为$O(pn)$的跳跃表的期望路径长度; 449 | 2. 从第$1$层下降至第$0$层的一条指针; 450 | 3. 在第$0$层右行的路径长度。每次能够右行的概率为$1-p$。 451 | 452 | 于是: 453 | 454 | $$ 455 | \begin{align} 456 | T(n)&=T(pn)+1+(1-p)+(1-p)^2+(1-p)^3+...\\ 457 | &=T(pn)+1/p 458 | \end{align} 459 | $$ 460 | 461 | 解函数得到: 462 | 463 | $$ 464 | \begin{align} 465 | T(n)&=-\frac {log_pn}p\\ 466 | &=-\frac 1 {plog_2p}⋅log_2n 467 | \end{align} 468 | $$ 469 | 470 | **2. 单一结点的期望层数** 471 | 472 | - 结点层数恰好等于$1$的概率为$1-p$; 473 | - 结点层数恰好等于$2$的概率为$p(1-p)$; 474 | - 结点层数恰好等于$3$的概率为$p^2(1-p)$; 475 | - ...... 476 | 477 | 478 | 那么一个结点的期望层数计算如下: 479 | 480 | $$ 481 | \begin{align} 482 | E(l)&=1⋅(1-p)+2⋅p(1-p)+3⋅p^2(1-p)+...\\ 483 | &=(1-p)⋅\sum_{i=1}^{+∞} {i⋅p^{i-1}}\\ 484 | &=\frac 1 {1-p} 485 | \end{align} 486 | $$ 487 | 488 | **3. 期望空间复杂度** 489 | 490 | 对于一个有$n$个结点的跳跃表,其期望空间复杂度为: 491 | 492 | $$ 493 | \begin{align} 494 | S(n)&=n⋅E(l)\\ 495 | &=\frac n {1-p} 496 | \end{align} 497 | $$ 498 | 499 | **4. 最大层数分析** 500 | 501 | 设最大层数为$h​$,则$h​$不超过$m​$的概率为: 502 | 503 | $$ 504 | P(h≤ m)=(1-p^{m-1})^n 505 | $$ 506 | 507 | 当没有最高层限制的时候(即$h→+∞$)才是真正的Skip List,但是实际应用中为了程序实现简单,往往设置这样一个常数$h$。根据刚才对最高层概率的分析,我们可以选取一个适当的$h$。比如,对于$p=1/4,n=10^6$时,取$h=16$就是一个不错的选择(个人觉得,参照BST来,直接取$h=log_2n$就可以)。此时 508 | $$ 509 | P(h=16)=(1-0.25^{15})^{10^6}≈0.999069111 510 | $$ 511 | 512 | **5. p的综合分析** 513 | 514 | 根据上述第1点所求的查找时间复杂度,进一步化简: 515 | 516 | $$ 517 | T(n)=-\frac 1{p⋅log_2p}⋅log_2n 518 | $$ 519 | 520 | 我们只需讨论左边的表达式即可, 521 | 522 | $$ 523 | f(p)=-\frac 1{p⋅log_2p}\tag{$0系列文章目录 2 | > 3 | >[B-树(1):定义及其代码实现](https://61mon.com/index.php/archives/224/) 4 | >B-树(2):应用及其拓展 5 | 6 | ## 一:应用领域 7 | 8 | B-树与其它二叉树最大的不同,就是其可以拥有多余2个的子结点。这也正是B-树被应用于文件系统和数据库底层实现的重要原因。 9 | 10 | 我们再回顾下B-树的性质: 11 | 12 | 13 | 14 | 15 | 16 | 1. 所有的叶子结点在同一层; 17 | 2. 每棵 B - 树有一个 Minimum Degree,称其为 t; 18 | 3. 除了根结点,其余每个结点至少包含 t-1 个 keys,根结点可以只包含 1 个 key; 19 | 4. 每个结点(包括根结点)最多包含 2t-1 个 keys; 20 | 5. 一个结点的孩子指针数等于这个结点的 keys 数 + 1; 21 | 6. 每个结点的 keys 都按升序排列; 22 | 7. 对于每个 key,其左边孩子结点的所有 keys 都小于它,右边孩子结点的所有 keys 都大于它。 23 | 24 | 对于一棵结点数为$n$的B-树,每一个结点的孩子指针数范围为$[t-1, 2t-1]$,所以树的高度在$[log_{t-1}n, log_{2t-1}n]$。那么当$n=10,000,000,000,t=512$时,只需要小于$4$次即可定位到该结点,然后再采用二分查找即可找到要找的值。 25 | 26 | ![](https://61mon.com/images/illustrations/b_tree/12.png) 27 | 28 | 我们计算机的主存基本都是随机访问存储器(Random-Access Memory,简称RAM),它分为两类:静态随机访问存储器(SRAM)和动态随机访问存储器(DRAM)。SRAM比DRAM快,但是也贵的多,一般作为CPU的高速缓存,DRAM通常作为内存。 29 | 30 | 我们使用更多的是磁盘,磁盘能够保存大量的数据,从GB到TB级,但是它的读取速度比较慢,因为涉及到机器操作,读取速度为毫秒级,从DRAM读速度比从磁盘度快10万倍,从SRAM读速度比从磁盘读快100万倍。下面来看下磁盘的结构: 31 | 32 | ![](https://61mon.com/images/illustrations/b_tree/13.png) 33 | 34 | 如上图,磁盘由盘片构成,每个盘片有两面,又称为盘面(Surface),这些盘面覆盖有磁性材料。盘片中央有一个可以旋转的主轴(Spindle),它使得盘片以固定的旋转速率旋转,通常是5400转每分钟(Revolution Per Minute,简称RPM)或者是7200RPM。磁盘包含多个这样的盘片并封装在一个密封的容器内。上图左,展示了一个典型的磁盘表面结构,每个表面是由一组成为磁道(Track)的同心圆组成的,每个磁道被划分为了一组扇区(Sector),每个扇区包含相等数量的数据位,通常是512子节,扇区之间由一些间隔(Gap)隔开,不存储数据。 35 | 36 | 现在来看下磁盘的读写操作: 37 | 38 | ![](https://61mon.com/images/illustrations/b_tree/14.png) 39 | 40 | 如上图,磁盘用读写头来读写存储在磁性表面的位,而读写头连接到一个传动臂的一端。通过沿着半径轴前后移动传动臂,驱动器可以将读写头定位到任何磁道上,这称之为寻道操作。一旦定位到磁道后,盘片转动,磁道上的每个位经过磁头时,读写磁头就可以感知到位的值,也可以修改值。对磁盘的访问时间分为寻道时间,旋转时间,以及传送时间。 41 | 42 | 由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,因此为了提高效率,要尽量减少磁盘IO操作。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。 43 | 44 | 程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。预读的长度一般为页(Page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。 45 | 46 | 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个结点的大小设为等于一个页,这样每个结点只需要一次IO就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧: 47 | 48 | 每次新建一个结点时,直接申请一个页的空间( 512或者1024),这样就保证一个结点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,这样就实现了一个结点只需一次IO操作。 49 | 50 | ## 二:B树的拓展 51 | 52 | ### 2.1 B+树 53 | 54 | B+树是B树的变体,也是一种多路搜索树,其定义基本与B树相同,除了: 55 | 56 | 1. 非叶子结点的孩子指针与keys数目相同; 57 | 2. 各层结点中的keys均是下一层相应结点中最小的key(或最大的key); 58 | 3. 为所有叶子结点增加一个链指针(key_pointer); 59 | 4. 所有keys都会在叶子结点出现; 60 | 61 | ![](https://61mon.com/images/illustrations/b_tree/15.png) 62 | 63 | 相比B树,B+树更适合用于实现文件存储和数据库。这主要基于以下两点: 64 | 65 | 1. B+树的磁盘读写代价更低 66 | 67 | 因为B+树的所有keys终究都会出现在最底层的叶子结点,所以对于非叶子结点,我们只需存储"基于比较所用的关键字"即可(仅仅作为索引),相比B树,一个结点可以存储更多的keys,也就是说,IO读写次数降低了。 68 | 69 | 2. B+树的查询效率更稳定 70 | 71 | 由于非叶子结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 72 | 73 | ### 2.2 B*树 74 | 75 | B*树是B+树的变体,其定义基本与B+树相同,除了: 76 | 77 | 1. 为所有非叶子结点增加指向兄弟结点的指针; 78 | 2. 非叶子结点的keys数提高到至少$2/3$,即块的最低使用率为$2/3$(代替B+树的$1/2$)。 79 | 80 | ![](https://61mon.com/images/illustrations/b_tree/16.png) 81 | 82 | 考虑分裂操作, 83 | 84 | B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中$1/2$的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。 85 | 86 | B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制$1/3$的数据到新结点,最后在父结点增加新结点的指针。 87 | 88 | 所以,B*树分配新结点的概率比B+树要低,空间使用率更高。 89 | 90 | ### 2.3 总结 91 | 92 | 通过以上介绍,大致将B树,B+树,B*树总结如下: 93 | 94 | B树:有序数组+平衡多叉树,数据存在于非叶子和叶子结点上; 95 | 96 | B+树:有序数组链表+平衡多叉树,数据只存在于叶子结点上; 97 | 98 | B*树:一棵丰满的B+树。 99 | 100 | ## 三:结后语 101 | 102 | 最后,就B树的叫法作些补充。 103 | 104 | - B-Tree,也称"B-树","B树",读作"B 树"。注意里面的"-"不是"减"的意思,而是"杠"的意思。从不存在"B减 树"的叫法,你所听到的"B减 树",其实就是"B 杠 树",但为了叫起来顺口,直接叫"B 树"更佳。 105 | - B+ Tree,(也可以写作"B+ - Tree",读作"B加 杠 树"),读作"B加 树"; 106 | - B\* Tree,(也可以写作"B* - Tree",读作"B星 杠 树"),读作"B星 树"。 107 | 108 | ## 四:参考文献 109 | 110 | - [B树、B-树、B+树、B*树](http://www.cnblogs.com/oldhorse/archive/2009/11/16/1604009.html). 111 | - [https://zh.coursera.org/learn/gaoji-shuju-jiegou/lecture/5iuEZ/b-shu](https://zh.coursera.org/learn/gaoji-shuju-jiegou/lecture/5iuEZ/b-shu). 112 | - [浅谈算法和数据结构: 十 平衡查找树之B树](http://www.cnblogs.com/yangecnu/p/Introduce-B-Tree-and-B-Plus-Tree.html). 113 | - [为什么文件存储要选用B+树这样的数据结构?](http://kongchen.github.io/why-b-tree/). 114 | -------------------------------------------------------------------------------- /201~300/226-位运算总结.md: -------------------------------------------------------------------------------- 1 | 位运算,相比普通的代码最大的优点就是其带来的高效性,也因此可以常在底层源码中看见它们的踪影。 2 | 3 | 本文就位运算常见的操作作一个总结,若您另有关于位运算巧妙的运用可以于底部留言区留言。 4 | 5 | 首先还是先来回顾下位操作的基础知识。(除非特别说明,否则以下都以2进制为例) 6 | 7 | 8 | 9 | 10 | 11 | ## 相关基础 12 | 13 | ### 1. 与运算 14 | 15 | 与运算符"&"是双目运算符。只有对应的两个二进位均为1时,结果位才为1,否则为0。例如: 16 | ```c++ 17 | 9 & 5 = 00001001 18 | & 00000101 19 | = 00000001 20 | = 1 21 | ``` 22 | ### 2. 或运算 23 | 24 | 或运算符"|"是双目运算符。只要对应的两个二进位有一个为1时,结果位就为1。例如: 25 | 26 | ```c++ 27 | 9 | 5 = 00001001 28 | | 00000101 29 | = 00001101 30 | = 13 31 | ``` 32 | 33 | ### 3. 非运算 34 | 35 | 非运算符" ~ "为单目运算符。其功能是对参与运算的各二进位求反。例如: 36 | 37 | ```c++ 38 | ~ 9 = ~ 00001001 39 | = 11110110 40 | = -10 41 | ``` 42 | 43 | ### 4. 异或运算 44 | 45 | 异或运算符" ^ "是双目运算符。其功能是对参与运算的二进位相异或,即当两二进位相异时,结果为1,相同就为0。例如: 46 | 47 | ```c++ 48 | 9 ^ 5 = 00001001 49 | ^ 00000101 50 | = 00001100 51 | = 12 52 | ``` 53 | 54 | ### 5. 左移和右移 55 | 56 | | | 例1 | 例2 | 57 | | :----------: | :------: | :------: | 58 | | x | 01100011 | 10010101 | 59 | | x << 4 | 00110000 | 01010000 | 60 | | x >> 4(逻辑右移) | 00000110 | 00001001 | 61 | | x >> 4(算术右移) | 00000110 | 11111001 | 62 | 63 | 左移动就是向左移动k位,丢弃最高的k位,并在右端补k个0,也就是常说的当前值乘以2的k次方。 64 | 65 | 右移动的原理也是相同的,右移k位就是当前数除以2的k次方。唯一不同的是分为逻辑右移和算术右移。 66 | 67 | 逻辑右移就是无符号移位,右移几位,就在左端补几个0。 68 | 69 | 算术右移动是有符号移位,和逻辑右移不同的是,算术右移是在左端补k个最高有效位的值,如此看着有些奇特,但对有符号整数数据的运算非常有用。我们知道有符号的数,首位字节,是用来表示数字的正负(1为负)。负数采用补码形式来存储,比如-26(11100110),算术右移1位之后-13(11110011)。如若不是补最高有效位的值1而是补作0的话,右移之后就变成正数了。 70 | 71 | ## 经典应用 72 | 73 | ### 1. i+(~i)=-1 74 | 75 | i取反再与i相加,相当于把所有二进制位设为1,其十进制结果为-1。 76 | 77 | ### 2. 计算n+1与n-1 78 | 79 | `-~n == n + 1`,~n为其取反,负号 ' - ' 再对其取反并加1。 80 | 81 | `~-n == n - 1`,思路就是找到最低位的第一个1,对其取反并把该位后的所有位也取反,即`01001000`变为`01000111`。 82 | 83 | ### 3. 取相反数 84 | 85 | 思路就是取反并加1,也即`~n + 1`或者`(n ^ -1) + 1`。 86 | 87 | ### 4. if(x == a) x = b; if(x == b) x = a; 88 | 89 | 利用^运算符的性质,即得`x = a ^ b ^ x`。 90 | 91 | ### 5. n倍数补全 92 | 93 | 当n为2的幂时,`(x + n - 1) & ~(n - 1)`会找到第一个大于x的数,且它正好是n的整数倍。 94 | 95 | ### 6. 求二进制中1的个数 96 | 97 | ```c++ 98 | /* Version 1 */ 99 | int count_1_bits(int n) 100 | { 101 | int count = 0; 102 | 103 | while (n) 104 | { 105 | count++; 106 | n = n & (n - 1); 107 | } 108 | 109 | return count; 110 | } 111 | 112 | /* Version 2 */ 113 | int count_1_bits(int n) 114 | { 115 | x = (x & 0x55555555) + ((x >> 1) & 0x55555555); 116 | x = (x & 0x33333333) + ((x >> 2) & 0x33333333); 117 | x = (x & 0x0f0f0f0f) + ((x >> 4) & 0x0f0f0f0f); 118 | x = (x & 0x00ff00ff) + ((x >> 8) & 0x00ff00ff); 119 | x = (x & 0x0000ffff) + ((x >> 16) & 0x0000ffff); 120 | 121 | return x; 122 | } 123 | ``` 124 | 125 | 关于第二个版本,分析如下:(摘自[Matrix67-位运算](http://www.matrix67.com/blog/archives/264),并作稍微修改) 126 | 127 | 以十进制数211为例,其二进制为11010011, 128 | 129 | ```c++ 130 | | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | <— 原数 131 | +---+---+---+---+---+---+---+---+ 132 | | 1 0 | 0 1 | 0 0 | 1 0 | <— 第一次运算后 133 | +-------+-------+-------+-------+ 134 | | 0 0 1 1 | 0 0 1 0 | <— 第二次运算后 135 | +---------------+---------------+ 136 | | 0 0 0 0 0 1 0 1 | <— 第三次运算后,得数为 5 137 | +-------------------------------+ 138 | ``` 139 | 140 | 整个程序是一个分治的思想。第一次我们把每相邻的两位加起来,得到每两位里1的个数,比如前两位10就表示原数的前两位有2个1。第二次我们继续两两相加,10+01=11,00+10=10,得到的结果是00110010,它表示原数前4位有3个1,末4位有2个1。最后一次我们把0011和0010加起来,得到的就是整个二进制中1的个数。 141 | 142 | ### 7. 判断二进制中1的奇偶性 143 | 144 | ```c++ 145 | x = x ^ (x >> 1); 146 | x = x ^ (x >> 2); 147 | x = x ^ (x >> 4); 148 | x = x ^ (x >> 8); 149 | x = x ^ (x >> 16); 150 | 151 | cout << (x & 1) << endl; // 输出 1 为奇数 152 | ``` 153 | 154 | 以下分析摘自[Matrix67-位运算](http://www.matrix67.com/blog/archives/264),并作稍微修改, 155 | 156 | 以十进制数1314520为例,其二进制为0001 0100 0000 1110 1101 1000。 157 | 158 | 第一次异或操作的结果如下: 159 | 160 | ```c++ 161 | 0001 0100 0000 1110 1101 1000 162 | ^ 0000 1010 0000 0111 0110 1100 163 | = 0001 1110 0000 1001 1011 0100 164 | ``` 165 | 166 | 得到的结果是一个新的二进制数,其中右起第i位上的数表示原数中第i和i+1位上有奇数个1还是偶数个1。比如,最右边那个0表示原数末两位有偶数个1,右起第3位上的1就表示原数的这个位置和前一个位置中有奇数个1。 167 | 168 | 对这个数进行第二次异或的结果如下: 169 | 170 | ```c++ 171 | 0001 1110 0000 1001 1011 0100 172 | ^ 0000 0111 1000 0010 0110 1101 173 | = 0001 1001 1000 1011 1101 1001 174 | ``` 175 | 176 | 结果里的每个1表示原数的该位置及其前面三个位置中共有奇数个1,每个0就表示原数对应的四个位置上共偶数个1。 177 | 178 | 一直做到第五次异或结束后,得到的二进制数的最末位就表示整个32位数里1的奇偶性。 179 | 180 | ### 8. 判断奇偶性 181 | 182 | ```c++ 183 | /* 判断是否是奇数 */ 184 | bool is_odd(int n) 185 | { 186 | return (n & 1 == 1); 187 | } 188 | ``` 189 | 190 | ### 9. 不用临时变量交换两个数 191 | 192 | ```c++ 193 | /* 此方法对 a 和 b 相等的情况不适用 */ 194 | a ^= b; 195 | b ^= a; // 相当于 b = b ^ ( a ^ b ); 196 | a ^= b; 197 | ``` 198 | 199 | ### 10. 取绝对值 200 | 201 | ```c++ 202 | /* 注意:以下的数字 31 是针对 int 大小为 32 而言 */ 203 | int abs(int n) 204 | { 205 | return (n ^ (n >> 31)) - (n >> 31); 206 | } 207 | ``` 208 | 209 | 其中`n >> 31`取得n的正负号。 210 | 211 | - 若n为正数,`n >> 31`的所有位等于0,其值等于0。表达式转化为`n ^ 0 - 0`,等于n; 212 | 213 | 214 | - 若n为负数,`n >> 31`的所有位等于1,其值等于-1。表达式转化为`(n ^ -1) + 1`,这很好理解,负数的相反数就是对其补码取反再加1,`(n ^ -1) + 1`就是在做这样的事。 215 | 216 | ### 11. 取两数的较大值 217 | 218 | ```c++ 219 | /* 注意:以下的数字 31 是针对 int 大小为 32 而言 */ 220 | int max(int a, int b) 221 | { 222 | return (b & ((a - b) >> 31)) | (a & (~(a - b) >> 31)); 223 | } 224 | ``` 225 | 226 | 如果`a >= b`,`(a - b) >> 31`为0,否则为-1。 227 | 228 | ### 12. 判断符号是否相同 229 | 230 | ```c++ 231 | /* 若 x,y 都为 0,输出真;若只有一个为 0,不会报错但运行结果是错的,因为 0 没有正负之分 */ 232 | bool is_same_sign(int x, int y) 233 | { 234 | return (x ^ y) >= 0; 235 | } 236 | ``` 237 | 238 | ### 13. 判断一个数是不是2的幂 239 | 240 | ```c++ 241 | bool is_power_of_two(int n) 242 | { 243 | return (n > 0) ? (n & (n - 1)) == 0 : false; 244 | } 245 | ``` 246 | 247 | 如果是2的幂,`n - 1`就是把n的二进制的最低的那个1取反为0,并把后面的0全部取反为1。 248 | 249 | ### 14. 取余2的幂次方 250 | 251 | ```c++ 252 | /* 其中 m 为 2 的幂次方,并对 m 取余 */ 253 | int mod(int n, int m) 254 | { 255 | return n & (m - 1); 256 | } 257 | ``` 258 | 259 | ## 参考文献 260 | 261 | - [Matrix67](http://www.matrix67.com/blog/archives/264). 262 | - [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html). -------------------------------------------------------------------------------- /201~300/227-向量积与线段相交问题.md: -------------------------------------------------------------------------------- 1 | ## 一:什么是向量积? 2 | 3 | 向量积,也称(向量)叉积,(向量)叉乘,外积,是一种在向量空间中对向量进行的二元运算。常见于物理学力学、电磁学、光学和计算机图形学等理工学科中,是一种很重要的概念。 4 | 5 | 6 | 7 | 8 | 设向量$\overrightarrow{c}$由两个向量$\overrightarrow{a}$和$\overrightarrow{b}$按如下公式定出:$\overrightarrow{c}$的模$|\overrightarrow{c}|=|\overrightarrow{a}||\overrightarrow{b}|sinθ$,其中$θ$为$\overrightarrow{a}$和$\overrightarrow{b}$间的夹角;$\overrightarrow{c}$的方向垂直于$\overrightarrow{a}$和$\overrightarrow{b}$所决定的平面,指向按右手规则从$\overrightarrow{a}$转向$\overrightarrow{b}$来确定,如下图: 9 | 10 | ![](https://61mon.com/images/illustrations/vector_product/1.jpg) 11 | 12 | 那么,向量$\overrightarrow{c}$叫做向量$\overrightarrow{a}$与$\overrightarrow{b}$的向量积,记作$\overrightarrow{a}×\overrightarrow{b}$。 13 | 14 | 由上述的定义,我们很容易总结出两条性质: 15 | 16 | $$ 17 | \begin{align} 18 | \overrightarrow{a}×\overrightarrow{b}&=\overrightarrow{0} \tag{其中$\overrightarrow{a} 平行 \overrightarrow{b}$}\\ 19 | \overrightarrow{a}×\overrightarrow{b}&=- \overrightarrow{b}×\overrightarrow{a} \tag{不满足交换律} 20 | \end{align} 21 | $$ 22 | 23 | 下面来推导向量积的坐标表达式,以二维向量为例。设$\overrightarrow{a}=(a_x, a_y),\overrightarrow{b}=(b_x, b_y)$,得: 24 | $$ 25 | \begin{align} 26 | \overrightarrow{a}×\overrightarrow{b}&=(a_x\overrightarrow{i}+a_y\overrightarrow{j})×(b_x\overrightarrow{i}+b_y\overrightarrow{j}) \tag{其中$\overrightarrow{i}$和$\overrightarrow{j}分别是$xy$轴上的单位向量$}\\ 27 | &=a_x\overrightarrow{i}×(b_x\overrightarrow{i}+b_y\overrightarrow{j})+a_y\overrightarrow{j}×(b_x\overrightarrow{i}+b_y\overrightarrow{j}) \tag{分解}\\ 28 | &=a_xb_x(\overrightarrow{i}×\overrightarrow{i})+a_yb_y(\overrightarrow{j}×\overrightarrow{j})+(a_xb_y-a_yb_x)(\overrightarrow{i}×\overrightarrow{j})\tag{合并}\\ 29 | &=(a_xb_x+a_yb_y)\overrightarrow{0}+(a_xb_y-a_yb_x)(\overrightarrow{i}×\overrightarrow{j}) \tag{消去平行向量}\\ 30 | &=(a_xb_y-a_yb_x)(\overrightarrow{i}×\overrightarrow{j}) \tag{消去0向量} 31 | \end{align} 32 | $$ 33 | 仔细观察上式,得出: 34 | 35 | 1. $a_xb_y-a_yb_x>0$,则$\overrightarrow{b}$在$\overrightarrow{a}$的逆时针方向上(参照$\overrightarrow{i}$和$\overrightarrow{j}$的位置); 36 | 2. $a_xb_y-a_yb_x<0$,则$\overrightarrow{b}$在$\overrightarrow{a}$的顺时针方向上; 37 | 3. $a_xb_y-a_yb_x=0$,则$\overrightarrow{a}$和$\overrightarrow{b}$共线,但是否同向不确定。 38 | 39 | 40 | ## 二:向量积与计算几何(线段相交) 41 | 42 | 在计算几何中,向量积也有很大的应用,最经典的就是"线段相交"问题:给定两条线段$AB$和$CD$,其中$A(a_x, a_y),B(b_x, b_y),C(c_x, c_y),D(d_x, d_y)$,判断它们是否相交。 43 | 44 | 我们只需利用两个实验即可完成判断,快速排斥实验和跨立实验。 45 | 46 | 先看跨立实验,若两条线段相交,则两线段必然会相互跨立对方。 47 | 48 | ![](https://61mon.com/images/illustrations/vector_product/2.png) 49 | 50 | 代码如下: 51 | 52 | ```c++ 53 | struct Point 54 | { 55 | double x, y; 56 | }; 57 | 58 | /* 求出向量 AB 与向量 AC 的向量积,返回 0 代表共线 */ 59 | double cross(Point A, Point B, Point C) 60 | { 61 | return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x); 62 | } 63 | 64 | if (cross(A, B, C) * cross(A, B, D) <= 0 && 65 | cross(C, D, A) * cross(C, D, B) <= 0) 66 | pass test; 67 | ``` 68 | 69 | 但是请考虑一个特殊情况,若两条线段共线呢?此时仅仅通过上面的跨立实验是无法检测的,因此还需要借助快速排斥实验。 70 | 71 | 所谓的快速排斥实验,就是以线段$AB$,$CD$为对角线作两个矩形,若两个矩形外离,则两条线段必定不相交。 72 | 73 | ![](https://61mon.com/images/illustrations/vector_product/1.png) 74 | 75 | 代码如下: 76 | 77 | ```c++ 78 | if (min(A.x, B.x) <= max(C.x, D.x) && 79 | min(C.x, D.x) <= max(A.x, B.x) && 80 | min(A.y, B.y) <= max(C.y, D.y) && 81 | min(C.y, D.y) <= max(A.y, B.y)) 82 | pass test; 83 | ``` 84 | 85 | 综合上面的分析,若要断定两条线段相交,则它们必须同时通过快速排斥实验和跨立实验。 86 | 87 | ![](https://61mon.com/images/illustrations/vector_product/3.png) 88 | 89 | 全部代码如下: 90 | 91 | ```c++ 92 | struct Point 93 | { 94 | double x, y; 95 | }; 96 | 97 | /* 求出向量 AB 与向量 AC 的向量积,返回 0 代表共线 */ 98 | double cross(Point A, Point B, Point C) 99 | { 100 | return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x); 101 | } 102 | 103 | /* 判断线段 AB 与线段 CD 是否相交,相交返回 true */ 104 | bool is_intersect(Point A, Point B, Point C, Point D) 105 | { 106 | if (min(A.x, B.x) <= max(C.x, D.x) && 107 | min(C.x, D.x) <= max(A.x, B.x) && 108 | min(A.y, B.y) <= max(C.y, D.y) && 109 | min(C.y, D.y) <= max(A.y, B.y) && 110 | cross(A, B, C) * cross(A, B, D) <= 0 && 111 | cross(C, D, A) * cross(C, D, B) <= 0) 112 | return true; 113 | 114 | return false; 115 | } 116 | ``` 117 | 118 | ## 三:参考文献 119 | 120 | - [【计算几何】线段相交](http://www.cnblogs.com/dwdxdy/p/3230485.html). 121 | - 高等数学【下册】. 第六版. 同济大学数学系编. 122 | -------------------------------------------------------------------------------- /201~300/228-凸包问题.md: -------------------------------------------------------------------------------- 1 | 首先介绍下什么是凸包?如下图: 2 | 3 | ![](https://61mon.com/images/illustrations/convex_hull/1.png) 4 | 5 | 在一个二维坐标系中,有若干点杂乱排列着,将最外层的点连接起来构成的凸多边型,它能包含给定的所有的点,这个多边形就是凸包。 6 | 7 | 8 | 9 | 10 | 11 | 寻找凸包的算法有很多种,Graham Scan算法是一种十分简单高效的二维凸包算法,能够在$O(nlogn)$的时间内找到凸包。 12 | 13 | Graham Scan算法的做法是先确定一个起点(一般是最左边的点和最右边的点),然后一个个点扫过去,如果新加入的点和之前已经找到的点所构成的"壳"凸性没有变化,就继续扫,否则就把已经找到的最后一个点删去,再比较凸性,直到凸性不发生变化。分别扫描上下两个"壳",合并在一起,凸包就找到了。这么说很抽象,我们看图来解释: 14 | 15 | ![](https://61mon.com/images/illustrations/convex_hull/2.png) 16 | 17 | 先找"下壳",上下其实是一样的。首先加入两个点A和B。 18 | 19 | ![](https://61mon.com/images/illustrations/convex_hull/3.png) 20 | 21 | 然后插入第三个点C,并计算$\overrightarrow{AB}×\overrightarrow{BC}$的向量积,却发现[向量积系数](https://61mon.com/index.php/archives/227/#menu_index_1)小于(等于)0,也就是说$\overrightarrow{BC}$在$\overrightarrow{AB}$的顺时针方向上。 22 | 23 | ![](https://61mon.com/images/illustrations/convex_hull/4.png) 24 | 25 | 于是删去B点。 26 | 27 | ![](https://61mon.com/images/illustrations/convex_hull/5.png) 28 | 29 | 按照这样的方法依次扫描,找完"下壳"后,再找"上壳"。 30 | 31 | 关于扫描的顺序,有坐标序和极角序两种,本文采用前者。坐标序是比较两个点的x坐标,如果小的先被扫描(扫描上凸壳的时候反过来);如果两个点x坐标相同,那么就比较y坐标,小的先被扫描(扫描上凸壳的时候也是反过来)。极角序使用`atan2`函数的返回值进行比较,读者可以自己尝试写下。 32 | 33 | 下面贴下代码: 34 | 35 | ```c++ 36 | struct Point 37 | { 38 | double x, y; 39 | 40 | Point operator-(Point & p) 41 | { 42 | Point t; 43 | t.x = x - p.x; 44 | t.y = y - p.y; 45 | return t; 46 | } 47 | 48 | double cross(Point p) // 向量叉积 49 | { 50 | return x * p.y - p.x * y; 51 | } 52 | }; 53 | 54 | bool cmp(Point & p1, Point & p2) 55 | { 56 | if (p1.x != p2.x) 57 | return p1.x < p2.x; 58 | 59 | return p1.y < p2.y; 60 | } 61 | 62 | Point point[1005]; // 无序点 63 | int convex[1005]; // 保存组成凸包的点的下标 64 | int n; // 坐标系的无序点的个数 65 | 66 | int GetConvexHull() 67 | { 68 | sort(point, point + n, cmp); 69 | int temp; 70 | int total = 0; 71 | 72 | for (int i = 0; i < n; i++) // 下凸包 73 | { 74 | while (total > 1 && 75 | (point[convex[total - 1]] - point[convex[total - 2]]).cross(point[i] - point[convex[total - 1]]) <= 0) 76 | total--; 77 | 78 | convex[total++] = i; 79 | } 80 | 81 | temp = total; 82 | 83 | for (int i = n - 2; i >= 0; i--) // 上凸包 84 | { 85 | while (total > temp && 86 | (point[convex[total - 1]] - point[convex[total - 2]]).cross(point[i] - point[convex[total - 1]]) <= 0) 87 | total--; 88 | 89 | convex[total++] = i; 90 | } 91 | 92 | return total - 1; // 返回组成凸包的点的个数,实际上多了一个,就是起点,所以组成凸包的点个数是 total - 1 93 | } 94 | ``` 95 | 96 | ### 参考文献: 97 | 98 | - [Graham Scan凸包算法](https://segmentfault.com/a/1190000000488339). 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 所有文章已搬家至 https://subetter.com 2 | 3 | # 新开源地址:https://github.com/Hapoa 4 | 5 | --------------------------------------------------------------------------------