算法设计与分析基础

绪论

什么是算法

一系列解决问题的明确指令,对于符合一定规范的输入,能够在有限的时间内获得要求的输出。

例子:最大公约数:俩个不全为0 的非负整数 m m m和 n n n的最大公约数记为 g c d ( m , n ) gcd(m,n) gcd(m,n)​,代表能够整除(即余数为0) m m m 和 n n n的最大正整数。

欧几里得算法

g c d ( m , n ) = g c d ( n , m m o d n ) ( m m o d n 表 示 m 除 以 n 之 后 的 余 数 ) gcd(m,n)=gcd(n, m \ mod \ n)(m \ mod \ n 表示m除以n 之后的余数) gcd(m,n)=gcd(n,m mod n)(m mod n表示m除以n之后的余数)​​​

u n t i l m m o d n = 0 until\ m \ mod\ n = 0 until m mod n=0​​ 其中 g c d ( m , 0 ) = m gcd(m, 0) = m gcd(m,0)=m

证明: m m m​ , n n n​, 其中 m > n m > n m>n​​

则 m = n ∗ k + r , r = m m o d n , k 是 整 数 m = n*k+r \ , \ r = m \ mod \ n, k是整数 m=n∗k+r , r=m mod n,k是整数​​​​​

假设存在 u u u​​.使得 m = s ∗ u , n = t ∗ u m = s*u, n = t*u m=s∗u,n=t∗u​​ , u u u​​ 为 m m m​​, n n n​​的约数

则 r = s ∗ u − k ∗ ( t ∗ u ) = ( s − k ∗ t ) ∗ u r = s*u - k*(t*u) = (s-k*t)*u r=s∗u−k∗(t∗u)=(s−k∗t)∗u​,

m m m​ 和 n n n​ 的约数也整除它们的余数 r r r​​​

所以 m m m​ 和 n n n​ 的任一约数同时也是 n n n​和 r r r​​ 的约数

反之, n n n和 r r r 的任一约数也是 m m m 和 n n n​​​ 的约数。

// Euclid(m,n)
/** 欧几里得算法求取最大公约数 */
public static int Eulcid(int m, int n){while (n!=0){int r = m % n;m = n;n = r;}return m;
}

连续值检测算法

选取俩者的最小值,向下连续检测数值

局限:当 m m m, n n n​中输入为0时,结果是错误的。

t = min(m, n);
while(t > 0){if(m % t == 0 && n % t == 0){return t;} else{t--;}
}

质因数相乘法

找出俩者公共的质因数,相乘得到结果

总结:对比三种计算最大公约数的方法,连续值检测法未能清晰规定算法输入的值域,当输入为0时,计算结果出错;质因数相乘法,对于如果计算质因数未能明确给定计算步骤。算法,应当清晰定义输入输出的值域,清晰定义计算的步骤。

例子:用来阐述一个不大于给定整数 n n n的连续质数序列

埃拉托色尼筛选法

  1. 初始化 2 2 2 ​~ n n n​​ 的连续整数序列作为候选质数
  2. 第一次循环,消去 2 2 2​的倍数(不包括2)
  3. 第二次循环,消去 3 3 3的倍数(不包括3)
  4. 第三次循环,消去 5 5 5的倍数(不包括5),4之前已经被消去了
  5. … u n t i l n until \ n until n​
/** Sievea(n)* 连续质数序列产生算法* Input:正整数n>1* Output:包含所有小于等于n的质数的数组L*/
public static List<Integer> Sieve(int n){int[] A = new int[n+1];for(int p = 2; p <= n; ++p){A[p] = p;}for(int p = 2; p <= Math.sqrt(n); ++p){if(A[p] != 0){int j = p * p;while(j <= n){A[j] = 0;j += p;}}}List<Integer> L = new ArrayList<>();for(int p = 2; p < n+1; ++p){if(A[p] != 0){L.add(A[p]);}}return L;
}

当我们正在消去p的倍数,第一个值得考虑的是 p ∗ p p*p p∗p ,因其他更小的倍数 2 p , ⋯ , ( p − 1 ) ∗ p 2p, \cdots ,(p-1)*p 2p,⋯,(p−1)∗p已经在之前的步骤中从序列中消去了,所以 p ∗ p < = n p*p <= n p∗p<=n, 因此 p < = s q r t ( n ) p <= sqrt(n) p<=sqrt(n)​​

算法问题求解基础

算法是问题程序化解决方案

  1. 理解问题:输入输出范围、特殊情况考虑(边界条件等)

  2. 确定:

    (1)计算方法(了解设备性能,并行/串行)

    (2)精确或近似解法

    (3)算法设计技术

  3. 设计算法:确定合适的数据结构,伪代码描述,流程图

  4. 正确性证明

  5. 分析算法:简单(易读,易懂),一般(问题的一般性,接受输入的一般性),时间、空间

  6. 根据算法写代码

算法效率分析基础

效率分析框架

  1. 算法的时间效率和空间效率都用输入规模的函数进行度量
  2. 算法基本操作的执行次数来度量算法的时间效率;通过计算算法消耗的额外存储单元的数量来度量空间效率
  3. 输入规模相同的情况下,部分算法的效率会有显著差异,需要区分最差效率,平均效率,最优效率
  4. 当算法的输入规模趋向于无限大时,它的运行时间(消耗的额外空间)函数的增长次数

渐近符号和基本效率类型

效率分析框架主要关心一个算法的基本操作次数的增长次数,并把它作为算法效率的主要指标,主要用三种渐进符号表示

  1. O ( g ( n ) ) O(g(n)) O(g(n)):增长次数小于等于 g ( n ) g(n) g(n)​(及其常数倍, n n n​趋向于无穷大)的函数集合
  2. Ω ( n ) \Omega(n) Ω(n):代表增长次数大于等于 g ( n ) g(n) g(n)(及其常数倍, n n n趋向于无穷大)的函数集合
  3. Θ ( n ) \Theta(n) Θ(n):增长次数等于 g ( n ) g(n) g(n)(及其常数倍, n n n趋向于无穷大)的函数集合

利用极限比较增长次数

基本的效率类型

