《資料結構與演算法之美》專欄閱讀筆記2——線性表
換個方式來寫筆記,最近啃完了《Thinking in Java》,想要在看專欄的時候多做點擴充套件性的東西,比如把難撩的泛型加進來做實現,程式碼還是要寫起來才曉得怎麼寫更酷。總之最近看書的過程中、搜尋答案的過程中發出了很多“哇~超厲害!超酷!我也要這樣棒棒噠!”的嘆聲。新的開始,繼續加油
文章目錄
陣列和連結串列
簡單說明下:
1、單鏈表反轉:兩種方式,新建連結串列,逆序遍歷轉儲;遞迴改變next的指向。此處用的是遞迴。
2、環的檢測:快慢指標法。對於環的入口節點的證明還是每太吃透(數學不好?
3、有序連結串列合併:直接是寫了個有序連結串列,insert。在插入位置的查詢上應該是可以根據有序這個特點來優化的,偷懶了,沒寫……
4、刪除倒數第N個節點:用雙指標來確定長度。
5、連結串列的中間節點:還是雙指標,寫法和環的檢測很像,木有證明哦,只是畫了簡單的圖來推一下邏輯。(我好像直接刪掉了中間節點,嘎~
程式碼如下
【單鏈表】
public class SingleLinkedList<T> { private int size; private Node head = new Node(); public Node addFirst(T data) { Node node = new Node(data); node.next = head.next; head.next = node; size++; return node; } public Node addLast(T data) { Node tail = getTail(); Node node = new Node(data); node.next = tail.next; tail.next = node; return node; } public Node getTail() { Node tail = head; Node entry = getEntryNodeOfLoop(); if (entry != null) { return entry; } while (tail != null && tail.next != null) { tail = tail.next; } return tail; } public Node find(T data) { Node ptr = head.next; while (ptr != null) { if (ptr.item.equals(data)) { return ptr; } ptr = ptr.next; } return null; } // 遞迴反轉 public void reversal() { head.next = reversal(head.next); } // 連結串列反轉 public Node reversal(Node nextNode) { if (nextNode == null || nextNode.next == null) { return nextNode; } Node newHead = reversal(nextNode.next); nextNode.next.next = nextNode; nextNode.next = null; return newHead; } // 環的檢測,入口節點非空表示有環 public Node getEntryNodeOfLoop() { if (head == null || head.next == null || head.next.next == null) { return null; } Node fastNode = head.next; Node slowNode = head.next; while (fastNode != null && fastNode.next != null) { slowNode = slowNode.next; fastNode = fastNode.next.next; if (fastNode == slowNode) { slowNode = head.next; while (slowNode != fastNode) { slowNode = slowNode.next; fastNode = fastNode.next; } return slowNode; } } return null; } // 刪除連結串列的倒數第N個節點 public Node removeFromEnd(int n) { Node pHead = head.next; Node pTail = pHead; for (int i = 0; i < n; i++) { pTail = pTail.next; // 給定的N可以保證該操作有效 } while (pTail.next != null) { pHead = pHead.next; pTail = pTail.next; } Node target = pHead.next; pHead.next = pHead.next.next; return target; } public Node removeMidNode() { Node firstNode = head.next; if (firstNode == null || firstNode.next == null) { head.next = null; return head; } Node slow = firstNode; Node fast = slow.next.next; while(fast.next.next != null && slow.next != null) { slow = slow.next; fast = fast.next.next; } Node target = slow.next; slow.next = slow.next.next; return target; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("["); Node node = head; while(node != null) { sb.append(node.item); sb.append(" "); node = node.next; } sb.append("]"); return sb.toString(); } // 連結串列節點 private class Node { private T item; private Node next; Node() { item = null; next = null; } Node(T data) { item = data; next = null; } Node(T data, Node next) { this.item = data; this.next = next; } @Override public String toString() { return "Node: " + item; } } public static void main(String[] args){ System.out.println(" ---- Test reversal of linkedlist ----"); SingleLinkedList<String> ssl = new SingleLinkedList<String>(); for (String s : "A B C D E F G".split(" ")) { ssl.addLast(s); } System.out.println("source: " + ssl); ssl.reversal(); System.out.println("reversal: " + ssl); System.out.println(" ---- remove node from end ----"); ssl.removeFromEnd(3); System.out.println("remove the 3rd node from the end: " + ssl); System.out.println(" --- remove mid node -----"); ssl.removeMidNode(); System.out.println("remove mid node: " + ssl); System.out.println(" ---- Check loop in linkedlist ----"); SingleLinkedList<String> loopList = new SingleLinkedList<String>(); for (String s : "A B C D E F G H I J K".split(" ")) { loopList.addLast(s); } System.out.println("Set Loop in D"); loopList.getTail().next = loopList.find("D"); System.out.println("entry node of loop: " + loopList.getEntryNodeOfLoop()); } }
【有序連結串列】
import java.util.Iterator;
/**
* @Description : TODO
* @Author : Ellie
* @Date : 2018/11/7
*/
public class SortedLinkedList<T extends Comparable<T>> {
Node head = new Node();
Node curNode = head;
// 由大到小
public Node insert(T data) {
Node cur = head.next;
Node pre = head;
while (cur != null && cur.item.compareTo(data) > 0) {
pre = cur;
cur = cur.next;
}
Node node = new Node(data);
node.next = cur;
pre.next = node;
return node;
}
public void insert(Node o) {
Node cur = head.next;
Node pre = head;
while (cur != null && cur.item.compareTo(o.item) > 0) {
pre = cur;
cur = cur.next;
}
o.next = cur;
pre.next = o;
}
public void insert(SortedLinkedList<T> sortedLinkedList) {
Node ptr = sortedLinkedList.head.next;
while (ptr != null) {
insert(ptr.item);
ptr = ptr.next;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node node = head.next;
while (node != null) {
sb.append(node.item);
sb.append(" ");
node = node.next;
}
return sb.toString();
}
class Node implements Comparable<Node> {
T item;
Node next;
Node() {
item = null;
next = null;
}
Node(T data) {
item = data;
next = null;
}
Node(T data, Node next) {
this.item = data;
this.next = next;
}
public int compareTo(Node o) {
return item.compareTo(o.item);
}
@Override
public String toString() {
return "Node: " + item;
}
}
public static void main(String[] args){
SortedLinkedList<String> sll1 = new SortedLinkedList<String>();
for (String s : "A B C X Y Z D E F G".split(" ")) {
sll1.insert(s);
}
System.out.println("s1: " + sll1);
SortedLinkedList<String> sll2 = new SortedLinkedList<String>();
for (String s : "Q X P J K M D A".split(" ")) {
sll2.insert(s);
}
System.out.println("s2: " + sll2);
sll1.insert(sll2);
System.out.println("s1+s2: " + sll1);
}
}
多寫多練部分完整工程:https://github.com/EllieYY/linkedlistDemo.git
棧
特定的資料結構是對特定場景的抽象。棧這種結構屬於介面比較簡單的,後進先出。
棧的兩種實現:陣列實現的順序棧和連結串列實現的鏈式棧。最近看泛型比較多,實現的時候也用上了。程式碼如下:
// 基於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();
}
適用場景:最大深度比較小。
如何避免遞迴中的重複計算
使用資料結構來儲存已經計算過的結果。
髒資料產生的無限遞迴問題
環的檢測。連結串列中也有環的檢測?