看得見的資料結構Android版之單連結串列篇

張風捷特烈發表於2018-11-24

零、前言

1.前面用陣列實現了表結構,也分析了陣列表的侷限性(頭部修改困難)
2.今天來講另一種資料結構:單連結串列,它是一種最簡單的動態資料結構
3.連結串列有點像火車,一節拴著一節,想要在某節後加一節,開啟連線處,再拴上就行了
4.本例操作演示原始碼:希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star


1.留圖鎮樓:單連結串列的最終實現的操作效果:

單連結串列操作合集.gif

2.對於單連結串列簡介:
單連結串列是對節點(Node)的操作,而節點(Node)又承載資料(T)  
總的來看,單連結串列通過操作節點(Node)從而運算元據(T),就像車廂運送獲取,車廂只是載體,貨物是我們最關注

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

為了統一節點的操作,通常在連結串列最前面新增一個虛擬頭結點(避免對頭部單獨判斷)
複製程式碼

一個連結串列.png

注意:單連結串列的實際使用場景並不多,因為有比他更厲害的雙連結串列
在某些特定場景,比如只是頻繁對頭結點進行操作,單連結串列最佳,
單連結串列的講解為雙連結串列做鋪墊,直接講雙連結串列肯跨度有點大。作為資料結構之一,還是要不要了解一下。


3.單連結串列的實現:本文要務

單連結串列實現表結構.png


一、單連結串列結構的實現:SingleLinkedChart

1.表的介面定義在陣列表篇,這裡就不貼了

這裡給出實現介面後的SingleLinkedChart以及簡單方法的實現

/**
 * 作者:張風捷特烈<br/>
 * 時間:2018/11/22 0022:15:36<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:單連結串列實現表結構
 */
public class SingleLinkedChart<T> implements IChart<T> {
    private Node headNode;//虛擬頭結點
    private int size;//元素個數

    public SingleLinkedChart() {
        headNode = new Node(null, null);
        size = 0;
    }
    
    @Override
    public void add(int index, T el) {
    }

    @Override//預設新增到頭部
    public void add(T el) {
        add(0, el);
    }

    @Override//預設刪除頭部
    public T remove(int index) {
        return null;
    }

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

    @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);
        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)的設計:

SingleLinkedChart相當於一列火車,暫且按下,先把車廂的關係弄好,最後再拼接列車會非常方便
這裡將Node作為SingleLinkedChart的一個私有內部類,是為了隱藏Node,並能使用SingleLinkedChart的資源
就像心臟在身體內部,外面人看不見,但它卻至關重要,並且還能獲取體內的資訊與資源
一節車廂,最少要知道里面的貨物(node.T)是什麼,它的下一節車廂(node.next)是哪個

 /**
 * 內部私有節點類
 */
private class Node {
    /**
     * 節點資料元素
     */
    private T el;
    /**
     * 下一節點的引用
     */
    private Node next;
    private Node(Node next, T el) {
        this.el = el;
        this.next = next;
    }
}
複製程式碼

二、節點(Node)的底層操作(CRUD)----連結串列的心臟

1.查詢車廂:

比如你的可視區域就是一節車廂的長度,一開始只能看見火車頭
從火車頭開始,你需要一節一節往下找,也就相當於,列車每次開一節,到你想要的位置,就停下來
這是你就能檢視車廂的貨物(get到節點資料),如何用程式碼來模擬呢?

單連結串列查詢.png

/**
 * 根據索引尋找節點
 *
 * @param index 索引
 * @return 節點
 */
private Node getNode(int index) {
    //宣告目標節點
    Node targetNode = headNode;
    for (int i = 0; i < index; i++) {//火車運動驅動源
        //一節一節走,直到index
        targetNode = targetNode.next;
    }
    return targetNode;
}
複製程式碼

可以檢測一下:index=0時,targetNode = targetNode.next 執行1次,也就獲得了T0車廂
index=1時,targetNode = targetNode.next 執行2次,也就獲得了T1車廂...


2.定點插入

還是想下火車頭:想在2號車廂(target)和1號車廂之間插入一節T4車廂怎麼辦?
第一步:找到2號車廂的前一節車廂--1號廂(target-1)
第二步:將1號廂(target-1)的鏈子(next)栓到T4車廂上,再把T4的鏈子栓到2號車廂(target)

節點插入.png

/**
 * 在指定連結串列前新增節點
 *
 * @param index 索引
 * @param el    資料
 */
private void addNode(int index, T el) {
    Node preTarget = getNode(index - 1);//獲取前一節車廂
    //新建節點,同時下一節點指向target的下一節點--
    //這裡有點繞,分析一下:例子:2號車廂和1號車廂之間插入一節T4車廂
    //preTarget:1號車廂   preTarget.next:2號車廂
    //T4車廂:new Node(preTarget.next, el)---建立時就把鏈子拴在了:preTarget.next:2號車廂
    Node tNode = new Node(preTarget.next, el);
    //preTarget的next指向tNode--- 1號車廂栓到T4車廂
    preTarget.next = tNode;
    size++;
}
複製程式碼

