c语言进阶11-算法设计思想

本文深入讲解了算法设计的要求,包括正确性、可读性、健壮性和效率等,并详细介绍了算法的时间复杂度与空间复杂度的概念及其计算方法。此外,还通过具体的代码示例解析了常见的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、  算法设计的要求:

为什么要学算法?

/* 输出Hello word! */

#include "stdio.h"

void main()

{

    printf("Hello word!\n");

}

在此程序中,要综合运用数据结构和算法。数据结构是加工对象,语言是工具,变成需要合适的方法,但没有一个合格的算法,我们称不上合格的开发程序。所以,算法是程序设计的灵魂和核心。

  1. 1.        正确性

正确性:算法应当满足具体问题的需求。

 “正确”一词的含义在通常的用法中有很大的差别,大体分为以下四个层次:

程序不含语法错误;

例如,程序片段如下

int a;

float b;

a=3;b=4.5;

printf("%f%d\n",a,b);

编译时不给出出错信息,但运行结果将与原意不符,输出为

0.000000  16402

常见的语法错误,输入输出语句要求变量及格式说明一定要类型不一致。

程序对于合法的输入数据能够产生满足要求的输出结果;        

程序能够正常的输出数据。

程序对于非法的输入数据能够得出满足规格说明的结果;

程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果;

显然,达到层次4是最困难的,我们几乎不可能之一验证所有的输入都得到正确的结果。一般情况下,通常以第3层意义的正确性作为衡量一个程序是否合格的标准。

  1. 2.        可读性

可读性:算法设计的另一目的是为了便于阅读、理解和交流。

可读性好有助于人们对算法的理解;晦涩难懂的算法往往隐含错误,不易被发现,并难于调试和修改。可读性是算法好坏很重要的标志。

例:a=a+b;

b=a-b;       a=a-b;

 

此片段表达的意思是什么?难道是a,b互换?

  1. 3.        健壮性

一个好的算法应该能对输入数据不合法的情况做合适的处理。

健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。

如下程序片断:

if ((fp=fopen(filename,”w”))==NULL)

{

printf(“cannot open file\n”);

exit(0);

}
  1. 4.        时间效率高和存储量低

时间效率指的是算法的执行时间,对于同一个问题如果有多个算法可以解决,执行时间短的算法效率高,执行时间长的效率低。

存储量需求指算法执行过程中所需要的最大存储空间。

效率和低存储量需求这两者都与问题的规模有关。

如:求100个人的平均分与求1000个人的平均分所花的执行时间或运行空间显然有一定的差别。

二、  算法效率的度量方法

算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法:事后统计方法和事前分析估算方法。

  1. 1.        事后统计方法

这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序运行时间进行比较,从而确定算法效率的高低。

但这种方法显然有很大的缺陷:

(1)必须先运行依据算法编制的程序,通常需要花费大量的时间和精力;

(2)所得时间的统计量依赖于计算机的硬件和软件等环境因素,有时容易掩饰算法本身的优劣;

基于这样的缺陷,我们常常采用另一种事前分析估算的方法。

  1. 2.        事前分析估算的方法

事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。

经分析,我们发现,一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:

算法采用的策略、方法。

编译产生的代码质量。

问题的输入规模。

机器执行指令的速度

第1条是算法好坏的根本,第2条要有软件来支持,第4条要看硬件性能。就是说,抛开这些与计算机硬件、软件有关的因素,可以认为一个特定算法的“运行工作量”的大小,只依赖于问题的规模(通常用整数量n表示),或者说,它是问题规模的函数。

三、  函数的渐近增长

给定两个算法A和B,假设两个算法的输入规模都是n,算法A要做2n+3次操作,你可以理解为现有一个n次循环,执行完成后,再有一个n次循环,最后有三次赋值或运算,共2n+3次操作。算法B要做3n+1次操作。你觉得它们谁更快呢?

答案是不一定的。

次数

算法A(2n+3)

算法A'(2n)

算法B(3n+1)

算法B'(3n)

n=1

5

2

4

3

n=2

7

4

7

6

n=3

9

6

10

9

n=10

23

20

31

30

n=100

203

200

301

300

当n=1时,算法A效率不如算法B(次数比算法B要多一次)。而当n=2时,两者效率相同;当n>2时,算法A就开始优于算法B了,随着n的增加,算法A比算法B越来越好了(执行的次数比B要少)。于是我们可以得出结论,算法A总体上要好过算法B。

此时我们给出这样的定义,输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另一个函数,我们称函数是渐近增长的。

函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐近快于g(n)。

四、  算法的时间复杂度

  1. 1.        算法时间复杂度定义

在进行算法分析时,语句中的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。

一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。(量增加,时间增长慢)

显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n2)。我们分别给它们取了非官方的名称,O(1)叫做常数阶、O(n)叫线性阶、O(n2)叫平方阶。

如:

for(i=1;i<=n;++i)

for(j=1;j<=n;++j){

c[i][j]=0;

for(k=1;k<=n;++k)

c[i][j]+=a[i][k]*b[k][j];

}

此程序的时间复杂度是O(n3)。

  1. 2.        常数阶

