第一章 什么是算法
算法与问题求解的基本概念
1. 什么是算法?
算法是解决问题的方法和过程,严格讲是满足下述性质的指令序列:
输入: 有零个或多个外部量作为算法的输入;
输出: 算法至少一个量作为输出;
确定性:组成算法的每条指令清晰、无歧义;
有限性:算法中每条指令的执行次数有限,执行每条指令的时间也有限。
* 算法效率的比较: 一层循环 > 递归方法 > 二层循环 > 三层循环
2. 什么是问题?
问题是需要我们回答的一般性提问,通常包含有若干参数,由问题求解描述,输入条件以及输出要求等要素组成。
问题实例则表示确定问题描述参数后的一个对象,一个问题可以包含若干个问题实例。
问题求解周期是指从一个问题的提出,到计算机可执行的、满足准确性和复杂性要求的程序算法的完成,可以看作是‘计算机问题求解的一个周期’。问题求解周期包括问题简化,模型构建,算法设计,程序设计与测试等过程。
主要步骤:
1. 抽象:对问题形式化描述,使得问题的定义是清晰的,无歧义的
2. 建模:对问题和数据分析和建模
3. 算法:设计有效的计算机算法
4. 实现:用特定的程序设计语言实现算法
5. 验证:测试、验证算法的正确性、性能以及推广
计算思维是运用计算机科学的基础概念进行问题求解、系统设计、以及人类行为理解等涵盖计算机科学之广度的一系列思维活动,计算思维是一种普适思维方法和基本技能。
3. 算法复杂度
- 算法复杂度是算法运行所需要的计算机资源的量,需要时间资源的量称为时间复杂性,需要的空间资源的量称为空间复杂性。
- 时间复杂度和空间复杂度是只依赖于算法求解的问题规模和算法输入的函数
- 影响算法执行时间的因素:计算机配置、问题规模、输入实例。时间复杂度不应该是在特定计算机上求解某一个输入示例所需要的运行时间,而应该是一个不依赖于计算机配置、问题规模和输入示例的抽象表示。
- 渐近复杂性,对于算法A的复杂度函数T(N),如果存在T'(N)使得当 N->无穷时有 (T(N)-T'(N))/T(N) -> 0,那么就称T'(N)为算法A当N->无穷时的渐近复杂性。
- 算法时间复杂性的大O表示方法以及运算法则:给定多项式函数,T(n) = akn^k + ak-1n^k + ......+ a1n + a0,则T(n) = O(n^k)
- 根据O的定义,容易证明它有如下运算规则:1. O(f) + O(g) = O(max(f, g));2. O(f) + O(g) = O(f+g);3. O(f)*O(g) = O(f*g);4. 如果 g(N) = O(f(N)), 则 O(f) + O(g) = O(f);5. O(C*F(N)) = O(f(N)),其中 C 是一个正的常数;6. f = O(f)。
- 时间复杂度分为两类:多项式复杂度,形如O(n^c);指数复杂度,形如:O(c^n)
- 时间复杂度的基本排序:O(1) < O(logn) < O(n) < O(n*logn) < O(n^2) < ... < O(2^n) < O(n!) < O(n^n)
4. 非递归程序的时间复杂度分析方法
(1)确定关键操作:可以看做是高级程序语言中的复制、比较、算术运算、逻辑运算、读写单个常量或单个变量等操作(一般被看做是基本操作,并约定所用的时间都是一个单位);也可以是由常数个基本操作构成的程序块。
(2)计算关键操作总的执行步数:一般是数列和的形式。
(3)求解渐近阶:用O(.)来表示。
4.1 实例1:给定数组 A(长度为n)和数 t,判断数组 A 中是否包含 t。
算法描述:
bool isExist(float*iArray, int n, float t)
{
for (int i = 0; i < n; i++)
{
if (iArray[i] == t)
return true;
}
return false;
}
上述算法中首先判断关键操作为循环体里的判断语句,执行次数为n次,则有 T(n) = n,因此时间复杂度为O(n)。
4.2 实例2:给定数组A和B(长度为n)和数 t,判断数组A和B中是否包含 t?
算法描述:
bool isExist(float*iArrayA, float*iArrayB, int n, float t)
{
for (int i = 0; i < n; i++)
{
if (iArrayA[i] == t) return true;
}
for (int i = 0; i < n; i++)
{
if (iArrayB[i] == t) return true;
}
return false;
}
上述算法中首先判断关键操作为循环体里的判断语句,执行次数为2*n次,则有 T(n) = 2*n,因此时间复杂度为O(n)。
4.3 实例3:给定数组A和B(长度为n),判断数组A和B中是否包含相同元素?
算法描述:
bool isCommon(float*iArrayA, float*iArrayB, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if iArrayA[i] == iArrayB[i]
return true;
}
}
return false;
}
上述算法中首先判断关键操作为循环体里的判断语句,执行次数为n^2次,则有 T(n) = n^2,因此时间复杂度为O(n^2)。
4.4 实例4:给定数组A(长度为n),判断数组A中是否包含重复元素?
bool isDuplicate(float*iArrayA int n)
{
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
if (iArrayA[i] == iArrayA[j])
return true'
}
return false;
}
上述算法中首先判断关键操作为循环体里的判断语句,执行次数(n^2-n)/2,则有 T(n) = (n^2-n)/2,因此时间复杂度为O(n^2)。
4.5 实例5:给定圣墟排列的整数数组A(长度为n)和整数x,判断数组A中是否包含元素x,如果存在则返回其位置,否则返回-1?
算法描述:
int BinarySearch(int*iArrayA, int n, int x)
{
int left = 0; int right = n-1;
while(left <= right)
{
int middle = (left + right) / 2;
if(x == iArrayA[middle]) return middle;
if(x > iArrayA[middle]) left = middle + 1;
else right = middle - 1;
}
return -1;
}
上述循环结构的 while 循环和之前的算法实例中的 for 循环有所不同,此算法为二分算法,相当于每次从一半数据中搜寻,每次剔除一半,则执行的次数为 T(n) = log2(n)(以2为底n的对数),则时间复杂度为 O(logn)。
5. 递归程序的时间复杂度分析方法
(1)分析递归程序的结构,确定每一逻辑块的时间复杂性,非递归的程序块(或者子函数)用非递归方法分析其复杂性;递归函数的复杂性则根据其输入规模递归地表示。
(2)构造复杂度函数的递推方程。
(3)求解递推方程方程,并用O(.)表示。
5.1 实例1:求整数n的阶乘
算法描述:
long FN(long n)
{
if (n <= 1)
{
return 1;
}
return n * FN(n - 1);
}
将递推方程表示出来则为: 当递归了 n 次时,其时间为 c' + (n-1)*c,可知时间复杂度为O (n)。
5.2 实例2:汉诺塔问题。三根杆子A/B/C杆上有N个穿孔圆盘。盘的尺寸由下到上依次变小。要求按照下列规则将所有圆盘移至C杆:其中每次只能移动一个圆盘,大盘不能叠加在小盘上面。
算法描述:
int Hanoi(int n, int a, int b, int c)
{
if (n <= 0) return 0;
Hanoi(n - 1, a, c, b);
Move(a, c);
Hanoi(n - 1, b, a, c);
return -1;
}
将递推方程表示出来为: 同理,化简可得该算法的时间复杂度为O(2^n)。
- 上述时间复杂度分析均为内存算法的时间复杂度分析,而在如今大数据的情景下,算法可能需要处理的数据规模是TB级别或以上,无法在内存中存储和处理所有数据;数据I/O将占用大部分的时间,内存是随机存储,外存是顺序存储,因此数据的存取将是时间复杂度的另一个重要的考虑因素。
- 分布式算法,在分布式情景下,数据迁移类的通信模块将占用大部分的时间。
6.小结
算法的直观体验和基本概念
1. 算法是解决问题的方法或过程,严格讲是满足特定性质的指令序列。
2. 了解相同问题的不同求解算法及差异。
3. 关注算法应用带来的风险。
计算机问题求解的基本概念
1. 问题与问题实例
2. 问题求解周期
算法时间复杂度的渐进性复杂性和O记号
1. 算法执行时间差异的因素:问题规模的不同、问题实例的不同、计算机配置的不同,因此选择直观定义度量算法的时间复杂性时不恰当的。
2. 渐进复杂性
3. O记号
非递归和递归算法的时间复杂性分析方法
1. 非递归算法:关键操作、数列求和
2. 递归算法的分析:递推方程