11.7 LeetCode 题目汇总与解题思路
链表算法
第一题:移除链表元素
初版尝试(哨兵节点法)
class Solution {
public:ListNode* removeElements(ListNode* head, int val) {// 学会了防御性编程if (head != nullptr) {cout << "head->val: " << head->val << endl;} else {cout << "head是空指针" << endl;}// 掌握了哨兵节点的妙用ListNode dummy(0, head);auto cur = &dummy; // 理解了对象地址转为指针while (cur->next) {auto nextnode = cur->next;if (nextnode->val == val) {cur->next = nextnode->next;delete nextnode;} else {cur = nextnode;}}return dummy.next;}
};
核心收获:哨兵节点让边界处理变得统一,auto cur = &dummy 实现了对象到指针的转换,统一了操作语法。
优化版本(双循环法)
class Solution {
public:ListNode* removeElements(ListNode* head, int val) {// 先处理头节点连续等于val的情况while (head != nullptr && head->val == val) {ListNode* tmp = head;head = head->next;delete tmp;}// 学会了正确的判空顺序ListNode* cur = head;while (cur != nullptr && cur->next != nullptr) {ListNode* nextnode = cur->next;if (nextnode->val == val) {cur->next = nextnode->next;delete nextnode;} else {cur = nextnode;}}return head;}
};
关键突破:理解了短路求值的重要性,cur != nullptr && cur->next != nullptr 的顺序防止了空指针访问。
第二题:设计链表
初版问题分析
我的初始版本存在多个严重问题:
- 头插法函数名混乱 (
myLinkedListAddAtobj) - 尾插法死循环 (
while (cur->next != NULL) { cur->next = newNode; }) - 索引计算错误(没有正确区分头节点和真实节点)
修正后的完整实现
typedef struct MyLinkedList {int val;struct MyLinkedList* next;
} MyLinkedList;MyLinkedList* myLinkedListCreate() {MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList));obj->val = 0;obj->next = NULL;return obj;
}int myLinkedListGet(MyLinkedList* obj, int index) {if (index < 0 || obj->next == NULL) return -1;MyLinkedList* cur = obj->next; // 关键:从第一个真实节点开始int tag = 0;while (cur != NULL) {if (tag == index) return cur->val;cur = cur->next;tag++;}return -1;
}void myLinkedListAddAtHead(MyLinkedList* obj, int val) {MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));newNode->val = val;newNode->next = obj->next; // 新节点指向原第一个节点obj->next = newNode; // 头节点指向新节点
}void myLinkedListAddAtTail(MyLinkedList* obj, int val) {MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));newNode->val = val;newNode->next = NULL;MyLinkedList* cur = obj;while (cur->next != NULL) {cur = cur->next; // 关键:移动指针,不是赋值}cur->next = newNode;
}
重要领悟:
- 头节点只是辅助,真实数据从
obj->next开始 - 遍历时要移动指针 (
cur = cur->next),不是修改指针指向 - 所有操作前都要检查边界条件
第三题:反转链表
我的错误尝试
// 错误代码:混淆了对象和指针
ListNode tail = ListNode(cur->val); // 对象
tail = dummy.next; // 这根本不work!
正确解法(头插法)
class Solution {
public:ListNode* reverseList(ListNode* head) {if (head == nullptr) return head;ListNode dummy(0);ListNode* cur = head;while (cur != nullptr) {// 正确创建指针节点ListNode* newNode = new ListNode(cur->val, dummy.next);dummy.next = newNode;cur = cur->next;}return dummy.next;}
};
更优解法(原地反转)
class Solution {
public:ListNode* reverseList(ListNode* head) {ListNode* prev = nullptr;ListNode* cur = head;while (cur != nullptr) {ListNode* nextTemp = cur->next;cur->next = prev; // 反转的核心prev = cur;cur = nextTemp;}return prev;}
};
核心收获:
new ListNode()返回指针,ListNode()创建对象- 原地反转的空间复杂度O(1)更优
- 三指针技巧是反转链表的经典模式
总结:我的链表学习过程
思维转变
- 从盲目自信到防御性编程:现在本能地先判空
- 从语法混淆到清晰区分:对象用
.,指针用-> - 从边界崩溃到全面考虑:学会用哨兵节点统一处理
技术掌握
- 哨兵节点的应用场景
- 指针操作的正确语法
- 链表遍历的边界处理
- 内存管理的注意事项
- 多种解法的比较选择
心路历程
从一开始的"这代码应该work啊"到现在的"让我先检查边界情况",这种思维转变是最大的收获。链表问题需要耐心和细心,但一旦掌握了核心模式,就发现它们都有相似的解题模板。
现在的我面对链表问题时更加从容,知道如何系统化分析问题、处理边界、选择最优解法。这种成长比单纯AC题目更有价值!