类型 名称 注释
1 1 1 常量 为数很少的效率最高的算法,很难举出几个合适的例子,因为典型情况下,当输入的规模变得无穷大时,算法的运行时间也会趋向于无穷大
l o g n log \ n log n​ 对数 一般来说,算法的每一次循环都消去问题规模的一个常数因子,注意,一个对数算法不可能关注它的输入的每一个部分(哪怕是输入的一个固定部分):对任何能做到这一点的算法最起码拥有线性运行时间
n n n 线性 扫描规模为 n n n的列表(顺序查找)的算法属于这个类型
n l o g n n \ log \ n n log n​ 线性对数 许多分治算法,包括合并排序和快速排序的平均效率,都属于这个类型
n 2 n^2 n2 平方 一般来说,这是包含两重嵌套循环的算法的典型效率。线性代数中一些著名的算法属于这一类型
n 3 n^3 n3 立方 一般来说,这是包含三重嵌套循环的算法的典型效率。线性代数中一些著名的算法属于这一类型
2 n 2^n 2n 指数 求 n n n个元素集合的所有子集是这种类型的典型例子,“指数”这个术语常常被用在一个更广的层面上,不仅包括这种类型,还包括那些增长速度更快的类型
n ! n! n! 阶乘 求 n n n个元素集合的完全排列的算法是这种类型的典型例子

非递归算法的数学分析

例1:从 n n n​个元素的列表中查找元素最大值的问题

MaxElement(A[0...n-1])
// 求给定数组中的最大元素的值
// 输入:实数数组A[0..n-1]
// 输出:A中最大元素的值
maxVal = A[0]
for(int i = 1; i < n; ++i){if(A[i] > maxVal){maxVal = A[i];}
}
return maxVal;

分析非递归算法时间效率的通用方案

  1. 决定用哪个(哪些)参数表示输入规模
  2. 找出算法的基本操作(作为一个规律,总是位于算法的最内层循环)
  3. 检查基本操作的执行次数是否之依赖于输入规模,如果还依赖于一些其他的特性,则最差效率,平均效率以及最优效率(如有必要)需要分析研究。
  4. 建立一个算法的基本操作执行次数的求和表达式
  5. 利用求和运算的标准共识和法则来建立一个操作次数的闭合公式,或者至少确定它的增长次数

例2: 元素唯一性问题,验证给定数组的 n n n个元素是否全部唯一

UniqueElements(A[0,,n-1])
//验证给定数组中的元素是否全部唯一
//输入:数组A[0,,n-1]
//输出:如果A中的元素全部唯一,返回true
//     否则,返回false
for(int i = 0; i < n-1; i++){for(int j = i + 1; j < n-1; j++){if(A[i] == A[j]){return false;}}
}
return true;

例3:矩阵乘积计算问题 C = A B C=AB C=AB​

// MatrixMultiplication(A[0..n-1,0..n-1],B[0..n-1,0..n-1])
// 用基于定义的算法计算俩个n阶矩阵的乘积
// 输入:两个n阶矩阵A,B
// 输出:矩阵C=AB
for(int i = 0; i < n; i++){for(j = 0; j < n; j++){C[i,j]=0;for(int k = 0; k < n; k++){C[i,j] = C[i,j]+A[i,k]+B[k,j];}}
}
return C;

例4: 十进制正整数在二进制表示中的数字个数

// Binary(n)
// 输入:十进制正整数n
// 输出:n在二进制表示中的二进制数字个数
count = 1;
while(n > 1){count += 1;n = Math.floor(n/2);//向下取整
}
return count;

递归算法的数学分析

例1:计算 n ! n! n!

// F(n) = n!
// 递归计算n!
// 输入:非负整数n
// 输出:n!的值
if(n == 0){return 1;
}else{return F(n-1)*n;
}

当 n > 0 n > 0 n>0, F ( n ) = F ( n − 1 ) + 1 F(n)=F(n-1)+1 F(n)=F(n−1)+1

M ( n ) M(n) M(n)表示乘法的执行次数,则 M ( n ) = M ( n − 1 ) + 1 M(n) = M(n-1)+1 M(n)=M(n−1)+1

M ( 0 ) = 0 M(0) = 0 M(0)=0

M ( n ) = M ( n − 1 ) + 1 = . . . = M ( n − i ) + i = . . . = M ( n − n ) + n = n M(n)=M(n-1)+1=...=M(n-i)+i=...=M(n-n)+n=n M(n)=M(n−1)+1=...=M(n−i)+i=...=M(n−n)+n=n

分析递归算法时间效率的通用方案

  1. 决定用哪个(哪些)参数作为输入规模的度量标准
  2. 找出算法的基本操作
  3. 检查一下,对于相同规模的不同输入,基本操作的执行次数是否可能不同。如果有这个可能,则必须对最差效率,平均效率以及最优效率做单独研究
  4. 对于算法基本操作的执行次数,建立一个递推关系以及相应的初始条件
  5. 解这个递推式,或者至少确定它的解的增长次数

例2: 汉诺塔游戏

M ( n ) = M ( n − 1 ) + 1 + M ( n − 1 ) = 2 M ( n − 1 ) + 1 M(n)=M(n-1)+1+M(n-1)=2M(n-1)+1 M(n)=M(n−1)+1+M(n−1)=2M(n−1)+1

M ( 1 ) = 1 M(1)=1 M(1)=1

$$​

M ( n ) = 2 [ 2 M ( n − 2 ) + 1 ] + 1 = 2 2 M ( n − 2 ) + 1 M(n)=2[2M(n-2)+1]+1=2^2M(n-2)+1 M(n)=2[2M(n−2)+1]+1=22M(n−2)+1​

M ( n ) = 2 n − 1 M ( n − ( n − 1 ) ) + 2 n − 1 + 1 M(n)=2^{n-1}M(n-(n-1))+2^{n-1}+1 M(n)=2n−1M(n−(n−1))+2n−1+1

M ( n ) = 2 n − 1 M(n)=2^n-1 M(n)=2n−1

计算斐波那契数列讨论

F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n−1)+F(n−2)

F ( 0 ) = 0 , F ( 1 ) = 1 F(0)=0,F(1)=1 F(0)=0,F(1)=1

算法的经验分析

