程式設計的第一堂課

C++ 基礎語法

簡單題目(一):

輸入一個數字 $n$,代表接下來會輸入 $n$ 個數字,請輸出那些數字的總和。 Input: 5 1 2 3 4 5 Output: 15

//practice
#include <iostream>
using namespace std;

int main()
{
  int sum = 0, tmp, runt;
  cin >> runt;
  for (int i = 0; i < runt; i++)
  {
    cin >> tmp;
    sum += tmp;
  }
  cout << sum << "\n";
  return 0;
}

簡單題目(二):

輸入一個字串,然後將其反轉輸出。 Input: ABCDE Output: EDCBA

//practice
#include <iostream>
using namespace std;

int main()
{
  char str[5];
  cin >> str;
  int i = strlen(str);
  // 計算字元數量可以用 strlen
  while (i >= 0)
  {
    cout << str[i];
    i--;
  }
  cout << "\n";

  return 0;
}

未定義行爲(Undefined Behavior)

未定義行爲是有可能可以編譯過的,但是執行結果會與編譯器有關。

最常見的比如說:

cout << 1 / 0 << endl;

編譯器有時候會做一些優化動作,比如以下:

int func(unsigned char x)
{
    int value; /* assuming 32 bit int */
    cin >> value; // type 2147483647
    if (value + 1 < value)
        bar();
    return value;
}

由於編譯器知道 if 不會成立,因此可能程式碼會被優化成:

int func(unsigned char x)
{
    int value = 2147483647; 
    return value;
}

但事實上 value + 1 會 overflow 變成負數,此時 bar() 會被觸發。

浮點數誤差

在現在的 CPU 中,幾乎都支援雙精度浮點數運算,因此 floatdouble 的效能幾乎是相同的,但 float 精度是 $10^{-6}$,而 double 的效能是 $10^{-15}$;因此請使用 doublefloat bad。

string

C 語言沒有所謂的 string,我們都用 char 陣列模擬,遇到 /0 中止。 而 C++ 的 STL 有提供所謂 string,讓我們可以很方便的進行 IO、字串操作。

I/O

C++ 鼓勵使用 stream 來進行 I/O,且純 stream 效能比 stdio 高。

int a; double b; string c; string d;
cin >> a >> b >> c;
cin.getline(str); 
// 注意此時 str 可能會讀到空的東西,因爲 buffer 中可能有換行,預設 getline 使用 /n 劃分

I/O 優化

stream 雖然本身比 stdio 效能高,但因爲要頻繁 flush 且要與 stdio 同步,因此我們可以選擇關閉同步;

ios::sync_with_stdio(false);
cin.tie(0);

且與其使用 endl 換行,不如使用 \n 換行,因爲前者會 flush buffer。

有興趣可以參考這篇文章,寫的很仔細。

模板(template)

模板是個非常重要的概念!

這是一隻我覺得拍的不怎麼好但簡潔有力的影片

不過注意模板其實是語法糖,會在編譯器間展開,所以同一個模板函數可能會生出很多不同函數,如果用 static variable 要注意哦。

會發現 intmyswapcntdouble 的完全無關,因為在編譯階段就會被展開成兩個不同函數!

有興趣可以參考這篇文章,但內容可能會有點艱澀。

巨集(macro)

巨集是編譯器提供的很強大的工具,我們可以簡單的使用:

#define REP(i, n) for(int i = 0; i < n; i ++)

REP(i, 3) {
    REP(j, 3) {
        cout << i << " " << j << endl;
    }
}

也可以用巨集寫出偽函數:

#define max(a, b) (a > b ? a : b)

cout << (max(5, 3) + 3) << endl;

不過要注意,如果你是下面的寫法,就可能會出問題:

#define max(a, b) a > b ? a : b

cout << (max(5, 3) + 3) << endl;

因為 define 的本質是帶入,因此會變成:

cout << (5 > 3 ? 5 : 3 + 3) << endl;

他和模板類似,都是在編譯期進行展開。

有興趣可以參考這篇文章,講解 Linux 核心原始程式碼巨集,相當有趣。

如果有興趣,還有一個叫做 inline 的 C++ feature,也是將函式在呼叫他的地方展開,能節省呼叫時間;但不能做遞迴操作,這邊不細談。

編譯器優化

前面有提到,編譯器會進行所謂的優化(因此你寫組合語言寫出來的東西可能還沒有寫 C 語言快,因為 gcc 的優化做的不錯)。

在你的程式前面加上這一行就可以了:

#pragma GCC optimize("Ofast,unroll-loops")

Ofast 可以改成 O1O2O3,代表要進行多少優化(數字越小優化越多),越多優化程式就越偏移你的本意,這是雙面刃;但我迄今沒有出現過因為編譯器優化而導致程式執行錯誤。

有興趣可以參考這篇文章

時間複雜度

一隻簡潔扼要的影片,但你看完可能不會分析複雜度。

複雜度有什麼用呢?能讓我們估算程式的執行時間。

雖然說 $O(N) = O(2N)$,但我們在寫 code 的時候還是儘量估準一點。 以現在電腦的運算量,一秒大概可以執行 $5 * 10^8$ 計算。

所以一個 $O(N^3)$ 的演算法,如果 $N = 1000$,他可能會需要一到兩秒來執行;在有優化的簡單操作下,也是有可能壓到一秒以下的,

例題:

void bubble_sort(int arr[], int len) {
    int i, j;
    for (i = 0; i < len - 1; i++)
        for (j = 0; j < len - 1 - i; j++)
            if (arr[j] > arr[j + 1])
                swap(arr[j], arr[j + 1]);
}

這個叫做 bubble_sort,是很經典的排序演算法,可以參考下圖:

每一次循環最大個的都會跑到最後面,因此可以完成排序。

請分析看看他的時間複雜度吧!

可以看看這隻影片,會讓你更瞭解這個演算法,也可以驗證你的答案是否正確哦。

我們之後的所有操作,都會探討時間複雜度。

資料結構

基本上電腦程式的動態資訊都是儲存在記憶體(RAM)裡面,RAM 的全名是 Random Access Memory,其中 Random Access 就是你可以任意存取其中某一位。

而 RAM 就是你可以在 O(1) 時間內存取某一個地址的一個電腦元件。

而資料結構就是在電腦當中要用怎樣的格式儲存資料,不同的格式能帶來不同的優缺點。

可以先參考 基本資料結構: linked list、queue、stack,再參考 STL 所提供的資料結構