前言
發一下牢騷,本來這個數據結構梳理的系列是在我找工作之前開始的,但是在中間找工作的過程中,一部分原因是面試太忙時間比較少,只能舍重就輕,當然更大的原因還是我自己的惰性,爲了保持這個系列的完整性,以及自己日後的複習,於是決定重新開始,按照之前的思路,將這個系列梳理完,中間也會穿插一些關於這些數據結構在面試中的考題。
好了,上一篇講的最基礎的線性表的順序表示,也就是基於數組,那麼本篇的主要內容就是線性表的鏈式表示,簡言之就是鏈表
目錄
1、鏈表的概念
2、鏈表的基本操作
3、鏈表的優缺點
4、鏈表和數組的對比
5、單(雙)向鏈表的完整代碼
正文
1、鏈表的概念
用一組任意的存儲單元存儲線性表的數據元素的的數據結構。
很簡單,值得一說的是,這個任意的存儲單元,既可以是連續的,也可以是不連續的,在這個概念中的“單元”這個詞,其實就是我們所熟知的節點,每個節點包含兩個域,一個數據域,一個指針域或者兩個指針域,如果是一個指針域,就是單向鏈表,如果是兩個指針域,就是雙向鏈表。
2、鏈表的基本操作
因爲這裏只是基本操作,所以就拿單向鏈表來舉例子,雙向鏈表只是比單向鏈表多了個指針域,只要把單向鏈表弄明白了,對應雙向鏈表的基本操作就自然而然會了。
a.初始化
上一節說線性表的順序表示時,也說過初始化,這裏的初始化也大同小異,主要包括這樣一些值的確定,一個是初始容量,另一個就是空節點的值。
空節點的值可以使用int
類型的默認值,也就是0,因爲是鏈表,所以初始容量一般是0,還記得線性表初始化時的初始容量和負載因子這兩個概念嗎,這裏之所以不需要這兩個東東,是因爲我們在聲明一個數組的時候,必須要給它一個初始容量,而鏈表就比較靈活了,需要一個元素直接鏈上去就行。
由於是鏈表,另一個需要初始化確定的就是節點,因爲這裏以單向鏈表來說明,所以每個節點一個數據成員變量,一個next
指針成員變量,由於內部類的特性,所以節點類一般聲明爲內部類,如下
public class Node {
private int data;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
然後是單向鏈表類的初始化,按照上面描述的,如下
private Node head;//頭結點
private Node tail;//尾節點
private int size = 0;//鏈表長度
public SingleLinkList() {
head = null;
tail = null;
size = 0;
}
public SingleLinkList(int data) {
head = new Node(data, null);
tail = head;
size++;
}
爲了使用的方便,這裏額外加了個使用一個元素值初始化鏈表的構造方法,不過這個不重要,看使用的需求即可,也可爲了方便,自己加上兩個、三個等元素的構造方法來初始化。
b.增加元素
增加元素爲了增加的效率,這裏分爲三種情況,增加在鏈表頭的位置,增加在鏈表尾的位置,增加在鏈表中間的位置。所以我們可以寫三個對應的方法。其中相對複雜點的就是增加在鏈表中間的位置,對應的方法代碼如下
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);//找到前一個節點
preNode.next = new Node(data, preNode.next);
size++;
}
}
}
如果不是自己動手寫的話,還是會發現即便只是一個增加代碼,還是要考慮很多細節問題的,比如,一開始判斷插入位置的合法性,以及鏈表的判空,這樣就可以直接使用尾插來增加元素了,如果查找到應插在鏈表頭,則使用頭插來插入這個元素,否則,我們才手動進行元素的插入。因爲我使用了Node
的構造方法,所以這裏我只用了一行代碼,其實也是最核心的代碼,大致分爲兩步,第一步,將待插入節點的next
指針指向插入位置的next
指針指向的節點,第二步,將前一個節點的next
指針指向待插入節點。
然後再貼上頭插和尾插的方法代碼,頭插的代碼如下
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, head);
head = newNode;
}
size++;
}
尾插的代碼如下
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, null);
tail.next = newNode;
// 將尾指針指向最新的最後一個節點
tail = newNode;
}
size++;
}
c.刪除元素
刪除元素,爲了使用的方便性,也寫了兩個方法,一個是根據下標來刪除,也就是從鏈表頭開始的小標,另一種是根據元素值來刪除。
首先我們來看根據下標來刪除元素,代碼如下
// 刪除指定位置的節點
public void deleteByIndex(int index) {
if (head == null) {
System.out.println("鏈表爲空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 刪除頭節點
{
deleteNode = head;
head = head.next;
size--;
} else if (index == size - 1) {// 刪除尾節點
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
size--;
}
deleteNode = null;
}
}
同樣的,核心操作也只有三步,代碼註釋寫的很清楚所以就不贅述了,但是爲了健壯性和效率性,我們要儘可能的考慮到所有的情況,來優化我們的代碼。
接下來是根據元素值來定向刪除,代碼如下
// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (index == -1) {
System.out.println("鏈表爲空");
} else if (index == -2) {
System.out.println("未找到對應的元素");
} else {
deleteByIndex(index);
}
}
其核心就是調用了一個查找方法和上面的刪除方法,查找方法後面會說到,先不急,我們主要是思路。當然爲了效率, 我們也可以寫一個尾刪的方法,三行搞定
// 刪除 鏈表中最後一個元素
public void deleteLast() {
deleteByIndex(size - 1);
}
所以我們只要弄懂了最核心的方法,其它的我們都可以去利用這個方法自由擴展。
d.查找元素
好了,到了最後一個基本操作,就是查找元素。同樣的,根據我們平時使用的需求,主要是分爲兩種,根據下標查找對應的值,以及已知值查找對應的下標。
我們首先看第一種查找,根據下標查找對應的值,代碼如下
// 通過index查找指定的節點
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
首先是一連串的判斷,然後來到一個核心的循環,由於是單向鏈表,所以只能從鏈表頭開始一個個去遍歷。弄明白了這個方法,那麼第二種查找,根據值來查找對應的下標也就是小case了。代碼如下
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 數組爲空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 從第一個節點開始查找對比數據
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到對應的節點
}
要多說一下的是,如果值沒有找到,我這裏是返回的負數,來代表不同的含義,當然在實際中,最好是拋出異常處理,這樣便於代碼的異常排查。
3、鏈表的優缺點
在熟悉了鏈表的基本操作之後,我們可以很明顯的發現一個問題,就是鏈表的插入和刪除非常簡單,但是鏈表的遍歷就有點麻煩了,總感覺沒有數組方便,數組直接定位,真快,但是鏈表還得從頭開始去遍歷,所以對於鏈表來說,優點是插入和刪除效率高,但是查找效率低,相應的,鏈表適用於大量插入刪除的場景,而對於一些需要頻繁查找的場景就不那麼適合了。
4、鏈表和數組的對比與合體
分析了一波鏈表的優缺點之後,我們再回想下數組,發現數組的優缺點和鏈表是正好反着的,數組是適用於查找,但是插入刪除效率低,因爲涉及到元素大量移動的問題,而鏈表正好彌補了數組的缺點,但是它卻在查找方面表現不佳。
所以沒有哪一種是完美的,或者說通用的,這二者的取捨需要我們根據實際的使用場景來自行選擇。
那麼我們是否想過這樣一個問題,既然這兩個互補,那我們能不能有一種東西把這兩個對象的優點綜合起來呢,這豈不是很完美了,對吧,當然,Java的設計者當然考慮過這個問題,於是他們設計了一個容器叫做HashMap
,嘿嘿嘿,想了解它的原理嗎?想了解其中的奧祕嗎?想知道優秀的Java設計者是如何綜合數組和鏈表的優勢的嗎?限於篇幅,我就不詳解了,這裏我就丟下三個字:哈希表。剩下的就是你們自己去研究啦,hhhh…
5、單(雙)向鏈表的完整代碼
單向鏈表的完整代碼如下
public class SingleLinkList {
public class Node {
private int data;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
private Node head;
private Node tail;
private int size = 0;
public SingleLinkList() {
head = null;
tail = null;
size = 0;
}
public Node getHead() {
return head;
}
public Node getTail() {
return tail;
}
public SingleLinkList(int data) {
head = new Node(data, null);
tail = head;
size++;
}
public int getLength() {
return size;
}
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, head);
head = newNode;
}
size++;
}
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, null);
tail.next = newNode;
// 將尾指針指向最新的最後一個節點
tail = newNode;
}
size++;
}
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);
preNode.next = new Node(data, preNode.next);
size++;
}
}
}
// 通過index查找指定的節點
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 數組爲空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 從第一個節點開始查找對比數據
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到對應的節點
}
// 刪除指定位置的節點
public void deleteByIndex(int index) {
if (head == null) {
System.out.println("鏈表爲空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 刪除頭節點
{
deleteNode = head;
head = head.next;
size--;
} else if (index == size - 1) {// 刪除尾節點
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
size--;
}
deleteNode = null;
}
}
// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (index == -1) {
System.out.println("鏈表爲空");
} else if (index == -2) {
System.out.println("未找到對應的元素");
} else {
deleteByIndex(index);
}
}
// 刪除 鏈表中最後一個元素
public void deleteLast() {
deleteByIndex(size - 1);
}
// 更新指定位置的值
public void updateByIndex(int data, int index) {
findNodeByIndex(index).data = data;
}
// 清除鏈表中所有的元素
public void clear() {
head = null;
tail = null;
size = 0;
}
// 判斷鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
// 鏈表的輸出 重寫toString方法
public String toString() {
if (isEmpty()) {
return "null";
} else {
StringBuilder sb = new StringBuilder("");
for (Node current = head; current != null; current = current.next)// 從head開始遍歷
{
sb.append(current.data + "-");
}
int len = sb.length();
return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
}
}
public static void main(String[] args) {
SingleLinkList list = new SingleLinkList();
list.addAtHead(1);
list.addAtHead(2);
list.addAtHead(3);
list.addAtTail(4);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtIndex(9, 3);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteByIndex(4);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteByData(2);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteLast();
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtTail(5);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtTail(5);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.updateByIndex(66, 3);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.clear();
System.out.println(list+" size="+list.getLength());
}
}
同理,我們可以稍加擴展,寫出雙向鏈表的代碼,如下
public class DoubleLinkList {
public class Node {
private int data;
private Node pre;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node getPre() {
return pre;
}
public Node(int data, Node pre, Node next) {
this.data = data;
this.pre = pre;
this.next = next;
}
}
private Node head;
private Node tail;
private int size = 0;
public DoubleLinkList() {
head = null;
tail = null;
size = 0;
}
public Node getHead() {
return head;
}
public Node getTail() {
return tail;
}
public DoubleLinkList(int data) {
head = new Node(data, null, null);
tail = head;
size++;
}
public int getLength() {
return size;
}
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null, null);
tail = head;
} else {
Node newNode = new Node(data, null, head);
head = newNode;
}
size++;
}
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null, null);
tail = head;
} else {
Node newNode = new Node(data, tail, null);
tail.next = newNode;
// 將尾指針指向最新的最後一個節點
tail = newNode;
}
size++;
}
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);
Node addNode = new Node(data, preNode, preNode.next);
preNode.next.pre = addNode;// 先設置後一個節點的前驅
preNode.next = addNode;// 再設置前一個節點的後繼
size++;
}
}
}
// 通過index查找指定的節點
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 數組爲空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 從第一個節點開始查找對比數據
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到對應的節點
}
// 刪除指定位置的節點
public void deleteByIndex(int index) {
if (isEmpty()) {
throw new RuntimeException("鏈表爲空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 刪除頭節點
{
Node current = head.next;
current.pre = null;
deleteNode = head;
head = current;
size--;
} else if (index == size - 1) {// 刪除尾節點
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
deleteNode.next.pre = prevNode;// 設置刪除節點的後繼節點的前驅等於 刪除節點的前驅
size--;
}
deleteNode = null;
}
}
// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (isEmpty()) {
throw new RuntimeException("鏈表爲空");
} else if (index == -2) {
System.out.println("未找到對應的元素");
} else {
deleteByIndex(index);
}
}
// 刪除 鏈表中最後一個元素
public void deleteLast() {
deleteByIndex(size - 1);
}
// 更新指定位置的值
public void updateByIndex(int data, int index) {
findNodeByIndex(index).data = data;
}
// 判斷鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
// 清除鏈表中所有的元素
public void clear() {
head = null;
tail = null;
size = 0;
}
// 鏈表的輸出 重寫toString方法
public String toString() {
if (isEmpty()) {
return "null";
} else {
StringBuilder sb = new StringBuilder("");
for (Node current = head; current != null; current = current.next)// 從head開始遍歷
{
sb.append(current.data + "-");
}
int len = sb.length();
return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
}
}
public static void main(String[] args) {
DoubleLinkList list = new DoubleLinkList();
list.addAtHead(1);
list.addAtHead(2);
list.addAtHead(3);
list.addAtTail(4);
list.addAtTail(5);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.addAtIndex(9, 0);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteByIndex(2);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteByData(4);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteLast();
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(55, 1);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(44, 0);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(66, 2);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
}
}
結語
好了,本篇就到此爲止了,雖然內容有點多,但是原理都比較簡單,主要是要自己動手,基本上手動實現一遍就差不多了,按照計劃,下一篇應該是棧的梳理,下一篇見!!!