对算法效率做经验分析的通用方案

  1. 了解实验的目的
  2. 决定用来度量效率的度量标准M和度量单位(用操作次数还是直接用时间)
  3. 决定输入样本的特性(它的范围和大小等)
  4. 为实验准备算法(或若干算法)的程序实现
  5. 生成输入样本
  6. 对输入样本运行算法(或若干算法),并记录观察到的实验数据
  7. 分析获得的实验数据

蛮力法

选择排序和冒泡排序

选择排序

选择出当前元素应该放置的元素(升序排列,找出当前轮次的最小元素),依次循环

Θ ( n 2 ) \Theta(n^2) Θ(n2)

SelectionSort(A[0],,A[n-1])
// 该算法用选择排序对给定的数组排序
// 输入:一个可排序的数组A[0..n-1]
// 输出:升序排列的数组A[0..n-1]
for(int i = 0; i < n-1; ++i){int minPos = i;for(int j = i+1; j < n; ++j){if(A[minPos] > A[j]){minPos = j;}swap(A[i], A[minPos]);}
}

冒泡排序

比较相邻元素

BubbleSort(A[0..n-1])
// 该算法用冒泡排序对数组A[0.n-1]进行排序
// 输入:一个可排序数组A[0..n-1]
// 输出:非降序排列的数组A[0..n-1]
for(int i = 0; i < n-1; ++i){for(int j = 0; j < n-1-i; ++j){if(A[j] > A[j+1]){swap(A[j], A[j+1]);}}
}

顺序查找和蛮力字符串匹配

顺序查找

SequentialSearch(A[0..n],K)
// 顺序查找的算法实现。它用了查找键来作限位器
// 输入:一个n个元素的数组A和一个查找键K
// 输出:第一个值等于K的元素的位置,如果找不到这样的元素,返回-1
A[n] = K;
i = 0;
while(A[i] != K){i++;
}
if(i < n)return i;
elsereturn -1;

如果查找序列是有序的话,可以查找到或者大于查找键后直接返回

蛮力字符串匹配

给定一个 n n n​个字符串组成的串[称为文本(text)],一个 m ( m < = n ) m(m<=n) m(m<=n)个字符的串[称为模式(pattern)],从文本中寻找匹配模式的子串。

BrureForeStringMatch(T[0..n-1],P[0..m-1])
// 该算法实现了蛮力字符串匹配
// 输入:一个n个字符的数组T[0..n-1],代表一段文本
//          一个m个字符的数组P[0..m-1],代表一个模式
// 输出:如果查找成功,返回文本的第一个匹配子串中第一个字符的位置,否则返回-1
for(int i = 0; i < n-m+1; ){j = 0;while(j < m && P[j] = T[i+j]){j++;if(j == m){return i;}}
}
return -1;

最坏情况 O ( n m ) O(nm) O(nm)​​

最近对和凸包问题的蛮力算法

最近对问题

最近点对问题要求在一个包含 n n n个点的集合中,找出距离最近的俩个点。

一个重要的应用是统计学中的聚类分析。对于 n n n个数据点的集合,层次聚类分析希望基于某种相似度度量标准将数据点构成的簇按照层次关系组织起来。

(1)对于数值型数据,相似度度量标准的通常采用欧几里得距离

(2)对于文本和其他非数值型数据,通常采用诸如汉明距离这样的相似度度量标准。

d ( p i , p j ) = ( x i − x j ) 2 + ( y i − y j ) 2 d(p_i,p_j)=\sqrt {(x_i-x_j)^2+(y_i-y_j)^2} d(pi​,pj​)=(xi​−xj​)2+(yi​−yj​)2 ​

BruteForceClosestPoints(p)
// 使用蛮力法求平面中距离最近的俩点
// 输入:一个n(n>=2)个点的列表p,p1=(x1,y1)...pn=(xn,yn)
// 输出:俩个最近点的距离
$d = \infin$
for(int i = 1; i < n; ++i){for(j = i+1; j < n+1; ++j){d = min(d, sqrt((xi-xj)^2+(yi-yj)^2));//sqrt是平方根函数}
}
return d;

基本操作是计算平方根,其实可以转而比较平方本身,而避免平方根计算,算法的基本操作转为求平方,加快内层循环的速度。

凸包问题

凸集合:对于平面上的一个点集合(有限的/无限的),如果以集合中的任意俩点 p p p, q q q为端点的线段都属于该集合,我们说这个集合是凸的。

凸包概念:对于平面上 n n n个点的集合,它的凸包就是包含所有这些点(或者在内部,或者在边界上)的最小的凸多边形。

凸包:一个点集合 S S S的凸包是包含 S S S的最小凸集合,(“最小”意指 S S S 的凸包一定是所有包含 S S S 的凸集合的子集)。

定理:任意包含 n > 2 n>2 n>2个点(不共线的点)的集合S的凸包是以S中的某些点为顶点的凸多边形(如果所有的点都位于一条直线上),多边形退化为一条线段,但它的俩个端点仍然包含在S中。

凸集合中的极点:对于任何以集合中的点为端点的线段来说,它不是这种线段的中点。

单纯形法用于解决现行规划问题,找到极点也就解出了凸包问题。

对于一个 n n n个点集合中的俩个点 p i , p j p_i,p_j pi​,pj​​​​,当且仅当该集合中的其他点都位于穿过这俩点的直线的同一边时,他们的连线是该集合凸包边界的一部分。对每一对点都做一遍检验之后,满足条件的线段构成了凸包的边界。

在坐标平面上穿过俩个点 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_2,y_2) (x1​,y1​),(x2​,y2​)的直线有下列方程定义:

a x + b y = c ax+by=c ax+by=c​

其中

a = y 2 − y 1 a=y_2-y_1 a=y2​−y1​

b = x 1 − x 2 b = x_1-x_2 b=x1​−x2​

c = x 1 y 2 − y 1 x 2 c=x_1y_2-y_1x_2 c=x1​y2​−y1​x2​

这样直线可以将平面划分为俩个半平面,其中一个半平面的点都满足 a x + b y > c ax+by>c ax+by>c

另一个半平面中的点 a x + b y < c ax+by<c ax+by<c(直线上的点满足 a x + b y = c ax+by=c ax+by=c)。

