鏈表應該是最重要的基本數據結構之一。本科數據結構白學了都忘光了(too naive!!),現在藉助找實習的契機重新溫故一下(學習學習,漲漲姿勢)。
那麼,鏈表有哪些特性呢?
鏈表是一種動態的數據結構,創建鏈表的時候無需知道它的長度。
當插入一個新的結點的時候只需要爲新節點分配內存並調整指針即可。
故內存分配不是在創建時一次性完成的,而是每添加一個結點分配一次內存。
(而創建數組時需要指定數組的大小,然後根據大小分配內存,故會造成空間浪費。)
雖然鏈表的空間效率很高,但是時間效率卻沒有數組好,因爲內存不是一次性分配,所以無法保證鏈表的內存和數組一樣是連續的。因此如果想在鏈表中找到第i個結點,只能從頭遍歷鏈表,時間效率爲O(n),而數組是O(1)。
c++定義一個單向鏈表的結點如下:
struct ListNode
{
int m_Value; //值
ListNode* m_pNext; //指向下一個結點的指針
}
或者
struct ListNode
{
int m_Value;
ListNode *m_pNext;
struct ListNode(int x):m_Value(x),m_pNext(NULL){}
};
鏈表一些常用的操作:插入,刪除。
(1)在鏈表末尾添加一個結點:
void AddToTail(ListNode **pHead, int value)
{
ListNode* pNew = new ListNode(); // 從堆中實例化新的結點
pNew->m_Value = value;
pNew->m_pNext = NULL;
//空鏈表的情況,這種情況需要對pHead進行修改,故上面傳參的時候需要用指向指針的指針
//否則這個函數調用完了仍然是空鏈表,這涉及到值傳參傳遞的是副本
if (*pHead == NULL)
{
*pHead = pNew;
}
else
{
ListNode* pNode = *pHead;
while(pNode->m_Next != NULL)
pNode = pNode->m_pNext;
pNode->m_pNext = pNew;//在末尾添加新的結點
}
}
如果上面指向指針的指針不明白的話可以參考下面這個博客,通透!
http://blog.csdn.net/shen_jz2012/article/details/50631317
(2)刪除某特定值第一次出現的結點
void RemoveNode(ListNode** pHead, int value)
{
//空鏈表
if(pHead == NULL || *pHead == NULL)
return;
//如果要刪除的就是第一個結點,那麼需要修改頭指針
ListNode* pToBeDeleted = NULL;
if ((*pHead)->m_Value == value)
{
pToBeDeleted = *pHead;
*pHead = (*pHead)->m_pNext;
}
//其它結點
else
{
ListNode* pNode = *pHead;
while(pNode->m_pNext != NULL && pNode->m_pNext->m_Value != value)
pNode = pNode->m_pNext;
//如果找到那個點的前驅
if(pNode->m_pNext != NULL && pNode->m_pNext->m_Value == value)
{
pToBeDeleted = pNode->m_pNext;
pNode->m_pNext = pNode->m_pNext->m_pNext;
}
}
//刪除結點
if(pToBeDeleted != NULL)
{
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
(3)給定單向鏈表的頭指針和一個結點指針,在O(1)時間刪除該結點
分析:之前刪除結點,需要找到該結點的前驅結點,然後pNode->m_pNext = pNode->m_pNext->m_pNext,而因爲鏈表的內存不像數組那樣是連續的,故需要從頭開始遍歷,時間複雜度是O(n)。那麼如何才能在O(1)時間刪除一個結點呢?
其實方法很簡單,我們有了一個結點的信息,其實也就知道了它下一個結點的位置,那麼我們只要將下一個結點的值複製到當前結點,並讓當前結點的指針指向下下一個結點即可。當然,實際編程過程中還有一些特殊情況需要考慮。
void DeleteNode(ListNode** pHead, ListNode* pToBeDelete)
{
if(!pHead || !pToBeDelete)
return;
//正常情況,不是最後一個結點
if(pToBeDelete->m_pNext != NULL)
{
//pToBeDeleted->m_Value = pToBeDeleted->m_pNext->m_Value;
//pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;
ListNode* pNext = pToBeDeleted->m_pNext;
pToBeDeleted->m_Value = pNext->m_Value;
pToBeDeleted->m_pNext = pNext->m_pNext;
delete pNext;
pNext = NULL;
}
//如果只有一個結點,要刪除的是第一個結點也即是最後一個結點,那麼刪除了就什麼都沒了,需要把頭指針和指向待刪除的結點的指針置空
else if(*pHead == pToBeDeleted)
{
delete pToBeDeleted;
pToBeDelete = NULL;
*pHead = NULL;
}
//如果有多個結點,但是要刪除的結點是最後一個結點,那麼上面方法就失效了,就只能按照從頭遍歷的方法
else
{
ListNode* pNode = *pHead;
while(pNode->m_pNext != pToBeDeleted)
pNode = pNode->m_pNext;
pNode->m_pNext = NULL;
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
分析一下時間複雜度:[(n-1)*O(1)+O(n)]/n = O(1).
(4) 逆序打印鏈表
很容易想到先進後出的堆棧
void PrintListReverse(ListNode* pHead)
{
std::stack<ListNode*> nodes;
ListNode* pNode = pHead;
while(pNode != NULL)
{
nodes.push(pNode);// 壓入堆棧
pNode = pNode->m_pNext;
}
while(!nodes.empty())//若非空打印棧頂元素並彈出
{
pNode = nodes.top();//返回棧頂元素
printf("%d\t", pNode->m_Value);
nodes.pop();// 彈出
}
}
進一步想了一下,遞歸的代碼更簡潔
void PrintListReverse(ListNode* pHead)
{
if(pHead == NULL)
return;
ListNode* pNode = pHead;
if (pNode->m_Next != NULL)
PrintListReverse(pNode->m_Next);
printf("%d\t",pNode->m_Value)
}