<剑指OFFER18> 18_01_DeleteNodeInList在O(1)时间删除链表结点
https://blog.csdn.net/shen_jz2012/article/details/50631317
// 面试题18(一):在O(1)时间删除链表结点
// 题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该
// 结点。
#include<cstdio> struct ListNode { int m_nValue; ListNode* m_pNext; }; void DeleteNode(ListNode** pListHead, ListNode* pToBeDeleted); void DeleteNode(ListNode** pListHead, ListNode* pToBeDeleted) { //头指针和要删除节点不为空 if (!pListHead || !pToBeDeleted) { return; } else { // 要删除的节点不是尾节点,可以是头节点,链表有多个元素 if (pToBeDeleted->m_pNext != nullptr) { ListNode* pNew = pToBeDeleted->m_pNext; pToBeDeleted->m_nValue = pNew->m_nValue; pToBeDeleted->m_pNext = pNew->m_pNext; delete pNew; pNew = nullptr; } // 链表只有一个节点,删除头节点(也是尾节点) // 进入这个条件说明 pToBeDeleted->m_pNext == nullptr,是尾节点 // 如果 *pListHead == pToBeDeleted 说明链表只有一个节点 else if (*pListHead == pToBeDeleted) { delete pToBeDeleted; pToBeDeleted = nullptr; *pListHead = nullptr; } //链表有多个节点,删除尾节点 else { ListNode *pNode = *pListHead; while (pNode->m_pNext != pToBeDeleted) { pNode = pNode->m_pNext; } pNode->m_pNext == nullptr; delete pToBeDeleted; pToBeDeleted = nullptr; } // 要删除的节点是尾节点 } }
/******************************************************************* Copyright(c) 2016, Harry He All rights reserved. Distributed under the BSD license. (See accompanying file LICENSE.txt at https://github.com/zhedahht/CodingInterviewChinese2/blob/master/LICENSE.txt) *******************************************************************/ //================================================================== // 《剑指Offer——名企面试官精讲典型编程题》代码 // 作者:何海涛 //================================================================== // 面试题18(一):在O(1)时间删除链表结点 // 题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该 // 结点。 #include <cstdio> #include "..\Utilities\List.h" void DeleteNode(ListNode** pListHead, ListNode* pToBeDeleted) { if(!pListHead || !pToBeDeleted) return; // 要删除的结点不是尾结点 if(pToBeDeleted->m_pNext != nullptr) { ListNode* pNext = pToBeDeleted->m_pNext; pToBeDeleted->m_nValue = pNext->m_nValue; pToBeDeleted->m_pNext = pNext->m_pNext; delete pNext; pNext = nullptr; } // 链表只有一个结点,删除头结点(也是尾结点) else if(*pListHead == pToBeDeleted) { delete pToBeDeleted; pToBeDeleted = nullptr; *pListHead = nullptr; } // 链表中有多个结点,删除尾结点 else { ListNode* pNode = *pListHead; while(pNode->m_pNext != pToBeDeleted) { pNode = pNode->m_pNext; } pNode->m_pNext = nullptr; delete pToBeDeleted; pToBeDeleted = nullptr; } } // ====================测试代码==================== void Test(ListNode* pListHead, ListNode* pNode) { printf("The original list is: \n"); PrintList(pListHead); printf("The node to be deleted is: \n"); PrintListNode(pNode); DeleteNode(&pListHead, pNode); printf("The result list is: \n"); PrintList(pListHead); } // 链表中有多个结点,删除中间的结点 void Test1() { ListNode* pNode1 = CreateListNode(1); ListNode* pNode2 = CreateListNode(2); ListNode* pNode3 = CreateListNode(3); ListNode* pNode4 = CreateListNode(4); ListNode* pNode5 = CreateListNode(5); ConnectListNodes(pNode1, pNode2); ConnectListNodes(pNode2, pNode3); ConnectListNodes(pNode3, pNode4); ConnectListNodes(pNode4, pNode5); Test(pNode1, pNode3); DestroyList(pNode1); } // 链表中有多个结点,删除尾结点 void Test2() { ListNode* pNode1 = CreateListNode(1); ListNode* pNode2 = CreateListNode(2); ListNode* pNode3 = CreateListNode(3); ListNode* pNode4 = CreateListNode(4); ListNode* pNode5 = CreateListNode(5); ConnectListNodes(pNode1, pNode2); ConnectListNodes(pNode2, pNode3); ConnectListNodes(pNode3, pNode4); ConnectListNodes(pNode4, pNode5); Test(pNode1, pNode5); DestroyList(pNode1); } // 链表中有多个结点,删除头结点 void Test3() { ListNode* pNode1 = CreateListNode(1); ListNode* pNode2 = CreateListNode(2); ListNode* pNode3 = CreateListNode(3); ListNode* pNode4 = CreateListNode(4); ListNode* pNode5 = CreateListNode(5); ConnectListNodes(pNode1, pNode2); ConnectListNodes(pNode2, pNode3); ConnectListNodes(pNode3, pNode4); ConnectListNodes(pNode4, pNode5); Test(pNode1, pNode1); DestroyList(pNode1); } // 链表中只有一个结点,删除头结点 void Test4() { ListNode* pNode1 = CreateListNode(1); Test(pNode1, pNode1); } // 链表为空 void Test5() { Test(nullptr, nullptr); } int main(int argc, char* argv[]) { Test1(); //Test2(); //Test3(); //Test4(); //Test5(); return 0; }
#if 0 /*正确写法,指向指针的指针*/ /* 其实真的很好理解,既然你懂得函数中的值传参,假设int a,作为参数传入的时候没被修改, 所以需要用指向a的指针,那么应该也可以理解,指针变量pHead作为参数传入的时候被修改无效, 因此需要用指向pHead的指针,只不过pHead本身就是一个指针了, 所以才存在有指针的指针看起来稍微复杂一点的说法。 因为,指向a的指针作为参数传入进去时,如果你对它进行修改,其实也是无效的, 但是修改指针指向的内容的修改是有效的,也即,(&a)对a取地址得到的指针传入进去之后, 此时你修改这个指针也是没有什么实际作用的,原因我等下会说。 但是,你修改指针指向的内容这就有效了,因此通常我们在函数体内是修改对指针取内容后的内存, 即*(&a)。所以,你对指针pHead的修改时无效的,只有对指向pHead的指针指向的内容(很绕吧,其实就是pHead), 这时候才是有效的,因此AddToTail的第一个参数必须用指针的指针。 现在来说说为什么对值传参在函数体内的修改无效。因为a传进去的时候会被复制了一份copy, 此后的修改都是在临时变量copy上,出了函数体copy被销毁,a还是原来的a, 根本就没被修改过,所以才会值传参对变量的修改无效。要使得对a的修改有效, 一方面是传入a的地址,也就是对指向a的指针作为值传参(反正修改的不是a的指针, 修改了也无所谓,反正只是修改a的指针的copy),此时a的指针的copy指向的内容也是a, 因此对copy指向的内容修改会导致a的内容也被修改,check!另外一种方式就是引用传参, 引用传参往往要比值传参高效,因为它是直接将a作为参数传入进去,而少了对a进行复制这部分的开销, 既然传入进去的是a,那么对a的修改肯定也生效。 */ #include<iostream> #include<string> using namespace std; struct ListNode { int val; ListNode* next; }; /*pHead必须定义成指向指针的指针,形参传进去的时候会被复制一份copy,此后的修改 都是在临时变量的copy上,出了函数体copy被销毁,传进去的形参还是原来的数,根本就 没被修改过,所以值传参对变量修改无效。要使得对传进去参数修改有效,有两种方法 1。传入值得地址 2. 引用传参,没有copy这部分的开销 */ void AddToTail(ListNode** pHead, int value); int main() { ListNode* head = NULL; AddToTail(&head, 10); if (head != NULL) { cout << head->val << endl; } else { cout << "head is NULL.." << endl; } } void AddToTail(ListNode** pHead, int value) { ListNode* pNew = new ListNode(); pNew->val = value; pNew->next = NULL; if (*pHead == NULL) { *pHead = pNew; } else { ListNode* p = *pHead; while (p->next != NULL) { p = p->next; } p->next = pNew; } } #endif #if 0 /*错误写法 如果pHead定义成ListNode* ,则指针pHead的值不会被修改 就像swap(a,b),如果直接传值进去,a,b值交换无效 必须得交换swap(&a,&b),地址是真实的,可以修改地址里的内容,达到交换a,b的目的 但是如果函数里改变,&a,&b则没有意义,形参出了函数就失效了,什么都没变 */ #include<iostream> #include<string> using namespace std; struct ListNode { int val; ListNode* next; }; void AddToTail(ListNode* pHead, int value); int main() { ListNode* head = NULL; AddToTail(head, 10); if (head != NULL) { cout << head->val << endl; } else { cout << "head is NULL.." << endl; } } void AddToTail(ListNode* pHead, int value) { ListNode* pNew = new ListNode(); pNew->val = value; pNew->next = NULL; if (pHead == NULL) { pHead = pNew; } else { ListNode* p = pHead; while (p->next != NULL) { p = p->next; } p->next = pNew; } } #endif #if 1 /* 正确写法:引用传参 */ #include<iostream> #include<string> using namespace std; struct ListNode { int val; ListNode* next; }; void AddToTail(ListNode* &pHead, int value); int main() { ListNode* head = NULL; AddToTail(head, 10); if (head != NULL) { cout << head->val << endl; } else { cout << "head is NULL.." << endl; } } void AddToTail(ListNode* &pHead, int value) { ListNode* pNew = new ListNode(); pNew->val = value; pNew->next = NULL; if (pHead == NULL) { pHead = pNew; } else { ListNode* p = pHead; while (p->next != NULL) { p = p->next; } p->next = pNew; } } #endif
在看书的时候有个往链表里添加节点的函数,代码中考虑到可能给出的头指针为空,并做另外一些处理。具体代码如下:
#include <iostream>
#include <string>
using namespace std;
struct ListNode
{
int val;
ListNode* next;
};
void AddToTail(ListNode** pHead, int value);
int main() {
// TODO
}
void AddToTail(ListNode** pHead, int value) {
ListNode* pNew = new ListNode();
pNew->val = value;
pNew->next = NULL;
if (*pHead == NULL) {
*pHead = pNew;
}
else {
ListNode* p = *pHead;
while (p->next != NULL) {
p = p->next;
}
p->next = pNew;
}
}
网上其他人的博客中对函数AddToTail的参数的描述跟书中如出一辙:第一个参数pHead是一个指向指针的指针,当向一个空链表插入一个节点时,新插入的节点是链表的头指针,此时会改动头指针,因此必须把pHead参数设置为指向指针的指针。
为什么呢?在以前学习C++的时候,我们只知道在参数中,以传值的形式作为参数的变量在函数体内被修改之后,出了函数体就会失效,准确的说这个变量没有被修改过,因此需要传入该变量的指针或者使用引用传参的方式。可是上述AddToTail中已经是一个指针了啊?于是我测试了一下,不使用指针的指针会怎样:
#include <iostream>
#include <string>
using namespace std;
struct ListNode
{
int val;
ListNode* next;
};
void AddToTail(ListNode* pHead, int value);
int main() {
// TODO
ListNode* head = NULL;
AddToTail(head, 10);
if (head != NULL) {
cout << head->val << endl;
}
else {
cout << "head is NULL.." << endl;
}
}
void AddToTail(ListNode* pHead, int value) {
ListNode* pNew = new ListNode();
pNew->val = value;
pNew->next = NULL;
if (pHead == NULL) {
pHead = pNew;
}
else {
ListNode* p = pHead;
while (p->next != NULL) {
p = p->next;
}
p->next = pNew;
}
}
运行结果如下
作为指针pHead竟然真的没被修改过!
其实真的很好理解,既然你懂得函数中的值传参,假设int a,作为参数传入的时候没被修改,所以需要用指向a的指针,那么应该也可以理解,指针变量pHead作为参数传入的时候被修改无效,因此需要用指向pHead的指针,只不过pHead本身就是一个指针了,所以才存在有指针的指针看起来稍微复杂一点的说法。因为,指向a的指针作为参数传入进去时,如果你对它进行修改,其实也是无效的,但是修改指针指向的内容的修改是有效的,也即,(&a)对a取地址得到的指针传入进去之后,此时你修改这个指针也是没有什么实际作用的,原因我等下会说。但是,你修改指针指向的内容这就有效了,因此通常我们在函数体内是修改对指针取内容后的内存,即*(&a)。所以,你对指针pHead的修改时无效的,只有对指向pHead的指针指向的内容(很绕吧,其实就是pHead),这时候才是有效的,因此AddToTail的第一个参数必须用指针的指针。
现在来说说为什么对值传参在函数体内的修改无效。因为a传进去的时候会被复制了一份copy,此后的修改都是在临时变量copy上,出了函数体copy被销毁,a还是原来的a,根本就没被修改过,所以才会值传参对变量的修改无效。要使得对a的修改有效,一方面是传入a的地址,也就是对指向a的指针作为值传参(反正修改的不是a的指针,修改了也无所谓,反正只是修改a的指针的copy),此时a的指针的copy指向的内容也是a,因此对copy指向的内容修改会导致a的内容也被修改,check!另外一种方式就是引用传参,引用传参往往要比值传参高效,因为它是直接将a作为参数传入进去,而少了对a进行复制这部分的开销,既然传入进去的是a,那么对a的修改肯定也生效。
为了证明上述废话,我将代码2中的AddToTail函数的第一个参数也作为引用参数传入(指向指针的指针肯定正确啦,就不测试了),此时预测的结果是修改有效。代码如下:
#include <iostream>
#include <string>
using namespace std;
struct ListNode
{
int val;
ListNode* next;
};
void AddToTail(ListNode* &pHead, int value);
int main() {
// TODO
ListNode* head = NULL;
AddToTail(head, 10);
if (head != NULL) {
cout << head->val << endl;
}
else {
cout << "head is NULL.." << endl;
}
}
void AddToTail(ListNode* &pHead, int value) {
ListNode* pNew = new ListNode();
pNew->val = value;
pNew->next = NULL;
if (pHead == NULL) {
pHead = pNew;
}
else {
ListNode* p = pHead;
while (p->next != NULL) {
p = p->next;
}
p->next = pNew;
}
}
只是简单的在代码2中的函数声明和定义中,第一个参数加入了"&"表示使用一个引用参数,结果如下图,check!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix