題記:花了斷斷續續四個月的時間,終於將Coursera上Robert Sedgewick老師的普林斯頓兩部分算法課學習完。上課時僅以滿分完成作業為目標,每周都趕得緊緊張張,回想起來才發現這些基本數據結構有些已經遺忘到只剩下一個概念了,於是做個計划:溫習一遍教材,並把這些基本數據結構手寫一遍加深印象。
第一章 基礎
1. 算法課的正確打開方式
1.1. 研究一個新的應用領域時,如何將其轉換為可計算問題:
1)定義API;
2)根據特定的應用場景開發用例代碼;
3)定義類實例變量所需的數據結構(一組值集的表示),並通過它實現API所對應的抽象數據類型;
4)描述算法(實現一系列操作的方法),並根據它實現類中的實例方法;
5)分析算法的性能特點。
1.2. 在實驗可以重復執行以及假定可以被證偽的前提下,如何通過科學方法得出正確的計算模型(算法):
1)細致地觀察應用場景的特點;
2)根據觀察結果提出假設模型;
3)根據模型預測計算結果;
4)繼續觀察並核實預測的准確性;
5)如此反復直到預測和觀察一致。
1.3. 書中研究各類問題的基本步驟(上文1&2):
1)完整而詳細地定義問題,找出解決問題所需的基本抽象操作並定義一份API;
2)簡潔地實現一種初級算法,給出一個精心組織的開發用例並使用實際數據作為輸入;
3)當實現所能解決的問題的最大規模達不到期望時決定改進還是放棄;
4)逐步改進實現,通過經驗性分析或(和)數學分析驗證改進后的效果;
5)用更高層次的抽象表示數據結構或算法來設計更高級的改進版本;
6)如果可能盡量為最壞情況下的性能提供保證,但在處理普通數據時也要有良好的性能;
7)在適當的時候講更細致的深入研究留給有經驗的研究者並繼續解決下一個問題。
評:“九層之台,起於累土;千里之行,始於足下。”先要定義好問題,然后才能解決問題;解決問題的方法很多,最直接的莫過於蠻力求解;蠻力求解不行的情況下,回顧定義問題的模型是否可以優化,是否可以提煉更優秀的數據結構來抽象數據類型;如果一個子問題的性能不能更高了,是否可以旁敲側擊從其他地方提高整體算法性能。如果實在沒有辦法了,也不要氣餒,還有百度、谷歌、StackOverflow,畢竟世上還有好多NP問題存在。
2. 數組(順序表)以及鏈表
2.1 聲明:
數組的聲明:
private Item[] array;
鏈表的數據結構:
private class Node { Item item; Node next; }
2.2 數組以及鏈表的比較
數據結構 |
優點 |
缺點 |
數組 |
通過索引可以直接訪問任意元素 |
在初始化時就需要知道元素的數量 |
鏈表 |
使用的空間大小和元素數量成正比 |
需要通過參照物來訪問一個元素 |
2.3. 背包、棧、隊列
2.3.1 定義:
背包(Bag):一種不支持從中刪除元素的集合數據類型,它的目的是幫助用例收集元素並遍歷所有收集到的元素;
隊列(Queue):一種基於FIFO(先進先出)策略的集合類型,數據操作集中在隊頭(刪除)、隊尾(添加)兩端;
棧(Stack):一種基於LIFO(后進先出)策略的集合類型,數據操作只發生在棧頂(添加、刪除);
2.3.2 API:
Bag |
|||
public class |
Bag<Item> implements |
Iterable<Item> |
|
Bag() |
創建一個空背包 |
||
void |
add(Item item) |
添加一個元素 |
|
boolean |
isEmpty() |
背包是否為空 |
|
int |
size() |
背包中的元素數量 |
|
FIFO queue |
|||
public class |
Queue<Item> implements |
Iterable<Item> |
|
Queue() |
創建一個空隊列 |
||
void |
enqueue(Item item) |
添加一個元素 |
|
Item |
dequeue() |
刪除隊列中最早添加的元素 |
|
boolean |
isEmpty() |
隊列是否為空 |
|
int |
size() |
隊列中的元素數量 |
|
Pushdown (LIFO) stack |
|||
public class |
Stack<Item> implements |
Iterable<Item> |
|
Stack() |
創建一個空棧 |
||
void |
push(Item item) |
添加一個元素 |
|
Item |
pop() |
刪除最近添加的元素 |
|
boolean |
isEmpty() |
棧中是否為空 |
|
int |
size() |
棧中的元素數量 |
2.3.3 隊列和棧的實現方式:
書中分別給出了通過數組以及鏈表兩種不同數據結構實現棧數據類型的方式,需要留意的是如何通過數組的動態調整來實現棧的擴張、收縮:
2.3.3.1 resize()函數:
private void resize(int max) { Item[] temp = (Item[]) new Object[max]; for (int i = 0; i < N; i++) temp[i] = a[i]; a = temp; }
2.3.3.2 Stack用例:
public void push(Item item) { // Add item to top of stack. if (N == a.length) resize(2*a.length); a[N++] = item; } public Item pop() { // Remove item from top of stack. Item item = a[--N]; a[N] = null; // Avoid loitering (see text). if (N > 0 && N == a.length/4) resize(a.length/2); return item; }
評:resize調整的方式,使得數組也可以支持很深的棧,並在棧變淺時回收內存,不至於浪費資源。缺陷在於棧很深時,復制棧內容需要一定的開銷,在單位時間並發度很高的系統中可能存在響應時間問題。