This commit is contained in:
estomm
2020-01-04 23:56:24 +08:00
parent f005fca50e
commit 69da6ff9de
81 changed files with 5312 additions and 35 deletions

View File

@@ -23,6 +23,12 @@
![](image/算法流程.png)
1. 理解问题
2. 选择策略
3. 算法设计
4. 正确性证明
5. 算法分析
6. 程序设计
## 3 算法分类

View File

@@ -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)<cg(n)$
4. $f(n)=ω(g(n))$
4. $f(n)=ω(g(n))$不可达下界
对任意正数$c$存在$n0$使得对一切$n≥n0有0≤cg(n)<f(n)$
5. $f(n)=Θ(g(n)) ⇔ f(n)=O(g(n))$ 且$f(n)=Ω(g(n))$紧渐进界
6. $O(1)$表示常数函数
@@ -71,10 +71,21 @@ $$
1. 设f,g,h 是定义域为自然数集N上的函数若对某个其它的函数h, 我们有f =O(h)和g=O(h)那么f+g = O(h).
2. 假设f 和g是定义域为自然数集合的函数且满足g=O(f)那么f+g=Θ(f).
### 常见函数的届
$$
\log_2n=o(\sqrt{n})\\
\log_an=Θ(log_bn)\\
\log_bn=o(n^α)\\
n^α=o(b^n)\\
n!=o(n^n)\\
n!= ω(2^n)\\
log(n!)= Θ(n\log n)
$$
## 3 算法的基本复杂性类型
### 复杂性分析的基本步骤
$$
n^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)

View File

@@ -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个字符组成的串称为文本一个mm≤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<m and P[j]=T[i+j]
j←j+1
if j=m return i
return -1
```
## 5 最近对问题
### 理解问题
找出一个包含n个点的集合中距离最近的两个点。
### 选择策略
分别计算每一点对之间的距离然后从中找出距离最小的那一对。为了避免同一点对计算两次可以只考虑i<j的点对(Pi, Pj)
### 算法设计
算法 bruteForceClosesPoints(P)
```
//蛮力法求解平面中距离最近的两点
//输入一个n(n≥2)个点的列表PP1=(x1, y1)Pn=(xn, yn)
//输出:两个最近点的下标
dmin←∞
for i←0 to n-2 do
for j←i+1 to n-1 do
d←(xi-xj)2+(yi-yj)2
if d<dmin
dmin←d; index1←i; index2←j;
return index1,index2
```
### 正确性证明
### 算法分析
O(n2)
### 程序设计

View File

@@ -0,0 +1,428 @@
# 查找算法
> 阅读目录
1. 顺序查找
2. 二分查找
3. 插值查找
4. 斐波那契查找
5. 树表查找
6. 分块查找
7. 哈希查找
## 0 概述
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。
### 查找定义
根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
### 查找算法分类:
1. 静态查找和动态查找;
静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
2. 无序查找和有序查找。
* 无序查找:被查找数列有序无序均可;
* 有序查找:被查找数列必须为有序数列。
### 平均查找长度Average Search LengthASL
需和指定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<n; i++)
if(a[i]==value)
return i;
return -1;
}
```
## 2 二分查找
### 说明
元素必须是有序的,如果是无序的则要先进行排序操作。
### 基本思想
也称为是折半查找属于有序查找算法。用给定值k先与中间结点的关键字比较中间结点把线形表分成两个子表若相等则查找成功若不相等再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表这样递归进行直到查找到或查找结束发现表中没有这样的结点。
### 复杂度分析
最坏情况下关键词比较次数为log2(n+1)且期望时间复杂度为O(log2n)
> 注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
### 代码实现
```
//二分查找折半查找版本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)
low = mid+1;
}
return -1;
}
//二分查找,递归版本
int BinarySearch2(int a[], int value, int low, int high)
{
int mid = low+(high-low)/2;
if(a[mid]==value)
return mid;
if(a[mid]>value)
return BinarySearch2(a, value, low, mid-1);
if(a[mid]<value)
return BinarySearch2(a, value, mid+1, high);
}
```
## 3 插值查找
### 说明
折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
$$
mid=(low+high)/2, 即mid=low+1/2*(high-low);
$$
通过类比,我们可以将查找的点改进为如下:
$$
mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
$$
也就是将上述的比例参数1/2改进为自适应的根据关键字在整个有序表中所处的位置让mid值的变化更靠近关键字key这样也就间接地减少了比较次数。
### 基本思想
基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
> 注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
### 复杂度分析
查找成功或者失败的时间复杂度均为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]<value)
return InsertionSearch(a, value, mid+1, high);
}
```
## 4 斐波那契查找
  
> 在介绍斐波那契查找算法之前,我们先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。
>  黄金比例又称黄金分割是指事物各部分间一定的数学比例关系即将整体一分为二较大部分与较小部分之比等于整体与较大部分之比其比值约为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 <memory>
#include <iostream>
using namespace std;
const int max_size=20;//斐波那契数组的长度
/*构造一个斐波那契数组*/
void Fibonacci(int * F)
{
F[0]=0;
F[1]=1;
for(int i=2;i<max_size;++i)
F[i]=F[i-1]+F[i-2];
}
/*定义斐波那契查找法*/
int FibonacciSearch(int *a, int n, int key) //a为要查找的数组,n为要查找的数组长度,key为要查找的关键字
{
int low=0;
int high=n-1;
int F[max_size];
Fibonacci(F);//构造一个斐波那契数组F
int k=0;
while(n>F[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;i<F[k]-1;++i)
temp[i]=a[n-1];
while(low<=high)
{
int mid=low+F[k-1]-1;
if(key<temp[mid])
{
high=mid-1;
k-=1;
}
else if(key>temp[mid])
{
low=mid+1;
k-=2;
}
else
{
if(mid<n)
return mid; //若相等则说明mid即为查找到的位置
else
return n-1; //若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<<key<<" is located at:"<<index;
return 0;
}
```
## 5 树表查找
* 二叉查找树
* 平衡二叉树AVL树
* 红黑树(平衡二叉树的一种实现)
* B树平衡多叉树
* B+树(非叶节点建立索引,叶节点保存数据)
* 2-3树B树或平衡多叉树的例子
二叉查找树平均查找性能不错为O(logn)但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化我们可以使用平衡查找树。平衡查找树中的2-3查找树这种数据结构在插入之后能够进行自平衡操作从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难红黑树是2-3树的一种简单高效的实现他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树应用非常广泛很多编程语言的内部实现都或多或少的采用了红黑树。
  除此之外2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。
