Download as pdf or txt
Download as pdf or txt
You are on page 1of 101

Chapter 7

• Problem Solving
and Algorithms

1 Copyright © 滄海圖書
⽬標
研讀完本章後,你應該可以:
• 描述電腦問題解決過程以 • 區分⼀個選擇排序與⼀個
及將它與Polya 的「如何 插⼊排序。
解決它」(How to SolveIt 說明快速排序演算法。
) 表單之間建⽴關聯性。

區分簡單型態與複合型 • 以⼿寫⽅式應⽤選擇排

態。 序、插⼊排序、泡沫排序
與快速排序到⼀個陣列的
• 描述三種複合資料結構的 項⽬上。
機制。 • 應⽤⼆元搜索演算法。
• 分辨遞迴問題及寫⼀個遞 說明你對本章中所提的演
迴演算法來解決它。 •
算法之了解程度,並以⼀
• 區分⼀個未排序的陣列與 序列的項⽬⽤⼿寫⽅式模
⼀個已排序的陣列。 擬這些演算法。
2 Copyright © 滄海圖書
7.1 如何解決問題-1
• 1945 年,G. Polya 寫了⼀本名為《如何解決
它:⼀種數學⽅法的新觀念》(How to
Solve It: A New Aspect of Mathematical
Method) 的書。
• 雖然這本書是在超過60 年前當電腦還是實
驗品的時候寫成的,但它提供了問題解決
過程的經典描述,這種過程摘錄於圖7.1。
3 Copyright © 滄海圖書
圖 7.1 Polya 的「如何解決它」表單

4 Copyright © 滄海圖書
如何解決問題-2
• 雖然他所寫的內容是解決數學問題,但我
們可以⽤問題(problem) 來取代未知
數(unknown),⽤資訊(information) 來取
代資料(data),⽤解答(solution) 來取代理
論(theorem),這樣便可將表單應⽤於任何
類型的問題上。

5 Copyright © 滄海圖書
Problem Solving (1 of 3)

Problem Solving
The act of finding a solution to a perplexing,
distressing, vexing, or unsettled question

How do you define problem solving?

6
Strategies (1 of 3)
Ask questions!
• What do I know about the problem?
• What is the information that I have to process
in order the find the solution?
• What does the solution look like?
• What sort of special cases exist?
• How will I recognize that I have found
the solution?

7
提出問題
• 某些你應該問的典型問題如下:
• 關於這個問題我知道什麼?
• 解答看起來像什麼?
• 能夠存在的特殊情況種類是什麼?
• 我要如何辨識所找到的解答?

8 Copyright © 滄海圖書
Don't reinvent the wheel!

• 你應該不會將事情都重新做起。
• 如果有⼀種解決⽅法存在,那就使⽤它。
• 如果你以前已經解決過相同或類似的問
題,你只要重複這些成功的解答⽅法即
可。
Can you think of two similar
problems?
9 Copyright © 滄海圖書
Divide and conquer

• Break up a large problem into smaller units


and solve each smaller problem
• The divide-and-conquer approach can be
applied over and over again until each subtask
is manageable

10 Copyright © 滄海圖書
演算法
表單的第⼆步驟最後⼀⾏說,你最後
應該會得到解答的計畫。⽤在計算的領域
• Polya

時,這個計畫稱為演算法(algorithm)。
• 演算法是以有限時間⽤有限資料量來解決
問題或⼦問題的⼀組指令。這個定義暗⽰
指令是不含糊的。
• Polya 表單的第三步驟是要完成計畫,也就
是說,測試這個解答來看它是否能解決問
題。
• 第四步驟則是要檢查這個解答未來是否適
11⽤。 Copyright © 滄海圖書
Computer Problem-Solving

• 問題解答有四個階段:
1. 分析與定義階段(analysis and specification
phase)
2. 演算法開發階段(algorithm development phase)
3. 實作階段(implementation phase)
4. 維護階段(maintenance phase)

12 Copyright © 滄海圖書
電腦的問題解決過程-2
• ⿊⾊線顯⽰通過各階
段的⼀般流程。
• 紅⾊線代表如果有問
題發⽣,回溯到前⼀
個階段的路徑。

