底層存儲結構
與 數組 對比,數組需要一塊 連續的內存空間 來存儲,對內存要求很高。如果我們申請 50MB 的內存,即便內存的剩餘內存大於 50MB,但是如果內存不是連續的,也是很有可能申請失敗。
而 鏈表 與之相反,它並不需要一塊連續的內存,通過 指針 將一組 零散的內存塊 串聯起來使用。
下面的圖片可以看出兩者之間的區別:
鏈表分類
單鏈表
單鏈表每個節點有兩部分組成,數據 和 後繼指針 next 。
第一個節點稱爲 頭結點, 最後一個節點稱爲 尾節點 ,尾節點 next 指向 null。鏈表的插入增加和刪除,不需要移動,只需要改變前後節點 next 的指針即可實現。
但是,如果需要訪問第 N 個元素,則需要時間複雜度爲 。
循環鏈表
循環鏈表是從鏈尾直接連接到鏈頭。
雙向鏈表
相比於單鏈表,雙向鏈表具有兩個節點:前繼節點和後繼節點。
LRU 緩存淘汰算法
維護一個有序單鏈表,越靠近鏈尾的節點是越早訪問的,當有一個新的數據訪問時,我們從鏈表頭部開始順序遍歷鏈表。
- 如果該數據已經存在於鏈表中,那麼遍歷拿個這個數據的節點刪除,並將新的插入到頭部。
- 如果此數據沒有再緩存鏈表中:
- 如果緩存未滿,則直接將該節點插入鏈表的頭部
- 如果此時鏈表已滿,則鏈表尾節點刪除,將新的數據節點插入到鏈表的頭部。
鏈表小總結
說一句尷尬的話,本人大學的時候也是掛了 算法與數據結構 這門課,說實話,對算法也是有一點陰影,總感覺自己會學不好。就拿寫鏈表而言,經常被引用的移動產生誤解,在過程中經常不知道指針移到到哪了,導致並不會實現自己預期想要的結果。其實練習題做下來還是蠻有感觸的,還是要多練習,從中可以找到一些技巧,下面總結一下:
理解指針或引用的含義
對於我這個學習 Java 的而言,其實就是要理解引用的含義。
將某個變量賦值給引用,實際上是將這個變量的地址賦值給該引用,或者反過來說,引用中存儲了這個變量的內存地址,指向了這個變量,通過這個指針就能找到這個變量。
例子
Node node1 = new Node(null, 1);
node1 = new Node(null, 2);
Node node2 = new Node(null, 3);
node1 = node2;
node1.value = 3;
只要是 node =
引用後面直接跟上等於,那就證明只是單純的將這個引用指向另一個內存地址,並不會影響任何變量的值。而如果 node.
引用通過 .
的方式,那麼他的值就有變化的可能。
警惕引用丟失和內存泄漏
比如鏈表插入節點操作。
Node targetNode = new Node(null, 1); // 待被插入的節點
Node preNode; // 已經被找到插入前一個節點
錯誤示範:
preNode.next = targetNode;
targeNode.next = preNode.next.next;
這裏現將前節點指向目標節點,然後目標節點再指向前節點的下下個節點,意思是沒有錯誤,但是會導致後節點以及之後都會被丟失。
正確的做法應該是:
targetNode.next = preNode.next.next;
preNode.next = targeNode;
雖然僅僅是調換了順序,就會產生不同的結果。同樣刪除節點時,也要記得手動釋放內存。
利用哨兵簡化實現難度
針對鏈表的插入、刪除操作,需要對插入第一個結點和刪除最後一個結點的情況進行特殊處理。
重點留意邊界條件處理
- 如果鏈表爲空,代碼是否能正常工作
- 如果鏈表只包含一個節點,代碼是否能正常工作
- 如果鏈表只包含兩個節點,代碼是否能正常工作
- 代碼邏輯在處理頭結點和尾節點的時候,代碼是否能正常工作
舉例畫圖,輔助思考
我平常在做練習題時,身邊必備一張紙和一紙筆,對特殊的邏輯進行畫圖舉例演示,很大程度上可以幫助自己理清邏輯。
多寫多練
多多練習吧!
下面寫幾個練習題
代碼實現單鏈列表
class SinglyLinkedList {
private Node head = null;
void insertToHead(Node node) {
if (head == null) {
head = node;
} else {
node.next = head;
head = node;
}
System.out.println("insert head " + node.value + " data:\t" + toString());
}
void insertToHead(int value) {
insertToHead(createNode(value));
}
void insertAfter(Node node, Node newNode) {
if (node == null) return;
newNode.next = node.next;
node.next = newNode;
System.out.println("insertAfter data:\t" + toString());
}
void insertAfter(Node node, int value) {
insertAfter(node, createNode(value));
}
void insertBefore(Node before, Node newNode) {
if (before == null) return;
if (head == before) {
insertToHead(newNode);
return;
}
Node temp = head;
while (temp != null && temp.next != before) {
// 直到找打 before 的上個節點
temp = temp.next;
}
newNode.next = before;
temp.next = newNode;
System.out.println("insertBefore data:\t" + toString());
}
void insertBefore(Node before, int value) {
insertBefore(before, createNode(value));
}
Node findByValue(int value) {
Node temp = head;
while (temp != null && temp.value != value) {
temp = temp.next;
}
return temp;
}
Node findByIndex(int index) {
Node temp = head;
int pos = 0;
while (temp != null && pos != index) {
temp = temp.next;
pos++;
}
return temp;
}
void insertLast(Node node) {
if (head == null) {
head = node;
System.out.println("insert last " + node.value + " data:\t" + toString());
return;
}
Node temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
System.out.println("insert last " + node.value + " data:\t" + toString());
}
void insertLast(int value) {
insertLast(createNode(value));
}
private Node createNode(int value) {
return new Node(null, value);
}
void deleteByValue(int value) {
if (head == null) {
return;
}
Node before = null;
Node current = head;
while (current != null && current.value != value) {
before = current;
current = current.next;
}
if (current == null) {
return;
}
if (before == null) {
head = head.next;
} else {
before.next = current.next;
}
System.out.println("deleteByValue:\t" + toString());
}
void deleteByNode(Node node) {
if (head == null || node == null) {
return;
}
Node temp = head;
while (temp != null && temp.next != node) {
temp = temp.next;
}
temp.next = node.next;
System.out.println("deleteByNode:\t" + toString());
}
@Override
public String toString() {
Node temp = head;
StringBuilder sb = new StringBuilder();
sb.append("[");
while (temp != null) {
sb.append(temp.value).append(",");
temp = temp.next;
}
sb.append("]");
return sb.toString();
}
static class Node {
int value;
Node next;
Node(Node next, int value) {
this.next = next;
this.value = value;
}
int getData() {
return value;
}
}
public static void main(String[] args) {
SinglyLinkedList linkedList = new SinglyLinkedList();
linkedList.insertLast(1);
linkedList.insertLast(2);
linkedList.insertLast(3);
linkedList.insertLast(4);
linkedList.insertToHead(5);
linkedList.insertAfter(linkedList.findByValue(3), 6);
linkedList.insertBefore(linkedList.findByValue(1), 7);
linkedList.deleteByValue(2);
linkedList.deleteByNode(linkedList.findByValue(1));
}
}
輸出結果:
insert last 1 data: [1,]
insert last 2 data: [1,2,]
insert last 3 data: [1,2,3,]
insert last 4 data: [1,2,3,4,]
insert head 5 data: [5,1,2,3,4,]
insertAfter data: [5,1,2,3,6,4,]
insertBefore data: [5,7,1,2,3,6,4,]
deleteByValue: [5,7,1,3,6,4,]
deleteByNode: [5,7,3,6,4,]
判斷是否爲迴文數
核心思想:使用兩個指針,一個慢指針,每次移動一個節點,一個快指針,每次移動兩個節點。
private boolean isPalindrome() {
if (head == null || head.next == null) {
return false;
}
Node pre = null;
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
// 快指針每次跳兩個
fast = fast.next.next;
// 臨時指針指向下個節點
Node temp = slow.next;
// 將 slow 迴轉
slow.next = pre;
pre = slow;
slow = temp;
}
if (fast != null) {
// 這種代表着節點數爲偶數,slow 需要往後移動一位
slow = slow.next;
}
while (slow != null && pre != null) {
if (slow.value != pre.value) {
return false;
}
slow = slow.next;
pre = pre.next;
}
return true;
}
輸出結果
insert last 1 data: [1,]
insert last 2 data: [1,2,]
insert last 2 data: [1,2,2,]
insert last 1 data: [1,2,2,1,]
是否爲迴文函數: true
節點回轉
核心思想:每次遍歷時,迴轉節點的邏輯操作順序,且看代碼
private static Node reversed(Node node) {
if (node.next == null) {
return node;
}
Node pre = null;
while (node != null) {
// 現將下個節點用 temp 保存下來
Node temp = node.next;
node.next = pre;
pre = node;
node = temp;
}
return pre;
}
輸出結果:
01234
43210
判斷是否是循環節點
核心思想:循環節點的特點就在於鏈表的尾部指向頭部,只需要是用快慢節點進行遍歷,如果不是循環節點,循環一次就結束了。
/**
* 判斷是否是循環鏈表
* 快慢節點,如果兩者相等必然是循環鏈表,時間複雜度爲 O(n)
*/
private static boolean isCircle(Node node) {
int count = 0;
Node slow = node;
Node fast = node;
while (slow != null && fast != null && fast.next != null) {
count++;
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
System.out.println("循環了 " + count + "次");
return true;
}
}
System.out.println("循環了 " + count + "次");
return false;
}
輸出結果
循環了 5次
is circle: true
拼接兩個有序節點
核心思想:首先創建一個空的頭節點,然後兩個有序鏈表依次遍歷,誰小就把這個空節點指向誰(默認從小到大排序)。其中需要注意的是,如果一個節點提前結束了,那麼就需要將新節點直接指向剩下的那個節點。
private static Node mergeNode(Node aNode, Node bNode) {
Node head = new Node(null, -1);
Node tail = head;
if (aNode == null) {
head = bNode;
}
if (bNode == null) {
head = aNode;
}
while (aNode != null && bNode != null) {
if (aNode.value > bNode.value) {
tail.next = bNode;
bNode = bNode.next;
} else {
tail.next = aNode;
aNode = aNode.next;
}
tail = tail.next;
}
if (aNode == null) {
tail.next = bNode;
}
if (bNode == null) {
tail.next = aNode;
}
if (head == null) {
return null;
}
return head.next;
}
輸出結果:
A 節點: 135
B 節點: 24689
12345689
刪除倒數第 N 個節點
核心思想:快慢指針,快的比慢 的多 N 個,遍歷到最後,慢節點指向倒數第 N 個節點,然後直接進行刪除。
/**
* 刪除倒數 N 個節點
*
*/
private static void deleteLast(Node node, int n) {
int count = 0;
Node head = node;
while (head != null) {
count ++;
head = head.next;
}
if (n > count) {
return;
}
if (n == count) {
if (n == 1) {
node.next = null;
} else {
node.value = node.next.value;
node.next = node.next.next;
}
return;
}
Node slow = node;
Node fast = node;
int tempCount = 0;
while (slow != null && fast.next != null) {
if (tempCount < n) {
tempCount ++;
} else {
slow = slow.next;
}
fast = fast.next;
}
slow.next = slow.next.next;
}
輸出結果:
01234567
0124567
找出中間節點
核心思想:直接使用快慢指針進行遍歷,慢指針每次移動一個,快指針每次移動兩個。
private static int findMiddleNode(Node node) {
Node fast = node;
Node slow = node;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow.value;
}
輸出結果:
01234567
4
參考自極客時間