在筆者幹家教,教 C language 時,曾有位學生拿了學校老師的作業出來問我,作業說明不多,只有一張圖,類似下面這張。

 

plot_fn  

 

畫出 sin 圖形,另那份作業是需要「標座標」的,也就是要標 x, y 軸刻度,及交點座標。當下是怕太擔誤課程進度,所以並沒當下解,而是回去時候才花了一、二個小時畫出來 (其中半小時是在推座標公式,半小時是在調刻度及標號交點,剩下 Coding 很快)。

 

解題方向

 

無論如何記得一件事,一般 y 軸座標遞增方向,和一般數學上 y 軸座標遞增方式是反過來的。

首先要聲明,筆者認為 [在 console 裡畫圖型] 只是練習用而已,然後解法大致分成三大類。

 

[1] 直接用兩個 for-loop 下去做,配合座標轉換把點標出來,效率較低;

[2] 另一種方式是直接開一個二維字元陣列,char graphic[HEIGHT][WIDTH] ,所有的畫圖動作都對該陣列做處理,最後再一次印出來,這種方式若寫得好,效率非常高!

[3] 直接調用現有 API 去設定 console 游標位置,如 SetConsoleCursorPosition ;若支援 ANSI Escape sequence 也是一樣的原理。

如果用 C 的話,這種題目解過就算了,不要一直窮解這題;用 C++ 的話可以練 OO 或 design pattern 能力,因可擴充的部份太多了,座標非線性、一對一函式(如三角函式)、一對多函式 (如圓的方程式) 等,算是一個不大,但應算是不錯的題目。

另一般而言,在畫 plot 時,確實蠻多人挑用 gnuplot  做繪圖,這裡不是本文重點,有空再發一下筆者常用的 gnuplot 指令。

 

第一種解法

 

先看一個題目是,畫出 y = x^2 之曲線圖,畫出來長如此。

plot_x2  

第一種解法其實沒什麼技巧,效率低,code 不長,直接放上。

 

Code Snippet
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <math.h>
  4.  
  5. #define YLOW 0.0
  6. #define YUP  25.0
  7. #define Yd   1.0
  8. #define XLOW -10.0
  9. #define XUP  10.0
  10. #define Xd   0.2
  11. #define TEXT_WIDTH 4 // y 標示寬度
  12.  
  13. // 修改函式圖形
  14. double func(double x) {return x*x;}
  15.  
  16. int main()
  17. {
  18.     double x, y, fvalue;
  19.     double area=0.0;
  20.     unsigned cnt=0;
  21.  
  22.     // 畫圖
  23.     for(y=YUP; y>=YLOW; y-=Yd){
  24.        printf("%*.1lf|",TEXT_WIDTH, y);
  25.        cnt=0;
  26.        for(x=XLOW; x<=XUP; x+=Xd){
  27.           fvalue = func(x); // 算每個 x 之值
  28.           if(fabs(fvalue-y)<=Yd/2) putchar('*'); // 若與現在的 y 誤差在 xd/2 以下,輸出 '*'
  29.           else if(fabs(y)<=Yd/2) putchar('-'); //基準線
  30.           else putchar(' '); // 誤差超過 xd/2, 輸出空白
  31.           cnt++;
  32.        }
  33.        putchar('\n');
  34.     }
  35.     getchar();
  36.     return 0;
  37. }

 

要注意的是,這裡的寬度其實沒算好,所以要把 console 的可視寬度調大 ( 筆者是調到 120 )。

 

陣列作圖 + func-ptr

 

一個符號先說,i = [a,b] ,代表 for(i=a; i<=b; ++i)

 

這裡主要記流程,呼叫時必須傳入 5 個參數,分別是 double (*fn)(double) , xlow, xup, ylow, yup,其它的都是在 function 做計算。

由於一般 console window 預設是 W = 80, H = 25,所以一開始先準備 char gra[H][W],然後在 VC (.c) 裡面,靜態陣列也不吃 const 當 size ,所以用 malloc。另外注意三件事,

 

(a) 每一個 row 的最後一個字元做成 new-line 字元,這樣只要 output 一次便行,另記得補上 gra[H-1][W-1] = 0。

(b) 周圍做邊框,另做x,y 基準線, 但為分析方便,基準線並不一定代表是 x==0 或 y == 0。

(c) 因畫邊框、做換行的關係,所以 gra[i][j] 真正可用之範圍為,i = [1,H-2],j = [1,W-3]

 

了解上面三件事後,下面的流程就清楚多了 (注意,流程是示意,實際撰碼時一定有 loop 可以合併)。

第一大步驟是先建立空白的座標畫布。

 

(1) 將 gra 全弄成空白

gra[i][j] = ' ', for all i, j 

(2) 畫邊線

gra[i][j] = '#' , for i==0, i==H-1 , j==0, j==W-2

(3) 做 row 換行

gra[i][W-1] = '\n', for i=[0,W-2]

(4) 塞結束字元

gra[H-1][W-1] = '\0'

(5) 畫 x 軸基準線

