看得見的數據結構Android版之雙鏈表篇

零、前言

1.上一篇分析了單鏈表,鏈表是一種數據結構,用來承載數據,每一個表節點裝載一個數據元素
2.雙鏈表是每一個節點除了數據元素外還分別持有前、後兩個節點的引用
3.爲了統一節點的操做,通常在真實鏈表的首尾各加一個虛擬節點,稱爲頭節點和尾節點
4.若是說單鏈表是一列火車,那雙鏈表就是一輛雙頭加固版火車,java中的LinkedList底層即是此結構
5.本例操做演示源碼:但願你能夠和我在Github一同見證:DS4Android的誕生與成長,歡迎starjava

1.留圖鎮樓:雙鏈表的最終實現的操做效果:

雙鏈表操做合集.gif

2.對於雙鏈表簡介:
雙鏈表是對節點(Node)的操做,而節點(Node)又承載數據(T)  
總的來看,雙鏈表經過操做節點(Node)從而操做數據(T),就像車箱運送獲取,車箱只是載體,貨物是咱們最關注  
雙鏈表不一樣於單鏈表至處在於一個節點同時持有前、後兩個節點的引用,使得對頭尾操做都方便 

Node只不過是一個輔助工具,並不會暴露在外。它與數據相對應,又使數據按鏈式排布,
操縱節點也就等於操縱數據,就像提線木偶,並非直接操做木偶的各個肢體自己(數據T)。

爲了統一節點的操做,一般在鏈表最前面添加一個虛擬頭(headNode)和虛擬尾(tileNode)(封裝在內中,不暴露)
複製代碼

雙鏈表.png


3.雙鏈表的實現:本文要務

本文要務.png


1、雙鏈表結構的實現:LinkedChart

1.表的接口定義在數組表篇,這裏就不貼了

這裏給出實現接口後的LinkedChart以及簡單方法的實現node

/**
 * 做者:張風捷特烈<br/>
 * 時間:2018/11/22 0022:15:36<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:雙鏈表實現表結構
 */
public class LinkedChart<T> implements IChart<T> {
    private Node headNode;//虛擬尾節點--至關於頭部火車頭
    private Node tailNode;//虛擬尾節點--至關於尾部火車頭
    private int size;//元素個數

    public SingleLinkedChart() {//一開始兩個火車頭彼此相連
        headNode = new Node(null, null, null);//實例化頭結點
        tailNode = new Node(headNode, null, null);//實例化尾節點,並將prev指向頭
        headNode.next = tailNode;//頭指尾
        size = 0;//鏈表長度置零
    }
    
    @Override
    public void add(int index, T el) {
    }

    @Override
    public void add(T el) {
        add(size, el);
    }

    @Override
    public T remove(int index) {
        return null;
    }

    @Override
    public T remove() {
        return remove(size);
    }

    @Override
    public int removeEl(T el) {
        return 0;
    }

    @Override
    public boolean removeEls(T el) {
        return false;
    }

    @Override
    public void clear() {
        headNode = new Node(null, null, null);//實例化頭結點
        tailNode = new Node(headNode, null, null);//實例化尾節點,並將prev指向頭
        headNode.next = tailNode;//頭指尾
        size = 0;//鏈表長度置零
    }

    @Override
    public T set(int index, T el) {
      return null;
    }

    @Override
    public T get(int index) {
         return null;
    }

    @Override
    public int[] getIndex(T el) {
        return null;
    }

    @Override
    public boolean contains(T el) {
        return getIndex(el).length != 0;
    }

    @Override
    public IChart<T> contact(IChart<T> iChart) {
        return null;
    }

