回目錄

第六章 字元與字串

原 Google Sites 連結:/cppzero/di-liu-zhang-zi-yuan-yu-zi-chuan

目錄

6.1 字元型別

c276. 沒有手機的下課時間

題目大意:這題就是我們常玩的 xAxB 猜數字遊戲。答案及猜測均是不重覆的四位數字,若兩者含有相同的數字且位置相同者為 A,含有相同的數字但位置不同者為 B,判斷所給的每個猜測為幾 A 幾 B。 要算出幾 A 幾 B,我們必須將這個四位數字拆開來,再一一地去比對每一位數是否相同。我們可以用數字的方式輸入,再以 % 10 的運算一位數一位數地取出來。然後依題意算出幾 A 幾 B。

//c276. 沒有手機的下課時間 by Snail
#include <iostream>
using namespace std;

int main () {
    int a1, a2, a3, a4, g1, g2, g3, g4;
    int n, A, B;
    cin >> a4;                                  // 輸入 4 位數整數
    a1 = a4 / 1000;                             // 取千位數
    a2 = a4 / 100 % 10;                         // 取百位數
    a3 = a4 / 10 % 10;                          // 取十位數
    a4 %= 10;                                   // 取個位數
    cin >> n;
    while (n--) {
        cin >> g4;                              // 輸入 4 位數整數
        g1 = g4 / 1000;                         // 取千位數
        g2 = g4 / 100 % 10;                     // 取百位數
        g3 = g4 / 10 % 10;                      // 取十位數
        g4 %= 10;                               // 取個位數
        A = (a1 == g1) + (a2 == g2) + (a3 == g3) + (a4 == g4);
        B = (a1 == g2) + (a1 == g3) + (a1 == g4) +
            (a2 == g1) + (a2 == g3) + (a2 == g4) +
            (a3 == g1) + (a3 == g2) + (a3 == g4) +
            (a4 == g1) + (a4 == g2) + (a4 == g3);
        cout << A << "A" << B << "B\n";
    }
}

字元型別 (char)

基本上,char 也是一個整數型態,它的長度只有一個 byte,範圍則是從 -128 到 127。它和其它整數型態 (int, short, long long 等) 不同的是,它在輸入或輸出時會以字元的型式來進行。

char ch;
ch = 13;
ch *= 5;
cout << ch;

從上面的程式碼片段中我們可以看得出來,char 的變數的確可以當作整數來使用,只是它的範圍比較小,要小心溢位。但是在輸出的時候,它就不是輸出整數值了哦!以上面的程式來說,變數 ch 的值是 65,但是輸出時會輸出 ASCII 碼為 65 的字元,也就是 A。

char 在輸入時也有別於其他整數,它會一次輸入一個字元,並儲存該字元的 ASCII 碼。

char ch;
cin >> ch;
cout << (int)ch;

這段程式碼可以輸入一個字元,並輸出它的 ASCII 碼。由於字元變數在輸入時,一次只輸入一個字元,我們就可以把「沒有手機的下課時間」程式改寫如下:

//c276. 沒有手機的下課時間 by Snail
#include <iostream>
using namespace std;

int main () {
    char a1, a2, a3, a4, g1, g2, g3, g4;
    int n, A, B;
    cin >> a1 >> a2 >> a3 >> a4;                // 輸入 4 個字元
    cin >> n;
    while (n--) {
        cin >> g1 >> g2 >> g3 >> g4;            // 輸入 4 個字元
        A = (a1 == g1) + (a2 == g2) + (a3 == g3) + (a4 == g4);
        B = (a1 == g2) + (a1 == g3) + (a1 == g4) +
            (a2 == g1) + (a2 == g3) + (a2 == g4) +
            (a3 == g1) + (a3 == g2) + (a3 == g4) +
            (a4 == g1) + (a4 == g2) + (a4 == g3);
        cout << A << "A" << B << "B\n";
    }
}

在這個版本中,a1 只會輸入一個字元,也就是千位數,剩下的百位、十位、個位數則分別輸入到 a2, a3, 及 a4 了。不像 int 會輸入整個整數,之後還得把它拆開來,輸入字元時,每個字元都會分開存在不同的變數裡,不需要再去拆了。

