常用的算法思想


枚举算法思想


将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,丢弃不合适。在C语言中,枚举算法一般使用while循环实现。使用枚举算法解题的基本思路如下。

  • 确定枚举对象、枚举范围和判定条件。
  • 逐一列举可能的解,验证每个解是否是问题的解。

枚举算法一般按照如下3个步骤进行。

  • 题解的可能范围,不能遗漏任何一个真正解,也要避免有重复。
  • 判断是否是真正解的方法。
  • 使可能解的范围降至最小,以便提高解决问题的效率。

问题:公鸡每只5元,母鸡每只3元,小鸡3只一元。用100元钱买100只鸡,问公鸡、母鸡、小鸡各多少?

#include <stdio.h>

int main()
{
    int x,y,z;//x:公鸡,y:母鸡,z:小鸡
    for(x=0;x<=20;x++)
    {
        for(y=0;y<=33;y++)
        {
            z=100-x-y;
            if(z%3==0 && x*5+y*3+z/3==100)
                printf("公鸡:%d,母鸡:%d,小鸡:%d\n", x,y,z);
        }
    }
    getchar();
    return 0;
}
输出
+++++++++++++++++++++++
公鸡:0,母鸡:25,小鸡:75
公鸡:4,母鸡:18,小鸡:78
公鸡:8,母鸡:11,小鸡:81
公鸡:12,母鸡:4,小鸡:84
+++++++++++++++++++++++

递推算法思想


递推算法可以不断利用已有的信息推导出新的东西,在日常应用有两种递推算法。

  • 顺推法:从已知条件出发,逐步推算出要解决问题的方法。例如斐波那契数列就可以通过顺推法不断递推算出新的数据。
  • 逆推法:从已知的结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程。

问题:斐波那契数列以兔子繁殖为例子而引入,所以又称为“兔子数列”。兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生一对小兔子。如果所以兔子都不死,那么一年之后可以繁殖多少对兔子

算法分析:

  • 第一个月小兔子没有繁殖能力,所以还是一对。
  • 2个月后,一对小兔子生下一对新的小兔子,所以共有两对兔子。
  • 3个月以后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是3对。

月数与兔子对数关系表

- - - - - - - - - -
月数: 1 2 3 4 5 6 7 8
对数: 1 1 2 3 5 8 13 21

对数:1,1,2,3,5,8··· ···构成了一个数列,这个数列有个十分明显的特点:前面相邻两项之和,构成了后一项。

由此可以得出具体算法如下所示:

第一个月兔子的总数是F1 = 1.

第2个月的兔子总数是F2 = F0 + F1.

第3个月的兔子总数是F3 = F1 + F2.

····

第n个月的兔子总数是Fn = F(n-1) + F(n-2).

根据“斐波那契数列”的顺推算法分析,代码实现如下

#include <stdio.h>
#define MONTH 12

int main(void)
{
    int i;
    int fib[MONTH] = {1,1};

    //计算
    for(i = 2; i < MONTH; i++)
    {
        fib[i] = fib[i-1] + fib[i-2];
    }

    //输出
    for(i = 0; i < MONTH; i++)
    {
        printf("第%d个月的兔子对数:%d\n", i+1, fib[i]);
    }

    getchar();
    return 0;
}

输出结果:

第1个月兔子对数:1
第2个月兔子对数:1
第3个月兔子对数:2
第4个月兔子对数:3
第5个月兔子对数:5
第6个月兔子对数:8
第7个月兔子对数:13
第8个月兔子对数:21
第9个月兔子对数:34
第10个月兔子对数:55
第11个月兔子对数:89
第12个月兔子对数:144

问题描述

母亲为儿子4年的大学生活准备了一笔存款,方式是整存零取,规定儿子每个月底取下一个月的生活费。现在假设银行的年利息为1.71%,计算母亲最少需要存入多少钱?

算法分析

可以采用逆推法分析存钱和取钱的过程,因为按照月为周期取钱,所以需要将4年分为48个月,并分别对每个月进行计算。

如果在第48个月后儿子大学毕业时连本带息取1000元。要求求出第47个月时候银行存款的钱数:

