單鏈表題目匯總


內容會持續更新,有錯誤的地方歡迎指正,謝謝!

前言

單鏈表雖然簡單,但是由於其涉及很多的指針操作,細微之處都特別考驗程序員的能力,所以,是面試時被提及最頻繁的數據結構,博主在此列出一些單鏈表常見的題型。

目錄

現在看不懂目錄里的解決方法的同志,請跳過,直接進入下方內容。

以下所言的中指針即當前指針。

1、逆序構造單鏈表

前中指針。定義一個前指針,再在循環里申請新結點(即當前結點)的空間並為之賦值並讓其next指向前指針並更新前指針。

2、從尾到頭打印鏈表

法一:中指針+利用棧的逆序特性
法二:中指針+利用遞歸(存到vector)的逆序特性

3、反轉鏈表

法一:迭代(前中后三個指針)
法二:遞歸(前中指針,遞歸前半部分使當前結點走到最后一個結點、前結點走到倒數第二個結點,遞歸后半部分只需利用前指針進行反轉)

4、找出單鏈表的中間結點

快慢指針(快指針一次兩步,慢指針一次一步)

5、鏈表中環的入口結點

快慢指針求得環中一點,利用該點求得環中結點數,再用前后指針,邂逅點就是目標點。

6、兩個鏈表的第一個公共結點

法一:尾結點和其中一個鏈表的頭結點相連,就變成了上一個問題
法二:先通過兩個指針求出兩個鏈表的長度差,再用前后指針,邂逅點就是目標點。

7、鏈表升序排序

前后指針+快排思想

8、鏈表中倒數第k個結點

法一:前后指針,倒數第k個結點就是正着數第n-k+1個結點,前指針先走k-1步,再一起走。前指針到達最后一個結點時,后指針則到達了目標結點。
法二:也可以先求總的結點數n,再讓頭結點直接走n-k步,到達n-k+1處的結點即可。

9、刪除指定結點

法一:前中后指針,時間復雜度O(n)
法二:中后指針,若要刪除2,則把2和3的val交換,2的next指向3的next,即4,再delete3。

10、合並兩個升序的鏈表

法一:遞歸,兩個鏈表的頭指針+目標鏈表的頭指針
法二:迭代,兩個鏈表的頭指針+目標鏈表的頭指針

本文默認已定義了以下結點結構:

  struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) { }
//盡量使用初始化列表,因為它調用的是賦值構造函數,可少調用一次默認構造函數,快一些。
};

1、逆序構造單鏈表

前中指針。逆序構造單鏈表就是先讓最后一個結點指向nullptr,再讓倒數第二個結點指向最后一個結點。。。思路:先申請新結點的空間,再為之賦值。

ListNode* DescConstruct(ListNode* pHead)
{
ListNode* preNode=nullptr;//由於是逆序,所以需定義一個前結點
int val;
while(cin>>val&&val!=-1)//-1表示結束輸入
{
ListNode* curNode=new ListNode;//為新結點開辟空間
curNode->val=val;
curNode->next=preNode;
preNode=curNode;
}
pHead->next=preNode;//讓頭結點指向“第一個結點”,因為你把身體搭建好了,要有個腦殼嘛
return pHead;
}

2、從尾到頭打印鏈表

就是倒序打印單鏈表,輸入1->2->3->4,輸出4->3->2->1,注意鏈表為空的情況,可以使用棧或遞歸,時間復雜度為O(n)

    //使用棧,先依次入棧,再依次出棧即可,具體實現略

//使用遞歸
vector<int> res;
vector<int> printListFromTailToHead(ListNode* head)
{
ListNode* pNode=head;
if(pNode!=nullptr)
{
printListFromTailToHead(pNode->next);
res.push_back(pNode->val);
}
return res;
}

3、反轉鏈表

例如:假設現有鏈表:4->3->2->1,進行反轉操作后,鏈表變成:1->2->3->4。從頭到尾遍歷原鏈表,將每個結點摘下放在新鏈表的最前端。需要前中后三個指針,一個指向前一個結點,一個指向當前結點,一個指向下一個結點,再進行反轉。注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)

    //迭代實現
