看得見的資料結構Android版之雙鏈表篇
零、前言
1.上一篇分析了單鏈表,連結串列是一種資料結構,用來承載資料,每個表節點裝載一個數據元素
2.雙鏈表是每個節點除了資料元素外還分別持有前、後兩個節點的引用
3.為了統一節點的操作,一般在真實連結串列的首尾各加一個虛擬節點,稱為頭節點和尾節點
4.如果說單鏈表是一列火車,那雙鏈表就是一輛雙頭加固版火車,java中的LinkedList底層便是此結構
5.本例操作演示原始碼:希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star
1.留圖鎮樓:雙鏈表的最終實現的操作效果:
2.對於雙鏈表簡介:
雙鏈表是對節點(Node)的操作,而節點(Node)又承載資料(T)
總的來看,雙鏈表通過操作節點(Node)從而操作資料(T),就像車廂運送獲取,車廂只是載體,貨物是我們最關注
雙鏈表不同於單鏈表至處在於一個節點同時持有前、後兩個節點的引用,使得對頭尾操作都方便
Node只不過是一個輔助工具,並不會暴露在外。它與資料相對應,又使資料按鏈式排布,
操縱節點也就等於操縱資料,就像提線木偶,並不是直接操作木偶的各個肢體本身(資料T)。
為了統一節點的操作,通常在連結串列最前面新增一個虛擬頭(headNode)和虛擬尾(tileNode)(封裝在內中,不暴露)
複製程式碼
3.雙鏈表的實現:本文要務
一、雙鏈表結構的實現: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的連線
/**
* 根據目標節點插入新節點--目標節點之前
*
* @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孤獨的離去...
/**
* 移除目標節點
*
* @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)開始,不帶你們玩了,
於是火車頭和火車尾,手牽手,走向遠方...
/**
* 清空所有節點
*/
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
@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
@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
@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版之棧篇
- 看得見的資料結構Android版之佇列篇
- 看得見的資料結構Android版之二分搜尋樹篇
- 更多資料結構---以後再說吧
後記:捷文規範
2.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-23 | 看得見的資料結構Android版之雙鏈表的實現 |
3.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
4.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援