鏈表操作在面試中經常出現,這一方面考察了對指針的使用,以下整理了一些關於鏈表的面試題以及注意事項。
首先對單鏈表進行定義。
struct ListNode{
int val;
ListNode *next;
};
關於鏈表,有以下幾個方面需要注意:
1、指針方面。判斷指針是否爲NULL,何時判斷等等。
2、不要貪圖少指針,儘量清楚明瞭邏輯清晰,多幾個指針也沒關係。
3、一般單鏈表的題目較多,一定要注意單鏈表的單向特性,該保存指針就保存,同2,不要妄想就用一個指針解決問題。
4、指針作爲實參傳入時要注意,雖然指針可以改變所指向的地址的值,但是指針所保存的地址依然是作爲按值傳遞進入函數的,所以若想使用最後的指針,需要使用指針的指針、引用或者利用函數返回指針,切記這三種方式。
接下來對幾個常見問題進行介紹。
問題1:輸入一個單向鏈表,輸出該鏈表中倒數第k個結點
先第一個指針向前走k,然後第二個指針此時和第一個指針一起走即可。
情況:pHead=NULL、鏈表小於k、k輸入爲0;
ListNode* LastK(ListNode* pHead, unsigned int k)
{
// 保證輸入的正確性;
if (pHead == NULL || k == 0)
return NULL;
ListNode *pAhead = pHead;
ListNode *pBehind = NULL;
for ( int i = 0; i < k - 1; i++)
{
if (pAhead->next != NULL)
pAhead = pAhead->next;
else
return NULL;
}
pBehind = pHead;
while ( pAhead != NULL)
{
pAhead = pAhead->next;
pBehind = pBehind->next;
}
return pBehind;
}
其他方法:先遍歷一次得到總節點數。
不足:未考慮有環的情況。
問題2:如何判斷一個鏈表有環
這個問題可以這麼來分析我們可以設置兩個指針(p1, p2),初始值都指向頭,p1每次前進一步,p2每次前進二步,如果鏈表存在環,則p2先進入環,p1後進入環,兩個指針在環中走動,必定相遇
bool isCircle(ListNode* pHead, ListNode** pNode)
{
if (pHead == NULL)
return NULL;
ListNode *fast = pHead;
ListNode *slow = pHead;
while(1)
{
if ( fast->next == NULL)
{
*pNode = fast;
return false;
}
fast = fast->next;
if ( fast->next == NULL)
{
*pNode = fast;
return false;
}
fast = fast->next;
if ( fast == slow)
{
*pNode = fast;
return true;
}
slow = slow->next;
}
}
附:若鏈表有環的情況下如何找出環的入口。
當fast若與slow相遇時,slow肯定沒有走遍歷完鏈表,而fast已經在環內循環了n圈(1<=n)。假設slow走了s步,則 fast走了2s步(fast步數還等於s 加上在環上多轉的n圈),設環長爲r,則:
2s = s + nr
s= nr
設整個鏈表長L,入口環與相遇點距離爲x,起點到環入口點的距離爲a。
a + x = nr
a + x = (n – 1)r +r = (n-1)r + L - a
a = (n-1)r + (L – a – x)
(L – a – x)爲相遇點到環入口點的距離,由此可知,從鏈表頭到環入口點等於(n-1)循環內環+相遇點到環入口點,於是我們從鏈表頭、與相遇點分別設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點爲環入口點。
分析:問題可以分爲兩種情況,第一種情況如果兩個鏈表都沒有環的話,那麼兩個鏈表要是相交,那麼從他們相交的那一點開始到尾節點兩個鏈表應該完全相同,這樣我們就可以直接判斷鏈表的尾節點是否相同來進行判斷兩鏈表是否相交來進行判斷。第二種情況的話如果兩個鏈表存在環的話,那麼我們則應該判斷一鏈表上倆指針相遇的那個節點,在不在另一條鏈表上,如果在,則相交,如果不在,則不相交。
bool isIntersect(ListNode* pHead1, ListNode* pHead2)
{
ListNode **Last1 = NULL;
ListNode **Last2 = NULL;
bool isCir1 = isCircle(pHead1, Last1);
bool isCir2 = isCircle(pHead2, Last2);
// 一個有環一個沒有,肯定不相交;
if ( isCir1 != isCir2)
return false;
// 如果都無環,判斷最後一個是否相同即可;
if ( isCir1 == false)
{
if ( *Last1 == *Last2)
return true;
else
return false;
}
ListNode *tmp = *Last2;
if ( tmp == *Last1)
return true;
tmp = tmp->next;
// tmp繞環一圈,尋找是否和第一個鏈表有相同節點;
while ( tmp != (*Last2))
{
if ( tmp == *Last1)
return true;
tmp = tmp->next;
}
return false;
}
問題4:反轉鏈表
其實問題很簡單,但是在代碼的過程中要注意保存好相應的指針,以及考慮特殊的情況,如終止條件,特殊輸入等。
void inver(ListNode **Head)
{
if ( Head == NULL || *Head == NULL || (*Head)->next == NULL)
return;
ListNode *p1 = NULL;
ListNode *p2 = *Head;
while( p2 != NULL)
{
*Head = p2;
p2 = (*Head)->next;
(*Head)->next = p1;
p1 = *Head;
}
}
問題5:合併兩個排序的鏈表
該題需要對合並的過程考慮清楚,如何處理合並問題是關鍵,可以以採用遞歸算法解決,返回什麼?輸入什麼?一旦這些考慮完善,寫出來就容易很多。
void inver(ListNode **Head)
{
if ( Head == NULL || *Head == NULL || (*Head)->next == NULL)
return;
ListNode *p1 = NULL;
ListNode *p2 = *Head;
while( p2 != NULL)
{
*Head = p2;
p2 = (*Head)->next;
(*Head)->next = p1;
p1 = *Head;
}
}
問題6:在O(1)時間刪除給點節點
由於節點沒有前向節點的地址,所以遍歷是需要O(n)時間。
轉換思維,我們可以將後一節點的值賦給該節點,然後刪去後一節點。
功能測試:鏈表只含一個節點;節點是鏈表最後節點。