APCS Python 程式識讀|自訂題目試作

APCS Python 程式識讀|自訂題目試作 30 題

本版本將既有自訂題目與補強題合併,作為 APCS Python 程式識讀的試作題庫。

題目涵蓋程式追蹤、填空、測試除錯與演算法辨識,並加入 Python 串列參照、切片及可變物件等特性。

每題分為「1. 題目」與「2. 解析」;解析預設收合,作答後再點開。

題型分布:迴圈與遞迴、搜尋與排序、字串與串列、函式參數、Stack、Queue、後序運算式、測試除錯、布林短路求值、二維串列走訪與輾轉相除法。
第 1 題

1. 題目

下列程式執行後,輸出結果為何?

total = 0

for i in range(3):
    for j in range(i, 4, 2):
        for k in range(j, 5):
            total += 1

print(total)
  1. 12
  2. 14
  3. 17
  4. 20
2. 解析(點擊後顯示)

最內層迴圈的執行次數取決於 j,因為 range(j, 5) 會產生 5-j 個整數。

ij 的值各次 k 迴圈次數小計
00、25、38
11、34、26
2233
total = 8 + 6 + 3 = 17

答案:C,17。

第 2 題

1. 題目

下列程式執行後,輸出結果為何?

t = 0

def g(x, y):
    global t
    if x == 0 and y == 1:
        t += 1
    if x <= 0 or y <= 0:
        return 3
    return 5 + f(x, y - 1)

def f(x, y):
    if x <= 0 or y <= 0:
        return 2
    return g(x - 1, y) + 2 * g(x - 2, y - 1)

print(f(5, 5), t)
  1. 229 8
  2. 229 16
  3. 226 8
  4. 226 16
2. 解析(點擊後顯示)

先計算回傳值。對正整數參數而言:

g(x, y) = 5 + f(x, y - 1)
f(x, y) = g(x - 1, y) + 2 * g(x - 2, y - 1)

由較小狀態往上計算,可得:

f(2, 2) = 20
f(3, 3) = 53
f(4, 4) = 108

f(5, 5)
= g(4, 5) + 2 * g(3, 4)
= (5 + f(4, 4)) + 2 * (5 + f(3, 3))
= 113 + 2 * 58
= 229

t 只在實際呼叫到 g(0, 1) 時增加。注意 2 * g(...) 是把回傳值乘以 2,不是呼叫函式兩次。

到達 g(0, 1) 的次數:
1 → 2 → 3 → 5 → 8

因此 t=8

答案:A,229 8

第 3 題

1. 題目

下列函式使用「左右端點都包含」的區間進行二分搜尋。空格(甲)、(乙)、(丙)應填入哪一組,才能正確搜尋?

def binary_search(values, target):
    left = 0
    right = len(values) - 1

    while (甲):
        mid = (left + right) // 2
        if values[mid] == target:
            return mid
        elif values[mid] < target:
            left = (乙)
        else:
            right = (丙)

    return -1

values = [1, 3, 4, 7, 8, 10, 12, 15]
print(binary_search(values, 10),
      binary_search(values, 9))
  1. left <= rightmid + 1mid - 1
  2. left < rightmid + 1mid - 1
  3. left <= rightmidmid
  4. left < rightmidmid
2. 解析(點擊後顯示)

目前的搜尋區間是閉區間 [left, right],左右端點都仍可能是答案,因此只要 left <= right 就要繼續搜尋。

values[mid] < target,索引 mid 已確定不是答案,下一個區間應從 mid + 1 開始;反方向同理,右端點應改為 mid - 1

while left <= right:
    ...
    left = mid + 1
    ...
    right = mid - 1

完成後輸出為 5 -1

答案:A。

第 4 題

1. 題目

下列程式執行後,輸出結果為何?

data = [95, 27, 90, 49, 80, 58, 6, 9, 18, 50]
count = 0
n = len(data)

for i in range(n - 1):
    for j in range(n - i - 1):
        if data[j] < data[j + 1]:
            data[j], data[j + 1] = data[j + 1], data[j]
            count += 1

print(count, data[0], data[-1])
  1. 14 95 6
  2. 31 95 6
  3. 14 6 95
  4. 24 95 6
2. 解析(點擊後顯示)

條件 data[j] < data[j + 1] 會在左邊元素較小時交換,讓較大的數往左移,因此最後是由大到小排序。

各輪發生的交換次數依序為:

7、4、1、1、1、0、0、0、0

總交換次數為:

7 + 4 + 1 + 1 + 1 = 14

排序結果為:

[95, 90, 80, 58, 50, 49, 27, 18, 9, 6]