取得白空白

c007. TeX Quotes

題意:照樣輸出所輸入的每個字元,但是第奇數個雙引號「”」要改成兩個左單引號「``」,而第偶數個雙引號要改成兩個右單引號「’‘」。

如果我們只是要把輸入的內容一模一樣地輸出,我們可以試試下面的小程式:

int main() { char ch; while (cin >> ch) cout << ch; }

執行上面的程式,輸入題目所給的範例測資:

"To be or not to be," quoth the Bard, "that
is the question".
The programming contestant replied: "I must disagree.
To `C' or not to `C', that is The Question!"

你卻得到以下的結果:

"Tobeornottobe,"quoththeBard,"thatisthequestion".Theprogrammingcontestantreplied:"Imustdisagree.To`C'orno
tto`C',thatisTheQuestion!"

也就是所有的「白空白」,包括空白與換行,統統不見了!當我們用 >> 運算子來作輸入時,它會自動跳過白空白,以致於我們無法取得輸入的白空白。如果程式中要取得輸入的白空白,我們就必須用 cin 的成員函數 get()。

要用 get() 來輸入一個字元到字元變數 ch 中有兩種用法:

ch = cin.get() 或 cin.get(ch)

cin.get() 的括號中若沒有任何參數,那麼它會回傳它所讀到的字元,我們再用指定運算子「=」把回傳的值設定給 ch。

//272 - TeX Quotes (by Snail)
#include <iostream>
using namespace std;

int main () {
    char ch;
    int n = 1;
    while (cin.get(ch)) {
        if (ch == '"')
            if (n++ % 2)                    // 第奇數個雙引號
                cout << "``";
            else
                cout << "''";
        else
            cout << ch;
   }
}

d103. NOIP 2008 1.ISBN 號碼

題目大意:判斷所輸入的 ISBN 號碼是否正確。

因為輸入進來的 “數字” 其實 c1, c2…… 存的是代表該 “數字” 的 ASCII 碼,例如 c1 = 字元「0」, c1 存的其實是 48 , 所以要做計算的時候必須先減掉 48(0 的 ASCII 碼) !

//d103. NOIP 2008 1.ISBN 號碼 by Snail
#include <iostream>
#include <string>
using namespace std;

int main () {
    char d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, cs, ch;  // cs (checksum) -- 檢查碼
    while (cin >> d1 >> ch >> d2 >> d3 >> d4 >> ch >> d5 >> d6 >> d7 >> d8 >> d9 >> ch >> d10) {
        cs = (d1*1 + d2*2 + d3*3 + d4*4 + d5*5 + d6*6 + d7*7 + d8*8 + d9*9 - '0'*45) % 11 + '0';
        if (cs > '9')
            cs = 'X';
        if (cs == d10)
            cout << "Right\n";
        else
            cout << d1 << ch << d2 << d3 << d4 << ch << d5 << d6 << d7 << d8 << d9 << ch << cs << endl;
    }
}

a001. 哈囉

a065. 提款卡密碼

d095. 579 - ClockHands

6.2 字串

c381. 聖經密碼

題目大意:給定 n 個英文單字,m 個整數。將 n 個英文單字全部接在一起成為一個字串,然後依 m 個整數所指定的位置取出 m 個字母輸出即可。

說明:

如果要把兩個字串變數接在一起,可以直接用「+」運算子來連接它們。例如下列的程式便會輸出「ABCDEF」。

string s1 = "ABC", s2 = "DEF";
cout << s1 + s2 << endl;

下面的程式碼可以用來輸入 n 個字串,並把它們接在一起。

s = "";
while (n--) {
    cin >> w;
    s = s + w;
}

a466. 12289 - One-Two-Three

a022. 迴文

要判斷一個字串是否為迴文,依序比對第一個字元和最後一個字元、第二個字元和倒數第二個字元、…… 是否一樣,如果比到中間還是一樣,那麼這個字串

幾個有關迴文的觀念

(1) 迴文左右對稱所以由左而右讀及由右而左讀都會是一樣的

(2) 同種文字出現的個數中,只有一種會出現奇數次
例如: aaabbccddeeee

a 共 3 個、b 共 2 個、c 共 2 個、d 共 2 個、e 共 4 個
將多出來的 a 擺中間即可排成左右對稱的
b a c e d e a e d e c a b

可想而知若其中有兩種以上的文字出現個數為奇數個
則多出的文字沒地方可以擺而排不出合法的字串

(3) 若題目只問排列的方法數,因為其左右對稱的特性
左半邊若被決定了,右半邊就一定只能是相對應的
所以只要排出半邊的方法數就好,可以節省時間

//a022. 迴文

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
    string i, ii;
    int a;
    while (cin >> i) {
        a = i.size();
        if (a%2)     // 若字串長度為奇數
            i.erase(a/2, 1); // 將中間字元消除 (其實在這題中消掉中間字元沒多大意義 =="")
        ii = i;
        reverse(i.begin(), i.end());         // 將字串倒過來排

        /**  reverse 用法
         *     需 #include <algorithm>
         *     設字串 a
         *     reverse(從字串中第 s 個記憶體位置 , 到字串中第 d 個記憶體位置)
         *     則從 a[s] 至 a[d] 會倒過來排
         */

        if (i == ii)                   // 如果倒過來後字串與原自串相同
            cout << "yes" << endl;
        else
            cout << "no" << endl;
    }
}

d086. 態度之重要的證明

題目大意:輸入一個字串,將其中字母所代表的數字加總輸出,A 或 a 代表 1、B 或 b 代表 2、…..,如果字串中有任何非字母的字元,請輸出 “Fail”。

說明:

雖然這題看起來是「0 尾版」,但是我們是用字串來輸入那個 0,所以「!= “0”」不可以省略,因為 “0” 不是真的 0。你也不可以把 s != “0” 寫成 s != ‘0’,因為後者是拿一個字串來和字元相比,這是不允許的。

while (cin >> s, s != "0")

要判斷一個字元是不是字母,我們可以從它的編碼去處理,如果它不是介於 ‘A’ 和 ‘Z’ 之間,也不是介於 ‘a’ 和 ‘z’ 之間,那麼它就不是字母。如果它不是字母,你就讓它立刻 break 跳出迴圈,如果程式沒有跳出去,就代表這個字元是個字母。這時候,我們還得判斷它是大寫還是小寫字母,因為要減的數字不同。程式如下:

sum = 0;
for (i=0; i!=s.size(); i++) {
    if (s[i]<'A' || s[i]>'Z' && s[i]<'a' || s[i]>'z')
        break;                          // 遇到非字母字元則跳出迴圈
    if (s[i] >= 'a')                    // 若是小寫字母
        sum += s[i] - 'a' + 1;
    else
        sum += s[i] - 'A' + 1;
}

當迴圈結束後,我們可以用 i 的值來判斷剛才的迴圈是否遇到了非字母字元。如果迴圈結束後的 i == s.size(),那就代表迴圈是「壽終正寢」地掃瞄完了整個字串,如果 i < s.size(),就代表迴圈是在字串還沒完全掃瞄完畢整個字串就遇到了非字母字元而 break 出來了。我們可以據此判斷字串中是否含有非字母字元以作出適當的輸出。

if (i == s.size())                        // 沒有非字母字元
    cout << sum << endl;
else
    cout << "Fail\n";

<ccytpe>

在 <cctype> 標頭檔中,定義了一些字元的函數,我們可以用它們來簡化我們的程式。其中 isalpha(ch) 便可以幫你判斷 ch 是不是字母,如果是字母,它會回傳一個非 0 的字,如果 ch 是字母,那麼它就會回傳 0。

另外還有一個函數 toupper(ch),如果 ch 是小寫字母,它會回傳 ch 的大寫字母。如果 ch 不是小寫字母,它就直接回傳 ch。簡化之後的程式如下:

sum = 0;
for (i=0; i!=s.size(); i++) {
    if (!isalpha(s[i]))
        break;                          // 遇到非字母字元則跳出迴圈
    sum += toupper(s[i]) - 'A' + 1;
}

中常用的函數表列於附錄 G 中。即然這些函數是定義在 <cctype> 中,理論上用到這些函數時要先 include <cctype>,但實測發現,不管是 Visual C++ 或是 ZeroJudge,不用 include <cctype> 也一樣能編譯成功。可以是其他的標頭檔把它 include 進來了。

b223: B. 外星人的訊息

c054. WERTYU

題目大意:輸入一行文字,將其中每個非空白的字元以鍵盤上在它左邊的字元取代並輸出。例如 S 以 A 取代,[ 以 P 取代….。

說明:

這題以一行為一筆測資,且其中含有空白,所以不能用 cin >> s 來輸入,因為用 >> 輸入時,遇到白空白就會停下來。例如以下程式:

while (cin >> s)
    cout << '[' << s << ']' << endl;

如果你輸入「This is a book.」,你不會得到:

[This is a book.]

而是得到:

[This]
[is]
[a]
[book.]

如果你要一次輸入一整行,不管有沒有空白在裡面,那麼就需要用到 getline() 函數了。常見的用法如下:

getline(cin, s)

這個函數和 cin >> s 一樣,會從 cin 輸入一個字串到 s 變數之中。它的回傳值也是 cin,所以同樣可以直接放在 while 迴圈的小括號中來讀取 EOF 版的測資。

while (getline(cin, s))

如果你要的話,這題也可以用 43 個 if 硬暴:

if (s[i]=='W')
    cout << 'Q';
if (s[i]=='E')
    cout << 'W';
/*   ......  */

但這樣一來程式就變得很冗長,即使 ‘2’ 到 ‘9’ 等 8 個數字可以合併成一個 if 陳述式,

if (s[i]>='2' && s[i]<='9')
    cout << char(s[i]-1);

這樣也還有 36 個 if 陳述式。有沒有比較簡潔的方法呢? 我們可以把題目中鍵盤的配置儲存在一個字串 k 中:

string k = "`1234567890-=QWERTYUIOP[]\\\\ASDFGHJKL;'ZXCVBNM,./";

