diff --git a/Algorithm/A类:基本算法/1 算法概述.md b/Algorithm/A类:基本算法/1 算法概述.md index 735dac06..332db835 100644 --- a/Algorithm/A类:基本算法/1 算法概述.md +++ b/Algorithm/A类:基本算法/1 算法概述.md @@ -23,6 +23,12 @@ ![](image/算法流程.png) +1. 理解问题 +2. 选择策略 +3. 算法设计 +4. 正确性证明 +5. 算法分析 +6. 程序设计 ## 3 算法分类 diff --git a/Algorithm/A类:基本算法/2 算法效率.md b/Algorithm/A类:基本算法/2 算法效率.md index e1722fac..659604ed 100644 --- a/Algorithm/A类:基本算法/2 算法效率.md +++ b/Algorithm/A类:基本算法/2 算法效率.md @@ -3,8 +3,8 @@ > 目录 1. 算法效率的度量 2. 函数的渐进的界 -3. 算法的基本复杂性类型 -4. 算法复杂性分析的基本方法 +3. 算法的基本复杂性类型 +4. 算法复杂性分析的基本方法 5. 非递归算法的复杂性分析 6. 递归算法的复杂性分析 7. 递归算法与非递归算法比较 @@ -15,8 +15,8 @@ ## 1 算法效率的度量 ### 分类 -* 时间效率 -* 空间效率 +* 时间复杂度 +* 空间复杂度 ### 算法效率的表示 @@ -46,9 +46,9 @@ $$ 若存在正数$c$和$n0$使得对一切$n≥n0有0≤f(n)≤cg(n)$ 2. $f(n)= Ω(g(n))$渐进下届 若存在正数$c$和$n0$使得对一切$n≥n0有0≤cg(n)≤ f(n)$ -3. $f(n)=o(g(n))$ +3. $f(n)=o(g(n))$不可达上届 对任意正数$c$存在$n0$使得对一切$n≥n0$有$0≤f(n)n!>a^n>n^a>n\log n>n>\sqrt{n}>\log n +$$ +## 4 复杂性分析的基本步骤 1. 决定表示输入规模的参数。 2. 找出算法的基本操作。 3. 检查基本操作的执行次数是否只依赖于输入规模。如果还依赖于输入的其它特性,考虑最差、平均以及最优情况下的复杂性。 @@ -89,7 +100,11 @@ $$ 5. 确定增长的阶 ## 6 递归算法的复杂性分析 + +* 线性收缩递归 ![](image/递归算法复杂性1.png) + +* 等比收缩递归 ![](image/递归算法复杂性2.png) ![](image/递归算法复杂性3.png) diff --git a/Algorithm/A类:基本算法/3 蛮力法(枚举法).md b/Algorithm/A类:基本算法/3 蛮力法(枚举法).md new file mode 100644 index 00000000..594d7611 --- /dev/null +++ b/Algorithm/A类:基本算法/3 蛮力法(枚举法).md @@ -0,0 +1,103 @@ +# 蛮力法 +## 1 蛮力法概述 +蛮力法是一种简单直接地解决问题的方法,常常直接 基于问题的描述和所涉及的概念定义。 + +## 2 排序问题 +(主要描述解决问题的步骤) +### 理解问题 +* 问题:给定一个可排序的n个元素序列(数字、字符或字符串),对它们按照非降序方式重新排列。 + +### 选择策略 +思想:首先扫描整个序列,找到其中一个最小元素,然后和第一个元素交换,将最小元素归位。然后从第二个元素开始扫描序列,找到后n-1个元素中的一个最小元素,然后和第二个元素交换,将第二小元素归位。进行n-1遍扫描之后,排序完成。 +### 算法设计 + +算法 selectSort(A[n]) +``` +//用选择法对给定数组排序 +//输入:一个可排序数组A[0..n-1] +//输出:升序排序的数组A[0..n-1] +for i←0 to n-2 do + min←i + for j=i+1 to n-1 do + if A[j] < A[min] min←j + swap A[i] and A[min] +``` +### 正确性证明 + +### 算法分析 +* 输入规模:序列元素个数n +* 基本操作:比较次数A[j] < A[min] +* 影响操作执行的其他因素:n +* 构建基本操作的求和表达式: +利用求和公式分析算法的时间复杂度: +![](image/排序算法.png) + +### 程序设计 + +## 3 顺序查找问题 +(主要是分析解决问题的步骤) +### 理解问题 +思想:查找键与表中元素从头至尾逐个比较。 +结果:找到 或 失败 +限位器:把查找键添加到列表末尾—— 一定成功,避免每次循环时对检查是否越界(边界检查) +选择策略 + +### 算法设计 + +![](image/蛮力法-顺序查找.png) + + +### 正确性证明 + +### 算法分析 +* 最佳效率:Tbest (n) = 1 +* 最差效率:Tworst(n) = n + 1 +* 问:为何定义 A 数组为 n+1 维?答:有一个位置放限位器 +* 问:若输入有序,算法可改进?答:遇到 ≤ 或 ≥ 查找键元素,立即停止查找。 + +### 程序设计 + + +## 4 字符串匹配问题 +### 理解问题 +问题:给定一个n个字符组成的串,称为文本,一个m(m≤n)个字符组成的串称为模式,从文本中寻找匹配模式的子串。 +### 选择策略 +思想:将模式对准文本的前m个字符,然后从左到右匹配每一对相应的字符,若遇到一对不匹配字符,模式向右移一位,重新开始匹配;若m对字符全部匹配,算法可以停止。注意,在文本中,最后一轮子串匹配的起始位置是n-m(假设文本的下标从0到n-1) +### 算法设计 +算法 bruteForceStringMatch(T[0..n-1],P[0..m-1]) +``` +//蛮力字符串匹配算法实现 +//输入1:一个n个字符的数组T[0..n-1]代表一段文本 +//输入2:一个m个字符的数组P[0..m-1]代表一个模式 +//输出:若查找成功,返回文本第一个匹配子串中的第一个字符的位置,否则返回-1 +for i←0 to n-m do + j←0 + while j 阅读目录 +1. 顺序查找 +2. 二分查找 +3. 插值查找 +4. 斐波那契查找 +5. 树表查找 +6. 分块查找 +7. 哈希查找 + +## 0 概述 +查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。 + +### 查找定义 + +根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。 + +### 查找算法分类: +1. 静态查找和动态查找; + + 静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。 +2. 无序查找和有序查找。 + * 无序查找:被查找数列有序无序均可; + * 有序查找:被查找数列必须为有序数列。 + +### 平均查找长度(Average Search Length,ASL) + +需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。 + +对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。 +* Pi:查找表中第i个数据元素的概率。 +* Ci:找到第i个数据元素时已经比较过的次数。 + +## 1 顺序查找 +### 说明 + +顺序查找适合于存储结构为顺序存储或链接存储的线性表。 + +### 基本思想 + +顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。 + +### 复杂度分析:  +  查找成功时的平均查找长度为:(假设每个数据元素的概率相等) +$$ +ASL = 1/n(1+2+3+…+n) = (n+1)/2; +$$ +当查找不成功时,需要n+1次比较,时间复杂度为 +$$ +O(n); +$$ +所以,顺序查找的时间复杂度为O(n)。 + +### 代码实现 +``` +//顺序查找 +int SequenceSearch(int a[], int value, int n) +{ + int i; + for(i=0; i 注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》 + +### 代码实现 +``` +//二分查找(折半查找),版本1 +int BinarySearch1(int a[], int value, int n) +{ + int low, high, mid; + low = 0; + high = n-1; + while(low<=high) + { + mid = (low+high)/2; + if(a[mid]==value) + return mid; + if(a[mid]>value) + high = mid-1; + if(a[mid]value) + return BinarySearch2(a, value, low, mid-1); + if(a[mid] 注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。 + +### 复杂度分析 +查找成功或者失败的时间复杂度均为O(log2(log2n))。 + +### 代码实现 +``` +//插值查找 +int InsertionSearch(int a[], int value, int low, int high) +{ + int mid = low+(value-a[low])/(a[high]-a[low])*(high-low); + if(a[mid]==value) + return mid; + if(a[mid]>value) + return InsertionSearch(a, value, low, mid-1); + if(a[mid] 在介绍斐波那契查找算法之前,我们先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。 +>  黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。 +>  0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。 +>  大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。 + +### 基本思想 +也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。 + +相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况: +* 相等,mid位置的元素即为所求 +* `>,low=mid+1;` +* `<,high=mid-1` + +斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数减1,即n=F(k)-1; +开始将k值与第F(k-1)位置的记录进行比较(mid=low+F(k-1)-1),比较结果也分为三种 + +* 相等,mid位置的元素即为所求 +* `>,low=mid+1,k-=2;` +   + 说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。 + +* `<,high=mid-1,k-=1` +   + 说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。 + + +### 复杂度分析 + +最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。 + + +### 代码实现 +``` +// 斐波那契查找.cpp + +#include "stdafx.h" +#include +#include +using namespace std; + +const int max_size=20;//斐波那契数组的长度 + +/*构造一个斐波那契数组*/ +void Fibonacci(int * F) +{ + F[0]=0; + F[1]=1; + for(int i=2;iF[k]-1)//计算n位于斐波那契数列的位置 + ++k; + + int * temp;//将数组a扩展到F[k]-1的长度 + temp=new int [F[k]-1]; + memcpy(temp,a,n*sizeof(int)); + + for(int i=n;itemp[mid]) + { + low=mid+1; + k-=2; + } + else + { + if(mid=n则说明是扩展的数值,返回n-1 + } + } + delete [] temp; + return -1; +} + +int main() +{ + int a[] = {0,16,24,35,47,59,62,73,88,99}; + int key=100; + int index=FibonacciSearch(a,sizeof(a)/sizeof(int),key); + cout< 分块查找又称索引顺序查找,它是顺序查找的一种改进方法。 + +### 算法思想 + +将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,…… +### 算法流程: +1. step1 先选取各块中的最大关键字构成一个索引表; +2. step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。 + +## 7 哈希查找 +### 哈希表-哈希函数原理 +什么是哈希表? + +我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。 + +总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。 + +什么是哈希函数? + +哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。 + +### 算法思想 + +哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。 + +### 算法流程: +1. 用给定的哈希函数构造哈希表; +2. 根据选择的冲突处理方法解决地址冲突;常见的解决冲突的方法:拉链法和线性探测法。详细的介绍可以参见:浅谈算法和数据结构: 十一 哈希表。 +3. 在哈希表的基础上执行哈希查找。 + + +  哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。 + +### 复杂度分析: +  单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。 +   +使用Hash,我们付出了什么? + +  我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么? + +  Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。 + +Hash算法和其他查找算法的性能对比: +![](image/查找算法-哈希搜索效率.png) diff --git a/Algorithm/A类:基本算法/3.2 搜索算法-广度优先搜索.md b/Algorithm/A类:基本算法/3.2 搜索算法-广度优先搜索.md new file mode 100644 index 00000000..38165e64 --- /dev/null +++ b/Algorithm/A类:基本算法/3.2 搜索算法-广度优先搜索.md @@ -0,0 +1,173 @@ +# 广度优先搜索 + +## 1 概述 + +### 特点 +广度优先搜索(BFS:Breadth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作: +* 访问图中的一个节点; +* 访问该节点的邻居节点; + +### 过程 + +广度优先搜索(BFS)由 Edward F. Moore 在 1950 年发表,起初被用于在迷宫中寻找最短路径。在 Prim 最小生成树算法和 Dijkstra 单源最短路径算法中,都采用了与广度优先搜索类似的思想。 + +对图的广度优先搜索与对树(Tree)的广度优先遍历(Breadth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。BFD 算法首先会发现和源顶点 s 距离边数为 k 的所有顶点,然后才会发现和 s 距离边数为 k+1 的其他顶点。 + +![](image/广度优先搜索-层次.png) + +### 例子 +例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。 + +![](image/广度优先搜索-例子.png) + +上图的 BFS 遍历结果为 [ 2, 0, 3, 1 ]。 + +### 实现 +BFS 算法的实现通常使用队列(Queue)数据结构来存储遍历图中节点的中间状态,过程如下: +1. 将 root 节点 Enqueue; +2. Dequeue 一个节点,并检查该节点: + * 如果该节点就是要找的目标节点,则结束遍历,返回结果 "Found"; + * 否则,Enqueue 所有直接后继子节点(如果节点未被访问过); +3. 如果 Queue 为空,并且图中的所有节点都被检查过,仍未找到目标节点,则结束搜索,返回结果 "Not Found"; +4. 如果 Queue 不为空,重复步骤 2; + + +如果需要记录搜索的轨迹,可以为顶点着色。起初所有顶点为白色,随着搜索的进行变为灰色,然后变成黑色。灰色和黑色顶点都是已发现的顶点。 +### 时间复杂度 +广度优先搜索(BFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。 +### BFS算法伪码 +``` + 1 procedure BFS(G,v) is + 2 create a queue Q + 3 create a set V + 4 add v to V + 5 enqueue v onto Q + 6 while Q is not empty loop + 7 t = Q.dequeue() + 8 if t is what we are looking for then + 9 return t +10 end if +11 for all edges e in G.adjacentEdges(t) loop +12 u = G.adjacentVertex(t,e) +13 if u is not in V then +14 add u to V +15 enqueue u onto Q +16 end if +17 end loop +18 end loop +19 return none +20 end BFS +``` + + +### BFS算法代码 +``` + 1 using System; + 2 using System.Collections.Generic; + 3 + 4 namespace GraphAlgorithmTesting + 5 { + 6 class Program + 7 { + 8 static void Main(string[] args) + 9 { + 10 Graph g = new Graph(4); + 11 g.AddEdge(0, 1); + 12 g.AddEdge(0, 2); + 13 g.AddEdge(1, 2); + 14 g.AddEdge(2, 0); + 15 g.AddEdge(2, 3); + 16 g.AddEdge(3, 3); + 17 + 18 List traversal = g.BFS(2); + 19 foreach (var vertex in traversal) + 20 { + 21 Console.WriteLine(vertex); + 22 } + 23 + 24 Console.ReadKey(); + 25 } + 26 + 27 class Edge + 28 { + 29 public Edge(int begin, int end) + 30 { + 31 this.Begin = begin; + 32 this.End = end; + 33 } + 34 + 35 public int Begin { get; private set; } + 36 public int End { get; private set; } + 37 } + 38 + 39 class Graph + 40 { + 41 private Dictionary> _adjacentEdges + 42 = new Dictionary>(); + 43 + 44 public Graph(int vertexCount) + 45 { + 46 this.VertexCount = vertexCount; + 47 } + 48 + 49 public int VertexCount { get; private set; } + 50 + 51 public void AddEdge(int begin, int end) + 52 { + 53 if (!_adjacentEdges.ContainsKey(begin)) + 54 { + 55 var edges = new List(); + 56 _adjacentEdges.Add(begin, edges); + 57 } + 58 + 59 _adjacentEdges[begin].Add(new Edge(begin, end)); + 60 } + 61 + 62 public List BFS(int start) + 63 { + 64 List traversal = new List(); + 65 int current = start; + 66 + 67 // mark all the vertices as not visited + 68 bool[] visited = new bool[VertexCount]; + 69 for (int i = 0; i < VertexCount; i++) + 70 { + 71 visited[i] = false; + 72 } + 73 + 74 // create a queue for BFS + 75 Queue queue = new Queue(); + 76 + 77 // mark the current node as visited and enqueue it + 78 visited[current] = true; + 79 queue.Enqueue(current); + 80 + 81 while (queue.Count > 0) + 82 { + 83 current = queue.Dequeue(); + 84 + 85 // if this is what we are looking for + 86 traversal.Add(current); + 87 + 88 // get all adjacent vertices of the dequeued vertex, + 89 // if a adjacent has not been visited, + 90 // then mark it visited and enqueue it + 91 if (_adjacentEdges.ContainsKey(current)) + 92 { + 93 foreach (var edge in _adjacentEdges[current]) + 94 { + 95 if (!visited[edge.End]) + 96 { + 97 visited[edge.End] = true; + 98 queue.Enqueue(edge.End); + 99 } +100 } +101 } +102 } +103 +104 return traversal; +105 } +106 } +107 } +108 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.3 搜索算法-深度优先搜索.md b/Algorithm/A类:基本算法/3.3 搜索算法-深度优先搜索.md new file mode 100644 index 00000000..ddc629af --- /dev/null +++ b/Algorithm/A类:基本算法/3.3 搜索算法-深度优先搜索.md @@ -0,0 +1,200 @@ +# 深度优先搜索 +## 1 概述 + +### 特点 +深度优先搜索(DFS:Depth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作: +(a) 访问图中的一个节点; +(b) 访问该节点的子节点; + +### 过程 +在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续探测下去。当顶点 v 的所有边都已被探寻过后,搜索将回溯到发现顶点 v 有起始点的那些边。这一过程一直进行到已发现从源顶点可达的所有顶点为止。实际上深度优先搜索最初的探究也是为了解决迷宫问题。 + +对图的深度优先搜索与对树(Tree)的深度优先遍历(Depth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。 + +### 例子 + +例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,子顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 DFS 的算法实现中需要对顶点是否访问过做标记。 +![](image/深度优先搜索-例子.png) +上图的 DFS 遍历结果为 2, 0, 1, 3。 + +### 实现 +DFS 算法可以通过不同方式来实现: + +递归方式 + +非递归方式:使用栈(Stack)数据结构来存储遍历图中节点的中间状态; + +### 时间复杂度 +深度优先搜索(DFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。 + +### DFS算法的递归方式伪码 +``` +1 procedure DFS(G,v): +2 label v as discovered +3 for all edges from v to w in G.adjacentEdges(v) do +4 if vertex w is not labeled as discovered then +5 recursively call DFS(G,w) +``` +### DFS算法的非递归方式伪码 +``` +1 procedure DFS-iterative(G,v): +2 let S be a stack +3 S.push(v) +4 while S is not empty +5 v ← S.pop() +6 if v is not labeled as discovered: +7 label v as discovered +8 for all edges from v to w in G.adjacentEdges(v) do +9 S.push(w) +``` + +### DFS算法实现代码如下: +``` + 1 using System; + 2 using System.Linq; + 3 using System.Collections.Generic; + 4 + 5 namespace GraphAlgorithmTesting + 6 { + 7 class Program + 8 { + 9 static void Main(string[] args) + 10 { + 11 Graph g = new Graph(4); + 12 g.AddEdge(0, 1); + 13 g.AddEdge(0, 2); + 14 g.AddEdge(1, 2); + 15 g.AddEdge(2, 0); + 16 g.AddEdge(2, 3); + 17 g.AddEdge(3, 3); + 18 + 19 foreach (var vertex in g.DFS(2)) + 20 { + 21 Console.WriteLine(vertex); + 22 } + 23 foreach (var vertex in g.RecursiveDFS(2)) + 24 { + 25 Console.WriteLine(vertex); + 26 } + 27 + 28 Console.ReadKey(); + 29 } + 30 + 31 class Edge + 32 { + 33 public Edge(int begin, int end) + 34 { + 35 this.Begin = begin; + 36 this.End = end; + 37 } + 38 + 39 public int Begin { get; private set; } + 40 public int End { get; private set; } + 41 } + 42 + 43 class Graph + 44 { + 45 private Dictionary> _adjacentEdges + 46 = new Dictionary>(); + 47 + 48 public Graph(int vertexCount) + 49 { + 50 this.VertexCount = vertexCount; + 51 } + 52 + 53 public int VertexCount { get; private set; } + 54 + 55 public void AddEdge(int begin, int end) + 56 { + 57 if (!_adjacentEdges.ContainsKey(begin)) + 58 { + 59 var edges = new List(); + 60 _adjacentEdges.Add(begin, edges); + 61 } + 62 + 63 _adjacentEdges[begin].Add(new Edge(begin, end)); + 64 } + 65 + 66 public List DFS(int start) + 67 { + 68 List traversal = new List(); + 69 int current = start; + 70 + 71 // mark all the vertices as not visited + 72 bool[] visited = new bool[VertexCount]; + 73 for (int i = 0; i < VertexCount; i++) + 74 { + 75 visited[i] = false; + 76 } + 77 + 78 // create a stack for DFS + 79 Stack stack = new Stack(); + 80 + 81 // mark the current node as visited and push it + 82 visited[current] = true; + 83 stack.Push(current); + 84 + 85 while (stack.Count > 0) + 86 { + 87 current = stack.Pop(); + 88 + 89 // if this is what we are looking for + 90 traversal.Add(current); + 91 + 92 // get all child vertices of the popped vertex, + 93 // if a child has not been visited, + 94 // then mark it visited and push it + 95 if (_adjacentEdges.ContainsKey(current)) + 96 { + 97 foreach (var edge in _adjacentEdges[current].OrderByDescending(e => e.End)) + 98 { + 99 if (!visited[edge.End]) +100 { +101 visited[edge.End] = true; +102 stack.Push(edge.End); +103 } +104 } +105 } +106 } +107 +108 return traversal; +109 } +110 +111 public List RecursiveDFS(int start) +112 { +113 List traversal = new List(); +114 int current = start; +115 +116 // mark all the vertices as not visited +117 bool[] visited = new bool[VertexCount]; +118 for (int i = 0; i < VertexCount; i++) +119 { +120 visited[i] = false; +121 } +122 +123 // traversal +124 RecursiveDFSTraversal(current, visited, traversal); +125 +126 return traversal; +127 } +128 +129 private void RecursiveDFSTraversal(int current, bool[] visited, List traversal) +130 { +131 visited[current] = true; +132 traversal.Add(current); +133 +134 if (_adjacentEdges.ContainsKey(current)) +135 { +136 foreach (var edge in _adjacentEdges[current].OrderBy(e => e.End)) +137 { +138 if (!visited[edge.End]) +139 { +140 RecursiveDFSTraversal(edge.End, visited, traversal); +141 } +142 } +143 } +144 } +145 } +146 } +147 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.4 排序算法-简单排序.md b/Algorithm/A类:基本算法/3.4 排序算法-简单排序.md new file mode 100644 index 00000000..920ce6c5 --- /dev/null +++ b/Algorithm/A类:基本算法/3.4 排序算法-简单排序.md @@ -0,0 +1,1768 @@ +# 排序算法 +## 0 比较排序算法分类 +比较排序(Comparison Sort)通过对数组中的元素进行比较来实现排序。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