四個問題解決階段之
13 間的互動情形
Algorithms
Algorithm
A set of unambiguous instructions for solving a
problem or subproblem in a finite amount of
time using a finite amount of data
Abstract Step
An algorithmic step containing unspecified details
Concrete Step
An algorithm step in which all details are specified

14
電腦的問題解決過程-2
• 這些階段間如何相互
影響的關聯。紅⾊線
顯⽰通過各階段的⼀
分析與定義
(測試)

般流程。
• 灰⾊線代表如果有問
演算法開發
(⼀般解答)
(測試)

題發⽣,回溯到前⼀
個階段的路徑。 實作
(具體解答)
(測試)

四個問題解決階段之 維護
15 間的互動情形 Copyright © 滄海圖書
Summary of Methodology
Analyze the Problem
Understand the problem!!
Develop a plan of attack
List the Main Tasks (Becomes Main Module)
Restate problem as a list of tasks (modules)
Give each task a name
Write the Remaining Modules
Restate each abstract module as a list of tasks
Give each task a name
Re-sequence and Revise as Necessary
Process ends when all steps (modules) are concrete
16
⽅法的總結
• 設計演算法的⽅法可以被分成四個主要步
驟:
1. 分析問題
2. 列出主要任務
3. 寫出剩餘部分的模組
4. 如果有必要,重新排好順序並做修改

17 Copyright © 滄海圖書
測試演算法
• 測試演算法通常包含執⾏⼀個程式包括了
在不同條件下編碼的演算法,以及為問題
檢測結果。
• 越早發現問題並加以修正,則花費成本越
低,也越容易處理。

18 Copyright © 滄海圖書
Algorithms
Algorithm
A set of unambiguous instructions for solving a
problem or subproblem in a finite amount of
time using a finite amount of data
Abstract Step
An algorithmic step containing unspecified details
Concrete Step
An algorithm step in which all details are specified

19
Algorithm with Selection

Problem: Write the appropriate dress for a given


temperature.

Write "Enter temperature"


Read temperature
Determine Dress

Which statements are


concrete?
Which statements are abstract?

20
Algorithm with Selection

Determine dress
• 將氣溫和我們的描述連結在⼀起:
• 超過(華式) 90 度代表熱,
• 70 度代表良好
• 50 度以上代表冷
• 32 度以上表⽰寒冷。

• 現在我們可以寫出「決定⾐服」的虛擬
碼。
21 Copyright © 滄海圖書
可選擇的演算法

22 Copyright © 滄海圖書
重複演算法
• 兩種典型的迴圈型態:
• 計數控制型(count controlled)
• 事件控制型(event controlled)

23 Copyright © 滄海圖書
計數控制型迴圈
(count-controlled loops)

• 下列的演算法重複⼀個步驟共計limit 次
數:

24 Copyright © 滄海圖書
計數控制型迴圈
(count-controlled loops)
• while迴圈⼜被稱為先測型迴圈,這表⽰它
是在迴圈執⾏前先測試條件是否滿⾜。
• 如果⼀開始條件就不為真,那麼就不會進
⼊迴圈。
• ⼀個迴圈如果無法終⽌,那就稱為無窮迴
圈(infinite loop)。

25 Copyright © 滄海圖書
計數控制型迴圈
(count-controlled loops)

26 Copyright © 滄海圖書
事件控制型迴圈
(Event-Controlled Loops)

• 迴圈中重複執⾏的次數是由發⽣在迴圈主
體內的事件來控制的,被稱為事件控制型
迴圈。

27 Copyright © 滄海圖書
An event-controlled loop

Set sum to 0
Set allPositive to true
WHILE (allPositive) Why is it
Read number called an
IF (number > 0) event-
Set sum to sum + number controlled
ELSE loop?
Set allPositive to false What is the
Write "Sum is " + sum event?

28
事件控制型迴圈
(Event-Controlled Loops)

29 Copyright © 滄海圖書
事件控制型迴圈
(Event-Controlled Loops)
• Try:
• 讀⼊並加總輸⼊的資料數值,直到我們讀到⼀個
負數值為⽌。

30 Copyright © 滄海圖書
事件控制型迴圈
(Event-Controlled Loops)
• Try:
• 寫⼀個演算法來讀⼊並加總正的數值,直到計
數了10 次為⽌。忽略掉0 及負的數值。

