算法
@@ -23,6 +23,12 @@
|
||||
|
||||

|
||||
|
||||
1. 理解问题
|
||||
2. 选择策略
|
||||
3. 算法设计
|
||||
4. 正确性证明
|
||||
5. 算法分析
|
||||
6. 程序设计
|
||||
|
||||
## 3 算法分类
|
||||
|
||||
|
||||
@@ -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 递归算法的复杂性分析
|
||||
|
||||
* 线性收缩递归
|
||||

|
||||
|
||||
* 等比收缩递归
|
||||

|
||||

|
||||
|
||||
|
||||
103
Algorithm/A类:基本算法/3 蛮力法(枚举法).md
Normal 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
|
||||
* 构建基本操作的求和表达式:
|
||||
利用求和公式分析算法的时间复杂度:
|
||||

|
||||
|
||||
### 程序设计
|
||||
|
||||
## 3 顺序查找问题
|
||||
(主要是分析解决问题的步骤)
|
||||
### 理解问题
|
||||
思想:查找键与表中元素从头至尾逐个比较。
|
||||
结果:找到 或 失败
|
||||
限位器:把查找键添加到列表末尾—— 一定成功,避免每次循环时对检查是否越界(边界检查)
|
||||
选择策略
|
||||
|
||||
### 算法设计
|
||||
|
||||

|
||||
|
||||
|
||||
### 正确性证明
|
||||
|
||||
### 算法分析
|
||||
* 最佳效率: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<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)个点的列表P,P1=(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)
|
||||
### 程序设计
|
||||
428
Algorithm/A类:基本算法/3.1 查找算法.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# 查找算法
|
||||
> 阅读目录
|
||||
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<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. 任意节点的左、右子树也分别为二叉查找树。
|
||||
|
||||
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
|
||||
|
||||
不同形态的二叉查找树如下图所示:
|
||||

|
||||
|
||||
### 复杂度分析
|
||||
|
||||
它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。
|
||||
|
||||
原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
|
||||
|
||||
下图为二叉树查找和顺序查找以及二分查找性能的对比图:
|
||||

|
||||
|
||||
## 5.2 平衡查找树之2-3查找树(2-3 Tree)
|
||||
|
||||
### 基本思想
|
||||
2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个子节点。对应3节点(3-node),保存两个Key,2-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还要大。
|
||||
|
||||

|
||||
|
||||
2-3查找树的性质:
|
||||
1. 如果中序遍历2-3查找树,就可以得到排好序的序列;
|
||||
2. 在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中“平衡”一词的概念,根节点到叶节点的最长距离对应于查找算法的最坏情况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。)
|
||||

|
||||
|
||||
### 复杂度分析:
|
||||
2-3树的查找效率与树的高度是息息相关的。
|
||||
* 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
|
||||
* 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN
|
||||
|
||||
距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。
|
||||
|
||||
对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:
|
||||

|
||||
|
||||
## 5.3 平衡查找树之红黑树(Red-Black Tree)
|
||||
2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。
|
||||
|
||||
### 基本思想
|
||||
红黑树首先是一种树形结构,同时又是一个二叉树(每个节点最多只能有两个孩子节点,左节点小于等于父节点,右节点大于父节点),为了保证树的左右孩子树相对平衡(深度相同),红黑树使用了节点标色的方式,将节点标记为红色或者黑色,在计算树的深度时只统计黑色节点的数量,不统计红色节点数量。
|
||||
|
||||
为了保证左右子树的平衡,红黑树定义了一些规则或者特点来维持平衡。
|
||||
|
||||
主要特点(规则)
|
||||
* 每个节点要么是黑色,要么是红色。(节点非黑即红)
|
||||
* 根节点是黑色。
|
||||
* 每个叶子节点(NULL)是黑色(为了简单期间,一般会省略该节点)。
|
||||
* 如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
|
||||
* 从一个节点到该节点的每一个叶子子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
|
||||
* 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行操作。
|
||||
|
||||

|
||||
|
||||
|
||||
红黑树平衡方法
|
||||
前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
|
||||
|
||||
* 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
|
||||
* 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
|
||||
* 变色:结点的颜色由红变黑或由黑变红。
|
||||
|
||||
### 复杂度分析
|
||||
最坏的情况就是,红黑相间的路径长度是全黑路径长度的2倍。
|
||||
红黑树的平均高度大约为2logn。
|
||||
下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
|
||||

