《算法设计与分析基础》【part1】

Chapter 1 绪论

数据结构的基本概念:

数据结构三要素:逻辑结构、物理(存储)结构、数据的运算。

线性数据结构:数组(Array)和链表(Linked list);栈(Stack)和队列(Queue);堆(Heap)。

树形数据结构:无序树和有序树。

图形数据结构:有向图和无向图。

集合与字典:集合是互不相同项的无序组合(可以为空);字典的结构为{key : value}。

抽象数据类型(ADT):由一个表示数据项的抽象对象集合和一系列对这些对象所做的操作构成。

算法的基本概念:

  • 程序 = 数据结构(Data Structure) + 算法(Algorithm)
  • 五大特性:
    • 有穷性
    • 确定性
    • 可行性
    • 输入和输出
  • “好”算法的特质:
    • 正确性、可读性、健壮性、高效率与低存储量需求。

**算法的在位性(in-place):**算法排序时,不借助额外的存储空间,特别的存储单元除外。

欧几里得算法:

gcd(m,n)算法思想

①如果 n = 0 n=0 n=0,返回的 m m m值作为结果,同时过程结束;否则进入②;

m m m除以 n n n,将余数赋给 r r r

③将 n n n的值赋给 m m m,将 r r r的值赋给 n n n,返回①。

伪代码描述

Euclid(m,n)
//使用欧几里得算法计算gcd(m,n)
//输入:两个不全为0的非负整数m,n
//输出:m,n的最大公约数
while n ≠ 0 do
    r ← m mod n
    m ← n
    n ← r
return m

1.算法问题求解

  • 理解问题(如确定算法输入——所解问题的实例往往需要考虑”边界值“);
  • 确定精确解法与近似解法(一些重要问题很多情况下无法求得精确解,如求平方根等);
  • 算法的设计技术(也叫”策略“或者”范例“,如蛮力法、分治法等);
  • 确定适当的数据结构;
  • 算法的描述(算法思想文字描述、伪代码[pseudocode]或者流程图[flowchart]等);
  • 算法的正确性证明;
  • 算法分析(效率[efficiency]分析:时间效率和空间效率);
  • 代码编写。

exercise 1.求方程 a x 2 + b x + c = 0 ax^2+bx+c=0 ax2+bx+c=0的根

//求方程ax^2+bx+c=0的根;利用求根公式
#include 
#include 
int main()
{
	float a,b,c,dis,x1,x2;
	printf("请输入a,b,c的值: ");
	scanf("%f %f %f",&a,&b,&c);
	dis=b*b-4*a*c;
	if(dis<0)
	{
		printf("该函数无实根。\n");
		return 0;
	}
	else if(dis==0)
	{
		x1=x2=(-b)/(a*a);
		printf("该函数有两个相等的实根:\n");
	}
	else 
	{
		x1=(-b+sqrt(dis))/(2*a);
		x2=(-b-sqrt(dis))/(2*a);
		printf("该函数有两个不等的实根:");
	}
	printf("x1=%f x2=%f\n",x1,x2);
	return 0;}

exercise 2.整型转2进制

#include "stdio.h"
#include 
void dectobin(const long dec,char *pbin)
{
  long ys=0;  // 余数。
  int s=dec;  // 商。
  int ii=0;   // 位数的计数器。
  char result[65];  // 十进制转换成二进制后,保存在result中,再反过来存放到pbin中。
  memset(result,0,sizeof(result));
  // 把十进制转换为二进制,存放在result中。
  while (s>0)
  {
    ys=s%2;
    s=s/2;
    result[ii]=ys+'0';
    ii++;
  }
  // 再把result字符串反过来,存放在pbin中。
  int jj=0;
  for (;ii>0;ii--)
  {
    pbin[jj]=result[ii-1];
    jj++;
  }
  pbin[jj]=0; // 出于安全的考虑,加上0表示字符串结束。
}

int main()
{
  long ii=12;
  char str[65];
  dectobin(ii,str);
  printf("%d的二进制输出是:%s\n",ii,str);
}

2. 重要问题类型

  • 排序

​ 常常对数字、字符的列表;记录(如数据库对数据表中一条条数据)进行按照选定的信息(键,key)进行排序。

​ 通常,排序算法如果保证了等值元素在输入中的相对顺序,可以说它的稳定的(stable)

《算法设计与分析基础》【part1】_第1张图片
  • 查找

​ 在给定的集合中找一个给定的值(查找键,search key)。

​ 如果应用里的数据相对于查找次数频繁变化,查找问题需要考虑在数据集合中添加或删除元素的情况

  • 字符串处理(字符串匹配问题
  • 图问题
  • 组合问题(combinatorial problems)
  • 几何问题(geometric problems)

最近对(closest-pair)问题:给定平面上n个点中,距离最近的两个点;

凸包(convex-hull)问题:找一个能把给定集合中所有点都包含在内的最小凸边形。

  • 数值问题

涉及具有连续性的数学问题:解方程、计算定积分以及求函数值等;这类问题的特点是只能近似求解。

Chapter 2 算法效率分析基础

分析算法的时间复杂度空间复杂度

算法的输入规模

如对 n n n次多项式求值问题,这个参数是多项式的次数,或者是它系数的个数。

运行时间的度量单位

​ 对于大规模的输入,效率分析框架忽略乘法常量,而仅关注执行次数的增长次数(order of growth)及其常数倍。 T ( n ) ≈ c o p C ( n ) T(n)≈c_{op}C(n) T(n)copC(n),如 C ( n ) = 1 2 n ( n − 1 ) C(n)=\frac{1}{2}n(n-1) C(n)=21n(n1)为执行基本操作的次数, T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)

增长次数
log ⁡ 2 n < n < n log ⁡ 2 n < n 2 < 2 n < n ! 因为 log ⁡ a n = log ⁡ a b log ⁡ b n , 可以忽略对数的底,简写为 log ⁡ n \log_{2}n < n < n\log_{2}n < n^2 < 2^n < n! \\ 因为\log_{a}n = \log_{a}b\log_{b}n,可以忽略对数的底,简写为\log{n} log2n<n<nlog2n<n2<2n<n!因为logan=logablogbn,可以忽略对数的底,简写为logn

1.渐进符号

非正式地来说

O ( g ( n ) ) O(g(n)) O(g(n))是增长次数小于等于 g ( n ) g(n) g(n)(及其常数倍, n → + ∞ n\rightarrow +\infty n+)的函数集合
n ∈ O ( n 2 ) , 100 n + 5 ∈ O ( n 2 ) , 1 2 n ( n − 1 ) ∈ O ( n 2 ) n \in O(n^2),100n+5 \in O(n^2),\frac{1}{2}n(n-1) \in O(n^2) nO(n2),100n+5O(n2),21n(n1)O(n2)

Ω ( g ( n ) ) \Omega(g(n)) Ω(g(n))代表增长次数大于等于 g ( n ) g(n) g(n)(及其常数倍, n → + ∞ n\rightarrow +\infty n+)的函数集合
n 3 ∈ Ω ( n 2 ) , 1 2 n ( n − 1 ) ∈ Ω ( n 2 ) , 但是 100 n + 5 ∉ O ( n 2 ) n^3 \in \Omega(n^2),\frac{1}{2}n(n-1) \in \Omega(n^2),但是100n+5 \notin O(n^2) n3Ω(n2),21n(n1)Ω(n2),但是100n+5/O(n2)
Θ ( g ( n ) ) \Theta(g(n)) Θ(g(n))代表增长次数等于 g ( n ) g(n) g(n)(及其常数倍, n → + ∞ n\rightarrow +\infty n+)的函数集合

2.非递归算法的数学分析