为检验某些点事都位于直线的同一边,只需把每个点带入 a x + b y − c ax+by-c ax+by−c,检验这个表达式的符号是否相同。

穷举查找

旅行商问题

(Traveling salesman problem, TSP), 要求找出一条 n n n 个给定城市间的最短路径,使我们在回到出发的城市之前,对每个城市都只访问一次。

该问题可以表述为求一个图的最短哈密顿回路。

哈密顿回路:一个对图的每一个顶点都只穿越一次的回路。

可以假设,所有的回路都开始和结束于相同的特定顶点。可以通过生成 n − 1 n-1 n−1个中间城市的组合来得到所有的旅行线路,计算这些线路的长度,然后求得最短的线路。

背包问题

给定 n n n个重量为 w 1 , w 2 , . . . w n w_1,w_2,...w_n w1​,w2​,...wn​,价值为 v 1 , v 2 , . . . v n v_1,v_2,...v_n v1​,v2​,...vn​的物品和一个承重为 W W W的背包,求这些物品中一个最有价值的子集,并且能够装到背包中。

穷举查找需要考虑给定的 n n n个物品集合的所有子集,为了找出可行的子集(也就是说,总重量不超过背包承重能力的子集),要计算出每个子集的总重量,然后找出它们中间价值最大的子集。

分配问题

有 n n n个任务需要分配给 n n n个人执行,一个任务对应一个人(每个任务只分配给一个人,每个人只分配一个任务),对于每一对 i , j = 1 , 2 , . . . , n i,j=1,2,...,n i,j=1,2,...,n来说,将第 j j j个任务分配给第 i i i个人的成本是 C [ i , j ] C[i,j] C[i,j]。该问题是要找出总成本最小的分配方案。

一般情况下,需要考虑的排列数量是 n ! n! n!,对于该问题有一个效率高效得多的算法是匈牙利算法

深度优先查找和广度优先查找

深度优先查找

可以从任意顶点开始访问图的顶点,然后把该顶点标记为已访问。在每次迭代的时候,该算法紧接着处理与当前顶点邻接的未访问顶点。(如果有若干个这样的顶点,可以任意选择一个顶点,选择哪一个邻接的未访问的候选顶点主要是由表示图的数据结构决定的)。过程持续直到遇到一个终点,该顶点的所有邻接点都已被访问过,在后退到起始顶点。如果未访问的顶点仍然存在,该算法必须从其中一顶点开始,重复上述过程。

用栈跟踪深度优先查找的操作是比较方便的。

DFS(G)
//  实现给定图的深度优先查找遍历
// 输入:图G=(V,E)
// 输出:图G的顶点,按照DFS遍历第一次访问到的先后次序,用连续的整数标记将V
//      中的每个顶点标记为0,表示“未访问”
count = 0;
for(Vertex v: V){if(v.markedCount == 0){dfs(v);}
}
dfs(v);
// 递归访问所有和v相接的未被访问的顶点,然后按照全局变量count的值
// 根据遇到他们的先后顺序,给他们赋值相应的数字
count++;
// 标记v访问
v.markedCount = count;
for(Vertex w : V){// w是v的邻接点并且未被访问过if(w.adjacent(v) && w.markedCount == 0){dfs(w);    }
}

DFS产生俩种节点的排列顺序,第一次访问顶点(入栈)的次序和顶点称为终点(出栈)的次序。

DFS在访问所有和厨师顶点有路径相连的顶点后结束,可以用于检查一个图的连通性以及连通分量,利用图的DFS森林形式的表示法可以检查图中是否包含回路。

广度优先查找

首先访问所有和初始顶点邻接的点,然后是离它俩条边的所有未访问顶点,以此类推,直到所有与初始顶点同在一个连通分量中的顶点都被访问过了为止。如果仍然存在未被访问的顶点,该算法必须从图中的其他连通分量重的任意顶点重新开始。

使用队列来跟踪广度优先查找操作时比较方便的。

该队列从遍历的初始顶点开始,将该顶点标记为已访问,在每次迭代的时候,该算法找出所有和队头顶点邻接的未访问顶点,将它们标记为已访问,再把他们入队,然后将队头顶点从队列中移去。

BFS(G)
// 实现给定图的广度优先遍历
// 输入:图G=(V,E)
// 输出:图G的顶点,按照BFS遍历访问到的先后次序,用连续的整数标记
//      将V中的每个顶点标记为0,表示还“未访问”
count = 0;
for(Vertex v: V){if(v.markerdCount == 0){bfs(v);}
}
bfs(v);
//访问所有和v连接的未访问顶点,然后按照全局变量count的值
//根据访问他们的先后次序,给它们赋上相应的数字
count++;
v.markedCount = count;
while(!queue.isEmpty()){for(Vertex w : V){if(w.adjacent(v) && w.markedCount == 0){count++;w.markedCount = count;queue.push(w);}}queue.pop(w);
}

BFS 只产生顶点的一种排序,因为队列时先进先出的结构,所以顶点入队和出队次序一致。

BFS检查图的连通性和无环性,可以求俩个顶点间边的数量最少的路径。

减治法

利用来一个问题给定实例的解和同样问题较小实例的解之间的某种关系,一旦建立了这种关系,我们既可以从顶向下也可以由底向上来运用该关系。

3种主要变化形式

  • 减去一个常量
  • 减去一个常量因子
  • 减去的规模是可变的

减一技术:规模为 n n n​的问题—>规模为 n − 1 n-1 n−1​的问题—>子问题的解—>原问题的解

减半技术:规模为 n n n​的问题—>规模为 n / 2 n/2 n/2​​的问题—>子问题的解—>原问题的解

减可变规模:计算最大公约数的欧几里得算法 g c d ( m , n ) = g c d ( n , m m o d e n ) gcd(m,n)=gcd(n,m\ mode\ n) gcd(m,n)=gcd(n,m mode n)​

减一技术

插入排序

数组 A [ 0.. n − 1 ] A[0..n-1] A[0..n−1]

