零、前言
1.前面用
陣列
實現了表結構
,也分析了陣列表的侷限性(頭部修改困難)
2.今天來講另一種資料結構:單連結串列
,它是一種最簡單的動態資料結構
3.連結串列有點像火車,一節拴著一節,想要在某節後加一節,開啟連線處,再拴上就行了
4.本例操作演示原始碼:希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star
1.留圖鎮樓:單連結串列的最終實現的操作效果:
2.對於單連結串列簡介:
單連結串列是對節點(Node)的操作,而節點(Node)又承載資料(T)
總的來看,單連結串列通過操作節點(Node)從而運算元據(T),就像車廂運送獲取,車廂只是載體,貨物是我們最關注
Node只不過是一個輔助工具,並不會暴露在外。它與資料相對應,又使資料按鏈式排布,
操縱節點也就等於操縱資料,就像提線木偶,並不是直接操作木偶的各個肢體本身(資料T)。
為了統一節點的操作,通常在連結串列最前面新增一個虛擬頭結點(避免對頭部單獨判斷)
複製程式碼
注意:單連結串列的實際使用場景並不多,因為有比他更厲害的雙連結串列
在某些特定場景,比如只是頻繁對頭結點進行操作,單連結串列最佳,
單連結串列的講解為雙連結串列做鋪墊,直接講雙連結串列肯跨度有點大。作為資料結構之一,還是要不要了解一下。
3.單連結串列的實現:本文要務
一、單連結串列結構的實現: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到節點資料),如何用程式碼來模擬呢?
/**
* 根據索引尋找節點
*
* @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版之佇列篇
- 看得見的資料結構Android版之二分搜尋樹篇
- 更多資料結構---以後再說吧
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-21 | 看得見的資料結構Android版之表的陣列實現(資料結構篇) |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援