菱形打印程序——谈如何学习算法


菱形打印程序——谈如何学习算法

1.菱形打印

很多人,打印菱形在控制台的思路是,把菱形上下拆分,分两段很接近的代码来打印,其实这样代码很不好看,并且不好阅读.
   
我们知道,要打印的图案是这种:
                           *
                         ***
         *****
            ***
            *

满足上下对称,左右对称,那么,你能不能也弄一个二重循环,同样是对称的?
     
很简单,首先我们要抛开习惯性思维,for循环不一定要在0开始或者0结束,我们可以让循环从 -c 到 c ,这样不就轻松产生一个对称的吗?(只要取个绝对值)我们把菱形的中心看成是坐标0,0,那么,会输出星号的坐标,是 |x| + |y| <= c 的点.

由此可得
#include <stdio.h>
#define IABS(x) ( (x) >= 0 ? (x) : -(x) )       //定义一个计算绝对值的宏
    
void print(int size)                                 // size是这个菱形的半径,直径会是size * 2 + 1
          
{
                
int x, y;
                 for (y = -size; y <= size; y++)
      {
      for (x = -size; x <= size; x++)
      {
        if ( IABS(x) + IABS(y) <= size ) //x和y各自的绝对值的和,即 |x|
+ |y| <= size
          putchar(‘*‘);
          else
        putchar(‘ ‘);
      }
        putchar(‘\n‘);
      }
    }

  int main()
    {
    print(5);                  //输出一个半径为5的菱形
         
getchar();
    return 0;
          
}

如果我需要得到空心菱形呢?非常非常简单,因为菱形边界上的点,满足的是|x| + |y| == c,所以,我们只要把那个if里的小于等于号,改成双等于号 == 就可以了
    
再类似地,如果我不要*号,我要最外层是字母A,然后里一层是B这样呢?即:
                                                       A
                               ABA
                              ABCBA
                               ABA
                                A

那么,我们只要在putchar那里做一个字符计算:
void print(int size)                    // size是这个菱形的半径,直径会是size * 2 + 1
{
int x, y;
for (y = -size; y <= size; y++)
{
for (x = -size; x <= size; x++)
{
if ( IABS(x) + IABS(y) <= size )        //x和y各自的绝对值的和,即 |x|
+ |y| <= size
putchar( ‘A‘ + (size - IABS(x) - IABS(y)) );    //留意这里的计算方法
else
putchar(‘ ‘);
}
putchar(‘\n‘);
}
}

类似地,如果我们要打印的是X形:
*     *
 *  *
  *
 *  *
*    *
同样可以利用这个思路完成,这题就作为思考题吧

其实include stdlib.h后可以直接用abs()- -,而且严格来说int main(void)是最正确的,当然这些不是重点。
拿到一个算法程序,最简单的分析方式就是先跟踪看一下运行过程,由于size理论上可以取任意值,我们只要弄懂一种情况就等于弄懂了所有情况,所以为了简化分析先size=3,看一下第一次大的循环:
y=-3
x=-3 ‘ ‘
x=-2 ‘ ‘
x=-1 ‘ ‘
x=0 ‘*‘
x=1 ‘ ‘
x=2 ‘ ‘
x=3 ‘ ‘
到这里我们会发现,开始|x|+|y|>size,所以前面输出了三个‘ ‘,然而由于|x|渐渐减小,之后输出了一个‘*‘,由于|x|又渐渐增大,又输出了三个‘ ‘,这样就巧妙地完成了左右的对称。
又由于下一次循环|y|比这次循环小1,所以就“更容易”输出‘*‘,由于|y|也有对称的特性,所以完成了上下的对称。
至此,我们大概就对为什么可以上下左右对称有了一个感性的认识,不过恐怕更多的是对这些代码的惊叹:为什么这样写就能巧妙地做到这些?IABS(x) + IABS(y) <= size这个表达式也太神奇了!其实我们上来看程序的运行过程,只是为了从运行过程中试着找到入口去探索这个算法的本质是什么,这是一个从现象到本质的过程,而不能被现象吓到了。

我们把菱形的中心看成是坐标0,0,那么,会输出星号的坐标,是
|x| + |y| <= c 的点
那么程序的原理实际上就是通过枚举{(x,y), |x| <= size, |y| <= size,
x, y ∈Z}中的点(即下图中红色部分,包括边界),再通过一个式子来确认合法点,如果合法,就输出‘*‘,反之输出‘ ‘:

那么我们又有疑问了,为什么是|x| + |y| <= c?
我们来证明一下上面右上角的红色三角区域(包括边界)的所有点(x,y)(x,y∈N),x+y<=size(自己先试试看):
通过观察,我们可以发现y的取值范围随着x在改变,至于“怎么改变”则与size有关,那么我们可以想一下,如果我们把其中的y用size和x表示,这个不等式不就容易得证了吗?
那么我们可以得知y∈[0, size-x](x≠size),取两边分别讨论,当y=0时,x+y=x<=size,当y=size-x时,x+y=x+size- x=size,当y∈(0, size-x)(x≠size)时,x+y<size。综上所述,x+y<=size,证毕。
其实即使你不会证明,也可以当作公式默认下来,这都无关紧要。
于是我们就能自己写出|x| + |y| <= c这个式子了,也就知道了这个算法的本质,现在一切都掌握在我们手中,甚至我们可以对原程序做一点改动:
由于是枚举(x,y)∈{(x,y), |x| <= size, |y| <= size, x, y
∈Z},而且有绝对值的对称做保障,那么无论x、y谁在外面只要全枚举到就行了,所以也可以把x放在外层循环:
for (x = -size; x <= size; x++)
{
for (y = -size; y <= size; y++)
{
if (IABS(x) + IABS(y) <= size) //x和y各自的绝对值的和,即 |x| + |y| <= size
putchar(‘*‘);
else
putchar(‘ ‘);
}
putchar(‘\n‘);
}
观察原程序的输出,我们可以发现由于对称的缘故,程序输出的右侧多输出了我们看不见的空格,我们也可以不输出它:
if (IABS(x) + IABS(y) <= size ) //x和y各自的绝对值的和,即 |x| + |y| <= size
putchar(‘*‘);
else if (x < 0)
putchar(‘ ‘);
else
break;
虽然上述改动没有太大意义,但是说明了如果我们掌握了算法的本质,我们就掌握了全局,那么就可以进行任何改动的道理。