第47月月末存款=1000/(1+0.0171/12);

第46月月末存款=(第47月月末存款+1000)/(1+0.0171/12);

第45月月末存款=(第46月月末存款+1000)/(1+0.0171/12);

第44月月末存款=(第45月月末存款+1000)/(1+0.0171/12);

··· ···

第2月月末存款=(第3月月末存款+1000)/(1+0.0171/12);

第1月月末存款=(第2月月末存款+1000)/(1+0.0171/12);

代码如下:

#include<stdio.h>
#define MONTH 48

int main(void)
{
    int i;
    float money = 1000;

    printf("第48个月存款:1000\n");

    for(i = MONTH - 1; i > 0; i--) {
        money = (money + 1000) / (1+0.0171/12);
        printf("第%d个月存款:%.2f\n", i, money);
    }

    getchar();
    return 0;
}

输出结果

第48个月存款:1000
第47个月存款:1997.15
第46个月存款:2992.89
第45个月存款:3987.21
第44个月存款:4980.11
第43个月存款:5971.60
第42个月存款:6961.68
第41个月存款:7950.35
第40个月存款:8937.62
第39个月存款:9923.47
第38个月存款:10907.93
第37个月存款:11890.99
第36个月存款:12872.64
第35个月存款:13852.90
第34个月存款:14831.77
第33个月存款:15809.24
第32个月存款:16785.32
第31个月存款:17760.01
第30个月存款:18733.31
第29个月存款:19705.23
第28个月存款:20675.77
第27个月存款:21644.93
第26个月存款:22612.71
第25个月存款:23579.11
第24个月存款:24544.13
第23个月存款:25507.78
第22个月存款:26470.06
第21个月存款:27430.97
第20个月存款:28390.52
第19个月存款:29348.69
第18个月存款:30305.51
第17个月存款:31260.96
第16个月存款:32215.05
第15个月存款:33167.79
第14个月存款:34119.17
第13个月存款:35069.20
第12个月存款:36017.87
第11个月存款:36965.20
第10个月存款:37911.17
第9个月存款:38855.80
第8个月存款:39799.09
第7个月存款:40741.03
第6个月存款:41681.64
第5个月存款:42620.90
第4个月存款:43558.83
第3个月存款:44495.43
第2个月存款:45430.69
第1个月存款:46364.62

递归算法思想


递归算法思想往往用函数的形式来体现,递归算法需要预先编写功能函数。

递归算法特点

  • 递归过程一般通过函数或子过程来实现。
  • 递归算法在函数或子过程的内部,直接或者间接的调用自己的算法。
  • 递归算法实际上是把问题转化为规模缩小了的同类问题的子问题,然后再递归调用函数或过程来表示问题的解。

在使用递归算法时,应该注意如下4点。

  • 递归是在过程或函数中调用自身的过程
  • 在使用递归策略时,必须有一个明确的递归结束条件,这叫做递归出口。
  • 递归算法通常显得很简洁,但是运行效率比较低,所以一般不提倡用递归算法设计程序。
  • 在递归调用过程中,系统用栈来存储每一层的返回点和局部量。如果递归次数过多,则容易造成栈溢出,所以一般不提倡用递归算法设计程序。

问题描述(汉诺塔)

寺院有三根柱子,第一根有64个盘子,从上往下盘子越来越大。方丈要求小和尚A1把这64个盘子全部移动到第三根柱子上。在移动的时候,始终只能小盘子压着大盘子,而且每次只能移动一个。

分析

汉诺塔的递归实现算法,将A中的圆盘借助B圆盘完全移动到C圆盘上,

每次只能移动一个圆盘,并且每次移动时大盘不能放在小盘上面

递归函数的伪算法为如下:

if(n == 1)

   直接将A柱子上的圆盘从A移动到C

else

   先将A柱子上的n-1个圆盘借助C柱子移动到B柱子上

   直接将A柱子上的第n个圆盘移动到C柱子上

   最后将B柱子上的n-1个圆盘借助A柱子移动到C柱子上