31 Copyright © 滄海圖書
事件控制型迴圈
(Event-Controlled Loops)
• 這不是⼀個計數控制迴圈,因為我們沒有
讀取10 個數值:我們⼀直讀取數值直到我
們達到10 個。
• 在⼀個控制結構中的結構被嵌⼊另⼀個控
制結構裡,稱為巢狀結構(nested
structure)。

32 Copyright © 滄海圖書
平⽅根(Square Root)
• Try:

• 「讀取平⽅」(Read in square) 不需要進⼀


步的擴展。
• 「計算平⽅根」(Calculate the square root)
則需要進⼀步的擴展,
33 Copyright © 滄海圖書
平⽅根(Square Root)

34 Copyright © 滄海圖書
平⽅根(Square Root)
• 演算法:

35 Copyright © 滄海圖書
平⽅根(Square Root)
• 圖 7.4 平⽅根演算法的過程

36 Copyright © 滄海圖書
平⽅根(Square Root)
• 需要被擴展的步驟稱為抽象步驟(abstract
step)
• 不需要被擴展的步驟稱為具體步驟(concrete
step)。
• 每⼀個抽象步驟都必須分開擴展。

37 Copyright © 滄海圖書
Composite Data Types ( 複合變數)
Records
A named heterogeneous collection of items in
which individual items are accessed by name. For
example, we could bundle name, age, and hourly
wage items into a record named Employee.
Arrays
A named homogeneous collection of items in
which an individual item is accessed by its position
(index) within the collection

38
Composite Data Types

• 陣列(Array)
• 所謂的陣列是⼀個同質性(homogeneous) 項⽬
的集合名稱,個別的項⽬可以依它們在這個集
合中的位置來存取。
• 在這個集合中的位置叫作索引(index)。
• 如果陣列被稱作「數字」(numbers),我們⽤
來表⽰每⼀個數值的存取。⽽索引值position
表⽰介於0〜9 的數。
39 Copyright © 滄海圖書
Array 陣列
• 以下是將數值放到陣列上的演算法:

40 Copyright © 滄海圖書
Array 陣列
• 圖 7.5 包含10 個
數字的陣列

41 Copyright © 滄海圖書
Array 陣列
• 陣列的演算法可以分成三種:搜索、排序
以及處理。
• 搜索(searching)就如同字義所說 —— 在陣列項
⽬中尋找⼀個特定的數值。
• 排序(sorting) 表⽰將陣列中的項⽬按順序排
好;
• 處理(processing) 是⼀個包含所有在⼀個陣列
上完成計算或者完成項⽬間計算的各階段統合
步驟。
42 Copyright © 滄海圖書
Record 紀錄
• 紀錄是⼀種異類(hetergeneous) 項⽬集合的
名稱,其中個別的項⽬可以依名稱來存
取。
• 「異類」代表的意思是集合中的各個元素可以
是不同的資料型態。
• 這個集合可以包含整數、實數、字串或其他任
何型態的資料。

43 Copyright © 滄海圖書
Record 紀錄
• 圖 7.6 員⼯紀錄

44 Copyright © 滄海圖書
Record 紀錄
• 以下是將資料儲存到紀錄各欄位的演算
法:

第三種複合資料結構是類別(class),即物件
導向程式語⾔的特性。
45 Copyright © 滄海圖書
7.4 搜索演算法
• 循序搜索
• 我們依次查詢每⼀個項⽬並且將它與我們搜索
的項⽬做⽐較。
• 如果它符合的話,表⽰我們找到該項⽬。
• 如果不符合,我們繼續查詢下⼀個項⽬。

46 Copyright © 滄海圖書
循序搜索
• 我們可以⽤NOT 運算⼦簡化第⼆個布林表
⽰式(found is FALSE)。
• 當found 是偽時,NOT found 為真。因此
我們可以說
• 因此,當索引⼩於10 ⽽且我們尚未找到相
符的項⽬時,迴圈會⼀直重複。
47 Copyright © 滄海圖書
在排序陣列中循序搜索
• 圖 7.7 ⼀個未排序陣列

48 Copyright © 滄海圖書
在排序陣列中循序搜索
• 圖 7.8 ⼀個排序陣列

49 Copyright © 滄海圖書
在排序陣列中循序搜索

50 Copyright © 滄海圖書
在排序陣列中循序搜索

51 Copyright © 滄海圖書
在排序陣列中循序搜索