## 5.1 树表查找—二叉树查找算法。
### 基本思想
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在和每个节点的父节点比较大小,查找最适合的范围。这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉查找树BinarySearch Tree也叫二叉搜索树或称二叉排序树Binary Sort Tree或者是一棵空树或者是具有下列性质的二叉树
1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3. 任意节点的左、右子树也分别为二叉查找树。
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
不同形态的二叉查找树如下图所示:
![](image/查找算法-二叉搜索树.jpeg)
### 复杂度分析
它和二分查找一样插入和查找的时间复杂度均为O(logn)但是在最坏的情况下仍然会有O(n)的时间复杂度。
原因在于插入和删除元素的时候树没有保持平衡比如我们查找上图b中的“93”我们需要进行n次查找操作。我们追求的是在最坏的情况下仍然有较好的时间复杂度这就是平衡查找树设计的初衷。
下图为二叉树查找和顺序查找以及二分查找性能的对比图:
![](image/查找算法-二叉树与二分法.png)
## 5.2 平衡查找树之2-3查找树2-3 Tree
### 基本思想
2-3查找树定义和二叉树不一样2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node)他保存1个key和左右两个子节点。对应3节点(3-node)保存两个Key2-3查找树的定义如下
1. 要么为空,要么:
2. 对于2节点该节点保存一个key及对应value以及两个指向左右节点的节点左节点也是一个2-3节点所有的值都比key要小右节点也是一个2-3节点所有的值比key要大。
3. 对于3节点该节点保存两个key及对应value以及三个指向左中右的节点。左节点也是一个2-3节点所有的值均比两个key中的最小的key还要小中间节点也是一个2-3节点中间节点的key值在两个跟节点key值之间右节点也是一个2-3节点节点的所有key值比两个key中的最大的key还要大。
![](image/查找算法-2-3树.png)
2-3查找树的性质
1. 如果中序遍历2-3查找树就可以得到排好序的序列
2. 在一个完全平衡的2-3查找树中根节点到每一个为空节点的距离都相同。这也是平衡树中“平衡”一词的概念根节点到叶节点的最长距离对应于查找算法的最坏情况而平衡树中根节点到叶节点的距离都一样最坏情况也具有对数复杂度。
![](image/查找算法-2-3树性质.png)
### 复杂度分析:
2-3树的查找效率与树的高度是息息相关的。
* 在最坏的情况下也就是所有的节点都是2-node节点查找效率为lgN
* 在最好的情况下所有的节点都是3-node节点查找效率为log3N约等于0.631lgN
距离来说对于1百万个节点的2-3树树的高度为12-20之间对于10亿个节点的2-3树树的高度为18-30之间。
对于插入来说只需要常数次操作即可完成因为他只需要修改与该节点关联的节点即可不需要检查其他节点所以效率和查找类似。下面是2-3查找树的效率
![](image/查找算法-2-3树效率.png)
## 5.3 平衡查找树之红黑树Red-Black Tree
2-3查找树能保证在插入元素之后能保持树的平衡状态最坏情况下即所有的子节点都是2-node树的高度为lgn从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂于是就有了一种简单实现2-3树的数据结构即红黑树Red-Black Tree
### 基本思想
红黑树首先是一种树形结构,同时又是一个二叉树(每个节点最多只能有两个孩子节点,左节点小于等于父节点,右节点大于父节点),为了保证树的左右孩子树相对平衡(深度相同),红黑树使用了节点标色的方式,将节点标记为红色或者黑色,在计算树的深度时只统计黑色节点的数量,不统计红色节点数量。
为了保证左右子树的平衡,红黑树定义了一些规则或者特点来维持平衡。
主要特点(规则)
* 每个节点要么是黑色,要么是红色。(节点非黑即红)
* 根节点是黑色。
* 每个叶子节点NULL是黑色为了简单期间一般会省略该节点
* 如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
* 从一个节点到该节点的每一个叶子子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
* 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行操作。
![](image/查找算法-红黑树.png)
红黑树平衡方法
前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
* 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
* 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
* 变色:结点的颜色由红变黑或由黑变红。
### 复杂度分析
最坏的情况就是红黑相间的路径长度是全黑路径长度的2倍。
红黑树的平均高度大约为2logn。
  下图是红黑树在各种情况下的时间复杂度可以看出红黑树是2-3查找树的一种实现它能保证最坏情况下仍然具有对数的时间复杂度。
![](image/查找算法-红黑树效率.png)
## 5.4 B树和B+树B Tree/B+ Tree
### 基本思想
B树定义
  B树可以看作是对2-3查找树的一种扩展即他允许每个节点有M-1个子节点。B树的插入及平衡化操作和2-3树很相似。
