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

2.對於單鏈表簡介:
單鏈表是對節點(Node)的操作,而節點(Node)又承載資料(T) 總的來看,單鏈表通過操作節點(Node)從而操作資料(T),就像車廂運送獲取,車廂只是載體,貨物是我們最關注 Node只不過是一個輔助工具,並不會暴露在外。它與資料相對應,又使資料按鏈式排布, 操縱節點也就等於操縱資料,就像提線木偶,並不是直接操作木偶的各個肢體本身(資料T)。 為了統一節點的操作,通常在連結串列最前面新增一個虛擬頭結點(避免對頭部單獨判斷) 複製程式碼

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

一、單鏈表結構的實現:SingleLinkedChart
1.表的介面定義在陣列表篇,這裡就不貼了
這裡給出實現介面後的SingleLinkedChart以及簡單方法的實現
/** * 作者:張風捷特烈<br/> * 時間:2018/11/22 0022:15:36<br/> * 郵箱:[email protected]<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到節點資料),如何用程式碼來模擬呢?

/** * 根據索引尋找節點 * * @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)

/** * 在指定連結串列前新增節點 * * @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被孤立了,把自己的鏈子拿掉,傷心地走開...

/** * 移除指定索引的節點 * * @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
可見在選中點的前面新增一個節點
處於單鏈表的特點:頭部新增容易,尾部新增要查詢一遍,所以預設是新增在頭部

@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
處於單鏈表的特點:頭部刪除容易,尾部刪除要查詢一遍,所以預設是刪除在頭部

@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

@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; } 複製程式碼
四、其他操作

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); } } 複製程式碼
本系列後續更新連結合集:(動態更新)
看得見的資料結構Android版之表的陣列實現(資料結構篇)
看得見的資料結構Android版之雙鏈表篇(待完成)
看得見的資料結構Android版之棧(待完成)
看得見的資料結構Android版之佇列(待完成)
看得見的資料結構Android版之二叉樹篇(待完成)
看得見的資料結構Android版之二分搜尋樹篇(待完成)
看得見的資料結構Android版之AVL樹篇(待完成)
看得見的資料結構Android版之紅黑樹篇(待完成)
更多資料結構---以後再說吧
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-23 | 看得見的資料結構Android版之單鏈表的實現 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