因此第一個元素是 95,最後一個元素是 6。

答案:A,14 95 6

第 5 題

1. 題目

數字 1、2、3、4、5 只能依此順序放入 Stack(堆疊),但可在任意時機取出頂端元素。下列程式會判斷指定的取出順序是否可能發生。哪一個選項會使函式回傳 True

def possible(order):
    stack = []
    next_value = 1

    for x in order:
        while next_value <= 5 and \
              (not stack or stack[-1] != x):
            stack.append(next_value)
            next_value += 1

        if stack and stack[-1] == x:
            stack.pop()
        else:
            return False

    return True
  1. [3, 2, 5, 4, 1]
  2. [3, 1, 2, 5, 4]
  3. [2, 4, 1, 5, 3]
  4. [4, 2, 5, 3, 1]
2. 解析(點擊後顯示)

Stack 採用 LIFO(後進先出):最後放入的元素必須最先取出。

選項 A 可依下列步驟完成:

push 1、2、3 → pop 3、2
push 4、5   → pop 5、4、1

實際取出順序正是:

3、2、5、4、1

其他選項都曾嘗試略過仍壓在目標元素上方的數字。例如選項 B 取出 3 後,堆疊頂端是 2,不可能直接取出位於下方的 1。

答案:A,[3, 2, 5, 4, 1]

第 6 題

1. 題目

下列程式以列表搭配 front 模擬 Queue(佇列)。每次從佇列前端取出一個數字,轉換後再加入尾端。程式執行後輸出為何?

q = [2, 5, 1, 4]
front = 0

