劍指Offer程式設計題筆記之連結串列相關
前言
本來呢,是想十題十題這樣寫幾篇筆記的。後面發現,按照題型來分類會更好。比如這篇,雖然只有九題,但是都是跟連結串列相關的。按題型分類,這樣,也好做最後的總結,是吧?
題目
從尾到頭列印連結串列
第1題
題目描述
輸入一個連結串列,從尾到頭列印連結串列每個節點的值。
思路:
立馬想到的是有先進後出特性的棧!先把節點值存到棧中,再通過出棧方法輸出節點值。
實現如下:
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack<Integer> stack = new Stack<>();
ListNode p = listNode;
while(p!=null){
stack.push(p.val);
p = p.next;
}
ArrayList<Integer> list = new ArrayList<>();
while(!stack.isEmpty()){
list.add(stack.pop());
}
return list;
}
連結串列中倒數第K個節點
第2題
題目描述
輸入一個連結串列,輸出該連結串列中倒數第k個結點。
思路:
看到倒數,也就是反序,就想到了棧。但是棧會耗費額外的空間記憶體。實現略。
思路2:
遍歷一遍,得到連結串列個數。第二次遍歷,就知道倒數第K個時哪個了。但需要遍歷兩遍。實現略。
思路3:
使用兩個指標ab,a指標先走K步,然後ab兩個指標同時前進,當a指標到達尾部時,b指標所指向的節點正是倒數第K個節點。因為a指標先走了k步,所以ab節點差了k個位置啊。這樣只需要遍歷一次,且沒有額外空間開銷。
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if (head==null)
return null;
ListNode p1 = head;
ListNode p2 = head;
int i=1;//i用來記錄實際先走了多少步
while(i<k && p1.next!=null){
p1 = p1.next;
i++;
}
if(i!=k)//如果i不等於k,說明i小於k,說明k比連結串列長度還長
return null;
while(p1.next!=null){
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
}
大神更簡潔的實現:
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
ListNode p,q;
p = head;
q = head;
int i=0;
for(;p!=null;i++){
if(i>=k)
q = q.next;
p = p.next;
}
return i<k?null:q;
}
}
相比之下,我的寫法程式碼重複率大很多。
反轉連結串列
第3題
題目描述
輸入一個連結串列,反轉連結串列後,輸出連結串列的所有元素。
思路:第一個想到的又是棧…要使用額外記憶體空間啊。實現略。
思路2:不使用額外記憶體空間!利用三個指標,遍歷的同時實現節點next域的反轉。
實現如下:
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head==null||head.next==null)
return head;
ListNode p1 = head;
ListNode p2 = p1.next;
ListNode p3 = p2.next;
do{
p2.next = p1; //實現反轉,下面重新設定指標
p1 = p2;
p2 = p3;
if(p3!=null && p3.next!=null)
p3 = p3.next;
else
p3 = null;
}while(p2!=null);
head.next = null;
return p1;
}
}
利用三個指標,另一種寫法:
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head==null)
return null;
ListNode p1 = head;
ListNode p2 = p1.next;
ListNode p3;
p1.next = null;
while(p1!=null && p2!=null){
p3 = p2.next;
p2.next = p1;
p1 = p2;
p2 = p3;
}
return p1;
}
}
合併兩個排序的連結串列
第4題
題目描述
輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。
思路:
利用一個指標(作為新連結串列的尾指標),另外關鍵是建立一個頭節點,通過頭節點和一個指標,將兩個連結串列串起來。
具體是當比較兩條連結串列的節點時,找到那個值小的節點,並讓它作為尾指標指向的節點的下一個節點,再重新更新尾指標。
實現如下:
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null)
return list2;
if(list2==null)
return list1;
ListNode preNode = new ListNode(0);//頭節點
ListNode p = preNode;
while(list1!=null && list2!=null){
if(list1.val<=list2.val){
p.next = list1;
p = list1;
list1 = list1.next;
}else{
p.next = list2;
p = list2;
list2 = list2.next;
}
if(list1==null) //這兩步不能漏
p.next = list2;
if(list2==null) //這兩步不能漏
p.next = list1;
}
return preNode.next;
}
}
複雜連結串列的複製
第5題
題目描述
輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為複製後複雜連結串列的head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)
思路:
思路???就建立個頭結點,然後就遍歷舊連結串列,複製節點,串成新連結串列啊。不然還能怎樣。這題是想考我什麼啊。
實現如下:
public class Solution {
public RandomListNode Clone(RandomListNode pHead)
{
if(pHead==null)
return null;
RandomListNode preHead = new RandomListNode(0);
RandomListNode p = preHead;
while(pHead!=null){
RandomListNode node = new RandomListNode(pHead.label);
node.next = pHead.next;
node.random = pHead.random;
p.next = node;
p = node;
pHead = pHead.next;
}
return preHead.next;
}
}
遞迴實現:
public class Solution {
public RandomListNode Clone(RandomListNode pHead)
{
if(pHead==null)
return null;
RandomListNode node = new RandomListNode(pHead.label);
node.random = pHead.random;
node.next = Clone(pHead.next);
return node; //不能漏
}
}
兩個連結串列的第一個公共節點
第6題
題目描述
輸入兩個連結串列,找出它們的第一個公共結點。
一開始想到的思路是暴力法,比較每個節點!後來才明白,只有有一個節點是同一個節點,往後的節點都是不需要比較的,因為往後都是同樣的節點啊,肯定相同啊。
暴力思路(可不看):
兩條連結串列ab,
a:1-2-3-4-5
b:2-3-4-5
連結串列a,可以看成是5個連結串列,分別是:
a1: 1-2-3-4-5
a2: 2-3-4-5
a3: 3-4-5
a4: 4-5
a5: 5
判斷連結串列a1是否連結串列b的後半部分,是的話則返回公共節點a1,否則
判斷連結串列a2是否連結串列b的後半部分,是的話則返回公共節點a2,否則
…
最後a5都不滿足,則返回null。
實現如下(可不看):
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null || pHead2==null)
return null;
while(pHead2!=null){ //每層迴圈,p2指向的就是上面思路中的a1,a2,a3,a4,a5
ListNode p1 = pHead1; //連結串列a的指標
ListNode p2 = pHead2; //連結串列b的指標
while(p1!=null&&p2!=null){ //判斷p2指向的連結串列是否為連結串列pHead的後半部分
if(p1.val==p2.val){
p1 = p1.next;
p2 = p2.next;
}else{
p1 = p1.next;
}
}
if(p1==null&&p2==null) //遍歷到最後為null,說明到尾節點都是相同,則說明從pHead2開始兩連結串列就是相同的
return pHead2;
pHead2 = pHead2.next; //以下個節點作為頭結點
}
return null;
}
}
另一種思路:
先遍歷兩條連結串列,獲取兩條連結串列長度,計算差值x。然後讓較長的連結串列先走x步。然後,兩條連結串列就一樣長了。遍歷兩連結串列,返回第一個相等的節點。這種解法空間複雜度為O(1)。
實現如下:
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null || pHead2==null)
return null;
int p1Len = getLength(pHead1);
int p2Len = getLength(pHead2);
int x = 0;
if(p1Len>=p2Len){
x = p1Len-p2Len;
pHead1 = getNextN(pHead1,x);
}else{
x = p2Len-p1Len;
pHead2 = getNextN(pHead2,x);
}
while(pHead1!=pHead2){
pHead1 = pHead1.next;
pHead2 = pHead2.next;
}
return pHead1;
}
private int getLength(ListNode p){
int n=0;
while(p!=null){
n++;
p=p.next;
}
return n;
}
private ListNode getNextN(ListNode p,int x){
int i=0;
while(i<x){
i++;
p = p.next;
}
return p;
}
}
另一個思路:
想起了大神解法,利用兩個棧!!把兩條連結串列壓入兩個棧,再依次比較兩棧棧頂的節點,若相同,則彈出,繼續比較棧頂節點。如此迴圈,就可以實現節點從後往前一個一個對比了!雖然需要額外維護兩個棧,但是該解法卻很巧妙。
實現如下:
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null || pHead2==null)
return null;
Stack<ListNode> stack1 = new Stack();
Stack<ListNode> stack2 = new Stack();
while(pHead1 != null){
stack1.push(pHead1);
pHead1 = pHead1.next;
}
while(pHead2 != null){
stack2.push(pHead2);
pHead2 = pHead2.next;
}
ListNode result = null;
while(!stack1.isEmpty() && !stack2.isEmpty() && stack1.peek()==stack2.peek()){
result = stack1.pop();
stack2.pop();
}
return result;
}
}
雖然該思路不是我先想到的,但是沒有學完就忘,還是可以的吧?
連結串列中環的入口節點
第7題
題目描述
一個連結串列中包含環,請找出該連結串列的環的入口結點。
思路:
如果可以使用額外記憶體空間的話,那麼可以建立一個ArrayList,用來儲存節點,然後每次呼叫add方法往容器新增節點前,先用contains方法判斷容器裡是否該節點,若存在,則說明該節點就是重複出現的節點,即環的入口節點。若果不存在,則新增進容器。
實現如下:
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead){
ArrayList<ListNode> list = new ArrayList();
ListNode p = pHead;
while(p!=null){
if(list.contains(p))
return p;
else
list.add(p);
p = p.next;
}
return null;
}
}
另一種解法:
空間複雜度為O(1)的高階解法,使用快慢指標。還沒搞懂。弄懂了再補上。
參考連結
刪除連結串列中的重複的節點
第8題
題目描述
在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5
思路:
注意,題目說了,這是排序的連結串列。如果沒排序,就另當別論了。
建立一個頭結點和尾指標。關鍵是用到尾指標。遍歷連結串列,當找到節點值相等的兩個節點,設其節點值為x,然後通過迴圈將節點值為x的移除。怎麼移除?藉助尾指標,通過操控尾指標來剔除那些重複的節點。
要注意的是,這個尾指標並不是相對於舊連結串列而言,而是相對於無重複節點的連結串列而已。
實現如下:
public class Solution {
public ListNode deleteDuplication(ListNode pHead){
ListNode preNode = new ListNode(0);
preNode.next = pHead;
ListNode last = preNode;
ListNode p = pHead;
while(p!=null&&p.next!=null){//遍歷連結串列
if(p.val==p.next.val){
int val = p.val;
while(p!=null&&p.val==val){//剔除重複節點
p = p.next;
last.next = p; //剔除
}
}else{ //如果不重複,則無需剔除,直接移動尾指標
last = p;
p = p.next;
}
}
return preNode.next;
}
}
二叉搜尋樹變雙向連結串列
第9題
題目描述
輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。
思路:
通過中序遍歷實現,關鍵是要建立一個成員引用pre。用來表示上一個剛被訪問過的節點,也可看做是新連結串列的尾指標。
通過在遍歷中設定pre.right=current,current.left=pre,pre=current
,即可將二叉樹轉變成雙向連結串列。
實現如下:
public class Solution {
TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
TreeNode p = pRootOfTree;
if(p==null)
return p;
middleTraverse(p);
//通過向左遍歷找到連結串列的首結點
while(p.left!=null)
p = p.left;
return p;
}
//中序遍歷
public void middleTraverse(TreeNode cur){
if(cur==null)
return;
middleTraverse(cur.left);
//關鍵,重新調整節點的指標
if(pre!=null)
pre.right = cur;
cur.left = pre;
pre = cur;
middleTraverse(cur.right);
}
}
總結
連結串列相關的題,很多優秀的解法空間複雜度都為O(1),通常呢是利用一個或多個指標,或者使用快慢指標來遍歷(像題2,題3,題8)。有可能需要多次遍歷(一次用來獲取連結串列長度,像題6的第二種解法)。有時,還需要用一個尾指標來維護新的連結串列,像題4、題8、題9。如果出現反序遍歷(像題1)或者從尾部開始比較(像題6),則可以考慮使用棧。再不然,可以使用ArrayList、HashMap來配合解決問題(像題8)。