时间: 2025-01-05 13:38:46

菱形打印程序——谈如何学习算法的相关文章

从决策树学习谈到贝叶斯分类算法、EM、HMM

从决策树学习谈到贝叶斯分类算法.EM.HMM 引言 近期在面试中,除了基础 &  算法 & 项目之外,经常被问到或被要求介绍和描写叙述下自己所知道的几种分类或聚类算法(当然,这全然不代表你将来的面试中会遇到此类问题,仅仅是由于我的简历上写了句:熟悉常见的聚类 & 分类算法而已),而我向来恨对一个东西仅仅知其皮毛而不得深入,故写一个有关数据挖掘十大算法的系列文章以作为自己备试之用,甚至以备将来经常回想思考.行文杂乱,但侥幸若能对读者起到一点帮助,则幸甚至哉. 本文借鉴和參考了两本书,

从决策树学习谈到贝叶斯分类算法、EM、HMM --别人的,拷来看看

从决策树学习谈到贝叶斯分类算法.EM.HMM 引言 最近在面试中,除了基础 &  算法 & 项目之外,经常被问到或被要求介绍和描述下自己所知道的几种分类或聚类算法(当然,这完全不代表你将来的面试中会遇到此类问题,只是因为我的简历上写了句:熟悉常见的聚类 & 分类算法而已),而我向来恨对一个东西只知其皮毛而不得深入,故写一个有关数据挖掘十大算法的系列文章以作为自己备试之用,甚至以备将来常常回顾思考.行文杂乱,但侥幸若能对读者起到一点帮助,则幸甚至哉. 本文借鉴和参考了两本书,一本是T

黑马程序员——C学习总结之数组排序算法实现

发表试试 黑马程序员--C学习总结之数组排序算法实现,布布扣,bubuko.com

浅谈流形学习(转)

http://blog.pluskid.org/?p=533 总觉得即使是“浅谈”两个字,还是让这个标题有些过大了,更何况我自己也才刚刚接触这么一个领域.不过懒得想其他标题了,想起来要扯一下这个话题,也是因为和朋友聊起我自己最近在做的方向.Manifold Learning 或者仅仅 Manifold 本身通常就听起来颇有些深奥的感觉,不过如果并不是想要进行严格的理论推导的话,也可以从许多直观的例子得到一些感性的认识,正好我也就借这个机会来简单地谈一下这个话题吧,或者说至少是我到目前为止对这它的

好程序员Java学习路线分享Java面试题之加载机制

好程序员Java学习路线分享Java面试题之加载机制,面试场景:面试官第一问:请问,我现在编写一个类,类全名如下:java.lang.String,我们知道JDK也给我们听过了一个java.lang.String,那么,我们编写的这个String类能否替换到JDK默认提供,也就是说程序实际运行的时候,会加载我们的String还是JDK的String?为什么?如果,你无法确定?那么第二问:了解类的加载机制吗?知道JDK的类加载器吗?双亲委托机制说说看如果,你还不了解,那么我们聊聊今天的天气吧!1,

生成式学习算法

考虑一个分类问题: 根据一个动物的特征来区分该动物是大象(y=1)还是狗(y = 0).利用逻辑回归找到一条直线,即分界线,将训练集中的大象和狗分开,当给定一个新的动物特征时,检查该动物位于分界线的哪一边,然后做出判断是大象,还是狗,就是对p(y|x:θ)进行建模. 这里我们来看另一种不同的思路,首先根据训练集,我们找出大象有什么特征,然后找出狗有什么特征,当要对一个新的动物进行分类的时候,我们就对比该动物是与大象的特征更加匹配还是与狗的特征更加匹配,从而进行分类. 直接学习p(y|x)的算法是

黑马程序员_OC学习笔记之Foundation框架集合类

OC--集合类 1.OC集合类包括NSArray,NSSet,NSDictionary都是以面向对象的方式操作数组,而且OC数组不像C语言中的数组只能存放同一种数据类型,它可以存放任意类型的对象,但是不能存放非OC对象类型如基本数据类型int,struct,enum等 2.OC数组是以对象的方式存在,因此在创建的时候需要为创建的对象前面加* 3.NSArray数组一旦创建就决定了是不是可变,而且永远是可变或不可变 4.NSArray数组和子类NSMutableArray的基本操作: 1>使用NS

黑马程序员_OC学习笔记之description方法和sel

OC--description方法 1.Description方法包括类方法和对象方法.(NSObject类所包含) -description(对象方法) 2.使用NSLog和@%输出某个对象时,会调用对象的description方法,并拿到返回值进行输出. +description(类方法) 3.使用NSLog和@%输出某个对象时,会调用类对象的description方法,并拿到返回值进行输出,把整个对象一次性打印出来,打印对象使用%@. 4.使用@%打印对象如(“@%”,P)默认打印输出为<

黑马程序员_OC学习笔记之@property和@synthesize

[objc] view plaincopyprint? <span style="font-size:24px;">#import <Foundation/Foundation.h> @interface Person : NSObject { int _age; int age; int _height; int height; int _weight; int weight; int _money; int money; } @property int ag