题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
函数的声明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
这个题我的想法是这样的:
传统的做法是,从头结点开始,一直找到要删除结点的前趋结点,然后前趋结点的next指针直接指向要删除结点的后继,再free掉要删除的结点就OK了。当然,这个方法的时间复杂度是O(n)。
要在O(1)时间内删除某个结点,所以不能用传统的做法从头结点开始遍历链表找到要删除结点的前趋,而从要删除的结点出发也找不到其自身的前趋,这样要改变一种思路,就是不找前趋!
具体做法是这样的,把头结点的数据直接copy到要删除的结点处,然后头指针向后移动一个结点,再free掉原来的头指针指向的结点,这样等于把要删除的结点删除了。当链表只有一个结点或者要删除的结点是头结点或尾结点时,这种方法也是成立的,所以不需要做特殊的处理。大概的代码如下,没有测试过。
- void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted){
- if(NULL==pListHead||NULL==pToBeDeleted)
- return;
- pToBeDeleted->m_nKey = pListHead->m_nKey;
- pToBeDeleted = pListHead;
- pListHead = pListHead->m_pNext;
- free(pToBeDeleted);
- }
另外,原文中思路有一点不一样,原文作者还是想找到前趋,然后再删除结点。他的做法是,把要删除结点的后继结点的数据copy到要删除结点中,则后继结点就成了要删除的结点了,那么要删除的结点就变成真正要删除结点的前趋了,然后完成删除。不过这种思路要考虑到待删除结点是尾结点的特殊情况,因为尾结点是没有后继的。
两种方法比较起来,我觉得第一种是比较方便的,但是第二种方法可以保证原来链表的数据排序(如果需要的话)。各有好处吧。
下面就引用原文作者的做法。
=========== 以下内容引自原文===============
分析:这是一道广为流传的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)。
基于前面的分析,我们不难写出下面的代码。
参考代码:
///
// Delete a node in a list
// Input: pListHead - the head of list
// pToBeDeleted - the node to be deleted
///
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
if(!pListHead || !pToBeDeleted)
return;
// if pToBeDeleted is not the last node in the list
if(pToBeDeleted->m_pNext != NULL)
{
// copy data from the node next to pToBeDeleted
ListNode* pNext = pToBeDeleted->m_pNext;
pToBeDeleted->m_nKey = pNext->m_nKey;
pToBeDeleted->m_pNext = pNext->m_pNext;
// delete the node next to the pToBeDeleted
delete pNext;
pNext = NULL;
}
// if pToBeDeleted is the last node in the list
else
{
// get the node prior to pToBeDeleted
ListNode* pNode = pListHead;
while(pNode->m_pNext != pToBeDeleted)
{
pNode = pNode->m_pNext;
}
// deleted pToBeDeleted
pNode->m_pNext = NULL;
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的鲁棒性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。当然,在面试中,我们可以把这些假设和面试官交流。这样,面试官还是会觉得我们考虑问题很周到的。