问题:假设一个没有头指针的单链表。一个指针指向此单链表中间的一个节点(既不是第一个,也不是最后一个节点),请将该节点从单链表中删除。链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
解答:假设给定的指针为pCurrent,ListNode* pNext = pCurrent->m_pNext;
由题意知,pCurrent指向链表的某一个中间节点,因此pCurrent->m_pNext != NULL。
要删除pCurrent指向的节点B很简单,但必须将节点B前后两个节点A和C连接起来,但是单链表节点没有头指针,因此无法追溯到A,也就无法将A和C相连了。
无法删除节点B,但我们可以删除B的后继节点C,并通过pCurrent->m_pNext = pCurrent->m_pNext->m_pNext重新将链表连接起来,而唯一丢失的是节点C的数据项m_nKey。因此,我们只需要将节点C的数据项取代节点B的数据项,然后将真正指向节点C的指针删除即可实现将节点B“删除”!!关键代码如下:
pCurrent->m_pNext = pNext->m_pNext;
pCurrent->m_nKey = pNext->m_nKey;
delete pNext;
完整代码如下:
#include
#include
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
//参数必须是指向指针的指针,因为要修改指针
//所以要考虑“参数本地拷贝”问题
void InitList(ListNode** pList)
{
*pList = (ListNode*)malloc(sizeof(ListNode));
(*pList)->m_pNext = NULL;
}
//第一个参数不必是指向指针的指针,因为函数内只修改指针指向的数据
//所以不用担心“参数本地拷贝”问题
void InsertList(ListNode* pList, int data)
{
ListNode* pNewNode = (ListNode*)malloc(sizeof(ListNode));
pNewNode->m_nKey = data;
pNewNode->m_pNext = pList->m_pNext;
pList->m_pNext = pNewNode;
}
//核心算法
void DeleteRandomNode(ListNode* pCurrent)
{
assert(pCurrent != NULL);
ListNode* pNext = pCurrent->m_pNext;
if(pNext != NULL)
{
pCurrent->m_pNext = pNext->m_pNext;
pCurrent->m_nKey = pNext->m_nKey;
delete pNext;
pNext = NULL;
}
}
//打印链表元素
void PrintListNormally(ListNode* pListHead)
{
ListNode* pTempNode = pListHead->m_pNext;
while(pTempNode != NULL)
{
std::cout<
pTempNode = pTempNode->m_pNext;
}
}
int main()
{
ListNode* pHead = NULL;
InitList(&pHead);
for(int i=9; i>=0; i--)
{
InsertList(pHead,i);
}
PrintListNormally(pHead);
ListNode* pNext = pHead->m_pNext->m_pNext; //删除第个元素
DeleteRandomNode(pNext);
PrintListNormally(pHead);
system("pause");
return 0;
}
=======================================================
扩展题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
函数的声明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
分析:这是一道广为流传的Google面试题,能有效考察我们的编程基本功,还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解。
在链表中删除一个结点,最常规的做法是从链表的头结点开始,顺序查找要删除的结点,找到之后再删除。由于需要顺序查找,时间复杂度自然就是O(n) 了。
我们之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。我们试着换一种思路。我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。
上面的思路还有一个问题:如果删除的结点位于链表的尾部,没有下一个结点,怎么办?我们仍然从链表的头结点开始,顺便遍历得到给定结点的前序结点,并完成删除操作。这个时候时间复杂度是O(n)。
那题目要求我们需要在O(1)时间完成删除操作,我们的算法是不是不符合要求?实际上,假设链表总共有n个结点,我们的算法在n-1总情况下时间复杂度是O(1),只有当给定的结点处于链表末尾的时候,时间复杂度为O(n)。那么平均时间复杂度[(n-1)*O(1)+O(n)]/n,仍然为O(1)。
基于前面的分析,我们不难写出下面的代码。
核心参考代码:
//核心算法
//pListHead是链表头指针,pCurrent是要删除的节点指针
void DeleteRandomNode(ListNode* pListHead, ListNode* pCurrent)
{
assert(pCurrent != NULL || pListHead != NULL);
if(pCurrent->m_pNext != NULL) //要删除的节点不是最后一个节点
{
ListNode* pNext = pCurrent->m_pNext;
pCurrent->m_nKey = pNext->m_nKey;
pCurrent->m_pNext = pNext->m_pNext;
delete pNext;
pNext = NULL;
}
else //要删除的节点是链表中最后一个节点
{
ListNode* pNode = pListHead;
while(pNode->m_pNext != pCurrent) //得到要删除节点的前继节点
{
pNode = pNode->m_pNext;
}
pNode->m_pNext = NULL;
delete pCurrent;
pCurrent = NULL;
}
}
完整的代码只需替换第一题的相应函数,并在main函数调用中做相应修改即可。
值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的鲁棒性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。当然,在面试中,我们可以把这些假设和面试官交流。这样,面试官还是会觉得我们考虑问题很周到的。