对数的性质:
l o g a x y = y log ⁡ a x a log ⁡ b x = x log ⁡ b a log ⁡ a x = log ⁡ b x log ⁡ b a = log ⁡ a b log ⁡ b x log_{a}x^{y} = y\log_{a}x \\ a^{\log_{b}x} = x^{\log_{b}a} \\ \log{a}x = \frac{\log_{b}x}{\log_{b}a} = \log_{a}b\log_{b}x logaxy=ylogaxalogbx=xlogbalogax=logbalogbx=logablogbx
组合:
一个 n 元素集合的排列数量: P ( n ) = n ! 一个 n 元素集合中 k 个元素的组合数: C n k = n ! k ! ( n − k ) ! 一个 n 元素集合的子集数: 2 n 一个n元素集合的排列数量:P(n)=n! \\ 一个n元素集合中k个元素的组合数:C_{n}^{k} = \frac{n!}{k!(n-k)!} \\ 一个n元素集合的子集数:2^n 一个n元素集合的排列数量:P(n)=n!一个n元素集合中k个元素的组合数:Cnk=k!(nk)!n!一个n元素集合的子集数:2n
重要求和公式:
∑ i = l u 1 = 1 + 1 + ⋯ + 1 ⏟ u − l + 1 ( u , l 为整数边界,且 l ≤ u ) ; ∑ i = 1 n 1 = n ∑ i = 1 n i = 1 + 2 + ⋯ + n = n ( n + 1 ) 2 ∑ i = 1 n i 2 = 1 2 + 2 2 + ⋯ + n 2 = n ( n + 1 ) ( 2 n + 1 ) 6 = 1 3 n 3 ∑ i = 1 n i k = 1 k + 2 k + ⋯ + n k ≈ 1 k + 1 n k + 1 ∑ i = 0 n a i = 1 + a + ⋯ + a n = a n + 1 − 1 a − 1 ( a ≠ 1 ) ; ∑ i = 0 n 2 i = 2 n + 1 − 1 ∑ i = 1 n i 2 i = 1 × 2 + 2 × 2 2 + ⋯ + n 2 n = ( n − 1 ) 2 n + 1 + 2 ∑ i = 1 n 1 i = 1 + 1 2 + ⋯ + 1 n ≈ ln ⁡ n + γ , 其中 γ ≈ 0.5772 … ( 欧拉常数 ) ∑ i = 1 n lg ⁡ i ≈ n lg ⁡ n 注意: ∑ i = 1 n a i = ∑ i = 1 m a i + ∑ i = m + 1 n a i ∑ i = l u ( a i − a i − 1 ) = a u − a l − 1 近似计算: ∫ l − 1 u f ( x ) d x ≤ ∑ i = l u f ( i ) ≤ ∫ l u + 1 f ( x ) d x , f ( x ) 非递减 ∫ l u + 1 f ( x ) d x ≤ ∑ i = l u f ( i ) ≤ ∫ l − 1 u f ( x ) d x , f ( x ) 非递增 \sum_{i=l}^{u}1 = \underbrace{1+1+ \dots +1}_{u-l+1} \quad (u,l为整数边界,且l\leq u); \sum_{i=1}^{n}1 = n \\ \sum_{i=1}^{n}i = 1+2+\dots+n = \frac{n(n+1)}{2} \\ \sum_{i=1}^{n}i^2 = 1^2 + 2^2 + \dots + n^2 = \frac{n(n+1)(2n+1)}{6} = \frac{1}{3}n^3 \\ \sum_{i=1}^{n}i^k = 1^k + 2^k + \dots + n^k ≈ \frac{1}{k+1}n^{k+1} \\ \sum_{i=0}^{n}a^i = 1 + a + \dots + a^n = \frac{a^{n+1}-1}{a-1}(a \neq 1);\sum_{i=0}^{n}2^i = 2^{n+1}-1 \\ \sum_{i=1}^{n}i 2^i = 1\times{2} + 2\times{2^2} + \dots + n{2^n} = (n-1)2^{n+1}+2 \\ \sum_{i=1}^{n}\frac{1}{i} = 1 + \frac{1}{2} + \dots + \frac{1}{n} ≈ \ln{n}+\gamma,其中\gamma≈0.5772\dots(欧拉常数) \\ \sum_{i=1}^{n}\lg{i} ≈ n\lg{n} \\ 注意:\sum_{i=1}^{n}a_{i} = \sum_{i=1}^{m}a_{i} + \sum_{i=m+1}^{n}a_{i} \qquad \sum_{i=l}^{u}(a_{i} - a_{i-1}) = a_{u} - a_{l-1} \\ 近似计算:\int_{l-1}^u f(x) dx \leq \sum_{i=l}^{u}f(i) \leq \int_{l}^{u+1} f(x) dx,f(x)非递减\\ \int_{l}^{u+1} f(x) dx \leq \sum_{i=l}^{u}f(i) \leq \int_{l-1}^{u} f(x) dx,f(x)非递增 i=lu1=ul+1 1+1++1(u,l为整数边界,且lu);i=1n1=ni=1ni=1+2++n=2n(n+1)i=1ni2=12+22++n2=6n(n+1)(2n+1)=31n3i=1nik=1k+2k++nkk+11nk+1i=0nai=1+a++an=a1an+11(a=1);i=0n2i=2n+11i=1ni2i=1×2+2×22++n2n=(n1)2n+1+2i=1ni1=1+21++n1lnn+γ,其中γ0.5772(欧拉常数)i=1nlginlgn注意:i=1nai=i=1mai+i=m+1naii=lu(aiai1)=aual1近似计算:l1uf(x)dxi=luf(i)lu+1f(x)dxf(x)非递减lu+1f(x)dxi=luf(i)l1uf(x)dxf(x)非递增

通用方案:

  • 输入规模确定;
  • 找出算法的基本操作(一般位于算法最内层循环中);
  • 检查基本操作的执行次数是否只依赖于输入规模;
  • 算法基本操作执行次数的求和表达式。

3.递归算法的数学分析

递归算法一般都有程序出口(递归停止条件)
递推方程: x ( n ) = x ( n − 1 ) + n , 其中 n > 0 初始条件: x ( 0 ) = 0 递推方程:x(n) = x(n-1) + n ,其中n > 0 \qquad 初始条件:x(0) = 0 递推方程:x(n)=x(n1)+n,其中n>0初始条件:x(0)=0
前向替换法(forward substutution):
递推式: x ( n ) = 2 x ( n − 1 ) + 1 ,其中 n > 1 x ( 1 ) = 1 前面几项: x ( 1 ) = 1 ; x ( 2 ) = 2 x ( 1 ) + 1 = 3 ; x ( 3 ) = 2 x ( 2 ) + 1 = 7 ; … 数学归纳法总结规律 ( 通项 ) : x ( n ) = 2 n − 1 , n = 1 , 2 , 3 … 递推式:x(n) = 2x(n-1)+1,其中n>1 \qquad x(1)=1 \\ 前面几项:x(1) = 1; \quad x(2) = 2x(1)+1 = 3; \quad x(3) = 2x(2)+1 = 7; \quad \dots \\ 数学归纳法总结规律(通项):x(n) = 2^n - 1, n=1,2,3\dots 递推式:x(n)=2x(n1)+1,其中n>1x(1)=1前面几项:x(1)=1;x(2)=2x(1)+1=3;x(3)=2x(2)+1=7;数学归纳法总结规律(通项)x(n)=2n1,n=1,2,3
反向替换法(backward substutution):
递推式: x ( n ) = x ( n − 1 ) + n , 其中 n > 0 用 n − 1 代替原式的 n : x ( n − 1 ) = x ( n − 2 ) + n − 1 原式即为: x ( n ) = x ( n − 2 ) + ( n − 1 ) + n … x ( n ) = x ( n − i ) + ( n − i + 1 ) + ( n − i + 2 ) + ⋯ + n 初始条件为 0 ,则需要 n − i = 0 ,即 i = n : x ( n ) = x ( 0 ) + 1 + 2 + ⋯ + n = n ( n + 1 ) 2 递推式:x(n) = x(n-1) + n ,其中n > 0 \\ 用n-1代替原式的n:x(n-1) = x(n-2) + n-1 \\ 原式即为:x(n) = x(n-2) + (n-1) + n \\ \dots \\ x(n) = x(n-i)+(n-i+1)+(n-i+2)+\dots+n \\ 初始条件为0,则需要n-i=0,即i=n:x(n)=x(0)+1+2+\dots+n = \frac{n(n+1)}{2} 递推式:x(n)=x(n1)+n,其中n>0n1代替原式的nx(n1)=x(n2)+n1原式即为:x(n)=x(n2)+(n1)+nx(n)=x(ni)+(ni+1)+(ni+2)++n初始条件为0,则需要ni=0,即i=nx(n)=x(0)+1+2++n=2n(n+1)

通用方案:

  • 输入规模确定;
  • 找出算法的基本操作;
  • 检查基本操作的执行次数,建立递推关系式以及相应初始条件;
  • 解递推式,确定解的增长次数。

