您的位置:首页 > 理论基础 > 数据结构算法

学习严蔚敏数据结构: 第二集

2009-03-05 14:09 204 查看
今天接昨天的算法:

1.3 算法及算法的衡量
1. 算法
算法是为了解决某类问题而规定的一个有限长的操作序列. 一个算法必须满足以下五个重要特征:
1). 有穷性: 对于任意一组合法输入值, 在执行有穷步骤之后一定能结束, 即: 算法中每个步骤都能在有限时间内完成
2). 确定性: 对于每种情况下所应执行的操作, 在算法中都有确切的规定, 使算法的执行者或阅读者都能明确其含义及如何执行. 并且在任何条件下, 算法都只有一条执行路径
3). 可行性: 算法是一个有限的操作序列, 每一个操作都必须足够基本, 都可以通过已经实现的基本操作运算有限次实现之
4). 有输入: 作为算法加工对象的量值, 通常体现为算法中的一组变量. 有些输入量需要在算法执行过程中输入, 而有的算法表面上可以没有输入, 实际上已被嵌入到算法中
5). 有输出: 它是一组与"输入"有确定关系的量值, 是算法进行信息加工后得到的结果, 这种确定关系即为算法的功能
2. 算法的设计原则
设计算法时, 通常应该考虑达到以下目标:
1). 正确性: 首先, 算法应当满足以特定的"规格说明"方式给出的需求.
其次, 对算法是否"正确"的理解可以有以下四个层次:
1>. 程序中不含语法错误;
2>. 程序对于几组输入数据能够得出满足要求的结果
3>. 程序对于精心选择的、 典型的、 苛刻且带有刁难性的几组输入数据能够得出满足要求的结果;
4>. 程序对于一切合法的输入数据都能得出满足要求的结果
通常以第三层意义的正确性作为衡量一个算法是否合格的标准
2). 可读性: 算法主要是为了人的阅读和交流, 其次才是计算机的执行. 因此算法应该易于人的理解; 另一方面, 晦涩难读的程序易于隐藏较多错误而难以调试
3). 健壮性: 当输入的数据非法时, 算法应恰当地做出反应或进行相应的处理, 而不是产生莫名其妙的输出结果.
并且, 处理错误的方法不应是中断程序的执行, 而应是返回一个表示错误或错误性质的值, 以便在更高的抽象层次上进行处理
4). 高效率与低存储量需求:
通常, 效率指定是算法执行时间; 存储量指的是算法执行过程中所需的最大的存储空间. 两者都和问题的规模有关
3. 算法效率的衡量方法和准则
通常有两种衡量算法效率的方法:
1). 事后统计法
缺点: 1>. 必须执行程序
2>. 其他因素掩盖算法本质
2). 事前分析估算法
和算法执行时间相关的因素:
1>. 算法选用的策略
2>. 问题的规模
3>. 编写程序的语言
4>. 编译程序产生的机器代码的质量
5>. 计算机执行指令的速度
一个特定算法的"运行工作量"的大小, 只依赖于问题的规模(通常用整数量n表示), 或者说, 它是问题规模的函数
假如, 随着问题规模n的增长, 算法执行时间的增值率和f(n)的增长率相同, 则可记作:
T(n) = O(f(n))
称T(n)为算法的(渐近)时间复杂度, 算法的执行时间T(n)随着n增大而增大的函数的曲线的趋势和f(n)函数的趋势是相同的,
我们称T(n)是和f(n)函数成正比, 或者说这个算法是f(n)这个函数数量级
算法 = 控制结构 + 原操作(固有数据类型的操作)
算法的执行时间 = ∑原操作(i)的执行次数 * 原操作(i)的执行时间
算法的执行时间与原操作的执行次数之和成正比
从算法中选取一种对于所研究的问题来说是基本操作的原操作, 以该基本操作在算法中重复执行的次数作为算法运行时间的衡量标准

例一:
for(i = 1; i <= n; ++i) //n次
{
for(j = 1; j <= n; ++j) //n次
{
c[i, j] = 0;
for(k = 1; k <= n; ++k) //n次
c[i, j] += a[i, k] * b[k, j];
}
}

基本操作: 乘法操作
时间复杂度: O(n³)

例二:
  // 将a中整数序列重新排列成自小到大有序的整数序列