下面这个算法,即高斯算法,为什么时间复杂度不是O(3),而是O(1)。

int sum = 0,n = 100;     /*   执行一次   */

sum = (1+n) *n/2;        /*   执行一次   */

printf("%d",sum);      /*   执行一次   */

这个算法的运行次数函数是f(n)=3。把常数项3改为1,在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。

另外,我们试想一下,如果这个算法当中的语句sum = (1+n) *n/2有10句,即:

 

事实上无论n为多少,上面的两段代码就是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶

注意:不管这个常数是多少,我们都记作O(1),而不是O(3),O(12)等其他任何数字,这是初学者常常犯的错误。

对于分支结构而言,无论是真,还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所以单纯的分支结构(不包含在循环结构中),其时间复杂度也是O(1)

  1. 3.        线性阶

线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。

下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。

int i;

for(i = 0;i < n;i++)

{

/* 时间复杂度为O(1)的程序步骤序列 */

}
  1. 4.        对数阶

我们以一段代码为例,说明对数阶:

int count = 1;

while (count < n)

{

count = count *2

/* 时间复杂度为O(1)的程序步骤序列 */

}

 

由于每次count乘以2之后,就距离n更近了一分。也就是说,有多少个2相乘后大于n,则会退出循环。由2x=n得到x=log2n。所以这个循环的时间复杂度为O(log2n)

  1. 5.        平方阶

下面例子是一个循环嵌套,它的内循环刚才我们已经分析过,时间复杂度为O(n2)。

int i,j;

for(i = 0;i < n; i++)

{

   for (j = 0;j < n;j++)

{

      

/* 时间复杂度为O(1)的程序步骤序列 */

}

}

对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n2)。

常见的时间复杂度

常见的时间复杂度如下所示。

执行次数函数

非正式用语

12

O(1)

常数阶

2n+3

O(n)

线性阶

3n2+2n+1

O(n2)

平方阶

5log2n+20

O(log2n)

对数阶

2n+3nlog2n+19

O(nlog2n)

nlog2n阶

6n3+2n2+3n+4

O(n3)

立方阶

2n

O(2n)

指数阶

常用的时间复杂度所耗费的时间从小到大依次是:

 

1           3         10          30         100     1000    1024    

O(1)< O(log2n)< O(n)< O(nlog2n)< O(n2)< O(n3)< O(2n)< O(n!)<O(nn)

最坏情况与平均情况

我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),最坏的情况是这个数字在最后一个位置上,那么算法的时间复杂度就是O(n)。

最坏情况运行时间是一种保证,那就是运行时间将不会再坏了(在应用中,这是一种最重要的需求,通常除非特别指定,我们提到的运行时间都是最坏情况的运行时间)。

平均运行时间是从概率的角度看,这个数字在每个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。

平均运行时间是期望的运行时间。也就是说,我们运行一段代码时,是希望看到平均运行时间的。可现实中,平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。

对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有特殊说明的情况下,都是指最坏时间复杂度。

例如:冒泡排序法

void bubble_sort(int a[],int n)

{

chang=false;

for(i=n-1;change=TURE;i>1&&change;-i)

for (j=0;j<I;++j)

if(a[j]>a[j+1])

{

a[j]←→a[j+1];change=TURE;}

}

最好情况:0次

最坏情况:1+2+3……+n-1=n(n-1)/2

平均时间复杂度为:O(n2)

4.7算法的空间复杂度

算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),表示随着问题规模n的增大,算法运行所需存储量的增长率与f(n)的增长率相同。

算法的存储量包括:

输入数据所占空间;

程序本身所占空间;

辅助变量所占空间。

若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。

所需额外空间相对于输入数据量来说是常数,则称此算法为原地工作。

若所需存储量依赖于特定的输入,则通常按最坏情况考虑。

希尔排序代码

/*
 希尔排序  缩小增量排序----->通俗的讲就是改进后的直接插入排序

增加了k  增量序列  分组的组数

k=MAX/2 增量k的值是越来越小

先分小组,分别对每个组内进行直接插入排序

然后在k=k/2 分组

直到组数为1截止  进行最终的一趟直接插入排序结束
 */
#include "stdio.h"
#define MAX 11
void main()
{
    int a[MAX]={6,3,8,1,7,4,9,12,52,54,2};
    int i;//控制循环趟数 以及 待排序元素的下标
    int j;//控制有序数组的下标
    int temp;//存放 待排序元素 temp数据类型 与数组类型一致
    int k;//增量 k代表把元素分为几组
     //希尔排序开始
    for(k=MAX/2;k>=1;k=k/2) // 缩小增量排序 继续分组  继续进行直接插入排序 
    {
        //直接插入排序开始
        for(i=k;i<MAX;i++)
        {
             temp=a[i];//待排序元素
             if(temp<a[i-k])
             {                
                   for(j=i-k;a[j]>temp&&j>=0;j=j-k)//i-k有序数组最后一个元素的下标
                   {
                       a[j+k]=a[j];
                   }
                   //当我们结束第二层for循环时候,结束时j=j-k
                   a[j+k]=temp;                 
             }          
        }
        //直接插入排序结束
               
    }
    //希尔排序结束
     printf("希尔排序结果:\n");   
    for(i=0;i<MAX;i++)
    {
        printf("%d\t",a[i]);
    }
}