|
||||
|
||||
## 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树:
|
||||

|
||||
|
||||
|
||||
B+树定义:
|
||||
|
||||
B+树是对B树的一种变形树,它与B树的差异在于:
|
||||
* 有k个子结点的结点必然有k个关键码;
|
||||
* 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
|
||||
* 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
|
||||
|
||||
如下图,是一个B+树:
|
||||

|
||||
|
||||
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算法和其他查找算法的性能对比:
|
||||

|
||||
173
Algorithm/A类:基本算法/3.2 搜索算法-广度优先搜索.md
Normal file
@@ -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 的其他顶点。
|
||||
|
||||

|
||||
|
||||
### 例子
|
||||
例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。
|
||||
|
||||

|
||||
|
||||
上图的 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 }
|
||||
```
|
||||
200
Algorithm/A类:基本算法/3.3 搜索算法-深度优先搜索.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 深度优先搜索
|
||||
## 1 概述
|
||||
|
||||
### 特点
|
||||
深度优先搜索(DFS:Depth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作:
|
||||
(a) 访问图中的一个节点;
|
||||
(b) 访问该节点的子节点;
|
||||
|
||||
### 过程
|
||||
在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续探测下去。当顶点 v 的所有边都已被探寻过后,搜索将回溯到发现顶点 v 有起始点的那些边。这一过程一直进行到已发现从源顶点可达的所有顶点为止。实际上深度优先搜索最初的探究也是为了解决迷宫问题。
|
||||
|
||||
对图的深度优先搜索与对树(Tree)的深度优先遍历(Depth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。
|
||||
|
||||
### 例子
|
||||
|
||||
例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,子顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 DFS 的算法实现中需要对顶点是否访问过做标记。
|
||||

|
||||
上图的 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 }
|
||||
```
|
||||
1768
Algorithm/A类:基本算法/3.4 排序算法-简单排序.md
Normal file
406
Algorithm/A类:基本算法/3.5 排序算法-线性排序.md
Normal 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> <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;"> 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> <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;"> 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;"> 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> 桶排序</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;"> 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 直接放到它在最终输出数组中的位置上。
|
||||

|
||||
|
||||
例如:有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
|
||||
|
||||
### 算法描述
|
||||
算法的步骤如下:
|
||||
1. 找出待排序的数组中最大和最小的元素;
|
||||
2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
|
||||
3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
|
||||
4. 反向填充目标数组,将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1;
|
||||

|
||||
|
||||
### 算法复杂度
|
||||
* 最差时间复杂度 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)算法,因为它们在元素较少时拥有更好的效率。
|
||||

|
||||
|
||||
|
||||
基数排序中可以选择采用最低有效位基数排序(LSD Radix Sort:Least Significant Digit Radix Sort)或最高有效位基数排序(MSD Radix Sort:Most Significant Digit Radix Sort)。LSD 的排序方式由值的最低位也就是最右边开始,而 MSD 则相反,由值的最高位也就是最左边开始。
|
||||
|
||||

|
||||
|
||||
例如,如下这个无序的数列需要排序:
|
||||
```
|
||||
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. 从不是空的桶里把数值再放回原来的数组中。
|
||||
|
||||

|
||||
|
||||
### 算法复杂度
|
||||
* 最差时间复杂度 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 }
|
||||
```
|
||||
222
Algorithm/A类:基本算法/3.6 线性时间选择算法.md
Normal 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 }
|
||||
```
|
||||
602
Algorithm/A类:基本算法/3.7 字符串算法-匹配算法.md
Normal 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)。
|
||||
|
||||

|
||||
|
||||
比如上图中,目标是找出所有在文本 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)。所以算法的总运行时间为预处理和匹配的时间的总和。
|
||||
|
||||

|
||||
上图描述了常见字符串匹配算法的预处理和匹配时间。
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||

|
||||
如上图中,对于模式 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 个字符不能与相应的文本字符匹配。
|
||||

|
||||
|
||||
此时,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 }
|
||||
```
|
||||
2
Algorithm/A类:基本算法/3.8 字符串算法-其他算法.md
Normal file
@@ -0,0 +1,2 @@
|
||||
> 等到以后再处理
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# 分治法
|
||||
|
||||
## 1 概述
|
||||
## 0 分治法概述
|
||||
### 基本思想
|
||||
|
||||
将一个难以直接解决的大问题,分解为规模较小的相同子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。
|
||||
* 求解问题算法的复杂性一般都与问题规模相关,问题规模越小越容易处理。
|
||||
* 分治法的基本思想是,将一个难以直接解决的大问题,分解为规模较小的相同子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。
|
||||
* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。递归是分治法中最常用的技术。
|
||||
|
||||