void select_sort(int a[], int n)
{
int temp;
for(i = 0; i < n-1; ++i) // 外层循环确定a中每个元素的位置, n次
{
j = i; // 将未排序的最小位的位置记录在j中
for (k = i+1; k < n; ++k) // 内层循环将该位置之后的每位元素拿来和它做比较, 从而确定每个元素都是在自它之后的元素中为最小, 循环随i变化变化(n-1, n-2, ... 0, 1)次
{
if (a[k] < a[j]) // 如果比较位比当前位(本次循环中的要确定的元素的位置)元素小, 就将它的位置保存(也即j中保存的为当前循环位的最小值)
{
j = k;
}
}
if (j != i) // 如果当前位不是最小的(j中保存的最小位的位置, i保存的是当前位), 就交换它们, 确定当前位为最小
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}// 选择排序, 该算法只和问题规模有关, 与输入数据无关

基本操作: 比较(数据元素)操作
时间复杂度: 和(n²-n)/2成正比, 当然和n²成正比, 为O(n²)

语句的执行次数为频度

例三:
// 将a中整数序列重新排列成自小到大有序的整数序列
void bubble_sort(int a[], int n)
{
for (i = n-1, change = TRUE; i > 1 && change; --i)
{
change = FALSE; // 这个量是关键, 影响了时间复杂度
for (j = 0; j < i; ++j)
{
if (a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
change = TRUE; // 如果传入的数据是本来有序的, 此步不会执行, 那么外层循环只会执行1次
}
}
}
}// 冒泡排序, 此排序受输入数据的影响, 时间复杂度最低为n-1(内层的循环), 最高为(n²-n)/2

基本操作: 赋值操作
时间复杂度: 一般考虑最坏的时间复杂度O(n²)
算法的时间复杂度都是指的是最坏的情况下算法的渐近时间复杂度

4. 算法的存储空间需求
算法的空间复杂度
S(n) = O(g(n))
表示随着问题规模n的增大, 算法运行所需要存储量的增长率与g(n)的增长率相同
算法的存储量包含:
1). 输入数据所占空间
2). 程序本身所占空间(细微差别, 可以不比较)
3). 辅助变量所占空间
若输入数据所占空间只取决与问题本身, 和算法无关, 则只需要分析除输入和程序之外的辅助变量所占额外空间
若所需额外空间相对于输入数据量来说是常数, 则称此算法为原地工作, 其空间复杂度为O(1)

本章学习要点
1. 熟悉各名词、 术语的含义, 掌握基本概念
2. 理解算法五个要素的确切含义
3. 掌握计算语句频度和估算算法时间复杂度的方法

好吧, 我承认我听了之后有点晕...

咱们一点一点的分析额...

首先是算法的五个特征: 有穷性、 确定性、 可行性、 可输入、 和输出

基本概念, 没什么不理解的, 需要死记

其中确定性有点意思, 有点和我们写代码规范挂钩...可读性嘛 =.=

然后是算法的设计原则

正确性, 我记得程序错误分三种, 包括语法错误, 运行时错误, 逻辑错误; 这里的正确性就包含了这三种在内(我们写OO时, 方法也就是算法)

正确性中后面2, 3貌似都和测试挂钩, 2好像是单元测试的内容, 3就是黑盒测试的内容了

4 太理论了, 是种追求, 实际达不到, 所以一般黑盒测试(专门的测试)后, 软件也就可以交付了

可读性, 哈, 又和代码规范挂钩, 各位注意啊, 一定要代码规范, 不然会发生可怕滴事情...(什么事情? 我不告诉你)

健壮性, =.= 这不就是java的口号吗... 异常处理啊异常处理, 所以说, 平时要注意异常处理的正确嘛, 符合算法的设计原则不是?

高效率和低存储, 来了, 涉及到了关键的问题了, 算法的效率

为啥说这里是关键, 前面不是呢? 个人认为, 前面的目标的达成是相对容易的

算法效率的统计方法, 事后统计法, 个人觉得这种是最稳妥的方法, 虽然麻烦点;

因为只有在实际情况下, 才能真正的体会到算法的效率, 事前估算也只是估算, 毛主席他老人家教导我们要用事实说话嘛!

好吧, 由于实际条件比较难达成, 就有了第二种, 事前估算算法

这里面噔噔噔说了5点, 第3点一般是在软件设计情况下就给出了方案, 由不得我们改, 当然你要有这种选择编程语言的能力, 也就不会是普通程序员了

后两点为客户选择, 硬软件环境

值得我们普通程序员考虑的也就前面的1, 2两点, 算法的策略和问题的规模

下面就是用公式来表示了, 代数和几何都快忘完了, 看的真痛苦, 不过大致意思明白了

几个名词, 渐近时间复杂度, 渐近空间复杂度

个人理解计算算法时间复杂度就是, 1. 找出代表性的基本操作 2. 计算其执行的次数 3. 对于随输入变化的时间复杂度, 我们取最坏的情况

几个有代表性的例子: 三层循环为n的三方的时间复杂度, 选择排序, 冒泡排序

空间复杂度就是指这个算法总体消耗的存储空间的大小, 当然是越小越好

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: