文章簡述
你們好,本篇是我的的第4篇文章。java
承接第3篇文章《開啓算法之路,還原題目,用debug調試搞懂每一道題》,本篇文章繼續分享關於鏈表的算法題目。node
本篇文章共有5道題目算法
一,反轉鏈表(經典題目)
1.1.1 題目分析
反轉鏈表是經典的題目,題中信息描述很清晰,給定一個單鏈表,將其反轉。數組
先說說有什麼思路呢?從題中給的案例輸出結果看,是否是隻須要將輸入的鏈表的指針改爲相反方向,就能夠獲得要輸出的結果。緩存
就比如以下圖所示:ide
可是問題來了,咱們是單鏈表,是沒辦法將下個節點直接指向該節點的上個節點。this
所以就須要定義一個輔助指針,用來指向該節點的上個節點,這樣就能完成,以下圖所示。debug
那按照咱們上面分析也就是將cur指針指向pre節點就能夠了。3d
注意:此處有坑
指針
當咱們將當前節點【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道題目,基本已經包含了鏈表簡單題目的全部類型。當你把本篇文章的題目看完後,關於鏈表的簡單題目你也已經作完了。
本人已經將鏈表的全部簡單題目刷完,總結出來的結論即套路都是同樣的。簡單來講,大部分的題目均可以利用雙指針,遞歸,數組來完成。
在下篇文章中會對鏈表的簡單題目作一個小總結。
最後,求關注
原創不易,每一篇都是用心在寫。若是對您有幫助,就請一鍵三連(關注,點贊,再轉發)
我是楊小鑫,堅持寫做,分享更多有意義的文章。
感謝您的閱讀,期待與您相識!