03數據結構和算法(Java描述)~單鏈表
本文是上一篇文章的後續,詳情點擊該鏈接
鏈表
單鏈表
單鏈表的定義
單鏈表採用的是鏈式存儲結構,使用一組地址任意的存儲單元來存放數據元素。在單鏈表中, 存儲的每一條數據都是以節點來表示的,每個節點的構成爲:元素(存儲數據的存儲單元) + 指 針(存儲下一個節點的地址值),單鏈表的節點結構如下圖所示:
另外,單鏈表中的開始節點,我們又稱之爲首節點;單鏈表中的終端節點,我們又稱之爲尾節 點。如下圖所示:
根據序號獲取結點的操作:
在線性表中,每個節點都有一個唯一的序號,該序號是從 0開始遞增的。通過序號獲取單鏈表 的節點時,我們需要從單鏈表的首節點開始,從前往後循環遍歷,直到遇到查詢序號所對應的節點 時爲止。
以下圖爲例,我們需要獲得序號爲2的節點,那麼就需要依次遍歷獲得“節點 11”和“節點 22”, 然後才能獲得序號爲 2的節點,也就是“節點 33”。
因此,在鏈表中通過序號獲得節點的操作效率是非常低的,查詢的時間複雜度爲 O(n)。
根據序號刪除節點的操作
根據序號刪除節點的操作,我們首先應該根據序號獲得需要刪除的節點,然後讓“刪除節點的 前一個節點”指向“刪除節點的後一個節點”,這樣就實現了節點的刪除操作。
以下圖爲例,我們需要刪除序號爲2的節點,那麼就讓“節點22”指向“節點44”即可,這樣 就刪除了序號爲 2的節點,也就是刪除了“節點 33”。
通過序號來插入節點,時間主要浪費在找正確的刪除位置上,故時間複雜度爲 O(n)。但是,單 論刪除的操作,也就是無需考慮定位到刪除節點的位置,那麼刪除操作的時間複雜度就是 O(1)。
根據序號插入節點的操作
根據序號插入節點的操作,我們首先應該根據序號找到插入的節點位置,然後讓“插入位置的 上一個節點”指向“新插入的節點”,然後再讓“新插入的節點”指向“插入位置的節點”,這樣 就實現了節點的插入操作。
以下圖爲例,我們需要在序號爲 2 的位置插入元素值“00”,首先先把字符串“00”封裝爲一 個節點對象,然後就讓“節點 22”指向“新節點 00”,最後再讓“節點00”指向“節點 33”,這 樣就插入了一個新節點。
通過序號來插入節點,時間主要浪費在找正確的插入位置上,故時間複雜度爲 O(n)。但是,單 論插入的操作,也就是無需考慮定位到插入節點的位置,那麼插入操作的時間複雜度就是 O(1)。
順序表和單鏈表的比較
存儲方式比較
順序表採用一組地址連續的存儲單元依次存放數據元素,通過元素之間的先後順序來確定元素 之間的位置,因此存儲空間的利用率較高
單鏈表採用一組地址任意的存儲單元來存放數據元素,通過存儲下一個節點的地址值來確定節 點之間的位置,因此存儲空間的利用率較低。
順序表查找的時間複雜度爲 O(1),插入和刪除需要移動元素,因此時間複雜度爲 O(n)。若是需 要頻繁的執行查找操作,但是很少進行插入和刪除操作,那麼建議使用順序表。
單鏈表查找的時間複雜度爲 O(n),插入和刪除無需移動元素,因此時間複雜度爲 O(1)。若是需 要頻繁的執行插入和刪除操作,但是很少進行查找操作,那麼建議使用鏈表。
補充:根據序號來插入和刪除節點,需要通過序號來找到插入和刪除節點的位置,那麼整體的 時間複雜度爲 O(n)。因此,單鏈表適合數據量較小時的插入和刪除操作,如果存儲的數據量較大, 那麼就建議使用別的數據結構,例如使用二叉樹來實現。
空間性能比較
順序表需要預先分配一定長度的存儲空間,如果事先不知道需要存儲元素的個數,分配空間過 大就會造成存儲空間的浪費,分配空間過小則需要執行耗時的擴容操作。
單鏈表不需要固定長度的存儲空間,可根據需求來進行臨時分配,只要有內存足夠就可以分配, 在鏈表中存儲元素的個數是沒有限制的,無需考慮擴容操作。
代碼實現
定義List接口
public interface List {
int size();
void add(Object element);
Object get(int index);
void remove(int index);
void add(int index, Object element);
String toString();
}
SingleLinkedList實現類
public class SingleLinkedList implements List{
// 用於保存單鏈表中的首節點
private Node headNode;
// 用於保存單鏈表中的尾節點
private Node lastNode;
// 用於保存單鏈表中節點的個數
private int size;
// 獲取單鏈表中節點的個數
public int size() {
return this.size;
}
/**
* 添加元素
* @param element 需要添加的數據
*/
public void add(Object element) {
// 1.把需要添加的數據封裝成節點對象
Node node = new Node(element);
// 2.處理單鏈表爲空表的情況
if(headNode == null) {
// 2.1把node節點設置爲單鏈表的首節點
headNode = node;
// 2.2把node節點設置爲單鏈表的尾節點
lastNode = node;
}
// 3.處理單鏈表不是空表的情況
else {
// 3.1讓lastNode指向node節點
lastNode.next = node;
// 3.2更新lastNode的值
lastNode = node;
}
// 4.更新size的值
size++;
}
/**
* 根據序號獲取元素
* @param index 序號
* @return 序號所對應節點的數據值
*/
public Object get(int index) {
// 1.判斷序號是否合法,合法取值範圍:[0, size - 1]
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("序號不合法,index:" + index);
}
// 2.根據序號獲得對應的節點對象
Node node = node(index);
// 3.獲取並返回node節點的數據值
return node.data;
}
/**
* 根據序號刪除元素
* @param index 序號
*/
public void remove(int index) {
// 1.判斷序號是否合法,合法取值範圍:[0, size - 1]
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("序號不合法,index:" + index);
}
// 2.處理刪除節點在開頭的情況
if (index == 0) {
// 2.1獲得刪除節點的後一個節點
Node nextNode = headNode.next;
// 2.2設置headNode的next值爲null
headNode.next = null;
// 2.3設置nextNode爲單鏈表的首節點
headNode = nextNode;
}
// 3.處理刪除節點在末尾的情況
else if (index == size - 1) {
// 3.1獲得刪除節點的前一個節點
Node preNode = node(index - 1);
// 3.2設置preNode的next值爲null
preNode.next = null;
// 3.3設置preNode爲單鏈表的尾節點
lastNode = preNode;
}
// 4.處理刪除節點在中間的情況
else {
// 4.1獲得index-1所對應的節點對象
Node preNode = node(index - 1);
// 4.2獲得index+1所對應的節點對象
Node nextNode = preNode.next.next;
// 4.3獲得刪除節點並設置next值爲null
preNode.next.next = null;
// 4.4設置preNode的next值爲nextNode
preNode.next = nextNode;
}
// 5.更新size的值
size--;
}
/**
* 根據序號插入元素
* @param index 序號
* @param element 需要插入的數據
*/
public void add(int index, Object element) {
// 1.判斷序號是否合法,合法取值範圍:[0, size]
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("序號不合法,index:" + index);
}
// 2.把需要添加的數據封裝成節點對象
Node node = new Node(element);
// 3.處理插入節點在開頭位置的情況
if(index == 0) {
// 3.1設置node的next值爲headNode
node.next = headNode;
// 3.2設置node節點爲單鏈表的首節點
headNode = node;
}
// 4.處理插入節點在末尾位置的情況
else if(index == size) {
// 4.1設置lastNode的next值爲node
lastNode.next = node;
// 4.2設置node節點爲單鏈表的尾節點
lastNode = node;
}
// 5.處理插入節點在中間位置的情況
else {
// 5.1獲得index-1所對應的節點對象
Node preNode = node(index - 1);
// 5.2獲得index所對應的節點對象
Node curNode = preNode.next;
// 5.3設置preNode的next爲node
preNode.next = node;
// 5.4設置node的next爲curNode
node.next = curNode;
}
// 6.更新size的值
size++;
}
/**
* 根據序號獲得對應的節點對象
* @param index 序號
* @return 序號對應的節點對象
*/
private Node node(int index) {
// 1.定義一個零時節點,用於輔助單鏈表的遍歷操作
Node tempNode = headNode;
// 2.定義一個循環,用於獲取index對應的節點對象
for(int i = 0; i < index; i++) {
// 3.更新tempNode的值
tempNode = tempNode.next;
}
// 4.返回index對應的節點對象
return tempNode;
}
// 節點類
private static class Node {
/**
* 用於保存節點中的數據
*/
private Object data;
/**
* 用於保存指向下一個節點的地址值
*/
private Node next;
/**
* 構造方法
* @param data
*/
public Node(Object data) {
this.data = data;
}
}
public String toString() {
//判斷是否爲空,如果爲空就直接返回 []
if(headNode == null){
return "[]";
}
StringBuilder stringBuilder = new StringBuilder("[");
Node p = headNode.next;
while(p != null){
stringBuilder.append(p.data + ",");
p = p.next;
}
//最後一個逗號刪掉
stringBuilder.deleteCharAt(stringBuilder.length()-1);
stringBuilder.append("]");
return stringBuilder.toString();
}
}
Test
public class Test {
public static void main(String[] args) {
// 1.創建一個對象
List list = new SingleLinkedList();
// 2.添加元素
list.add("11"); // 0
list.add("22"); // 1
list.add("33"); // 2
list.add("44"); // 3
list.add("55"); // 4
//刪除
list.remove(0);
//序號添加
list.add(2, "00");
//測試get
for(int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
//toString
System.out.println(list.toString());
}
}