52 Copyright © 滄海圖書
⼆元搜索
• ⼆元搜索法則是使⽤不同的策略在⼀個陣
列中搜索項⽬:分割並征服(divide and
conquer)。
• ⼆元搜索(binary search) 的演算法是假設要
被搜索的陣列中項⽬已排序好,⽽且在每
⼀次的⽐較後,不是找到了該項⽬就是可
以減少⼀半的陣列不⽤去尋找。
53 Copyright © 滄海圖書
⼆元搜索-2
• 圖 7.9 ⼆元搜索範例

54 Copyright © 滄海圖書
⼆元搜索-3

55 Copyright © 滄海圖書
⼆元搜索-4
• 讓我們以桌⾯核
對(⾛過⼀遍) 搜索
貓(cat)、⿂(fish)
及斑⾺(zebra) 的
演算法。

56
圖 7.10 追蹤⼆元搜索過程
⼆元搜索-5

57 Copyright © 滄海圖書
7.5 排序
• 選擇排序

• 選擇排序演算法可能是最容易的⼀種,因為它
反映了我們通常要⾃⼰動⼿去排列⼀串數值時
所使⽤的⽅法。
• 這個演算法很簡單,但是它有⼀個缺點:它需
要兩個完整表列(陣列)的空間。
58 Copyright © 滄海圖書
選擇排序
• 圖7.11,我們要排序⼀個有五個項⽬的陣
列。
• 圖 7.11 選擇排序的範例(排序過的項⽬以灰
⾊表⽰)

59 Copyright © 滄海圖書
選擇排序
• 我們將陣列分為兩個部分:未排序部分(無
陰影) 與排序過的部分(有陰影的)。
• 每⼀次我們將⼀個項⽬放到它的正確位置
時,我們縮短未排序部分並延伸排序過的
部分。

60 Copyright © 滄海圖書
選擇排序

61 Copyright © 滄海圖書
選擇排序
• 迴圈繼續重複的做到firstUnsorted 的值⼩
於array – 1 的⻑度。

62 Copyright © 滄海圖書
選擇排序
• 我們在未排序的部分尋找最⼩的項⽬
時,是由⽬前firstUnsorted 位置⼀直找
到length – 1。

63 Copyright © 滄海圖書
選擇排序
• 交換演算法必須讓兩個項⽬的索引值被交
換的。

64 Copyright © 滄海圖書
泡沫排序
• 泡沫排序(bubble sort) 是⼀種使⽤不同的機
制來找出最⼩值的選擇排序⽅法。
• 由陣列的最後⼀項開始,我們⽐較相鄰的
⼀對項⽬,當這對項⽬在底層中的⼩於在
它上層的時就進⾏交換。

65 Copyright © 滄海圖書
泡沫排序
• 圖 7.12 泡沫排序的範例

66 Copyright © 滄海圖書
泡沫排序
• 泡沫排序是所有排序演算法中最慢的⼀
種。
• 排序演算法通常根據排序⼀個陣列所需重
複的次數來做⽐較的,⽽這個過程需要將
每個項⽬都做⼀次重複,除了最後⼀個項
⽬。
• ⽐較泡沫排序與選擇排序的處理過程在⼀
個已經排序好的陣列上。選擇排序不會試
圖去判斷陣列是否已經排序好了;所
以,我們將執⾏完整個演算法。
67 Copyright © 滄海圖書
泡沫排序

68 Copyright © 滄海圖書
泡沫排序

69 Copyright © 滄海圖書
插⼊排序-1
• 當你找到⼀個可插⼊的位置⽽在該位置上
要插⼊的項⽬是⼩於陣列上的項⽬,那麼
你可以將項⽬儲存在那裡。
• 圖 7.13 插⼊排序

70 Copyright © 滄海圖書
插⼊排序-2

71 Copyright © 滄海圖書
插⼊排序-3
• 在選擇排序的每⼀次重複中,會有⼀個以
上的項⽬被放在它正確的位置上。
• 在插⼊排序的每⼀次重複,⼀個以上的項
⽬被放在相對⾼於它們項⽬的合適位置。

