一 簡(jiǎn)介
- 單鏈表中的每個(gè)結(jié)點(diǎn)不僅包含值押框,還包含鏈接到下一個(gè)結(jié)點(diǎn)的引用字段。
image
1.1 結(jié)點(diǎn)結(jié)構(gòu)
// Definition for singly-linked list.
public class SinglyListNode {
int val;
SinglyListNode next;
SinglyListNode(int x) { val = x; }
}
1.2 操作
- 與數(shù)組不同哈肖,我們無(wú)法在常量時(shí)間內(nèi)訪問(wèn)單鏈表中的隨機(jī)元素凝赛。
- 如果我們想要獲得第 i 個(gè)元素,我們必須從頭結(jié)點(diǎn)逐個(gè)遍歷季眷。 我們按索引來(lái)訪問(wèn)元素平均要花費(fèi) O(N) 時(shí)間,其中 N 是鏈表的長(zhǎng)度卷胯。
1.3 添加操作 - 單鏈表
如果我們想在給定的結(jié)點(diǎn) prev 之后添加新值子刮,我們應(yīng)該:
- 使用給定值初始化新結(jié)點(diǎn) cur;
- 將 cur 的“next”字段鏈接到 prev 的下一個(gè)結(jié)點(diǎn) next窑睁;
- 將 prev 中的“next”字段鏈接到 cur 挺峡。
與數(shù)組不同,我們不需要將所有元素移動(dòng)到插入元素之后,因此担钮,您可以在 O(1) 時(shí)間復(fù)雜度中將新結(jié)點(diǎn)插入到鏈表中橱赠,這非常高效。
1.4 刪除操作 - 單鏈表
如果我們想從單鏈表中刪除現(xiàn)有結(jié)點(diǎn) cur箫津,可以分兩步完成:
- 找到 cur 的上一個(gè)結(jié)點(diǎn) prev 及其下一個(gè)結(jié)點(diǎn) next狭姨;
- 接下來(lái)鏈接 prev 到 cur 的下一個(gè)節(jié)點(diǎn) next。
- 第一步中苏遥,我們需要找出 prev 和 next饼拍。使用 cur 的參考字段很容易找出 next,但是田炭,我們必須從頭結(jié)點(diǎn)遍歷鏈表师抄,以找出 prev,它的平均時(shí)間是 O(N)教硫,其中 N 是鏈表的長(zhǎng)度叨吮。因此,刪除結(jié)點(diǎn)的時(shí)間復(fù)雜度將是 O(N)瞬矩。
- 空間復(fù)雜度為 O(1)挤安,因?yàn)槲覀冎恍枰A靠臻g來(lái)存儲(chǔ)指針。
1.5 設(shè)計(jì)鏈表
設(shè)計(jì)鏈表的實(shí)現(xiàn)丧鸯。您可以選擇使用單鏈表或雙鏈表蛤铜。
- 單鏈表中的節(jié)點(diǎn)應(yīng)該具有兩個(gè)屬性:val 和 next。
- val 是當(dāng)前節(jié)點(diǎn)的值
- next 是指向下一個(gè)節(jié)點(diǎn)的指針/引用
如果要使用雙向鏈表丛肢,則還需要一個(gè)屬性 prev 以指示鏈表中的上一個(gè)節(jié)點(diǎn)围肥。假設(shè)鏈表中的所有節(jié)點(diǎn)都是 0-index 的。
在鏈表類(lèi)中實(shí)現(xiàn)這些功能:
- get(index):獲取鏈表中第 index 個(gè)節(jié)點(diǎn)的值蜂怎。如果索引無(wú)效穆刻,則返回-1。
- addAtHead(val):在鏈表的第一個(gè)元素之前添加一個(gè)值為 val 的節(jié)點(diǎn)杠步。插入后氢伟,新節(jié)點(diǎn)將成為鏈表的第一個(gè)節(jié)點(diǎn)榜轿。
- addAtTail(val):將值為 val 的節(jié)點(diǎn)追加到鏈表的最后一個(gè)元素。
- addAtIndex(index,val):在鏈表中的第 index 個(gè)節(jié)點(diǎn)之前添加值為 val 的節(jié)點(diǎn)朵锣。如果 index 等于鏈表的長(zhǎng)度谬盐,則該節(jié)點(diǎn)將附加到鏈表的末尾。如果 index 大于鏈表長(zhǎng)度诚些,則不會(huì)插入節(jié)點(diǎn)飞傀。
- deleteAtIndex(index):如果索引 index 有效,則刪除鏈表中的第 index 個(gè)節(jié)點(diǎn)诬烹。
二 雙指針技巧
2.1 給定一個(gè)鏈表砸烦,判斷鏈表中是否有環(huán)。
可能已經(jīng)使用哈希表提出了解決方案
- 想象一下绞吁,有兩個(gè)速度不同的跑步者幢痘。如果他們?cè)谥甭飞闲旭偅炫苷邔⑹紫鹊竭_(dá)目的地家破。但是雪隧,如果它們?cè)趫A形跑道上跑步,那么快跑者如果繼續(xù)跑步就會(huì)追上慢跑者员舵。
- 這正是我們?cè)阪湵碇惺褂脙蓚€(gè)速度不同的指針時(shí)會(huì)遇到的情況:
- 如果沒(méi)有環(huán)脑沿,快指針將停在鏈表的末尾。
- 如果有環(huán)马僻,快指針最終將與慢指針相遇庄拇。
所以剩下的問(wèn)題是:這兩個(gè)指針的適當(dāng)速度應(yīng)該是多少?
- 一個(gè)安全的選擇是每次移動(dòng)慢指針一步韭邓,而移動(dòng)快指針兩步措近。每一次迭代,快速指針將額外移動(dòng)一步女淑。如果環(huán)的長(zhǎng)度為 M瞭郑,經(jīng)過(guò) M 次迭代后,快指針肯定會(huì)多繞環(huán)一周鸭你,并趕上慢指針屈张。
親參考2.2圖
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow.equals(fast)) {
return true;
}
}
return false;
}
}
2.2 返回鏈表開(kāi)始入環(huán)的第一個(gè)節(jié)點(diǎn)
給定一個(gè)鏈表,返回鏈表開(kāi)始入環(huán)的第一個(gè)節(jié)點(diǎn)袱巨。 如果鏈表無(wú)環(huán)阁谆,則返回 null。
解題愉老,有了2.1的基礎(chǔ)场绿,來(lái)計(jì)算一下。
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow.equals(fast)) {
break;
}
}
while (head != null && slow != null) {
if (head.equals(slow)) {
return slow;
}
head = head.next;
slow = slow.next;
}
return null;
}
}
2.3 相交鏈表
(判斷是否相交嫉入,可以遍歷兩個(gè)鏈表焰盗,看最后一個(gè)節(jié)點(diǎn)是否相等)
編寫(xiě)一個(gè)程序璧尸,找到兩個(gè)單鏈表相交的起始節(jié)點(diǎn)。
例如熬拒,下面的兩個(gè)鏈表:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
在節(jié)點(diǎn) c1 開(kāi)始相交爷光。
注意:
- 如果兩個(gè)鏈表沒(méi)有交點(diǎn),返回 null.
- 在返回結(jié)果后梦湘,兩個(gè)鏈表仍須保持原有的結(jié)構(gòu)。
- 可假定整個(gè)鏈表結(jié)構(gòu)中沒(méi)有循環(huán)件甥。
- 程序盡量滿足 O(n) 時(shí)間復(fù)雜度捌议,且僅用 O(1) 內(nèi)存。
/**
* 返回相交單向鏈表的交點(diǎn)
*/
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
//記錄鏈表的長(zhǎng)度
int lenA = 1;
ListNode tempA = headA;
while (tempA.next != null) {
lenA++;
tempA = tempA.next;
}
int lenB = 1;
ListNode tempB = headB;
while (tempB.next != null) {
lenB++;
tempB = tempB.next;
}
//判斷鏈表是否相交引有,不想交直接返回null
if (!tempA.equals(tempB)) {
return null;
}
//截取后半段瓣颅,相同長(zhǎng)度的鏈表
int reduseCount = lenA - lenB;
tempA = headA;
tempB = headB;
if (reduseCount > 0) {
for (int i = 0; i < reduseCount; i++) {
tempA = tempA.next;
}
} else {
reduseCount = Math.abs(reduseCount);
for (int i = 0; i < reduseCount; i++) {
tempB = tempB.next;
}
}
//循環(huán)遍歷找到交點(diǎn)
while (tempB != null && tempA != null) {
if (tempB.equals(tempA)) {
return tempB;
}
tempA = tempA.next;
tempB = tempB.next;
}
return null;
}
2.4 刪除鏈表的倒數(shù)第N個(gè)節(jié)點(diǎn)
給定一個(gè)鏈表,刪除鏈表的倒數(shù)第 n 個(gè)節(jié)點(diǎn)譬正,并且返回鏈表的頭結(jié)點(diǎn)宫补。
示例:
給定一個(gè)鏈表: 1->2->3->4->5, 和 n = 2.
當(dāng)刪除了倒數(shù)第二個(gè)節(jié)點(diǎn)后,鏈表變?yōu)?1->2->3->5.
- 說(shuō)明:
給定的 n 保證是有效的曾我。 - 進(jìn)階:
你能?chē)L試使用一趟掃描實(shí)現(xiàn)嗎粉怕? - 解題思路
- 典型的利用雙指針?lè)ń忸}。首先讓指針first指向頭節(jié)點(diǎn)抒巢,然后讓其向后移動(dòng)n步贫贝,接著讓指針sec指向頭結(jié)點(diǎn),并和first一起向后移動(dòng)蛉谜。當(dāng)first的next指針為NULL時(shí)稚晚,sec即指向了要?jiǎng)h除節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn),接著讓first指向的next指針指向要?jiǎng)h除節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)即可型诚。
- 注意如果要?jiǎng)h除的節(jié)點(diǎn)是首節(jié)點(diǎn)客燕,那么first向后移動(dòng)結(jié)束時(shí)會(huì)為NULL,這樣加一個(gè)判斷其是否為NULL的條件狰贯,若為NULL則返回頭結(jié)點(diǎn)的next指針也搓。
/**
* 刪除鏈表的倒數(shù)第 n 個(gè)節(jié)點(diǎn)
*/
public static ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) {
return null;
}
if (n == 0) {
return null;
}
ListNode first = head;
ListNode sec = head;
for (int i = 0; i < n; i++) {
first = first.next;
}
while (first != null && first.next != null) {
first = first.next;
sec = sec.next;
}
if (sec.next == null) {
return null;
}
sec.next = sec.next.next;
return head;
}
2.5小結(jié)
2.5.1 提示
-
- 在調(diào)用 next 字段之前,始終檢查節(jié)點(diǎn)是否為空涵紊。
- 獲取空節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)將導(dǎo)致空指針錯(cuò)誤还绘。例如,在我們運(yùn)行 fast = fast.next.next 之前栖袋,需要檢查 fast 和 fast.next 不為空拍顷。
- 仔細(xì)定義循環(huán)的結(jié)束條件。
2.5.2 復(fù)雜度分析
- 空間復(fù)雜度分析容易塘幅。如果只使用指針昔案,而不使用任何其他額外的空間尿贫,那么空間復(fù)雜度將是 O(1)。
三 經(jīng)典問(wèn)題
3.1 反轉(zhuǎn)鏈表
- 一種解決方案是按原始順序迭代結(jié)點(diǎn)踏揣,并將它們逐個(gè)移動(dòng)到列表的頭部庆亡。
單鏈表--反轉(zhuǎn)
3.2 刪除鏈表中的節(jié)點(diǎn)
刪除鏈表中等于給定值 val 的所有節(jié)點(diǎn)。
示例:
輸入: 1->2->6->3->4->5->6, val = 6
輸出: 1->2->3->4->5
代碼
/**
* 刪除鏈表中的節(jié)點(diǎn)
*/
public static ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
//定義前指針 是為了刪除節(jié)點(diǎn)
ListNode pre = null;
//定義next是為了指針后移
ListNode next;
for (ListNode i = head; i != null; i = next) {
next = i.next;
if (i.val == val) {
//這個(gè)判斷說(shuō)明頭一個(gè)節(jié)點(diǎn)捞稿,就需要?jiǎng)h除又谋,因此頭指針后移
if (head.equals(i)) {
head = head.next;
}
//前節(jié)點(diǎn)next指向后節(jié)點(diǎn)
if (pre != null) {
pre.next = i.next;
}
i.next = null;
} else {
pre = i;
}
}
return head;
}
3.3 奇偶鏈表
- 給定一個(gè)單鏈表,把所有的奇數(shù)節(jié)點(diǎn)和偶數(shù)節(jié)點(diǎn)分別排在一起娱局。請(qǐng)注意彰亥,這里的奇數(shù)節(jié)點(diǎn)和偶數(shù)節(jié)點(diǎn)指的是節(jié)點(diǎn)編號(hào)的奇偶性,而不是節(jié)點(diǎn)的值的奇偶性衰齐。
- 請(qǐng)嘗試使用原地算法完成任斋。你的算法的空間復(fù)雜度應(yīng)為 O(1),時(shí)間復(fù)雜度應(yīng)為 O(nodes)耻涛,nodes 為節(jié)點(diǎn)總數(shù)废酷。
示例 1:
輸入: 1->2->3->4->5->NULL
輸出: 1->3->5->2->4->NULL
示例 2:
輸入: 2->1->3->5->6->4->7->NULL
輸出: 2->3->6->7->1->5->4->NULL
說(shuō)明:
- 應(yīng)當(dāng)保持奇數(shù)節(jié)點(diǎn)和偶數(shù)節(jié)點(diǎn)的相對(duì)順序。
- 鏈表的第一個(gè)節(jié)點(diǎn)視為奇數(shù)節(jié)點(diǎn)抹缕,第二個(gè)節(jié)點(diǎn)視為偶數(shù)節(jié)點(diǎn)澈蟆,以此類(lèi)推。
代碼
public static ListNode oddEvenList(ListNode head) {
if (head == null) {
return null;
}
ListNode odd = head;
ListNode even = head.next;
ListNode evenHead = head.next;
while (odd.next != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
3.4 回文鏈表
請(qǐng)判斷一個(gè)鏈表是否為回文鏈表卓研。
示例 1:
輸入: 1->2
輸出: false
示例 2:
輸入: 1->2->2->1
輸出: true
進(jìn)階:
你能否用 O(n) 時(shí)間復(fù)雜度和 O(1) 空間復(fù)雜度解決此題丰介?
/**
* 斷一個(gè)鏈表是否為回文鏈表
* 輸入: 1->2->2->1
* 輸出: true
*/
public static boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode reverseNode = null;//指向反轉(zhuǎn)的鏈表
ListNode nomalNode;//指向后面后半截鏈表
if (head.next.next == null) {
reverseNode = head;
nomalNode = head.next;
reverseNode.next = null;
} else {
//快慢指針找中間值
//順便反轉(zhuǎn)前半截鏈表
ListNode slow = head;
ListNode fast = head;
ListNode tempSlow;
ListNode tempFast;
while (fast.next != null && fast.next.next != null) {
tempSlow = slow.next;
tempFast = fast.next.next;
slow.next = reverseNode;
reverseNode = slow;
slow = tempSlow;
fast = tempFast;
}
tempSlow = slow.next;
slow.next = reverseNode;
reverseNode = slow;
//考慮鏈表是奇數(shù)長(zhǎng)度鏈表
if (fast.next == null) {
reverseNode = reverseNode.next;
}
nomalNode = tempSlow;
}
//遍歷后半截找不同
while (nomalNode != null && reverseNode != null) {
if (nomalNode.val != reverseNode.val) {
return false;
}
nomalNode = nomalNode.next;
reverseNode = reverseNode.next;
}
return true;
3.5 小結(jié)
快慢指針:可以找環(huán),可以找中間點(diǎn)鉴分,可以相差n個(gè)節(jié)點(diǎn)的鏈表
四 雙鏈表
4.1 簡(jiǎn)介 - 雙鏈表
4.1.1 定義
雙鏈表以類(lèi)似的方式工作哮幢,但還有一個(gè)引用字段,稱(chēng)為“prev”字段志珍。有了這個(gè)額外的字段橙垢,您就能夠知道當(dāng)前結(jié)點(diǎn)的前一個(gè)結(jié)點(diǎn)。
4.1.2 結(jié)點(diǎn)結(jié)構(gòu)
// Definition for doubly-linked list.
class DoublyListNode {
int val;
DoublyListNode next, prev;
DoublyListNode(int x) {val = x;}
}
與單鏈接列表類(lèi)似伦糯,我們將使用頭結(jié)點(diǎn)來(lái)表示整個(gè)列表柜某。
4.1.3 操作
與單鏈表類(lèi)似,我們將介紹在雙鏈表中如何訪問(wèn)數(shù)據(jù)敛纲、插入新結(jié)點(diǎn)或刪除現(xiàn)有結(jié)點(diǎn)喂击。
- 我們不能在常量級(jí)的時(shí)間內(nèi)訪問(wèn)隨機(jī)位置。
- 我們必須從頭部遍歷才能得到我們想要的第一個(gè)結(jié)點(diǎn)淤翔。
- 在最壞的情況下翰绊,時(shí)間復(fù)雜度將是 O(N),其中 N 是鏈表的長(zhǎng)度。
4.2 添加操作 - 雙鏈表
如果我們想在現(xiàn)有的結(jié)點(diǎn) prev 之后插入一個(gè)新的結(jié)點(diǎn) cur监嗜,我們可以將此過(guò)程分為兩個(gè)步驟:
-
鏈接 cur 與 prev 和 next谐檀,其中 next 是 prev 原始的下一個(gè)節(jié)點(diǎn);
image -
用 cur 重新鏈接 prev 和 next裁奇。
image
4.3 刪除操作 - 雙鏈表
如果我們想從雙鏈表中刪除一個(gè)現(xiàn)有的結(jié)點(diǎn) cur桐猬,我們可以簡(jiǎn)單地將它的前一個(gè)結(jié)點(diǎn) prev 與下一個(gè)結(jié)點(diǎn) next 鏈接起來(lái)。
與單鏈表不同刽肠,使用“prev”字段可以很容易地在常量時(shí)間內(nèi)獲得前一個(gè)結(jié)點(diǎn)溃肪。
因?yàn)槲覀儾辉傩枰闅v鏈表來(lái)獲取前一個(gè)結(jié)點(diǎn),所以時(shí)間和空間復(fù)雜度都是O(1)音五。
五惫撰、小結(jié) - 鏈表
5.1 小結(jié)
5.1.1 回顧
讓我們簡(jiǎn)要回顧一下單鏈表和雙鏈表的表現(xiàn)。
它們?cè)谠S多操作中是相似的放仗。
- 它們都無(wú)法在常量時(shí)間內(nèi)隨機(jī)訪問(wèn)數(shù)據(jù)
- 它們都能夠在 O(1) 時(shí)間內(nèi)在給定結(jié)點(diǎn)之后或列表開(kāi)頭添加一個(gè)新結(jié)點(diǎn)润绎。
- 它們都能夠在 O(1) 時(shí)間內(nèi)刪除第一個(gè)結(jié)點(diǎn)
但是刪除給定結(jié)點(diǎn)(包括最后一個(gè)結(jié)點(diǎn))時(shí)略有不同撬碟。
- 在單鏈表中诞挨,它無(wú)法獲取給定結(jié)點(diǎn)的前一個(gè)結(jié)點(diǎn),因此在刪除給定結(jié)點(diǎn)之前我們必須花費(fèi) O(N) 時(shí)間來(lái)找出前一結(jié)點(diǎn)呢蛤。
- 在雙鏈表中惶傻,這會(huì)更容易,因?yàn)槲覀兛梢允褂谩皃rev”引用字段獲取前一個(gè)結(jié)點(diǎn)其障。因此我們可以在 O(1) 時(shí)間內(nèi)刪除給定結(jié)點(diǎn)银室。
5.1.2 對(duì)照
這里我們提供鏈表和其他數(shù)據(jù)結(jié)構(gòu)(包括數(shù)組,隊(duì)列和棧)之間時(shí)間復(fù)雜度
的比較:
經(jīng)過(guò)這次比較励翼,我們不難得出結(jié)論:
- 如果你需要經(jīng)常添加或刪除結(jié)點(diǎn)蜈敢,鏈表可能是一個(gè)不錯(cuò)的選擇。
- 如果你需要經(jīng)常按索引訪問(wèn)元素汽抚,數(shù)組可能是比鏈表更好的選擇抓狭。
5.2 合并兩個(gè)有序鏈表
將兩個(gè)有序鏈表合并為一個(gè)新的有序鏈表并返回。新鏈表是通過(guò)拼接給定的兩個(gè)鏈表的所有節(jié)點(diǎn)組成的造烁。
示例:
輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4
/**
* 合并兩個(gè)有序鏈表
*/
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
ListNode temp1 = l1;
ListNode temp2 = l2;
ListNode mergeListNode;
if (l1.val > l2.val) {
mergeListNode = l2;
temp2 = l2.next;
} else {
mergeListNode = l1;
temp1 = l1.next;
}
ListNode mergeListNodePointer = mergeListNode;
//每次循環(huán)只前進(jìn)一個(gè)指針
while (temp1 != null && temp2 != null) {
if (temp1.val > temp2.val) {
mergeListNodePointer.next = temp2;
mergeListNodePointer=mergeListNodePointer.next;
temp2 = temp2.next;
} else {
mergeListNodePointer.next = temp1;
mergeListNodePointer=mergeListNodePointer.next;
temp1 = temp1.next;
}
}
//將剩余的節(jié)點(diǎn)拼接起來(lái)
if (temp1 != null) {
mergeListNodePointer.next = temp1;
}
if (temp2 != null) {
mergeListNodePointer.next = temp2;
}
return mergeListNode;
}