该递归算法的时间复杂度为O(2的n次方),当有n个圆盘时,需要移动圆盘2的n次方-1次

实现


#include<stdio.h>
 
void move(int,char,char,char);
 
int main()
{
    //A、B、C分别代表三个柱子
    char ch1 = 'A';
    char ch2 = 'B'; 
    char ch3 = 'C'; 
    
    //n代表圆盘的个数
    int n;
    printf("请输入圆盘个数:");
    scanf("%d",&n);
    move(n,ch1,ch2,ch3);
    
    return 0;
}
 
//将n个圆盘从x柱子借助y柱子移动到z柱子上
void move(int n,char x,char y,char z)
{
    if(n == 1)
        printf("圆盘编号%d:从%c移动到%c\n",n,x,z);
    else
    {
        move(n-1,x,z,y);
        printf("圆盘编号%d:从%c移动到%c\n",n,x,z);
        move(n-1,y,x,z);
    }
}

输出结果

请输入圆盘个数:5
圆盘编号1:从A移动到C
圆盘编号2:从A移动到B
圆盘编号1:从C移动到B
圆盘编号3:从A移动到C
圆盘编号1:从B移动到A
圆盘编号2:从B移动到C
圆盘编号1:从A移动到C
圆盘编号4:从A移动到B
圆盘编号1:从C移动到B
圆盘编号2:从C移动到A
圆盘编号1:从B移动到A
圆盘编号3:从C移动到B
圆盘编号1:从A移动到C
圆盘编号2:从A移动到B
圆盘编号1:从C移动到B
圆盘编号5:从A移动到C
圆盘编号1:从B移动到A
圆盘编号2:从B移动到C
圆盘编号1:从A移动到C
圆盘编号3:从B移动到A
圆盘编号1:从C移动到B
圆盘编号2:从C移动到A
圆盘编号1:从B移动到A
圆盘编号4:从B移动到C
圆盘编号1:从A移动到C
圆盘编号2:从A移动到B
圆盘编号1:从C移动到B
圆盘编号3:从A移动到C
圆盘编号1:从B移动到A
圆盘编号2:从B移动到C
圆盘编号1:从A移动到C

阶乘算法

阶乘是典型可以使用递归算法来计算的。

代码如下

#include <stdio.h>

int fact(int n);

int main(void)
{
    int i;
    printf("输入要计算阶乘的一个整数:");
    scanf("%d", &i);
    printf("%d的阶乘结果为:%d\n", i ,fact(i));
    getchar();
    return 0;
}

int fact(int n)
{
    if(n <= 1)
        return 1;
    else
        return n*fact(n-1);
}

输出结果

请输入要计算阶乘的一个整数:5
5的阶乘的结果为:120

分治算法思想


先把问题分解成几个较小的子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个大问题的解。如果这些子问题还是比较大,还可以继续再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治算法的基本思想。

使用分治算法解题的一般步骤如下

  • 分解,将要解决的问题划分成若干个规模较小的同类问题。
  • 求解,当子问题划分得足够小时,用较简单的方法解决。
  • 合并,按照原问题的要求,将子问题的解逐层合并构成原问题的解。

问题描述

大数相乘,就是计算两个大数的积。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>

// 翻转data[start...end-1]
void reverse_data( char *data, int start, int end  )
{
    char temp = '0';

    assert( data != NULL && start < end );
    while ( start < end )
    {
        temp = data[start];
        data[start] = data[--end];
        data[end] = temp;
        ++start;
    }
}

/**< 判断数据中数值的合法性,以及非空格字符的起始位置。
 *  1. 是否均有正确的正负号,例如"+123", "-123"等
 *  2. 字符串中字符是否均是数字字符。
 *  3. 字符串开始部分放入空格是合法的,例如,”  +123“等
 *  4. 只有一个'.'标记
 **< 参数:
 * @data: 表示传入字符;
 * @nonspace_index:非空格起点
 **< 返回值:若数据是合法数值,则返回1;否则返回0;
 */