72 Copyright © 滄海圖書
7.6 遞迴演算法
• 當⼀個演算法使⽤⾃⼰的名稱在該演算法
內,稱為遞迴演算法。也就是,如果⼀個
程序名稱在⼀個層級上呼叫它⾃⼰,這個
呼叫就是遞迴呼叫(recursive call)。
• 每⼀次遞迴的結果,⾄少含有兩種狀
況:底層狀況(base case) 與⼀般狀
況(general case)。
• 底層狀況就是可以得到答案的那種;
• ⼀般狀況的結果是表⽰成另⼀個較⼩版本的問
73 題,然後再次呼叫程式本⾝。 滄海圖書
Copyright ©
副程式敘述-1
• 我們會將⼀段的程式碼命名,然後在程式
的另外⼀個部分使⽤這個名稱於程式的敘
述上。
• 當程式執⾏中遇到了這個名稱敘述時,在
程式另⼀個部分的處理程序將暫停等待這
個名稱程式碼的執⾏。
• 當這個名稱的程式碼執⾏完畢後,緊跟著
這個名稱之後的敘述會被繼續執⾏下
去。名稱程式碼出現的地⽅也叫作呼叫單
元(calling unit)。
74 Copyright © 滄海圖書
副程式敘述-2
• 有兩種基本的副程式形式:⼀種是單純的
命名⼀段程式碼⽽執⾏⼀項特定的⼯作(無
值副程式),另⼀種也是執⾏⼀項⼯作但同
時會傳回⼀個值給呼叫單元(傳回值副程
式)。

75 Copyright © 滄海圖書
副程式敘述-3
• 圖 7.14 副程式控制流程

76 Copyright © 滄海圖書
副程式敘述-4
• 圖 7.14 副程式控制流程

77 Copyright © 滄海圖書
遞迴階乘-1
• ⼀個數字的階乘是定義為這個數乘上所有
介於這個數與0 之間的數字:
• 0 的階乘為1,⽽範圍因⼦就是這個數
字,也就是我們⽤來計算階乘的數。我們
可以訂出⼀個底層狀況:
• 我們同時有⼀個⼀般狀況是:
78 Copyright © 滄海圖書
遞迴階乘-2

• 每呼叫⼀次Factorial 階乘,N 值就會變


⼩。每次被給予的資料就稱作引
79
數(argument)。 Copyright © 滄海圖書
遞迴⼆元搜索
• 我們僅使⽤first 與last 的新數值再次呼叫此
演算法即可。

80 Copyright © 滄海圖書
快速排序-1
• 快速排序演算法,由C. A. R. Hoare 所提
出,是基於排序兩個較⼩的陣列要⽐⼀個
⼤的陣列來得快速且容易⼀點的構想。
• ⽽這名稱的由來也是基於在⼀般情況
下,快速排序可以將⼀陣列的資料相當快
速排好的這個事實⽽命名。
• 這個排序的基本策略就是「先分割再逐⼀
征服」。
81 Copyright © 滄海圖書
快速排序-2
• 圖 7.15 使⽤快速排序演算法排序

82 Copyright © 滄海圖書
快速排序-3

83 Copyright © 滄海圖書
快速排序-4
• 要如何來選擇splitVal (分割值) 呢?
• ⼀個較簡易的⽅法就是使⽤陣列
的data[first] (第⼀個值) 來當成分割值。
• 以下的範例就是使⽤data[first] 當作
是splitVal。

84 Copyright © 滄海圖書
快速排序-5
• 在呼叫完Split ⽅法之後,所有⼩於或等
於splitVal 的項⽬將放到陣列的左邊,⽽所
有⼤於splitVal 的項⽬將在陣列的右邊。

85 Copyright © 滄海圖書
快速排序-6
• 最後我們將splitVal (也就是data[first]) 的值
與data[splitPoint] 的值交換。

• 我們對Quicksort ⽤的遞迴呼叫是使
⽤splitPoint 這個索引值來降低在⼀般狀況
下問題的範圍⼤⼩。
86 Copyright © 滄海圖書
快速排序-7
• 我們必須找出⼀個⽅法來將所有等於或⼩
於splitVal 的項⽬趕到splitVal 的⼀邊,⽽
將所有⼤於splitVal 的項⽬移到另⼀邊。
• 這個⽅法是以⼀對的索引值由陣列的兩端
向中間移動,並檢視這對項⽬是否在分割
值的錯誤⼀邊上。

87 Copyright © 滄海圖書
快速排序-8

88 Copyright © 滄海圖書
圖 7.16 分割的演算法

