原文發布在我個人小站:here
最近在准備筆試,於是在各種網站上刷題嘛。期間做了百度某年的一道編程題。
小B最近對電子表格產生了濃厚的興趣,她覺得電子表格很神奇,功能遠比她想象的強大。她正在研究的是單元格的坐標編號,她發現表格單元一般是按列編號的,第1列編號為A,第2列為B,以此類推,第26列為Z。之后是兩位字符編號的,第27列編號為AA,第28列為AB,第52列編號為AZ。之后則是三位、四位、五位……字母編號的,規則類似。
表格單元所在的行則是按數值從1開始編號的,表格單元名稱則是其列編號和行編號的組合,如單元格BB22代表的單元格為54列中第22行的單元格。
小B感興趣的是,編號系統有時也可以采用RxCy的規則,其中x和y為數值,表示單元格位於第x行的有第y列。上述例子中的單元格采用這種編碼體系時的名稱為R22C54。
小B希望快速實現兩種表示之間的轉換,請你幫忙設計程序將一種方式表示的坐標轉換為另一種方式。
輸入 輸出 輸入的第一行為一個正整數T,表示有T組測試數據( 1<=T<=10^5
)。隨后的T行中,每行為一組測試數據,為一種形式表示的單元格坐標。保證所有的坐標都是正確的,且所有行列坐標值均不超過10^6對每組測試數據,單獨輸出一行,為單元格坐標的另一種表示形式。 2
R23C55
BC23BC23
R23C55
寫這道題目的時候,正好復習了C++的類,於是居然鬼使神差的想設計一個類來實現它。C++的核心思想是面向對象,而此處的單元格正好有顯著的對象特征。一個單元格,應該有座標,以及其中的內容。這是很自然的,當初看C++ Primer的時候,書中也是以書本銷量作為引入,介紹並闡述類的設計。
Definitions
閑話少說,現在就來看看如何實現這個類。
// Unit/Unit.h
class Unit {
private:
using pos = unsigned int;
pos rowIdx;
pos colIdx;
public:
// constructors
Unit(pos _r, pos _c) : rowIdx(_r), colIdx(_c) { }
// constructor2
Unit(string);
};
先看這么多,我希望每個單元個有兩個屬性,一個行索引rowIdx
一個列索引colIdx
. 而且我定義了兩個構造函數,一個是直接將行列索引傳入,另一個則是通過讀取一個字符串,來解析出其中的行列索引的值。這里有一個問題,因為不同類型的字符串解析的方式不一樣,所以需要一個指示變量來指明傳入的字符串到底是哪個類型:BC23
還是R23C55
?除此之外,我還需要一個可以轉換不同類型表示的函數,給了我表示類型1,我可以直接調用一個函數,輸出表示類型2. 需求先大致到這里,來改進一下之前的類。
// Unit/Unit.h
#ifndef _UNIT_H_
#define _UNIT_H_
#include<string>
class Unit {
private:
using pos = unsigned int; // type alias
pos rowIdx;
pos colIdx;
bool isRC; // is the type `R23C55`?
void getIdx_RC(std::string); // build index for type 'R23C55'
void getIdx_NRC(std::string); // for type 'BC23'
public:
// constructor1
Unit(pos _r, pos _c) : rowIdx(_r), colIdx(_c) { }
// constructor2
Unit(std::string);
// selectors
pos getRow() { return rowIdx; }
pos getCol() { return colIdx; }
// modifiers
void setRow(pos _r) { rowIdx = _r; }
void setCol(pos _c) { colIdx = _c; }
// utilties
void printer(void);
void convertor(void);
};
#endif
這里先不要糾結定義了很多不知道要干嘛的函數,往下看類的實現,你會慢慢明白為什么需要它們。
Implementions
類的頭文件中只定義了類,除少數內聯函數外,大多數函數仍未實現。按照模塊化的哲學,新開一個文件寫類的實現。
構造函數的實現
// Unit/Unit.hpp
#include "Unit.h"
// implement constructor2
Unit::Unit(string _s) {
// wishful thinking
isRC = typeInfer(_s);
if (isRC) {
/*
* wishful thinking: suppose i have the function
* that helps me do this job
*/
getIdx_RC(_s);
} else {
// again wishful thinking
getIdx_NRC(_s);
}
}
好了,現在我們實現了第二個構造函數,它接受一個string
對象,從中解析出行列索引的值,然后初始化rowIdx
和colIdx
.
但是,我們想當然的調用了一些我們還沒有實現的函數。這里要特別注意一點:在寫程序的時候,這種想法很有用!以下思想來自我學習SICP一段時間以后的自我體會:程序往往會包含很多的函數,為什么?因為有時候一個稍微復雜的問題,並不是一個函數就能解決的,所以需要多個函數相互協調,相互調用才能共同完成或是解決一項工作。如果你費盡心思把它們都寫在一個函數里,可能你覺得很好,但是一旦程序報錯,你將無從下手,很難定位到錯誤發生在哪里。這也是程序設計講究模塊化的理由。將復雜的功能抽象成一個一個的互不干涉的模塊(子程序),在每一個小模塊里盡可能的將代碼寫好,使得它只完成並且高效准確地完成這一項任務。那么當所有模塊相互協同起來,將會使難以想象的高效,且不容易出錯,即便是出錯了,也能很快定位到錯誤發生的地點,便於調試。
這樣做的好處還有一個,就是你在寫程序的時候變得更加輕松。為什么?因為我不用考慮所有的細節,只是調用了一些函數,而實現這些函數很可能不是我們要干的活。Oh, that's cool! George will do for me.
你甚至可以去喝杯咖啡。但是現在,讓我們短暫的充當以下 George 的角色。就拿Unit
類的設計來說,我現在想要實現第二個構造函數,根據題目的意思,我可能接受兩個代表不同表示方法的string
,我要將它們解析成行列索引。讓我們回頭看看這個函數的實現,它先判斷輸入的是那個類型,然后分情況做不同的事(調用不同的函數)。這里我用到了3個wishful thinking:
- 我希望有一個叫
typeInfer()
的函數,我給它一個字符串,它告訴我這屬於哪個類型的表示方法。 - 如果是
RxCy
型,我希望有一個函數getIdx_RC()
能夠處理這種類型的輸入,解析出行列索引。 - 如果是
BC23
型,我希望有一個函數getIdx_NRC()
能夠處理這種類型的輸入,解析出行列索引。
這樣一來,我們不容易犯錯。為什么?因為這個構造函數的邏輯足夠簡單,僅僅是分兩個情況做不同的事。做的事也很簡單:僅僅是調用一個函數!如果你能保證所調用的函數不犯錯,那么整個過程也不會出錯。既簡單,又robust!
還有我個人認為的好處就是,寫程序變得簡單了。我到每一步的時候,需要什么,想象一下,假設它已經有了,我該怎么寫,怎么去調用它。這樣你就對為什么要有這個函數,以及這個函數要干什么,心知肚明了。然后上層建設好之后,我再去考慮如何實現那些想象!現在,我們來看看,之前想當然的幾個函數如何實現。
// Unit/Unit.hpp
#include "Unit.h"
#include <string>
/*
* Predefined things...
*/
bool typeInfer(std::string _s) { // infer the representation type
if (_s[0] == 'R' && std::isdigit(_s[1]))
return true;
else
return false;
}
void Unit::getIdx_RC(std::string _s) {
/*
* build the row/col index for
* type 'R23C55'
*/
_s.erase(0, 1); // remove first 'R'
// find where 'C' is
std::string::size_type c_pos = 0;
for (; c_pos != _s.size(); ++c_pos) {
if (_s[c_pos] == 'C') break;
}
auto s1 = _s.substr(0, 0 + c_pos); // s1 = "23"
auto s2 = _s.substr(c_pos + 1, _s.size()); // s2 = "55"
// set index
rowIdx = std::stoi(s1);
colIdx = std::stoi(s2);
}
void Unit::getIdx_NRC(std::string _s) {
/*
* build the row/col index for
* type 'BC23'
*/
// find the first num
std::string::size_type n_pos = 0;
for (; n_pos != _s.size(); ++n_pos) {
if (std::isdigit(_s[n_pos]))
break;
}
auto s1 = _s.substr(0, n_pos); // s1 should be "BC"
auto s2 = _s.substr(n_pos, _s.size()); // s2 should be "23"
rowIdx = std::stoi(s2);
colIdx = letter2pos(s1);
}
Note: 注意到
getIdx_RC()
和getIdx_NRC()
需要訪問類內私有變量,所以應該聲明成類的成員函數。
好了,看了上面的實現,我又想當然的引入了幾個函數。但是通過上下文,你可以很明顯的看出來我引入這個函數實干嘛用的。正是因為這個時候我需要有一個函數幫我去干這個事情,而我又不想把這些復雜的工作都寫到一個函數里面(因為容易出錯,且很難調試)。所以我引入了它們:
letter2pos()
接受一個字符串,返回解析出來的數值索引。
好吧,居然只引入了一個(╬ Ò ‸ Ó),再來看看它的實現。
// Unit/Unit.hpp
/*
* Predefined things...
*/
// return the corresponding num for a given string
int letter2pos(const std::string& _s) {
auto len = _s.size();
int res = 0;
for (; len != 0; --len) {
res = res*26 + alph2num(_s[_s.size() - len]);
// std::cout << "res: " << res << std::endl;
}
return res;
}
// return the corresponding string for a given num
std::string pos2letter(int _p) {
if (_p <= 26){
char c[1] = {num2alph(_p)};
std::string s(c);
return s;
}
std::string res;
int r = 0;
while (_p > 26) {
r = _p%26;
res += num2alph(r);
_p /= 26;
}
res += num2alph(_p);
std::reverse(res.begin(), res.end());
// cout << "res: " << res << endl;
return res;
}
哈哈,我又想象了幾個不存在的函數。它們的作用很容易通過上下文得知。letter2pos()
和pos2letter()
是一對相反的函數,它們的作用是完成BC<->55
的映射。至於alph2num()
和num2alph()
,其實也是一對相反的函數,用於檢索26個字母對應的數值。
// Unit/Unit.hpp
// global map for quick search
char MAP[26] = {'A','B','C','D','E','F','G',
'H','I','J','K','L','M','N',
'O','P','Q','R','S','T',
'U','V','W','X','Y','Z'};
// return a num for the given char
int alph2num(char _c) {
int idx = 0;
for (; idx != 26; ++idx) {
if (MAP[idx] == _c)
return idx + 1;
}
std::cerr << _c << "Not found!" << std::endl;
return 0;
}
// return a char for the given num
char num2alph(int _i) { return MAP[_i - 1]; }
轉換函數的實現
基於上面的工作,轉換函數的實現就顯得格外清晰簡單。所謂轉換函數,就是當我輸入的是類型1的字符串,它輸出轉換之后的類型2的字符串,由此達到一個轉換單元格表示方法的效果。
// Unit/Unit.hpp
/*
* Predefined things...
*/
void Unit::convertor() {
if (isRC) {
std::cout << pos2letter(colIdx)
+ std::to_string(rowIdx) << std::endl;
} else {
std::cout << "R"
+ std::to_string(rowIdx)
+ "C"
+ std::to_string(colIdx) << std::endl;
}
}
void Unit::printer() {
std::cout << "row index: " << rowIdx
<< "\ncol index: " << colIdx << std::endl;
}
最后附加一個printer()
方便打印信息。至此,整個類的設計大概就完了。值得注意的是,最后的convertor()
之所以能夠如此簡單地寫出來,是因為我們合理將一些工作模塊化,這樣帶來的好處就是可以重復利用,易於維護。
總結
我之前寫代碼,總是不注意模塊化,不注意抽象化。能一個函數完成的事為什么要寫兩個函數?然而,最后只能自食其果。一旦報錯,一步步地定位錯誤,從下往上調試,陷入苦海。也有的時候,因為函數過於復雜,把自己繞糊塗,陷入一個圈子里想不通,弄不懂,出不來。這些結果都以失敗告終!而且會打擊自信心,感覺別人寫代碼是寫代碼,我寫代碼就純粹是寫bug啊!
好在前段時間看了點SICP,B站上有視頻的,我自己也在對着書看,真的是非常好的課程。循着書中傳遞的思想,慢慢就這么寫着,發現有的問題可以寫出來了,得益於代碼結構的改變,調試錯誤也比以前稍微輕松一些。到這次寫這道編程題,要是在考試中這么寫,我肯定來不及的。但是我在課余花了不少時間構思,終於用面向對象的思想將它初步實現。雖然這個類設計的很簡單,也有很多瑕疵:
- 異常輸入的處理
- 類結構的優化以及完備性檢查
- 代碼細節的優化
但是和以前半途而廢相比,起碼完成了類的實現,雖然很粗糙。謹以此文記錄一下!
附贈:SICP的學習資源
感謝SICP視頻翻譯工作者,感謝B站up主的搬運,感謝開源社區!