* 根节点至少有两个子节点
* 每个节点有M-1个key并且以升序排列
* 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
* 其它节点至少有M/2个子节点
下图是一个M=4 阶的B树:
![](image/搜索算法-B树.png)
  
B+树定义:
B+树是对B树的一种变形树它与B树的差异在于
* 有k个子结点的结点必然有k个关键码
* 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
* 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
如下图是一个B+树:
![](image/查找算法-B+树.png)
B和B+树的区别在于B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
由于B+树在内部节点上不好含数据信息因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
B+树的叶子结点都是相链的因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻所以缓存命中性没有B+树好。
但是B树也有优点其优点在于由于B树的每一个节点都包含key和value因此经常访问的元素可能离根节点更近因此访问也更迅速。
## 6 分块查找
> 分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
### 算法思想
将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)

View File

@@ -0,0 +1,173 @@
# 广度优先搜索
## 1 概述
### 特点
广度优先搜索BFSBreadth-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<int> 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<int, List<Edge>> _adjacentEdges
42 = new Dictionary<int, List<Edge>>();
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<Edge>();
56 _adjacentEdges.Add(begin, edges);
57 }
58
59 _adjacentEdges[begin].Add(new Edge(begin, end));
60 }
61
62 public List<int> BFS(int start)
63 {
64 List<int> traversal = new List<int>();
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<int> queue = new Queue<int>();
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 }
```

View File

@@ -0,0 +1,200 @@
# 深度优先搜索
## 1 概述
### 特点
深度优先搜索DFSDepth-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<int, List<Edge>> _adjacentEdges
46 = new Dictionary<int, List<Edge>>();
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<Edge>();
60 _adjacentEdges.Add(begin, edges);
61 }
62
63 _adjacentEdges[begin].Add(new Edge(begin, end));
64 }
65
66 public List<int> DFS(int start)
67 {
68 List<int> traversal = new List<int>();
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<int> stack = new Stack<int>();
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<int> RecursiveDFS(int start)
112 {
113 List<int> traversal = new List<int>();
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<int> 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 }
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,406 @@
# 线性时间排序算法
## 0 线性时间排序算法列表
<table style="border: 1px solid #000000; background-color: #ffffff;" border="1" align="center">
<tbody>
<tr style="background-color: #f8f8f8;" align="left" valign="middle">
<td style="text-align: center;" scope="col" colspan="6" align="left" valign="middle">
<p><span style="font-size: 16px;"><strong>线性时间排序</strong></span></p>
</td>
</tr>
<tr style="background-color: #f8f8f8;" align="left" valign="middle">
<td style="text-align: center;" scope="col" align="left" valign="middle"><span style="font-size: medium;"><strong>Name</strong></span></td>
<td style="text-align: center;"><strong><span style="font-size: 16px;">Average</span></strong></td>
<td style="text-align: center;"><strong><span style="font-size: 16px;">Worst</span></strong></td>
<td style="text-align: center;"><strong><span style="font-size: 16px;">Memory</span></strong></td>
<td style="text-align: center;"><strong><span style="font-size: 16px;">Stable</span></strong></td>
<td style="text-align: center;">
<p><strong><span style="font-size: 16px;"><span style="font-size: medium;">Description</span></span></strong></p>
</td>
</tr>
<tr align="left" valign="middle">
<td scope="col" align="left" valign="middle">
<p><span style="font-size: 16px;"><strong>&nbsp;<a href="#counting_sort">计数排序</a></strong></span></p>
<p><span style="font-size: 12px;"><strong><a href="#counting_sort">Counting Sort</a></strong></span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n + k</span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n + k</span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n + k</span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">Stable</span></p>
</td>
<td>
<p><span style="font-size: 16px;">&nbsp;Indexes using key values.</span></p>
</td>
</tr>
<tr align="left" valign="middle">
<td scope="col" align="left" valign="middle">
<p><span style="font-size: 16px;"><strong>&nbsp;<a href="#radix_sort">基数排序</a></strong></span></p>
<p><a href="#radix_sort"><span style="font-size: 12px;"><strong>Radix Sort</strong></span></a></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n * k</span></p>
</td>
<td style="text-align: center;"><span style="font-size: 16px;">&nbsp;n * k</span></td>
<td style="text-align: center;"><span style="font-size: 16px;">n + k</span></td>
<td style="text-align: center;"><span style="font-size: 16px;">Stable</span></td>
<td>
<p><span style="font-size: 16px;">&nbsp;Examines individual bits of keys.</span></p>
</td>
</tr>
<tr align="left" valign="middle">
<td scope="col" align="left" valign="middle">
<p><a href="#bucket_sort"><span style="font-size: 16px;"><strong>&nbsp;桶排序</strong></span></a></p>
<p><a href="#bucket_sort"><span style="font-size: 12px;"><strong>Bucket Sort</strong></span></a></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n + k</span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n<sup>2</sup></span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">n * k</span></p>
</td>
<td style="text-align: center;">
<p><span style="font-size: 16px;">Stable</span></p>
</td>
<td>
<p><span style="font-size: 16px;">&nbsp;Examine bits of keys.</span></p>
</td>
</tr>
</tbody>
</table>
### 特点
给定含 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 SortLeast Significant Digit Radix Sort或最高有效位基数排序MSD Radix SortMost 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<int>[] buckets = new List<int>[max - min + 1];
38
39 // initialize the buckets
40 for (int i = 0; i < buckets.Length; i++)
41 {
42 buckets[i] = new List<int>();
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 }
```

View File