遵循减一的思路,假设数组 A [ 0... n − 2 ] A[0...n-2] A[0...n−2]​​已经有序, A [ 0 ] ≤ . . . ≤ A [ n − 2 ] A[0]\le ... \le A[n-2] A[0]≤...≤A[n−2]​​​

则对于 A [ n − 1 ] A[n-1] A[n−1],我们需要做的就是在这些有序的元素中为 A [ n − 1 ] A[n-1] A[n−1]找到合适的位置,插入进去。

一般来说,可以从左至右扫描该有序数组的子数组,直到遇到一个小于等于 A [ n − 1 ] A[n-1] A[n−1]的元素,然后把 A [ n − 1 ] A[n-1] A[n−1]​插在这个元素的后面,这种被称为直接插入排序

InsertionSort(A[0..n-1])
// 用插入排序对给定数组排序
// 输入:n个可排序数组构成的一个数组A[0..n-1]
// 输出:非降序排列的数组A[0..n-1]
for(int i = 1; i < n; ++i){val = A[i]j = i-1;while(j >= 0 && A[j] > val){A[j+1] = A[j];j--;}A[j+1] = val;
}

拓扑排序

有向图:一个对所有边都指定方向的图

邻接矩阵和邻接链表是俩种表示有向图的主要手段。

当采用这这俩种方式表示时,无向图和有向图只有俩个显著的差异:

(1)有向图的邻接矩阵并不一定表现出对称性

(2)有向图的一条边在图的邻接链表中只有一个相应的节点(不是俩个)

有向图的遍历,深度优先和广度优先查找是主要的遍历算法

在对图的边引入方向后,讨论一个问题,

例如:

一个必修课集合 {} C 1 , C 2 , C 3 , C 4 , C 5 {C_1,C_2,C_3,C_4,C_5} C1​,C2​,C3​,C4​,C5​​学生必须在某个阶段修完这几门课程,可以按照任何次序学习这些课程,只要满足下列条件:

(1) C 1 C_1 C1​​和 C 2 C_2 C2​​没有任何先决条件

(2)修完 C 1 C_1 C1​​, C 2 C_2 C2​​才能修 C 3 C_3 C3​​

(3)修完 C 3 C_3 C3​​才能修 C 4 C_4 C4​​

(4)修完 C 3 C_3 C3​​和 C 4 C_4 C4​​才能修 C 5 C_5 C5​​​

(5)每个学习只能修一门课程

是否可以按照这种次序列出它的顶点,使得对于图中每一条边来说,边的起始顶点总是排在边的结束顶点之前(是不是能够求出该图节点的这样一个序列?)这个问题称为拓扑排序

如果有向图具有一个有向的回路,该问题无解,为使得拓扑排序成为可能,充要条件是问题中的图必须是一个无环有向图。

有俩种高效的算法是既可以验证是否是无环有向图,又可以在是的情况下输出拓扑排序的一个顶点序列。

第一种:深度优先查找的一个简单应用(DFS):执行一次DFS遍历,并记住顶点变成死端(即推出遍历栈)的顺序。

将该次序反过来就得到拓扑排序的一个解,当然,在遍历的时候不能遇到回边。

如果遇到一条回边,该图就不是无环有向图,并且对它的顶点的拓扑排序是不可能的。

当一个顶点 v v v退出DFS栈时,在比 v v v更早退出栈的顶点中,不可能存在顶点 u u u拥有一条从 u u u到 v v v的边(否则, ( u , v ) (u,v) (u,v)会成为一条回边),所以,在退栈的队列中,任何这样的顶点 u u u都会排在 v v v的后面,并且在逆序队列中会排在 v v v的前面。

第二种:基于减一技术的一个直接实现(源删除算法):不断地做这样一件事,在余下的有向图中求出一个源(source),它是一个没有输入边的顶点,然后把它和从它出发的边都删除,(如果有多个这样的源,可以任意选择一个。如果这样的源不存在,算法停止,因为该问题无解)顶点被删除的次序就是拓扑排序问题的一个解。

拓扑排序在计算机科学中有很多应用,包括程序编译中的指令调度,电子表格单元格的公式求值顺序以及解决链接器中的符号依赖问题。

生成组合对象的算法

组合对象中最重要的类型就是排列,组合,给定集合的子集。离散数学有一个分支名为组合数学,专门研究组合对象。我们这里感兴趣的主要是如何生成它们。

生成排列

假如需要对元素进行排列的集合是从 1 1 1到 n n n的简单整数集合,解释为 n n n个元素 a 1 , . . a n {a_1,..a_n} a1​,..an​​的元素下标。

对于生成 1 , . . . n {1,...n} 1,...n的所有 n ! n! n!个排列的问题:

减一技术:将问题规模减一,转化成 ( n − 1 ) ! (n-1)! (n−1)!​​个排列,把 n n n插入 n − 1 n-1 n−1个元素的每一种排列中的 n n n个可能位置中去,来得到较大规模问题的一个解。

Johnson Trotter(n)
// 实现用来生成排列的Johnson Trotter算法
// 输入:一个正整数n
// 输出: {1,...,n}的所有排列的列表
将第一个排列初始化为12..n
while(存在一个移动元素){求最大的移动元素k把k和它箭头指向的相邻元素互换调转所有大雨k的元素的方向将新排列添加到列表中
}

对于 n = 3 n=3 n=3,字典序:

123 , 132 , 213 , 231 , 312 , 321 123, 132,213,231,312,321 123,132,213,231,312,321

LexicograhicPermute(n)
// 以字典序产生排列
// 输入:一个正整数n
// 输出:在字典序下{1,..,n}所有排列的列表
init(第一个排列为12..n)
while(最后一个排列有俩个连续升序的元素){找出使得a_i<a_i+1的最大的i(a_i+1>a_i+2>..>a_n)找到使得a_i<a_j的最大索引j(j>=i+1)因为a_i < a_i+1// 交换a_i,a_jswap(a_i,a_j);// 将a_i+1到a_n反序reverse(a,i+1,n);// 将这个新排列添加到列表中resultList.add(a);
}

生成子集

幂集:一个集合的所有子集的集合称为它的幂集。

是否存在一种生成位串的最小变化算法,使得每一个位串和它直接前趋之间仅仅相差一位(就子集来说,我们希望每一个子集和它的直接前趋之间的区别,要么是增加一个元素,要么是删除一个元素,但俩者不能同时发生)