int check_logic( const char *data, int *nonspace_index )
{
    int flag = 1;
    int start = 0;
    int point_cnt = 0;

    assert( data != NULL );
    /* PS. if data is not space(' ', '\n'), isspace() return 0. */
    for ( ; isspace( data[start] )!= 0
            && data[start] != '\0'; ++start );

    // 判断数据是否为负数
    *nonspace_index = start;
    if ( data[start] == '-' || data[start] == '+' )
    {
        ++start;
    }

    /* PS. if ch is digit character, isdigit() return 1; otherwise return 0. */
    for ( ; data[start] != '\0'; ++start )
    {
        if ( isdigit( data[start] ) || data[start] == '.' )
        {
            // 判断数据为小数的格式是否正确。
            if ( data[start] == '.' && point_cnt == 0 )
            {
                ++point_cnt;
            }
            else if ( point_cnt > 1 )
            {
                break;
            }
        }
    }

    // 若小数点后面无数据,则不合法
    if ( data[start] != '\0' )
    {
        flag = 0;
    }

    return flag;
}

/**< notice: 传入到该函数的数据已经被翻转后的数值。即,最左边为个位,最右边为最高位
 **< return: 结果数据的长度。
 */
int compute_value( const char *lhs, int lhs_start_index,
                   const char *rhs, int rhs_start_index,
                   char *result )
{
    int i = 0, j = 0, res_i = 0;
    int tmp_i = 0;
    int carry = 0;

    for ( i = lhs_start_index; lhs[i] != '\0'; ++i, ++tmp_i )
    {
        res_i = tmp_i;
        carry = 0;

        for ( j = rhs_start_index; rhs[j] != '\0'; ++j )
        {
            int tmp_lhs = lhs[i] - '0';
            int tmp_rhs = rhs[j] - '0';
            carry += ( result[res_i] - '0' );
            carry += ( tmp_lhs * tmp_rhs );
            result[res_i++] = ( carry % 10 + '0' );
            carry /= 10;
        }

        while ( carry )
        {
            result[res_i++] = ( carry % 10 + '0' );
            carry /= 10;
        }
    }
    result[res_i] = '\0';

    return res_i;
}

int has_point( char *data, int index, int *point_index )
{
    int start = index;

    for ( ; data[start] != '\0'; ++start )
    {
        if ( data[start] == '.' )
        {
            *point_index = start;
            break;
        }
    }

    return ( data[start] != '\0' );
}

int is_neg( char *data, int *index )
{
    int flag = 0;
    int start = *index;
    if ( data[start] == '-' || data[start] == '+' )
    {
        if ( data[start] == '-' )
            flag = 1;
        ++start;
    }

    *index = start;
    return flag;
}

void copy_c( char * dest, const char *src )
{
    while ( *src != '\0' )
    {
        if ( *src != '.' )
            *dest++ = *src;
        src++;
    }
}

int compute_decimals( char *lhs, int lhs_point_index,
                      char *rhs, int rhs_point_index,
                      char *result  )
{
    int lhs_length = strlen( lhs );
    int rhs_length = strlen( rhs );
    int result_point_index = lhs_length + rhs_length;
    int result_length = 0, i = 0;
    char *tmp_lhs = NULL;
    char *tmp_rhs = NULL;

    // 计算在结果中放置小数点的位置,根据的是两个小数部分长度之和
    // 例如,rhs = "12.345", lhs = "3.45", result = "xxx.xxxxx"
    result_point_index -= ( lhs_point_index + rhs_point_index );

    // 分配并拷贝
    if ( lhs_point_index )
    {
        tmp_lhs = (char *)malloc( sizeof(char) * lhs_length );
        assert( tmp_lhs != NULL );
        copy_c( tmp_lhs, lhs );
        tmp_lhs[lhs_length - 1] = '\0';
    }
    else
    {
        tmp_lhs = lhs;
    }

    if ( rhs_point_index )
    {
        tmp_rhs = (char *)malloc( sizeof(char) * rhs_length );
        assert( tmp_rhs != NULL );
        copy_c( tmp_rhs, rhs );
        tmp_rhs[rhs_length - 1] = '\0';
    }
    else
    {
        tmp_rhs = rhs;
    }

    // tmp_lhs比lhs少一个小数点
    reverse_data( tmp_lhs, 0, lhs_length - 1 );
    reverse_data( tmp_rhs, 0, rhs_length - 1 );
    result_length = compute_value( tmp_lhs, 0, tmp_rhs, 0, result );
    for ( i = result_length; i > result_point_index; --i )
    {
        result[i] = result[i - 1];
    }

    result[result_point_index] = '.';
    ++result_length;
    result[result_length] = '\0';

    // 释放资源
    if ( lhs_point_index )
    {
        free( tmp_lhs ), tmp_lhs = NULL;
    }

    if ( rhs_point_index )
    {
        free( tmp_rhs ), tmp_rhs = NULL;
    }

    return result_length;
}

