目錄
鏈表的基本操作
- 改/遍歷:
while(?)
- 查: 返回倒數(shù)K個(gè)節(jié)點(diǎn)
- 增/刪除:反轉(zhuǎn)鏈表,刪除鏈表中的重復(fù)節(jié)點(diǎn)II
鏈表的一種重要題型 : 快慢指針
- 回環(huán)鏈表
- 環(huán)路檢測(cè)
- 將有序鏈表轉(zhuǎn)換成平衡BST
PART I : 鏈表的基本操作
改/遍歷
因?yàn)?strong>鏈表不是像數(shù)組一樣占用了連續(xù)地址的結(jié)構(gòu),所以很難通過(guò)索引(index
)的方式進(jìn)行存取,同時(shí)我們也沒(méi)有辦法不通過(guò)一次遍歷就知道鏈表的元素個(gè)數(shù)。它的訪問(wèn)和修改往往需要通過(guò)通過(guò)一個(gè)指針,并非C語(yǔ)言意義上的指針(pointer
),而更應(yīng)該被稱作游標(biāo)(cursor
),如果沒(méi)有說(shuō)明,下文還是通過(guò)指針這個(gè)詞代指鏈表中訪問(wèn)節(jié)點(diǎn)的指示器。
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
ListNode cur = head;
while (cur != null) {
cur = cur.next;
}
增
比如,向鏈表中結(jié)尾后面新增加一個(gè)節(jié)點(diǎn),節(jié)點(diǎn)的值為-1:
public void addEnd(ListNode head){
ListNode cur = head;
while(cur.next != null){
cur = cur.next;
}
cur .next = new ListNode(-1);
}
再比如,向鏈表(鏈表的元素是不重復(fù)的)中的元素4前面增加一個(gè)數(shù)值為6的節(jié)點(diǎn):
public void addBefore4(ListNode head){
ListNode cur = head;
while(cur.next.val != 4){
cur = cur.next;
}
ListNode temp = cur.next;
cur.next = new ListNode(6);
cur.next.next = temp;
}
上面幾個(gè)例子的難點(diǎn)都在于:while(?)
,while
語(yǔ)句中需要填什么內(nèi)容?
while(?)
什么時(shí)候使用cur.next
什么時(shí)候用cur
?這個(gè)地方涉及到對(duì)while()
語(yǔ)句的理解。我們可以形象地引入英語(yǔ)中的兩個(gè)概念,英語(yǔ)的將來(lái)時(shí)(cur.next
)和完成時(shí)(cur
)。
cur.next!=null
這個(gè)判斷條件表示當(dāng)跳出while
循環(huán)時(shí),cur.next == null
,即cur
將要將鏈表迭代完,而cur == null
,表示指針cur
已經(jīng)將鏈表迭代完成。
cur != null
同理:
至于while
的條件判斷填什么內(nèi)容,一般來(lái)說(shuō),如果是單純迭代鏈表的話,使用cur!=null
,如果想做插入,刪除等等操作,需要利用被插入/刪除的前一個(gè)節(jié)點(diǎn)的的話,則需要cur.next != null
。
下面看看兩個(gè)題目,加深對(duì)while(?)這個(gè)問(wèn)題的理解:
反轉(zhuǎn)鏈表
第一種思路:頭插法
頭插法的時(shí)候需要使用在做鏈表相關(guān)問(wèn)題中的一個(gè)非常重要的性質(zhì),dummy
節(jié)點(diǎn)(空頭結(jié)點(diǎn)),它的重要性,后面都會(huì)有體現(xiàn):
public ListNode reverseList(ListNode head) {
ListNode dummy = new ListNode(0);
ListNode cur = head;
while(cur!= null){
ListNode node = new ListNode(cur.val);
node.next = dummy.next;
dummy.next = node;
cur = cur.next;
}
return dummy.next;
}
這種方法在實(shí)現(xiàn)過(guò)程中,其實(shí)吧原來(lái)的鏈表head
,重新復(fù)制了它的值,創(chuàng)建了新的鏈表,內(nèi)存開(kāi)銷很大。
第二種思路:交換元素法
具體代碼如下:
public ListNode reverseList(ListNode head){
ListNode cur = head;
ListNode pre= null;
while(cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
返回倒數(shù)K節(jié)點(diǎn)
思路很簡(jiǎn)單,通過(guò)兩個(gè)指針就可以確定一個(gè)“標(biāo)尺”,通過(guò)這個(gè)標(biāo)尺往下走,直到標(biāo)尺的底端碰到鏈表的最后一個(gè)節(jié)點(diǎn)。很明顯,當(dāng)K=2時(shí),因?yàn)殒湵淼淖詈笠粋€(gè)節(jié)點(diǎn)是5(本題中),倒數(shù)第二個(gè)節(jié)點(diǎn)應(yīng)該是4,應(yīng)當(dāng)使用“將來(lái)時(shí)”,即cur.next != null
。
返回倒數(shù)K節(jié)點(diǎn)
代碼如下:
class Solution {
public int kthToLast(ListNode head, int k) {
ListNode cur = head;
while( -- k > 0)
cur = cur.next;
ListNode p = head;
while(cur.next != null){
cur = cur.next;
p = p.next;
}
return p.val;
}
}
刪除排序鏈表中的重復(fù)節(jié)點(diǎn)II
此外,大家可能對(duì)dummy
節(jié)點(diǎn)的使用理解不夠深入,這里再給出一道題:
刪除排序鏈表中的重復(fù)節(jié)點(diǎn)II
排序鏈表的重復(fù)節(jié)點(diǎn)一定是相鄰的,根據(jù)題意,一旦有重復(fù)的節(jié)點(diǎn)就會(huì)全部刪除。所以我們很容易想到,通過(guò)一個(gè)指針cur
記錄當(dāng)前節(jié)點(diǎn)的位置,cur.next
和 cur.next.next
比較后面有沒(méi)有節(jié)點(diǎn)重復(fù),如果一旦有重復(fù),就通過(guò)temp
節(jié)點(diǎn)搜索到?jīng)]有重復(fù)的節(jié)點(diǎn),直接連接到cur
的下一個(gè):
這里用的是指針的完成時(shí),所以可以看到當(dāng)指針cur.next
指向temp
的時(shí)候并沒(méi)有消除2和3之間的指針。不給過(guò)這個(gè)題這樣寫也是ac的,如果要改掉這里的話可以使用指針的將來(lái)時(shí)。
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(Integer.MAX_VALUE);
dummy.next = head;
ListNode cur = dummy;
while(cur.next != null && cur.next.next!= null){
if(cur.next.val == cur.next.next.val){
ListNode temp = cur.next;
while(temp != null && temp.val == cur.next.val){
temp = temp.next;
}
cur.next = temp;
}else
cur = cur.next;
}
return dummy.next;
}
PART II: 快慢指針
直接通過(guò)一道題目來(lái)解釋快慢指針的概念:
環(huán)路檢測(cè)
如果在鏈表中存在環(huán),則返回環(huán)的入口,在這個(gè)題目中就是第三個(gè)節(jié)點(diǎn)。這道題可以分解成兩個(gè)子問(wèn)題:判斷鏈表是否存在環(huán)路,如果存在環(huán)路,返回環(huán)路的入口。
判斷是否存在環(huán)路
閃電俠和你在一個(gè)環(huán)形的賽道上跑步,你剛邁出腿,閃電俠就和你相遇了。
如果鏈表中存在環(huán)的話,存在這一快一慢兩個(gè)指針,這兩個(gè)指針在若干次迭代后必然相遇。
直接看看代碼:
class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast){
if(fast == null || fast.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
但是一般情況下,快慢指針使用的代碼模板是這樣的:
//完成時(shí) 后中
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//將來(lái)時(shí) 前中
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
}
我們下面來(lái)分析這段代碼.
完成時(shí)+當(dāng)鏈表節(jié)點(diǎn)為偶數(shù)時(shí):
很明顯,當(dāng)?shù)鷦倓偨Y(jié)束時(shí),slow
恰好在偶數(shù)鏈表的中間的后半部分,所以這樣的情況我稱之為“后中”。
完成時(shí)+當(dāng)鏈表節(jié)點(diǎn)為奇數(shù)時(shí):
slow
節(jié)點(diǎn)指向的是鏈表的正中間節(jié)點(diǎn)
將來(lái)時(shí)+當(dāng)鏈表節(jié)點(diǎn)為偶數(shù)時(shí):
與上面的相對(duì)應(yīng)的,這中情況被稱為前中。
將來(lái)時(shí)+當(dāng)鏈表節(jié)點(diǎn)為奇數(shù)時(shí):
和上面完全相同。所以我們可以知道,指針的將來(lái)時(shí)和完成時(shí)分別影響的是slow
指針的位置,分成了“前中”和“后中”兩種情況。具體的我們?cè)賮?lái)分析。
如何找出環(huán)路的入口
大致上知道了快慢指針的找環(huán)是否存在的流程之后,我們看看如何找到入口。
設(shè)慢指針slow
在和快指針fast
第一次相遇走過(guò)的節(jié)點(diǎn)的距離為ls
,快指針走過(guò)的距離為lf
,其中m
為鏈表的第一個(gè)節(jié)點(diǎn)到環(huán)形入口的距離,環(huán)的距離為r
,環(huán)的入口到兩個(gè)指針相遇的距離為d
,如圖,則有以下的關(guān)系:
ls = m + d;
lf = m + d + n*r
快指針可能在整個(gè)環(huán)內(nèi)走了n
圈,但是因?yàn)榭炻羔樛瑫r(shí)出發(fā),兩個(gè)指針也滿足如下關(guān)系:
ls*2 = lf
m + d = n * r
m = (n-1)*r + r - d
當(dāng)快慢指針相遇時(shí),如果把慢指針?lè)呕仄瘘c(diǎn),快指針在相遇點(diǎn)繼續(xù)走,當(dāng)慢指針走完m,快指針就回走完r-d,再次相遇時(shí)就是鏈表的入口,代碼如下:
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 == fast) break;
}
if(fast == null || fast.next == null) return null;
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
這里采用了fast
的完成時(shí),如果沒(méi)有回環(huán)的話直接遍歷完畢整個(gè)鏈表,退出while
。
此外快慢指針的應(yīng)用除了檢測(cè)環(huán)路之外,還可以利用上述的“前中”或者“后中”,找到鏈表的中間節(jié)點(diǎn)。
將有序鏈表變成平衡二叉搜索樹(shù)
要想變成平衡二叉搜索樹(shù)肯定要找中間的節(jié)點(diǎn)。使用后中法或者前中法都行,這里使用的是的后中法:
將有序鏈表變成平衡二叉搜索樹(shù)
class Solution {
public ListNode findMiddleElement(ListNode node){
ListNode prev = null;
ListNode slow = node;
ListNode fast = node;
while(fast != null && fast.next != null){
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
if(prev != null) prev.next = null;
return slow;
}
public TreeNode sortedListToBST(ListNode head) {
if(head == null) return null;
ListNode mid = findMiddleElement(head);
if(mid == head) return new TreeNode(mid.val);
TreeNode midTreeNode = new TreeNode(mid.val);
midTreeNode.left = sortedListToBST(head);
midTreeNode.right = sortedListToBST(mid.next);
return midTreeNode;
}
}