文章簡述
大家好,本篇是個人的第 3 篇文章。
承接第一篇文章《手寫單連結串列基礎之增,刪,查!附贈一道連結串列題》,在第一篇文章中提過,在刷演算法題之前先將基礎知識過一遍,這樣對後面的做演算法題是很有幫助的。
在本次的文章中,按照個人的刷題計劃,會分享關於連結串列的 3 道簡單級別的演算法題(可是依然感覺不簡單)

但是不要緊,從本篇文章開始分享的演算法題個人都會把關於這道題的全部程式碼寫出來,並用debug的形式
,分解每一步來整理出來。
通過還原題目場景,用 debug 除錯的方式去分析,印象更加深刻些。

本篇文章中共有 3 道題目。

一,合併兩個有序連結串列

1.1 題目分析
看到這道題的時候,第一反應就是先將兩個連結串列合併,然後再排序。嗯。。。不用想,絕對的暴力寫法。
或者是迴圈兩個連結串列,然後兩兩相比較,就像:
for(){
for(){
if(){}
}
}
好吧,其實這道題精華在於可以使用遞迴
,這個。。。來個草圖簡單描述下。
第一步:
兩個連結串列的首節點進行比較

兩個節點相等,則使 L2 連結串列【1】,和 L1 連結串列的【2】進行比較
注意:
L1節點【1】和L2節點【1】比較完成後,需要修改1.next指標,以指向它的下個節點。
第二步:
現在我們獲取到了 L2 連結串列【1】,那它的 next 指向誰?也就是 L2 連結串列【1】去和 L1 連結串列的【2】進行比較。

比較完成後,L2 連結串列【1】的 next 就指向了 L1 連結串列【2】,接著以此類推。

L2 連結串列【3】去和 L1 連結串列【4】比較。

最後 L1 連結串列【4】和 L2 連結串列【4】比較。

全部比較完成後,整個連結串列就已經排序完成了。

遞迴的方式就在於,兩兩節點進行比較,當有一個連結串列為null時,表示其中一個連結串列已經遍歷完成,那就需要終止遞迴,並將比較結果進行返回。
可能只是單純的畫圖並不好理解,下面用程式碼 debug 的方式去分析,還請耐心往下看。

1.2 程式碼分析
按照題意需要先建立 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 +
'}';
}
}
定義新增連結串列方法。
public ListNode add(ListNode node) {
// 1,定義輔助指標
ListNode temp = nodeHead;
// 2,首先判斷當前節點是否為最後節點
if (null == temp.next) {
// 當前節點為最後節點
temp.next = node;
return nodeHead;
}
// 3,迴圈遍歷節點,如果當前節點的下個節點不為空,表示還有後續節點
while (null != temp.next) {
// 否則將指標後移
temp = temp.next;
}
// 4,遍歷結束,將最後節點的指標指向新新增的節點
temp.next = node;
return nodeHead.next;
}
接著建立 2 個連結串列。
/**
* 定義L1連結串列
*/
ListNode l11 = new ListNode(1);
ListNode l12 = new ListNode(2);
ListNode l13 = new ListNode(4);
MergeLinkedList l1 = new MergeLinkedList();
l1.add(l11);
l1.add(l12);
/**
* 返回新增完的L1連結串列
*/
ListNode add1 = l1.add(l13);
/**
* 定義L2連結串列
*/
ListNode l21 = new ListNode(1);
ListNode l22 = new ListNode(3);
ListNode l23 = new ListNode(4);
MergeLinkedList l2 = new MergeLinkedList();
l2.add(l21);
l2.add(l22);
/**
* 返回L2連結串列
*/
ListNode add2 = l2.add(l23);
我們先把上述圖中使用遞迴的程式碼貼出來。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
/**
* 如果L1連結串列為null,終止遞迴
*/
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
/**
* 按照圖中的描述,兩兩比較連結串列的節點
*/
if (l1.val <= l2.val) {
/**
* L1的節點比L2的小,按照圖中就是需要比較L1連結串列的下個節點
* l1.next 就是指當比較出節點大小後,需要修改指標的指引,將整個連結串列全部串聯起來
*/
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
/**
* 同理,與上個if判斷一樣
*/
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
1.3 debug 除錯
1.3.1 L1 連結串列已經建立完成,同理 L2 也被建立完成。

1.3.2 比較兩個連結串列的首節點

注意:
l1.next是指向遞迴方法的,也就是上圖中我們描述的l1連結串列【1】指向了l2連結串列【1】,但是L2連結串列【1】又指向誰?開始進入遞迴
1.3.3 如上圖,開始比較 L2 連結串列【1】與 L1 連結串列的【2】

1.3.4 比較 L1 連結串列【2】與 L2 連結串列【3】

1.3.5 後面操作是一樣的,下面就直接展示最後兩個節點比較

這裡已經到最後兩個節點的比較。

這個時候 L1 連結串列先遍歷完成,需要終止遞迴,返回 L2 連結串列。為什麼返回 L2 連結串列?直接看圖。

因為最後一步比較的是 L1 連結串列【4】和 L2 連結串列【4】,也就是說 L2 連結串列【4】是最後的節點,如果返回 L1 連結串列,那 L2 連結串列【4】就會被丟棄,可參考上面圖解的最後一張。
重點來了!!!
重點來了!!!
重點來了!!!
L1 連結串列已經遍歷完成,開始觸發遞迴將比較的結果逐次返回。

這是不是我們最後 L1 連結串列【4】和 L2 連結串列【4】比較的那一步,是不是很明顯,l1.next 指向了 l1 的節點【4】,而 L1 節點也就是它最後的節點【4】,和我們那上面圖解中最後的結論一樣。

再接著執行下一步返回。

L2 連結串列的【3】指向了 L1 連結串列的【4】
同理,按照之前遞迴的結果以此返回就可以了,那我們來看下最終的排序結果。

二,刪除排序連結串列中的重複元素

2.1 題目分析
初次看這道題好像挺簡單的,這不就是個人之前寫的第一篇文章裡面,刪除連結串列節點嗎!
仔細審題其實這道題要更簡單些,因為題中已說明是一個排序連結串列,因此我們只需要將當前節點與下一個節點進行比較,如果相等則直接修改 next 指標即可。

2.1 程式碼分析
同樣是連結串列的定義,與上面第一題中的建立是一樣的,只不過我們是需要再重新建立一個單連結串列。
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(1);
ListNode l3 = new ListNode(3);
ListNode l4 = new ListNode(4);
ListNode l5 = new ListNode(4);
ListNode l6 = new ListNode(5);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
nodeFun.add(l4);
nodeFun.add(l5);
ListNode listNode = nodeFun.add(l6);
建立完成後,接著看去重複的程式碼。
public ListNode deleteDuplicates(ListNode head) {
/**
* 定義輔助指標
*/
ListNode temp = head;
/**
* 判斷當前節點和下一個節點不能為空,因為是需要將當前節點和下一個節點進行比較的
*/
while (temp != null && temp.next != null) {
/**
* 如果節點值相同
*/
if (temp.val == temp.next.val) {
/**
* 表示當前節點與下一個節點的值相同,則移動指標
*/
temp.next = temp.next.next;
} else {
/**
* 必須移動指標,否則會產生死迴圈
*/
temp = temp.next;
}
}
return head;
}
}
2.2 debug 除錯