》〉》〉》 二进制反射格雷码(binary reflected Gray code)

例如 n = 3 n=3 n=3,

000 001 011 010 110 111 101 100 000 \ 001 \ 011\ 010\ 110\ 111\ 101\ 100 000 001 011 010 110 111 101 100

BRGC(n)
// 递归生成n位的二进制反射格雷码
// 输入:一个正整数n
// 输出:所有长度为n的格雷码位串列表
if(n==1){表L包含位串0和位串1
}else{// 生成长度为n-1的位串列表L1L1 = BRGC(n-1)// 把表L1倒序后复制给L2copy(reverse(L1), L2)// 把0加到表L1中的每个位串前面L1.addFront(0)// 把1加到表L2中的每个位串前面L2.addFront(1)// 把表L2添加到表L1后面得到表LL = L1.addTail(L2)
}
return L;

减常因子算法

减常因子算法常常具有对数时间效率,非常高效,因此实例并不多。

折半查找

对于有序数组来说,折半查找是一种性能卓越的算法,它通过逼阿胶查找键K和数组中间元素 A [ m ] A[m] A[m]来完成查找工作,如果它们相等,算法结束。否则,如果 K < A [ m ] K<A[m] K<A[m]​,就对数组的前半部分执行该操作,如果 K > A [ m ] K>A[m] K>A[m],则对数组的后半部分执行该操作。

折半查找是基于递归的思想,也可以非递归算法实现。

BinarySearch(A[0..n-1],K)
// 实现非递归的折半查找
// 输入:一个升序数组A[0..n-1]和一个查找键K
// 输出:一个数组元素的下标,该元素等于K;如果没有这样一个元素,返回-1
l = 0, r = n-1;
while(l <= r){int m = (l+r)/2if(K == A[m]) return m;else if(K < A[m]) r = m-1;else if(K > A[m]) l = m+1;
}
return -1;

C a v g = log ⁡ 2 n C_{avg} \ = \log_2n Cavg​ =log2​n​​

假币问题

在 n n n枚外观相同的硬币中,有一枚假币。

在一架天平上,可以比较任意俩组硬币,可以通过观察天平向右倾还是向左倾还是水平,判断俩组硬币重量是否相同,或者哪一组更重,要求设计一个高效的算法来检测出这枚假币。

假设假币相对真币较轻

最自然的思路是将 n n n枚硬币分为俩摊,每堆有 n / 2 n/2 n/2枚硬币

(1)如果 n n n​​为奇数,就留下一枚额外的硬币,然后把俩堆硬币放在天平上,如果俩堆硬币重量相同,那么放在旁边的即为假币;否则循环比较较轻的一堆硬币

(2)如果 n n n​为偶数,则循环比较较轻的一堆硬币

W ( n ) = W ( n / 2 ) + 1 , 当 n > 1 , W ( 1 ) = 0 W(n)=W(n/2)+1,当n>1,W(1)=0 W(n)=W(n/2)+1,当n>1,W(1)=0

W ( n ) = l o g 2 n W(n)=log_2n W(n)=log2​n

这并不是最高效的解法,如果把硬币分为三堆呢?每堆 n / 3 n/3 n/3枚硬币,将会更好

W ( n ) = l o g 3 n W(n)=log_3n W(n)=log3​n

俄式乘法

假设 n n n和 m m m​是俩个真整数,需要计算它们的乘积。

同时,我们用 n n n​的值作为实例规模的度量标准,这样,

(1)如果 n n n​是偶数,一个规模为原来一半的实例必须要对 n / 2 n/2 n/2​进行处理,对于该问题较大的实例的解和较小实例的解的关系,有一个显而易见的公式:

n ∗ m = n ∗ 2 m / 2 n*m=n*2m/2 n∗m=n∗2m/2​

(2)如果 n n n是奇数,只需要对该公式做轻微调整:

n ∗ m = ( n − 1 ) / 2 ∗ 2 m + m n*m=(n-1)/2*2m+m n∗m=(n−1)/2∗2m+m

通过应用这个公式,并以 1 ∗ m = m 1*m=m 1∗m=m作为算法停止的条件。

既可以采用递归也可以采用迭代计算,该算法只包括折半,加倍,相加这几个操作,硬件实现速度也很快,使用移位即可完成折半和加倍操作

约瑟夫斯问题

n n n个人围成一个圈,并将他们从 1 1 1到 n n n​​编上号码。从编号为 1 1 1的那个人那里开始这个残酷的计数,每次消去第二个人直到只留下最后一个幸存者。

要求算出幸存者的号码 J ( n ) J(n) J(n)​

(1)如果 n n n为偶数, n = 2 k n=2k n=2k,对整个圆圈处理第一遍之后,生成了同样问题的规模减半的实例。唯一差别是位置的编号。

例如一个初始位置为 3 3 3​的人在第 2 2 2​轮会处于 2 2 2​号位置上,初始位置 5 5 5​的人会处在 3 3 3​号位置上,以此类推,

J ( 2 k ) = 2 J ( k ) − 1 J(2k)=2J(k)-1 J(2k)=2J(k)−1​​

(2)如果 n n n​为奇数, n = 2 k + 1 n=2k+1 n=2k+1​。第一轮消去所有偶数位置上的人,如果把紧接着消去的位置 1 1 1​上的人也加进来,留下一个规模为 k k k​的实例。这里,为了得到与新的位置编号相对应的初始位置编号,我们必须把新的位置编号乘 2 2 2​再加上 1 1 1​,因此对于奇数 n n n​,

J ( 2 k + 1 ) = 2 J ( k ) + 1 J(2k+1)=2J(k)+1 J(2k+1)=2J(k)+1

由于这个游戏可以看成一个环形,位置的变化是一种环形内位置的移位过程,我们可以对 n n n本身做一次向左的循环移位来得到 J ( n ) J(n) J(n),

J ( 6 ) = J ( 11 0 2 ) = 10 1 2 = 5 J(6)=J(110_2)=101_2=5 J(6)=J(1102​)=1012​=5

