數據結構與算法——鏈表(1)

最近一直在看《劍指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、記錄當前結點的下一個結點





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章