@@ -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 }
```

View File

@@ -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 的匹配过程中,模板的一个特定位移 sq = 5 个字符已经匹配成功,但模式 P 的第 6 个字符不能与相应的文本字符匹配。
![](image/字符串匹配算法-KMP.jpg)
此时q 个字符已经匹配成功的信息确定了相应的文本字符,而知道这 q 个文本字符就使我们能够立即确定某些位移是非法的。例如上图a我们可以判断位移 s+1 是非法的,因为模式 P 的第一个字符 a 将与模式的第二个字符 b 匹配的文本字符进行匹配显然是不匹配的。而图b中则显示了位移 s = s+2 处,使模式 P 的前三个字符和相应的三个文本字符对齐后必定会匹配。KMP 算法的基本思路就是设法利用这些已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。
### 算法原理
> The basic idea behind KMPs 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] = 0a 仅一个字符,前缀和后缀为空集,共有元素最大长度为 0
* π[2] = 0ab 的前缀 a后缀 b不匹配共有元素最大长度为 0
* π[3] = 1aba前缀 a ab后缀 ba a共有元素最大长度为 1
* π[4] = 2abab前缀 a ab aba后缀 bab ab b共有元素最大长度为 2
* π[5] = 3ababa前缀 a ab aba abab后缀 baba aba ba a共有元素最大长度为 3
* π[6] = 4ababab前缀 a ab aba abab ababa后缀 babab abab bab ab b共有元素最大长度为 4
* π[7] = 5abababa前缀 a ab aba abab ababa ababab后缀 bababa ababa baba aba ba a共有元素最大长度为 5
* π[8] = 6abababab前缀 .. ababab ..,后缀 .. ababab ..,共有元素最大长度为 6
* π[9] = 0ababababc前缀和后缀不匹配共有元素最大长度为 0
* π[10] = 1ababababca前缀 .. 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-MATCHERKMP-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 }
```

View File

@@ -0,0 +1,2 @@
> 等到以后再处理

View File

@@ -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 分治法应用
### 排列问题
### 整数划分问题
### 二分搜索问题
### 大数乘法
### 矩阵乘法
### 快速排序
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
### 合并排序
### 线性时间选择
### 最近点对问题
### 棋盘覆盖问题

View File

@@ -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}
$$

View File

@@ -3,11 +3,13 @@
## 1 概述
### 基本思想
动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。
动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。
要素
### 对比分治法
动态规划中分解得到的子问题不是互相独立的。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。
动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。
### 条件
@@ -25,3 +27,27 @@
* 最有子结构
* 重叠子问题
* 备忘录方法(矩阵表格)
## 1 常见问题
### 矩阵连乘问题
### 凸多边形最优三角剖分
### 最长公共子序列
### 图像压缩问题
### 最大子段和问题
### 流水作业调度问题
### 投资问题
### 01背包问题
### 0n背包问题
### 最优二叉搜索树问题
### 序列匹配问题

View File

@@ -0,0 +1,48 @@
# 凸多边形最优三角剖分
## 凸多边形最优三角剖分-动态规划
### 问题描述
给定凸多边形P={v0v1vn-1}以及定义在由凸多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分使得该三角剖分所对应的权即三角剖分中诸三角形上权之和为最小。
### 问题分析
1. 若凸(n+1)边形P={v0,v1,…,vn}的最优三角剖分T包含三角形v0vkvn1≤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<j≤n$为凸子多边形{vi-1,vi,…,vj}的最优三角剖分所对应的权函数值,即其最优值。
为方便起见,设退化的多边形{vi-1,vi}具有权值0。据此定义要计算的凸(n+1)边形P的最优权值为t[1][n]。
t[i][j]的值可以利用最优子结构性质递归地计算。当j-i≥1时凸子多边形至少有3个顶点。
由最优子结构性质t[i][j]的值应为t[i][k]的值加上t[k+1][j]的值再加上三角形vi-1vkvj的权值其中i≤k≤j-1。
由于在计算时还不知道k的确切位置而k的所有可能位置只有j-i个因此可以在这j-i个位置中选出使t[i][j]值达到最小的位置。
$$
t[i][j]=min\{t[i][k]+t[k+1][j]+w(v[i-1]v[k]v[j])\}
$$
### 算法复杂度
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$
### 算法实现
```
void MinWeightTriangulation(int nint **tint **s) {
for (int i = 1; i <= n; i++) t[i][i] = 0;
for (int r = 2; r <= n; r++)
for (int i = 1; i <= n - r+1; i++) {
int j = i+r-1;
t[i][j] = t[i+1][j]+ w(i-1, i, j);
s[i][j] = i;
for (int k = i+1; k < j; k++) {
int u = t[i][k] + t[k+1][j] + w(i-1, k, j);
if (u < t[i][j]) { t[i][j] = u; s[i][j] = k;}
}
}
}
```

View File

@@ -13,8 +13,14 @@
### 贪心算法的适用条件
* 最优子结构性质
一个问题的最优解包含其子问题的最优解。问题具有最优子结构性质时,可以用动态规划算法或者贪心算法求解。
* 贪心选择性质
* 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
* 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。
* 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。
### 贪心选择性质
* 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到
* 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。
* 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。
### 贪心算法正确性
贪心算法不一定得到最优解。贪心策略的选择可能有多重,选择合适的贪心策略并进行正确性证明
方法
* 算法步数的归纳
* 问题规模的归纳

View File

