最近同學介紹了一個lettcode(力扣)OJ給我,個人認爲這個網站比母校的oj,杭電oj界面友好很多,題庫充足,且支持多種主流語言,很適合閒時刷刷提高算法能力,算法的練習如同內功的修煉,碰到算法問題,經常有一種無力感,只能慢慢總結了。
鏈表是一種重要的數據結構,因爲有遞歸性質,所以總是難以理解,涉及鏈表的複雜操作總是感覺一頭霧水,看別人的實現代碼總是似懂非懂,看完就忘,實際上就是沒有理解透徹,特意花了一天時間重新學習了單鏈表的常見操作-單鏈表反轉,理解和總結兩種實現方法。
首先要了解一個鏈表節點的數據結構:
value代表該節點存儲的值,next指向下一個節點,next可能指向一個單一節點,也可能指向一個鏈表,也可能指向null(代表尾節點)。
方法一:頭節點插入法
實現步驟:
- 創建一個帶頭節點的鏈表resultList
- 定義一個循環鏈表變量p,p的初始值爲待反轉的鏈表
- 遍歷待反轉的鏈表,遍歷條件爲 (p !=null)
3.1 定義一個臨時鏈表變量tempList保存p->next的值(因爲p->next值會改變),用於下一次循環。
3.2 把p當前指向的首節點和resultList鏈表的頭節點之後的節點拼接起來。
3.3 把3.2步驟中拼接的節點 再拼接到resultList頭節點後。
3.4 p重新賦值爲3.1步驟中保存的tempList的值。 - 返回resultList->next.即反轉後的鏈表。
圖解:
圖1就是待反轉的原始鏈表,圖2是 帶頭節點的鏈表resultList。
可以看到p是一個循環變量,初始值指向待反轉的原始鏈表。
通過p變量遍歷鏈表,在遍歷中(列舉的值爲第一次循環的情況):
- 定義一個臨時鏈表變量tempList保存p->next的值,tempList指向圖1.2
- 把p當前指向的首節點(圖1.1)和resultList鏈表的頭節點之後的節點(圖2.2)拼接起來。
- 把3.2步驟中拼接的節點(圖3.2) 再拼接到resultList頭節點後。
- p重新賦值爲3.1步驟中保存的tempList的值,即圖1.2。
最後返回resultList->next,也就是去除了頭節點-1的值。
通過頭結點插入法實現鏈表反轉功能,主要難點就是在循環中的3.2和3.3步驟的理解。
需要注意的是,就是關注循環變量p的值,也就是指向的變化,傳統的遍歷過程就是條件爲p!=null,處理,p=p->next。但是在這個頭結點插入代碼中,p->next的值發生的變化,因爲要將resultList結果鏈表的內容拼接到p的首節點後,所以要定義一個臨時變量存p->next。
方法二:就地反轉
頭結點插入法的實質是重新創建了一個新的鏈表,通過遍歷待反轉鏈表,將鏈表每一個節點插入到創建的鏈表中,然後的到的這個創建的鏈表就是反轉後的鏈表。而就地反轉實質就是在待反轉鏈表基礎上修改節點順序得到反轉鏈表。所以原理還是不同的。
圖解:
個人認爲就地反轉比頭結點插入稍微難理解一點。
宏觀上來看,要實現節點1和節點2反轉,需要將節點1插入節點2和節點3當中,然後將頭節點爲2的鏈表插入結果鏈表頭結點後面,然後再後移一個節點做同樣的操作。
然而涉及到兩個節點的循環賦值,這個操作順序就比較重要了。
正確的步驟是先將節點2切分出來,再將節點2插入-1和1之間。
實現步驟
- 創建一個帶頭節點的鏈表resultList,頭結點指向待反轉的鏈表。
- 創建p、pNext兩個用於循環操作,分別指向兩個待反轉節點的位置,初始值如圖所示,指向1和2
- 遍歷帶反轉的鏈表,循環條件是pNext!=null.
3.1 從鏈表中分割出pNext節點,也就是讓p指向pNext->next。
3.2 讓pNext指向經過3.1操作之後的resultList.next(1->3->4->5)
3.3 讓resultList頭結點指向pNext(2->1->3->4->5)
3.4 讓pNext指向p的下一個節點
難點在於理解循環中resultList.next指向性的變化,以及p和pNext兩個變量的變化,p指向的鏈表首結點永遠是1,只是節點1在resultList鏈表中位置在發生變化,而pNext是隨着p移動的,腦子中間可以有一個擺繩模型,在起始點位置發力,繩子的高點位置會移到繩尾,那個最高點就是p變量位置。
代碼實現:
頭結點插入:
public static ListNode reverseListByInsert(ListNode listNode){
//定義一個帶頭節點的
ListNode resultList = new ListNode(-1);
//循環節點
ListNode p = listNode;
while(p!= null){
//保存插入點之後的數據
ListNode tempList = p.next;
p.next = resultList.next;
resultList.next = p;
p = tempList;
}
return resultList.next;
}
就地反轉:
public static ListNode reverseListByLocal(ListNode listNode){
ListNode resultList = new ListNode(-1);
resultList.next= listNode;
ListNode p = listNode;
ListNode pNext = p.next;
while (pNext!=null){
p.next = pNext.next;
pNext.next = resultList.next;
resultList.next = pNext;
pNext=p.next;
}
return resultList.next;
}
單鏈表的操作困擾了很多年,或許還將困擾下去,但是至少有了更加深入的認識。這部分內容確實相對有點難以理解。推薦的學習方法就是先看別人代碼,再畫圖瞭解代碼的每一個過程,再根據畫的圖獨立寫代碼實現,然後再調試debug,查看變量變化情況,再找類似練習題,多想多練。(就地反轉的方法還需要細講)。