01 認識『鏈表』
鏈表的結構十分簡單,其本身是一種線性的存儲結構,通過物理地址不連續的節點相連接成鏈。最簡單的單鏈表只包含一條鏈,並且每一個節點包括兩部分內容,數據元素和下一個節點的地址。因此可通過已知節點訪問它的下一個節點。
相比於數組而言,由於鏈表不必須按順序存儲,因而在插入的時候可以達到O(1)的複雜度,一般而言比數組的效率要高得多;但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而數組由於可以直接通過下標尋址,相應的時間複雜度僅爲O(1)。
一般可通過定義結構體的方式來實現鏈表:
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
02 高頻算法題
1. 輸入一個鏈表,輸出該鏈表中倒數第k個結點。
快慢指針的方法在鏈表相關的操作中經常使用。
通過設置快慢指針,快指針先前進k步,之後快慢指針同時前進,此時快慢指針間隔k步;當快指針到達鏈表尾部,此時慢指針所在節點即爲倒數第k個節點。這樣的方法僅通過一次遍歷即可獲得倒數第k個節點。
代碼如下:
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(pListHead==NULL||k==0)
return NULL;
ListNode*pTail=pListHead,*pHead=pListHead;
for(int i=1;i<k;++i)
{
if(pHead->next!=NULL)
pHead=pHead->next;
else
return NULL;
}
while(pHead->next!=NULL)
{
pHead=pHead->next;
pTail=pTail->next;
}
return pTail;
}
};
2. 輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。
非遞歸方法的解題思路需要理解以下四步,具體見下圖:
代碼如下:
//第一種方法:非遞歸方法 /*
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(pHead==NULL)
return NULL;
ListNode* cur = pHead;
ListNode* pre = NULL;
ListNode* head = NULL;
while(cur != NULL)
{
if(cur->next==NULL)
head = cur;
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return head;
}
};
//第二種方法是:遞歸方法 /*
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
//如果鏈表爲空或者鏈表中只有一個元素
if(pHead==NULL||pHead->next==NULL) return pHead;
//先反轉後面的鏈表,走到鏈表的末端結點
ListNode* pReverseNode=ReverseList(pHead->next);
//再將當前節點設置爲後面節點的後續節點
pHead->next->next=pHead;
pHead->next=NULL;
return pReverseNode;
}
};
3. 將兩個有序鏈表合併爲一個新的有序鏈表並返回。
這題可通過設置雙指針的方式對鏈表不同節點元素進行篩選:
代碼如下:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
4. 尋找兩個相交鏈表的公共起點。
這題同樣可以設置快慢指針,比較常規的方法可以先求得2個鏈表的長度,然後讓長鏈表指針先前進兩個鏈表的長度差,然後再一起前進;最後當快慢指針相遇的位置即是公共起點。
代碼如下:
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2) {
int len1 = findListLenth(pHead1);
int len2 = findListLenth(pHead2);
if(len1 > len2){
pHead1 = walkStep(pHead1,len1 - len2);
}else{
pHead2 = walkStep(pHead2,len2 - len1);
}
while(pHead1 != NULL){
if(pHead1 == pHead2) return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
return NULL;
}
int findListLenth(ListNode *pHead1){
if(pHead1 == NULL) return 0;
int sum = 1;
while(pHead1 = pHead1->next) sum++;
return sum;
}
ListNode* walkStep(ListNode *pHead1, int step){
while(step--){
pHead1 = pHead1->next;
}
return pHead1;
}
};
5. 給一個鏈表,若其中包含環,請找出該鏈表的環的入口結點,否則,輸出null。
這題同樣可採用快慢指針的方法來做。
快指針每次前進2步,慢指針每次前進1步。若鏈表有環,則快慢指針會在環內相遇。假設圖中b是環的入口,a爲鏈表起點,c爲快慢指針相遇的位置。由於快慢指針的前進時間相同,則根據速度關係可得:
2(ab+bc)=ab+bc+cb+bc,則得ab=bc
所以當快慢指針相遇後,從相遇點到環入口的距離與從鏈表頭到環入口的距離一樣。通過設置一指針從鏈表頭部前進,一指針從相遇點同時前進,兩指針相遇的位置即是鏈表環的入口。
代碼如下:
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode*fast=pHead,*low=pHead;
while(fast&&fast->next){
fast=fast->next->next;
low=low->next;
if(fast==low)
break;
}
if(!fast||!fast->next)
return NULL;
low=pHead;//low從鏈表頭出發
while(fast!=low){//fast從相遇點出發
fast=fast->next;
low=low->next;
}
return low;
}
};
ps:個人公衆號【業餘碼農】,每天更新互聯網面試經驗分享,還有技術基礎整理和常見算法詳解。