// 返回结果数值的长度
int big_number_multiply( char *lhs, char *rhs, char *result )
{
    int lhs_start_index = 0, lhs_point_index = 0;
    int rhs_start_index = 0, rhs_point_index = 0;
    int result_is_neg = 0;
    int result_length = 0;

    assert( lhs != NULL && rhs != NULL && result != NULL );
    // 检查数据的合法性
    if ( !(check_logic( lhs, &lhs_start_index )
            && check_logic( rhs, &rhs_start_index )) )
    {
        return -1;
    }

    // 检查数据是否为负数
    result_is_neg = is_neg( lhs, &lhs_start_index );
    if ( is_neg( rhs, &rhs_start_index) )
    {
        result_is_neg = result_is_neg == 1 ?  0 : 1;
    }

    // 检查是否两个数值中存在一个小数或两者都是小数
    if ( !( has_point( lhs, lhs_start_index, &lhs_point_index )
            && has_point( rhs, rhs_start_index, &rhs_point_index ) ) )
    {
        reverse_data( lhs, lhs_start_index, strlen(lhs) );
        reverse_data( rhs, rhs_start_index, strlen(rhs) );
        result_length = compute_value( lhs, lhs_start_index,
                                       rhs, rhs_start_index, result );
        reverse_data( lhs, lhs_start_index, strlen(lhs) );
        reverse_data( rhs, rhs_start_index, strlen(rhs) );
    }
    else // 一个数值中有小数部分
    {
        result_length = compute_decimals(
                          lhs + lhs_start_index, lhs_point_index - lhs_start_index + 1,
                          rhs + rhs_start_index, rhs_point_index - rhs_start_index + 1,
                          result );
    }
    if ( result_is_neg )
        result[result_length++] = '-';
    reverse_data( result, 0, result_length );
    result[result_length] = '\0';

    return result_length;
}

int main()
{
    char lhs[] = "-1.235";
    char rhs[] = "3.456";
    char result[40];

    memset( result, '0', sizeof(result) );

    big_number_multiply( lhs, rhs, result );
    printf( "%s\n", result );
    return 0;
}

问题描述

一年一度的欧洲冠军杯马上就要打响,在初赛阶段采用循环制,设共有n队参加,初赛共进行n-1天,每队要和其他各队进行一场比赛,然后按照最后积分选拔进入决赛的球队。要求每队每天只能进行一场比赛,并且不能轮空。请按照上述需求安排比赛日程,决定每天各队的对手。

算法分析

根据分治算法思路,将所有参赛队伍分为两半,则n队的比赛日程表可以通过n/2个队的比赛日程来决定。然后继续按照上述一分为二的方法对参赛队进行划分,直到只剩余2队时为止。

假设n队的编号为1,2,3,··· ···,n,比赛日程表制作为一个二维表格,每行表示每队所对阵队的编号。

编号 第一天 第二天 第三天 第四天 第五天 第六天 第七天
1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 2 1 4 3
7 8 5 6 3 4 1 2
8 7 6 5 4 3 2 1

根据上面的表分析,可以将复杂的问题分治而解。

编号 第1天 第2天 第3天
1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1

后面的等完全理解了再写

贪心算法思想


试探性算法思想


迭代算法


模拟算法思想





上一页  C指针知识点收集

下一页  数据结构和算法学习理解C语言实现(二)