for _ in range(5):
    x = q[front]
    front += 1

    if x % 2 == 0:
        q.append(x // 2)
    else:
        q.append(x + 3)

print(q[front:])
  1. [1, 8, 4, 2]
  2. [8, 4, 2, 4]
  3. [4, 2, 4, 8]
  4. [2, 5, 1, 4]
2. 解析(點擊後顯示)

Queue 採用 FIFO(先進先出)。front 左側的資料雖然仍留在列表內,但已經視為離開佇列。

次數取出 x加入尾端處理後的有效佇列
121[5, 1, 4, 1]
258[1, 4, 1, 8]
314[4, 1, 8, 4]
442[1, 8, 4, 2]
514[8, 4, 2, 4]

最後 front=5,所以切片 q[front:] 只保留尚未出隊的四個元素。

答案:B,[8, 4, 2, 4]

第 7 題

1. 題目

下列程式使用 Stack(堆疊)計算 Postfix Expression(後序運算式)。其中除法使用整數除法。程式執行後輸出為何?

tokens = [9, 2, '-', 4, 3, '+', '*', 5, '/']
stack = []

for token in tokens:
    if isinstance(token, int):
        stack.append(token)
    else:
        b = stack.pop()
        a = stack.pop()

        if token == '+':
            stack.append(a + b)
        elif token == '-':
            stack.append(a - b)
        elif token == '*':
            stack.append(a * b)
        else:
            stack.append(a // b)

print(stack[-1])
  1. 7
  2. 9
  3. 10
  4. 49
2. 解析(點擊後顯示)

遇到數字就推入堆疊;遇到運算子時,先取出的值是右運算元 b,後取出的值是左運算元 a

讀到的片段完成的運算堆疊的重要結果
9 2 -9 - 27
4 3 +4 + 37
*7 × 749
5 /49 // 59

這串後序運算式等價於:

((9 - 2) * (4 + 3)) // 5

因為使用整數除法,所以 49 // 5 = 9

答案:B,9。

第 8 題

1. 題目

下列函式使用 Binary Search(二分搜尋)尋找數字 3。四個數列都包含 3,但哪一個數列會使函式錯誤地回傳 -1

def binary_search(values, target):
    left = 0
    right = len(values) - 1

    while left <= right:
        mid = (left + right) // 2

        if values[mid] == target:
            return mid
        elif values[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1
  1. [1, 2, 3, 4, 5, 6]
  2. [1, 4, 5, 2, 3, 6]
  3. [2, 2, 3, 4, 4, 5]
  4. [1, 1, 2, 3, 3, 3]
2. 解析(點擊後顯示)

這個版本的二分搜尋假設數列已按照「由小到大」排列。它根據中間值與目標值的大小關係,直接排除左半邊或右半邊。

選項 B 並未依照由小到大的順序排列。追蹤搜尋過程:

values = [1, 4, 5, 2, 3, 6]

left=0, right=5, mid=2
values[2]=5 > 3
→ right=1

left=0, right=1, mid=0
values[0]=1 < 3
→ left=1

left=1, right=1, mid=1
values[1]=4 > 3
→ right=0

搜尋區間因此變空,函式回傳 -1,但數字 3 實際位於索引 4。

二分搜尋每次都會根據中間值排除一半資料;只有資料已依照搜尋規則排序時,被排除的一半才保證不含目標值。本題因為資料順序混亂,第一次比較後就把真正含有 3 的右半邊排除了。

答案:B,[1, 4, 5, 2, 3, 6]

第 9 題

1. 題目

下列 Bubble Sort(氣泡排序)不使用暫存變數,也不使用 Python 的多重指定,而是利用加減法交換相鄰元素。空格(甲)、(乙)應填入哪一組,才能正確完成由小到大排序?

def bubble_sort(data):
    n = len(data)

    for i in range(n - 1):
        for j in range(n - i - 1):
            if data[j] > data[j + 1]:
                data[j] = data[j] + data[j + 1]
                data[j + 1] = data[j] - (甲)
                data[j] = data[j] - (乙)

data = [7, 2, 5, 1]
bubble_sort(data)
print(data)
  1. (甲)data[j + 1] (乙)data[j + 1]
  2. (甲)data[j]   (乙)data[j + 1]
  3. (甲)data[j + 1] (乙)data[j]
  4. (甲)data[j]   (乙)data[j]
2. 解析(點擊後顯示)

假設交換前:

data[j]     = x
data[j + 1] = y

第一行先把兩數總和存到左邊:

data[j] = x + y
data[j + 1] = y

接著用總和減去右邊原本的 y,即可把右邊改成原本的 x

data[j + 1] = (x + y) - y = x

最後再用總和減去目前右邊的 x,即可把左邊改成原本的 y

data[j] = (x + y) - x = y

因此兩個空格都必須填入 data[j + 1]。排序完成後輸出:

[1, 2, 5, 7]

答案:A。

第 10 題

1. 題目

將下列 Infix Expression(中序運算式)轉換成 Postfix Expression(後序運算式),哪一個結果正確?

3 * 5 + 6 * 8 - 8 / 9
  1. 3 5 * 6 8 * + 8 9 / -
  2. 3 5 6 8 * + * 8 9 / -
  3. 3 5 * 6 8 * 8 9 / - +
  4. 3 5 * 6 8 * + 8 9 - /
2. 解析(點擊後顯示)

乘法與除法的優先順序高於加法與減法,因此可以先看成:

(3 * 5) + (6 * 8) - (8 / 9)

三個乘除子運算式轉成後序後分別為:

3 5 *
6 8 *
8 9 /

原式的加法與減法具有相同優先順序,依由左到右結合,所以先把前兩段相加,再減去最後一段:

3 5 * 6 8 * + 8 9 / -

後序運算式不需要括號,因為運算子的出現位置已經完整表示運算順序。

答案:A,3 5 * 6 8 * + 8 9 / -

第 11 題

1. 題目

下列程式執行後輸出為何?

a = [1, 2, 3]
b = a

b[1] = 9
b.append(4)

print(a)
  1. [1, 2, 3]
  2. [1, 9, 3]
  3. [1, 2, 3, 4]
  4. [1, 9, 3, 4]
2. 解析(點擊後顯示)

b = a 沒有建立新的串列;ab 指向同一個 List(串列)物件。

因此透過 b 修改元素或加入資料,也會從 a 觀察到相同結果。

答案:D,[1, 9, 3, 4]

第 12 題

1. 題目

下列程式執行後,ab 分別為何?

a = [[1, 2], [3, 4]]
b = a[:]

b[0][0] = 9
b[1] = [7, 8]

print(a)
print(b)
  1. [[1, 2], [3, 4]]
    [[9, 2], [7, 8]]
  2. [[9, 2], [3, 4]]
    [[9, 2], [7, 8]]
  3. [[9, 2], [7, 8]]
    [[9, 2], [7, 8]]
  4. [[1, 2], [7, 8]]
    [[9, 2], [7, 8]]
2. 解析(點擊後顯示)

先用 L1~L5 為串列物件命名,方便追蹤參照關係:

L1 = [1, 2]
L2 = [3, 4]
L3 = [L1, L2]

a → L3

執行 b = a[:] 時,完整切片會建立一個新的外層串列 L4;但 L4 裡放入的仍是原本對 L1L2 的參照:

L3 = [L1, L2]
L4 = [L1, L2]

a → L3
b → L4

所以 ab 的外層串列不同,但 a[0]b[0] 共同參照 L1a[1]b[1] 共同參照 L2。這就是 Shallow Copy(淺層複製)。

= 到底做了什麼?

= 本身不保證「建立新物件」。指定敘述會先計算右側運算式,取得某個物件,再把左側指定目標改成參照該物件。

  • x = y:通常只是讓 x 參照 y 已經參照的物件,不會自動複製。
  • b = a[:]:是右側的切片運算建立新串列 L4,再讓 b 參照 L4
  • b[1] = [7, 8]:是右側的串列常值建立新串列 L5,再把 b[1] 改成參照 L5

第一個操作:b[0][0] = 9

先沿著參照找到真正要修改的格子:

b → L4
b[0] → L1
b[0][0] → L1[0]

因此這行真正修改的是 L1[0]:把它原本指向整數 1 的參照,換成指向整數 9 的參照。

修改前:L1 = [1, 2]
修改後:L1 = [9, 2]

整數 1 本身沒有被改成 9;被改的是 L1[0] 這一格所保存的參照。因為 a[0]b[0] 都指向同一個 L1,所以兩邊都會看到 [9, 2]

第二個操作:b[1] = [7, 8]

右側 [7, 8] 會建立新的串列物件,暫時命名為 L5

L5 = [7, 8]

左側指定目標是 b[1],也就是 L4[1]。因此程式把 L4[1] 原本指向 L2 的參照,改成指向 L5

修改前:
L3 = [L1, L2]
L4 = [L1, L2]

修改後:
L3 = [L1, L2]
L4 = [L1, L5]

這個操作只改到 L4[1],沒有進入或修改 L2,也沒有修改 L3[1]。因此:

a[1] → L2 → [3, 4]
b[1] → L5 → [7, 8]
程式真正被修改的格子被替換的參照原物件是否被修改
b[0][0] = 9 L1[0] 1 → 9 整數 1 沒被修改
b[1] = [7, 8] L4[1] L2 → L5 L2 沒被修改

最後的參照關係為:

L1 = [9, 2]
L2 = [3, 4]
L5 = [7, 8]

a → L3 = [L1, L2]
b → L4 = [L1, L5]

所以輸出為:

[[9, 2], [3, 4]]
[[9, 2], [7, 8]]

答案:B。

第 13 題

1. 題目

下列程式原本想建立三個互相獨立的列。執行後實際輸出為何?

grid = [[0] * 3] * 3
grid[1][2] = 7

print(grid)
  1. [[0, 0, 0], [0, 0, 7], [0, 0, 0]]
  2. [[0, 0, 7], [0, 0, 7], [0, 0, 7]]
  3. [[7, 0, 0], [7, 0, 0], [7, 0, 0]]
  4. 程式會在建立 grid 時發生錯誤
2. 解析(點擊後顯示)

我們用紙筆標籤來表示物件:

  • K:代表整數物件 0
  • M:代表整數物件 7
  • L1:代表內層串列物件
  • L2:代表最外層的 grid 串列物件

KM 是為了畫圖方便所使用的標籤;實際 Python 程式中沒有建立名為 KM 的變數。

第一步:計算 [0] * 3

右側先建立一個只含 0 的串列,再把其中的元素參照重複三次。概念上可以寫成:

K = 整數物件 0
L1 = [K, K, K]

也就是:

L1[0] → K
L1[1] → K
L1[2] → K

L1 的三個格子都參照整數物件 0。整數是 Immutable Object(不可變物件),但這不是本題發生三列連動的主要原因。

第二步:計算 [L1] * 3

外層的 * 3 並不會建立三個獨立的內層串列,而是把同一個 L1 的參照放入外層三次:

L2 = [L1, L1, L1]
grid → L2

因此:

grid[0] → L1
grid[1] → L1
grid[2] → L1

這三個位置看到的是同一個可變串列物件 L1

第三步:執行 grid[1][2] = 7

先追蹤左側到底指向哪一個格子:

grid      → L2
grid[1]   → L1
grid[1][2]→ L1[2]

右側的 7 會求值得到整數物件 7,我們用紙筆標籤 M 表示它。

所以這行真正做的是:

原本:L1[2] → K
後來:L1[2] → M

也就是把 L1 索引 2 這一格所保存的參照,從整數物件 0 改成整數物件 7

修改前:L1 = [K, K, K]
修改後:L1 = [K, K, M]

這裡沒有把 K 物件本身改成 M,也沒有改變其他仍指向 K 的格子;真正被修改的是 L1[2] 這個串列位置。

為什麼三列都出現 7?

因為外層三個位置全部指向同一個 L1

grid[0] → L1
grid[1] → L1
grid[2] → L1

L1 現在已經是:

[K, K, M] = [0, 0, 7]

所以從 grid[0]grid[1]grid[2] 查看,都會看到同一份內容:

[[0, 0, 7], [0, 0, 7], [0, 0, 7]]
操作真正被修改的位置參照變化沒有被修改的東西
grid[1][2] = 7 L1[2] K → M K 代表的整數物件 0、以及 L1[0]L1[1]

如何建立三個互相獨立的列?

要在每一輪都重新建立一個新的內層串列:

grid = [[0] * 3 for _ in range(3)]

概念上會得到三個不同物件:

L1 = [K, K, K]
L2 = [K, K, K]
L3 = [K, K, K]

grid = [L1, L2, L3]

此時修改其中一列,不會連動其他列。

答案:B。

第 14 題

1. 題目

下列程式執行後輸出為何?

def change(x, data):
    x = x + 5
    data[0] = data[0] + 5
    data = data + [99]

n = 10
a = [10]
change(n, a)

print(n, a)
  1. 10 [10]
  2. 15 [15, 99]
  3. 10 [15]
  4. 15 [15]
2. 解析(點擊後顯示)

這題要分清楚兩種操作:

  • 修改物件內容:例如 data[0] = ...,被修改的是串列物件裡的某一格,串列本身仍是原本同一個物件。
  • 重新指定名稱:例如 data = ...,被改變的是名稱 data 的參照;原本的物件不會因此在所有地方一起被替換。

先替物件加上紙筆標籤

為了追蹤參照關係,我們使用下列標籤:

  • K:整數物件 10
  • M:整數物件 15
  • N:整數物件 99
  • L1:原本的串列物件 [10]
  • L2:右側串列常值所建立的串列物件 [99]
  • L3:串接後產生的新串列物件 [15, 99]

這些字母只是紙筆分析用的標籤;原程式中沒有真的宣告 KMNL1L2L3

第一步:建立外部變數

n = 10
a = [10]

概念上可表示為:

K = 整數物件 10
L1 = [K]

n → K
a → L1

第二步:呼叫 change(n, a)

呼叫函式時,不會自動複製 10 或串列 [10]。函式參數名稱會參照呼叫端傳入的物件:

外部:
n    → K
a    → L1

函式內:
x    → K
data → L1

因此一開始:

  • nx 都參照整數物件 K
  • adata 都參照同一個串列物件 L1

第三步:執行 x = x + 5

先計算右側:

x + 5
10 + 5
→ 15

右側運算得到整數物件 15,用標籤 M 表示。接著 = 只讓函式內的名稱 x 改為參照 M

執行前:
n → K
x → K

執行後:
n → K
x → M

真正被修改的是名稱 x 的參照;整數物件 K 沒有被改成 M,外部名稱 n 也沒有被重新指定,所以 n 仍是 10

第四步:執行 data[0] = data[0] + 5

先追蹤左側位置:

data    → L1
data[0] → L1[0]

右側 data[0] + 5 得到整數物件 15,也就是 M。因此真正發生的是:

原本:L1[0] → K
後來:L1[0] → M

修改後:

L1 = [M] = [15]

這裡修改的是原串列物件 L1 的內容。L1 仍然是原本同一個串列物件,只是索引 0 保存的參照由 K 換成 M

因為外部的 a 和函式內的 data 都參照 L1

a    → L1
data → L1

所以外部的 a 也會看到 L1 已變成 [15]

第五步:為何 data = data + [99] 會產生新串列?

這一行必須分成「計算右側」與「指定左側」兩部分:

5.1 先建立右側的 [99]

串列常值 [99] 會建立新的串列物件,假設叫做 L2

N = 整數物件 99
L2 = [N] = [99]

5.2 再計算 data + [99]

此時:

data → L1
L1 = [M] = [15]

L2 = [N] = [99]

串列的 + 是 Concatenation(串接)運算。它會建立一個新的串列,把左右兩個串列中的元素參照依序放入新串列,而不會直接修改原本的 L1L2

因此:

L1 + L2
→ 建立 L3

L3 = [M, N] = [15, 99]

運算完成後,三個串列物件同時存在:

L1 = [15]
L2 = [99]
L3 = [15, 99]

5.3 最後執行 data = L3

右側完成後,= 才讓函式內的名稱 data 改為參照新串列 L3

執行前:
a    → L1
data → L1

執行後:
a    → L1
data → L3

所以這裡不是把原本的 L1 在所有地方「整個換掉」,而只是重新指定函式內的區域名稱 data

外部名稱 a 沒有被重新指定,仍然參照原本的 L1

a    → L1 = [15]
data → L3 = [15, 99]

函式結束後,區域名稱 xdata 消失;外部仍然是:

n → K  = 10
a → L1 = [15]

= 本身不一定建立新物件

本題中,新串列不是由 = 自己建立,而是由右側運算式建立:

  • [99] 建立新的串列 L2
  • data + [99] 的串接運算建立新的串列 L3
  • = 最後只把左側名稱 data 改為參照 L3

例如 x = y 通常不會建立副本,只會讓 x 也參照 y 當時所參照的物件。

三個指定敘述對照

程式 真正被修改的目標 參照變化 外部是否受影響
x = x + 5 區域名稱 x x:K → M n 不受影響
data[0] = data[0] + 5 原串列 L1[0] L1[0]:K → M a 會看到 L1 的修改
data = data + [99] 區域名稱 data data:L1 → L3 a 仍參照 L1

append() 的差別

若最後一行改成:

data.append(99)

它會直接修改原本的 L1

L1:[15] → [15, 99]

因為 a 也參照 L1,此時外部的 a 就會變成 [15, 99]。但原題使用的是 data = data + [99],所以建立新串列後只重新指定區域名稱 data

最後結果

n = 10
a = [15]

因此:

print(n, a)
→ 10 [15]

答案:C,10 [15]

第 15 題

1. 題目

下列程式執行後輸出為何?

a = [3, 1, 2]
b = a.sort()
c = sorted(a, reverse=True)

print(a)
print(b)
print(c)
  1. [1, 2, 3]
    None
    [3, 2, 1]
  2. [3, 1, 2]
    [1, 2, 3]
    [3, 2, 1]
  3. [1, 2, 3]
    [1, 2, 3]
    [3, 2, 1]
  4. [3, 2, 1]
    None
    [1, 2, 3]
2. 解析(點擊後顯示)

list.sort() 直接修改原串列,並回傳 Nonesorted() 則建立並回傳新的排序結果。

因此 a 先變成遞增排序,bNone,而 c 是依目前 a 建立的遞減排序新串列。

答案:A。

第 16 題

1. 題目

下列程式執行後輸出為何?

s = "ABCDEFG"
print(s[-2], s[1:6:2], s[::-2])
  1. F BDF GECA
  2. F BDF ACEG
  3. G BCE GEC
  4. F BDF GFEDCBA
2. 解析(點擊後顯示)

s[-2] 是倒數第二個字元 Fs[1:6:2] 取索引 1、3、5,得到 BDF

s[::-2] 從最後一個字元開始,每次向左兩格,索引依序為 6、4、2、0,得到 GECA

答案:A。

第 17 題

1. 題目

下列程式執行後輸出為何?

def check(s):
    left = 0
    right = len(s) - 1
    count = 0

    while left < right:
        count += 1
        if s[left] != s[right]:
            return False, count
        left += 1
        right -= 1

    return True, count

print(check("LEVEL"), check("LEVER"))
  1. (True, 2) (False, 1)
  2. (True, 3) (False, 2)
  3. (False, 2) (True, 1)
  4. (True, 5) (False, 5)
2. 解析(點擊後顯示)

LEVEL 只需比較第 0 與 4、以及第 1 與 3 個字元,中間字元不需與自己比較,因此共 2 次。

LEVER 的第一組字元 LR 就不同,第一次比較後立即回傳。

答案:A。

第 18 題

1. 題目

下列程式要將 AAABBCCCC 壓縮成 A3B2C4。空格應填入哪一行?

s = "AAABBCCCC"
result = ""
count = 1

for i in range(1, len(s)):
    if s[i] == s[i - 1]:
        count += 1
    else:
        result += s[i - 1] + str(count)
        count = 1

____________________________
print(result)
  1. result += s[0] + str(count)
  2. result += s[-1] + str(count)
  3. result += s[i - 1] + str(i)
  4. result += str(count) + s[0]
2. 解析(點擊後顯示)

迴圈只在字元發生變化時輸出前一組,因此最後一組 CCCC 不會在迴圈內被加入。

迴圈結束後,最後字元是 s[-1],而 count 保存最後一組的數量。

答案:B,result += s[-1] + str(count)

第 19 題

1. 題目

下列 Selection Sort 執行完第一次外層迴圈,也就是 i == 0 的那一輪後,a 為何,且實際交換幾次?

a = [7, 3, 5, 2]

for i in range(len(a) - 1):
    pos = i
    for j in range(i + 1, len(a)):
        if a[j] < a[pos]:
            pos = j
    a[i], a[pos] = a[pos], a[i]
  1. [3, 7, 5, 2]
  2. [2, 3, 5, 7],且第一輪發生兩次交換
  3. [2, 3, 5, 7],且第一輪發生三次交換
  4. [2, 3, 5, 7],且第一輪只在最後交換一次
2. 解析(點擊後顯示)

第一輪會掃描索引 1~3,記住最小值 2 的位置,直到內層迴圈結束後才與 a[0] 交換。

因此第一輪結束後是 [2, 3, 5, 7],而不是每次發現較小值就交換。

答案:D。

第 20 題

1. 題目

下列程式將索引 3 的元素插入前方已排序區域。執行後 a 為何?

a = [2, 5, 7, 3, 9]

key = a[3]
j = 2

while j >= 0 and a[j] > key:
    a[j + 1] = a[j]
    j -= 1

a[j + 1] = key
  1. [2, 5, 3, 7, 9]
  2. [2, 3, 5, 7, 9]
  3. [3, 2, 5, 7, 9]
  4. [2, 5, 7, 9, 3]
2. 解析(點擊後顯示)

key=3。7 與 5 都比 3 大,因此依序向右移動;2 不大於 3,迴圈停止,最後將 3 放到索引 1。

答案:B,[2, 3, 5, 7, 9]

第 21 題

1. 題目

下列程式的主要特徵是選出一個 key,把較大的元素向右搬移,再把 key 放入空出的正確位置。這是哪一種演算法?

for i in range(1, len(a)):
    key = a[i]
    j = i - 1

    while j >= 0 and a[j] > key:
        a[j + 1] = a[j]
        j -= 1

    a[j + 1] = key
  1. Bubble Sort(氣泡排序)
  2. Selection Sort(選擇排序)
  3. Insertion Sort(插入排序)
  4. Binary Search(二分搜尋)
2. 解析(點擊後顯示)

程式維持左側的已排序區域,每輪把新元素插入其中,這是 Insertion Sort。

Bubble Sort 以相鄰比較交換為主;Selection Sort 每輪尋找剩餘資料中的最小值。

答案:C。

第 22 題

1. 題目

下列程式執行後輸出為何?

a = [4, 8, 1, 6, 9, 3]
target = 9
comparisons = 0

for i in range(len(a)):
    comparisons += 1
    if a[i] == target:
        break

print(i, comparisons)
  1. 4 4
  2. 4 5
  3. 5 5
  4. 5 6
2. 解析(點擊後顯示)

目標 9 位於索引 4。程式會比較索引 0、1、2、3、4,共 5 次,找到後停止。

答案:B,4 5

第 23 題

1. 題目

下列函式想回傳串列中的最大值,但初始化方式有問題。哪一組輸入最能測出錯誤?

def find_max(a):
    answer = 0

    for x in a:
        if x > answer:
            answer = x

    return answer
  1. [1, 2, 3]
  2. [0, 5, 2]
  3. [-5, -2, -8]
  4. [10]
2. 解析(點擊後顯示)

所有元素都是負數時,沒有任何元素大於初始值 0,函式會錯誤回傳 0,而實際最大值應為 -2。

較可靠的初始化方式是使用 answer = a[0],再從後續元素開始比較。

答案:C。

第 24 題

1. 題目

下列函式要回傳已排序串列中 target 第一次出現的索引。找到目標後,空格應填什麼?

def first_pos(values, target):
    left = 0
    right = len(values) - 1
    answer = -1

    while left <= right:
        mid = (left + right) // 2

        if values[mid] == target:
            answer = mid
            __________________
        elif values[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return answer
  1. left = mid + 1
  2. right = mid - 1
  3. return mid
  4. right = mid + 1
2. 解析(點擊後顯示)

找到一個目標後,先記錄索引,但還要繼續檢查左半部是否有更早出現的相同值,因此應將右界移到 mid - 1

答案:B。

第 25 題

1. 題目

下列程式以固定長度串列模擬 Stack。關於這組函式,下列敘述何者正確?

storage = [None] * 3
top = 0

def push(x):
    global top
    storage[top] = x
    top += 1

def pop():
    global top
    top -= 1
    return storage[top]
  1. 空堆疊呼叫 pop() 時會回傳 None,且 top 仍維持 0
  2. 空堆疊呼叫 pop() 時,top 會變成 -1,並讀取串列最後一格,未正確偵測下溢
  3. 堆疊滿時呼叫 push(),Python 會自動擴充 storage
  4. top 永遠指向目前堆疊頂端的元素
2. 解析(點擊後顯示)

這裡的 top 指向「下一個可寫入位置」。空堆疊時為 0,若直接 pop(),會先減成 -1。

Python 的負索引 -1 會讀取最後一格,所以程式不一定立即報錯,反而可能回傳無效資料,這是更隱蔽的 Stack Underflow 問題。

答案:B。

第 26 題

1. 題目

下列程式以固定長度串列模擬 Queue。依序加入 10、20、30,取出兩次,再加入 40、50。此時若再加入 60,主要問題是什麼?

Q = [None] * 5
front = 0
rear = 0

def enqueue(x):
    global rear
    Q[rear] = x
    rear += 1

def dequeue():
    global front
    x = Q[front]
    front += 1
    return x
  1. 60 會自動存到索引 0,因此沒有問題
  2. 雖然索引 0、1 已空出,但 rear 已到 5,再寫入會越界;函式沒有重用前端空間
  3. dequeue() 會從尾端取出資料
  4. Queue 中仍有五個有效元素,因此本來就不應再加入
2. 解析(點擊後顯示)

取出兩次後,索引 0、1 已不再保存有效佇列資料,但 rear 只會持續增加,不會回到前方空位。

加入 40、50 後 rear=5,下一次執行 Q[5] = 60 會越界。可用 Circular Queue(循環佇列)或搬移有效資料解決。

答案:B。

第 27 題

1. 題目

下列程式執行後輸出為何?

a = [1, 2, 2, 2, 3]

for x in a:
    if x == 2:
        a.remove(x)

print(a)
  1. [1, 3]
  2. [1, 2, 3]
  3. [1, 2, 2, 3]
  4. 迴圈會無限執行
2. 解析(點擊後顯示)

迴圈走訪串列時刪除元素,後方元素會向左移,但迭代位置仍繼續前進,因此某些元素會被跳過。

本題第一次刪除後,下一個 2 移到目前索引卻未被檢查;最後仍留下其中一個 2。

答案:B,[1, 2, 3]

第 28 題

1. 題目

下列程式執行後輸出為何?

calls = 0

def check(x):
    global calls
    calls += 1
    return x > 0

result = check(0) and check(1) or check(2)

print(result, calls)
  1. False 1
  2. False 2
  3. True 2
  4. True 3
2. 解析(點擊後顯示)

and 的優先順序高於 or,因此可先理解成:

(check(0) and check(1)) or check(2)

第一次呼叫:

check(0)
calls = 1
回傳 False

因為 False and ... 的結果已確定為 False,所以 check(1) 不會執行。

接著運算:

False or check(2)

or 左側為 False,因此必須計算右側:

check(2)
calls = 2
回傳 True
運算是否執行回傳值calls
check(0)False1
check(1)否,因 and 短路1
check(2)True2

所以輸出為:

True 2

答案:C。

第 29 題

1. 題目

下列程式執行後輸出為何?

grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

total = 0

for i in range(len(grid)):
    for j in range(len(grid[i])):
        if i == j or i + j == len(grid) - 1:
            total += grid[i][j]

print(total)
  1. 20
  2. 25
  3. 30
  4. 35
2. 解析(點擊後顯示)

grid 是一個 3×3 的二維串列:

索引       j=0  j=1  j=2
i=0         1    2    3
i=1         4    5    6
i=2         7    8    9

i == j 代表主對角線:

(0, 0) → 1
(1, 1) → 5
(2, 2) → 9

i + j == len(grid) - 1 代表副對角線。因為 len(grid) - 1 = 2

(0, 2) → 3
(1, 1) → 5
(2, 0) → 7

中心位置 (1, 1) 同時符合兩個條件,但同一回合的 if 只會執行一次,因此 5 只加一次。

1 + 3 + 5 + 7 + 9 = 25
座標符合條件
(0, 0)1主對角線
(0, 2)3副對角線
(1, 1)5兩者皆符合,但只加一次
(2, 0)7副對角線
(2, 2)9主對角線

所以輸出為:

25

答案:B。

第 30 題

1. 題目

下列程式執行後輸出為何?

a = 48
b = 18
count = 0

while b != 0:
    a, b = b, a % b
    count += 1

print(a, count)
  1. 6 2
  2. 6 3
  3. 12 2
  4. 12 3
2. 解析(點擊後顯示)

這段程式使用 Euclidean Algorithm(輾轉相除法)計算最大公因數。

這一行會先使用更新前的 ab 計算右側,再同時指定給左側:

a, b = b, a % b
回合原本 a原本 ba % b更新後 (a, b)count
1481812(18, 12)1
218126(12, 6)2
31260(6, 0)3

第三回合後 b = 0,所以 b != 0 不成立,迴圈結束。

a = 6
count = 3

所以輸出為:

6 3

答案:B。