《資料結構與演算法之美》專欄閱讀筆記2
換個方式來寫筆記,最近啃完了《Thinking in Java》,想要在看專欄的時候多做點擴充套件性的東西,比如把難撩的泛型加進來做實現,程式碼還是要寫起來才曉得怎麼寫更酷。總之最近看書的過程中、搜尋答案的過程中發出了很多“哇~超厲害!超酷!我也要這樣棒棒噠!”的嘆聲。新的開始,繼續加油
棧
特定的資料結構是對特定場景的抽象。棧這種結構屬於介面比較簡單的,後進先出。
棧的兩種實現:陣列實現的順序棧和連結串列實現的鏈式棧。最近看泛型比較多,實現的時候也用上了。程式碼如下:
// 基於linkedList實現 public class LinkedListStack<T> { private LinkedList<T> storage = new LinkedList<T>(); public boolean push(T item) { storage.addFirst(item); return true; } public T pop() { return storage.removeFirst(); } public T peek() { return storage.getFirst(); } } // 基於陣列實現 public class ObjectArrayStack<T> { private Object[] storage = null; private int size; private int count; public ObjectArrayStack(int size) { if (size >= 0) { this.size = size; this.count = 0; storage = new Object[size]; } else { throw new RuntimeException("Illegal stack size: " + size); } } public boolean push(T item) { if (count == size) { return false; } storage[count++] = item; return true; } public T pop() { if (count == 0) { return null; } --count; T item = (T)storage[count]; storage[count] = null; return item; } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); sb.append(":"); for (int i = 0; i < count; i++) { sb.append(storage[i]).append(" "); } return sb.toString(); } public static void main(String[] args){ ObjectArrayStack<String> stack = new ObjectArrayStack<String>(15); for (String s : "A B C D E F G H I".split(" ")) { stack.push(s); } System.out.println(stack); } }
擴充套件:泛型與陣列
上面基於陣列的實現是看過其他技術部落格之後這樣寫的。很迷書裡面講的Generator,之前寫的版本是強行把Generator用上了的。 一開始沒有使用Object陣列是因為,看過書中的例子,Object陣列是可以同時放多種型別的資料,最大的弊端就是無法進行編譯器檢查。但是此處因為是泛型類,反而沒有這樣的困擾呢~ 強用Generator的程式碼如下:
public class ArrayStack<T> { private T[] storage; private int size; // 棧的大小 private int count; // 元素的個數 public ArrayStack(Class<T> type, Generator<T> gen, int n) { if (size >= 0) { this.size = n; this.count = 0; storage = Generated.array(type, gen, n); } else { throw new RuntimeException("Illegal stack size: " + size); } } public boolean push(T item) { if (count == size) { return false; } storage[count++] = item; return true; } public boolean push(Generator<T> gen) { if (count == size) { return false; } storage[count++] = gen.next(); return true; } public T pop() { if (count == 0) { return null; } --count; T item = storage[count]; storage[count] = null; return item; } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); sb.append(":"); for (int i = 0; i < count; i++) { sb.append(storage[i]).append(" "); } return sb.toString(); } public static void main(String[] args){ ArrayStack<String> stack = new ArrayStack<String>(String.class, new PrimitiveGenerator.String(), 15); for (String s : "A B C D E F G H I".split(" ")) { stack.push(s); } System.out.println(stack); } }
關於生成器的示例程式碼可以參考:生成器 在用生成器的過程中也搞明白了一個事情。利用反射機制寫出來的BasicGenerator的使用條件:
- 具有預設構造器(無參構造器)
- public
無法用於基本型別和包裝器型別的原因也在於此,所以另外單獨寫了CountingGenerator(書中程式碼的名稱),我改成了PrimitiveGenerator啦。
佇列
佇列和棧非常相似,只是取資料的方式不同。就是名字看到的那種,排隊用的。
關於佇列的實現,無論是構造還是優化,兩點判斷十分重要:隊滿條件和隊空條件。迴圈佇列就是構造利用率最大的兩個條件的做法。 基於陣列的實現如下:
public class ArrayQueue<T> { private Object[] storage = null; private int size; private int head; private int tail; public ArrayQueue(int size) { if (size >= 0) { this.size = size; head = tail = 0; storage = new Object[size]; } else { throw new RuntimeException("Illegal queue size: " + size); } } public boolean enqueue(T item) { if ((tail + 1) % size == head) { return false; } storage[tail] = item; tail = (tail + 1) % size; return true; } public T dequeue() { if (tail == head) { return null; } T item = (T)storage[head]; storage[head] = null; head = (head + 1) % size; return item; } }
(直接丟程式碼好像很沒意思耶,那下次丟bug吧~)
阻塞佇列和併發佇列
四月份接觸到的一個專案:客戶端需要呼叫服務端的介面(Thrift……如果我表示出了鄙視,可能是因為我對Thrift用法有什麼誤解)進行計算,一般情況下單個計算的時間在1~15分鐘不等,客戶端需要處理多個併發計算請求。當時使用了執行緒池,面臨的問題是,服務端計算失敗或者由於計算方法(計算髮起人編輯)導致計算時間超長的情況時,服務端沒有任何反饋(團隊一致認為這個很正常……),此時客戶端就會面臨執行緒池被佔滿需要進行排隊的情況。 排隊佇列要怎麼處理可能很快就被塞滿的情況,對於生產者和消費者效率協調問題,想起來昨天看的一個段子:
藝術是對特定現實的抽象?
專欄作者給出的一個建議是:多配置幾個消費者
那在服務端已經是分散式計算(用的Akka)的情況下,是否算多個消費者呢?
遞迴
大概是第一次用遞迴的時候就用對了,所以常常自信心滿滿地講:這都不是事~(不要lian 專欄作者對遞迴的幾個總結的超好呢~
遞迴需要滿足的三個條件
- 一個問題可以分解成幾個子問題的解
- 這個問題和分解之後的子問題,除了資料規模不同,求解思路完全一樣
- 存在遞迴終止條件
寫遞迴程式碼的關鍵
- 找到大問題化小問題的規律
- 基於規律寫遞推公式
- 推敲終止條件
- 翻譯以上步驟
遞迴時如何避免堆疊溢位
- 限制遞迴深度
int depth = 0;
int f() {
++depth;
if (depth > N) throw new RuntimeException();
}
適用場景:最大深度比較小。
如何避免遞迴中的重複計算
使用資料結構來儲存已經計算過的結果。
髒資料產生的無限遞迴問題
環的檢測。連結串列中也有環的檢測?