ListNode* ReverseList(ListNode* pHead)
{
if(pHead==nullptr||pHead->next==nullptr)
return pHead;
ListNode* preNode=nullptr;
ListNode* curNode=pHead;
ListNode* nextNode=curNode->next;//只用於記錄下一個結點的位置
while(curNode!=nullptr)
{
curNode->next=preNode;//調整指向
preNode=curNode;//preNode前進一步
curNode=nextNode;//curNode前進一步
if(nextNode!=nullptr)
nextNode=nextNode->next;//nextNode前進一步
}
return preNode;
}

//遞歸實現,很有意思
ListNode* ReverseList(ListNode* pHead)
{
if(pHead==nullptr||pHead->next==nullptr)
return pHead;
//遞歸前半部分使頭結點走到末結點,curNode是最后一個結點,pHead是前一個結點
ListNode* curNode=ReverseList(pHead->next);
//遞歸后半部分進行反轉,使下一個節點的next指針指向自己
//不能另設變量,因為非形參的變量無法傳遞,所以得用形參pHead實現該步
pHead->next->next=pHead;
pHead->next=nullptr;
return curNode;
}

4、找出單鏈表的中間結點

給定鏈表頭結點,在最小復雜度下輸出該鏈表的中間結點。那我們就用快慢指針唄,快指針一次走兩步,慢指針一次走一步。當快指針走到鏈表尾,慢指針剛好在中間結點。

    ListNode* FindMidNode(ListNode* pHead)
{
ListNode* pFast=pHead;
ListNode* pSlow=pHead;
while(pFast->next->next!=nullptr)
{
pFast=pFast->next->next;
pSlow=pSlow->next;
}
return pSlow;
}

5、鏈表中環的入口結點

這里寫圖片描述

如圖,已知一個單鏈表存在環,求進入環的第一個節點:先通過速度不同的快慢指針求得環中的一個結點,再通過該結點遍歷該環求得環內的結點數n,最后通過前后指針:前指針先走n步后,前后指針再一起走。一定要隨時注意是否存在環,若不存在則返回nullptr。

    ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode* meetNode=MeetingNode(pHead);//得到環中的一個結點
if(meetNode==nullptr)//判斷是否有環
return nullptr;
int nodeCount=1;
ListNode* tempNode=meetNode->next;
while(tempNode!=meetNode)//求環中的結點數
{
++nodeCount;
tempNode=tempNode->next;
}

//前指針先走nodeCount步后,前后指針同時走,兩個指針相遇的位置就是入口節點
ListNode* pAhead=pHead;
ListNode* pBehind=pHead;
while(nodeCount--)//前指針先走nodeCount步
pAhead=pAhead->next;
while(pAhead!=pBehind)//前后指針一起走
{
pAhead=pAhead->next;
pBehind=pBehind->next;
}
return pAhead;
}

//MeetingNode的迭代實現(用快慢指針)
ListNode* MeetingNode(ListNode* pHead)//求環中的一個結點
{
if(pHead==nullptr)//邊界條件之空鏈表
return nullptr;
ListNode* pBehind=pHead;
ListNode* pAhead=pHead->next;//前指針和后指針開始的時候不能在一起
//pAhead!=nullptr千萬別忘了!
while(pAhead!=nullptr&&pBehind!=pAhead)
{
//由一快一慢指針求相遇節點,快指針一次兩步,慢指針一次一步
pBehind=pBehind->next;
pAhead=pAhead->next;
if(pAhead!=nullptr)
pAhead=pAhead->next;
}
return pAhead;
}

/*補充:MeetingNode的遞歸實現
ListNode* GetMeetNode(ListNode* pAhead,ListNode* pBehind)
{
if(pAhead==nullptr)
return nullptr;
if(pAhead==pBehind)
return pAhead;
if(pAhead!=pBehind)
{
pAhead=pAhead->next;
pBehind=pBehind->next;
if(pAhead!=nullptr)
pAhead=pAhead->next;
}
ListNode* meetNode=GetMeetNode(pAhead,pBehind);
return meetNode;
}
*/

類似題之判斷鏈表是否有環

只用GetMeetNode函數就可解決