比较排序算法(Comparison Sorts)

+
+

Category

+
+

Name

+
+

Best

+
AverageWorstMemoryStability
+

 插入排序 

+

(Insertion Sorts)

+

 

+
+

 插入排序

+

(Insertion Sort)

+
+

n

+
+

n2

+
+

n2

+
+

1

+
+

Stable

+
+

 希尔排序

+

(Shell Sort)

+
+

n

+
+

n log2 n

+
+

n log2 n

+
+

1

+
+

Not Stable

+
+

 交换排序

+

(Exchange Sorts )

+

 

+

 

+

 

+
+

 快速排序

+

(Quick Sort)

+
+

 n log n

+
+

n log n

+
+

n2

+
+

log n

+
+

Not Stable

+
+

 冒泡排序

+

(Bubble Sort)

+
+

n

+
+

n2

+
+

n2

+
+

1

+
+

Stable

+
+

 鸡尾酒排序

+

(Cocktail Sort)

+
+

n

+
+

n2

+
+

n2

+
+

1

+
+

Stable

+
+

 奇偶排序

+

(Odd-Even Sort)

+
+

n

+
+

n2

+
+

n2

+
+

1

+
+

Stable

+
+

 选择排序

+

(Selection Sorts)

+

 

+
+

 选择排序

+

(Selection Sort)

+
+

 n2

+
+

n2

+
+

n2

+
+

1

+
+

Not Stable

+
+

 堆排序

+

(Heap Sort)

+
+

 n log n

+
+

n log n

+
+

n log n

+
+

1

+
+

Not Stable

+
+

 合并排序

+

(Merge Sorts)

+
+

 合并排序

+

(Merge Sort)

+
+

 n

+
+

n log n

+
+

n log n

+
+

n

+
+

Stable

+
+

 混合排序

+

(Hybrid Sorts)

+
+

 内省排序

+

(Introspective Sort)

+
+

 n log n

+
+

n log n

+
+

n log n

+
+

log n

+
+

Not Stable