4.算法分析中常见的递推类型

  • 减一法(decrease-by-one)

如对 n ! n! n!递归求值和插入排序,其时间效率递推方程一般形式如下:
T ( n ) = T ( n − 1 ) + f ( n ) , 其中 f ( n ) 代表把实例简化成一个更小的实例并将其扩展为更大实例的解所需时间 应用反向替换法: T ( n ) = T ( n − 1 ) + f ( n ) = T ( n − 2 ) + f ( n − 1 ) + f ( n ) = … = T ( 0 ) + ∑ j = 1 n f ( j ) T(n) = T(n-1)+f(n),其中f(n)代表把实例简化成一个更小的实例并将其扩展为更大实例的解所需时间 \\ 应用反向替换法: \begin{aligned} T(n) &= T(n-1) + f(n) \\ &= T(n-2) + f(n-1) + f(n) \\ &= \dots \\ &= T(0)+\sum_{j=1}^{n}f(j) \end{aligned} T(n)=T(n1)+f(n),其中f(n)代表把实例简化成一个更小的实例并将其扩展为更大实例的解所需时间应用反向替换法:T(n)=T(n1)+f(n)=T(n2)+f(n1)+f(n)==T(0)+j=1nf(j)

  • 减常因子(decrease-by-constant-factor)

将规模为 n n n一个实例化简为一个规模为 n b ( b > 1 ) \frac{n}{b}(b>1) bnb>1的实例求解:如折半查找、平方求幂、俄式乘法和假币问题。
递推式: T ( n ) = T ( n b ) + f ( n ) , 其中 b > 1 按照 n = b k 求解: T ( b k ) = T ( b k − 1 ) + f ( b k ) = T ( b k − 2 ) + f ( b k − 1 ) + f ( b k ) = … = T ( 1 ) + ∑ j = 1 k f ( b j ) 若 f ( n ) = 1 : ∑ j = 1 k f ( b j ) = k = log ⁡ b n 若 f ( n ) = n : ∑ j = 1 k f ( b j ) = ∑ j = 1 k b j = b b k − 1 b − 1 = b n − 1 b − 1 递推式:T(n)=T(\frac{n}{b})+f(n) ,其中b>1 \\ 按照n=b^k求解:\begin{aligned} T(b^k) &= T(b^{k-1})+f(b^k) \\ &= T(b^{k-2})+f(b^{k-1})+f(b^k) \\ &= \dots \\ &= T(1)+\sum_{j=1}^{k}f(b^{j}) \end{aligned} \\ 若f(n)=1:\sum_{j=1}^{k}f(b^j) = k = \log_{b}n \\ 若f(n)=n:\sum_{j=1}^{k}f(b^j) = \sum_{j=1}^{k} b^j = b \frac{b^k - 1}{b-1} = b \frac{n-1}{b-1} 递推式:T(n)=T(bn)+f(n),其中b>1按照n=bk求解:T(bk)=T(bk1)+f(bk)=T(bk2)+f(bk1)+f(bk)==T(1)+j=1kf(bj)f(n)=1:j=1kf(bj)=k=logbnf(n)=n:j=1kf(bj)=j=1kbj=bb1bk1=bb1n1

  • 分治法(divide-and-conquer)

将给定规模为n的实例化简为若干个规模为 n b ( b > 1 ) \frac{n}{b}(b>1) bnb>1的实例求解,a个实例(子问题个数)都递归求解;若有必要,再将较小实例的解合并成给定实例的一个解。
递推式: T ( n ) = a T ( n b ) = f ( n ) , 其中 a ≥ 1 , b ≥ 2 ( 若 a = 1 , 属于减常因子而非分治法; f ( n ) 表示合并消耗时间 ) T ( b k ) = a T ( b k − 1 ) = f ( b k ) = a [ a T ( b k − 2 ) + f ( b k − 1 ) ] + f ( b k ) = … = a k [ T ( 1 ) + ∑ j = 1 k f ( b j ) a j ] 其中 a k = a l o g b n = n l o g b a 递推式:T(n)=aT(\frac{n}{b})=f(n),其中a≥1,b≥2 \quad (若a=1,属于减常因子而非分治法;f(n)表示合并消耗时间) \\ \begin{aligned} T(b^k) &= aT(b^{k-1}) = f(b^k) \\ &= a[aT(b^{k-2})+f(b^{k-1})]+f(b^k) \\ &= \dots \\ &= a^k[T(1)+\sum_{j=1}^{k}\frac{f(b^{j})}{a^j}] \end{aligned} \qquad 其中a^k = a^{log_{b}n} = n^{log_{b}a}\\ 递推式:T(n)=aT(bn)=f(n),其中a1,b2(a=1,属于减常因子而非分治法;f(n)表示合并消耗时间)T(bk)=aT(bk1)=f(bk)=a[aT(bk2)+f(bk1)]+f(bk)==ak[T(1)+j=1kajf(bj)]其中ak=alogbn=nlogba

  • 主定理(对 O O O Ω \Omega Ω同样成立)