|
||||
|
||||
|
||||
### 分治法解决问题的先决条件
|
||||
* 该问题的规模缩小到一定的程度就可以容易地解决;
|
||||
@@ -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)
|
||||
$$
|
||||
* 关系式说明
|
||||
|
||||

|
||||
### 等比收缩递归算法
|
||||
|
||||
* 递推关系式
|
||||
$$
|
||||
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)
|
||||
$$
|
||||
* 关系式说明
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## 1 分治法应用
|
||||
|
||||
### 排列问题
|
||||
### 整数划分问题
|
||||
|
||||
### 二分搜索问题
|
||||
|
||||
### 大数乘法
|
||||
|
||||
### 矩阵乘法
|
||||
|
||||
### 快速排序
|
||||
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
|
||||
|
||||
### 合并排序
|
||||
|
||||
### 线性时间选择
|
||||
|
||||
### 最近点对问题
|
||||
|
||||
### 棋盘覆盖问题
|
||||
44
Algorithm/A类:基本算法/4.1 排列问题.md
Normal 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}
|
||||
$$
|
||||
@@ -3,11 +3,13 @@
|
||||
## 1 概述
|
||||
### 基本思想
|
||||
动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。
|
||||
|
||||
动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。
|
||||
要素
|
||||
|
||||
### 对比分治法
|
||||
动态规划中分解得到的子问题不是互相独立的。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。
|
||||
|
||||
动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。
|
||||
|
||||
### 条件
|
||||
@@ -25,3 +27,27 @@
|
||||
* 最有子结构
|
||||
* 重叠子问题
|
||||
* 备忘录方法(矩阵表格)
|
||||
|
||||
## 1 常见问题
|
||||
|
||||
### 矩阵连乘问题
|
||||
|
||||
### 凸多边形最优三角剖分
|
||||
|
||||
### 最长公共子序列
|
||||
|
||||
### 图像压缩问题
|
||||
|
||||
### 最大子段和问题
|
||||
|
||||
### 流水作业调度问题
|
||||
|
||||
### 投资问题
|
||||
|
||||
### 01背包问题
|
||||
|
||||
### 0n背包问题
|
||||
|
||||
### 最优二叉搜索树问题
|
||||
|
||||
### 序列匹配问题
|
||||
48
Algorithm/A类:基本算法/5.2 凸n边形最优三角剖分.md
Normal file
@@ -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<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 n,int **t,int **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;}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
0
Algorithm/A类:基本算法/5.3 最长公共子序列.md
Normal file
@@ -13,8 +13,14 @@
|
||||
### 贪心算法的适用条件
|
||||
* 最优子结构性质
|
||||
一个问题的最优解包含其子问题的最优解。问题具有最优子结构性质时,可以用动态规划算法或者贪心算法求解。
|
||||
* 贪心选择性质
|
||||
* 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
|
||||
* 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。
|
||||
* 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。
|
||||
|
||||
### 贪心选择性质
|
||||
* 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
|
||||
* 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。
|
||||
* 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。
|
||||
### 贪心算法正确性
|
||||
贪心算法不一定得到最优解。贪心策略的选择可能有多重,选择合适的贪心策略并进行正确性证明。
|
||||
|
||||
方法
|
||||
* 算法步数的归纳
|
||||
* 问题规模的归纳
|
||||
@@ -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难题。需要遍历解空间树。
|
||||
|
||||
### 回溯法的程序结构
|
||||
|
||||
* 递归回溯
|
||||
|
||||
42
Algorithm/A类:基本算法/7.1 N皇后问题.md
Normal 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
|
||||
74
Algorithm/A类:基本算法/7.2 旅行商问题.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 旅行商问题
|
||||
|
||||
## 1 旅行商问题-回溯法
|
||||
|
||||
### 问题描述
|
||||
某售货员要到若干城市去推销商品,已知各城市间的路程耗费(代价),如何选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使得总路程耗费最小。
|
||||

