文章簡述
大家好,本篇是個人的第4篇文章。
承接第3篇文章《開啟演算法之路,還原題目,用debug除錯搞懂每一道題》,本篇文章繼續分享關於連結串列的演算法題目。
本篇文章共有5道題目
一,反轉連結串列(經典題目)
1.1.1 題目分析
反轉連結串列是經典的題目,題中資訊描述很清晰,給定一個單連結串列,將其反轉。
先說說有什麼思路呢?從題中給的案例輸出結果看,是不是隻需要將輸入的連結串列的指標改成相反方向,就可以得到要輸出的結果。
就好比如下圖所示:
但是問題來了,我們是單連結串列,是沒辦法將下個節點直接指向該節點的上個節點。
因此就需要定義一個輔助指標,用來指向該節點的上個節點,這樣就能完成,如下圖所示。
那按照我們上面分析也就是將cur指標指向pre節點就可以了。
注意:此處有坑
當我們將當前節點【cur】指向上一個節點【pre】的時候,如何將指標向下移動呢?
此時的節點【cur】已經指向了上一個節點【pre】了,所以我們還需要一個臨時變數去儲存當前節點的下個節點,具體為什麼這麼做,我們在下面程式碼演示的時候debug看下過程。
接著我們上面的步驟,將指標向下移動,如圖所示。
移動指標後,再將當前節點的next指標指向上一個節點。
最後當前節點沒有下個節點的時候,就結束遍歷,如圖所示。
1.1.2 程式碼分析
按照套路,先初始化節點物件。
class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
@Override
public String toString() {
return "ListNode{" +
"val=" + val +
'}';
}
}
建立單連結串列結構。
// 建立單連結串列
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(3);
ListNode l4 = new ListNode(4);
ListNode l5 = new ListNode(5);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
nodeFun.add(l4);
// 返回建立的連結串列
ListNode node = nodeFun.add(l5);
反轉連結串列的程式碼。
public ListNode reverseListIteration(ListNode head) {
// 定義上節點輔助指標
ListNode pre = null;
// 定義當前節點輔助指標
ListNode cur = head;
// 迴圈當前節點不為空
while (null != cur) {
// 臨時變數儲存當前節點的下個節點
ListNode temp = cur.next;
// 當前節點的next指向上節點
cur.next = pre;
// 上節點向下移動
pre = cur;
// 當前節點指向下個節點
cur = cur.next;
}
return pre;
}
1.1.3 debug除錯
節點初始化完成了,按照分析我們定義了2個節點,如上圖第一次遍歷【pre】節點是null,【cur】從第一個節點開始。
下一步debug除錯我們先不急,回顧之前說的一個問題,為什麼要將當前節點的下一個節點用臨時變數儲存,那我們直接看debug除錯。
第一次遍歷的時候,修改完指標後當前節點已經指向上一個節點了,再看上述題目分析的圖解。
這就是為啥要先把當前節點的下個節點快取起來。
上圖debug我們看出,【cur】當前節點的指標已經指向null,下一步就是移動指標指向下一個節點。
我們再接著進行debug除錯,按照上述分析,第二步迴圈就是將節點【2】指向上一個節點【1】,如下圖所示。
現在當前節點【cur】已經指向【2】,那它的下個節點就是【1】,如下圖所示。
經過上面的兩步迴圈,成功的將指標進行了反轉,剩下的節點迴圈也就如出一轍了。
當迴圈到最後節點【5】時,下個節點為null,此時結束while迴圈,而節點【5】也是指向了上一個節點【4】。
最後我們再看下執行結果。
二,迴文連結串列
1.2.1 題目分析
如果做過字串的演算法題,裡面有個迴文字串的題目。沒錯,它倆的意思是一樣的。
看題目描述得知一個連結串列是不是迴文連結串列,就是看連結串列就是看連結串列正讀和反讀是不是一樣的。
假如說,我們拿到了後半部分連結串列,再將其反轉。去和連結串列的前半部分比較,值相等就是迴文連結串列了。
注意:
這種方式會破壞原連結串列的結構,為保證題目的一致性,最後再將連結串列再重新拼接
另外一種解題方式為:將整個連結串列節點遍歷儲存到陣列中,而陣列是有下標,並可以直接獲取陣列的大小,那麼只需從陣列的首尾去判斷即可
反轉連結串列上一道題我們已經分享了,現在重點是如何獲取後半部分的連結串列。
我們再說說快慢指標的思想,通常我們定義2個指標,一個移動快,一個移動慢。詳細的案例可以參考本人上一篇文章《開啟演算法之路,還原題目,用debug除錯搞懂每一道題》,有一道關於快慢指標的題目。
定義慢指標每次移動1個節點,快指標每次移動2個節點,當然我們是需要保證快節點的下下個
個節點不為空。
slow = slow.next;
fast = fast.next.next;
其實快慢指標的思想就是,假設連結串列是一個迴文連結串列,快指標比慢指標是多走一步,當快指標走完的時候,慢指標也就剛好走到該連結串列的一半。
上圖中slow指標正好走到連結串列的一半,此時也就得到連結串列的後半部分了,即slow.next
。
1.2.2 程式碼分析
老套路,先建立一個迴文連結串列。
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(2);
ListNode l4 = new ListNode(1);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
ListNode head = nodeFun.add(l4);
獲取後半部分連結串列程式碼。
private ListNode endOfFirstHalf(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
反轉連結串列的程式碼與上題目是一樣的。
最後將兩個連結串列進行判斷是否是一樣的。
// 判斷是否迴文
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean flag = true;
while (flag && p2 != null) {
if (p1.val != p2.val) {
flag = false;
}
p1 = p1.next;
p2 = p2.next;
}
1.2.3 debug除錯
先獲取連結串列的後半部分。
debug開始迴圈後,fast直接走到連結串列的第3個節點【2】
slow.next就是連結串列的後半部分,再將後半部分進行連結串列反轉
最後我們也就得到如下2個連結串列。
最後將這2個連結串列進行比較是否相等,相等則是迴文連結串列。
三,連結串列的中間節點
1.3.1 題目分析
獲取連結串列的中間節點乍一看和迴文連結串列中使用快慢指標獲取後半連結串列有點類似呢?
沒錯,這波操作是類似的,但也並不是完全一樣,其主要思想還是快慢指標。
換句話說,如果你已理解了上面的題,那這道題也就不是什麼事了。話不多說,先來分析一波。
同樣我們還是定義slow慢指標每次移動一個節點,fast快指標每次移動2個節點。
那麼fast快指標移動到最後節點時,slow慢指標也就是要返回的連結串列。
我想,你是不是有個疑問。就是為什麼慢指標是移動一個節點,快節點移動2個節點?如果是偶數個節點,這個規則還正確嗎!那就驗證下。
為了方便,就繼續上面節點的遍歷。
題目中描述,如果有2箇中間節點,返回第二個節點
,所以返回節點【4,5,6】也就符合要求了
1.3.2 程式碼分析
建立連結串列結構。
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(3);
ListNode l4 = new ListNode(4);
ListNode l5 = new ListNode(5);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
nodeFun.add(l4);
ListNode head = nodeFun.add(l5);
獲取後半部分連結串列程式碼。
// 快慢指標
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
//移動指標
fast = fast.next.next;
slow = slow.next;
}
return slow;
1.3.3 debug除錯
快指標移動到節點【3】,慢指標移動到節點【2】
接著再走一步,快指標移動到節點【5】,慢節點移動到節點【3】,到此也就滿足題意的要求了。
四,連結串列中倒數第k個節點
1.4.1 題目分析
這道題要求就是返回倒數K個節點,最笨的辦法就是參考上面連結串列反轉,先將連結串列反轉。獲取前K個節點,將獲取的節點再次進行反轉即可得到題目要求。
但是顯然這種方式只能滿足答案輸出,經過上面的3道題目,有沒有得到什麼啟發呢?
是的,這道題依然可以使用雙指標解決,是不是感覺雙指標可以解決所有的連結串列問題了(QAQ)。
再仔細一想,是不是感覺和上一道《連結串列的中間節點》題目很類似?獲取連結串列的中間節點是返回後半部分節點,而本道題是要求返回指定K個節點。
那就直接說結論吧,同樣是定義快慢指標。只不過在上道題中快指標是每次移動2個節點,本道題中給定的K,就是快指標移動的節點個數。
同樣初始化指標都在首節點,如果我們先將fast指標移動K個節點。
到此才算初始化節點完成,剩下的操作就是遍歷剩下的連結串列,直到fast指標指向最後一個節點。
一直遍歷到fast節點為null,此時返回slow指標所指引的節點。
1.4.2 程式碼分析
初始化連結串列,由於和前幾道題的操作是一樣的,此處就不在展示。
獲取倒數第K個節點的程式碼。
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head;
ListNode fast = head;
// 先將快指標向前移動K
while (k-- > 0) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
1.4.3 debug除錯
按照上面圖解分析,fast快指標指向節點【3】的時候才算真正初始化快慢指標完成。
當快指標指向節點【5】時,slow慢節點指向節點【3】
注意:中間省略了一步,即慢指標指向節點【2】時,快指標指向節點【4】
節點【5】是最後一個節點,再次進入while迴圈。
最後一次迴圈時,慢指標指向了4,快指標下一個節點已經為null,此時結束迴圈。
五,移除重複節點
1.5.1 題目分析
這道題和上一篇中的題目【刪除排序連結串列中的重複元素】是一樣的,簡單的做法即利用Set集合儲存未重複的節點,再遍歷連結串列判斷是否已存在Set集合中。
因此本道題就不在多分析,直接貼上程式碼。
1.5.2 程式碼分析
Set<Integer> set = new HashSet<>();
ListNode temp = head;
while(temp != null && temp.next != null){
set.add(temp.val);
if(set.contains(temp.next.val)){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
}
六,總結
本次文章共分享總結5道題目,仔細分析有沒有發現這些題套路都是一樣的。都利用了雙指標的思想,通過一定的規則移動快慢指標獲取指定連結串列節點。
本次的5道題目和上次的3道題目,基本已經包含了連結串列簡單題目的所有型別。當你把本篇文章的題目看完後,關於連結串列的簡單題目你也已經做完了。
本人已經將連結串列的所有簡單題目刷完,總結出來的結論即套路都是一樣的。簡單來說,大部分的題目都可以利用雙指標,遞迴,陣列來完成。
在下篇文章中會對連結串列的簡單題目做一個小總結。
最後,求關注
原創不易,每一篇都是用心在寫。如果對您有幫助,就請一鍵三連(關注,點贊,再轉發)
我是楊小鑫,堅持寫作,分享更多有意義的文章。
感謝您的閱讀,期待與您相識!