基于Linux的C++ Part 4: 算法

4.1 算法概念与特征

算法基本概念

  • 算法定义:解决问题的方法与步骤
  • 设计算法的目的:给出解决问题的逻辑描述,根据算法描述进行实际编程

算法特征

  • 有穷性:算法在每种情况下都可以在有限步后终止
  • 确定性:算法步骤的顺序和内容没有二义性
  • 输入:算法有零个或多个输入
  • 输出:算法至少具有一个输出
  • 有效性:所有操作具有明确含义,并能在有限时间内完成
    正确性不是算法的特征,算法的正确性需要数学证明

算法示例

  • 示例一:3*3 幻方
    在这里插入图片描述
    步骤1:把 1 写在第一行中间一格
    步骤2:在该格右上方的那一格中写入下一自然数
    在此过程中,若该数已超出 3 × 3 幻方范围,则将该数书写在其所在的那一排或列的另一端格子中,即相当于认为幻方外围仍然包含了同样的幻方,而该数就写在外围幻方的同样位置
    每写完三个数,将第四个数写在第三个数下面格子中
    步骤3:重复步骤2,直到所有格子均填满
  • 查英文单词
    步骤1:翻开词典任意一页
    步骤2:若所要的词汇按字母排列顺序在本页第一个单词之前,则往前翻开任意一页,重复步骤2;若所查词汇在本页最后一个单词之后,则往后翻开任意一页,重复步骤2
    步骤3:若上述两条件均不满足,则该单词要么在本页上,要么词典中不存在
    依次比较本页单词,或者查出该单词,或者得到该单词查不到的结论

4.2 算法描述

伪代码

  • 混合自然语言与计算机语言、数学语言的算法描述方法
  • 优点:方便,容易表达设计者思想,能够清楚描述算法流程,便于修改
  • 缺点:不美观,复杂算法不容易理解
  • 示例1:顺序结构
    执行某任务
    执行下一任务
  • 示例2:分支结构
 if( 条件表达式 )
 	处理条件为真的情况
 else 
 	处理条件为假的情况
switch( 条件变量 ){
case 常量表达式 1: 处理分支 1
case 常量表达式 2: 处理分支 2
……
default: 处理默认分支
}
  • 示例3:循环结构
for( 初始化表达式; 条件表达式; 步进表达式 ) || while( 条件表达式 )
{
循环体内部代码逻辑描述
}

流程图(程序框图)

  • 使用图形表示算法执行逻辑
  • 优点:美观,算法表达清晰
  • 缺点:绘制复杂,不易修改,占用过多篇幅
  • 常用流程图的框图与符号
    在这里插入图片描述
  • 示例1:幻方流程图
    在这里插入图片描述
  • 示例二: 查英文单词
    在这里插入图片描述

4.3 算法设计与实现

算法设计与实现步骤

  • 构造算法解决问题
  • 按照自顶向下、逐步求精的方式进行
  • 使用程序设计语言编程实现

典型示例1 :素性判定问题

判断给定的某个自然数 n(大于 2)是否为素数
算法逻辑:

  • 输入:大于 2 的正整数 n
  • 输出:该数是否为素数,若为素数返回 true,否则返回 false
  • 步骤 1:设除数 i 为 2
  • 步骤 2:判断除数 i 是否已为 n,若为真返回 true,否则继续
  • 步骤 3:判断 n % i 是否为 0,若为 0 返回 false,否则继续
  • 步骤 4:将除数 i 递增,重复步骤 2
素数判定函数方法一,循环次数多,效率非常低
bool IsPrime( unsigned int n )
{
unsigned int i = 2;
while( i < n )
{
if( n % i == 0 )
return false;
i++;
}
return true;
}
素数判定函数方法二:程序快于方法一,循环的遍历的次数由n降至sqrt(n)
bool IsPrime( unsigned int n )
{
unsigned int i = 2;
while( i <= (unsigned int)sqrt(n) )//若一个自然数n为和数,则一定存在在2~n之间的两个数p和q(p<=q),且p << sqrt(n)
{
if( n % i == 0 )//如果i能被n整除,则直接返回结果,n不是素数
return false;
i++;
}
return true;
}
素数判定函数方法三:更快,结合了方法一和方法二
bool IsPrime( unsigned int n )
{
unsigned int i = 3;//从3开始循环
if( n % 2 == 0 ) //如果这个数是偶数,则一定不是素数,直接返回结果
return false;
while( i <= (unsigned int)sqrt(n) )
{
if( n % i == 0 )
return false;
i += 2;//每次递增2,因为进入while循环之前已经判断奇偶性
}
return true;
}
素数判定函数方法四,更精确
bool IsPrime( unsigned int n )
{
unsigned int i = 3;
if( n % 2 == 0 )
return false;
while( i <= (unsigned int)sqrt(n) + 1 )//sqrt(n)保存的是浮点型,会有微小误差,会漏掉一些平方数
{
if( n % i == 0 )
return false;
i += 2;
}
return true;
}
素数判定函数方法五,更精确 更快
bool IsPrime( unsigned int n )
{
unsigned int i = 3, t = (unsigned int)sqrt(n) + 1;//只计算一次sqrt(n),程序就会变快,因为这里n不变。
if( n % 2 == 0 )
return false;
while( i <= t )
{
if( n % i == 0 )
return false;
i += 2;
}
return true;
}