折半法代码

/* 折半查找   前提  顺序存储  记录有顺序

   折半查找   low 头下标  high 尾巴下标   mid 中间位置下标=(low+high)/2

   拿要查找的值key  和  中间值  比较 

   key大于 中间值    去右边找    右边有尾巴没有头  按个头  low=mid+1
   
   key小于 中间值   去左边找    左边有头没有尾巴     high=mid-1
   
   key ==  中间值    找到了 输出下标  break;终止查找
  */
#include "stdio.h"
#define MAX 10
int a[MAX]={11,13,14,15,16,18,34,66,99,100};
//折半查找函数
int zheban(int key) //传递待查找的关键字
{
    int low=0;
    int high=MAX-1;  //数组最后一个元素下标
    int mid;
    while(low<=high)
    {
        mid=(low+high)/2;  //求出中间值得下标
        if(key>a[mid])
        {
            //去右边找
            low=mid+1;
            
        }else if(key<a[mid])
        {
            //去左边找 
            high=mid-1;
            
        }else{
           printf("查找成功\n");
           return mid;
        }        
    }
    //循环结束后
    /*if(low>high)
    {
        printf("查找失败\n");
    }    */
    return -1;
}
void main()
{
    int key;//存放待查找的关键件
    printf("请输入您要查找的数:");
    scanf("%d",&key);
    printf("@%d@\n",zheban(key));
}

 

转载于:https://www.cnblogs.com/TimVerion/p/11193165.html

算法设计分析》目录: 第一篇引入篇 第1章算法概述1.1用计算机求解问题算法 1.1.1用计算机求解问题的步骤 1.1.2算法及其要素和特性 1.1.3算法设计及基本方法 1.1.4从算法到实现 1.2算法描述 1.2.1算法描述简介 1.2.2算法描述约定 1.2.3一个简单问题的求解过程 1.3现代常用算法概览* 1.3.1压缩算法 1.3.2加密算法 1.3.3人工智能算法 1.3.4并行算法 1.3.5其他实用算法 第2章算法分析基础 2.1算法分析体系及计量 2.1.1算法分析的评价体系 2.1.2算法的时间复杂性 2.1.3算法空间复杂性 2.1.4NP完全性问题 2.2算法分析实例 2.2.1非递归算法分析 2.2.2递归算法分析 2.2.3提高算法质量 第二篇基础篇 第3章算法基本工具和优化技巧3.1循环递归 3.1.1循环设计要点 3.1.2递归设计要点 3.1.3循环递归的比较 3.2算法数据结构 3.2.1原始信息处理结果的对应存储 3.2.2数组使信息有序化 3.2.3数组记录状态信息 3.2.4大整数存储及运算 3.2.5构造趣味矩阵 3.3优化算法的基本技巧 3.3.1算术运算的妙用 3.3.2标志量的妙用 3.3.3信息数字化 3.4优化算法的数学模型 3.4.1杨辉三角形的应用 3.4.2最大公约数的应用 3.4.3公倍数的应用 3.4.4斐波那契数列的应用 3.4.5递推关系求解方程 习题 第三篇核心篇 第4章基本的算法策略4.1迭代算法 4.1.1递推法 4.1.2倒推法 4.1.3迭代法解方程 4.2蛮力法 4.2.1枚举法 4.2.2其他范例 4.3分治算法 4.3.1分治算法框架 4.3.2二分法 4.3.3二分法变异 4.3.4其他分治方法 4.4贪婪算法 4.4.1可绝对贪婪问题 4.4.2相对或近似贪婪问题 4.4.3贪婪策略算法设计框架 4.5动态规划 4.5.1认识动态规划 4.5.2动态规划算法设计框架 4.5.3突出阶段性的动态规划应用 4.5.4突出递推的动态规划应用 4.6算法策略间的比较 4.6.1不同算法策略特点小结 4.6.2算法策略间的关联 4.6.3算法策略侧重的问题类型 习题 第5章图的搜索算法 5.1图搜索概述 5.1.1图及其术语 5.1.2图搜索及其术语 5.2广度优先搜索 5.2.1算法框架 5.2.2广度优先搜索的应用 5.3深度优先搜索 5.3.1算法框架 5.3.2深度优先搜索的应用 5.4回溯法 5.4.1认识回溯法 5.4.2回溯法算法框架 5.4.3应用1——基本的回溯搜索 5.4.4应用2——排列及排列树的回溯搜索 5.4.5应用3——最优化问题的回溯搜索 5.5分支限界法 5.5.1分支搜索算法 5.5.2分支限界搜索算法 5.5.3算法框架 5.6 图的搜索算法小结 习题 第四篇应用篇 第6章算法设计实践6.1循环赛日程表 6.2求3个数的最小公倍数 6.3猴子选大王 6.4最大子段和问题 6.5背包问题 6.5.1利润无关的背包问题 6.5.2利润有关的背包问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值