最近一直在看《劍指Offer》這本書,現在總結一下對於鏈表方面的一點認識。
鏈表的結構很簡單,它由指針把若干個結點連成鏈狀結構。鏈表的基本操作包括:創建、插入結點、刪除結點等。鏈表是一種動態存儲結構,在創建鏈表的時候無需知道鏈表的長度,當插入一個結點時,我們只需要爲新插入的節點分配內存,然後調整指針的指向來確保新插入的結點被鏈接到鏈表當中。內存分配不是在創建鏈表時一次完成的,而是每添加一個結點分配一此內存,由於沒有閒置的內存,鏈表的效率比數組高。單向鏈表的結構可以如下表示:
struct ListNode
{
int value;
ListNode*next;
};
那麼往鏈表的末尾添加一個結點的代碼爲:
void AddToTail(ListNode** head,int value)
{
ListNode*pNew=new ListNode();
pNew->value=value;
pNew->next=NULL;
if(*head==NULL)
{
*head=pNew;
}
else
{
ListNode*Node=*head;
while(Node->next!=NULL)
{
Node=Node->next;
}
Node->next=pNew;
}
}
其中其中特別要注意的是函數的第一個參數傳入的是指針的指針。這是因爲我們在函數中有可能要改變頭結點的指向,當我們往一個空鏈表中插入結點時,新插入的結點就是鏈表的頭指針。此時,會改變頭指針的指向,因此必須把頭指針pHead參數設爲指向指針的指針。補充:編譯器會爲函數的參數製作一個臨時的副本,當指針Phead作爲函數參數時,編譯器會自動爲該指針創建一個副本PHead_,使Phead和Phead_指向同一個數據,在函數中,假若這是一個空的鏈表,當插入第一個結點時,會在函數的內部爲PHead_分配一塊內存,但是Phead沒有任何改變,任然是一個空指針。
鏈表側查找與刪除:
void RemoveNode(ListNode** head,int value)
{
if(head==NULL||*head==NULL)
return;
ListNode *ToBeDeleteNode=NULL;//新建節點的時候必須初始化爲NULL;
if((*head)->value==value)
ToBeDeleteNode=*head;
else
{
ListNode *pNode=*head;//當前檢測的節點
ListNode *pNode_pre=pNode;//當前已經檢測過的節點
while(pNode->value!=value&&pNode->next!=NULL)
{ pNode_pre=pNode;
pNode=pNode->next;
}
if(pNode->value==value)
{
ToBeDeleteNode=pNode;
pNode_pre->next=pNode->next;//一定要注意這個地方
}
else
{
cout<<"cannot find"<<endl;
}
}
if(ToBeDeleteNode!=NULL)
{
delete ToBeDeleteNode;
ToBeDeleteNode=NULL;//刪除結點之後,要將指針設爲空
}
}
在涉及鏈表的刪除問題時,一定要注意以下幾點:
一、所要刪除的結點是不是空的
二、所要刪除的結點是不是頭結點。
問題一:
求鏈表中倒數第K個結點。
第一種思路,從頭開始遍歷整個鏈表,求取鏈表的長度n,倒數第K個結點就是從頭開始第n-k+1個結點。但是這種方法,需要兩次遍歷鏈表,效率不高。
第二種思路,我們可以定義兩個指針p_fist和p_second,都指向表頭,p_first先走k-1步,然後,p_second從頭開始走,當p_fisrst走到末尾的時候,p_second就是倒數第K個結點,這種方法只遍歷一次鏈表,效率高。
//求解鏈表中倒數K個結點
ListNode* FindKthNode(ListNode*head,int k)
{
if(head==NULL||k==0)
return NULL;//檢查是否爲空鏈表
ListNode *pHead=head;
ListNode *BehindHead=head;
for(int i=0;i<k-1;i++)
{
if(pHead->next!=NULL)//檢查K是否大於鏈表的長度
pHead=pHead->next;
else
return NULL;
}
while(pHead->next!=NULL)
{
pHead=pHead->next;
BehindHead=BehindHead->next;//
}
return BehindHead;
這裏需要注意幾個問題,來保證程序的魯棒性。在函數的入口處添加代碼以驗證用戶的輸入是否合法。
一、輸入的頭指針爲空
二、輸入的k大於結點總數
三、輸入的參數K等於0或小於0,
for(int i=0;i<k-1;i++)
上述循環就會崩潰。
相似的題目:求鏈表的中間結點,如果鏈表中結點總數爲奇數,返回中間結點;如果鏈表中結點總數爲偶數,返回中間兩個結點中的任意一個結點。爲了解決這個問題,我們也可以定義兩個指針,同時從鏈表的頭結點出發,一個每次走一步,另一個每次走兩步,當走的快的指針走到鏈表的末尾時,走的慢的指針正好在鏈表的中間。
ListNode* FindMiddle(ListNode*head)
{
if(head==NULL)
return NULL;
if(head->next==NULL)
return head;
ListNode *p_first=head;
ListNode *p_second=head;
while(p_first->next!=NULL)
{
p_first=p_first->next;
if(p_first->next!=NULL)
p_first=p_first->next;
else
break;
p_second=p_second->next;
}
return p_second;
}
反轉鏈表:
ListNode* ReverseList(ListNode* head)
{
if(head==NULL)
return NULL;
if(head->next==NULL)
return head;
ListNode *pReversedHead=NULL;
ListNode *pNode=head;
ListNode *pPrev=NULL;
while(pNode!=NULL)
{
ListNode *next=pNode->next;
if(next==NULL)//到了最後一個結點,反轉之後的頭結點
pReversedHead=pNode;
pNode->next=pPrev;//當前結點指向前一個結點
pPrev=pNode;
pNode=next;
}
return pReversedHead;
}
總結了一下要點:
1、三個指針,當前結點的指針、當前結點的前一個結點,當前結點的下一個結點
2、記錄當前結點的下一個結點