    @Override
    public IChart<T> contact(int index, IChart<T> iChart) {
        return null;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public int capacity() {
        return size;
    }
複製代碼
2.單鏈節點類(Node)的設計:

LinkedChart至關於一列雙頭火車,暫且按下,先把車箱的關係弄好,最後再拼接列車會很是方便
這裏將Node做爲LinkedChart的一個私有內部類,是爲了隱藏Node,並能使用LinkedChart的資源
就像心臟在身體內部,外面人看不見,但它卻相當重要,而且還能獲取體內的信息與資源git

一節雙鏈車箱,最少要知道里面的貨物(node.T)是什麼,
它的下一節車箱(node.next)是哪一個,以及上一節車箱(node.prev)是哪一個github

private class Node {
    /**
     * 數據
     */
    private T el;
    
    /**
     * 前節點
     */
    private Node prev;
    
    /**
     * 後節點
     */
    private Node next;
    
    private Node(Node prev, Node next, T el) {
        this.el = el;
        this.prev = prev;
        this.next = next;
    }
}
複製代碼

2、節點(Node)的底層操做(CRUD)----鏈表的心臟

1.查詢操做:getNode

上一篇的單鏈表查詢:至關於在一個視口一個一個挨着找車箱
雙鏈表有兩個火車頭,這就表明兩頭都能操做,因此爲了儘可能高效,判斷一下車箱在前半仍是後半
這樣比起單鏈表要快上不少(越日後快得越明顯)編程

/**
  * 根據索引獲取節點
  *
  * @param index 索引
  * @return 索引處節點
  */
 private Node<T> getNode(int index) {
     Node<T> targetNode;//聲明目標節點
     if (index < 0 || index > size - 1) { //索引越界處理
         throw new IndexOutOfBoundsException();
     }
     //若是索引在前半,前序查找
     if (index < size / 2) {
         targetNode = headNode.next;
         for (int i = 0; i < index; i++) {
             targetNode = targetNode.next;
         }
     } else {  //若是索引在後半,反序查找
         targetNode = tailNode.prev;
         for (int i = size - 1; i < index; i++) {
             targetNode = targetNode.prev;
         }
     }
     return targetNode;
 }
複製代碼

2.插入操做:addNode()

仍是想下火車頭:想在T0和T1車箱之間插入一節T3車箱怎麼辦?
第一步:將T0的後鏈栓到T3上,T3的前鏈栓到T0上-----完成了T0和T3的鏈接
第二步:將T3的後鏈栓到T2上,T1的前鏈栓到T3上-----完成了T1和T3的鏈接canvas

雙鏈表添加.png

/**
 * 根據目標節點插入新節點--目標節點以前
 *
 * @param target 目標節點
 * @param el     新節點數據
 */
private void addNode(Node target, T el) {
    //想在T0和T1車箱之間插入一節T3車箱爲例:
    //新建T3,將前、後指向分別指向T0和T1
    Node newNode = new Node(target.prev, target, el);
    //T0的next指向T3
    target.prev.next = newNode;
    //T1的prev指向T3
    target.prev = newNode;
    //大功告成:鏈表長度+1
    size++;
}
複製代碼

3.移除操做:removeNode()

仍是想下火車頭:如何刪除T1:
很簡單:T0和T2手拉手就好了,而後再讓T1孤獨的離去...數組

雙鏈表刪除節點.png

/**
 * 移除目標節點
 *
 * @param target 目標節點
 * @return 目標節點數據
 */
private T removeNode(Node target) {
    //目標前節點的next指向目標節點後節點
    target.prev.next = target.next;
    //目標後節點的prev指向目標節點前節點
    target.next.prev = target.prev;
    target.prev = null;//放開左手
    target.next = null;//放開右手---揮淚而去
    //鏈表長度-1
    size--;
    return target.el;
}
複製代碼

3.修改節點:setNode
/**
 * 修改節點
 *
 * @param index 節點位置
 * @param el    數據
 * @return 修改後的節點
 */
private T setNode(int index, T el) {
    Node node = getNode(index);
    T tempNode = node.el;
    node.el = el;
    return tempNode;
}
複製代碼
4.清空操做:clearNode()

思路和刪除同樣:首尾虛擬節點互指,中間沒有元素,形式上看,當前鏈表上所有刪除
從新實例化頭結點------火車頭說:老子從頭(null)開始,不帶大家玩了,
從新實例化尾節點------火車尾說:老孃從頭(null)開始,不帶大家玩了,
因而火車頭和火車尾,手牽手,走向遠方...bash

雙鏈表清空.png

/**
 * 清空全部節點
 */
private void clearNode() {
    //實例化頭結點
    headNode = new Node<T>(null, null, null);
    //實例化尾節點,並將prev指向頭
    tailNode = new Node<T>(headNode, null, null);
    headNode.next = tailNode;
    //鏈表長度置零
    size = 0;
}
複製代碼

2、利用鏈表實現對數據的操做

鏈表只是對節點的操做,只是一種結構,並不是真正目的,最終要讓鏈表對外不可見,就像人的骨骼之於軀體
軀體的任何動做是骨骼以支撐,而骨骼並不可見,從外來看只是軀體的動做而已。
咱們須要的是按照這種結構對數據進行增刪改查等操做,並暴露接口由外方調用微信

一、定點添加操做--add

定點添加.gif

@Override
public void add(int index, T el) {
    // index能夠取到size,在鏈表末尾空位置添加元素。
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed. Illegal index");
    }
    addNode(getNode(index), el);
}
複製代碼

2.定點移除操做--remove

定點刪除.gif

@Override
public T remove(int index) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Remove failed. Illegal index");
    }
    return removeNode(getNode(index));
}
複製代碼