+
+ +> 注:关于 Memory,如果算法为 "in place" 排序,则仅需要 O(1) 内存;有时对于额外的 O(log(n)) 内存也可以称为 "in place"。 + +> 注:Microsoft .NET Framework 中 Array.Sort 方法的实现使用了内省排序(Introspective Sort)算法。 + + +### Stable 与 Not Stable 的比较 + +稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。 + +![](image/排序算法-稳定性.png) + +### O(n2) 与 O(n*logn) 的比较 + +合并排序和堆排序在最坏情况下达到上界 O(n*logn),快速排序在平均情况下达到上界 O(n*logn)。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 Ω(n*logn) 时间运行。 + +> 注:有关算法复杂度,可参考文章《算法复杂度分析》。有关常用数据结构的复杂度,可参考文章《常用数据结构及复杂度》。 + +![](image/排序算法-算法效率比较.png) + +## 1 冒泡排序(Bubble Sort) +### 算法描述 +重复地比较要排序的数列,一次比较两个元素,如果后者较小则与前者交换元素。 +1. 比较相邻的元素,如果前者比后者大,则交换两个元素。 +2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。 +3. 针对所有的元素重复以上的步骤,除了最后一个。 + +![](image/排序算法-冒泡排序.png) + +### 算法复杂度 +冒泡排序对 n 个元素需要 O(n2) 的比较次数,且可以原地排序。冒泡排序仅适用于对于含有较少元素的数列进行排序。 +* 最差时间复杂度 O(n2) +* 平均时间复杂度 O(n2) +* 最优时间复杂度 O(n) +* 最差空间复杂度 O(n),辅助空间 O(1) + +### 示例代码 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 OptimizedBubbleSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void BubbleSort(int[] unsorted) +18 { +19 for (int i = 0; i < unsorted.Length; i++) +20 { +21 for (int j = 0; j < unsorted.Length - 1 - i; j++) +22 { +23 if (unsorted[j] > unsorted[j + 1]) +24 { +25 int temp = unsorted[j]; +26 unsorted[j] = unsorted[j + 1]; +27 unsorted[j + 1] = temp; +28 } +29 } +30 } +31 } +32 +33 static void OptimizedBubbleSort(int[] unsorted) +34 { +35 int exchange = unsorted.Length - 1; +36 while (exchange > 0) +37 { +38 int lastExchange = exchange; +39 exchange = 0; +40 +41 for (int i = 0; i < lastExchange; i++) +42 { +43 if (unsorted[i] > unsorted[i + 1]) +44 { +45 int temp = unsorted[i]; +46 unsorted[i] = unsorted[i + 1]; +47 unsorted[i + 1] = temp; +48 +49 exchange = i; +50 } +51 } +52 } +53 } +54 } +``` + +## 2 鸡尾酒排序(Cocktail Sort) + +### 算法描述 +鸡尾酒排序,也就是双向冒泡排序(Bidirectional Bubble Sort),是冒泡排序的一种变形。此算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。如果序列中的大部分元素已经排序好时,可以得到比冒泡排序更好的性能。 +![](image/排序算法-鸡尾酒排序.png) +### 算法复杂度 +* 最差时间复杂度 O(n2) +* 平均时间复杂度 O(n2) +* 最优时间复杂度 O(n) +* 最差空间复杂度 О(1) + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 OptimizedCocktailSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void CocktailSort(int[] unsorted) +18 { +19 for (int i = 0; i < unsorted.Length / 2; i++) +20 { +21 // move the larger to right side +22 for (int j = i; j + 1 < unsorted.Length - i; j++) +23 { +24 if (unsorted[j] > unsorted[j + 1]) +25 { +26 int temp = unsorted[j]; +27 unsorted[j] = unsorted[j + 1]; +28 unsorted[j + 1] = temp; +29 } +30 } +31 +32 // move the smaller to left side +33 for (int j = unsorted.Length - i - 1; j > i; j--) +34 { +35 if (unsorted[j - 1] > unsorted[j]) +36 { +37 int temp = unsorted[j - 1]; +38 unsorted[j - 1] = unsorted[j]; +39 unsorted[j] = temp; +40 } +41 } +42 } +43 } +44 +45 static void OptimizedCocktailSort(int[] unsorted) +46 { +47 bool swapped = false; +48 int start = 0; +49 int end = unsorted.Length - 1; +50 do +51 { +52 swapped = false; +53 +54 // move the larger to right side +55 for (int i = start; i < end; i++) +56 { +57 if (unsorted[i] > unsorted[i + 1]) +58 { +59 int temp = unsorted[i]; +60 unsorted[i] = unsorted[i + 1]; +61 unsorted[i + 1] = temp; +62 +63 swapped = true; +64 } +65 } +66 +67 // we can exit the outer loop here if no swaps occurred. +68 if (!swapped) break; +69 swapped = false; +70 end = end - 1; +71 +72 // move the smaller to left side +73 for (int j = end; j > start; j--) +74 { +75 if (unsorted[j - 1] > unsorted[j]) +76 { +77 int temp = unsorted[j]; +78 unsorted[j] = unsorted[j - 1]; +79 unsorted[j - 1] = temp; +80 +81 swapped = true; +82 } +83 } +84 +85 start = start + 1; +86 } +87 while (swapped); +88 } +89 } +``` + +## 3 奇偶排序(Odd-Even Sort) +### 算法描述 +奇偶排序通过比较数组中相邻的(奇-偶)位置元素,如果该奇偶元素对是错误的顺序(前者大于后者),则交换元素。然后再针对所有的(偶-奇)位置元素进行比较。如此交替进行下去。 +![](image/排序算法-奇偶排序.png) + +### 算法复杂度 +最差时间复杂度 O(n2) +平均时间复杂度 O(n2) +最优时间复杂度 O(n) +最差空间复杂度 О(1) +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 OptimizedOddEvenSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void OddEvenSort(int[] unsorted) +18 { +19 for (int i = 0; i < unsorted.Length; ++i) +20 { +21 if (i % 2 > 0) +22 { +23 for (int j = 2; j < unsorted.Length; j += 2) +24 { +25 if (unsorted[j] < unsorted[j - 1]) +26 { +27 int temp = unsorted[j - 1]; +28 unsorted[j - 1] = unsorted[j]; +29 unsorted[j] = temp; +30 } +31 } +32 } +33 else +34 { +35 for (int j = 1; j < unsorted.Length; j += 2) +36 { +37 if (unsorted[j] < unsorted[j - 1]) +38 { +39 int temp = unsorted[j - 1]; +40 unsorted[j - 1] = unsorted[j]; +41 unsorted[j] = temp; +42 } +43 } +44 } +45 } +46 } +47 +48 static void OptimizedOddEvenSort(int[] unsorted) +49 { +50 bool swapped = true; +51 int start = 0; +52 +53 while (swapped || start == 1) +54 { +55 swapped = false; +56 +57 for (int i = start; i < unsorted.Length - 1; i += 2) +58 { +59 if (unsorted[i] > unsorted[i + 1]) +60 { +61 int temp = unsorted[i]; +62 unsorted[i] = unsorted[i + 1]; +63 unsorted[i + 1] = temp; +64 +65 swapped = true; +66 } +67 } +68 +69 if (start == 0) start = 1; +70 else start = 0; +71 } +72 } +73 } +``` + +## 4 快速排序(Quick Sort) +### 基本思想 +快速排序使用分治法(Divide-and-Conquer)策略将一个数列分成两个子数列并使用递归来处理。 + +1. 比如有如下这个 10 个数字,[13, 81, 92, 42, 65, 31, 57, 26, 75, 0]。 +2. 随机选择一个数作为中间的元素,例如选择 65。 +3. 这样数组就被 65 分成了两部分,左边的都小于 65,右边的都大于 65。 +4. 然后分别对左右两边的子数组按照相同的方式进行排序,并最终排序完毕。 + +### 算法描述 +1. 从数列中挑出一个元素,称为 "主元"(pivot)。 +2. 重新排序数列,所有元素比主元小的摆放在主元前面,所有元素比主元值大的摆在主元的后面(相同的数可以到任一边)。这个称为分区(partition)操作。在分区退出之后,该主元就处于数列的中间位置。 +3. 递归地(recursively)把小于主元值元素的子数列和大于主元值元素的子数列排序。递归的最底部情形,是数列的大小是 0 或 1 ,也就是总是被排序好的状况。这样一直递归下去,直到算法退出。 + +![](image/排序算法-快速排序.jpg) + +### 算法实现 +下面的过程实现快速排序,调用 QUICKSORT(A, 1, length[A])。 +``` +1 QUICKSORT(A, p, r) +2 if p < r +3 then q <- PARTITION(A, p, r) +4 QUICKSORT(A, p, q - 1) +5 QUICKSORT(A, q + 1, r) +``` +快速排序算法的关键是 Partition 过程,它对子数组进行就是重排。 +``` +1 PARTITION(A, p, r) +2 x <- A[r] +3 i <- p - 1 +4 for j <- p to r - 1 +5 do if A[j] <= x +6 then i <- i + 1 +7 exchange A[i] <-> A[j] +8 exchange A[i + 1] <-> A[r] +9 return i + 1 +``` +### 算法复杂度 +* 最差时间复杂度 O(n2) +* 平均时间复杂度 O(n*log n) +* 最优时间复杂度 O(n*log n) +* 最差空间复杂度 根据实现的方式不同而不同 O(n) 辅助空间 O(log n) + + +快速排序的运行时间与划分是否对称有关,而后者又与选择了哪一个元素来进行划分有关。如果划分是对称的,那么快速排序从渐进意义上来讲,就与合并算法一样快;如果划分是不对称的,那么从渐进意义上来讲,就与插入排序一样慢。 + +快速排序的平均运行时间与其最佳情况运行时间很接近,而不是非常接近于最差情况运行时间。 + +QUICKSORT 的运行时间是由花在过程 PARTITION 上的时间所决定的。每当 PARTITION 过程被调用时,就要选出一个 Pivot 元素。后续对 QUICKSORT 和 PARTITION 的各次递归调用中,都不会包含该元素。于是,在快速排序算法的整个执行过程中,至多只可能调用 PARTITION 过程 n 次。 + +### 快速排序的随机化版本 +快速排序的随机化版本是对足够大的输入的理想选择。 + +RANDOMIZED-QUICKSORT 的平均运行情况是 O(n lg n),如果在递归的每一层上,RANDOMIZED-PARTITION 所作出的划分使任意固定量的元素偏向划分的某一边,则算法的递归树深度为 Θ(lg n),且在每一层上所做的工作量都为 O(n)。 +``` +1 RANDOMIZED-PARTITION(A, p, r) +2 i <- RANDOM(p, r) +3 exchange A[r] <-> A[i] +4 return PARTITION(A, p, r) +``` + +### 算法比较 +快速排序是二叉查找树的一个空间优化版本。但其不是循序地把数据项插入到一个显式的树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。 + +快速排序的最直接竞争者是堆排序(Heap Sort)。堆排序通常会慢于原地排序的快速排序,其最坏情况的运行时间总是 O(n log n) 。快速排序通常情况下会比较快,但仍然有最坏情况发生的机会。 + +快速排序也会与合并排序(Merge Sort)竞争。合并排序的特点是最坏情况有着 O(n log n) 运行时间的优势。不像快速排序或堆排序,合并排序是一个稳定排序算法,并且非常的灵活,其设计可以应用于操作链表,或大型链式存储等,例如磁盘存储或网路附加存储等。尽管快速排序也可以被重写使用在链表上,但对于基准的选择总是个问题。合并排序的主要缺点是在最佳情况下需要 O(n) 额外的空间,而快速排序的原地分区和尾部递归仅使用 O(log n) 的空间。 + +### 代码示例 +``` + 1 class Program + 2 { + 3 static bool isPrintArrayEnabled = false; + 4 + 5 static void Main(string[] args) + 6 { + 7 int[] smallSeed = { 4, 1, 5, 2, 6, 3, 7, 9, 8, 0 }; + 8 + 9 MeasureQuickSort(smallSeed, 1000000); + 10 MeasureRandomizedQuickSort(smallSeed, 1000000); + 11 MeasureOptimizedQuickSorts(smallSeed, 1000000); + 12 + 13 Console.Read(); + 14 } + 15 + 16 static void MeasureQuickSort(int[] smallSeed, int arrayLength) + 17 { + 18 int[] unsorted = GenerateBigUnsortedArray(smallSeed, arrayLength); + 19 + 20 Stopwatch watch = Stopwatch.StartNew(); + 21 + 22 QuickSort(unsorted, 0, unsorted.Length - 1); + 23 + 24 watch.Stop(); + 25 + 26 Console.WriteLine( + 27 "ArrayLength[{0}], QuickSort ElapsedMilliseconds[{1}]", + 28 unsorted.Length, watch.ElapsedMilliseconds); + 29 + 30 PrintArray(unsorted); + 31 } + 32 + 33 static void MeasureRandomizedQuickSort(int[] smallSeed, int arrayLength) + 34 { + 35 int[] unsorted = GenerateBigUnsortedArray(smallSeed, arrayLength); + 36 + 37 Stopwatch watch = Stopwatch.StartNew(); + 38 + 39 RandomizedQuickSort(unsorted, 0, unsorted.Length - 1); + 40 + 41 watch.Stop(); + 42 + 43 Console.WriteLine( + 44 "ArrayLength[{0}], RandomizedQuickSort ElapsedMilliseconds[{1}]", + 45 unsorted.Length, watch.ElapsedMilliseconds); + 46 + 47 PrintArray(unsorted); + 48 } + 49 + 50 static void QuickSort(int[] unsorted, int left, int right) + 51 { + 52 // left 为子数列的最左边元素 + 53 // right 为子数列的最右边元素 + 54 if (!(left < right)) return; + 55 + 56 // Partition: + 57 // 所有元素比主元值小的摆放在主元的左边, + 58 // 所有元素比主元值大的摆放在主元的右边 + 59 int pivotIndex = Partition(unsorted, left, right); + 60 + 61 // Recursively: + 62 // 分别排列小于主元的值和大于主元的值的子数列 + 63 // 主元无需参加下一次排序 + 64 QuickSort(unsorted, left, pivotIndex - 1); + 65 QuickSort(unsorted, pivotIndex + 1, right); + 66 } + 67 + 68 static int Partition(int[] unsorted, int left, int right) + 69 { + 70 int pivotIndex = right; + 71 + 72 // 哨兵 + 73 int sentinel = unsorted[right]; + 74 + 75 // 子数组长度为 right - left + 1 + 76 int i = left - 1; + 77 for (int j = left; j <= right - 1; j++) + 78 { + 79 if (unsorted[j] <= sentinel) + 80 { + 81 i++; + 82 Swap(unsorted, i, j); + 83 } + 84 } + 85 + 86 Swap(unsorted, i + 1, pivotIndex); + 87 + 88 return i + 1; + 89 } + 90 + 91 static void RandomizedQuickSort(int[] unsorted, int left, int right) + 92 { + 93 // left 为子数列的最左边元素 + 94 // right 为子数列的最右边元素 + 95 if (!(left < right)) return; + 96 + 97 // Partition: + 98 // 所有元素比主元值小的摆放在主元的左边, + 99 // 所有元素比主元值大的摆放在主元的右边 +100 int pivotIndex = RandomizedPartition(unsorted, left, right); +101 +102 // Recursively: +103 // 分别排列小于主元的值和大于主元的值的子数列 +104 // 主元无需参加下一次排序 +105 RandomizedQuickSort(unsorted, left, pivotIndex - 1); +106 RandomizedQuickSort(unsorted, pivotIndex + 1, right); +107 } +108 +109 static int RandomizedPartition(int[] unsorted, int left, int right) +110 { +111 int i = random.Next(left, right); +112 Swap(unsorted, i, right); +113 return Partition(unsorted, left, right); +114 } +115 +116 static void Swap(int[] unsorted, int i, int j) +117 { +118 int temp = unsorted[i]; +119 unsorted[i] = unsorted[j]; +120 unsorted[j] = temp; +121 } +122 +123 static void MeasureOptimizedQuickSorts(int[] smallSeed, int arrayLength) +124 { +125 foreach (var pivotSelection in +126 Enum.GetValues(typeof(QuickSortPivotSelectionType))) +127 { +128 int[] unsorted = GenerateBigUnsortedArray(smallSeed, arrayLength); +129 +130 Stopwatch watch = Stopwatch.StartNew(); +131 +132 OptimizedQuickSort(unsorted, 0, unsorted.Length - 1, +133 (QuickSortPivotSelectionType)pivotSelection); +134 +135 watch.Stop(); +136 +137 Console.WriteLine( +138 "ArrayLength[{0}], " +139 + "QuickSortPivotSelectionType[{1}], " +140 + "ElapsedMilliseconds[{2}]", +141 unsorted.Length, +142 (QuickSortPivotSelectionType)pivotSelection, +143 watch.ElapsedMilliseconds); +144 +145 PrintArray(unsorted); +146 } +147 } +148 +149 static int[] GenerateBigUnsortedArray(int[] smallSeed, int arrayLength) +150 { +151 int[] bigSeed = new int[100]; +152 for (int i = 0; i < bigSeed.Length; i++) +153 { +154 bigSeed[i] = +155 smallSeed[i % smallSeed.Length] +156 + i / smallSeed.Length * 10; +157 } +158 +159 int[] unsorted = new int[arrayLength]; +160 for (int i = 0; i < unsorted.Length / bigSeed.Length; i++) +161 { +162 Array.Copy(bigSeed, 0, unsorted, i * bigSeed.Length, bigSeed.Length); +163 } +164 +165 return unsorted; +166 } +167 +168 static void OptimizedQuickSort(int[] unsorted, int left, int right, +169 QuickSortPivotSelectionType pivotSelection) +170 { +171 // left 为子数列的最左边元素 +172 // right 为子数列的最右边元素 +173 if (!(left < right)) return; +174 +175 // Partition: +176 // 所有元素比主元值小的摆放在主元的左边, +177 // 所有元素比主元值大的摆放在主元的右边 +178 Tuple pivotPair = +179 OptimizedPartition(unsorted, left, right, pivotSelection); +180 +181 // Recursively: +182 // 分别排列小于主元的值和大于主元的值的子数列 +183 // 主元无需参加下一次排序 +184 OptimizedQuickSort(unsorted, left, pivotPair.Item1 - 1, pivotSelection); +185 OptimizedQuickSort(unsorted, pivotPair.Item2 + 1, right, pivotSelection); +186 } +187 +188 static Tuple OptimizedPartition( +189 int[] unsorted, int left, int right, +190 QuickSortPivotSelectionType pivotSelection) +191 { +192 int pivotIndex = SelectPivot(unsorted, left, right, pivotSelection); +193 int sentinel = unsorted[pivotIndex]; +194 +195 // 子数组长度为 right - left + 1 +196 int i = left - 1; +197 int j = right + 1; +198 while (true) +199 { +200 while (unsorted[++i] < sentinel) ; +201 while (unsorted[--j] > sentinel) ; +202 if (i >= j) break; +203 +204 // 在主元左侧找到一个大于主元值的位置 i, +205 // 在主元右侧找到一个小于主元值的位置 j, +206 // 交换两个值 +207 Swap(unsorted, i, j); +208 } +209 +210 return new Tuple(i, j); +211 } +212 +213 static int SelectPivot(int[] unsorted, int left, int right, +214 QuickSortPivotSelectionType pivotSelection) +215 { +216 switch (pivotSelection) +217 { +218 case QuickSortPivotSelectionType.FirstAsPivot: +219 return left; +220 case QuickSortPivotSelectionType.MiddleAsPivot: +221 return (left + right) / 2; +222 case QuickSortPivotSelectionType.LastAsPivot: +223 return right; +224 case QuickSortPivotSelectionType.RandomizedPivot: +225 { +226 // 在区间内随机选择位置 +227 if (right - left > 1) +228 { +229 return random.Next(left, right); +230 } +231 else +232 { +233 return left; +234 } +235 } +236 case QuickSortPivotSelectionType.BalancedPivot: +237 { +238 // 选择起始、中间和结尾位置中的中位数 +239 int leftValue = unsorted[left]; +240 int middleValue = unsorted[(left + right) / 2]; +241 int rightValue = unsorted[right]; +242 +243 if (leftValue < middleValue) +244 { +245 if (middleValue < rightValue) +246 { +247 return (left + right) / 2; +248 } +249 else +250 { +251 return right; +252 } +253 } +254 else +255 { +256 if (leftValue < rightValue) +257 { +258 return left; +259 } +260 else +261 { +262 return right; +263 } +264 } +265 } +266 } +267 +268 return (left + right) / 2; +269 } +270 +271 static void PrintArray(int[] unsorted) +272 { +273 if (!isPrintArrayEnabled) return; +274 +275 foreach (var item in unsorted) +276 { +277 Console.Write("{0} ", item); +278 } +279 Console.WriteLine(); +280 } +281 +282 static Random random = new Random(new Guid().GetHashCode()); +283 +284 enum QuickSortPivotSelectionType +285 { +286 FirstAsPivot, +287 MiddleAsPivot, +288 LastAsPivot, +289 RandomizedPivot, +290 BalancedPivot, +291 } +292 } +``` + + +## 5 选择排序(Selection Sort) +### 算法原理 +首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 + +![](image/排序算法-选择排序.gif) +### 算法复杂度 +选择排序的交换操作介于 0 和 (n-1) 次之间。选择排序的比较操作为 n(n-1)/2 次之间。选择排序的赋值操作介于 0 和 3(n-1) 次之间。 + +比较次数 O(n2),比较次数与关键字的初始状态无关,总的比较次数 N = (n-1)+(n-2)+...+1 = n*(n-1)/2。交换次数 O(n),最好情况是,已经有序,交换 0 次;最坏情况是,逆序,交换 n-1 次。交换次数比冒泡排序较少,由于交换所需 CPU 时间比比较所需的 CPU 时间多,n 值较小时,选择排序比冒泡排序快。 + +原地操作几乎是选择排序的唯一优点,当空间复杂度(space complexity)要求较高时,可以考虑选择排序,实际适用的场合非常罕见。 + +* 最差时间复杂度 О(n²) +* 平均时间复杂度 О(n²) +* 最优时间复杂度 О(n²) +* 最差空间复杂度 О(n),辅助空间 O(1) + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 SelectionSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void SelectionSort(int[] unsorted) +18 { +19 // advance the position through the entire array +20 // could do i < n-1 because single element is also min element +21 for (int i = 0; i < unsorted.Length - 1; i++) +22 { +23 // find the min element in the unsorted a[i .. n-1] +24 // assume the min is the first element +25 int min = i; +26 +27 // test against elements after i to find the smallest +28 for (int j = i + 1; j < unsorted.Length; j++) +29 { +30 // if this element is less, then it is the new minimum +31 if (unsorted[j] < unsorted[min]) +32 { +33 // found new minimum, remember its index +34 min = j; +35 } +36 } +37 +38 // swap +39 if (min != i) +40 { +41 int temp = unsorted[i]; +42 unsorted[i] = unsorted[min]; +43 unsorted[min] = temp; +44 } +45 } +46 } +47 } +``` + +## 6 插入排序(Insertion Sort) +### 算法原理 +对于未排序数据,在已排序序列中从后向前扫描,找到相应位置,将位置后的已排序数据逐步向后挪位,将新元素插入到该位置。 +![](image/排序算法-插入排序.jpg) + +### 算法描述 +1. 从第一个元素开始,该元素可以认为已经被排序; +2. 取出下一个元素,在已经排序的元素序列中从后向前扫描; +3. 如果该元素(已排序)大于新元素,将该元素移到下一位置; +4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置; +5. 将新元素插入到该位置后; +6. 重复步骤 2~5; + +### 算法复杂度 +* In-Place 原地排序(即只需要用到 O(1) 的额外空间) +* 最差时间复杂度 O(n2) +* 平均时间复杂度 O(n2) +* 最优时间复杂度 O(n) +* 最差空间复杂度 O(n),辅助空间 O(1) + +插入排序算法的内循环是紧密的,对小规模输入来说是一个快速的原地排序算法。 + +### 示例代码 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 InsertionSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void InsertionSort(int[] unsorted) +18 { +19 for (int i = 1; i < unsorted.Length; i++) +20 { +21 if (unsorted[i - 1] > unsorted[i]) +22 { +23 int key = unsorted[i]; +24 int j = i; +25 while (j > 0 && unsorted[j - 1] > key) +26 { +27 unsorted[j] = unsorted[j - 1]; +28 j--; +29 } +30 unsorted[j] = key; +31 } +32 } +33 } +34 } +``` + +## 7 希尔排序(Shell Sort) +### 算法原理 +希尔排序是插入排序的一种更高效的改进版本,其基于插入排序的以下两个特点提出改进方法: +* 插入排序在对几乎已经排序的数据操作时,效率高,可以达到线性时间。 +* 插入排序一般来说是低效的,其每次只能将数据移动一位。 + +### 算法描述 +希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了,此时插入排序较快。 + +假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为 O(n2) 的排序(冒泡排序或插入排序),可能会进行 n 次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少量比较和交换即可移到正确位置。 +![](image/排序算法-希尔排序.png) + +### 步长序列(Gap Sequences) +步长的选择是希尔排序的重要部分。只要最终步长为 1 任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为 1 进行排序。当步长为 1 时,算法变为插入排序,这就保证了数据一定会被排序。 + +已知的最好步长串行是由 Sedgewick 提出的 (1, 5, 19, 41, 109,...),该步长的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式。这项研究也表明 "比较在希尔排序中是最主要的操作,而不是交换。" 用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。 + +### 算法复杂度 +* 最差时间复杂度 O(nlog2 n) +* 平均时间复杂度 依赖于步长间隔 O(nlog2 n) +* 最优时间复杂度 O(nlogn) +* 最差空间复杂度 O(n),辅助空间 O(1) + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 ShellSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void ShellSort(int[] unsorted) +18 { +19 int gap = (int)Math.Ceiling(unsorted.Length / 2D); +20 +21 // start with the largest gap and work down to a gap of 1 +22 while (gap > 0) +23 { +24 // do a gapped insertion sort for this gap size. +25 // the first gap elements a[0..gap-1] are already in gapped order +26 // keep adding one more element until the entire array is gap sorted +27 for (int i = 0; i < unsorted.Length; i++) +28 { +29 // add a[i] to the elements that have been gap sorted +30 // save a[i] in temp and make a hole at position i +31 int j = i; +32 int temp = unsorted[i]; +33 +34 // shift earlier gap-sorted elements up +35 // until the correct location for a[i] is found +36 while (j >= gap && unsorted[j - gap] > temp) +37 { +38 unsorted[j] = unsorted[j - gap]; +39 j = j - gap; +40 } +41 +42 // put temp (the original a[i]) in its correct location +43 unsorted[j] = temp; +44 } +45 +46 // change gap +47 gap = (int)Math.Floor(0.5 + gap / 2.2); +48 } +49 } +50 } +``` + +## 8 合并排序(Merge Sort) +### 算法原理 +合并排序是分治法的典型应用。 +分治法(Divide-and-Conquer):将原问题划分成 n 个规模较小而结构与原问题相似的子问题;递归地解决这些问题,然后再合并其结果,就得到原问题的解。 +分治模式在每一层上都有三个步骤: +1. 分解(Divide):将原问题分解成一系列子问题; +3. 解决(Conquer):递归地解决各个子问题。若子问题足够小,则直接求解; +3. 合并(Combine):将子问题的结果合并成原问题的解。 + +合并排序算法完全依照了上述模式: +1. 分解:将 n 个元素分成各含 n/2 个元素的子序列; +2. 解决:用合并排序法对两个子序列递归地排序; +3. 合并:合并两个已排序的子序列以得到排序结果。 + +![](image/排序算法-合并排序.gif) + +### 算法描述 +1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列; +2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置; +3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; +4. 重复步骤 3 直到某一指针到达序列尾; +5. 将另一序列剩下的所有元素直接复制到合并序列尾; + +### 算法复杂度 +* 最差时间复杂度 Θ(n*logn) +* 平均时间复杂度 Θ(n*logn) +* 最优时间复杂度 Θ(n) +* 最差空间复杂度 Θ(n) +* 合并排序有着较好的渐进运行时间 Θ(nlogn),但其中的 merge 操作不是 in-place 操作。 + +### 示例代码 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 int[] temp = new int[unsorted.Length]; + 8 MergeSort(unsorted, 0, unsorted.Length, temp); + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void MergeSort(int[] unsorted, int left, int right, int[] temp) +18 { +19 if (left + 1 < right) +20 { +21 // divide +22 int mid = (left + right) / 2; +23 +24 // conquer +25 MergeSort(unsorted, left, mid, temp); +26 MergeSort(unsorted, mid, right, temp); +27 +28 // combine +29 Merge(unsorted, left, mid, right, temp); +30 } +31 } +32 +33 static void Merge(int[] unsorted, int left, int mid, int right, int[] temp) +34 { +35 int leftPosition = left; +36 int rightPosition = mid; +37 int numberOfElements = 0; +38 +39 // merge two slots +40 while (leftPosition < mid && rightPosition < right) +41 { +42 if (unsorted[leftPosition] < unsorted[rightPosition]) +43 { +44 temp[numberOfElements++] = unsorted[leftPosition++]; +45 } +46 else +47 { +48 temp[numberOfElements++] = unsorted[rightPosition++]; +49 } +50 } +51 +52 // add remaining +53 while (leftPosition < mid) +54 { +55 temp[numberOfElements++] = unsorted[leftPosition++]; +56 } +57 while (rightPosition < right) +58 { +59 temp[numberOfElements++] = unsorted[rightPosition++]; +60 } +61 +62 // copy back +63 for (int n = 0; n < numberOfElements; n++) +64 { +65 unsorted[left + n] = temp[n]; +66 } +67 } +68 } +``` + +## 9 堆排序(Heap Sort) + +### 算法原理 + +堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。二叉堆数据结构是一种数组对象,它可以被视为一棵完全二叉树。树中每个节点与数组中存放该节点值的那个元素对应。 + +二叉堆有两种,最大堆和最小堆。最大堆特性是指除了根以外的每个节点 i ,有 A(Parent(i)) ≥ A[i] ,即某个节点的值至多是和其父节点的值一样大。最小堆特性是指除了根以外的每个节点 i ,有 A(Parent(i)) ≤ A[i] ,最小堆的最小元素在根部。 + +在堆排序算法中,我们使用的是最大堆。最小堆通常在构造有限队列时使用。 + +堆可以被看成一棵树,节点在堆中的高度定义为从本节点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度。因为具有 n 个元素的堆是基于一棵完全二叉树,因而其高度为 Θ(lg n) 。 + +![](image/排序算法-堆排序.png) +### 堆节点的访问 +通常堆是通过一维数组来实现的。在数组起始为 0 的情形中,如果 i 为当前节点的索引,则有 +* 父节点在位置 floor((i-1)/2); +* 左子节点在位置 (2*i+1); +* 右子节点在位置 (2*i+2); + +### 堆的操作 +在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作: +* 最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点,保持最大堆性质的关键。运行时间为 O(lg n)。 +* 创建最大堆(Build-Max-Heap):在无序的输入数组基础上构造出最大堆。运行时间为 O(n)。 +* 堆排序(HeapSort):对一个数组进行原地排序,卸载位在第一个数据的根节点,并做最大堆调整的递归运算。运行时间为 O(n*lg n)。 +* 抽取最大值(Extract-Max):相当于执行一次最大堆调整,最大值在根处。运行时间为 O(lg n)。 + +### 算法描述 +1. 初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个 堆,这时堆的根节点的数最大 +2. 然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆 +3. 依此类推,直到只有两个节点的堆,并对 它们作交换,最后得到有n个节点的有序序列 +### 算法复杂度 +* 最差时间复杂度 O(n*logn) +* 平均时间复杂度 Θ(n*logn) +* 最优时间复杂度 O(n*logn) +* 最差空间复杂度 O(n),辅助空间 O(1) + +### 示例代码 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 }; + 6 + 7 HeapSort(unsorted); + 8 + 9 foreach (var key in unsorted) +10 { +11 Console.Write("{0} ", key); +12 } +13 +14 Console.Read(); +15 } +16 +17 static void HeapSort(int[] unsorted) +18 { +19 // build the heap in array so that largest value is at the root +20 BuildMaxHeap(unsorted); +21 +22 // swap root node and the last heap node +23 for (int i = unsorted.Length - 1; i >= 1; i--) +24 { +25 // array[0] is the root and largest value. +26 // the swap moves it in front of the sorted elements +27 int max = unsorted[0]; +28 unsorted[0] = unsorted[i]; +29 unsorted[i] = max; // now, the largest one is at the end +30 +31 // the swap ruined the heap property, so restore it +32 // the heap size is reduced by one +33 MaxHeapify(unsorted, 0, i - 1); +34 } +35 } +36 +37 static void BuildMaxHeap(int[] unsorted) +38 { +39 // put elements of array in heap order, in-place +40 // start is assigned the index in array of the last parent node +41 // the last element in 0-based array is at index count-1; +42 // find the parent of that element +43 for (int i = (unsorted.Length / 2) - 1; i >= 0; i--) +44 { +45 // sift down the node at index start to the proper place +46 // such that all nodes below the start index are in heap order +47 MaxHeapify(unsorted, i, unsorted.Length - 1); +48 } +49 // after sifting down the root all nodes/elements are in heap order +50 } +51 +52 static void MaxHeapify(int[] unsorted, int root, int bottom) +53 { +54 int rootValue = unsorted[root]; +55 int maxChild = root * 2 + 1; // start from left child +56 +57 // while the root has at least one child +58 while (maxChild <= bottom) +59 { +60 // more children +61 if (maxChild < bottom) +62 { +63 // if there is a right child and that child is greater +64 if (unsorted[maxChild] < unsorted[maxChild + 1]) +65 { +66 maxChild = maxChild + 1; +67 } +68 } +69 +70 // compare roots and the older children +71 if (rootValue < unsorted[maxChild]) +72 { +73 unsorted[root] = unsorted[maxChild]; +74 root = maxChild; +75 +76 // repeat to continue sifting down the child now +77 maxChild = root * 2 + 1; // continue from left child +78 } +79 else +80 { +81 maxChild = bottom + 1; +82 } +83 } +84 +85 unsorted[root] = rootValue; +86 } +87 } +``` + +## 9 内省排序(Introspective Sort) +### 算法原理 + +内省排序(Introsort)是由 David Musser 在 1997 年设计的排序算法。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。采用这个方法,内省排序既能在常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持 O(nlog n) 的时间复杂度。由于这两种算法都属于比较排序算法,所以内省排序也是一个比较排序算法。 + +在快速排序算法中,一个关键操作就是选择主元(Pivot):数列将被此主元位置分开成两部分。最简单的主元选择算法是使用第一个或者最后一个元素,但这在排列已部分有序的序列上性能很差。Niklaus Wirth 为此设计了一个快速排序的变体,使用处于中间的元素来防止在某些特定序列上性能退化为 O(n2) 的状况。该 median-of-3 选择算法从序列的第一、中间和最后一个元素取得中位数来作为主元。虽然这个算法在现实世界的数据上性能表现良好,但经过精心设计的序列仍能大幅降低此算法性能。这样就有攻击者精心设计序列发送到因特网服务器以进行拒绝服务(DOS:Denial of Service)攻击的潜在可能性。 + + +Musser 研究指出,在为 median-of-3 选择算法精心设计的 100,000 个元素序列上,内省排序算法的运行时间是快速排序的 1/200。在 Musser 的算法中,最终较小范围内数据的排序由 Robert Sedgewick 提出的小数据排序算法完成。 + +### 算法复杂度 +* 最差时间复杂度 O(n*log n) +* 平均时间复杂度 O(n*log n) +* 最优时间复杂度 O(n*log n) + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { + 6 4, 1, 5, 2, 6, 3, 7, 9, 8, 10, + 7 20, 11, 19, 12, 18, 17, 15, 16, 13, 14 + 8 }; + 9 + 10 GenericQuickSorter.DepthLimitedQuickSort( + 11 unsorted, 0, unsorted.Length - 1, + 12 GenericQuickSorter.QuickSortDepthThreshold); + 13 + 14 foreach (var key in unsorted) + 15 { + 16 Console.Write("{0} ", key); + 17 } + 18 + 19 Console.Read(); + 20 } + 21 } + 22 + 23 internal class GenericQuickSorter + 24 where T : IComparable + 25 { + 26 internal const int QuickSortDepthThreshold = 32; + 27 + 28 internal static void DepthLimitedQuickSort(T[] keys, int left, int right, int depthLimit) + 29 { + 30 do + 31 { + 32 if (depthLimit == 0) + 33 { + 34 Heapsort(keys, left, right); + 35 return; + 36 } + 37 + 38 int i = left; + 39 int j = right; + 40 + 41 // pre-sort the low, middle (pivot), and high values in place. + 42 // this improves performance in the face of already sorted data, or + 43 // data that is made up of multiple sorted runs appended together. + 44 int middle = i + ((j - i) >> 1); + 45 SwapIfGreater(keys, i, middle); // swap the low with the mid point + 46 SwapIfGreater(keys, i, j); // swap the low with the high + 47 SwapIfGreater(keys, middle, j); // swap the middle with the high + 48 + 49 T x = keys[middle]; + 50 do + 51 { + 52 while (keys[i].CompareTo(x) < 0) i++; + 53 while (x.CompareTo(keys[j]) < 0) j--; + 54 Contract.Assert(i >= left && j <= right, + 55 "(i>=left && j<=right) Sort failed - Is your IComparer bogus?"); + 56 + 57 if (i > j) break; + 58 if (i < j) + 59 { + 60 T key = keys[i]; + 61 keys[i] = keys[j]; + 62 keys[j] = key; + 63 } + 64 i++; + 65 j--; + 66 } while (i <= j); + 67 + 68 // The next iteration of the while loop is to + 69 // "recursively" sort the larger half of the array and the + 70 // following calls recrusively sort the smaller half. + 71 // So we subtrack one from depthLimit here so + 72 // both sorts see the new value. + 73 depthLimit--; + 74 + 75 if (j - left <= right - i) + 76 { + 77 if (left < j) DepthLimitedQuickSort(keys, left, j, depthLimit); + 78 left = i; + 79 } + 80 else + 81 { + 82 if (i < right) DepthLimitedQuickSort(keys, i, right, depthLimit); + 83 right = j; + 84 } + 85 } while (left < right); + 86 } + 87 + 88 private static void SwapIfGreater(T[] keys, int a, int b) + 89 { + 90 if (a != b) + 91 { + 92 if (keys[a] != null && keys[a].CompareTo(keys[b]) > 0) + 93 { + 94 T key = keys[a]; + 95 keys[a] = keys[b]; + 96 keys[b] = key; + 97 } + 98 } + 99 } +100 +101 private static void Heapsort(T[] keys, int lo, int hi) +102 { +103 Contract.Requires(keys != null); +104 Contract.Requires(lo >= 0); +105 Contract.Requires(hi > lo); +106 Contract.Requires(hi < keys.Length); +107 +108 int n = hi - lo + 1; +109 for (int i = n / 2; i >= 1; i = i - 1) +110 { +111 DownHeap(keys, i, n, lo); +112 } +113 for (int i = n; i > 1; i = i - 1) +114 { +115 Swap(keys, lo, lo + i - 1); +116 DownHeap(keys, 1, i - 1, lo); +117 } +118 } +119 +120 private static void DownHeap(T[] keys, int i, int n, int lo) +121 { +122 Contract.Requires(keys != null); +123 Contract.Requires(lo >= 0); +124 Contract.Requires(lo < keys.Length); +125 +126 T d = keys[lo + i - 1]; +127 int child; +128 while (i <= n / 2) +129 { +130 child = 2 * i; +131 if (child < n +132 && (keys[lo + child - 1] == null +133 || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0)) +134 { +135 child++; +136 } +137 if (keys[lo + child - 1] == null +138 || keys[lo + child - 1].CompareTo(d) < 0) +139 break; +140 keys[lo + i - 1] = keys[lo + child - 1]; +141 i = child; +142 } +143 keys[lo + i - 1] = d; +144 } +145 +146 private static void Swap(T[] a, int i, int j) +147 { +148 if (i != j) +149 { +150 T t = a[i]; +151 a[i] = a[j]; +152 a[j] = t; +153 } +154 } +155 } +.NET Array.Sort 实现 +微软 .NET 框架中 Array.Sort 方法的实现使用了内省排序(Introspective Sort)算法: +当分区大小(Partition Size)小于 16 个元素时,使用插入排序(Insertion Sort)算法。 +当分区的数量超过 2 * LogN 时,N 是输入数组的范围,使用堆排序(Heapsort)算法。 +否则,使用快速排序(Quicksort)算法。 +这种实现也是不稳定的排序,也就是说,如果两个元素相等,则并不能保证它们的顺序,而相反一个稳定的排序算法则会保持相等元素原来的顺序。 +如果数组的长度为 n ,则该实现的平均情况为 O(n log n) ,最坏情况为 O(n2)。 +注:.NET 4.5 中使用这里描述的算法,而 .NET 4.0 及以前版本使用上面描述的快速排序和堆排序组合的内省排序算法。 + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = { + 6 4, 1, 5, 2, 6, 3, 7, 9, 8, 10, + 7 20, 11, 19, 12, 18, 17, 15, 16, 13, 14 + 8 }; + 9 + 10 GenericIntroSorter.IntrospectiveSort(unsorted, 0, unsorted.Length); + 11 + 12 foreach (var key in unsorted) + 13 { + 14 Console.Write("{0} ", key); + 15 } + 16 + 17 Console.Read(); + 18 } + 19 } + 20 + 21 internal class GenericIntroSorter + 22 where T : IComparable + 23 { + 24 internal const int IntrosortSwitchToInsertionSortSizeThreshold = 16; + 25 + 26 internal static void IntrospectiveSort(T[] keys, int left, int length) + 27 { + 28 Contract.Requires(keys != null); + 29 Contract.Requires(left >= 0); + 30 Contract.Requires(length >= 0); + 31 Contract.Requires(length <= keys.Length); + 32 Contract.Requires(length + left <= keys.Length); + 33 + 34 if (length < 2) + 35 return; + 36 + 37 IntroSort(keys, left, length + left - 1, 2 * FloorLog2(keys.Length)); + 38 } + 39 + 40 private static void IntroSort(T[] keys, int lo, int hi, int depthLimit) + 41 { + 42 Contract.Requires(keys != null); + 43 Contract.Requires(lo >= 0); + 44 Contract.Requires(hi < keys.Length); + 45 + 46 while (hi > lo) + 47 { + 48 int partitionSize = hi - lo + 1; + 49 if (partitionSize <= IntrosortSwitchToInsertionSortSizeThreshold) + 50 { + 51 if (partitionSize == 1) + 52 { + 53 return; + 54 } + 55 if (partitionSize == 2) + 56 { + 57 SwapIfGreaterWithItems(keys, lo, hi); + 58 return; + 59 } + 60 if (partitionSize == 3) + 61 { + 62 SwapIfGreaterWithItems(keys, lo, hi - 1); + 63 SwapIfGreaterWithItems(keys, lo, hi); + 64 SwapIfGreaterWithItems(keys, hi - 1, hi); + 65 return; + 66 } + 67 + 68 InsertionSort(keys, lo, hi); + 69 return; + 70 } + 71 + 72 if (depthLimit == 0) + 73 { + 74 Heapsort(keys, lo, hi); + 75 return; + 76 } + 77 depthLimit--; + 78 + 79 int p = PickPivotAndPartition(keys, lo, hi); + 80 + 81 // Note we've already partitioned around the pivot + 82 // and do not have to move the pivot again. + 83 IntroSort(keys, p + 1, hi, depthLimit); + 84 hi = p - 1; + 85 } + 86 } + 87 + 88 private static int PickPivotAndPartition(T[] keys, int lo, int hi) + 89 { + 90 Contract.Requires(keys != null); + 91 Contract.Requires(lo >= 0); + 92 Contract.Requires(hi > lo); + 93 Contract.Requires(hi < keys.Length); + 94 Contract.Ensures(Contract.Result() >= lo && Contract.Result() <= hi); + 95 + 96 // Compute median-of-three. But also partition them. + 97 int middle = lo + ((hi - lo) / 2); + 98 + 99 // Sort lo, mid and hi appropriately, then pick mid as the pivot. +100 SwapIfGreaterWithItems(keys, lo, middle); // swap the low with the mid point +101 SwapIfGreaterWithItems(keys, lo, hi); // swap the low with the high +102 SwapIfGreaterWithItems(keys, middle, hi); // swap the middle with the high +103 +104 T pivot = keys[middle]; +105 Swap(keys, middle, hi - 1); +106 +107 // We already partitioned lo and hi and put the pivot in hi - 1. +108 // And we pre-increment & decrement below. +109 int left = lo, right = hi - 1; +110 +111 while (left < right) +112 { +113 if (pivot == null) +114 { +115 while (left < (hi - 1) && keys[++left] == null) ; +116 while (right > lo && keys[--right] != null) ; +117 } +118 else +119 { +120 while (pivot.CompareTo(keys[++left]) > 0) ; +121 while (pivot.CompareTo(keys[--right]) < 0) ; +122 } +123 +124 if (left >= right) +125 break; +126 +127 Swap(keys, left, right); +128 } +129 +130 // Put pivot in the right location. +131 Swap(keys, left, (hi - 1)); +132 +133 return left; +134 } +135 +136 private static void Heapsort(T[] keys, int lo, int hi) +137 { +138 Contract.Requires(keys != null); +139 Contract.Requires(lo >= 0); +140 Contract.Requires(hi > lo); +141 Contract.Requires(hi < keys.Length); +142 +143 int n = hi - lo + 1; +144 for (int i = n / 2; i >= 1; i = i - 1) +145 { +146 DownHeap(keys, i, n, lo); +147 } +148 for (int i = n; i > 1; i = i - 1) +149 { +150 Swap(keys, lo, lo + i - 1); +151 DownHeap(keys, 1, i - 1, lo); +152 } +153 } +154 +155 private static void DownHeap(T[] keys, int i, int n, int lo) +156 { +157 Contract.Requires(keys != null); +158 Contract.Requires(lo >= 0); +159 Contract.Requires(lo < keys.Length); +160 +161 T d = keys[lo + i - 1]; +162 int child; +163 while (i <= n / 2) +164 { +165 child = 2 * i; +166 if (child < n +167 && (keys[lo + child - 1] == null +168 || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0)) +169 { +170 child++; +171 } +172 if (keys[lo + child - 1] == null +173 || keys[lo + child - 1].CompareTo(d) < 0) +174 break; +175 keys[lo + i - 1] = keys[lo + child - 1]; +176 i = child; +177 } +178 keys[lo + i - 1] = d; +179 } +180 +181 private static void InsertionSort(T[] keys, int lo, int hi) +182 { +183 Contract.Requires(keys != null); +184 Contract.Requires(lo >= 0); +185 Contract.Requires(hi >= lo); +186 Contract.Requires(hi <= keys.Length); +187 +188 int i, j; +189 T t; +190 for (i = lo; i < hi; i++) +191 { +192 j = i; +193 t = keys[i + 1]; +194 while (j >= lo && (t == null || t.CompareTo(keys[j]) < 0)) +195 { +196 keys[j + 1] = keys[j]; +197 j--; +198 } +199 keys[j + 1] = t; +200 } +201 } +202 +203 private static void SwapIfGreaterWithItems(T[] keys, int a, int b) +204 { +205 Contract.Requires(keys != null); +206 Contract.Requires(0 <= a && a < keys.Length); +207 Contract.Requires(0 <= b && b < keys.Length); +208 +209 if (a != b) +210 { +211 if (keys[a] != null && keys[a].CompareTo(keys[b]) > 0) +212 { +213 T key = keys[a]; +214 keys[a] = keys[b]; +215 keys[b] = key; +216 } +217 } +218 } +219 +220 private static void Swap(T[] a, int i, int j) +221 { +222 if (i != j) +223 { +224 T t = a[i]; +225 a[i] = a[j]; +226 a[j] = t; +227 } +228 } +229 +230 private static int FloorLog2(int n) +231 { +232 int result = 0; +233 while (n >= 1) +234 { +235 result++; +236 n = n / 2; +237 } +238 return result; +239 } +240 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.5 排序算法-线性排序.md b/Algorithm/A类:基本算法/3.5 排序算法-线性排序.md new file mode 100644 index 00000000..773d02ac --- /dev/null +++ b/Algorithm/A类:基本算法/3.5 排序算法-线性排序.md @@ -0,0 +1,406 @@ +# 线性时间排序算法 +## 0 线性时间排序算法列表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