J ( 7 ) = J ( 11 1 2 ) = 11 1 2 = 7 J(7)=J(111_2)=111_2=7 J(7)=J(1112​)=1112​=7

减可变规模算法

在减治法的第三个主要变化形式中,算法在每次迭代时,规模减小的模式和另一次迭代时不同的,计算最大公约数的欧几里得算法提供了这类算法的一个非常好的例子。

计算中值和选择问题

选择问题是求一个 n n n个数列表的第 k k k个最小元素的问题。

这个数字被称为第 k k k​个顺序统计量

对于 k = 1 , k = n k=1,k=n k=1,k=n,可以扫描元素列表,获取最小或最大元素。

该问题的一个有意思的情况是在 k = n / 2 k=n/2 k=n/2​​时,要求找出这样一个元素,列表中的一半元素哒,又比一半元素小。这个元素称为中值

(1)一种方法是先将列表排序,选出第 k k k个元素。算法的运行时间取决于排序算法的效率,选用类似合并排序的算法,效率是 O ( n l o g n ) O(nlog_n) O(nlogn​)​

当然,整个列表的排序可能没有必要,毕竟我们只是找出第 k k k小的元素

(2)划分的思路。将一个给定列表根据某个值 p p p(例如列表的第一个元素)进行划分,对列表元素进行重新整理,使左边部分包含所有小于等于 p p p的元素,紧接着是中轴本身,右边是所有大于或等于 p p p的元素

又俩种主要的划分算法, L o m u t o 划 分 Lomuto划分 Lomuto划分, H o a r e 算 法 Hoare算法 Hoare算法​

∣ 所 有 小 于 等 于 p 的 元 素 ∣ |所有小于等于p的元素| ∣所有小于等于p的元素∣ ∣ p ∣ |p| ∣p∣ ∣ 所 有 大 于 或 等 于 p 的 元 素 ∣ |所有大于或等于p的元素| ∣所有大于或等于p的元素∣​

这里讨论 L o m u t o 划 分 Lomuto划分 Lomuto划分

// LomutoPartition(A[l,,r])
// 采用Lomuto算法,用第一个元素作为中轴子对数组进行划分
// 输入:数组A[0..n-1]的一个子数组A[l..r],它由左右俩边的索引l和r(l<=r)定义
// 输出:A[l..r]的划分和中轴的新位置
p = A[l]
s = l
for(int i = l+1; i <= r; i++){if(A[i] < p){s = s+1;swap(A[s],A[i]);}
}
swap(A[l],A[s]);
return s;

如何利用划分列表来寻找第 k k k最小元素呢?

快速选择:假设列表时以数组实现的,其元素索引从 0 0 0开始,而 s s s是划分的分割位置,也就是划分后中轴所在元素的索引。

(1)如果 s = k − 1 s=k-1 s=k−1,中轴 p p p即为第 k k k小的元素

(2)如果 s > k − 1 s>k-1 s>k−1​,第 k k k​小元素就是被划分数组左边部分的第 k k k​​小元素

(3)如果 s < k − 1 s<k-1 s<k−1​,第 k k k​小元素就是被划分数组右边部分的第 ( k − s ) (k-s) (k−s)​​​小元素

递归

// Quickselect(A[l..r],k)
// 用基于划分的递归算法解决选择问题
// 输入:可排序的数组A[0..n-1]的子数组A[l..r]和整数k(1<=k<=r-l+1)
// 输出:A[0..n-1]中第k小元素的值
s = LomutoPartition(A[l,,r]) // 或者另一个划分算法
if s == l+k-1 return A[s]
else if s > l+k-1 Quickslect(A[l,,s-1], k)
else Quickselect(A[s+1,,r], l+k-1-s)

非递归


如果是求取第 k k k大元素,类似

效率分析: O ( n 2 ) O(n^2) O(n2)

插值查找

作为减可变规模算法的下一个例子,我们考虑一个查找有序数组的算法,插值查找

不同于折半查找总是查找键和给定有序数组的中间元素进行比较(也因此把问题规模消减了一半),插值查找为了找到用来和查找键进行比较的数组,考虑了查找键的值。

二叉树的查找和插入

二叉查找树:这种二叉查找树的节点包含了可排序项集合中的元素,每个节点一个元素,并使得对于每个节点来说,所有左子树的元素都小于子数根节点的元素,所有右子树的元素都大于子树根节点的元素。

当在这样一棵树中查找给定值 v v v的元素时,可以递归采用下面的方法。

(1)如果这棵树为空,则查找失败;

(2)如果这棵树不为空,把 v v v和根节点 K K K​作比较,

  • 如果等于 K K K,查找结束​
  • 如果比 K K K​​小,继续在左子树中查找
  • 如果比 K K K​​​大。继续在右子树中查找

一棵查找树的规模的最佳度量标准就是树的高度,树的高度的减少通常都不相同,这给我们一个很好的减可变规模算法的例子。

查找效率最差是当二叉查找树只有一边时,效率为 Θ ( n ) \Theta(n) Θ(n)​,平均查找效率为 Θ ( l o g n ) \Theta(logn) Θ(logn)​

拈游戏

一般来说,该游戏中会有若干堆棋子,但我们先来考单堆棋子的版本。

现在只有一堆 n n n个棋子。

俩个玩家轮流从堆中拿走最少一个,最多 m m m​个棋子。每次拿走的棋子数都可以不同,但能够拿走的上下限数量不变。如果每个玩家都做出了选择,哪个玩家能够胜利拿到最后那个棋子?是先走的还是后走的?

当且仅当 n m o d ( m + 1 ) ≠ 0 n \mod\ (m+1) \neq 0 nmod (m+1)​=0​​​,胜局

因此,胜利的策略是每次拿走 n m o d ( m + 1 ) n \mod\ (m+1) nmod (m+1)​个棋子,如果背离这个策略,则会把胜局留给对手

一般来说,拈游戏包含 I > 1 I>1 I>1堆棋子,每堆的棋子数分别 n 1 , n 2 , . . . n I n_1,n_2,...n_I n1​,n2​,...nI​​。每次走的时候,玩家可以从任意一堆棋子中拿走任意允许数量的棋子,甚至可以把一堆都拿光。游戏的目的同样是成为最后一个还能走的玩家。