|
||||
|
||||
### 问题分析
|
||||
|
||||
* 解向量为<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!)。
|
||||
|
||||
### 算法原理
|
||||
|
||||

|
||||
|
||||
|
||||
### 算法实现
|
||||
|
||||
```
|
||||
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;
|
||||
|
||||
### 算法原理
|
||||

|
||||
### 算法实现
|
||||
52
Algorithm/A类:基本算法/7.3 01背包问题.md
Normal file
@@ -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$
|
||||
|
||||
### 算法原理
|
||||

|
||||
### 算法实现
|
||||
```
|
||||
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}
|
||||
|
||||
### 问题分析
|
||||
|
||||
### 算法原理
|
||||

|
||||
### 算法实现
|
||||
13
Algorithm/A类:基本算法/7.4 作业调度问题.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 作业调度问题
|
||||
|
||||
## 1 作业调度问题-回溯法
|
||||
|
||||
### 问题描述
|
||||
|
||||
给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理,然后由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。
|
||||
|
||||
批处理作业调度问题要求对于给定的n个作业,制定最佳作业调度方案,使其完成时间和达到最小。
|
||||
|
||||
### 问题分析
|
||||
|
||||
* 解空间:排列树问题。
|
||||
12
Algorithm/A类:基本算法/7.5 完全背包问题.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 完全背包问题
|
||||
|
||||
## 1 完全背包问题
|
||||
|
||||
将物品放到背包中。
|
||||
* 无限件物品
|
||||
* 每件物品的重量为w[i]
|
||||
* 价值为v[i]
|
||||
* m个背包
|
||||
* 每个背包的容量为c[j]
|
||||
|
||||
求背包装载的的最大价值。
|
||||
@@ -11,3 +11,22 @@
|
||||
### 常见的两种分支界限法
|
||||
* 队列式(FIFO)分支限界法:按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。
|
||||
* 优先队列式分支限界法:按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。
|
||||
|
||||
### 使用条件
|
||||
|
||||
* 问题的多米诺性质
|
||||
|
||||
* 求解最优解或一个可行解
|
||||
|
||||
### 设计要素
|
||||
1. 针对问题定义解空间
|
||||
* 问题解向量
|
||||
* 解向量分量取值集合
|
||||
* 构造解空间树
|
||||
2. 判断是否满足多米诺性质
|
||||
3. 确定**剪枝函数**
|
||||
4. 确定存储搜索路径的数据结构
|
||||
5. 分支限界发的核心思想在于**界的设计**
|
||||
|
||||
### 分支限界法的程序结构
|
||||
迭代方法
|
||||
@@ -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把。我试的次数越多,打开(最优解)的机会就越大,但在打开之前,那些错的钥匙都是没有用的。这个试钥匙的算法,就是拉斯维加斯的——尽量找最好的,但不保证能找到。
|
||||
|
||||
### 结论
|
||||
|
||||
* 蒙特卡罗算法:采样越多,越近似最优解;
|
||||
|
||||
* 拉斯维加斯算法:采样越多,越有机会找到最优解;
|
||||
|
||||
这两类随机算法之间的选择,往往受到问题的局限。如果问题要求在有限采样内,必须给出一个解,但不要求是最优解,那就要用蒙特卡罗算法。反之,如果问题要求必须给出最优解,但对采样没有限制,那就要用拉斯维加斯算法。
|
||||
|
||||
|
||||
11
Algorithm/A类:基本算法/NP问题.md
Normal 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问题了。
|
||||
BIN
Algorithm/A类:基本算法/image/01背包问题-分支限界.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
Algorithm/A类:基本算法/image/01背包问题-回溯法.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
Algorithm/A类:基本算法/image/分治法原理.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Algorithm/A类:基本算法/image/字符串匹配算法-KMP.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
Algorithm/A类:基本算法/image/字符串匹配算法-算法原理.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Algorithm/A类:基本算法/image/字符串匹配算法.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Algorithm/A类:基本算法/image/字符串匹配算法效率.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
Algorithm/A类:基本算法/image/广度优先搜索-例子.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
Algorithm/A类:基本算法/image/广度优先搜索-层次.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-冒泡排序.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-合并排序.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-基数排序.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-基数排序演示.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-基数排序过程.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-堆排序.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-奇偶排序.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-希尔排序.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-快速排序.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-插入排序.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-桶排序.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-稳定性.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-算法效率比较.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-计数排序.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-选择排序.gif
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法-鸡尾酒排序.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
Algorithm/A类:基本算法/image/排序算法.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
Algorithm/A类:基本算法/image/搜索算法-B树.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Algorithm/A类:基本算法/image/搜索算法-红黑树.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Algorithm/A类:基本算法/image/旅行商问题-分支限界.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
Algorithm/A类:基本算法/image/旅行商问题-回溯法.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
Algorithm/A类:基本算法/image/旅行商问题.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-2-3树.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-2-3树性质.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-2-3树效率.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-B+树.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-二叉搜索树.jpeg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-二叉树与二分法.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-哈希搜索效率.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-红黑树.png
Normal file
|
After Width: | Height: | Size: 582 KiB |
BIN
Algorithm/A类:基本算法/image/查找算法-红黑树效率.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
Algorithm/A类:基本算法/image/深度优先搜索-例子.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
Algorithm/A类:基本算法/image/蛮力法-顺序查找.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
Algorithm/A类:基本算法/image/递归算法-时间复杂度.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Algorithm/A类:基本算法/image/递归算法-等比收缩说明.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Algorithm/A类:基本算法/image/递归算法-线性收缩说明.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
0
Algorithm/B类:数据结构算法/1 图算法.md
Normal file
168
Algorithm/B类:数据结构算法/1.1 图算法-Dijkstra算法 copy.md
Normal 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中只有起点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),直到遍历完所有顶点。
|
||||
|
||||
### 图解过程
|
||||