线性时间排序

+
NameAverageWorstMemoryStable +

Description

+
+

 计数排序

+

(Counting Sort)

+
+

n + k

+
+

n + k

+
+

n + k

+
+

Stable

+
+

 Indexes using key values.

+
+

 基数排序

+

(Radix Sort)

+
+

n * k

+
 n * kn + kStable +

 Examines individual bits of keys.

+
+

 桶排序

+

(Bucket Sort)

+
+

n + k

+
+

n2

+
+

n * k

+
+

Stable

+
+

 Examine bits of keys.

+
+ +### 特点 + +给定含 n 个元素的输入序列,任何比较排序在最坏情况下都需要 Ω(n log n) 次比较来进行排序。合并排序和堆排序在最坏情况下达到上界 O(n log n),它们都是渐进最优的排序算法,快速排序在平均情况下达到上界 O(n log n)。 + +本文介绍的三种以线性时间运行的算法:计数排序、基数排序和桶排序,都用非比较的一些操作来确定排序顺序。因此,下界 Ω(n log n) 对它们是不适用的。 + +## 1 计数排序(Counting Sort) +### 算法原理 +计数排序(Counting Sort)假设 n 个输入元素中的每一个都是介于 0 到 k 之间的整数,此处 k 为某个整数。 +计数排序的基本思想就是对每一个输入元素 x,确定出小于 x 的元素个数。有了这一信息,就可以把 x 直接放到它在最终输出数组中的位置上。 +![](image/排序算法-计数排序.jpg) + +例如:有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。 + +### 算法描述 +算法的步骤如下: +1. 找出待排序的数组中最大和最小的元素; +2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项; +3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加); +4. 反向填充目标数组,将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1; +![](image/排序算法-基数排序演示.png) + +### 算法复杂度 +* 最差时间复杂度 O(n + k) +* 平均时间复杂度 O(n + k) +* 最差空间复杂度 O(n + k) + +计数排序不是比较排序,排序的速度快于任何比较排序算法。 +计数排序的一个重要性质就是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的次序相同。 +之所以说计数排序的稳定性非常重要,还有一个原因是因为计数排序经常用作基数排序算法的一个子过程,其稳定性对于基数排序的正确性来说非常关键。 + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = + 6 { + 7 5, 9, 3, 9, 10, 9, 2, 4, 13, 10 + 8 }; + 9 +10 OptimizedCountingSort(unsorted); +11 +12 foreach (var key in unsorted) +13 { +14 Console.Write("{0} ", key); +15 } +16 +17 Console.Read(); +18 } +19 +20 static int[] CountingSort(int[] unsorted) +21 { +22 // find min and max value +23 int min = unsorted[0], max = unsorted[0]; +24 for (int i = 1; i < unsorted.Length; i++) +25 { +26 if (unsorted[i] < min) min = unsorted[i]; +27 else if (unsorted[i] > max) max = unsorted[i]; +28 } +29 +30 // creates k buckets +31 int k = max - min + 1; +32 int[] C = new int[k]; +33 +34 // calculate the histogram of key frequencies +35 for (int i = 0; i < unsorted.Length; i++) +36 { +37 C[unsorted[i] - min]++; +38 } +39 +40 // recalculate +41 C[0]--; +42 for (int i = 1; i < C.Length; i++) +43 { +44 C[i] = C[i] + C[i - 1]; +45 } +46 +47 // sort the array +48 int[] B = new int[unsorted.Length]; +49 for (int i = unsorted.Length - 1; i >= 0; i--) +50 { +51 // keep stable +52 B[C[unsorted[i] - min]--] = unsorted[i]; +53 } +54 +55 return B; +56 } +57 +58 static void OptimizedCountingSort(int[] unsorted) +59 { +60 // find min and max value +61 int min = unsorted[0], max = unsorted[0]; +62 for (int i = 1; i < unsorted.Length; i++) +63 { +64 if (unsorted[i] < min) min = unsorted[i]; +65 else if (unsorted[i] > max) max = unsorted[i]; +66 } +67 +68 // creates k buckets +69 int k = max - min + 1; +70 int[] C = new int[k]; +71 +72 // calculate the histogram of key frequencies +73 for (int i = 0; i < unsorted.Length; i++) +74 { +75 C[unsorted[i] - min]++; +76 } +77 +78 // copy to output array, +79 // preserving order of inputs with equal keys +80 int increment = 0; +81 for (int i = min; i <= max; i++) +82 { +83 for (int j = 0; j < C[i - min]; j++) +84 { +85 // in place, may not stable if you care +86 unsorted[increment++] = i; +87 } +88 } +89 } +90 } +``` + +## 基数排序(Radix Sort) +### 算法原理 + +基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数值按相同的有效位进行分组,然后在有效位区间内进行排序。 + +### 算法描述 +每个元素值首先被放入一个该值的最右位所对应的桶中,桶内会保持被放入元素值最初的顺序。这使得桶的数量和值的数量能够根据其最右位建立一对一的关系。然后,通过相同的方式重复处理下一位,直到所有的位都已被处理。 +1. 获得值的最右侧的最小的位。 +2. 根据该位的值将数组内的元素值进行分组,但仍然保持元素的顺序。(以此来保持算法稳定性) +3. 重复上述分组过程,直到所有的位都已被处理。 + +上述第 2 步中通常可以使用桶排序(Bucket Sort)或计数排序(Counting Sort)算法,因为它们在元素较少时拥有更好的效率。 +![](image/排序算法-基数排序.jpg) + + +基数排序中可以选择采用最低有效位基数排序(LSD Radix Sort:Least Significant Digit Radix Sort)或最高有效位基数排序(MSD Radix Sort:Most Significant Digit Radix Sort)。LSD 的排序方式由值的最低位也就是最右边开始,而 MSD 则相反,由值的最高位也就是最左边开始。 + +![](image/排序算法-基数排序过程.png) + +例如,如下这个无序的数列需要排序: +``` +  170, 45, 75, 90, 802, 2, 24, 66 +``` +使用 LSD 方式从最低位开始(个位)排序的结果是: +``` +  170, 90, 802, 2, 24, 45, 75, 66 +``` +再继续从下一位(十位)继续排序的结果是: +``` +  802, 2, 24, 45, 66, 170, 75, 90 +``` +再继续从下一位(百位)继续排序的结果是: +``` +  2, 24, 45, 66, 75, 90, 170, 802 +``` +### 算法复杂度 +* 最差时间复杂度 O(k*n) +* 最差空间复杂度 O(k*n) + +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = + 6 { + 7 15, 19, 13, 19, 10, 33, 12, 14, 13, 10, + 8 }; + 9 +10 RadixSort(unsorted); +11 +12 foreach (var key in unsorted) +13 { +14 Console.Write("{0} ", key); +15 } +16 +17 Console.Read(); +18 } +19 +20 static void RadixSort(int[] unsorted) +21 { +22 // our helper array +23 int[] t = new int[unsorted.Length]; +24 +25 // number of bits our group will be long +26 // try to set this also to 2, 8 or 16 to see if it is quicker or not +27 int r = 4; +28 +29 // number of bits of a C# int +30 int b = 32; +31 +32 // counting and prefix arrays +33 // (note dimensions 2^r which is the number of +34 // all possible values of a r-bit number) +35 int[] count = new int[1 << r]; +36 int[] pref = new int[1 << r]; +37 +38 // number of groups +39 int groups = (int)Math.Ceiling((double)b / (double)r); +40 +41 // the mask to identify groups +42 int mask = (1 << r) - 1; +43 +44 // the algorithm: +45 for (int c = 0, shift = 0; c < groups; c++, shift += r) +46 { +47 // reset count array +48 for (int j = 0; j < count.Length; j++) +49 count[j] = 0; +50 +51 // counting elements of the c-th group +52 for (int i = 0; i < unsorted.Length; i++) +53 count[(unsorted[i] >> shift) & mask]++; +54 +55 // calculating prefixes +56 pref[0] = 0; +57 for (int i = 1; i < count.Length; i++) +58 pref[i] = pref[i - 1] + count[i - 1]; +59 +60 // from a[] to t[] elements ordered by c-th group +61 for (int i = 0; i < unsorted.Length; i++) +62 t[pref[(unsorted[i] >> shift) & mask]++] = unsorted[i]; +63 +64 // a[]=t[] and start again until the last group +65 t.CopyTo(unsorted, 0); +66 } +67 // a is sorted +68 } +69 } +``` + +## 3 桶排序(Bucket Sort) +### 算法原理 + + +桶排序(Bucket Sort)的工作原理是将数组分解到有限数量的桶里,每个桶再分别进行排序。桶内排序有可能使用其他排序算法或是以递归的方式继续使用桶排序。 + +### 算法描述 +桶排序的步骤: +1. 在数组中查找数值的最大值和最小值; +2. 初始化一个数组当作空桶,长度为 (MaxValue - MinValue + 1)。 +3. 遍历被排序数组,并把数值逐个放入对应的桶中。 +4. 对每个不是空的桶进行排序。 +5. 从不是空的桶里把数值再放回原来的数组中。 + +![](image/排序算法-桶排序.png) + +### 算法复杂度 +* 最差时间复杂度 O(n2) +* 平均时间复杂度 O(n+k) +* 最差空间复杂度 O(n*k) + +当要被排序的数组中的数值是均匀分布时,桶排序的运行时间为线性时间 Θ(n)。桶排序不是比较排序,它不受 Ω(n log n) 下界的影响。 +### 代码示例 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = + 6 { + 7 15, 19, 13, 19, 10, 33, 12, 14, 13, 10, + 8 }; + 9 +10 BucketSort(unsorted); +11 +12 foreach (var key in unsorted) +13 { +14 Console.Write("{0} ", key); +15 } +16 +17 Console.Read(); +18 } +19 +20 static void BucketSort(int[] unsorted) +21 { +22 // find the maximum and minimum values in the array +23 int max = unsorted[0]; //start with first element +24 int min = unsorted[0]; +25 +26 // start from index 1 +27 for (int i = 1; i < unsorted.Length; i++) +28 { +29 if (unsorted[i] < min) min = unsorted[i]; +30 else if (unsorted[i] > max) max = unsorted[i]; +31 } +32 +33 // create a temporary "buckets" to store the values in order +34 // each value will be stored in its corresponding index +35 // scooting everything over to the left as much as possible +36 // e.g. 34 => index at 34 - minValue +37 List[] buckets = new List[max - min + 1]; +38 +39 // initialize the buckets +40 for (int i = 0; i < buckets.Length; i++) +41 { +42 buckets[i] = new List(); +43 } +44 +45 // move items to bucket +46 for (int i = 0; i < unsorted.Length; i++) +47 { +48 buckets[unsorted[i] - min].Add(unsorted[i]); +49 } +50 +51 // move items in the bucket back to the original array in order +52 int k = 0; //index for original array +53 for (int i = 0; i < buckets.Length; i++) +54 { +55 if (buckets[i].Count > 0) +56 { +57 for (int j = 0; j < buckets[i].Count; j++) +58 { +59 unsorted[k] = buckets[i][j]; +60 k++; +61 } +62 } +63 } +64 } +65 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.6 线性时间选择算法.md b/Algorithm/A类:基本算法/3.6 线性时间选择算法.md new file mode 100644 index 00000000..9e5f7ad0 --- /dev/null +++ b/Algorithm/A类:基本算法/3.6 线性时间选择算法.md @@ -0,0 +1,222 @@ +# 线性时间选择算法 + +## 1 问题概述 +在一个由 n 个元素组成的集合中,第 i 个顺序统计量(order statistic)是该集合中第 i 小的元素。也就是说,最小值是第 1 个顺序统计量(i = 1),最大值是第 n 个顺序统计量(i = n)。 +中位数(median)是它所在集合的中点元素。当 n 为奇数时,中位数是唯一的,出现在 i = (n + 1)/2 处。当 n 为偶数时,存在两个中位数,下中位数 i = n/2 和上中位数 i = n/2 + 1 处。因此,不考虑 n 的奇偶性,中位数总是出现在 i = (n+1)/2 的中位数处。本文中所用的中位数总是指下中位数。 +* 选择最大值和最小值 +* 选择中位数或任意位置值 + + +## 1 选择最大值和最小值 + +### 算法原理 +对于确定最大值和最小值的问题,n-1 次比较是最优的。 + +对于同时获取最大值和最小值,至多需要 3(n/2) 次比较就足以同时找到。如果 n 是奇数,那么总共需要 3(n/2) 次比较。如果 n 是偶数,则可先做一次初始比较,接着做 3((n - 2)/2) 次比较。 +### 代码实现 +``` + 1 class Program + 2 { + 3 static void Main(string[] args) + 4 { + 5 int[] unsorted = + 6 { + 7 4, 1, 5, 2, 6, 3, 7, 9, 8, 0 + 8 }; + 9 +10 Console.WriteLine("Min: {0}", GetMinimum(unsorted)); +11 Console.WriteLine("Max: {0}", GetMaximum(unsorted)); +12 +13 int min, max; +14 GetBothMinMax(unsorted, out min, out max); +15 Console.WriteLine("Min: {0}, Max: {1}", min, max); +16 +17 Console.Read(); +18 } +19 +20 static int GetMinimum(int[] a) +21 { +22 int min = a[0]; +23 +24 // n-1 次比较 +25 for (int i = 1; i < a.Length; i++) +26 { +27 if (a[i] < min) +28 min = a[i]; +29 } +30 +31 return min; +32 } +33 +34 static int GetMaximum(int[] a) +35 { +36 int max = a[0]; +37 +38 // n-1 次比较 +39 for (int i = 1; i < a.Length; i++) +40 { +41 if (a[i] > max) +42 max = a[i]; +43 } +44 +45 return max; +46 } +47 +48 static void GetBothMinMax(int[] a, out int min, out int max) +49 { +50 min = a[0]; +51 max = a[0]; +52 +53 if (a.Length % 2 > 0) // n 为奇数 +54 { +55 for (int i = 1; i < a.Length; i = i + 2) +56 { +57 if (a[i] < a[i + 1]) +58 { +59 if (a[i] < min) min = a[i]; +60 if (a[i + 1] > max) max = a[i + 1]; +61 } +62 else +63 { +64 if (a[i + 1] < min) min = a[i + 1]; +65 if (a[i] > max) max = a[i]; +66 } +67 } +68 } +69 else // n 为偶数 +70 { +71 for (int i = 1; i < a.Length - 1; i = i + 2) +72 { +73 if (a[i] < a[i + 1]) +74 { +75 if (a[i] < min) min = a[i]; +76 if (a[i + 1] > max) max = a[i + 1]; +77 } +78 else +79 { +80 if (a[i + 1] < min) min = a[i + 1]; +81 if (a[i] > max) max = a[i]; +82 } +83 } +84 +85 if (a[a.Length - 1] < min) min = a[a.Length - 1]; +86 if (a[a.Length - 1] > max) max = a[a.Length - 1]; +87 } +88 } +89 } +``` + +## 2 选择中位数或任意位置值 +RANDOMIZED-SELECT 算法采用快速排序算法的思想。区别是,快速排序会递归地处理划分的两边,而 RANDOMIZED-SELECT 则只处理一边。所以快速排序的期望运行时间是 Θ(n lg n),而 RANDOMIZED-SELECT 的期望运行时间为 Θ(n)。 + +RANDOMIZED-SELECT 的最坏运行时间为 Θ(n2),即使是要选择最小元素也是如此。因为它是随机化的,该算法的平均情况性能较好。 +``` + 1 public class QuickFindAlgorithm + 2 { + 3 public static void TestRandomizedQuickFind() + 4 { + 5 int[] unsorted = + 6 { + 7 4, 1, 5, 2, 6, 3, 7, 9, 8, 0 + 8 }; + 9 + 10 Console.WriteLine("Find Value : {0}", + 11 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 1)); + 12 Console.WriteLine("Find Value : {0}", + 13 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 2)); + 14 Console.WriteLine("Find Value : {0}", + 15 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 3)); + 16 Console.WriteLine("Find Value : {0}", + 17 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 4)); + 18 Console.WriteLine("Find Value : {0}", + 19 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 5)); + 20 Console.WriteLine("Find Value : {0}", + 21 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 6)); + 22 Console.WriteLine("Find Value : {0}", + 23 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 7)); + 24 Console.WriteLine("Find Value : {0}", + 25 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 8)); + 26 Console.WriteLine("Find Value : {0}", + 27 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 9)); + 28 Console.WriteLine("Find Value : {0}", + 29 RandomizedQuickFind(unsorted, 0, unsorted.Length - 1, 10)); + 30 + 31 int median = RandomizedQuickFind(unsorted, + 32 0, unsorted.Length - 1, (unsorted.Length + 1) / 2); + 33 Console.WriteLine("Find Median : {0}", median); + 34 + 35 Console.Read(); + 36 } + 37 + 38 static int RandomizedQuickFind(int[] a, int p, int r, int i) + 39 { + 40 if (p == r) + 41 return a[p]; + 42 + 43 int q = RandomizedPartition(a, p, r); + 44 int k = q - p + 1; + 45 + 46 if (i == k) // the pivot value is the answer + 47 { + 48 return a[q]; + 49 } + 50 else if (i < k) // i is in left side + 51 { + 52 return RandomizedQuickFind(a, p, q - 1, i); + 53 } + 54 else // i is in right side + 55 { + 56 return RandomizedQuickFind(a, q + 1, r, i - k); + 57 } + 58 } + 59 + 60 static void RandomizedQuickSort(int[] unsorted, int left, int right) + 61 { + 62 if (!(left < right)) return; + 63 + 64 int pivotIndex = RandomizedPartition(unsorted, left, right); + 65 + 66 RandomizedQuickSort(unsorted, left, pivotIndex - 1); + 67 RandomizedQuickSort(unsorted, pivotIndex + 1, right); + 68 } + 69 + 70 static int RandomizedPartition(int[] unsorted, int left, int right) + 71 { + 72 int i = random.Next(left, right); + 73 Swap(unsorted, i, right); + 74 return Partition(unsorted, left, right); + 75 } + 76 + 77 static int Partition(int[] unsorted, int left, int right) + 78 { + 79 int pivotIndex = right; + 80 + 81 // 哨兵 + 82 int sentinel = unsorted[right]; + 83 + 84 // 子数组长度为 right - left + 1 + 85 int i = left - 1; + 86 for (int j = left; j <= right - 1; j++) + 87 { + 88 if (unsorted[j] <= sentinel) + 89 { + 90 i++; + 91 Swap(unsorted, i, j); + 92 } + 93 } + 94 + 95 Swap(unsorted, i + 1, pivotIndex); + 96 + 97 return i + 1; + 98 } + 99 +100 static void Swap(int[] unsorted, int i, int j) +101 { +102 int temp = unsorted[i]; +103 unsorted[i] = unsorted[j]; +104 unsorted[j] = temp; +105 } +106 +107 static Random random = new Random(new Guid().GetHashCode()); +108 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.7 字符串算法-匹配算法.md b/Algorithm/A类:基本算法/3.7 字符串算法-匹配算法.md new file mode 100644 index 00000000..77bd466a --- /dev/null +++ b/Algorithm/A类:基本算法/3.7 字符串算法-匹配算法.md @@ -0,0 +1,602 @@ +# 字符串匹配算法 + +## 概述 + +### 字符串匹配问题的形式定义 +* 文本(Text)是一个长度为 n 的数组 T[1..n]; +* 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]; +* T 和 P 中的元素都属于有限的字母表 Σ 表; +* 如果 0≤s≤n-m,并且 T[s+1..s+m] = P[1..m],即对 1≤j≤m,有 T[s+j] = P[j],则说模式 P 在文本 T 中出现且位移为 s,且称 s 是一个有效位移(Valid Shift)。 + +![](image/字符串匹配算法.png) + +比如上图中,目标是找出所有在文本 T = abcabaabcabac 中模式 P = abaa 的所有出现。该模式在此文本中仅出现一次,即在位移 s = 3 处,位移 s = 3 是有效位移。 + + +### 解决字符串匹配的算法包括 +* 朴素算法(Naive Algorithm) +* Rabin-Karp 算法 +* 有限自动机算法(Finite Automation) +* Knuth-Morris-Pratt 算法(即 KMP Algorithm) +* Boyer-Moore 算法、Simon 算法、Colussi 算法 +* Galil-Giancarlo 算法、Apostolico-Crochemore 算法 +* Horspool 算法和 Sunday 算法等 + + +### 基本步骤和算法效率 + +字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。 + +![](image/字符串匹配算法效率.png) +上图描述了常见字符串匹配算法的预处理和匹配时间。 + +## 1 朴素的字符串匹配算法(Naive String Matching Algorithm) + +### 基本思想 +朴素的字符串匹配算法又称为暴力匹配算法(Brute Force Algorithm),它的主要特点是: +1. 没有预处理阶段; +2. 滑动窗口总是后移 1 位; +3. 对模式中的字符的比较顺序不限定,可以从前到后,也可以从后到前; +4. 匹配阶段需要 O((n - m + 1)m) 的时间复杂度; +5. 需要 2n 次的字符比较; + +很显然,朴素的字符串匹配算法 NAIVE-STRING-MATCHER 是最原始的算法,它通过使用循环来检查是否在范围 n-m+1 中存在满足条件 P[1..m] = T [s + 1..s + m] 的有效位移 s。 + +### 算法原理 +``` +1 NAIVE-STRING-MATCHER(T, P) +2 n ← length[T] +3 m ← length[P] +4 for s ← 0 to n - m +5 do if P[1 .. m] = T[s + 1 .. s + m] +6 then print "Pattern occurs with shift" s +``` + +![](image/字符串匹配算法-算法原理.png) +如上图中,对于模式 P = aab 和文本 T = acaabc,将模式 P 沿着 T 从左到右滑动,逐个比较字符以判断模式 P 在文本 T 中是否存在。 + +### NAIVE-STRING-MATCHER时间效率 +可以看出,NAIVE-STRING-MATCHER 没有对模式 P 进行预处理,所以预处理的时间为 0。而匹配的时间在最坏情况下为 Θ((n-m+1)m),如果 m = [n/2],则为 Θ(n2)。 + +### NAIVE-STRING-MATCHER 的代码示例。 +``` + 1 namespace StringMatching + 2 { + 3 class Program + 4 { + 5 static void Main(string[] args) + 6 { + 7 char[] text1 = "BBC ABCDAB ABCDABCDABDE".ToCharArray(); + 8 char[] pattern1 = "ABCDABD".ToCharArray(); + 9 + 10 int firstShift1; + 11 bool isMatched1 = NaiveStringMatcher.TryMatch1(text1, pattern1, out firstShift1); + 12 Contract.Assert(isMatched1); + 13 Contract.Assert(firstShift1 == 15); + 14 + 15 char[] text2 = "ABABDAAAACAAAABCABAB".ToCharArray(); + 16 char[] pattern2 = "AAACAAAA".ToCharArray(); + 17 + 18 int firstShift2; + 19 bool isMatched2 = NaiveStringMatcher.TryMatch2(text2, pattern2, out firstShift2); + 20 Contract.Assert(isMatched2); + 21 Contract.Assert(firstShift2 == 6); + 22 + 23 char[] text3 = "ABAAACAAAAAACAAAABCABAAAACAAAAFDLAAACAAAAAACAAAA".ToCharArray(); + 24 char[] pattern3 = "AAACAAAA".ToCharArray(); + 25 + 26 int[] shiftIndexes = NaiveStringMatcher.MatchAll(text3, pattern3); + 27 Contract.Assert(shiftIndexes.Length == 5); + 28 Contract.Assert(string.Join(",", shiftIndexes) == "2,9,22,33,40"); + 29 + 30 Console.WriteLine("Well done!"); + 31 Console.ReadKey(); + 32 } + 33 } + 34 + 35 public class NaiveStringMatcher + 36 { + 37 public static bool TryMatch1(char[] text, char[] pattern, out int firstShift) + 38 { + 39 firstShift = -1; + 40 int n = text.Length; + 41 int m = pattern.Length; + 42 int s = 0, j = 0; + 43 + 44 // for..for.. + 45 for (s = 0; s < n - m; s++) + 46 { + 47 for (j = 0; j < m; j++) + 48 { + 49 if (text[s + j] != pattern[j]) + 50 { + 51 break; + 52 } + 53 } + 54 if (j == m) + 55 { + 56 firstShift = s; + 57 return true; + 58 } + 59 } + 60 + 61 return false; + 62 } + 63 + 64 public static bool TryMatch2(char[] text, char[] pattern, out int firstShift) + 65 { + 66 firstShift = -1; + 67 int n = text.Length; + 68 int m = pattern.Length; + 69 int s = 0, j = 0; + 70 + 71 // while.. + 72 while (s < n && j < m) + 73 { + 74 if (text[s] == pattern[j]) + 75 { + 76 s++; + 77 j++; + 78 } + 79 else + 80 { + 81 s = s - j + 1; + 82 j = 0; + 83 } + 84 + 85 if (j == m) + 86 { + 87 firstShift = s - j; + 88 return true; + 89 } + 90 } + 91 + 92 return false; + 93 } + 94 + 95 public static int[] MatchAll(char[] text, char[] pattern) + 96 { + 97 int n = text.Length; + 98 int m = pattern.Length; + 99 int s = 0, j = 0; +100 int[] shiftIndexes = new int[n - m + 1]; +101 int c = 0; +102 +103 // while.. +104 while (s < n && j < m) +105 { +106 if (text[s] == pattern[j]) +107 { +108 s++; +109 j++; +110 } +111 else +112 { +113 s = s - j + 1; +114 j = 0; +115 } +116 +117 if (j == m) +118 { +119 shiftIndexes[c] = s - j; +120 c++; +121 +122 s = s - j + 1; +123 j = 0; +124 } +125 } +126 +127 int[] shifts = new int[c]; +128 for (int y = 0; y < c; y++) +129 { +130 shifts[y] = shiftIndexes[y]; +131 } +132 +133 return shifts; +134 } +135 } +136 } +``` +上面代码中 TryMatch1 和 TryMatch2 分别使用 for 和 while 循环达到相同效果。 + + +## 2 Knuth-Morris-Pratt 字符串匹配算法(即 KMP 算法) + +### 基本思想 +我们来观察一下朴素的字符串匹配算法的操作过程。如下图(a)中所描述,在模式 P = ababaca 和文本 T 的匹配过程中,模板的一个特定位移 s,q = 5 个字符已经匹配成功,但模式 P 的第 6 个字符不能与相应的文本字符匹配。 +![](image/字符串匹配算法-KMP.jpg) + +此时,q 个字符已经匹配成功的信息确定了相应的文本字符,而知道这 q 个文本字符,就使我们能够立即确定某些位移是非法的。例如上图(a)中,我们可以判断位移 s+1 是非法的,因为模式 P 的第一个字符 a 将与模式的第二个字符 b 匹配的文本字符进行匹配,显然是不匹配的。而图(b)中则显示了位移 s’ = s+2 处,使模式 P 的前三个字符和相应的三个文本字符对齐后必定会匹配。KMP 算法的基本思路就是设法利用这些已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。 + +### 算法原理 +> The basic idea behind KMP’s algorithm is: whenever we detect a mismatch (after some matches), we already know some of the characters in the text (since they matched the pattern characters prior to the mismatch). We take advantage of this information to avoid matching the characters that we know will anyway match. + + +已知模式 P[1..q] 与文本 T[s+1..s+q] 匹配,那么满足 P[1..k] = T[s’+1..s’+k] 其中 s’+k = s+q 的最小位移 s’ > s 是多少?这样的位移 s’ 是大于 s 的但未必非法的第一个位移,因为已知 T[s+1..s+q] 。在最好的情况下有 s’ = s+q,因此立刻能排除掉位移 s+1, s+2 .. s+q-1。在任何情况下,对于新的位移 s’,无需把 P 的前 k 个字符与 T 中相应的字符进行比较,因为它们肯定匹配。 + +可以用模式 P 与其自身进行比较,以预先计算出这些必要的信息。例如上图(c)中所示,由于 T[s’+1..s’+k] 是文本中已经知道的部分,所以它是字符串 Pq 的一个后缀。 + +此处我们引入模式的前缀函数 π(Pai),π 包含有模式与其自身的位移进行匹配的信息。这些信息可用于避免在朴素的字符串匹配算法中,对无用位移进行测试。 +$$ +π[q] = max {k : k < q and Pk ⊐ Pq} +$$ + +π[q] 代表当前字符之前的字符串中,最长的共同前缀后缀的长度。(π[q] is the length of the longest prefix of P that is a proper suffix of Pq.) +下图给出了关于模式 P = ababababca 的完整前缀函数 π,可称为部分匹配表(Partial Match Table)。 + +### 计算过程: +* π[1] = 0,a 仅一个字符,前缀和后缀为空集,共有元素最大长度为 0; +* π[2] = 0,ab 的前缀 a,后缀 b,不匹配,共有元素最大长度为 0; +* π[3] = 1,aba,前缀 a ab,后缀 ba a,共有元素最大长度为 1; +* π[4] = 2,abab,前缀 a ab aba,后缀 bab ab b,共有元素最大长度为 2; +* π[5] = 3,ababa,前缀 a ab aba abab,后缀 baba aba ba a,共有元素最大长度为 3; +* π[6] = 4,ababab,前缀 a ab aba abab ababa,后缀 babab abab bab ab b,共有元素最大长度为 4; +* π[7] = 5,abababa,前缀 a ab aba abab ababa ababab,后缀 bababa ababa baba aba ba a,共有元素最大长度为 5; +* π[8] = 6,abababab,前缀 .. ababab ..,后缀 .. ababab ..,共有元素最大长度为 6; +* π[9] = 0,ababababc,前缀和后缀不匹配,共有元素最大长度为 0; +* π[10] = 1,ababababca,前缀 .. a ..,后缀 .. a ..,共有元素最大长度为 1; + +### 算法原理 +KMP 算法 KMP-MATCHER 中通过调用 COMPUTE-PREFIX-FUNCTION 函数来计算部分匹配表。 +``` + 1 KMP-MATCHER(T, P) + 2 n ← length[T] + 3 m ← length[P] + 4 π ← COMPUTE-PREFIX-FUNCTION(P) + 5 q ← 0 //Number of characters matched. + 6 for i ← 1 to n //Scan the text from left to right. + 7 do while q > 0 and P[q + 1] ≠ T[i] + 8 do q ← π[q] //Next character does not match. + 9 if P[q + 1] = T[i] +10 then q ← q + 1 //Next character matches. +11 if q = m //Is all of P matched? +12 then print "Pattern occurs with shift" i - m +13 q ← π[q] //Look for the next match. +``` +``` + 1 COMPUTE-PREFIX-FUNCTION(P) + 2 m ← length[P] + 3 π[1] ← 0 + 4 k ← 0 + 5 for q ← 2 to m + 6 do while k > 0 and P[k + 1] ≠ P[q] + 7 do k ← π[k] + 8 if P[k + 1] = P[q] + 9 then k ← k + 1 +10 π[q] ← k +11 return π +``` + +预处理过程 COMPUTE-PREFIX-FUNCTION 的运行时间为 Θ(m),KMP-MATCHER 的匹配时间为 Θ(n)。 + +相比较于 NAIVE-STRING-MATCHER,KMP-MATCHER 的主要优化点就是在当确定字符不匹配时对于 pattern 的位移。 + +NAIVE-STRING-MATCHER 的位移效果是:文本向后移一位,模式从头开始。 +``` + s = s - j + 1; + j = 0; +``` + +KMP-MATCHER 首先对模式做了获取共同前缀后缀最大长度的预处理操作,位移过程是先将模式向后移 partial_match_length - table[partial_match_length - 1],然后再判断是否匹配。这样通过对已匹配字符串的已知信息的利用,可以有效节省比较数量。 +``` + if (j != 0) + j = lps[j - 1]; + else + s++; +``` + +下面描述了当发现字符 j 与 c 不匹配时的位移效果。 +``` + // partial_match_length - table[partial_match_length - 1] + rrababababjjjjjiiooorababababcauuu + ||||||||- + ababababca + // 8-6=2 + rrababababjjjjjiiooorababababcauuu + xx||||||- + ababababca + // 6-4=2 + rrababababjjjjjiiooorababababcauuu + xx||||- + ababababca + // 4-2=2 + rrababababjjjjjiiooorababababcauuu + xx||- + ababababca + // 2-0=2 + rrababababjjjjjiiooorababababcauuu + xx- + ababababca +``` +综上可知,KMP 算法的主要特点是: +1. 需要对模式字符串做预处理; +2. 预处理阶段需要额外的 O(m) 空间和复杂度; +3. 匹配阶段与字符集的大小无关; +4. 匹配阶段至多执行 2n - 1 次字符比较; +5. 对模式中字符的比较顺序时从左到右; + +### 算法实现 +下面是 KMP-MATCHER 的代码示例。 +``` + 1 namespace StringMatching + 2 { + 3 class Program + 4 { + 5 static void Main(string[] args) + 6 { + 7 char[] text1 = "BBC ABCDAB ABCDABCDABDE".ToCharArray(); + 8 char[] pattern1 = "ABCDABD".ToCharArray(); + 9 + 10 int firstShift1; + 11 bool isMatched1 = KmpStringMatcher.TryMatch1(text1, pattern1, out firstShift1); + 12 Contract.Assert(isMatched1); + 13 Contract.Assert(firstShift1 == 15); + 14 + 15 char[] text2 = "ABABDAAAACAAAABCABAB".ToCharArray(); + 16 char[] pattern2 = "AAACAAAA".ToCharArray(); + 17 + 18 int firstShift2; + 19 bool isMatched2 = KmpStringMatcher.TryMatch2(text2, pattern2, out firstShift2); + 20 Contract.Assert(isMatched2); + 21 Contract.Assert(firstShift2 == 6); + 22 + 23 char[] text3 = "ABAAACAAAAAACAAAABCABAAAACAAAAFDLAAACAAAAAACAAAA".ToCharArray(); + 24 char[] pattern3 = "AAACAAAA".ToCharArray(); + 25 + 26 int[] shiftIndexes3 = KmpStringMatcher.MatchAll1(text3, pattern3); + 27 Contract.Assert(shiftIndexes3.Length == 5); + 28 Contract.Assert(string.Join(",", shiftIndexes3) == "2,9,22,33,40"); + 29 int[] shiftIndexes4 = KmpStringMatcher.MatchAll2(text3, pattern3); + 30 Contract.Assert(shiftIndexes4.Length == 5); + 31 Contract.Assert(string.Join(",", shiftIndexes4) == "2,9,22,33,40"); + 32 + 33 Console.WriteLine("Well done!"); + 34 Console.ReadKey(); + 35 } + 36 } + 37 + 38 public class KmpStringMatcher + 39 { + 40 public static bool TryMatch1(char[] text, char[] pattern, out int firstShift) + 41 { + 42 // KMP needs a pattern preprocess to get the Partial Match Table + 43 int[] lps = PreprocessToComputeLongestProperPrefixSuffixArray(pattern); + 44 // pattern: ABCDABD + 45 // char: | A | B | C | D | A | B | D | + 46 // index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | + 47 // lps: | 0 | 0 | 0 | 0 | 1 | 2 | 0 | + 48 + 49 firstShift = -1; + 50 int n = text.Length; + 51 int m = pattern.Length; + 52 int s = 0, j = 0; + 53 + 54 while (s < n && j < m) + 55 { + 56 if (j == -1 || text[s] == pattern[j]) + 57 { + 58 s++; + 59 j++; + 60 } + 61 else + 62 { + 63 // here is different with naive string matcher + 64 if (j != 0) + 65 j = lps[j - 1]; + 66 else + 67 s++; + 68 } + 69 + 70 if (j == m) + 71 { + 72 firstShift = s - j; + 73 return true; + 74 } + 75 } + 76 + 77 return false; + 78 } + 79 + 80 static int[] PreprocessToComputeLongestProperPrefixSuffixArray(char[] pattern) + 81 { + 82 int m = pattern.Length; + 83 + 84 // hold the longest prefix suffix values for pattern + 85 int[] lps = new int[m]; + 86 lps[0] = 0; + 87 + 88 // length of the previous longest prefix suffix + 89 int k = 0; + 90 int q = 1; + 91 while (q < m) + 92 { + 93 if (pattern[k] == pattern[q]) + 94 { + 95 k++; + 96 lps[q] = k; + 97 q++; + 98 } + 99 else +100 { +101 if (k != 0) +102 { +103 k = lps[k - 1]; +104 } +105 else +106 { +107 lps[q] = 0; +108 q++; +109 } +110 } +111 } +112 +113 return lps; +114 } +115 +116 public static bool TryMatch2(char[] text, char[] pattern, out int firstShift) +117 { +118 // KMP needs a pattern preprocess +119 int[] next = PreprocessToGetNextArray(pattern); +120 // pattern: ABCDABD +121 // char: | A | B | C | D | A | B | D | +122 // index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +123 // lps: | 0 | 0 | 0 | 0 | 1 | 2 | 0 | +124 // next: |-1 | 0 | 0 | 0 | 0 | 1 | 2 | -> shift LPS 1 position to right +125 +126 firstShift = -1; +127 int n = text.Length; +128 int m = pattern.Length; +129 int s = 0, j = 0; +130 +131 while (s < n && j < m) +132 { +133 if (j == -1 || text[s] == pattern[j]) +134 { +135 s++; +136 j++; +137 } +138 else +139 { +140 // here is different with naive string matcher +141 j = next[j]; +142 } +143 +144 if (j == m) +145 { +146 firstShift = s - j; +147 return true; +148 } +149 } +150 +151 return false; +152 } +153 +154 static int[] PreprocessToGetNextArray(char[] pattern) +155 { +156 int m = pattern.Length; +157 int[] next = new int[m]; +158 next[0] = -1; +159 +160 int k = -1; +161 int q = 0; +162 while (q < m - 1) +163 { +164 if (k == -1 || pattern[k] == pattern[q]) +165 { +166 k++; +167 q++; +168 +169 //next[q] = k; // does not optimize +170 +171 if (pattern[k] != pattern[q]) +172 next[q] = k; +173 else +174 next[q] = next[k]; // with optimization +175 } +176 else +177 { +178 k = next[k]; +179 } +180 } +181 +182 return next; +183 } +184 +185 public static int[] MatchAll1(char[] text, char[] pattern) +186 { +187 // KMP needs a pattern preprocess +188 int[] lps = PreprocessToComputeLongestProperPrefixSuffixArray(pattern); +189 // pattern: ABCDABD +190 // char: | A | B | C | D | A | B | D | +191 // index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +192 // lps: | 0 | 0 | 0 | 0 | 1 | 2 | 0 | +193 +194 int n = text.Length; +195 int m = pattern.Length; +196 int s = 0, j = 0; +197 int[] shiftIndexes = new int[n - m + 1]; +198 int c = 0; +199 +200 while (s < n && j < m) +201 { +202 if (j == -1 || text[s] == pattern[j]) +203 { +204 s++; +205 j++; +206 } +207 else +208 { +209 // here is different with naive string matcher +210 if (j != 0) +211 j = lps[j - 1]; +212 else +213 s++; +214 } +215 +216 if (j == m) +217 { +218 shiftIndexes[c] = s - j; +219 c++; +220 +221 j = lps[j - 1]; +222 } +223 } +224 +225 int[] shifts = new int[c]; +226 for (int y = 0; y < c; y++) +227 { +228 shifts[y] = shiftIndexes[y]; +229 } +230 +231 return shifts; +232 } +233 +234 public static int[] MatchAll2(char[] text, char[] pattern) +235 { +236 // KMP needs a pattern preprocess +237 int[] next = PreprocessToGetNextArray(pattern); +238 // pattern: ABCDABD +239 // char: | A | B | C | D | A | B | D | +240 // index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +241 // lps: | 0 | 0 | 0 | 0 | 1 | 2 | 0 | +242 // next: |-1 | 0 | 0 | 0 | 0 | 1 | 2 | -> shift LPS 1 position to right +243 +244 int n = text.Length; +245 int m = pattern.Length; +246 int s = 0, j = 0; +247 int[] shiftIndexes = new int[n - m + 1]; +248 int c = 0; +249 +250 while (s < n && j < m) +251 { +252 if (j == -1 || text[s] == pattern[j]) +253 { +254 s++; +255 j++; +256 } +257 else +258 { +259 // here is different with naive string matcher +260 j = next[j]; +261 } +262 +263 if (j == m) +264 { +265 shiftIndexes[c] = s - j; +266 c++; +267 +268 j = next[j - 1]; +269 } +270 } +271 +272 int[] shifts = new int[c]; +273 for (int y = 0; y < c; y++) +274 { +275 shifts[y] = shiftIndexes[y]; +276 } +277 +278 return shifts; +279 } +280 } +281 } +``` \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3.8 字符串算法-其他算法.md b/Algorithm/A类:基本算法/3.8 字符串算法-其他算法.md new file mode 100644 index 00000000..63a92629 --- /dev/null +++ b/Algorithm/A类:基本算法/3.8 字符串算法-其他算法.md @@ -0,0 +1,2 @@ +> 等到以后再处理 + diff --git a/Algorithm/A类:基本算法/4 递归与分治法.md b/Algorithm/A类:基本算法/4 递归与分治法.md index 694b975d..7a73bbfa 100644 --- a/Algorithm/A类:基本算法/4 递归与分治法.md +++ b/Algorithm/A类:基本算法/4 递归与分治法.md @@ -1,9 +1,14 @@ # 分治法 -## 1 概述 +## 0 分治法概述 ### 基本思想 -将一个难以直接解决的大问题,分解为规模较小的相同子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。 +* 求解问题算法的复杂性一般都与问题规模相关,问题规模越小越容易处理。 +* 分治法的基本思想是,将一个难以直接解决的大问题,分解为规模较小的相同子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。 +* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。递归是分治法中最常用的技术。 + +![](image/分治法原理.png) + ### 分治法解决问题的先决条件 * 该问题的规模缩小到一定的程度就可以容易地解决; @@ -11,9 +16,89 @@ * 利用该问题分解出的子问题的解可以合并为该问题的解; * 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。 -## 2 具体问题 -### 合并排序 -基本思想:将数组一分为二,分别对每个集合单独排序,然后将已排序的两个序列归并成一个含n个元素的分好类的序列。如果分割后子问题还很大,则继续分治,直到只有一个元素。 +### 分治法的步骤 +一般来说,分治法的求解过程由以下三个阶段组成: +1. 划分:既然是分治,当然需要把规模为n的原问题划分为k个规模较小的子问题,并尽量使这k个子问题的规模大致相同。 +2. 求解子问题:各子问题的解法与原问题的解法通常是相同的,可以用递归的方法求解各个子问题,有时递归处理也可以用循环来实现。 +3. 合并:把各个子问题的解合并起来,合并的代价因情况不同有很大差异,分治算法的有效性很大程度上依赖于合并的实现。 + +在用分治法设计算法时,最好使子问题的规模大致相同。即将一个问题分成大小相等的k个子问题的处理方法是行之有效的。这种使子问题规模大致相等的做法是出自一种平衡(balancing)子问题的思想,它几乎总是比子问题规模不等的做法要好。 + +``` +divide-and-conquer(P){ + if ( | P | <= n0) adhoc(P); //解决小规模的问题 + divide P into smaller subinstances P1,P2,...,Pk;//分解问题 + for (i=1; i<=k; i++) + yi=divide-and-conquer(Pi); //递归的解各子问题 + return merge(y1,...,yk); //将各子问题的解合并为原问题的解 + } + +``` +### 分治法的复杂性 + +即递归法的时间复杂性。递归求解各个子问题。递归是实现分治算法的手段。 + +可以通过过计算递归法的时间复杂度,计算分治法的时间复杂度。 + + +## 0 递归法概述 + +### 基本思想 +直接或间接的调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数。 + +### 线性收缩递归算法 +* 递推关系式 + +$$ +T(n)=\begin{cases} + o(1) & n=1 \\ + \sum_{i=1}^k a_iT(n-i)+f(n) & n>1 +\end{cases} +$$ + +* 求解递推关系式 +$$ +T(n)=a^{n-1}T(1)+\sum_{i=2}^na^{n-i}f(i) +$$ +* 关系式说明 + +![](image/递归算法-线性收缩说明.png) +### 等比收缩递归算法 + +* 递推关系式 +$$ +T(n)=\begin{cases} + O(1)&n=1 \\ + aT(\frac{n}{b})+f(n) & n>1 +\end{cases} +$$ +* 求解递推关系式 +$$ +T(n)=n^{\log_ba} +\sum_{i=2}^{\log_bn-1}a^jf(n/b^j) +$$ +* 关系式说明 +![](image/递归算法-等比收缩说明.png) + +![](image/递归算法-时间复杂度.png) + + +## 1 分治法应用 + +### 排列问题 +### 整数划分问题 + +### 二分搜索问题 + +### 大数乘法 + +### 矩阵乘法 ### 快速排序 -基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列 + +### 合并排序 + +### 线性时间选择 + +### 最近点对问题 + +### 棋盘覆盖问题 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/4.1 排列问题.md b/Algorithm/A类:基本算法/4.1 排列问题.md new file mode 100644 index 00000000..ef61d406 --- /dev/null +++ b/Algorithm/A类:基本算法/4.1 排列问题.md @@ -0,0 +1,44 @@ +# 排列问题 + +## 1 排列问题-分治法 + +### 问题描述 + +R是由n个元素构成的序列集合,R={r1, r2, … ,rn},求R的全排列perm(R)。 + +### 问题分析 + +1. 若R中只有1个元素{r},则perm(R)=(r) +2. 若R中只有2个元素{r1, r2},则 + perm(R)=(r1)perm(R1)∪(r2)perm(R2) + 其中,Ri=R-{ri} +3. 若R中有3个元素{ r1, r2, r3},则 + perm(R)=(r1)perm(R1)∪(r2)perm(R2)∪(r3)perm(R3) + + +### 算法设计 + +分治法 +依次将待排列的数组的后n-1个元素与第一个元素交换,则每次递归处理的都是后n-1个元素的全排列。当数组元素仅有一个时为此递归算法的出口。 +``` +算法 perm(Type list[], int k, int m) +//生成列表list的全排列 +//输入:一个全排列元素列表list[0..n-1] +//输出:list的全排列集合 +if k == m + for i←0 to m do + 输出list[i] +else + for i←k to m do + swap list[k] and list[i] + perm(list, k+1, m) + swap list[k] and list[i] +``` +### 算法分析 + +$$ +T(n)=\begin{cases} + O(1)& n=1\\ + nT(n-1)+O(1)& n>1 +\end{cases} +$$ \ No newline at end of file diff --git a/Algorithm/A类:基本算法/5 动态规划.md b/Algorithm/A类:基本算法/5 动态规划.md index aeb4598a..75203a0a 100644 --- a/Algorithm/A类:基本算法/5 动态规划.md +++ b/Algorithm/A类:基本算法/5 动态规划.md @@ -3,11 +3,13 @@ ## 1 概述 ### 基本思想 动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。 + 动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。 要素 ### 对比分治法 动态规划中分解得到的子问题不是互相独立的。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。 + 动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。 ### 条件 @@ -25,3 +27,27 @@ * 最有子结构 * 重叠子问题 * 备忘录方法(矩阵表格) + +## 1 常见问题 + +### 矩阵连乘问题 + +### 凸多边形最优三角剖分 + +### 最长公共子序列 + +### 图像压缩问题 + +### 最大子段和问题 + +### 流水作业调度问题 + +### 投资问题 + +### 01背包问题 + +### 0n背包问题 + +### 最优二叉搜索树问题 + +### 序列匹配问题 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/3 蛮力法.md b/Algorithm/A类:基本算法/5.1 矩阵连乘问题.md similarity index 100% rename from Algorithm/A类:基本算法/3 蛮力法.md rename to Algorithm/A类:基本算法/5.1 矩阵连乘问题.md diff --git a/Algorithm/A类:基本算法/5.2 凸n边形最优三角剖分.md b/Algorithm/A类:基本算法/5.2 凸n边形最优三角剖分.md new file mode 100644 index 00000000..3ad23eeb --- /dev/null +++ b/Algorithm/A类:基本算法/5.2 凸n边形最优三角剖分.md @@ -0,0 +1,48 @@ +# 凸多边形最优三角剖分 + +## 凸多边形最优三角剖分-动态规划 +### 问题描述 +给定凸多边形P={v0,v1,… ,vn-1},以及定义在由凸多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分,使得该三角剖分所对应的权,即三角剖分中诸三角形上权之和为最小。 + +### 问题分析 + +1. 若凸(n+1)边形P={v0,v1,…,vn}的最优三角剖分T包含三角形v0vkvn,1≤k≤n-1,则T的权为3个部分权的和:三角形v0vkvn的权,子多边形{v0,v1,…,vk}和{vk,vk+1,…,vn}的权之和 +2. 由T所确定的这2个子多边形的三角剖分也是最优的。 +3. 因为若有{v0,v1,…,vk}或{vk,vk+1,…,vn}的更小权的三角剖分将导致T不是最优三角剖分的矛盾。 + + +### 算法原理 + +定义$t[i][j],1≤i的某个性质,那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即 P(x1,x2,…,xi+1) → P(x1,x2,…,xi) (0 | 1 ≤ xi ≤N, 1 ≤ i ≤N}(解空间)之中。可将问题解空间表示为一定的结构,通过对解空间的搜索,得到满足要求的问题解。 +> 简单来说,解空间是子集树时不具有排列性质,没有位置关系。为排列树时需要考虑每个要素的顺序。 + +### 回溯法的时间复杂度 +依赖以下条件 + +* 产生x[k]的时间; +* 满足显约束的x[k]值的个数; +* 计算约束函数constraint的时间; +* 计算上界函数bound的时间; +* 满足约束函数和上界函数约束的所有x[k]的个数。 + +是NPhard难题。需要遍历解空间树。 + ### 回溯法的程序结构 * 递归回溯 diff --git a/Algorithm/A类:基本算法/7.1 N皇后问题.md b/Algorithm/A类:基本算法/7.1 N皇后问题.md new file mode 100644 index 00000000..bb6769ec --- /dev/null +++ b/Algorithm/A类:基本算法/7.1 N皇后问题.md @@ -0,0 +1,42 @@ +# N皇后问题 + +## 1 N皇后问题-回溯法 + +### 问题描述 + +在N×N的棋盘中放置N个皇后,使得任何两个皇后之间不能相互攻击,试给出所有的放置方法。 + +### 问题分析 +* 问题解向量:(x1, x2, … , xn) +* 显约束:xi=1,2, … ,n +* 隐约束: + * 不同列:xi不等于xj; + * 不处于同一正、反对角线:|i-j|不等于|xi-xj| + + +### 算法时间复杂度 +(1)搜索1+n+n2+…+nn=(nn+1-1)/n-1≤2nn; +(2)每个节点判断是否符合规则,最多要判断3n个位置(列方向、主与副对角线方向是否有皇后) +故最坏情况下时间复杂度O(3n×2nn)=O(nn+1) + +### 算法实现 + +``` +bool Queen::Place(int k) { + for (int j=1;jn) sum++; + else + for (int i=1;i<=n;i++) { + x[t]=i; + if (Place(t)) Backtrack(t+1); + } +} + +``` + +## 2 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/7.2 旅行商问题.md b/Algorithm/A类:基本算法/7.2 旅行商问题.md new file mode 100644 index 00000000..920075d5 --- /dev/null +++ b/Algorithm/A类:基本算法/7.2 旅行商问题.md @@ -0,0 +1,74 @@ +# 旅行商问题 + +## 1 旅行商问题-回溯法 + +### 问题描述 +某售货员要到若干城市去推销商品,已知各城市间的路程耗费(代价),如何选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使得总路程耗费最小。 +![](image/旅行商问题.png) + +### 问题分析 + +* 解向量为,其中i2,i3,…,in为{2,3,…,n}的一个排列。搜索空间为排列树 + +* 显约束: + * i=n时,检查是否存在一条从顶点x[n-1]到x[n]的边和一条从顶点x[n]到顶点1的边,若存在,需要判断当前回路代价是否优于已找到的当前最优回路代价bestc,若为真,更新当前最优值bestc和最优解bestx。 + * 当i +void Traveling::Backtrack(int i){ + if (i == n) { + if (a[x[n-1]][x[n]] != NoEdge && a[x[n]][1] != NoEdge && + (cc + a[x[n-1]][x[n]] + a[x[n]][1] < bestc || bestc == NoEdge)) { + for (int j = 1; j <= n; j++) bestx[j] = x[j]; + bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];} + } + else { + for (int j = i; j <= n; j++) + // 是否可进入x[j]子树? + if (a[x[i-1]][x[j]] != NoEdge && + (cc + a[x[i-1]][x[i]] < bestc || bestc == NoEdge)) { + // 搜索子树 + Swap(x[i], x[j]); + cc += a[x[i-1]][x[i]]; + Backtrack(i+1); + cc -= a[x[i-1]][x[i]]; + Swap(x[i], x[j]);} + } +} +``` + + + + + + + +## 2 旅行商问题-分支限界 + +### 问题描述 + +### 问题分析 + +* 问题上界: + * 贪心法可求得问题近似解1→3 → 5 → 4 → 2 → 1,其路径长度为1+2+3+7+3=16,作为问题上界ub; + +* 问题下界: + * 图邻接矩阵每行最小元素相加,db=1+3+1+3+2=10; + * 每个顶点应该有出入两条边,将邻接矩阵中每行最小两个元素相加在除以2,并向上取整,可得到更好下界,db=((1+3)+(3+6)+(1+2)+(3+4)+(2+3))/2=14; + +### 算法原理 +![](image/旅行商问题-分支限界.png) +### 算法实现 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/7.3 01背包问题.md b/Algorithm/A类:基本算法/7.3 01背包问题.md new file mode 100644 index 00000000..d79791ab --- /dev/null +++ b/Algorithm/A类:基本算法/7.3 01背包问题.md @@ -0,0 +1,52 @@ +# 01背包问题 + +## 1 01背包问题-回溯法 + +### 问题描述 +将物品放到背包中。 +* n件物品 +* 每件物品的重量为w[i] +* 价值为v[i] +* m个背包 +* 每个背包的容量为c[j] + +求背包装载的最大价值。或者是否能装下所有。 + +具体问题n=3,C=20,(v1,v2,v3)=(20,15,25), (w1,w2,w3)=(10,5,15),求X=(x1,x2,x3)使背包价值最大? +### 问题分析 + +* 解空间是子集树 +* 可行性约束函数(剪枝函数)$\sum w_ix_i\leq c_i$ + +### 算法原理 +![](image/01背包问题-回溯法.png) +### 算法实现 +``` +template +Typep Knap::Bound(int i) +{// 计算上界 + Typew cleft = c - cw; // 剩余容量 + Typep b = cp; + // 以物品单位重量价值递减序装入物品 + while (i <= n && w[i] <= cleft) { + cleft -= w[i]; + b += p[i]; + i++; + } + // 装满背包 + if (i <= n) b += p[i]/w[i] * cleft; + return b; +} +``` + +## 2 01背包问题-分支限界 + +### 问题描述 +0-1背包问题: (分析队列式与优先队列式过程) +n=3, C=30, w={16, 15, 15}, v={45, 25, 25} + +### 问题分析 + +### 算法原理 +![](image/01背包问题-分支限界.png) +### 算法实现 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/7.4 作业调度问题.md b/Algorithm/A类:基本算法/7.4 作业调度问题.md new file mode 100644 index 00000000..3d939d90 --- /dev/null +++ b/Algorithm/A类:基本算法/7.4 作业调度问题.md @@ -0,0 +1,13 @@ +# 作业调度问题 + +## 1 作业调度问题-回溯法 + +### 问题描述 + +给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理,然后由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。 + +批处理作业调度问题要求对于给定的n个作业,制定最佳作业调度方案,使其完成时间和达到最小。 + +### 问题分析 + +* 解空间:排列树问题。 diff --git a/Algorithm/A类:基本算法/7.5 完全背包问题.md b/Algorithm/A类:基本算法/7.5 完全背包问题.md new file mode 100644 index 00000000..a0a8ebb0 --- /dev/null +++ b/Algorithm/A类:基本算法/7.5 完全背包问题.md @@ -0,0 +1,12 @@ +# 完全背包问题 + +## 1 完全背包问题 + +将物品放到背包中。 +* 无限件物品 +* 每件物品的重量为w[i] +* 价值为v[i] +* m个背包 +* 每个背包的容量为c[j] + +求背包装载的的最大价值。 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/8 分支限界.md b/Algorithm/A类:基本算法/8 分支限界.md index 43972bc6..bda9c750 100644 --- a/Algorithm/A类:基本算法/8 分支限界.md +++ b/Algorithm/A类:基本算法/8 分支限界.md @@ -11,3 +11,22 @@ ### 常见的两种分支界限法 * 队列式(FIFO)分支限界法:按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。 * 优先队列式分支限界法:按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。 + +### 使用条件 + +* 问题的多米诺性质 + +* 求解最优解或一个可行解 + +### 设计要素 +1. 针对问题定义解空间 + * 问题解向量 + * 解向量分量取值集合 + * 构造解空间树 +2. 判断是否满足多米诺性质 +3. 确定**剪枝函数** +4. 确定存储搜索路径的数据结构 +5. 分支限界发的核心思想在于**界的设计** + +### 分支限界法的程序结构 +迭代方法 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/9 随机化.md b/Algorithm/A类:基本算法/9 随机化.md index e69de29b..12be51e7 100644 --- a/Algorithm/A类:基本算法/9 随机化.md +++ b/Algorithm/A类:基本算法/9 随机化.md @@ -0,0 +1,73 @@ +# 随机化算法 + +## 1 伪随机数 + +### 伪随机数的产生 + +$$ +\begin{cases} + a_0=d\\ + a_n=(ba_{n-1}+c) mod m +\end{cases} +$$ + +其中$a\geq 0,b\geq0,d\geq m,d$是随机序列种子。 + +## 2 数值随机化算法 + + + +## 3 舍伍德算法 + +> 确定性算法随机化。 + + +## 4 蒙特卡洛算法 +蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是二十世纪四十年代中期由于科学技术的发展和电子计算机的发明,而被提出的一种以概率统计理论为指导的一类非常重要的数值计算方法。是指使用随机数(或更常见的伪随机数)来解决很多计算问题的方法。与它对应的是确定性算法。蒙特·卡罗方法在金融工程学,宏观经济学,计算物理学(如粒子输运计算、量子热力学计算、空气动力学计算)等领域应用广泛。 + + + +### 随机投点法计算$\pi$ + +### 随机投点法计算定积分 + +## 5 拉斯维加斯算法 + +拉斯维加斯算法的一个显著特征是它所作的随机性决策有可能导致算法找不到所需的解。因此通常用一个bool型函数表示拉斯维加斯算法。 + +### 拉斯维加斯算法+回溯法 解决N皇后问题 + +考虑用拉斯维加斯算法解决N皇后问题: + +对于n后问题的任何一个解而言,每一个皇后在棋盘上的位置无任何规律,不具有系统性,而更象是随机放置的。由此容易想到下面的拉斯维加斯算法。 +在棋盘上相继的各行中随机地放置皇后,并注意使新放置的皇后与已放置的皇后互不攻击,直至n个皇后已相容地放置好,或已没有下一个皇后的可放置位置时为止。注意这里解决的是找到其中一个方法,求不是求出N皇后的全部解。 + +这里提前说明一下,否则不好理解。 + +接下来的这个用Las Vegas算法解决N皇后问题,我们采用的是随机放置位置策略和回溯法相结合,具体就是比如八皇后中,前几行选择用随机法放置皇后,剩下的选择用回溯法解决。 + +这个程序不是很好理解,有的地方我特别说明了是理解程序的关键,大家看时一定要认真了,另外,王晓东的书上先是用单纯的随机法解决,大家可以先去理解书上这个例子。然后再来分析我这个程序。不过那本书上关于这一块错误比较多,大家看时要注意哪些地方他写错了。 + + +## 6 蒙特卡洛方法与拉斯维加斯算法对比 + +### 定义 +蒙特卡罗是一类随机方法的统称。这类方法的特点是,可以在随机采样上计算得到近似结果,随着采样的增多,得到的结果是正确结果的概率逐渐加大,但在(放弃随机采样,而采用类似全采样这样的确定性方法)获得真正的结果之前,无法知道目前得到的结果是不是真正的结果。​ + + +拉斯维加斯方法是另一类随机方法的统称。这类方法的特点是,随着采样次数的增多,得到的正确结果的概率逐渐加大,如果随机采样过程中已经找到了正确结果,该方法可以判别并报告,但在放弃随机采样,而采用类似全采样这样的确定性方法之前,不保证能找到任何结果(包括近似结果)​ + +### 场景 + +假如筐里有100个苹果,让我每次闭眼拿1个,挑出最大的。于是我随机拿1个,再随机拿1个跟它比,留下大的,再随机拿1个……我每拿一次,留下的苹果都至少不比上次的小。拿的次数越多,挑出的苹果就越大,但我除非拿100次,否则无法肯定挑出了最大的。这个挑苹果的算法,就属于蒙特卡罗算法——尽量找好的,但不保证是最好的。 + +而拉斯维加斯算法,则是另一种情况。假如有一把锁,给我100把钥匙,只有1把是对的。于是我每次随机拿1把钥匙去试,打不开就再换1把。我试的次数越多,打开(最优解)的机会就越大,但在打开之前,那些错的钥匙都是没有用的。这个试钥匙的算法,就是拉斯维加斯的——尽量找最好的,但不保证能找到。​ + +### 结论 + +* 蒙特卡罗算法:采样越多,越近似最优解; + +* 拉斯维加斯算法:采样越多,越有机会找到最优解;​ + +这两类随机算法之间的选择,往往受到问题的局限。如果问题要求在有限采样内,必须给出一个解,但不要求是最优解,那就要用蒙特卡罗算法。反之,如果问题要求必须给出最优解,但对采样没有限制,那就要用拉斯维加斯算法。​ + diff --git a/Algorithm/A类:基本算法/NP问题.md b/Algorithm/A类:基本算法/NP问题.md new file mode 100644 index 00000000..3c144364 --- /dev/null +++ b/Algorithm/A类:基本算法/NP问题.md @@ -0,0 +1,11 @@ +时间复杂度 +时间复杂度并不是表示一个程序解决问题需要花多少时间,而是当问题规模扩大后,程序需要的时间长度增长得有多快。也就是说,对于高速处理数据的计算机来说,处理某一个特定数据的效率不能衡量一个程序的好坏,而应该看当这个数据的规模变大到数百倍后,程序运行时间是否还是一样,或者也跟着慢了数百倍,或者变慢了数万倍。不管数据有多大,程序处理花的时间始终是那么多的,我们就说这个程序很好,具有O(1)的时间复杂度,也称常数级复杂度;数据规模变得有多大,花的时间也跟着变得有多长,这个程序的时间复杂度就是O(n),比如找n个数中的最大值;而像冒泡排序、插入排序等,数据扩大2倍,时间变慢4倍的,属于O(n^2)的复杂度。还有一些穷举类的算法,所需时间长度成几何阶数上涨,这就是O(a^n)的指数级复杂度,甚至O(n!)的阶乘级复杂度。不会存在O(2*n^2)的复杂度,因为前面的那个“2”是系数,根本不会影响到整个程序的时间增长。同样地,O (n^3+n^2)的复杂度也就是O(n^3)的复杂度。因此,我们会说,一个O(0.01*n^3)的程序的效率比O(100*n^2)的效率低,尽管在n很小的时候,前者优于后者,但后者时间随数据规模增长得慢,最终O(n^3)的复杂度将远远超过O(n^2)。我们也说,O(n^100)的复杂度小于O(1.01^n)的复杂度。 + 容易看出,前面的几类复杂度被分为两种级别,其中后者的复杂度无论如何都远远大于前者:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受。当我们在解决一个问题时,我们选择的算法通常都需要是多项式级的复杂度,非多项式级的复杂度需要的时间太多,往往会超时,除非是数据规模非常小。 +P类问题的概念 +如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题。 +NP问题的概念 +这个就有点难理解了,或者说容易理解错误。在这里强调(回到我竭力想澄清的误区上),NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题。比方说,我RP很好,在程序中需要枚举时,我可以一猜一个准。现在某人拿到了一个求最短路径的问题,问从起点到终点是否有一条小于100个单位长度的路线。它根据数据画好了图,但怎么也算不出来,于是来问我:你看怎么选条路走得最少?我说,我RP很好,肯定能随便给你指条很短的路出来。然后我就胡乱画了几条线,说就这条吧。那人按我指的这条把权值加起来一看,嘿,神了,路径长度98,比100小。于是答案出来了,存在比100小的路径。别人会问他这题怎么做出来的,他就可以说,因为我找到了一个比100 小的解。在这个题中,找一个解很困难,但验证一个解很容易。验证一个解只需要O(n)的时间复杂度,也就是说我可以花O(n)的时间把我猜的路径的长度加出来。那么,只要我RP好,猜得准,我一定能在多项式的时间里解决这个问题。我猜到的方案总是最优的,不满足题意的方案也不会来骗我去选它。这就是NP问题。当然有不是NP问题的问题,即你猜到了解但是没用,因为你不能在多项式的时间里去验证它。下面我要举的例子是一个经典的例子,它指出了一个目前还没有办法在多项式的时间里验证一个解的问题。很显然,前面所说的Hamilton回路是NP问题,因为验证一条路是否恰好经过了每一个顶点非常容易。但我要把问题换成这样:试问一个图中是否不存在Hamilton回路。这样问题就没法在多项式的时间里进行验证了,因为除非你试过所有的路,否则你不敢断定它“没有Hamilton回路”。 + 之所以要定义NP问题,是因为通常只有NP问题才可能找到多项式的算法。我们不会指望一个连多项式地验证一个解都不行的问题存在一个解决它的多项式级的算法。相信读者很快明白,信息学中的号称最困难的问题——“NP问题”,实际上是在探讨NP问题与P类问题的关系。 + +NPC问题的定义 +同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。证明一个问题是 NPC问题也很简单。先证明它至少是一个NP问题,再证明其中一个已知的NPC问题能约化到它(由约化的传递性,则NPC问题定义的第二条也得以满足;至于第一个NPC问题是怎么来的,下文将介绍),这样就可以说它是NPC问题了。 \ No newline at end of file diff --git a/Algorithm/A类:基本算法/image/01背包问题-分支限界.png b/Algorithm/A类:基本算法/image/01背包问题-分支限界.png new file mode 100644 index 00000000..275a6c2d Binary files /dev/null and b/Algorithm/A类:基本算法/image/01背包问题-分支限界.png differ diff --git a/Algorithm/A类:基本算法/image/01背包问题-回溯法.png b/Algorithm/A类:基本算法/image/01背包问题-回溯法.png new file mode 100644 index 00000000..0cbed367 Binary files /dev/null and b/Algorithm/A类:基本算法/image/01背包问题-回溯法.png differ diff --git a/Algorithm/A类:基本算法/image/分治法原理.png b/Algorithm/A类:基本算法/image/分治法原理.png new file mode 100644 index 00000000..a051f403 Binary files /dev/null and b/Algorithm/A类:基本算法/image/分治法原理.png differ diff --git a/Algorithm/A类:基本算法/image/字符串匹配算法-KMP.jpg b/Algorithm/A类:基本算法/image/字符串匹配算法-KMP.jpg new file mode 100644 index 00000000..ed303e8a Binary files /dev/null and b/Algorithm/A类:基本算法/image/字符串匹配算法-KMP.jpg differ diff --git a/Algorithm/A类:基本算法/image/字符串匹配算法-算法原理.png b/Algorithm/A类:基本算法/image/字符串匹配算法-算法原理.png new file mode 100644 index 00000000..c4fd9680 Binary files /dev/null and b/Algorithm/A类:基本算法/image/字符串匹配算法-算法原理.png differ diff --git a/Algorithm/A类:基本算法/image/字符串匹配算法.png b/Algorithm/A类:基本算法/image/字符串匹配算法.png new file mode 100644 index 00000000..2eb14a9e Binary files /dev/null and b/Algorithm/A类:基本算法/image/字符串匹配算法.png differ diff --git a/Algorithm/A类:基本算法/image/字符串匹配算法效率.png b/Algorithm/A类:基本算法/image/字符串匹配算法效率.png new file mode 100644 index 00000000..e8f8a383 Binary files /dev/null and b/Algorithm/A类:基本算法/image/字符串匹配算法效率.png differ diff --git a/Algorithm/A类:基本算法/image/广度优先搜索-例子.png b/Algorithm/A类:基本算法/image/广度优先搜索-例子.png new file mode 100644 index 00000000..fedf8aac Binary files /dev/null and b/Algorithm/A类:基本算法/image/广度优先搜索-例子.png differ diff --git a/Algorithm/A类:基本算法/image/广度优先搜索-层次.png b/Algorithm/A类:基本算法/image/广度优先搜索-层次.png new file mode 100644 index 00000000..0830d74f Binary files /dev/null and b/Algorithm/A类:基本算法/image/广度优先搜索-层次.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-冒泡排序.png b/Algorithm/A类:基本算法/image/排序算法-冒泡排序.png new file mode 100644 index 00000000..fbbdb59d Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-冒泡排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-合并排序.gif b/Algorithm/A类:基本算法/image/排序算法-合并排序.gif new file mode 100644 index 00000000..46f5ab1d Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-合并排序.gif differ diff --git a/Algorithm/A类:基本算法/image/排序算法-基数排序.jpg b/Algorithm/A类:基本算法/image/排序算法-基数排序.jpg new file mode 100644 index 00000000..b1616df4 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-基数排序.jpg differ diff --git a/Algorithm/A类:基本算法/image/排序算法-基数排序演示.png b/Algorithm/A类:基本算法/image/排序算法-基数排序演示.png new file mode 100644 index 00000000..81ae6336 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-基数排序演示.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-基数排序过程.png b/Algorithm/A类:基本算法/image/排序算法-基数排序过程.png new file mode 100644 index 00000000..1763ad78 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-基数排序过程.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-堆排序.png b/Algorithm/A类:基本算法/image/排序算法-堆排序.png new file mode 100644 index 00000000..20002a07 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-堆排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-奇偶排序.png b/Algorithm/A类:基本算法/image/排序算法-奇偶排序.png new file mode 100644 index 00000000..25586015 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-奇偶排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-希尔排序.png b/Algorithm/A类:基本算法/image/排序算法-希尔排序.png new file mode 100644 index 00000000..f18cfc31 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-希尔排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-快速排序.jpg b/Algorithm/A类:基本算法/image/排序算法-快速排序.jpg new file mode 100644 index 00000000..3cb74f56 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-快速排序.jpg differ diff --git a/Algorithm/A类:基本算法/image/排序算法-插入排序.jpg b/Algorithm/A类:基本算法/image/排序算法-插入排序.jpg new file mode 100644 index 00000000..cbe8848f Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-插入排序.jpg differ diff --git a/Algorithm/A类:基本算法/image/排序算法-桶排序.png b/Algorithm/A类:基本算法/image/排序算法-桶排序.png new file mode 100644 index 00000000..2827fb61 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-桶排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-稳定性.png b/Algorithm/A类:基本算法/image/排序算法-稳定性.png new file mode 100644 index 00000000..78951b2f Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-稳定性.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-算法效率比较.png b/Algorithm/A类:基本算法/image/排序算法-算法效率比较.png new file mode 100644 index 00000000..12ad1a4d Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-算法效率比较.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法-计数排序.jpg b/Algorithm/A类:基本算法/image/排序算法-计数排序.jpg new file mode 100644 index 00000000..dc8e7232 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-计数排序.jpg differ diff --git a/Algorithm/A类:基本算法/image/排序算法-选择排序.gif b/Algorithm/A类:基本算法/image/排序算法-选择排序.gif new file mode 100644 index 00000000..ef0c73ee Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-选择排序.gif differ diff --git a/Algorithm/A类:基本算法/image/排序算法-鸡尾酒排序.png b/Algorithm/A类:基本算法/image/排序算法-鸡尾酒排序.png new file mode 100644 index 00000000..d6c7a653 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法-鸡尾酒排序.png differ diff --git a/Algorithm/A类:基本算法/image/排序算法.png b/Algorithm/A类:基本算法/image/排序算法.png new file mode 100644 index 00000000..7ecd3092 Binary files /dev/null and b/Algorithm/A类:基本算法/image/排序算法.png differ diff --git a/Algorithm/A类:基本算法/image/搜索算法-B树.png b/Algorithm/A类:基本算法/image/搜索算法-B树.png new file mode 100644 index 00000000..77685825 Binary files /dev/null and b/Algorithm/A类:基本算法/image/搜索算法-B树.png differ diff --git a/Algorithm/A类:基本算法/image/搜索算法-红黑树.webp b/Algorithm/A类:基本算法/image/搜索算法-红黑树.webp new file mode 100644 index 00000000..6d2996b9 Binary files /dev/null and b/Algorithm/A类:基本算法/image/搜索算法-红黑树.webp differ diff --git a/Algorithm/A类:基本算法/image/旅行商问题-分支限界.png b/Algorithm/A类:基本算法/image/旅行商问题-分支限界.png new file mode 100644 index 00000000..4ad0b2f4 Binary files /dev/null and b/Algorithm/A类:基本算法/image/旅行商问题-分支限界.png differ diff --git a/Algorithm/A类:基本算法/image/旅行商问题-回溯法.png b/Algorithm/A类:基本算法/image/旅行商问题-回溯法.png new file mode 100644 index 00000000..097364bb Binary files /dev/null and b/Algorithm/A类:基本算法/image/旅行商问题-回溯法.png differ diff --git a/Algorithm/A类:基本算法/image/旅行商问题.png b/Algorithm/A类:基本算法/image/旅行商问题.png new file mode 100644 index 00000000..2daa8cd6 Binary files /dev/null and b/Algorithm/A类:基本算法/image/旅行商问题.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-2-3树.png b/Algorithm/A类:基本算法/image/查找算法-2-3树.png new file mode 100644 index 00000000..5b61114c Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-2-3树.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-2-3树性质.png b/Algorithm/A类:基本算法/image/查找算法-2-3树性质.png new file mode 100644 index 00000000..da00a1a0 Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-2-3树性质.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-2-3树效率.png b/Algorithm/A类:基本算法/image/查找算法-2-3树效率.png new file mode 100644 index 00000000..9321ad74 Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-2-3树效率.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-B+树.png b/Algorithm/A类:基本算法/image/查找算法-B+树.png new file mode 100644 index 00000000..f2fede1c Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-B+树.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-二叉搜索树.jpeg b/Algorithm/A类:基本算法/image/查找算法-二叉搜索树.jpeg new file mode 100644 index 00000000..571e21da Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-二叉搜索树.jpeg differ diff --git a/Algorithm/A类:基本算法/image/查找算法-二叉树与二分法.png b/Algorithm/A类:基本算法/image/查找算法-二叉树与二分法.png new file mode 100644 index 00000000..428adeeb Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-二叉树与二分法.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-哈希搜索效率.png b/Algorithm/A类:基本算法/image/查找算法-哈希搜索效率.png new file mode 100644 index 00000000..948fb926 Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-哈希搜索效率.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-红黑树.png b/Algorithm/A类:基本算法/image/查找算法-红黑树.png new file mode 100644 index 00000000..162c6868 Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-红黑树.png differ diff --git a/Algorithm/A类:基本算法/image/查找算法-红黑树效率.png b/Algorithm/A类:基本算法/image/查找算法-红黑树效率.png new file mode 100644 index 00000000..85d79095 Binary files /dev/null and b/Algorithm/A类:基本算法/image/查找算法-红黑树效率.png differ diff --git a/Algorithm/A类:基本算法/image/深度优先搜索-例子.png b/Algorithm/A类:基本算法/image/深度优先搜索-例子.png new file mode 100644 index 00000000..e40424ee Binary files /dev/null and b/Algorithm/A类:基本算法/image/深度优先搜索-例子.png differ diff --git a/Algorithm/A类:基本算法/image/蛮力法-顺序查找.png b/Algorithm/A类:基本算法/image/蛮力法-顺序查找.png new file mode 100644 index 00000000..40b3ce77 Binary files /dev/null and b/Algorithm/A类:基本算法/image/蛮力法-顺序查找.png differ diff --git a/Algorithm/A类:基本算法/image/递归算法-时间复杂度.png b/Algorithm/A类:基本算法/image/递归算法-时间复杂度.png new file mode 100644 index 00000000..b8343839 Binary files /dev/null and b/Algorithm/A类:基本算法/image/递归算法-时间复杂度.png differ diff --git a/Algorithm/A类:基本算法/image/递归算法-等比收缩说明.png b/Algorithm/A类:基本算法/image/递归算法-等比收缩说明.png new file mode 100644 index 00000000..8c9f7d6e Binary files /dev/null and b/Algorithm/A类:基本算法/image/递归算法-等比收缩说明.png differ diff --git a/Algorithm/A类:基本算法/image/递归算法-线性收缩说明.png b/Algorithm/A类:基本算法/image/递归算法-线性收缩说明.png new file mode 100644 index 00000000..59b3b57f Binary files /dev/null and b/Algorithm/A类:基本算法/image/递归算法-线性收缩说明.png differ diff --git a/Algorithm/B类:数据结构算法/1 图算法.md b/Algorithm/B类:数据结构算法/1 图算法.md new file mode 100644 index 00000000..e69de29b diff --git a/Algorithm/B类:数据结构算法/1.1 图算法-Dijkstra算法 copy.md b/Algorithm/B类:数据结构算法/1.1 图算法-Dijkstra算法 copy.md new file mode 100644 index 00000000..43e5bb7e --- /dev/null +++ b/Algorithm/B类:数据结构算法/1.1 图算法-Dijkstra算法 copy.md @@ -0,0 +1,168 @@ +# 图算法-Dijkstra算法 + +> 目录 +>* 图算法-Dijkstra算法 +>* 图算法-Floyd算法 +>* 图算法-Bellman-Ford算法 +>* 图算法-Prim算法 +>* 图算法-Kruskal算法 + + + +>参考文献 +> [https://www.cnblogs.com/msymm/p/9769915.html](https://www.cnblogs.com/msymm/p/9769915.html) + + +## 1 问题分析 +最短路径算法。用于计算一个节点到其他节点的最短路径。 + +Dijkstra算法算是贪心思想实现的,首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。 + +## 2 算法原理 +通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。 + +此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。 + +初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是"起点s到该顶点的路径"。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 ... 重复该操作,直到遍历完所有顶点。 + +## 3 算法步骤 + +### 基本步骤 +1. 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为"起点s到该顶点的距离"[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。 + +2. 从U中选出"距离最短的顶点k",并将顶点k加入到S中;同时,从U中移除顶点k。 + +3. 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。 + +4. 重复步骤(2)和(3),直到遍历完所有顶点。 + +### 图解过程 +![](image/Dijkstra算法.jpg) + +### 详细说明 + +初始状态:S是已计算出最短路径的顶点集合,U是未计算除最短路径的顶点的集合! + +* 第1步:将顶点D加入到S中。 + 此时,S={D(0)}, U={A(∞),B(∞),C(3),E(4),F(∞),G(∞)}。 注:C(3)表示C到起点D的距离是3。 + +* 第2步:将顶点C加入到S中。 + 上一步操作之后,U中顶点C到起点D的距离最短;因此,将C加入到S中,同时更新U中顶点的距离。以顶点F为例,之前F到D的距离为∞;但是将C加入到S之后,F到D的距离为9=(F,C)+(C,D)。 + 此时,S={D(0),C(3)}, U={A(∞),B(23),E(4),F(9),G(∞)}。 + +* 第3步:将顶点E加入到S中。 + 上一步操作之后,U中顶点E到起点D的距离最短;因此,将E加入到S中,同时更新U中顶点的距离。还是以顶点F为例,之前F到D的距离为9;但是将E加入到S之后,F到D的距离为6=(F,E)+(E,D)。 + 此时,S={D(0),C(3),E(4)}, U={A(∞),B(23),F(6),G(12)}。 + +* 第4步:将顶点F加入到S中。 + 此时,S={D(0),C(3),E(4),F(6)}, U={A(22),B(13),G(12)}。 + +* 第5步:将顶点G加入到S中。 + 此时,S={D(0),C(3),E(4),F(6),G(12)}, U={A(22),B(13)}。 + +* 第6步:将顶点B加入到S中。 + 此时,S={D(0),C(3),E(4),F(6),G(12),B(13)}, U={A(22)}。 + +* 第7步:将顶点A加入到S中。 + 此时,S={D(0),C(3),E(4),F(6),G(12),B(13),A(22)}。 + +此时,起点D到各个顶点的最短距离就计算出来了:A(22) B(13) C(3) D(0) E(4) F(6) G(12)。 + +## 4 算法效率 + +时间复杂度$O(n^2)$ +## 5 算法实现 + +``` +#include +#include +using namespace std; +const int Max=100; +string Int_to_String(int n)//int转换string +{ +ostringstream stream; +stream<>i>>j; + cin>>arc[i][j]; + } +} +void Dijkstra(MGraph G,int v){ + int dist[Max],s[Max]; + string path[Max]; + for(int i=0;i"+Int_to_String(G.vertex[i]); + else + path[i]=""; + } + s[0]=v; + dist[v]=0; + int num=1; + for(int i=0;i(dist[k]+G.arc[k][j])){ + dist[j]=dist[k]+G.arc[k][j]; + path[j]=path[k]+"->"+Int_to_String(j); + } + } + dist[k]=0; + if(num==G.vertexNum) + break; + } + cout<<"找到终点的顺序为:"<>v; + cout<<"输入图的顶点数和边数:"<>n>>e; + MGraph G(n,e); + Dijkstra(G,v); + return 0; +} +``` \ No newline at end of file diff --git a/Algorithm/B类:数据结构算法/1.2 图算法-Floyd算法.md b/Algorithm/B类:数据结构算法/1.2 图算法-Floyd算法.md new file mode 100644 index 00000000..671ec40d --- /dev/null +++ b/Algorithm/B类:数据结构算法/1.2 图算法-Floyd算法.md @@ -0,0 +1,281 @@ + +# 图算法-Floyd算法 + +> 目录 +>* 图算法-Dijkstra算法 +>* 图算法-Floyd算法 +>* 图算法-Bellman-Ford算法 +>* 图算法-Prim算法 +>* 图算法-Kruskal算法 + +> 参考文献 +> [https://www.jianshu.com/p/f73c7a6f5a53](https://www.jianshu.com/p/f73c7a6f5a53) +## 1 问题分析 + +Floyd算法是一个经典的**动态规划算法**,它又被称为插点法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。Floyd算法是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,算法目标是寻找从点i到点j的最短路径。 + +## 2 算法原理 + +Floyd算法的基本思想: +可以将问题分解: +第一、先找出最短的距离 +第二、然后在考虑如何找出对应的行进路线。 + +> 以后再整理一下文字内容 + + +如何找出最短路径呢,这里还是用到动态规划的知识,对于任何一个城市而言,i到j的最短距离不外乎存在经过i与j之间经过k和不经过k两种可能,所以可以令k=1,2,3,...,n(n是城市的数目),在检查d(ij)与d(ik)+d(kj)的值;在此d(ik)与d(kj)分别是目前为止所知道的i到k与k到j的最短距离,因此d(ik)+d(kj)就是i到j经过k的最短距离。所以,若有d(ij)>d(ik)+d(kj),就表示从i出发经过k再到j的距离要比原来的i到j距离短,自然把i到j的d(ij)重写为d(ik)+d(kj),每当一个k查完了,d(ij)就是目前的i到j的最短距离。重复这一过程,最后当查完所有的k时,d(ij)里面存放的就是i到j之间的最短距离了。 + +接下来就要看一看如何找出最短路径所行经的城市了,这里要用到另一个矩阵P,它的定义是这样的:p(ij)的值如果为p,就表示i到j的最短行经为i->...->p->j,也就是说p是i到j的最短行径中的j之前的最后一个城市。P矩阵的初值为p(ij)=i。有了这个矩阵之后,要找最短路径就轻而易举了。对于i到j而言找出p(ij),令为p,就知道了路径i->...->p->j;再去找p(ip),如果值为q,i到p的最短路径为i->...->q->p;再去找p(iq),如果值为r,i到q的最短路径为i->...->r->q;所以一再反复,到了某个p(it)的值为i时,就表示i到t的最短路径为i->t,就会的到答案了,i到j的最短行径为i->t->...->q->p->j。因为上述的算法是从终点到起点的顺序找出来的,所以输出的时候要把它倒过来。 + +但是,如何动态的回填P矩阵的值呢?回想一下,当d(ij)>d(ik)+d(kj)时,就要让i到j的最短路径改为走i->...->k->...->j这一条路,但是d(kj)的值是已知的,换句话说,就是k->...->j这条路是已知的,所以k->...->j这条路上j的上一个城市(即p(kj))也是已知的,当然,因为要改走i->...->k->...->j这一条路,j的上一个城市正好是p(kj)。所以一旦发现d(ij)>d(ik)+d(kj),就把p(kj)存入p(ij)。 + +## 3 算法过程 + +![](image/Floyd算法.png) + +1. 定义n×n的方阵序列D-1, D0 , … Dn-1, + +2. 初始化: D-1=C + D-1[i][j]=边的长度,表示初始的从i到j的最短路径长度,即它是从i到j的中间不经过其他中间点的最短路径。 + +3. 迭代:设Dk-1已求出,如何得到Dk(0≤k≤n-1)? + + * Dk-1[i][j]表示从i到j的中间点不大于k-1的最短路径p:i…j, + * 考虑将顶点k加入路径p得到顶点序列q:i…k…j, + * 若q不是路径,则当前的最短路径仍是上一步结果:Dk[i][j]= Dk-1[i][j]; + * 否则若q的长度小于p的长度,则用q取代p作为从i到j的最短路径 + +4. 因为q的两条子路径i…k和k…j皆是中间点不大于k-1的最短路径,所以从i到j中间点不大于k的最短路径长度为: +$$ +Dk[i][j]=min\{ Dk-1[i][j], Dk-1[i][k] +Dk-1[k][j]\} +$$ + +## 4 算法效率 + +时间复杂度为$O(n^3)$ + +## 5 算法实现 + +``` +#include +#include +using namespace std; + +#define len 100 +#define INF 999999 + +class Graph{ + // 内部类 + private: + // 邻接表中表对应的链表的顶点 + class ENode{ + public: + int vex; // 顶点 + int weight; // 权重 + ENode *nextEdge; // 指向下一条弧 + }; + + // 邻接表中表的顶点 + class VNode{ + public: + char data; // 顶点信息 + ENode *firstEdge; // 指向第一条依付该顶点的弧 + }; + + // 私有成员 + private: + int n; // 节点个数 + int e; // 边的个数 + VNode mVexs[len]; + + public: + Graph(){ + ENode *node1, *node2; + n = 7; + e = 12; + + // 设置节点为默认数值 + string nodes = "ABCDEFG"; + // 输入节点 + for(int i=0; i < n; i++){ + mVexs[i].data = nodes[i]; + mVexs[i].firstEdge = NULL; + } + + // 设置边为默认值 + char edges[][2] = { + {'A', 'B'}, + {'A', 'F'}, + {'A', 'G'}, + {'B', 'C'}, + {'B', 'F'}, + {'C', 'D'}, + {'C', 'E'}, + {'C', 'F'}, + {'D', 'E'}, + {'E', 'F'}, + {'E', 'G'}, + {'F', 'G'} + }; + + // 边的权重 + int weights[len] = {12, 16, 14, 10, 7, 3, 5, 6, 4, 2, 8, 9}; + + // 初始化邻接表的边 + for(int i=0; i < e; i++){ + int start = get_Node_Index(edges[i][0]); + int end = get_Node_Index(edges[i][1]); + + // 初始化 node1 + node1 = new ENode(); + node1->vex = end; + node1->weight = weights[i]; + node1->nextEdge = NULL; + // 将 node 添加到 start 所在链表的末尾 + if(mVexs[start].firstEdge == NULL){ + mVexs[start].firstEdge = node1; + } + else{ + linkLast(mVexs[start].firstEdge, node1); + } + + // 初始化 node2 + node2 = new ENode(); + node2->vex = start; + node2->weight = weights[i]; + node2->nextEdge = NULL; + // 将 node 添加到 end 所在链表的末尾 + if(mVexs[end].firstEdge == NULL){ + mVexs[end].firstEdge = node2; + } + else{ + linkLast(mVexs[end].firstEdge, node2); + } + } + } + + // 相邻节点链接子函数 + void linkLast(ENode*p1, ENode*p2){ + ENode*p = p1; + while(p->nextEdge){ + p = p->nextEdge; + } + p->nextEdge = p2; + } + + // 返回顶点下标 + int get_Node_Index(char number){ + for(int i=0; i < n; i++){ + if(number == mVexs[i].data){ + return i; + } + } + return -1; //这句话永远不会执行的 + } + + // 输出邻接表 + void print(){ + for(int i=0; i < n; i ++){ + cout< "<vex; + temp = temp->nextEdge; + } + cout<vex == n){ + return enode->weight; + } + enode = enode->nextEdge; + } + return INF; + } + + // 弗洛伊德算法 + void floyd(){ + int dist[n][n]; // 距离矩阵 + int path[7][7]; // 路径矩阵, 7为节点数目 + int i, j, k; + int temp; + + // 初始化权重 + for(i = 0; i < n; i++){ + for(j = 0; j < n; j++){ + if(i == j){ + dist[i][j] = 0; + } + else{ + dist[i][j] = getWeight(i, j); + } + path[i][j] = i; + } + } + + // floyd 算法开始 + for(k = 0; k < n; k++){ + for(i = 0; i < n; i++){ + for(j = 0; j < n; j++){ + temp = (dist[i][k] == INF || dist[k][j] == INF)? INF : (dist[i][k] + dist[k][j]); + if(temp < dist[i][j]){ + dist[i][j] = temp; + path[i][j] = path[k][j]; + } + } + } + } + + // 打印出两点之间最短距离 + 路径 + for(i = 0; i < n-1; i++){ + for(j = i+1; j < n; j++){ + if(dist[i][j] < 10){ + cout< "< "< 目录 +>* 图算法-Dijkstra算法 +>* 图算法-Floyd算法 +>* 图算法-Bellman-Ford算法 +>* 图算法-Prim算法 +>* 图算法-Kruskal算法 \ No newline at end of file diff --git a/Algorithm/B类:数据结构算法/1.4 图算法-Kruskal算法.md b/Algorithm/B类:数据结构算法/1.4 图算法-Kruskal算法.md new file mode 100644 index 00000000..328af8b0 --- /dev/null +++ b/Algorithm/B类:数据结构算法/1.4 图算法-Kruskal算法.md @@ -0,0 +1,113 @@ +# 图算法-Dijkstra算法 + +> 目录 +>* 图算法-Dijkstra算法 +>* 图算法-Floyd算法 +>* 图算法-Bellman-Ford算法 +>* 图算法-Prim算法 +>* 图算法-Kruskal算法 + +> 参考文献 +> [https://www.cnblogs.com/ggzhangxiaochao/p/9070873.html](https://www.cnblogs.com/ggzhangxiaochao/p/9070873.html) +## 1 问题描述 +Kruskal算法是一种用来寻找最小生成树的算法,由Joseph Kruskal在1956年发表。用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪婪算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。 + +## 2 算法原理 + +1. 记Graph中有v个顶点,e个边 + +2. 新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边 + +3. 将原图Graph中所有e个边按权值从小到大排序 + +4. 循环:从权值最小的边开始遍历每条边。if这条边连接的两个节点于图Graphnew中不在同一个连通分量中,添加这条边到图Graphnew中。直至图Graph中所有的节点都在同一个连通分量中。 + + +## 3 算法流程 + +1. 首先第一步,我们有一张图Graph,有若干点和边 +