算法选择

  • 算法选择的权衡指标
    正确性:算法是否完全正确?
    效率:在某些场合,对程序效率的追求具有重要意义
    可理解性:算法是否容易理解,也是必须要考虑的
  • 算法评估:
    衡量算法的好坏,主要是效率

典型示例2 :最大公约数问题

求两个正整数 x 与 y 的最大公约数
函数原型设计

unsigned int gcd( unsigned int x, unsigned int y );
最大公约数函数:穷举法 效率低
unsigned int gcd( unsigned int x, unsigned int y )
{
unsigned int t;
t = x < y ? x : y;
while( x % t != 0 || y % t != 0 )
t--;
return t;
}
最大公约数函数:欧氏算法*/
输入:正整数 x、y
输出:最大公约数
步骤 1:x 整除以 y,记余数为 r
步骤 2:若 r 为 0,则最大公约数即为 y,算法结束
步骤 3:否则将 y 作为新 x,将 r 作为新 y,重复上述步骤
unsigned int gcd( unsigned int x, unsigned int y )
{
unsigned int r;
while( true )
{
r = x % y;
if( r == 0 )
return y;
x = y;
y = r;
}
}

4.4 递归算法

递归问题

递归问题的引入

  • 递推公式:数学上非常常见
    例一:阶乘函数:1! = 1,n! = n × (n-1)!
    例二:斐波那契数列函数:f(1) = f(2) = 1,f(n) = f(n-1) + f(n-2)
  • 递推函数一定是分段函数,具有初始表达式
  • 递推函数的计算逻辑:逐步简化问题规模

递归的工作步骤

  • 递推过程:逐步分解问题,使其更简单
  • 回归过程:根据简单情形组装最后的答案

阶乘函数示例

使用循环实现
unsigned int GetFactorial( unsigned int n )
{
unsigned int result = 1, i = 0;
while( ++i <= n )
result *= i;
return result;
}
使用递归实现
unsigned int GetFactorial( unsigned int n )
{
unsigned int result;
if( n == 1 ) result = 1;
else result = n * GetFactorial( n - 1 );
return result;
}

斐波那契数列函数示例

使用循环实现
unsigned int GetFibonacci( unsigned int n )
{
unsigned int i, f1, f2, f3;
if( n == 2 || n == 1 ) return 1;
f2 = 1; f1 = 1;
for( i = 3; i <= n; i++ ){
f3 = f1 + f2; f1 = f2; f2 = f3;
}
return f3;
}
使用递归实现
unsigned int GetFibonacci( unsigned int n )
{
if( n == 2 || n == 1 ) return 1;
else return GetFibonacci( n - 1 ) + GetFibonacci( n - 2 );
}

循环与递归的比较

  • 循环使用显式的循环结构重复执行代码段,递归使用重复的函数调用执行代码段
  • 循环在满足其终止条件时终止执行,而递归则在问题简化到最简单情形时终止执行
  • 循环的重复是在当前迭代执行结束时进行,递归的重复则是在遇到对同名函数的调用时进行
  • 循环和递归都可能隐藏程序错误,循环的条件测试可能永远为真,递归可能永远退化不到最简单情形
  • 理论上,任何递归程序都可以使用循环迭代的方法解决
    递归函数的码更短小精悍
    一旦掌握递归的思考方法,递归程序更易理解

递归函数调用的栈框架

#include <iostream>
using namespace std;
void PrintWelcomeInfo();
unsigned int GetInteger();
unsigned int GetFactorial( unsigned int n );
int main()
{
unsigned int n, result;
PrintWelcomeInfo();
n = GetInteger();
result = GetFactorial( n );
cout << n << "! = " << result << ".\n";
return 0;
}
void PrintWelcomeInfo()
{
cout << "The program gets a number and computes the factorial.\n";
}
unsigned int GetInteger()
{
unsigned int t;
cout << "Input a non-negative number: ";
cin >> t;
return t;
}
unsigned int GetFactorial( unsigned int n )
{
unsigned int result;
if( n == 0 )
result = 1;
else
result = n * GetFactorial( n - 1 );
return result;
}
  • 函数调用栈框架说明
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