類似題之判斷兩個鏈表是否相交

這里寫圖片描述

法一:只需把尾結點和其中一個鏈表的頭結點相連,就變成了判斷另一個鏈表是否有環這個問題。
法二:遍歷第一個鏈表得到最后一個結點,再遍歷第二個鏈表得到最后一個結點,若這兩個結點相等則相交。

類似題之求兩個相交鏈表的交點

即求第一個公共結點,見下題:

6、兩個鏈表的第一個公共結點

法一:只需把尾結點和其中一個鏈表的頭結點相連,就變成了求解鏈表中環的入口結點這個問題。

if(pHead1==nullptr||pHead2==nullptr)//邊界條件之空鏈表
return nullptr;
ListNode* temp=pHead2;
while(temp->next!=nullptr)
temp=temp->next;
temp->next=pHead2;
//再利用pHead1去找第一個鏈表中環的入口結點。在每個return之前一定要復原:`temp->next=nullptr;`

法二(推薦):先用兩個指針,再用前后指針。先求兩個鏈表的長度差x,再讓長度長的那個鏈表對應的指針先走x步,再一起走,直到這兩個指針第一次邂逅,邂逅的那個結點就是第一個公共點。

    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) 
{
if(pHead1==nullptr||pHead2==nullptr)
return nullptr;
ListNode *p1=pHead1,*p2=pHead2;
int length1=1,length2=1;
while(p1->next!=nullptr)
{
++length1;
p1=p1->next;
}
while(p2->next!=nullptr)
{
++length2;
p2=p2->next;
}
p1=pHead1,p2=pHead2;//記得再次初始化
if(length1>length2)
{
int num1=length1-length2;
while(num1--)
{
p1=p1->next;
}
}
else if(length2>length1)
{
int num2=length2-length1;
while(num2--)
{
p2=p2->next;
}
}
while(p1->next!=nullptr&&p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
if(p1==p2)
return p1;
return nullptr;
}

7、鏈表升序排序

交換兩個結點的值即可!我們經常使用的一種快排:需要一個指針從頭往后掃描,一個指針從尾到頭掃描,並按一定規律交換值,最后使得支點左邊小於支點,支點右邊大於支點。但對單鏈表,從頭往尾很好辦,但從尾往頭做不到,因為單鏈表只有一個 next。所以,我們用另一種快排:使前后指針 i 和 j都往 next 方向移動並也可以按一定規律交換值。始終保持區間 [1, i] 的 val 都小於 base(最左邊(即索引為0)的結點是主元base),區間 [i+1, j) 的 val 都大於等於 base,那么當 j 走到末尾的nullptr時,便完成了一次支點的尋找。

這兩種排序方法搞不清除的同志,請見http://blog.csdn.net/billcyj/article/details/78461001

/**
* begin 鏈表的第一個結點,即輸入pHead
* end 鏈表的最后一個結點的next,即輸入nullptr
*/
void QuickSort(ListNode* begin, ListNode* end)
{
if (begin == end || begin->next == end) // 鏈表為空或只有一個結點
return;

int base = begin->val; // 設置主元
ListNode* i = begin; // i 左邊的小於 base
ListNode* j = begin->next; // i 和 j 中間的大於 base
while (j != end)
{
if (j->val< base)
{
i = i->next;
swap(i->val, j->val);
}
j = j->next;
}
swap(i->val, begin->val); // 交換主元和 i 的值

QuickSort(begin, i); // 遞歸左邊
QuickSort(i->next, end); // 遞歸右邊
}

//main函數中調用
QuickSort(pHead, nullptr);

8、鏈表中倒數第k個結點

倒着數第k個是不是等於正着數第n-k+1個?!So,問題迎面而解。
輸入一個鏈表,輸出該鏈表中倒數第k個結點。使用前后指針,假設有n個結點,第一個指針先走k-1步,到達第k個結點處,還剩個n-k結點未走。前后指針再一起走n-k步,第一個指針到達n處,第二個指針到達第n-k+1個結點處,即倒數第k個結點處。

    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) 
{
if(pListHead==nullptr||k<1)
return nullptr;
ListNode* pAhead=pListHead;
ListNode* pBehind=pListHead;
int times=k-1;
while(times--)
pAhead=pAhead->next;
int nodeCount=1;
while(pBehind->next!=nullptr)//求鏈表中總共有多少個結點
{
++nodeCount;
pBehind=pBehind->next;
}
pBehind=pListHead;
if(k>nodeCount)//必須要加,因為排避免k大於結點個數這種邊界條件。
return nullptr;
times=nodeCount-k;
while(times--)
{
pAhead=pAhead->next;
pBehind=pBehind->next;
}
return pBehind;
}

