看得見的資料結構Android版之雙鏈表的實現
零、前言
1.上一篇分析了單鏈表,連結串列是一種資料結構,用來承載資料,每個表節點裝載一個數據元素
2.雙鏈表是每個節點除了資料元素外還分別持有前、後兩個節點的引用
3.為了統一節點的操作,一般在真實連結串列的首尾各加一個虛擬節點,稱為頭節點和尾節點
4.如果說單鏈表是一列火車,那雙鏈表就是一輛雙頭加固版火車,java中的LinkedList底層便是此結構
5.本例操作演示原始碼: Android" target="_blank" rel="nofollow,noindex">希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star
1.留圖鎮樓:雙鏈表的最終實現的操作效果:

雙鏈表操作合集.gif
2.對於雙鏈表簡介:
雙鏈表是對節點(Node)的操作,而節點(Node)又承載資料(T) 總的來看,雙鏈表通過操作節點(Node)從而操作資料(T),就像車廂運送獲取,車廂只是載體,貨物是我們最關注 雙鏈表不同於單鏈表至處在於一個節點同時持有前、後兩個節點的引用,使得對頭尾操作都方便 Node只不過是一個輔助工具,並不會暴露在外。它與資料相對應,又使資料按鏈式排布, 操縱節點也就等於操縱資料,就像提線木偶,並不是直接操作木偶的各個肢體本身(資料T)。 為了統一節點的操作,通常在連結串列最前面新增一個虛擬頭(headNode)和虛擬尾(tileNode)(封裝在內中,不暴露)

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

本文要務.png
一、雙鏈表結構的實現:LinkedChart
1.表的介面定義在 陣列表篇 ,這裡就不貼了
這裡給出實現介面後的LinkedChart以及簡單方法的實現
/** * 作者:張風捷特烈<br/> * 時間:2018/11/22 0022:15:36<br/> * 郵箱:[email protected]<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的資源
就像心臟在身體內部,外面人看不見,但它卻至關重要,並且還能獲取體內的資訊與資源
一節雙鏈車廂,最少要知道里面的 貨物(node.T)
是什麼,
它的 下一節車廂(node.next)
是哪個,以及 上一節車廂(node.prev)
是哪個
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; } }
二、節點(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的連線

雙鏈表新增.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)開始,不帶你們玩了,
於是火車頭和火車尾,手牽手,走向遠方...

雙鏈表清空.png
/** * 清空所有節點 */ private void clearNode() { //例項化頭結點 headNode = new Node<T>(null, null, null); //例項化尾節點,並將prev指向頭 tailNode = new Node<T>(headNode, null, null); headNode.next = tailNode; //連結串列長度置零 size = 0; }
二、利用連結串列實現對資料的操作
連結串列只是對節點的操作,只是一種結構,並非真正目的,最終要讓連結串列對外不可見,就像人的骨骼之於軀體
軀體的任何動作是骨骼以支撐,而骨骼並不可見,從外來看只是軀體的動作而已。
我們需要的是按照這種結構對資料進行增刪改查等操作,並暴露介面由外方呼叫
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(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); }
四、其他操作
和單鏈表基本一致,就不演示了。
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; } ///////////////////////////////////////////////////////////////
五、小結:
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); } }
後記:捷文規範
本系列後續更新連結合集:(動態更新)
看得見的資料結構Android版之雙鏈表篇(待完成)
看得見的資料結構Android版之棧(待完成)
看得見的資料結構Android版之佇列(待完成)
看得見的資料結構Android版之二叉樹篇(待完成)
看得見的資料結構Android版之二分搜尋樹篇(待完成)
看得見的資料結構Android版之AVL樹篇(待完成)
看得見的資料結構Android版之紅黑樹篇(待完成)
更多資料結構---以後再說吧
2.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-23 | 看得見的資料結構Android版之雙鏈表的實現 |
3.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
4.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png