汉诺塔问题

假设有三个分别命名为 X、Y 和 Z 的塔座,在塔座 X 上插有 n 个直径大小不同、从小到大分别编号为 1, 2, …, n 的圆盘,如图所示:
在这里插入图片描述
要求将塔座X上的 n 个圆盘移动到塔座 Z 上并按相同顺序叠放,圆盘移动时必须遵循下述规则:

  • 每次只能移动一个圆盘;
  • 圆盘可以插在X、Y与Z中的任意塔座上;
  • 任何时刻都不能将较大的圆盘压在较小的圆盘上。

如何实现移动圆盘的操作呢?
问题分析
待解决的问题

  • Q1:是否存在某种简单情形,问题很容易解决
  • Q2:是否可将原始问题分解成性质相同但规模较小的子问题,且新问题的解
    答对原始问题有关键意义

解决思维

  • A1:只有一个圆盘时是最简单情形
  • A2:对于 n > 1,考虑 n – 1 个圆盘,如果能将 n - 1 个圆盘移动到某个塔
    座上,则可以移动第 n 个圆盘
    策略:首先将 n – 1 个圆盘移动到塔座 Y 上,然后将第 n 个圆盘移动到 Z 上,
    最后再将 n - 1 个圆盘从 Y 上移动到 Z 上

伪代码

void MoveHanoi( unsigned int n, HANOI from, HANOI tmp, HANOI to )
{
if( n == 1 )
将一个圆盘从 from 移动到 to
else
{
将 n – 1 个圆盘从 X 以 Z 为中转移动到 Y
将圆盘 n 从 X 移动到 Z
将 n - 1个圆盘从 Y 以 X 为中转移动到 Z
}
}

程序代码

#include <iostream>
using namespace std;
void Welcome();
int GetPlate();
void MoveHanoi(int n,char from,char tmp,char to);
void MovePlate(int n, char from, char to);
int main() {
	int plates;
	int sum=0;
	Welcome();
	plates = GetPlate();
	cout << "Move process: \n";
	MoveHanoi(plates, 'X', 'Y', 'Z');
	cout << "Finish!\n";
	return 0;
}

void Welcome() {
	cout << "This is a Tower of Hanoi problem!" << endl;
}

int GetPlate() {
	int n;
	cout << "Please input the counts of the plates: " << endl;
	cin >> n;
	cout << "You want to move " << n << " plates.\n";
	return n;
}

void MovePlate(int n, char from, char to) {
	cout << "No" << n << ": " << from << "  -  " << to << endl;
}

void MoveHanoi(int n, char from, char tmp, char to){
	if (n == 1) {
		MovePlate(1, from, to);
	}
	else {
		MoveHanoi(n - 1, from, to, tmp);
		MovePlate(n, from, to);
		MoveHanoi(n - 1, tmp, from, to);
	}
}

运行结果:
This is a Tower of Hanoi problem!
Please input the counts of the plates:
4
You want to move 4 plates.
Move process:
No1: X  -  Y
No2: X  -  Z
No1: Y  -  Z
No3: X  -  Y
No1: Z  -  X
No2: Z  -  Y
No1: X  -  Y
No4: X  -  Z
No1: Y  -  Z
No2: Y  -  X
No1: Z  -  X
No3: Y  -  Z
No1: X  -  Y
No2: X  -  Z
No1: Y  -  Z
Finish!

递归信任

  • 递归实现是否检查了最简单情形
    在尝试将问题分解成子问题前,首先应检查问题是否已足够简单
    在大多数情况下,递归函数以 if 开头
    如果程序不是这样,仔细检查源程序
  • 大量递归错误是由没有正确解决最简单情形导致的
    最简单情形不能调用递归
  • 递归分解是否使问题更简单
    只有分解出的子问题更简单,递归才能正确工作,否则将形成无限递
    归,算法无法终止
  • 问题简化过程是否能够确实回归最简单情形,还是遗漏了某些情况
    如汉诺塔问题需要调用两次递归过程,程序中如果遗漏了任意一个都
    会导致错误
  • 子问题是否与原始问题完全一致
    如果递归过程改变了问题实质,则整个过程肯定会得到错误结果
  • 使用递归信任时,子问题的解是否正确组装为原始问题的解
    将子问题的解正确组装以形成原始问题的解也是必不可少的步骤