设 T ( n ) 是一个最终非递减函数,且满足递推式 T ( n ) = a T ( n b ) + f ( n ) 其中 n = b k , k = 1 , 2 … T ( 1 ) = c a ≥ 1 , b ≥ 2 , c > 0 如果 f ( n ) ∈ Θ ( n d ) , d ≥ 0 ,那么 T ( n ) ∈ { Θ ( n d ) a < b d Θ ( n d log ⁡ n ) a = b d Θ ( n log ⁡ b a ) a > b d 设T(n)是一个最终非递减函数,且满足递推式 \\ T(n)=aT(\frac{n}{b})+f(n) \qquad 其中n=b^k,k=1,2\dots \\ T(1)=c \qquad a≥1,b≥2,c>0 \\ 如果f(n) \in \Theta(n^d),d≥0,那么 T(n) \in \begin{cases} \Theta(n^d) \qquad ab^d \end{cases} T(n)是一个最终非递减函数,且满足递推式T(n)=aT(bn)+f(n)其中n=bk,k=1,2T(1)=ca1b2c>0如果f(n)Θ(nd)d0,那么T(n) Θ(nd)a<bdΘ(ndlogn)a=bdΘ(nlogba)a>bd

Chapter 3 蛮力法

引例: a n = a × a × ⋯ × a ⏟ n 次 a^n=\underbrace{a\times{a}\times \dots \times{a}}_{n次} an=n a×a××a

1.选择排序和冒泡排序

选择排序

算法思想:拆分顺序表为【有序部分|待排序部分】,每轮排序确定一个元素的最终位置。首先在未排序序列中找到最小(大)元素,存放到有序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法分析:最差,最好及平均情况下时间复杂度都为 O ( n 2 ) O(n^2) O(n2),键的交换次数为 O ( n ) O(n) O(n),准确来说是 n − 1 n-1 n1次。

《算法设计与分析基础》【part1】_第2张图片

//选择排序
void SelectSort(char s[],int len)
{
	int i,j,min;
	for(i=0;i<len-1;i++){  // 0 to len-2
        min = i;
        for (j=i+1;j<len;j++){
            if(s[min]>s[j]) min = j;
        }
        if(min != i){
            char t=s[min];
            s[min]=s[i];
            s[i]=t;
        }
    }
}

冒泡排序

算法思想:**每轮排序确定一个元素的最终位置。**对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序。

算法分析:最差,平均情况下时间复杂度都为 O ( n 2 ) O(n^2) O(n2),最好情况下为 O ( n ) O(n) O(n)

《算法设计与分析基础》【part1】_第3张图片

public int[] bubbleSort(int[] arr) {
    for (int i = 0; i < arr.length-1; i++) { //比较趟数
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

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

顺序查找

算法思想:将给定列表中的连续元素和给定的查找键进行比较,直到遇到一个匹配的元素(成功查找);或者在遇到匹配元素之前就遍历了整个列表(查找失败)。

算法分析:最坏和平均时间复杂度都为O(n);最好情况是待查找元素在数组第一个位置,为O(1)。

int SequentialSearch(int *A,int n,int k){
// 输入:数组A[0...n-1],待查找元素
// 输出:若查找成功,返回对应数组下标;查找失败返回-1
	int i = 0;
	while(A[i] != k & i < n){
		i += 1;}
	if(i < n){return i;}
	else{return -1;}
}

蛮力字符串匹配

算法思想:给定一个n个字符组成的串,称为文本;一个 m ( m ≤ n ) m(m≤n) mmn个字符的串,称为模式

​ 将模式对准文本的前m个字符,然后从左到右匹配每一对相应的字符,直到m对字符全部匹配(算法就可停止了);或者遇到一对不匹配的字符。m对字符没有全部匹配时,模式向右移一位,然后从模式的第一个字符开始,继续把模式和文本中对应字符进行比较。

​ 在文本中,最后一轮子串匹配的起始位置是n-m(文本位置的下标是0到n-1),在这个位置以后,再也没有足够的字符可以匹配整个模式,算法可以停止。

算法分析:最好情况下,比较模式串的长度m,匹配成功,时间复杂度 O ( m ) O(m) O(m);最坏情况下,移动模式之前,算法可能会做足m次比较,而 n − m + 1 n-m+1 nm+1次尝试的每一次都可能比较m次然后匹配失败,因此时间复杂度为 ∑ i = 0 n − m ∑ j = 0 m − 1 1 = ∑ i = 0 n − m = m ( n − m + 1 ) = m n − m 2 + m = O ( m n ) \sum_{i=0}^{n-m}\sum_{j=0}^{m-1} 1 = \sum_{i=0}^{n-m} = m(n-m+1) = mn-m^2+m=O(mn) i=0nmj=0m11=i=0nm=m(nm+1)=mnm2+m=O(mn)

BruteForceStringMatch(char *T,int n,char *P,int m){
// 输入:一个n个字符的数组T[0...n-1] 代表文本;一个m个字符的数组P[0...m-1] 代表模式
// 输出:如果查找成功,返回文本的第一个匹配子串中第一个字符的位置,否则返回-1
    int i,j;
    for(i=0; i<n-m; i++){
        j=0;
        while(j<m&&P[j]==T[i+j]){ //字符串比较
            j++;
            if(j==m){return i+1;}//返回下标+1的值
        }
    }
    return -1;
}

KMP算法(时空权衡,预处理存储信息)

​ 蛮力字符串匹配算法的缺点是,当文本的某些子串与模式串能部分匹配时,主串和模式串的扫描指针经常回溯,导致时间开销增加。KMP算法是对蛮力字符串匹配算法的改进

算法思想:通过next数组的方式确定模式串失配时指针j回溯的位置。如果某个字符匹配成功,模式串首字符的位置保持不动,仅仅是i++、j++;如果匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。

计算模式串(“abcac”)后缀的最大公共长度:

模式串子串 前缀 后缀 最大公共字符串 最大公共字符串长度
a null null null 0
ab a b null 0
abc a,ab bc,c null 0
abca a,ab,abc bca,ca,a a 1
abcac a,ab,abc,abca bcab,cab,ab,b ab 2

next数组:

序号j 0 1 2 3 4
模式串 a b c a c
最大匹配 0 0 0 1 2
next[j] -1 0 0 0 1
//求模式串P的next数组
void cal_next(string &str, vector &next){	 
    int k =-1 , j = 0;
    next[0] = -1;
    len = str.size();
    while (j < len - 1)
    {	if (k == -1 || str[j] == str[k])
        {   ++k;++j;
            next[j] = k;//表示第j个字符有k个匹配(“最大长度值” 整体向右移动一位,然后初始值赋为-1)
        }   else k = next[k];//往前回溯
    }
}

// KMP算法
vector<int> KMP(string &str1, string &str2, vector<int> &next){	
    vector<int> vec;
    cal_next(str2, next);
    int i = 0;//i是str1的下标
    int j = 0;//j是str2的下标
    int str1_size = str1.size();
    int str2_size = str2.size();
    while (i < str1_size && j < str2_size)
    {   //如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),
        //都令i++,j++. 注意:这里判断顺序不能调换!
        if (j == -1 || str1[i] == str2[j])
        {  ++i;++j;
        }else
            j = next[j];//当前字符匹配失败,直接从str[j]开始比较,i的位置不变
        if (j == str2_size)//匹配成功
        {    vec.push_back(i - j);//记录下完全匹配最开始的位置
            j = -1;//重置
        }
    }
    return vec;
}

算法分析:最好情况下,比较模式串的长度m,匹配成功。O(m);最坏情况下,当模式串首字符位于 i − j i-j ij的位置时才匹配成功,算法结束。所以,当文本串长度为n,模式串长度为m,那么匹配过程的时间复杂度为O(n),算上计算next数组的O(m)时间,因此整体时间复杂度为 O ( m + n ) O(m+n) O(m+n)

3.最近对和凸包问题

最近对

问题描述:假设所讨论的点是以标准笛卡儿坐标(二维)形式 ( x , y ) (x, y) (x,y)给出的。因此,在两个点 P i = ( x i , y i ) P_i=(x_i, y_i) Pi=(xi,yi) P j = ( x j , y j ) P_j=(x_j, y_j) Pj=(xj,yj)之间的距离是标准的欧几里德距离: d = ( x i − x j ) 2 + ( y i − y j ) 2 d=\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2} d=(xixj)2+(yiyj)2

求解过程:

  1. 分别计算每一对点之间的距离;
  2. 然后找出距离最小的那一对,为了避免对同一对点计算两次距离,只考虑 i < j i<j ij的那些点对 ( P i , P j ) (P_i, P_j) (Pi,Pj)

算法分析:输入规模为点的个数 n n n,基本运算为求平方、求和、比较;时间复杂度为 O ( n 2 ) O(n^2) O(n2)

int BruteForceClosestPoints(int n, int x[ ], int y[ ], int &index1, int &index2){
    //输入:一个n(n>=2)个点的列表  p,p1=(x1,y1)  ,…,pn=(xn,yn)   
    //输出:两个最近点的距离 
    minDist=+;  
    for (i=1; i<n; i++)    
        for (j=i+1; j<=n; j++){    
            d=(x[i]-x[j])* (x[i]-x[j])+(y[i]-y[j])* (y[i]-y[j]);  
            if (d<minDist) {     
                minDist=d;       
                index1=i;         
                index2=j;       
            }    
        }   
    return  minDist;}

凸包

凸集合:对于平面上的一个点的有限集合,如果以集合中任意两点P和Q为端点的线段上的点都属于该集合,则称该集合是凸集合。

《算法设计与分析基础》【part1】_第4张图片

**凸包(convex-hull):**一个点集S的凸包是包含S的最小凸集合,其中,最小是指S的凸包一定是所有包含S的凸集合的子集。 对于平面上n个点的集合S,它的凸包就是包含所有这些点(或者在内部,或者在边界上)的最小凸多边形。

凸包问题是为一个具有n个点的集合构造凸多边形的问题。为了解决凸包问题,需要找出凸多边形的顶点,这样的点称为极点。一个凸集合的极点应该具有这样性质:对于任何以凸集合中的点为端点的线段来说,极点不是这种线段中的点。

《算法设计与分析基础》【part1】_第5张图片

**蛮力法求解凸包问题的基本思想:**对于一个由n个点构成的集合S中的两个点 P i P_i Pi P j P_j Pj,当且仅当该集合中的其他点都位于穿过这两点的直线的同一边时(假定不存在三点同线的情况),他们的连线是该集合凸包边界的一部分。对每一对顶点都检验一遍后,满足条件的线段构成了该凸包的边界。

​ 在平面上,穿过两个点 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) ( x 2 , y 2 ) (x_2, y_2) (x2,y2)的直线是由方程 a x + b y = c ( 其中, a = y 2 − y 1 , b = x 1 − x 2 , c = x 1 y 2 − y 1 x 2 ) ax + by = c (其中,a=y_2-y_1, b=x_1-x_2, c=x_1y_2-y_1x_2) ax+by=c(其中,a=y2y1,b=x1x2,c=x1y2y1x2)定义的。

​ 这样一条直线把平面分成两个半平面:其中一个半平面中的点都满足 a x + b y > c ax + by>c ax+byc,另一个半平面中的点都满足 a x + b y < c ax + by<c ax+byc,因此,为了检验这些点是否位于这条直线的同一边,可以简单地把每个点代入方程 a x + b y = c ax + by = c ax+by=c,检验这些表达式的符号是否相同。

4.穷举查找

旅行商问题

哈密顿回路
《算法设计与分析基础》【part1】_第6张图片

蛮力法求解算法思想:对于给定的无向图 G = ( V , E ) G=(V, E) G=(V,E),首先生成图中所有顶点的排列对象 ( v i 1 , v i 2 , … , v i n ) (v_{i1}, v_{i2}, …, v_{in}) (vi1,vi2,,vin),然后依次考察每个排列对象是否满足以下两个条件:

  • 相邻顶点之间存在边,即 ( v i j , v i j + 1 ) ∈ E ( 1 ≤ j ≤ n − 1 ) (v_{ij}, v_{ij+1})∈E(1≤j≤n-1) (vij,vij+1)E1jn1
  • 最后一个顶点和第一个顶点之间存在边,即 ( v i n , v i 1 ) ∈ E (v_in, v_i1)∈E (vin,vi1)E,满足这两个条件的回路就是哈密顿回路。

《算法设计与分析基础》【part1】_第7张图片

算法分析:最坏情况下需要考察所有顶点的排列对象,其时间复杂性为 O ( n ! ) O(n!) O(n!)

背包问题

问题描述:给定n个重量为 w 1 , w 2 , … , w n w_1,w_2,\dots,w_n w1,w2,,wn,价值为 v 1 , v 2 , … , v n v_1,v_2,\dots,v_n v1,v2,vn的物品和承重为 W W W的背包,求这些物品中最有价值的子集,且要能够装到背包中。

0-1背包问题建模

《算法设计与分析基础》【part1】_第8张图片
《算法设计与分析基础》【part1】_第9张图片

算法分析:对于一个具有n个元素的集合,其子集数量是 2 n 2^n 2n,所以,不论生成子集的算法效率有多高,蛮力法都会导致一个 Ω ( 2 n ) Ω(2^n) Ω(2n)的算法。

分配问题

问题描述:假设有n个任务需要分配给n个人执行,每个任务只分配给一个人,每个人只分配一个任务,且第j个任务分配给第i个人的成本是 C [ i , j ] ( 1 ≤ i , j ≤ n ) C[i, j](1≤i , j≤n) C[i,j]1i,jn,任务分配问题要求找出总成本最小的分配方案。

5.图的深度优先查找和广度优先查找

DFS

基本思想:类似树的先序遍历过程

  • 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点 w 1 w_1 w1
  • 再从 w 1 w_1 w1 出发,访问与 w1邻接但还未被访问过的顶点 w 2 w_2 w2
  • 然后再从 w 2 w_2 w2 出发,进行类似的访问,…
  • 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止;
  • 接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。
    • 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;
    • 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
void DFS(AMGraph G, int v){        		//图G为邻接矩阵类型   
    cout<<v;  visited[v] = true;  		//访问第v个顶点   
    for(w = 0; w< G.vexnum; w++)  	//依次检查邻接矩阵v所在的行    
        if((G.arcs[v][w]!=0)&& (!visited[w]))    
            DFS(G, w);       //w是v的邻接点,如果w未访问,则递归调用DFS
}

void DFS(ALGraph G, int v){        		//图G为邻接表类型 
    cout<<v;  visited[v] = true;    		//访问第v个顶点    
    p= G.vertices[v].firstarc;     //p指向v的边链表的第一个边结点  
    while(p!=NULL){              	//边结点非空    
        w=p->adjvex;               	//表示w是v的邻接点     
        if(!visited[w])  DFS(G, w); 	//如果w未访问,则递归调用DFS    
        p=p->nextarc;                	//p指向下一个边结点 
    } 
} 

算法分析:用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
邻接表来表示图,虽然有 2 e 2e 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为 O ( n + e ) O(n+e) O(n+e)

总结:稠密图适于在邻接矩阵上进行深度遍历;稀疏图适于在邻接表上进行深度遍历。

BFS

基本思想:类似树的层次遍历过程

  • 在访问了起始点v之后,依次访问 v的邻接点;
  • 然后再依次访问这些顶点中未被访问过的邻接点;
  • 直到所有顶点都被访问过为止。

算法描述:

(1)从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。
(2)只要队列不空,则重复下述处理:
① 队头顶点u出队。
② 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。

**注意:**广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有回退的情况。因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。

void BFS (Graph G, int v){ //按广度优先非递归遍历连通图G  
    cout<<v; visited[v] = true;     		//访问第v个顶点  
    InitQueue(Q);              			//辅助队列Q初始化,置空     
    EnQueue(Q, v);            			//v进队   
    while(!QueueEmpty(Q)){   		//队列非空     
        DeQueue(Q, u);        		 //队头元素出队并置为u     
        for(w = FirstAdjVex(G, u); w>=0; w = NextAdjVex(G, u, w))    
            if(!visited[w]){               	//w为u的尚未访问的邻接顶点      
                cout<<w; visited[w] = true;	EnQueue(Q, w); //w进队     
            }//if   
    }//while
}//BFS 

算法分析:如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行( n 个元素),总的时间代价为 O ( n 2 ) O(n^2) O(n2)
用邻接表来表示图,虽然有 2 e 2e 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为 O ( n + e ) O(n+e) O(n+e)

Chapter 4 减治法

减治(decrease-and-conquer)技术利用了规模为n的原问题的解与较小规模的子问题的解之间具有关系
(1)原问题的解只存在于其中一个较小规模的子问题中(如二叉树查找);
(2)原问题的解与其中一个较小规模的解之间存在某种对应关系。
所以,只需求解其中一个较小规模的子问题就可以得到原问题的解。

自底向上,由“少”到“多”;自顶向下,递归。

1.插入排序、希尔排序和拓扑排序

插入排序(减一)

算法思想:每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

算法分析:最差,平均情况下时间复杂度都为 O ( n 2 ) O(n^2) O(n2),最好情况下为 O ( n ) O(n) O(n)

《算法设计与分析基础》【part1】_第10张图片

//直接插入排序
void InSort(char s[],int len){
    int i,j;    // 数组数据整体右移 空出s[0]留作监视哨  
    for(i=2;i<=len;i++)    //数组下标从2开始比较,s[0]做监视哨,s[1]一个数据认为已有序   
    {        s[0]=s[i];    //给监视哨赋值    
     j=i-1;    //要比较数组的最左边位     
     while(s[0]<s[j] && j>0)    
     {          
         s[j+1]=s[j];    //数据右移     
         j--;     
     }      
     s[j+1]=s[0];    //在确定的位置插入s[i]  
    }
}

希尔排序

算法思想:把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

算法分析: 希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。
《算法设计与分析基础》【part1】_第11张图片

//希尔排序
#include
#include
void ShellSort(char A[],int len)
{
	int i,j;
	int d;//d为增量
	char temp;//temp为临时变量
	for(d=len/2;d>0;d /= 2)
	{
		//增量为d的直接插入排序 
		for(i=d;i<len;i++)
		{
			char temp = A[i];
			for(j=i;j>=d && A[j-d]>temp;j-=d)
				A[j] = A[j-d];
			A[j] = temp;
		}
		printf("增量为%d的shellsort:",d);
		for(int k=0;k<len;k++){
			printf("%c",A[k]);
		}
		printf("\n");
	} 
}

void main(){
	int len = 17;
	char a[len];
	a[0]='S';a[1]='H';a[2]='E';a[3]='L';a[4]='L';a[5]='S';a[6]='O';a[7]='R';a[8]='T';
	a[9]='I';a[10]='S';a[11]='U';a[12]='S';a[13]='E';a[14]='F';a[15]='U';a[16]='L';
	ShellSort(a,len);
}

拓扑排序

基于DFS的应用以及减一技术

前提:拓扑排序的图须为有向无环图

2.生成组合对象的算法

生成排列

算法思想:假设已经生成了所有 ( n − 1 ) ! (n-1)! (n1)!个排列,可以把n插入到 n − 1 n-1 n1个元素的每一种排列中的n个位置中去,来得到问题规模为n的所有排列。按照这种方式生成的所有排列都是独一无二的,并且他们的总数应该是 n ( n − 1 ) ! = n ! n(n-1)!=n! n(n1)!=n!

{ 1 , 2 , 3 } \{1,2,3\} {1,2,3}生成排列的过程(自底向上,满足最小变化要求)

开始 1
插入2 12 21
插入3 123 132 312 321 231 213

Johnson-Trotter算法

JohnsonTrotter(n)
    //输入:一个正整数n
    //输出:{1,…,n}的所有排列将第一个排列初始化为(←1)(←2)…(←n)  箭头在上
    while 存在一个移动元素 do    
        求最大的移动元素k  
        把k和它箭头指向的相邻元素互换    
        调转所有大于k的元素的方向   
        将新排列添加到列表中

算法分析:算法时间复杂性为 O ( n ! ) O(n!) O(n!),也就是说和排列对象的数量成正比。这不是算法的问题,而是该问题本身的复杂性。

递归算法生成排列对象

void print_permutation(int n,int A[],int cur){ 
    if(cur==n){     
        for (i=0; i<n; i++) 
            printf(%d”,A[i]);    //递归的边界   
        printf(“\n”);    
        else     
            for(i=1;i<=n;i++){     //尝试在A[cur]中填各种整数I       
                int ok=1;          
                for (j=0; j<cur; j++)            
                    if(A[j]==cur) ok=0;  //如果i已经在A[0]-A[cur-1]出现过,则不能再选      
                if(ok){ 
                    A[cur]=i;            
                    print_permutation(n,A,cur+1); //递归调用             
                      }       
            }
}

生成子集

算法思想:n个元素的集合 A = { a 1 , a 2 , … … , a n } A=\{a1, a2,……, an\} A={a1,a2,……,an}的所有 2 n 2n 2n个子集和长度为n的所有 2 n 2n 2n个比特串之间的一一对应关系;每一个子集指定一个比特串 b 1 b 2 … b n b_1b_2…b_n b1b2bn,如果 a i a_i ai属于该子集,则 b i = 1 b_i=1 bi1;如果 a i a_i ai不属于该子集,则 b i = 0 ( 1 ≤ i ≤ n ) b_i=0(1≤i≤n) bi01in

比特串 000 001 010 011 100 101 110 111
子集 ∅ \emptyset { a 3 } \{a_3\} {a3} { a 2 } \{a_2\} {a2} { a 2 , a 3 } \{a_2,a_3\} {a2,a3} { a 1 } \{a_1\} {a1} { a 1 , a 3 } \{a_1,a_3\} {a1,a3} { a 1 , a 2 } \{a_1,a_2\} {a1,a2} { a 1 , a 2 , a 3 } \{a_1,a_2,a_3\} {a1,a2,a3}

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

BRGC(n)//递归生成n位的二进制反射格雷码  n位元格雷码是基于n-1位元格雷码产生的
    //输入:一个正整数n
    //输出:所有长度为n的格雷码位串列表   
    if n=1,表L中包含位串0和位串1  
        else 调用BRGC(n-1)生成长度为n-1的位串列表L1  
            把表L1倒序后复制给表L2    
            把0加到表L1中的每个位串前面   
            把1加到表L2中的每个位串前面   
            把表L2添加到表L1后面得到表L1  
            return L

算法分析:算法生成了 2 n 2^n 2n个位串,并且全部位串都是不同的。应用在数字信号传输中,可降低误差影响。二进制反射格雷码是循环的(它的最后一个位串与第一个位串只相差一位)。

递归算法实现二进制反射格雷码的生成

/** 递归生成二进制格雷码  * 思路:1、获得n-1位生成格雷码的数组 
*      2、由于n位生成的格雷码位数是n-1的两倍,故只要在n为格雷码的前半部分加0,后半部分加1即可。 
* @param n 格雷码的位数 
* @return 生成的格雷码数组 */  
public static String[] GrayCode(int n) {    //数组的大小是2的n次方,因为n位的格雷码有2的n次方种排列   
    String[] grayCodeArr = new String[(int)Math.pow(2, n)];     
    if(n < 1){ System.out.println("你输入的格雷码位数有误!"); }       
    if(1 == n){         
        grayCodeArr[0] = "0";
        grayCodeArr[1] = "1";     
        return grayCodeArr;  
    }        
    //n-1 位格雷码的生成方式    
    String[] before = GrayCode(n-1);    
    for(int i = 0 ; i < before.length ; i++){     
        grayCodeArr[i] = "0" + before[i]; // 从前往后给每个位串之前拼接"0"  
        grayCodeArr[grayCodeArr.length -1 - i] = "1" + before[i];  // 从后往前给每个位串之前拼接"1"     
    }      
    return grayCodeArr;   
}

非递归算法实现二进制反射格雷码的生成

public static String[] GrayCode2(int n){ 
    int num = (int)Math.pow(2, n);//根据输入的整数,计算出此Gray序列大小 
    String[] s1 = {"0","1"};//第一个Gray序列   
    if(n < 1){ 
        System.out.println("你输入的格雷码位数有误!"); 
    }    
    for(int i=2;i<=n;i++){//循环根据第一个Gray序列,来一个一个的求     
        int p = (int)Math.pow(2, i);//到了第几个的时候,来计算出此Gray序列大小     
        String[] si = new String[p];     
        for(int j=0;j<p;j++){//循环根据某个Gray序列,来一个一个的求此序列     
            if(j<(p/2)){         
                si[j] = "0" + s1[j];//原始序列前面加上"0"      
            }else{       
                si[j] = "1" + s1[p-j-1];//原始序列反序,前面加上"1"     
            }     
        }     
        s1 = si;//把求得的si,附给s1,以便求下一个Gray序列  
    }  
    return s1;
}

3.减常因子算法

折半查找(常因子为2,减半)和排序

算法思想:折半查找通过比较待查找键k和有序表中间元素A[m]来完成查找工作,如果k==A[m],则查找成功,算法结束;否则当k>A[m],对有序表的后半部分执行折半查找操作;当k

算法分析:当 n > 1 n>1 n>1时, C w o r s t ( n ) = C w o r s t ( ⌊ n / 2 ⌋ ) + 1 , C w o r s t ( 1 ) = 1 C_{worst}(n)=C_{worst}(\lfloor n/2 \rfloor)+1, C_{worst}(1)=1 Cworst(n)=Cworst(⌊n/2⌋)+1,Cworst(1)=1

所以当 n = 2 k n=2^k n=2k时, C w o r s t = ⌊ l o g 2 n ⌋ + 1 C_{worst}=\lfloor log_2{n} \rfloor+1 Cworst=log2n+1;平均时间复杂度为 C a v g ≈ log ⁡ 2 n C_{avg} ≈ \log_{2}{n} Cavglog2n

// 减治法的应用:折半查找
void BinarySearch(A[], x, low, high){    //A为有序数组 x为待查找元素,low和high为数组上下界  
    if low > high return -1; 
    else   
        int mid = (low+high)/2;//下取整   
    if x = A[mid]      
        return mid;  
    else if x < A[mid] // 递归左半边和右半边    
        return BinarySearch(A,x,l,mid-1);  
    else return BinarySearch(A,x,mid+1,high)
}
void Half_InsertSort(SqList *L){
    //折半插入排序 初始无序仍为O(n^2) 稳定
    //仅减少了比较的次数O(nlogn)
    int i,j,low,high,mid;	
    for(i=L->Length;i>=0;i--) //将线性表全部往后移一位 空出第一位做监视哨	
        L->data[i]=L->data[i-1];
    for(i=2;i<=L->Length;i++){ //依次将L[2]~L[Length]插入到前面已排序序列	
        L->data[0]=L->data[i];	
        low=1;high=i-1;	
        while(low<=high){//折半查找	
            mid=(high+low)/2;		
            if(L->data[mid]>L->data[0]) high=mid-1;		
            else low=mid+1;	
        }		
        for(j=i-1;j>=high+1;--j)	
            L->data[j+1]=L->data[j];  //后移	
        L->data[high+1]=L->data[0];		
    }//显示	
    for(i=1;i<=L->Length;i++)	
        printf("%3d",L->data[i]);
    printf("\n");
}

假币问题

问题描述:在n枚外观相同的硬币中,有一枚是假币,并且已知假币较轻。可以通过一架天平来任意比较两组硬币,从而得知两组硬币的重量是否相同,或者哪一组更轻一些,但不知道轻多少,假币问题是要求设计一个高效的算法来检测出这枚假币。

​ 在假币问题中,每次用天平比较后,只需解决一个规模减半的问题。该算法在最坏情况下的时间性能有这样一个递推式: T ( n ) = { 0 n = 1 T ( n 2 ) + 1 n > 1 T(n)=\begin{cases}0 \qquad n=1 \\ T(\frac{n}{2})+1 \qquad n>1 \end{cases} T(n)={0n=1T(2n)+1n>1,可得 T ( n ) = O ( log ⁡ 2 n ) T(n)=O(\log_2{n}) T(n)=O(log2n)

改进算法思想:考虑不是把硬币分成两组,而是分成三组,前两组有 ⌈ n 3 ⌉ \lceil{\frac{n}{3}}\rceil 3n 组硬币,其余的硬币作为第三组,将前两组硬币放到天平上,如果他们的重量相同,则假币一定在第三组中,用同样的方法对第三组进行处理;如果前两组的重量不同,则假币一定在较轻的那一组中,用同样的方法对较轻的那组硬币进行处理。 T ( n ) = O ( log ⁡ 3 n ) T(n)=O(\log_3{n}) T(n)=O(log3n)

俄式乘法

问题描述:假设n和m是两个正整数,我们要计算它们的乘积,同时,我们用n的值作为实例规模的度量标准,则可以得到递推公式: n × m = { m n = 1 n 2 × 2 m n 为偶数 n − 1 2 × 2 m + m n 为奇数 n \times m=\begin{cases}m \qquad n=1 \\ \frac{n}{2} \times 2m \qquad n为偶数 \\ \frac{n-1}{2} \times 2m + m \qquad n为奇数 \end{cases} n×m= mn=12n×2mn为偶数2n1×2m+mn为奇数
《算法设计与分析基础》【part1】_第12张图片

4.减可变规模算法

计算中值和选择问题

问题描述:从给定的集合 L 中选择第 i 小的元素,不妨设 L 为 n 个不等的实数。

  • 输入:集合L(含n个不等的实数);

  • 输出:L中第i小元素

    • i=1, 称为最小元素;

    • i=n,称为最大元素;

    • i=n-1,称为第二大元素;

位置处在中间的元素,称为中位元素

  • 当 n为奇数时,中位数只有1个, i = ( n + 1 ) / 2 i=(n+1)/2 i=(n+1)/2
  • 当 n为偶数时,中位数有2个, i = n / 2 , n / 2 + 1 i=n/2, n/2+1 i=n/2,n/2+1, 也可以规定其中的一个。

找第k小的元素

#include 
#define N 1000
using namespace std;
int Kminselect(int a[],int s,int t,int k){ 
    int i=s,j=t,temp;   
    if(si && a[j]>=temp) j--; //从右边向中间扫描,直到a[j]temp      
            a[j]=a[i];                    //a[i]后移到a[j]的位置    
        }  
        a[i]=temp;    
        if(k-1==i) return a[i];    
        else if(k-1>n;  
    for(i=0;i>A[i];  
    cin>>k;    
    cout<

找最大最小

算法步骤:输入(n个数的数组L);输出(max,min)

1.将n个元素两两一组分成 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2

2.每组比较,得到 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2个较小和 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2个较大

3.在 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2个(n为奇数,是 ⌈ n / 2 ⌉ + 1 \lceil n/2 \rceil+1 n/2+1)较小中找最小min

4.在 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2 个(n为奇数,是 ⌈ n / 2 ⌉ + 1 \lceil n/2 \rceil+1 n/2+1)较大中找最大max

算法分析:行2 比较 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2次,行3–4 比较至多 2 ⌈ n / 2 ⌉ − 2 2 \lceil n/2 \rceil -2 2n/22

W(n) = ⌈ n / 2 ⌉ + 2 ⌈ n / 2 ⌉ − 2 = n + ⌈ n / 2 ⌉ − 2 = ⌈ 3 n / 2 ⌉ − 2 \lceil n/2 \rceil +2 \lceil n/2 \rceil -2 = n+\lceil n/2 \rceil -2 = \lceil 3n/2 \rceil -2 n/2+2n/22=n+n/22=3n/22

#include 
#include
#define N 1000
using namespace std;
void maxmin(int A[],int &e_max,int &e_min,int low,int high){ 
    int mid,x1,x2,y1,y2;  
    int *a,*b,*c,*d;  
    a=&x1;b=&x2;c=&y1;d=&y2;  
    if((high-low<=1))  
    {      
        if(A[high]>A[low])   
        {     
            e_max=A[high];    
            e_min=A[low];    
        }     
        else  
        {        
            e_max=A[low];        
            e_min=A[high];    
        }  
    }  
    else 
    {     
        mid=(low+high)/2;    
        maxmin(A,*a,*c,low,mid);   
        maxmin(A,*b,*d,mid+1,high);   
        e_max=(*a)>(*b)?(*a):(*b);    
        e_min=(*c)<(*d)?(*c):(*d);  
    }
}

int main(){  
    int A[N],i,e_max,e_min,n;   
    cin>>n;  
    for(i=0;i>A[i];    
    maxmin(A,e_max,e_min,0,n-1);  
    cout<

插值查找

​ 不同于折半查找总是把查找键和给定有序数组的中间元素进行比较,插值查找为了找到用来和查找键进行比较的数组元素,考虑了查找键的值。

二叉树的查找和插入

算法思想:二叉排序树的所有左子树的元素都小于子树根节点的元素,所有右子树的元素都大于子树根节点的元素。若要在这样一棵树中查找到一个给定值为k的元素时,递归以下步骤:

  • ① 若这棵树为空,则查找以失败结束。

  • ② 若这棵树不空,将k和该树的根T®进行比较。

    • 如果k==T®,查找成功,算法结束;
    • 如果它们不相等:
      • 当k
      • 当k>T®时,在右子树中继续查找。

算法的每次迭代,查找一棵二叉排序树的问题简化为查找一个更小的二叉查找树

// 递归查找算法如下
BSTNode* searchBST(BSTree &BT,int k){
    // 输入:二叉排序树 BT,待查找值value
    // 输出:若查找成功,返回对应的指针;否则返回NULL 
    if(BT==NULL)    
        return  NULL;  
    if(BT->data == k)     
        return BT;  
    if(BT->data > k)   
        return searchBST(BT->lchild,k);  
    if(BT->data < k)    
        return searchBST(BT->rchild,k);
}

算法分析:平均查找长度为 A S L = 1 n ∑ i = 1 m n i c i ASL = {1 \over n} \sum_{i=1}^{m}n_i c_i ASL=n1i=1mnici,其中 n i n_i ni二叉排序树每层的结点数, c i c_i ci是结点所在层数,m为树的深度;最好情况下为满二叉树, A S L 满 = 1 n ∑ i = 1 k 2 i − 1 × i ≈ log ⁡ 2 ( n + 1 ) ASL_满 = {1 \over n}\sum_{i=1}^k 2^{i-1}\times{i}≈\log_2{(n+1)} ASL=n1i=1k2i1×ilog2(n+1);最差情况下为斜二叉树, A S L 斜 = 1 n ∑ i = 1 n i = ( n + 1 ) / 2 ASL_斜 = {1 \over n}\sum_{i=1}^n i = (n+1)/2 ASL=n1i=1ni=(n+1)/2

Chapter 5 分治法

​ 将一个难以直接解决的大问题,划分成一些规模较小的子问题,以便各个击破,分而治之。更一般地说,**将要求解的原问题划分成k个较小规模的子问题,对这k个子问题分别求解。**如果子问题的规模仍然不够小,则再将每个子问题划分为k个规模更小的子问题,如此分解下去,直到问题规模足够小,很容易求出其解为止,再将子问题的解合并为一个更大规模的问题的解,自底向上逐步求出原问题的解。

分治法的求解过程:

①**划分:**把规模为n的原问题划分为k个规模较小的子问题,并尽量使这k个子问题的规模大致相同。

②**求解子问题:**各子问题的解法与原问题的解法通常是相同的,可以用递归(或者循环)的方法求解各个子问题。

③**合并:**把各个子问题的解合并起来,合并的代价因情况不同有很大差异,分治算法的有效性很大程度上依赖于合并的实现。

注意:不是所有的分治法都比简单的蛮力法更有效。

1.快速排序

算法思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

算法描述:快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
《算法设计与分析基础》【part1】_第13张图片

《算法设计与分析基础》【part1】_第14张图片

int Partition(SqList *L,int low,int high){
//快速排序的划分算法
	ElemType pivot=L->data[low];//将表中第一个元素设为枢纽值 划分表
	while(low<high){
		while(low<high&&L->data[high]>=pivot) --high;
		L->data[low]=L->data[high];  //将比枢纽值小的元素移动到左端
		while(low<high&&L->data[low]<=pivot) ++low;
		L->data[high]=L->data[low];  //将比枢纽值大的元素移动到右端
	}
	L->data[low]=pivot;  //枢纽元素存放到最终位置
		return low;
}
void QuickSort(SqList *L,int low,int high){
//快速排序 时间复杂度为O(nlogn){最坏为O(n^2)}  空间复杂度为O(logn){递归栈深度}
	int pivotpos;
	if(low<high){
		pivotpos=Partition(L,low,high);
		QuickSort(L,low,pivotpos-1);  //对两个子表进行递归排序
		QuickSort(L,pivotpos+1,high);
	}
}

算法分析:最好情况是数组乱序,且所有的分裂点位于相应子数组的中点;最差情况所有分裂点趋于极端,如升序数组的情况。
当 n > 1 时, C b e s t ( n ) = 2 C b e s t ( n / 2 ) + n C b e s t ( 1 ) = 0 根据主定理: C b e s t ( n ) = Θ ( n log ⁡ n ) 因为 a = b d 其中 a = 2 n = 2 k , b = 2 f ( n ) ∈ Θ ( n d ) ↔ n ,因此 d = 1 C w o r s t ( n ) = ( n + 1 ) + n + ⋯ + 3 ∈ Θ ( n 2 ) C a v g ( n ) ≈ 2 n ln ⁡ n ≈ 1.39 n log ⁡ 2 n 当n>1时,C_{best}(n) = 2C_{best}(n/2)+n \qquad C_{best}(1)=0 \\ 根据主定理:C_{best}(n) = \Theta(n\log{n}) \qquad 因为a=b^d \\ 其中a=2 \quad n=2^k,b=2 \quad f(n)\in \Theta(n^d) \leftrightarrow n,因此d=1 \\ C_{worst}(n) = (n+1) + n + \dots + 3 \in \Theta(n^2) \\ C_{avg}(n) ≈ 2n\ln{n} ≈ 1.39n\log_2{n} n>1时,Cbest(n)=2Cbest(n/2)+nCbest(1)=0根据主定理:Cbest(n)=Θ(nlogn)因为a=bd其中a=2n=2kb=2f(n)Θ(nd)n,因此d=1Cworst(n)=(n+1)+n++3Θ(n2)Cavg(n)2nlnn1.39nlog2n

2.归并排序

算法思想: 先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

算法分析:归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O ( n l o g n ) O(nlogn) O(nlogn)的时间复杂度。代价是需要额外的内存空间
当 n > 1 时, C w o r s t ( n ) = 2 C w o r s t ( n / 2 ) + n − 1 其中 C m e r g e = n − 1 根据主定理: C w o r s t ( n ) = Θ ( n log ⁡ n ) 因为 a = b d 其中 a = 2 n = 2 k , b = 2 f ( n ) ∈ Θ ( n d ) ↔ C m e r g e = n − 1 ,因此 d = 1 当n>1时,C_{worst}(n) = 2C_{worst}(n/2)+n-1 \qquad 其中C_{merge}=n-1 \\ 根据主定理:C_{worst}(n) = \Theta(n\log{n}) \qquad 因为a=b^d \\ 其中a=2 \quad n=2^k,b=2 \quad f(n)\in \Theta(n^d) \leftrightarrow C_{merge}=n-1,因此d=1 n>1时,Cworst(n)=2Cworst(n/2)+n1其中Cmerge=n1根据主定理:Cworst(n)=Θ(nlogn)因为a=bd其中a=2n=2kb=2f(n)Θ(nd)Cmerge=n1,因此d=1

每次合并操作的平均时间复杂度为 O ( n ) O(n) O(n),而完全二叉树的深度为 ⌈ l o g 2 n ⌉ \lceil log2n \rceil log2n。归并排序的最好,最坏,平均时间复杂度均为 O ( n l o g n ) O(nlogn) O(nlogn)

《算法设计与分析基础》【part1】_第15张图片

《算法设计与分析基础》【part1】_第16张图片

public class MergeSort {
	public static void main(String[] args) {
		int[] arrays = new int[] { 11, 3, 29, 49, 30, 7, 50, 63, 46 };
		System.out.println("未排序的数组:" + Arrays.toString(arrays));
		mergesort(arrays);
		System.out.println("排序后的数组:" + Arrays.toString(arrays));
	}
 
	public static void mergesort(int[] arr) {
         // 在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
		int[] temp = new int[arr.length];
		sort(arr, 0, arr.length - 1, temp);
	}
 
	private static void sort(int[] arr, int left, int right, int[] temp) {
		if (left < right) {
			int mid = (left + right) / 2;
			sort(arr, left, mid, temp);// 左边归并排序,使得左子序列有序
			sort(arr, mid + 1, right, temp);// 右边归并排序,使得右子序列有序
			merge(arr, left, mid, right, temp);// 将两个有序子数组合并操作
		}
	}
 
	private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
		int i = left;// 左序列指针
		int j = mid + 1;// 右序列指针
		int t = 0;// 临时数组指针
		while (i <= mid && j <= right) {
			if (arr[i] <= arr[j]) {
				temp[t++] = arr[i++];
			} else {
				temp[t++] = arr[j++];
			}
		}
		while (i <= mid) {// 将左边剩余元素填充进temp中
			temp[t++] = arr[i++];
		}
		while (j <= right) {// 将右序列剩余元素填充进temp中
			temp[t++] = arr[j++];
		}
		t = 0;
		// 将temp中的元素全部拷贝到原数组中
		while (left <= right) {
			arr[left++] = temp[t++];
		}
	}
}

3.分治法改进策略

减少子问题个数

分治算法的时间复杂度 W ( n ) = a W ( n ⁄ b ) + f ( n ) W(n)=aW(n⁄b)+f(n) W(n)=aW(nb)+f(n)a:子问题数,n⁄b:子问题规模,f(n):划分与综合工作量

当a较大,b较小,d(n)不大时,方程的解: W ( n ) = θ ( n log ⁡ b a ) W(n)=θ(n^{\log b^a}) W(n)=θ(nlogba),因此减少a是降低函数W(n)的阶的途径

增加预处理:可减少 f ( n ) f(n) f(n),如处理最近对问题中,对平面中的点按 x x x y y y轴坐标升序排列。

不应使用分治算法的情况

  • 一个规模为n的实例被划分成两个或多个实例,而每个实例的规模仍然几乎为n;
  • 一个规模为n的实例被划分为差不多n个规模为 n / c n/c n/c的实例,其中c为常量。(fabonacci)

4.平面点集的凸包问题

问题描述:给定大量离散点的集合Q,求一个最小的凸多边形,使得Q中的点在该多边形内或者边上。

应用背景: 图形处理中用于形状识别,如字形识别、碰撞检测等。

《算法设计与分析基础》【part1】_第17张图片
《算法设计与分析基础》【part1】_第18张图片

P.S 1

问题描述:设A和B都是从小到大排好序的的n个不等的数构成的数组,如果把A与B合并后的数组记住C,设计一个算法找出C的中位数。
《算法设计与分析基础》【part1】_第19张图片

P.S 2

问题描述:有n个人,其中有些人是诚实的,其他人可能会说谎。现在需要进行一项调查,该调查由一系列测试构成。每次测试如下进行:选两个人,然后提问:对方是否诚实?每个人的回答只能是“是”或者“否”。假定在这些人中,所有诚实的人回答都是正确的,而其他人的回答不能肯定是否正确。如果诚实的人数 > n / 2 >n/2 >n/2,试设计一个调查算法,以最小的测试数从其中找出一个诚实的人。

《算法设计与分析基础》【part1】_第20张图片

P.S 3

问题描述:考虑1,2,…,n的排列i1,i2,…in,如果其中存在ij,ik,使得j但是ij>ik,那么就称(ij,ik)是这个排列的一个逆序。一个排列含有逆序的个数称为这个排列的逆序数。例如排列2 6 3 4 5 1 含有8个逆序(2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1),它的逆序数就是8.显然由1,2,…,n构成的所有排列n!个排列中,最小的逆序数是0,对应的排列就是1 2… n;最大的逆序数是n(n-1)/2,对应的排列就是n (n-1) …2 1逆序数越大的排列与原始序列的差异度就越大。
《算法设计与分析基础》【part1】_第21张图片

你可能感兴趣的:(算法,算法,数据结构,排序算法,图搜索算法)