3.定點修改

你要把車廂的貨物換一下,這還不簡單,找到車廂,換唄

/**
 * 修改節點
 * @param index 節點位置
 * @param el 資料
 * @return 修改後的節點
 */
private Node<T> setNode(int index, T el) {
    Node<T> node = getNode(index);
    node.el = el;
    return node;
}
複製程式碼

4.定點移除

要把T1車廂移除:T0和T2手拉手,好朋友,T1被孤立了,把自己的鏈子拿掉,傷心地走開...

單連結串列刪除.png

/**
 * 移除指定索引的節點
 *
 * @param index 索引
 * @return 刪除的元素
 */
private T removeNode(int index) {
    Node preTarget = getNode(index - 1);//獲取前一節車廂
    Node target = preTarget.next;//目標車廂
    //前一節車廂的next指向目標車廂下一節點
    preTarget.next = target.next;//T0和T2手拉手
    target.next = null;//T1把自己的鏈子拿掉,傷心地走開...
    size--;
    return target.el;
}
複製程式碼

三、節點(Node)的操作完成了,下面拼火車吧(SingleLinkedChart)

感覺真正的連結串列就是一個包裝殼,暴漏了操作介面給外界,內部勞苦功高的還是Node
這種封裝在程式設計裡非常常見,有些聞名遐邇的類中有很多都是默默無聞的大佬

1、定點新增操作--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(index + 1, el);
    //為了介面規範,計數從0開始,而連結串列沒有索引概念,只是第幾個,T0被認為是第一節車廂。
    //比如選中點2---說明是目標是第3節車廂,所以index + 1 =2+1
}
複製程式碼

2.定點移除操作--remove

處於單連結串列的特點:頭部刪除容易,尾部刪除要查詢一遍,所以預設是刪除在頭部

定點刪除操作.gif

@Override
public T remove(int index) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Remove failed. Illegal index");
    }
    return removeNode(index + 1);//同理
}
複製程式碼

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

get和set操作.gif

@Override
public T set(int index, T el) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Set failed. Illegal index");
    }
    return setNode(index + 1, el).el;
}

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

四、其他操作

其他操作.gif

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 != 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 SingleLinkedChart)) {
    return null;
    }

    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Contact failed. Illegal index");
    }
    SingleLinkedChart<T> linked = (SingleLinkedChart<T>) iChart;
    Node firstNode = linked.getHeadNode().next;//接入連結串列 頭結點
    Node lastNode = linked.getLastNode();//接入連結串列 尾結點
    Node target = getNode(index + 1);//獲取目標節點
    Node targetNext = target.next;//獲取目標節點的下一節點
    target.next = firstNode;//獲取目標節點的next連到 接入連結串列 頭結點
    lastNode.next = targetNext; //待接入連結串列 尾結點連到 目標節點的下一節點
    return this;
}

public Node getHeadNode() {
    return headNode;
}
public Node getLastNode() {
    return getNode(size);
}
///////////////////////////////////////////////////////////////
複製程式碼

五、單連結串列小結:

1.簡單測試
方法\數量 1000 10000 10W 100W 1000W
add首 0.0002秒 0.0009秒 0.0036秒 0.5039秒 3.1596秒
add尾 0.0029秒 0.1096秒 9.1836秒 ---- ----
remove首 0.0001秒 0.0016秒 0.0026秒 0.0299秒 0.1993秒
remove尾 0.0012秒 0.1009秒 8.9750秒 ---- ----
2.優劣分析
優點:  動態建立,節省空間  
        頭部新增容易  
缺點:  空間上不連續,造成空間碎片化
        查詢困難,只能從頭開始一個一個找
使用場景:完全可用雙連結串列替代,只對前面元素頻繁增刪,單連結串列優勢最高。
複製程式碼
3.最後把檢視一起說了吧

介面都是相同的,底層實現更換了,並不會影響檢視層,只是把檢視層的單體繪製更改一下就行了。
詳細的繪製方案見這裡

/**
 * 繪製表結構
 *
 * @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++) {
        SingleNode 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) {
            SingleNode box_next = mArrayBoxes.get(i + 1);
            if (i % 6 == 6 - 1) {//邊界情況
                mPath.rLineTo(0, Cons.BOX_HEIGHT);
                mPath.rLineTo(-Cons.BOX_WIDTH / 2, 0);
                mPath.lineTo(box_next.x + Cons.BOX_WIDTH / 2f, box_next.y);
                mPath.rLineTo(Cons.ARROW_DX, -Cons.ARROW_DX);
            } else {
                mPath.rLineTo(0, Cons.BOX_HEIGHT / 2f);
                mPath.lineTo(box_next.x, box_next.y + Cons.BOX_HEIGHT / 2f);
                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);
    }
}
複製程式碼

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


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-21 看得見的資料結構Android版之表的陣列實現(資料結構篇)
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章