4.5 容错与算法复杂度

容 错

  • 容错的定义:允许错误的发生
  • 错误的处理
    很少见的特殊情况或普通错误:忽略该错误不对程序运行结果产生影响
    用户输入错误:通知用户错误性质,提醒用户更正输入
    致命错误:通知用户错误的性质,停止执行
  • 典型容错手段
    数据有效性检查
    程序流程的提前终止
  • 数据有效性检查
void GetUserInput()
{
获取用户输入数据
while( 用户输入数据无效 )
{
通知用户输入数据有误,提醒用户重新输入数据
重新获取用户输入数据
}
}
void Input()
{
GetInputData();
while( !IsValid() )
{
OutputErrorInfo();
GetinputData();
}
}
  • 程序流程的提前终止
const int failed_in_testing_primality = 1;
bool IsPrime( unsigned int n )
{
unsigned int i = 3, t = (unsigned int)sqrt(n) + 1;
if( n <= 1 )
{
cout << "IsPrime: Failed in testing the primality of " << n << endl;
exit( failed_in_testing_primality )//在"stdlib.h"定义的
}
if( n == 2 )
return true;
if( n % 2 == 0 )
return false;
while( i <= t )
{
if( n % i == 0 )
return false;
i += 2;
}
return true;
}

算法复杂度

  • 引入算法复杂度的目的
    度量算法的效率与性能
  • 大 O 表达式
    算法效率与性能的近似表示(定性描述)
    算法执行时间与问题规模的关系
  • 表示原则
    忽略所有对变化趋势影响较小的项,例如多项式忽略高阶项之外的所有项
    忽略所有与问题规模无关的常数,例如多项式的系数
  • 标准算法复杂度类型
    O(1):常数级,表示算法执行时间与问题规模无关
    O(log(n)):对数级,表示算法执行时间与问题规模的对数成正比
    O(sqrt(n)):平方根级,表示算法执行时间与问题规模的平方根成正比
    O(n):线性级,表示算法执行时间与问题规模成正比
    O(nlog(n))nlog(n) 级,表示算法执行时间与问题规模的 n*log(n) 成正比
    O(n2):平方级,表示算法执行时间与问题规模的平方成正比
    ……
  • 算法复杂度估计
    以嵌套的循环次数估计:
for( i = 0; i < n; i++ )
	cout << "No. " << I << ": Hello, World!\n";					//O(n)
for( i = 0; i < n; i++ )
	for( j = 0; j < n; j++ )
		cout << "Hello, World!\n";								//O(n2)
for( i = 0; i < n; i++ )
	for( j = i; j < n; j++ )
		cout << "Hello, World!\n";								//O(n2)

4.6 编程实战

  • 设计算法,将某个大于1的自然数n分解为其素因子的乘积,如6=23,7=7,8=22*2。
#include <iostream>
#include "stdlib.h"
using namespace std;
const int failed_in_input=1;
void Welcome();
int GetInteger();
void Decompose(int n);
int main(){
    int n;
    n=GetInteger();
    Decompose(n);
    cout << "Finish!\n";
    return 0;
}

void Welcome(){
    cout << "This is a program to decompose a natural number!\n";
}

int GetInteger(){
    int n;
    cout <<"Please enter a number !\n";
    cin >> n; 
    if (n<=1){
        cout << "Failed in decomposing because of your invalid input!!\n";
        exit(failed_in_input);
    }
    return n;
}

void Decompose(int n){
    int i=2;							/循环标志
    cout << n << "= ";
    if(n==2){
        cout << 1 << " * " << 2 << endl;
    }
    else{
        for(i=2;i<=n;i++){				//从最小的质数 2  开始执行循环
        	while (n!=i)				//停止条件
        	{
            	if (n%i==0){			//判断当前i是否能整除n,若能 求商 ,继续
                	cout << i << "*" ;
                	n = (n/i);			//把商赋值给n,继续,直到n<i,跳出
            	}
            	else{
                	break;				//判断当前i是否能整除n,若不能,则i+1
            	}
        	}  
    	} 
    	cout << n << endl;
    }
    
}

运行结果:
Please enter a number !
1
Failed in decomposing because of your invalid input!!

Please enter a number !
2
2= 1 * 2
Finish!

Please enter a number !
8
8= 2*2*2

Please enter a number !
11
11= 11
Finish!



  • 设计算法,分别使用循环和递归两种策略求二项式系数C(n,k)。其中,n为自然数,k为不大于n的非负整数。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kxwang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值