这种形式的拈游戏的解出人意料,竟然基于堆中棋子数的二进制表示。 b 1 , b 2 , . . b I b_1,b_2,..b_I b1​,b2​,..bI​​分别表示各堆棋子数的二进制表示。计算它们的二进制数位和,也称为拈和,即对每一位分别求和并忽略进位。

可以证实,当且仅当二进制数位和中包含至少一个 1 1 1​时,该实例是一个胜局,只包含 0 0 0​时是一个败局。

例如: n 1 = 3 , n 2 = 4 , n 3 = 5 n_1=3,n_2=4,n_3=5 n1​=3,n2​=4,n3​=5​,数位和(拈和):011+100+101=010。该实例对于先走的玩家来说是一个胜局,要找到该局的一个胜手,玩家需要改变三个位串在中的一个,使得新的二进制数位和仅包含 0 0 0​。因此,先手玩家从第一堆中拿走 2 2 2个棋子。

算法设计与分析基础-笔记-上相关推荐

  1. 算法设计与分析复习笔记(上)

    简介:本文是博主在复习算法设计与分析的笔记,参考了北大算法设计与分析以及王晓东编著的<计算机算法设计与分析>第四版相关内容,如有错误,欢迎指正. 文章目录 设计技术 分治 动态规划 设计技 ...

  2. 计算机算法设计与分析读后感,算法设计与分析基础经典读后感有感

    <算法设计与分析基础>是一本由Anany levitin著作,清华大学出版社出版的胶版纸图书,本书定价:49.00元,页数:409,特精心从网络上整理的一些读者的读后感,希望对大家能有帮助 ...

  3. 第一章 算法设计与分析基础知识

    系列文章目录 第一章 算法设计与分析基础知识 第二章 算法的分治策略 第三章 算法的动态规划 第四章 算法的贪心法 -- @[TOC](这里写目录标题) # 一级目录 ## 二级目录 ### 三级目录 ...

  4. 算法设计与分析基础知识

    一.算法设计基础 算法是(algorithm)是对特定问题求解步骤的一种描述,是指令的有限序列. 算法的五个特性: 输入:一个算法可以有零个或多个输入. 输出:一个算法有一个输出或多个输出. 有穷性( ...

  5. 算法设计与分析基础知识点

    前言:全文参考徐承志老师的PPT 适合期末复习,查缺补漏,有缺漏或错误欢迎指正,后面的第九章内容之后会继续补充. 目录 一.算法基础概念 二.算法分析基础 1.概念 2.算法设计的一般过程 3.时间复 ...

  6. 算法设计与分析基础第三版

    课后题答案 一.算法级基础知识 1.算法的基本概念 解决问题的确定方法和有限步骤称为算法,对于计算机科学来说,算法指的是对特定问题的求解步骤的一种描述,是若干条指令的有穷序列.并有以下特性:输入.输出 ...

  7. 算法设计与分析基础 第一章谜题

    习题1.1 10.b 欧几里得游戏 一开始,板上写有两个不相等的正整数,两个玩家交替写数字,每一次,当前玩家都必须在板上写出任意两个板上数字的差,而且这两个数字必须是新的,也就是说,不能与板上任何一个 ...

  8. 算法设计与分析基础 第六章谜题

    习题6.1 9.数字填空 给定n个不同的整数以及一个包含n个空格的序列,每个空格之间事先给定有不等(>或<)符号,请设计一个算法,将n个整数填入这n个空格中并满足不等式约束.例如,数4,6 ...

  9. 算法设计与分析基础 第五章谜题

    习题5.1 11.Tromino谜题 Tromino是一个由棋盘上的三个1×1方块组成的L型骨牌.我们的问题是,如何用Tromino覆盖一个缺少了一个方块的2n×2n棋盘.除了这个缺失的方块,Trom ...

最新文章

  1. ftok file php,Linux和PHP中的ftok函数返回值不一致问题跟踪
  2. php 文章读取_php实现获取文章内容第一张图片的方法
  3. 7-3 逆序的三位数 (10 分)
  4. Node.js的helloworld 程序
  5. JS_01JavaScript基础笔记
  6. php 函数漏洞,PHP绕过禁用函数漏洞的原理与利用分析
  7. 选择、冒泡、插入、快速排序
  8. 5ecsgo正在发送客户端_MQTT X 桌面客户端使用指南
  9. UNIX环境高级编程习题——第一章
  10. java导出下载文件_java导出excel及下载的实现-java下载文件
  11. spyder python下载_spyder中文版下载-spyder pythonv4.1.3 官方最新版下载__飞翔下载
  12. 大话跨度原始服务器信息怎么去除,大话西游2合服历史:独家整理 寻找你最初的服务器...
  13. 小白疑问3dsmax和maya的区别有什么?大佬来给你解答
  14. WSO2 IS 添加新的证书域名
  15. 简述网桥,网关,路由器之间的区别和联系
  16. 记录Widows10系统崩溃后安装Widows7系统的心酸历程
  17. 发散阅读、拓宽思路【PageRank、Tf-Idf、协同过滤、分布式训练、StyleTransfer、Node2vec】
  18. 登录功能的测试用例设计
  19. 注解@Primary
  20. 打开网站被挂马跳转到博彩页面 解决办法

热门文章

  1. 开板季滑雪热度暴涨,小红书“滑雪”搜索量涨150%
  2. 深入理解设计模式-简单工厂模式vs工厂方法模式vs抽象工厂模式对比讲解
  3. 记一次oracle 11g数据导入
  4. 哪款蓝牙耳机性价比高?捷波朗Elite 75t和南卡A2降噪蓝牙耳机测评
  5. 陶哲轩破解数十年前几何猜想,用反例证明它在高维空间不成立,同行:推翻的方式极尽羞辱...
  6. 《Turtle绘图》Python用Turtle库绘制圣诞树、圣诞节考研祝福礼物
  7. 亚马逊跨境健身器材成为新蓝海,星淘惠告诉你怎么选品
  8. 『麻省理工线性代数中文讲义』学习笔记
  9. aircrack-ng工具使用
  10. 2023最新通信工程毕业设计题目选题推荐100例