1. 程式人生 > >《資料結構與演算法之美》專欄閱讀筆記2——線性表

《資料結構與演算法之美》專欄閱讀筆記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();
}

適用場景:最大深度比較小。

如何避免遞迴中的重複計算

使用資料結構來儲存已經計算過的結果。

髒資料產生的無限遞迴問題

環的檢測。連結串列中也有環的檢測?