@@ -1,25 +1,27 @@
# 回溯法
## 1 概述
## 0 概述
### 基本思想
回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法的基本思想:该方法系统地搜索一个问题的所有的解或任一解。
与剪枝相结合,也称为**回溯-剪枝法**。
### 要素
* 问题解的表示:
回溯法将问题的解表示成n元式。
* 显示约束:
* **解向量**-问题解的表示:
回溯法将问题的解表示成n元式$(x_1,x_2,\cdots,x_n)$
* **显示约束**
对分量xi的取值限定。
* 隐式约束:
* **隐式约束**
对满足问题的解而对不同分量之间施加约束
* 解空间:
* **解空间**
对于一个实例,解向量满足显示约束条件的所有多元组,构成了该实例的一个解空间。
### 基本方法:
明确定义问题的解空间,将问题的解空间组织成树的结构,通过采用系统的方法隐含搜索解空间树,从而得到问题解。回溯法的基本做法是搜索,是一种组织的井井有条的,能避免不必要搜索的穷举式搜索。搜索策略主要有:深度优先、广度优先、函数优先、广度深度结合。
点分支判定条件:
* 满足约束条件:分扩展解向量。
点分支判定条件:
* 满足约束条件:分扩展解向量。
* 不满足约束条件:回溯到当前节点的父结点。
@@ -31,14 +33,14 @@
存储当前索索路径
搜索策略(避免无效搜索)
* 约束函数:在扩展节点处减去不满足约束条件的子树。
* 界限函数:在扩展节点处减去得不到最优解的子树。
* **约束函数**:在扩展节点处减去不满足约束条件的子树。
* **界限函数**:在扩展节点处减去得不到最优解的子树。
### 回溯法的适用条件
适用于搜索问题和优化问题
必要条件
* 多米诺性质:叶子节点的解一定满足其父节点。叶子结点为真则父节点一定为真。同理父节点为假则叶子结点一定为假(逆否命题)。用父节点为假的情况进行剪枝操作。
* **多米诺性质**:叶子节点的解一定满足其父节点。叶子结点为真则父节点一定为真。同理父节点为假则叶子结点一定为假(逆否命题)。用父节点为假的情况进行剪枝操作。
设P(x1,x2,…,xi)是关于向量<x1,x2,…,xi>的某个性质那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即
P(x1,x2,…,xi+1) → P(x1,x2,…,xi) (0<i<n) (n为向量维数)
@@ -49,16 +51,29 @@ P(x1,x2,…,xi+1) → P(x1,x2,…,xi) (0<i<n) (n为向量维数)
* 问题解向量
* 解向量分量取值集合
* 构造解空间树
2. 判断问题是否满足多米诺性质
3. 搜索解空间树,确定剪枝函数
4. 确定存储搜索路径的数据结构
2. 判断问题是否满足多米诺性质
3. 搜索解空间树,确定**剪枝函数**
4. 确定存储搜索路径的数据结构
### 两种典型的解空间树
* 子集树当所给的问题是从n个元素的集合S中找出满足某种性质的子集时相应的解空间树称为子集树。
* 排列树当所给的问题是确定n个元素满足某种性质的排列时相应的解空间成为排列树。排列树有n个叶结点。
* **子集树**当所给的问题是从n个元素的集合S中找出满足某种性质的子集时相应的解空间树称为子集树。
* **排列树**当所给的问题是确定n个元素满足某种性质的排列时相应的解空间成为排列树。排列树有n个叶结点。
问题所有可行解分布在集合{<x1, x2, …, xn> | 1 ≤ xi ≤N, 1 ≤ i ≤N}(解空间)之中。可将问题解空间表示为一定的结构,通过对解空间的搜索,得到满足要求的问题解。
> 简单来说,解空间是子集树时不具有排列性质,没有位置关系。为排列树时需要考虑每个要素的顺序。
### 回溯法的时间复杂度
依赖以下条件
* 产生x[k]的时间;
* 满足显约束的x[k]值的个数;
* 计算约束函数constraint的时间
* 计算上界函数bound的时间
* 满足约束函数和上界函数约束的所有x[k]的个数。
是NPhard难题。需要遍历解空间树。
### 回溯法的程序结构
* 递归回溯

View File

@@ -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;j<k;j++)
if ((abs(k-j)==abs(x[j]-x[k]))||(x[j]==x[k]))
return false;
return true;
}
void Queen::Backtrack(int t) {
if (t>n) sum++;
else
for (int i=1;i<=n;i++) {
x[t]=i;
if (Place(t)) Backtrack(t+1);
}
}
```
## 2

View File

@@ -0,0 +1,74 @@
# 旅行商问题
## 1 旅行商问题-回溯法
### 问题描述
某售货员要到若干城市去推销商品,已知各城市间的路程耗费(代价),如何选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使得总路程耗费最小。
![](image/旅行商问题.png)
### 问题分析
* 解向量为<i1=1,i2,i3,…,in>其中i2,i3,…,in为{2,3,…,n}的一个排列。搜索空间为排列树
* 显约束:
* i=n时检查是否存在一条从顶点x[n-1]到x[n]的边和一条从顶点x[n]到顶点1的边若存在需要判断当前回路代价是否优于已找到的当前最优回路代价bestc若为真更新当前最优值bestc和最优解bestx。
* 当i<n时当前扩展结点位于排列树的第i-1层。若图中存在从顶点x[i-1]到顶点x[i]的边且x[1:i]的代价小于当前最优值bestc时进入排列树的第i层否则剪去相应子树。
### 算法时间复杂度
算法backtrack在最坏情况下可能需要更新当前最优解O((n-1)!)次每次更新bestx需计算时间O(n)从而整个算法的计算时间复杂性为O(n!)。
### 算法原理
![](image/旅行商问题-回溯法.png)
### 算法实现
```
template<class Type>
void Traveling<Type>::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)
### 算法实现

View File

@@ -0,0 +1,52 @@
# 01背包问题
## 1 01背包问题-回溯法
### 问题描述
将物品放到背包中。
* n件物品
* 每件物品的重量为w[i]
* 价值为v[i]
* m个背包
* 每个背包的容量为c[j]
求背包装载的最大价值。或者是否能装下所有。
具体问题n=3C=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<class Typew, class Typep>
Typep Knap<Typew, Typep>::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)
### 算法实现