89 Copyright © 滄海圖書
快速排序-9
• 遞迴是⼀個⾮常有⽤與精緻的⼯具。
• 然⽽,並⾮每種問題都可以被遞迴所解
決,⽽且並⾮每種問題都有明顯的遞迴答
案⽽應該被遞迴所解答。
• 即使如此,遞迴解答對許多問題⽽⾔是最
佳的。
• 如果問題的邏輯性描述落在兩個狀況(底層
狀況與⼀般狀況),遞迴是⼀種可⾏的⽅
法。90 Copyright © 滄海圖書
7.7 重要的串連思路
• 資訊隱藏
• 資訊隱藏(information hiding),意即在設計較
⾼層級期間,讓較低層級的細節無法被存取。

91 Copyright © 滄海圖書
抽象概念-1
• 抽象概念是⼀個複雜系統的模型, 僅包含
觀察者需要的細節。
• 抽象概念(abstraction) 與資訊隱藏是事物的
⼀體兩⾯。資訊隱藏是隱藏細節的實
踐,⽽抽象概念則具有細節隱藏的效果。
• 資料抽象概念(data abstraction) 與資料的觀點
有關,它將資料的邏輯觀點和其實體加以區
分。
• 程序抽象概念(procedural abstraction) 與動作
92
的觀點有關,它將動作的邏輯觀點和其實體加 滄海圖書
Copyright ©
抽象概念-2
• 圖 7.17 相同概念的不同觀點

93 Copyright © 滄海圖書
抽象概念-3
• 第三種抽象概念稱為控制抽象概念(control
abstraction)。
• 控制抽象概念與控制結構的觀點有關,它是將
控制結構的邏輯觀點和其實體加以區分。
• 控制結構(control structure) 讓我們可以改變這
種接續式的控制⽅式。

94 Copyright © 滄海圖書
為事件命名
• 當我們撰寫演算法時,我們使⽤了速記法
來代表⼯作與我們所處理的資訊。我們給
資料與過程名稱,這些名稱稱為識別符
號(identifier)。
• 在將演算法翻譯成電腦可以執⾏的語⾔這
個階段,我們可能必須修改這些識別符
號。每⼀種語⾔都有關於形成識別符號的
特有規則。
95 Copyright © 滄海圖書
測試
• 測試有兩種典型的型態:透明盒⼦測
試,其根據程式碼本⾝做測試,以及⿊盒
⼦測試,即測試所有可能的輸⼊數值。
• 通常,⼀個測試計畫會包含兩種型態的測
試。

96 Copyright © 滄海圖書
總結-1
在他的經典著作《如何解決它》(How to
中勾勒了有關數學問題的問題解決策
• Polya

略。
Solve It)

這種策略可以應⽤於所有包括以電腦程式撰寫
⽽成的問題。

這些策略包括了提出問題、尋找熟悉的事物以
及各個擊破。

當這些⽅法被應⽤時,它們應該能夠編寫成⼀
個解決問題的計畫(plan)。

談到計算時,這個計畫則稱為演算
法(algorithm)。

97 滄海圖書
Copyright ©
總結-2
• 迴圈被區分成計數控制與事件控制兩個種
類。
• 計數控制迴圈執⾏預先決定好迴圈次
數,⽽事件控制迴圈執⾏直到擁有迴圈的
事件改變為⽌。

98 Copyright © 滄海圖書
總結-3
• 資料有兩種形式:原始(單⼀) 的與複合
的。
• 陣列即是同質性結構,它給予項⽬集合名
稱並且允許使⽤者在結構的部分存取個別
項⽬。

99 Copyright © 滄海圖書
總結-4
• 搜索是在陣列中尋找特別數值的動作。
• 在本章,我們討論了在⾮排序陣列中的線
性搜索、在排序陣列中的線性搜索以及在
排序陣列中的⼆元搜索。
• 排序指的是在陣列中按照某種順序排列項
⽬的動作。
• 選擇排序、泡沫排序、插⼊排序以及快速
排序是四項常⽤的排序演算法。
10 Copyright © 滄海圖書
0
總結-5
• 遞迴演算法是副程式的名稱出現在副程式
本⾝的演算法。
• 階乘與⼆元搜索都是⾃然的遞迴演算法。

10 Copyright © 滄海圖書
1

You might also like