要提醒您注意的是,字串中的倒斜線「\\」必須用逸出字元「\\\\」來表示。然後我們就可以在字串 k 中尋找 s 字串中所出現的字元 s[i],然後再以它在字串 k 中左邊的字元輸出。

for (j=1; k[j]!=s[i]; j++);         // 找 s[i] 出現在 k 中的位置
cout << k[j-1];                     // 以它左邊的字元輸出

要注意的是字串 k 的最前面我們擺了兩個空白字元,然後 j 是從 1 而不是從 0 開始找,如此一來,如果 s[i] 是空白的話,它會找到右邊的那個空白,然後輸出左邊的那個空白。

string.find()

其實在 string 類別中本來就定義了一個在字串中尋找某個字元的成員函數 find()。例如,我們要在字串 s 中尋找字元 ch 出現的位置的話,可以寫成 s.find(ch)。找到時,它會回傳 ch 在 s 中第一次出現的位置,如果找不到,它會回傳 string::npos,也就是 -1。你也可以在 find() 函數中提供第二個參數以指定開始尋找的位置,例如以下的程式就可以找到並輸出字串 s 中所有 ch 的位置並輸出:

for (j=s.find(ch); j!=-1; j=s.find(ch, j+1))
    cout << j << endl;

有了這個函數以後,剛剛那題在 k 中找 s[i] 的 for 迴圈就可以改成用 find() 來做了。

j = k.find(s[i], 1);                 // 找 s[i] 出現在 k 中的位置
cout << k[j-1];                     // 以它左邊的字元輸出

甚至可以直接把第一行的 find() 代入第二行的 j,如下:

cout << k[k.find(s[i], 1)-1];

整個程式就變成了:

//10082 - WERTYU (by Snail)
#include <iostream>
#include <string>
using namespace std;

int main () {
    string s, k = "  `1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";
    int i;
    while (getline(cin, s)) {
        for (i=0; i!=s.size(); i++)
            cout << k[k.find(s[i], 1)-1];
        cout << endl;
    }
}

相當簡潔,不是嗎?

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "\n\n  `1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";
    char ch;
    while (cin.get(ch))
        cout << s[s.rfind(ch, s.size() - 1) - 1];
}