View File

@@ -0,0 +1,13 @@
# 作业调度问题
## 1 作业调度问题-回溯法
### 问题描述
给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理然后由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。
批处理作业调度问题要求对于给定的n个作业制定最佳作业调度方案使其完成时间和达到最小。
### 问题分析
* 解空间:排列树问题。

View File

@@ -0,0 +1,12 @@
# 完全背包问题
## 1 完全背包问题
将物品放到背包中。
* 无限件物品
* 每件物品的重量为w[i]
* 价值为v[i]
* m个背包
* 每个背包的容量为c[j]
求背包装载的的最大价值。

View File

@@ -11,3 +11,22 @@
### 常见的两种分支界限法
* 队列式(FIFO)分支限界法按照队列先进先出FIFO原则选取下一个节点为扩展节点。
* 优先队列式分支限界法:按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。
### 使用条件
* 问题的多米诺性质
* 求解最优解或一个可行解
### 设计要素
1. 针对问题定义解空间
* 问题解向量
* 解向量分量取值集合
* 构造解空间树
2. 判断是否满足多米诺性质
3. 确定**剪枝函数**
4. 确定存储搜索路径的数据结构
5. 分支限界发的核心思想在于**界的设计**
### 分支限界法的程序结构
迭代方法

View File

@@ -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把。我试的次数越多打开最优解的机会就越大但在打开之前那些错的钥匙都是没有用的。这个试钥匙的算法就是拉斯维加斯的——尽量找最好的但不保证能找到。
### 结论
* 蒙特卡罗算法:采样越多,越近似最优解;
* 拉斯维加斯算法:采样越多,越有机会找到最优解;​
这两类随机算法之间的选择,往往受到问题的局限。如果问题要求在有限采样内,必须给出一个解,但不要求是最优解,那就要用蒙特卡罗算法。反之,如果问题要求必须给出最优解,但对采样没有限制,那就要用拉斯维加斯算法。​

View File