|
||||
|
||||
### 详细说明
|
||||
|
||||
初始状态: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;
|
||||
}
|
||||
```
|
||||
281
Algorithm/B类:数据结构算法/1.2 图算法-Floyd算法.md
Normal 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=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 算法过程
|
||||
|
||||

|
||||
|
||||
1. 定义n×n的方阵序列D-1, D0 , … Dn-1,
|
||||
|
||||
2. 初始化: D-1=C
|
||||
D-1[i][j]=边<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<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;
|
||||
}
|
||||
```
|
||||
11
Algorithm/B类:数据结构算法/1.3 图算法-Bellman-Ford算法.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 图算法-Dijkstra算法
|
||||
# 图算法-Floyd算法
|
||||
# 图算法-Bellman-Ford算法
|
||||
# 图算法-Prim算法
|
||||
# 图算法-Kruskal算法
|
||||
> 目录
|
||||
>* 图算法-Dijkstra算法
|
||||
>* 图算法-Floyd算法
|
||||
>* 图算法-Bellman-Ford算法
|
||||
>* 图算法-Prim算法
|
||||
>* 图算法-Kruskal算法
|
||||
113
Algorithm/B类:数据结构算法/1.4 图算法-Kruskal算法.md
Normal 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. 新建图Graphnew,Graphnew中拥有原图中相同的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,即DF,AB,BE。
|
||||
<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++;
|
||||
}
|
||||
}
|
||||
```
|
||||
227
Algorithm/B类:数据结构算法/1.5 图算法-Prim算法.md
Normal 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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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);
|
||||
//首先将与该起始点相关的所有边的信息:边的起始点和权值,存入辅助数组中相应的位置,例如(1,2)边,adjvex为0,lowcost为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);
|
||||
}
|
||||
```
|
||||
BIN
Algorithm/B类:数据结构算法/image/Dijkstra算法.jpg
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
Algorithm/B类:数据结构算法/image/Floyd算法.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
32
Algorithm/考前复习知识点.md
Normal 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
|
||||
分治法。
|
||||