+ +2. 将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。 + + +

+ +3. 在剩下的变中寻找。我们找到了CE。这里边的权重也是5 + +

+ +4. 依次类推我们找到了6,7,7,即DF,AB,BE。 +

+ +5. 下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。最后就剩下EG和FG了。当然我们选择了EG。 +

+ + +## 4 算法效率 + +时间复杂度:$O(E*\log_2V)$ + +## 5 算法实现 + +``` +typedef struct +{ + char vertex[VertexNum]; //顶点表 + int edges[VertexNum][VertexNum]; //邻接矩阵,可看做边表 + int n,e; //图中当前的顶点数和边数 +}MGraph; + +typedef struct node +{ + int u; //边的起始顶点 + int v; //边的终止顶点 + int w; //边的权值 +}Edge; + +void kruskal(MGraph G) +{ + int i,j,u1,v1,sn1,sn2,k; + int vset[VertexNum]; //辅助数组,判定两个顶点是否连通 + int E[EdgeNum]; //存放所有的边 + k=0; //E数组的下标从0开始 + for (i=0;i %d, %d",E[j].u,E[j].v,E[j].w); + k++; + for (i=0;i 目录 +>* 图算法-Dijkstra算法 +>* 图算法-Floyd算法 +>* 图算法-Bellman-Ford算法 +>* 图算法-Prim算法 +>* 图算法-Kruskal算法 + +> 参考文献 +> [https://www.cnblogs.com/ggzhangxiaochao/p/9070873.html](https://www.cnblogs.com/ggzhangxiaochao/p/9070873.html) + +## 1 问题分析 + +普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。 + +## 2 算法原理 + +1. 输入:一个加权连通图,其中顶点集合为V,边集合为E; + +2. 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空; + +3. 重复下列操作,直到Vnew = V: + + * 在集合E中选取权值最小的边,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一); + + * 将v加入集合Vnew中,将边加入集合Enew中; + +4. 输出:使用集合Vnew和Enew来描述所得到的最小生成树。 + + +## 3 算法过程 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
图例说明不可选可选已选(Vnew
  +

+
此为原始的加权连通图。每条边一侧的数字代表其权值。---
+

+
顶点D被任意选为起始点。顶点ABEF通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。C, GA, B, E, FD
  +

+
下一个顶点为距离DA最近的顶点。BD为9,距A为7,E为15,F为6。因此,FDA最近,因此将顶点F与相应边DF以高亮表示。C, GB, E, FA, D
算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。CB, E, GA, D, F
  +

+
在当前情况下,可以在CEG间进行选择。CB为8,EB为7,GF为11。E最近,因此将顶点E与相应边BE高亮表示。C, E, GA, D, F, B
  +

+
这里,可供选择的顶点只有CGCE为5,GE为9,故选取C,并与边EC一同高亮表示。C, GA, D, F, B, E
+

+
顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EGGA, D, F, B, E, C
+

+
现在,所有顶点均已被选取,图中绿色部分即为连通图的最小生成树。在此例中,最小生成树的权值之和为39。A, D, F, B, E, C, G
+ +## 4 算法效率 +顶点数V,边数E。时间复杂度: + +* 邻接矩阵:$O(V^2)$ +* 邻接表:$O(E\log_2V)$ + + +## 5 算法实现 + +``` +#include +#include +#define VertexType int +#define VRType int +#define MAX_VERtEX_NUM 20 +#define InfoType char +#define INFINITY 65535 +typedef struct { + VRType adj; //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。 + InfoType * info; //弧额外含有的信息指针 +}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; +typedef struct { + VertexType vexs[MAX_VERtEX_NUM]; //存储图中顶点数据 + AdjMatrix arcs; //二维数组,记录顶点之间的关系 + int vexnum,arcnum; //记录图的顶点数和弧(边)数 +}MGraph; +//根据顶点本身数据,判断出顶点在二维数组中的位置 +int LocateVex(MGraph G,VertexType v){ + int i=0; + //遍历一维数组,找到变量v + for (; ivexnum),&(G->arcnum)); + for (int i=0; ivexnum; i++) { + scanf("%d",&(G->vexs[i])); + } + for (int i=0; ivexnum; i++) { + for (int j=0; jvexnum; j++) { + G->arcs[i][j].adj=INFINITY; + G->arcs[i][j].info=NULL; + } + } + for (int i=0; iarcnum; i++) { + int v1,v2,w; + scanf("%d,%d,%d",&v1,&v2,&w); + int m=LocateVex(*G, v1); + int n=LocateVex(*G, v2); + if (m==-1 ||n==-1) { + printf("no this vertex\n"); + return; + } + G->arcs[n][m].adj=w; + G->arcs[m][n].adj=w; + } +} +//辅助数组,用于每次筛选出权值最小的边的邻接点 +typedef struct { + VertexType adjvex;//记录权值最小的边的起始点 + VRType lowcost;//记录该边的权值 +}closedge[MAX_VERtEX_NUM]; +closedge theclose;//创建一个全局数组,因为每个函数中都会使用到 +//在辅助数组中找出权值最小的边的数组下标,就可以间接找到此边的终点顶点。 +int minimun(MGraph G,closedge close){ + int min=INFINITY; + int min_i=-1; + for (int i=0; i0 && close[i].lowcost < min) { + min=close[i].lowcost; + min_i=i; + } + } + //返回最小权值所在的数组下标 + return min_i; +} +//普里姆算法函数,G为无向网,u为在网中选择的任意顶点作为起始点 +void miniSpanTreePrim(MGraph G,VertexType u){ + //找到该起始点在顶点数组中的位置下标 + int k=LocateVex(G, u); + //首先将与该起始点相关的所有边的信息:边的起始点和权值,存入辅助数组中相应的位置,例如(1,2)边,adjvex为0,lowcost为6,存入theclose[1]中,辅助数组的下标表示该边的顶点2 + for (int i=0; i