gra[H/2][j] = '-' , j = [1, W-3]

(6) 畫 y 軸基準線

gra[i][W/2] = '|', i = [1,H-2]

(7) 畫 x, y , 交點 

gra[H/2][W/2] = '+', gra[0][W/2] = 'Y', gra[H/2][W-1] = 'X'

 

第二大步驟是計算 y = fn(x) , 並做座標轉換, 最後一次輸出

(8) 計算 rate_x, rate_y

rate_x = (xup - xlow) / (W-4)

rate_y = (yup - ylow) / (H-3)

(9) for(i = 1 ; i<= W-3  ; ++i)  /* 以下動作都在此迴圈裡進行 */

(10) 計算 gra 裡的 i , 實際對應到的 x-value 是多少

x = (i-1) * rate_x + xlow

(11) 呼叫函式計算 y 值

y = fn(x)

(12) 將實際的 y 值對應到 gra 裡之 jtmp 座標, 考慮四捨五入

jtmp = (size_t)floor( (y-ylow) / rate_y + 0.5 )

(13) 將原本的 jtmp 座標,考慮電腦 y 軸和實際 y 軸遞增性相反,做修正

j = height - 2 - jtmp < -2 是因邊界不畫 >

(14) 將 gra[j][i] 畫上點

記得判斷 j>0 && j<height-1 是否成立。

 

上面做完後再一次輸出 gra 即可。講完了,看起來很複雜,實際上把回圈併過之後,其實碼不會很長。這裡的示例完全以上述流程,不再併回圈。

 

Code Snippet
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <math.h>
  5.  
  6. /**
  7. * plot f(x) function in console, C version
  8. *
  9. */
  10.  
  11. void plot_fn(
  12.     double (*fn)(double), /* f(x) function     */
  13.     const double xlow,    /* lower bound of x  */
  14.     const double xup,     /* upper bound of x  */
  15.     const double ylow,    /* max   bound of y  */
  16.     const double yup      /* min   bound of y  */
  17.     )     
  18. {
  19.     const size_t width = 80;
  20.     const size_t height= 25;
  21.     const size_t tsize = width * height;
  22.     size_t i, j, jtmp;
  23.     size_t ox = width/2, oy=height/2;
  24.  
  25.     double x, y, rate_x, rate_y;
  26.     
  27.     /* malloc memory */
  28.     char **gra = (char**)malloc(sizeof(char*) * height + tsize);
  29.     char * trunk = (char*)(gra + height);
  30.     for(i=0; i<height; ++i)
  31.         gra[i]=trunk, trunk+=width;
  32.  
  33.     /* initialize graphic  , boarder*/
  34.     memset((void*)*gra, ' ', tsize);
  35.     memset((void*)(gra[0]), '#', width);
  36.     memset((void*)(gra[height-1]), '#', width);
  37.     for(i=0; i<height; ++i){
  38.         gra[i][0] = '#';
  39.         gra[i][width-2] = '#';        
  40.         gra[i][width-1] = '\n';
  41.     }
  42.     gra[height-1][width-1]='\0';
  43.     
  44.     /* basement line for x axis*/
  45.     memset((void*)(gra[oy]+1), '-', width-3);
  46.     gra[oy][width-2]='X';
  47.  
  48.     /* basement line for y axis */
  49.     gra[0][ox]='Y';
  50.     for(i=1; i<height-1; ++i) gra[i][ox] = '|';
  51.     gra[oy][ox]='+';
  52.     
  53.     /* graphic */
  54.     rate_x = (xup - xlow) / (width-4);
  55.     rate_y = (yup - ylow) / (height - 3);
  56.  
  57.     for(i=1 ; i<=width-3; ++i){
  58.         x = (i-1)*rate_x + xlow;
  59.         y = fn(x);
  60.         jtmp = (size_t)floor((y-ylow)/rate_y + 0.5);
  61.         j = height-jtmp-2;
  62.         if(j>0 && j<height-1)
  63.             gra[j][i] = '*';
  64.     }
  65.     puts(*gra);
  66.     free((void*)gra);
  67. }
  68.  
  69. int main()
  70. {
  71.     const double PI = 3.1415926;
  72.     plot_fn(sin, -2.0*PI, +2.0*PI, -1.0, +1.0);
  73.     getchar();
  74.     return 0;
  75. }

 

結果圖就是第一張圖所示。

 

其他擴充功能

 

筆者稍找了一下,目前沒找到有 console plot library (不過並不意外就是了),有幾個功能有興趣自己再加

 

(1) 標刻度

(2) 標交點

(3) 給定 xmin, xup  時,程式裡面再自己計算 ymin, yup

(4) 支援 1 對 2 函式,如 x^2 + y^2 = C

(5) 非線性刻度,如 log10 刻度、exp 刻度、ln 刻度等

(6) 加入函式標題列、交點資訊、刻度資訊等

 

其他的筆者沒太大興趣 ( 主要是覺得不太實用 ),本文就介紹至此。

 

edisonx 發表在 痞客邦 PIXNET 留言(0) 人氣()