@@ -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问题了。

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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中只有起点sU中是除s之外的顶点并且U中顶点的路径是"起点s到该顶点的路径"。然后从U中找出路径最短的顶点并将其加入到S中接着更新U中的顶点和顶点对应的路径。 然后再从U中找出路径最短的顶点并将其加入到S中接着更新U中的顶点和顶点对应的路径。 ... 重复该操作,直到遍历完所有顶点。
## 3 算法步骤
### 基本步骤
1. 初始时S只包含起点sU包含除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<iostream>
#include<sstream>
using namespace std;
const int Max=100;
string Int_to_String(int n)//int转换string
{
ostringstream stream;
stream<<n; //n为int类型
return stream.str();
}
class MGraph{
public:
MGraph(){
}
MGraph(int n,int e);
~MGraph(){
}
public:
int vertex[Max];
int arc[Max][Max];
int vertexNum,arcNum;
};
MGraph::MGraph(int n,int e){
int i,j;
vertexNum=n;
arcNum=e;
for(int i=0;i<vertexNum;i++)
vertex[i]=i;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
arc[i][j]=10000;
cout<<"请输入图中各边的情况:"<<endl;
for(int k=0;k<e;k++){
cin>>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<G.vertexNum;i++)
{
dist[i]=G.arc[v][i];
if(dist[i]!=10000)
path[i]=Int_to_String(G.vertex[v])+"->"+Int_to_String(G.vertex[i]);
else
path[i]="";
}
s[0]=v;
dist[v]=0;
int num=1;
for(int i=0;i<G.vertexNum;i++)
{
int t=10000,k;
for(int j=0;j<G.vertexNum;j++)
{
if(dist[j]<t&&dist[j]!=0)
{
t=dist[j];
k=j;
}
}
cout<<"终点为:"<<k<<" 最短路径长度为:"<<dist[k]<<" 过程:"<<path[k]<<endl;
s[num++]=k;
for(int j=0;j<G.vertexNum;j++){
if(dist[j]!=0&&dist[j]>(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<<"找到终点的顺序为:"<<endl;
for(int i=1;i<num;i++)
cout<<s[i]<<" ";
cout<<endl;
}
int main(){
int n,e,v;
cout<<"输入起点下标:"<<endl;
cin>>v;
cout<<"输入图的顶点数和边数:"<<endl;
cin>>n>>e;
MGraph G(n,e);
Dijkstra(G,v);
return 0;
}
```

View File

@@ -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=123...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)如果值为qi到p的最短路径为i->...->q->p再去找p(iq)如果值为ri到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 , … Dn1,
2. 初始化: D-1C
D-1[i][j]=边<i,j>的长度表示初始的从i到j的最短路径长度即它是从i到j的中间不经过其他中间点的最短路径。
3. 迭代设Dk-1已求出如何得到Dk0≤k≤n-1
* Dk-1[i][j]表示从i到j的中间点不大于k-1的最短路径pi…j
* 考虑将顶点k加入路径p得到顶点序列qi…k…j
* 若q不是路径则当前的最短路径仍是上一步结果Dk[i][j]= Dk1[i][j]
* 否则若q的长度小于p的长度则用q取代p作为从i到j的最短路径
4. 因为q的两条子路径i…k和k…j皆是中间点不大于k1的最短路径所以从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<iostream>
#include<string.h>
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<<mVexs[i].data;
ENode *temp = mVexs[i].firstEdge;
while(temp){
cout<<" -> "<<temp->vex;
temp = temp->nextEdge;
}
cout<<endl;
}
cout<<endl;
}
// 得到两个节点之间的权重
int getWeight(int m, int n){
ENode *enode = mVexs[m].firstEdge;
while(enode){
if(enode->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<<mVexs[i].data<<" -> "<<mVexs[j].data<<": "<<dist[i][j]<<" , 路径为: ";
}
else{
cout<<mVexs[i].data<<" -> "<<mVexs[j].data<<": "<<dist[i][j]<<" , 路径为: ";
}
getPath(i, j, path);
cout<<endl;
}
cout<<endl;
}
// 输出路径矩阵观察, 可用此矩阵自己用笔演算一下路径查找过程
// for(i = 0; i < n; i++){
// for(j = 0; j < n; j++){
// cout<<path[i][j]<<" ";
// }
// cout<<endl;
// }
}
// 递归实现得到节点之间最短路径
void getPath(int start, int end, int path[][7]){
if(path[start][end] == start){
cout<<mVexs[start].data<<" "<<mVexs[end].data<<" ";
}
else{
getPath(start, path[start][end], path);
cout<<mVexs[end].data<<" ";
}
}
};
int main(){
Graph g;
// 输出邻接表
// g.print();
// 弗洛伊德算法
g.floyd();
return 0;
}
```

View File

@@ -0,0 +1,11 @@
# 图算法-Dijkstra算法
# 图算法-Floyd算法
# 图算法-Bellman-Ford算法
# 图算法-Prim算法
# 图算法-Kruskal算法
> 目录
>* 图算法-Dijkstra算法
>* 图算法-Floyd算法
>* 图算法-Bellman-Ford算法
>* 图算法-Prim算法
>* 图算法-Kruskal算法

View File

@@ -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. 新建图GraphnewGraphnew中拥有原图中相同的e个顶点但没有边
3. 将原图Graph中所有e个边按权值从小到大排序
4. 循环从权值最小的边开始遍历每条边。if这条边连接的两个节点于图Graphnew中不在同一个连通分量中添加这条边到图Graphnew中。直至图Graph中所有的节点都在同一个连通分量中。
## 3 算法流程
1. 首先第一步我们有一张图Graph有若干点和边
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015215729.jpg" alt="" width="200" height="168"></p>
2. 将所有的边的长度排序用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序对局部最优的资源进行选择排序完成后我们率先选择了边AD。
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015234045.jpg" alt="" width="200" height="168"></p>
3. 在剩下的变中寻找。我们找到了CE。这里边的权重也是5
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015313195.jpg" alt="" width="200" height="168"></p>
4. 依次类推我们找到了6,7,7即DFABBE。
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015332154.jpg" alt="" width="200" height="168"></p>
5. 下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了对于BC可以通过CE,EB来连接类似的EF可以通过EB,BA,AD,DF来接连。所以不需要选择他们。类似的BD也已经连通了这里上图的连通线用红色表示了。最后就剩下EG和FG了。当然我们选择了EG。
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015361536.jpg" alt="" width="200" height="168"></p>
## 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<G.n;i++)
{
for (j=0;j<G.n;j++)
{
if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)
{
E[k].u=i;
E[k].v=j;
E[k].w=G.edges[i][j];
k++;
}
}
}
heapsort(E,k,sizeof(E[0])); //堆排序,按权值从小到大排列
for (i=0;i<G.n;i++) //初始化辅助数组
{
vset[i]=i;
}
k=1; //生成的边数,最后要刚好为总边数
j=0; //E中的下标
while (k<G.n)
{
sn1=vset[E[j].u];
sn2=vset[E[j].v]; //得到两顶点属于的集合编号
if (sn1!=sn2) //不在同一集合编号内的话,把边加入最小生成树
{
printf("%d ---> %d, %d",E[j].u,E[j].v,E[j].w);
k++;
for (i=0;i<G.n;i++)
{
if (vset[i]==sn2)
{
vset[i]=sn1;
}
}
}
j++;
}
}
```

View File

@@ -0,0 +1,227 @@
# 图算法-Prim算法
> 目录
>* 图算法-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, v>其中u为集合Vnew中的元素而v不在Vnew集合当中并且v∈V如果存在有多条满足前述条件即具有相同权值的边则可任意选取其中之一
* 将v加入集合Vnew中将<u, v>边加入集合Enew中
4. 输出使用集合Vnew和Enew来描述所得到的最小生成树。
## 3 算法过程
<table class="wikitable" border="1" cellspacing="2" cellpadding="5">
<tbody>
<tr><th>图例</th><th>说明</th><th>不可选</th><th>可选</th><th>已选V<sub>new</sub></th></tr>
<tr>
<td>&nbsp;
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015154494.png" alt="" width="200" height="168"></p>
</td>
<td>此为原始的加权连通图。每条边一侧的数字代表其权值。</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073015175038.png" alt="" width="200" height="168"></p>
</td>
<td>顶点<strong>D</strong>被任意选为起始点。顶点<strong>A</strong>、<strong>B</strong>、<strong>E</strong>和<strong>F</strong>通过单条边与<strong>D</strong>相连。<strong>A</strong>是距离<strong>D</strong>最近的顶点,因此将<strong>A</strong>及对应边<strong>AD</strong>以高亮表示。</td>
<td>C, G</td>
<td>A, B, E, F</td>
<td>D</td>
</tr>
<tr>
<td>&nbsp;
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016090032.png" alt="" width="200" height="168"></p>
</td>
<td>下一个顶点为距离<strong>D</strong>或<strong>A</strong>最近的顶点。<strong>B</strong>距<strong>D</strong>为9距<strong>A</strong>为7<strong>E</strong>为15<strong>F</strong>为6。因此<strong>F</strong>距<strong>D</strong>或<strong>A</strong>最近,因此将顶点<strong>F</strong>与相应边<strong>DF</strong>以高亮表示。</td>
<td>C, G</td>
<td>B, E, F</td>
<td>A, D</td>
</tr>
<tr>
<td><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016130394.png" alt="" width="200" height="168"></td>
<td>算法继续重复上面的步骤。距离<strong>A</strong>为7的顶点<strong>B</strong>被高亮表示。</td>
<td>C</td>
<td>B, E, G</td>
<td>A, D, F</td>
</tr>
<tr>
<td>&nbsp;
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016143177.png" alt="" width="200" height="168"></p>
</td>
<td>在当前情况下,可以在<strong>C</strong>、<strong>E</strong>与<strong>G</strong>间进行选择。<strong>C</strong>距<strong>B</strong>为8<strong>E</strong>距<strong>B</strong>为7<strong>G</strong>距<strong>F</strong>为11。<strong>E</strong>最近,因此将顶点<strong>E</strong>与相应边<strong>BE</strong>高亮表示。</td>
<td>无</td>
<td>C, E, G</td>
<td>A, D, F, B</td>
</tr>
<tr>
<td>&nbsp;
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016154616.png" alt="" width="200" height="168"></p>
</td>
<td>这里,可供选择的顶点只有<strong>C</strong>和<strong>G</strong>。<strong>C</strong>距<strong>E</strong>为5<strong>G</strong>距<strong>E</strong>为9故选取<strong>C</strong>,并与边<strong>EC</strong>一同高亮表示。</td>
<td>无</td>
<td>C, G</td>
<td>A, D, F, B, E</td>
</tr>
<tr>
<td>
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016114494.png" alt="" width="200" height="168"></p>
</td>
<td>顶点<strong>G</strong>是唯一剩下的顶点,它距<strong>F</strong>为11距<strong>E</strong>为9<strong>E</strong>最近,故高亮表示<strong>G</strong>及相应边<strong>EG</strong>。</td>
<td>无</td>
<td>G</td>
<td>A, D, F, B, E, C</td>
</tr>
<tr>
<td>
<p><img src="https://pic002.cnblogs.com/images/2012/426620/2012073016100874.png" alt="" width="200" height="168"></p>
</td>
<td>现在所有顶点均已被选取图中绿色部分即为连通图的最小生成树。在此例中最小生成树的权值之和为39。</td>
<td>无</td>
<td>无</td>
<td>A, D, F, B, E, C, G</td>
</tr>
</tbody>
</table>
## 4 算法效率
顶点数V边数E。时间复杂度
* 邻接矩阵:$O(V^2)$
* 邻接表:$O(E\log_2V)$
## 5 算法实现
```
#include <stdio.h>
#include <stdlib.h>
#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 (; i<G.vexnum; i++) {
if (G.vexs[i]==v) {
return i;
}
}
return -1;
}
//构造无向网
void CreateUDN(MGraph* G){
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
for (int i=0; i<G->vexnum; i++) {
scanf("%d",&(G->vexs[i]));
}
for (int i=0; i<G->vexnum; i++) {
for (int j=0; j<G->vexnum; j++) {
G->arcs[i][j].adj=INFINITY;
G->arcs[i][j].info=NULL;
}
}
for (int i=0; i<G->arcnum; 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; i<G.vexnum; i++) {
//权值为0说明顶点已经归入最小生成树中然后每次和min变量进行比较最后找出最小的。
if (close[i].lowcost>0 && 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);
//首先将与该起始点相关的所有边的信息边的起始点和权值存入辅助数组中相应的位置例如12adjvex为0lowcost为6存入theclose[1]中辅助数组的下标表示该边的顶点2
for (int i=0; i<G.vexnum; i++) {
if (i !=k) {
theclose[i].adjvex=k;
theclose[i].lowcost=G.arcs[k][i].adj;
}
}
//由于起始点已经归为最小生成树所以辅助数组对应位置的权值为0这样遍历时就不会被选中
theclose[k].lowcost=0;
//选择下一个点,并更新辅助数组中的信息
for (int i=1; i<G.vexnum; i++) {
//找出权值最小的边所在数组下标
k=minimun(G, theclose);
//输出选择的路径
printf("v%d v%d\n",G.vexs[theclose[k].adjvex],G.vexs[k]);
//归入最小生成树的顶点的辅助数组中的权值设为0
theclose[k].lowcost=0;
//信息辅助数组中存储的信息,由于此时树中新加入了一个顶点,需要判断,由此顶点出发,到达其它各顶点的权值是否比之前记录的权值还要小,如果还小,则更新
for (int j=0; j<G.vexnum; j++) {
if (G.arcs[k][j].adj<theclose[j].lowcost) {
theclose[j].adjvex=k;
theclose[j].lowcost=G.arcs[k][j].adj;
}
}
}
printf("\n");
}
int main(){
MGraph G;
CreateUDN(&G);
miniSpanTreePrim(G, 1);
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,32 @@
PPT1
目录
算法效率
分类搜索匹配
分治
动态规划
贪心算法
回溯法
分支限界
NP问题
自适应算法
随机化算法
启发式算法
算法定义:对于每一个合法输入,算法都会在有限的时间内输出一个满足要求的结果
O(1) constant
O(log n) logarithmic
O(n) linear
O(n log n) n log n
O(n2) quadratic
O(n3) cubic
O(2n) exponential
O(n!) factorial
PPT2
PRIM 算法。在网络中寻找最短通信网络。即最小生成树。
PPT3
KRUSKAL算法。在网络上中寻找最短通信网络。即最小生成树。
PPT4
分治法。