3.獲取和修改--get和set

雙鏈表更新與查詢.gif

@Override
public T get(int index) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Get failed. Illegal index");
    }
    return getNode(index).el;
}

@Override
public T set(int index, T el) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("ISet failed. Illegal index");
    }
    return setNode(index, el);
}
複製代碼

4、其餘操做

和單鏈表基本一致,就不演示了。數據結構

1.是否包含某元素:
@Override
public boolean contains(T el) {
    Node target = headNode;
    while (target.next != null) {
        if (el.equals(target.next)) {
            return true;
        }
    }
    return false;
}
複製代碼
2.根據指定元素獲取匹配索引
@Override
public int[] getIndex(T el) {
    int[] tempArray = new int[size];//臨時數組
    int index = 0;//重複個數
    int count = 0;
    Node node = headNode.next;
    while (node.next != null) {
        if (el.equals(node.el)) {
            tempArray[index] = -1;
            count++;
        }
        index++;
        node = node.next;
    }
    int[] indexArray = new int[count];//將臨時數組壓縮
    int indexCount = 0;
    for (int i = 0; i < tempArray.length; i++) {
        if (tempArray[i] == -1) {
            indexArray[indexCount] = i;
            indexCount++;
        }
    }
    return indexArray;
}
複製代碼
3.按元素移除:(找到,再刪除...)
@Override
public int removeEl(T el) {
    int[] indexes = getIndex(el);
    int index = -1;
    if (indexes.length > 0) {
        index = indexes[0];
        remove(indexes[0]);
    }
    return index;
}

@Override
public boolean removeEls(T el) {
    int[] indexArray = getIndex(el);
    if (indexArray.length != 0) {
        for (int i = 0; i < indexArray.length; i++) {
            remove(indexArray[i] - i); // 注意-i
        }
        return true;
    }
    return false;
}
複製代碼
4.定點鏈接兩個單鏈表
///////////////只是實現一下,getHeadNode和getLastNode破壞了Node的封裝性,不太好/////////////
@Override
public IChart<T> contact(IChart<T> iChart) {
    return contact(0, iChart);
}

@Override
public IChart<T> contact(int index, IChart<T> iChart) {
    if (!(iChart instanceof LinkedChart)) {
        return null;
    }
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Contact failed. Illegal index");
    }
    LinkedChart linked = (LinkedChart) iChart;
    Node targetNode = getNode(index);
    Node targetNextNode = targetNode.next;
    //目標節點的next指向待接鏈表的第一個節點
    targetNode.next = linked.getHeadNode().next;
    //向待接鏈表的第一個節點的prev指向目標節點
    linked.getHeadNode().next.prev = targetNode;
    //目標節點的下一節點指的prev向待接鏈表的最後一個節點
    targetNextNode.prev = linked.getLastNode().prev;
    //向待接鏈表的最後一個節點的next指向目標節點的下一節點的
    linked.getLastNode().prev.next = targetNextNode;
    return this;
}
public Node getHeadNode() {
    return headNode;
}
public Node getLastNode() {
    return tailNode;
}
///////////////////////////////////////////////////////////////
複製代碼