也可以先求總的結點數n,再讓頭結點直接走n-k步,到達n-k+1處的結點即可。

9、刪除指定結點

給定要刪除的結點targetNode和頭結點pHead,現要你刪除這個結點,要求平均時間復雜度為O(1)
例如1->2->3->4中我們要刪除2,則我們需要先求2的前一個結點1和后一個結點3,如此,時間復雜度為O(n),不符合要求。那么我們可以把2和3這兩個結點的值互換,然后對於3來說,已經知道了其前后結點,所以我們刪除3即可。但是,如果要刪除的結點是最后一個結點,那么就只有用O(n)的方法來做,但平均時間復雜度還是O(1)

    ListNode* deleteNode(ListNode* targetNode,ListNode* pHead)
{
if(targetNode->next==nullptr)//要刪除的結點是最后一個結點 O(n)
{
ListNode* tempNode=pHead;
while(tempNode->next==targetNode)
tempNode=tempNode->next;//找到了targetNode的前一個結點
tempNode->next=nullptr;
delete targetNode;
targetNode=nullptr;//要將已刪除的結點置為nullptr,不然就是亂指的野指針
}
else//要刪除的結點不是最后一個結點 O(1)
{
ListNode* nextNode=targetNode->next;
swap(targetNode->val,nextNode->val);
targetNode->next=nextNode->next;
delete nextNode;//就像殺掉替身一樣2333
nextNode=nullptr;
}
return pHead;
}

10、合並兩個升序的鏈表

輸入兩個單調遞增的鏈表,輸出兩個鏈表合成后的鏈表,當然我們需要合成后的鏈表滿足單調不減規則。和合並兩個遞增數組一樣的做法:兩個鏈表中小的結點先被鏈接進最終鏈表,直到有一個鏈表的全部結點都被鏈接完,另外一個鏈表的剩余結點直接被整體鏈入即可。

    //遞歸實現,思路很簡單,代碼很優美
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1==nullptr)
return pHead2;
if(pHead2==nullptr)
return pHead1;
ListNode* pMergeHead=nullptr;
if(pHead1->val<pHead2->val)
{
pMergeHead=pHead1;
pMergeHead->next=Merge(pHead1->next,pHead2);
}
else
{
pMergeHead=pHead2;
pMergeHead->next=Merge(pHead1,pHead2->next);
}
return pMergeHead;
}

//迭代實現,思路很清晰,代碼不優美
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1==nullptr)
return pHead2;
if(pHead2==nullptr)
return pHead1;
ListNode* pMergeHead=nullptr;//用於參與運算的結點resNode
if(pHead1->val<pHead2->val)//初始化resNode
{
pMergeHead=pHead1;
pHead1=pHead1->next;
}
else
{
pMergeHead=pHead2;
pHead2=pHead2->next;
}
ListNode* res=pMergeHead;//保存最終鏈表的頭結點
while(pHead1!=nullptr&&pHead2!=nullptr)
{
if(pHead1->val<pHead2->val)//誰小就誰先被鏈接進最終的鏈表
{
pMergeHead->next=pHead1;
pMergeHead=pHead1;
pHead1=pHead1->next;
}
else
{
pMergeHead->next=pHead2;
pMergeHead=pHead2;
pHead2=pHead2->next;
}
}
if(pHead1==nullptr)//pNode1鏈表無剩余結點了,但pNode2鏈表還有剩余結點
pMergeHead->next=pHead2;
else if(pHead2==nullptr)//pNode2結束了,但pNode1還有結點
pMergeHead->next=pHead1;
return res;
}

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com