2.2.1 按照初始化的連結串列,應該是首節點【1】和第二個節點【1】進行比較。

不用說兩個節點是相等的,那下一步進入 if 判斷,就是修改指標的指向。

此時第二個節點【1】已經沒有被 next 指引了,就會被 GC 回收掉。
2.2.2 下一步就是節點【1】和節點【3】進行比較
兩個節點不相等,進入 else 將輔助指標移動到下個節點。

那麼剩下的節點判斷也都是一樣的,我們最後看下列印的結果。

三,環形連結串列

3.1 題目分析
如果這個連結串列裡面有環,其中一個節點必然是被指標指引了一次或者多次(如果有多個環的話)。因此個人當時簡單的做法就是遍歷連結串列,把遍歷過的節點物件儲存到 HashSet 中,每遍歷下一個節點時去 HashSet 中比對,存在就表示有環。
而這道題沒有設定過多的要求,只要有環返回 boolean 就好。
還有一種巧妙的寫法,使用快慢指標的思想。
這種方式大致意思就是說,快慢指標比作龜兔賽跑,兔子跑的快,如果存在環那麼兔子就會比烏龜先跑進環中。那麼它們就會在某個節點上相遇,相遇了也就說明連結串列是有環的。

那麼,你們問題是不是來了?這不公平啊,【兔子】本來就比【烏龜】跑的快,那咋兔子還先跑了。
試想,如果它倆都在一個節點上跑,那它們從開始不就是相遇了,因為我們我們是設定如果在一個節點上相遇,表示連結串列是有環的。所以,這不是“不打自招“了!

比賽開始,這【兔子大哥】有點猛啊,一下跑兩個節點。


果然,有情人終成眷屬,它們相遇了。
3.2 程式碼分析
這次建立連結串列的時候,就不能單純是個單連結串列了,還得加個環。

ListNode l1 = new ListNode(3);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(0);
ListNode l4 = new ListNode(-4);
/**
* 給主角加個環
*/
l4.next = l2;
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
ListNode listNode = nodeFun.add(l4);
那就一起來找環吧。
public boolean hasCycle(ListNode head) {
ListNode temp = head;
if(null == head){
// 為空表示沒有環
return false;
}
// 1,set集合儲存遍歷過的節點,如果新的節點已經在set中,表示存在環
// 2,使用快慢指標的思想
// 定義慢指標
ListNode slow = head;
// 定義快指標
ListNode fast = head.next;
// 迴圈,只要2個指標不重合,就一直迴圈
while(slow != fast){
// 如果2個指標都到達尾節點,表示沒有環
if(fast == null || fast.next == null){
return false;
}
// 否則就移動指標
slow = slow.next;
fast = fast.next.next;
}
return true;
}
3.3 debug 除錯
所以,尷尬的事情來了,這玩意 debug 不了啊。如果存在環,那麼 while 迴圈是不會進來的。

那就直接看下結果吧。

如果把環去掉就是?

那還用猜?沒有光環了肯定。。。
四,總結
本次分享總結的題目都是簡單的題,因為也是按照個人制定的刷題計劃開始的,後面我會把個人的刷題計劃整理分享出來。
關於文章中完整的程式碼,關注公眾號回覆【獲取原始碼】,目前所有程式碼都是一個工程,也是方便後面的程式碼更新,可以自己親自除錯。
期待與大家共勉!
最後,求關注
原創不易,每一篇都是用心在寫。如果對您有幫助,就請一鍵三連(關注,點贊,再轉發)
我是楊小鑫,堅持寫作,分享更多有意義的文章。
感謝您的閱讀,期待與您相識!