5、小結:

1.優劣分析
優勢:  動態建立,節省空間  
        頭部、尾部添加容易  
        定點插入/刪除整體上優於數組表(由於最多找一半,數組表最多所有)
缺點:  空間上不連續,形成空間碎片化
        查找相對困難,只能從頭開始或結尾一個一個找(但比單鏈表優秀)
使用場景:[雙鏈表]增刪性能整體優於[數組表]和[單鏈表],頻繁增刪不定位置時[雙鏈表]最佳
複製代碼
2.最後把視圖一塊兒說了吧

接口都是相同的,底層實現更換了,並不會影響視圖層,只是把視圖層的單體繪製更改一下就好了。
詳細的繪製方案見這裏

/**
 * 繪製表結構
 *
 * @param canvas
 */
private void dataView(Canvas canvas) {
    mPaint.setColor(Color.BLUE);
    mPaint.setStyle(Paint.Style.FILL);
    mPath.reset();
    for (int i = 0; i < mArrayBoxes.size(); i++) {
        LinkedNode box = mArrayBoxes.get(i);
        mPaint.setColor(box.color);
        canvas.drawRoundRect(
                box.x, box.y, box.x + Cons.BOX_WIDTH, box.y + Cons.BOX_HEIGHT,
                BOX_RADIUS, BOX_RADIUS, mPaint);
        mPath.moveTo(box.x, box.y);
        mPath.rCubicTo(Cons.BOX_WIDTH / 2, Cons.BOX_HEIGHT / 2,
                Cons.BOX_WIDTH / 2, Cons.BOX_HEIGHT / 2, Cons.BOX_WIDTH, 0);
        if (i < mArrayBoxes.size() - 1 ) {
            LinkedNode box_next = mArrayBoxes.get(i + 1);
            LinkedNode box_now = mArrayBoxes.get(i);
            if (i % LINE_ITEM_NUM == LINE_ITEM_NUM - 1) {//邊界狀況
                mPath.rLineTo(0, Cons.BOX_HEIGHT);
                mPath.lineTo(box_next.x + Cons.BOX_WIDTH, box_next.y);
                mPath.rLineTo(Cons.ARROW_DX, -Cons.ARROW_DX);
                mPath.moveTo(box_next.x, box_next.y);
                mPath.lineTo(box_now.x, box_now.y+Cons.BOX_HEIGHT);
                mPath.rLineTo(-Cons.ARROW_DX, Cons.ARROW_DX);
            } else {
                mPath.rLineTo(0, Cons.BOX_HEIGHT / 2.2f);
                mPath.lineTo(box_next.x+Cons.BOX_WIDTH * 0.2f, box_next.y + Cons.BOX_HEIGHT / 2f);
                mPath.rLineTo(-Cons.ARROW_DX, -Cons.ARROW_DX);
                mPath.moveTo(box_next.x, box_next.y);
                mPath.rLineTo(0, Cons.BOX_HEIGHT / 1.2f);
                mPath.lineTo(box_now.x + Cons.BOX_WIDTH * 0.8f, box_now.y + Cons.BOX_HEIGHT * 0.8f);
                mPath.rLineTo(Cons.ARROW_DX, Cons.ARROW_DX);
            }
        }
        canvas.drawPath(mPath, mPathPaint);
        canvas.drawText(box.index + "",
                box.x + Cons.BOX_WIDTH / 2,
                box.y + 3 * OFFSET_OF_TXT_Y, mTxtPaint);
        canvas.drawText(box.data + "",
                box.x + Cons.BOX_WIDTH / 2,
                box.y + Cons.BOX_HEIGHT / 2 + 3 * OFFSET_OF_TXT_Y, mTxtPaint);
    }
}
複製代碼

本系列後續更新連接合集:(動態更新)

後記:捷文規範


2.本文成長記錄及勘誤表
項目源碼 日期 備註
V0.1--github 2018-11-23 看得見的數據結構Android版之雙鏈表的實現
3.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
個人github 個人簡書 個人掘金 我的網站
4.聲明

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持


icon_wx_200.png
相關文章
相關標籤/搜索