参考
链表:
链表编程边界case检查:
如果链表为空时,代码是否能正常工作?
如果链表只包含一个结点时,代码是否能正常工作?
如果链表只包含两个结点时,代码是否能正常工作?
代码逻辑在处理头结点和尾结点的时候,代码是否能正常工作?
哨兵结点dummy node:
是虚拟出来的一个节点,不保存数据。
哨兵结点的next指针指向链表中的第一个节点。
对于哨兵结点,数据域可以不存储任何信息,也可存储如链表长度等附加信息。
哨兵结点不是链表所必需的。
头指针:
是指向第一个结点的指针。
如果链表没有引入哨兵结点,那么头指针指向的是链表的第一个结点。
如果有哨兵结点,头指针就指向哨兵结点。
头指针是链表所必需的。
哨兵结点解决什么问题:
(1)进行链表的删除、插入操作时,对第一个结点的操作更方便
如果链表没有哨兵结点,那么头指针指向的是链表的第一个结点,当在第一个结点前插入一个节点时,那么头指针要相应指向新插入的结点,把第一个结点删除时,头指针的指向也要更新。也就是说如果没有哨兵结点,我们需要维护着头指针的指向更新,因为头指针指向的是链表的第一个结点。(第一个节点变化,头指针也变化)
如果引入哨兵结点的话,那么哨兵结点的next始终都是链表的第一个结点。(哨兵结点保持不变,头指针也不变,只是哨兵结点的next指针变化)
(2)统一空表和非空表的处理
总结:
dummy node是在头结点可能有变化时使用
dummy node是链表问题中一个重要的技巧,中文翻译叫“哑节点”,使用通常针对单链表没有前向指针的问题,保证链表的 head不会在删除操作中丢失,
当链表的 head 有可能变化(被修改或者被删除)时,使用 dummy node 可以很好的简化代码,最终返回 dummy.next 即新的链表。
链表结构
// 详细版 struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; // 简单版 struct ListNode{ int val; ListNode *next; ListNode(): val(0), next(nullptr){} };
遍历链表
#include<iostream> using namespace std; struct ListNode{ int val; // ListNode *next; ListNode(): val(0), next(nullptr){} ListNode(int x): val(x), next(nullptr){} // 可忽略 ListNode(int x, ListNode* next): val(x), next(next){} // 可忽略 }; class solution{ public: }; int main(){ // 生成各个节点并连接 ListNode a(10); ListNode b(20); ListNode c(30); ListNode d(40); ListNode e(50); a.next = &b; b.next = &c; c.next = &d; d.next = &e; e.next = nullptr; // 生成头节点 ListNode *head = &a; // 遍历方法1:while循环 while(head){ cout<< head->val << endl; head = head->next; } /* // 遍历方法2:for循环 for (ListNode* p = head; p != null; p = p->next) { // 迭代访问 p->val } */ return 0; };
在函数一开始就经常要写的边界条件
if (!head || !head->next) return head; // 空节点或者只有一个节点
函数参数:一般输入链表头结点,返回头结点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { }
哨兵结点的使用(在链表的题目中,十道有九道会用到哨兵节点)
ListNode* dummy = new ListNode(); dummy->next = head; ListNode* pre = dummy; // 指向当前组的前一个节点 ListNode new_node(0); ListNode* tmp = &new_node;
class Solution { public: vector<int> printListFromTailToHead(ListNode* head) { if(head == nullptr) return vector<int> (); vector<int> result; while(head){ result.push_back(head->val); head = head -> next; } reverse(result.begin(), result.end()); return result; } };
class Solution { public: // 从尾到头问题,可以尝试用栈解决 // 有三种思路 // 思路1:利用栈先入后出的特性完成 // 思路2:存下来然后进行数组翻转 // 思路3:利用递归 vector<int> reversePrint(ListNode* head) { vector<int> result; stack<int> stk; while(head){ stk.push(head->val); head = head -> next; } while(!stk.empty()){ result.push_back(stk.top()); stk.pop(); } return result; } };
class Solution { public: ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { // 保存进位 int tmp = 0; ListNode *l3 = new ListNode(0); // 哨兵结点 ListNode *p = l3; // 节点有值或有进位都可以继续相加 while(l1 != nullptr || l2 !=nullptr || tmp !=0) { // 取两个加数 int l1val = l1 !=nullptr ? l1->val : 0; int l2val = l2 !=nullptr ? l2->val : 0; // 与进位相加 int sumVal = l1val + l2val + tmp; // 根据当前和结果得到下一位的进位,大于10有进位,小于10没有进位 tmp = sumVal / 10; // 取除数 // 根据当前和结果得到和节点值 p->next = new ListNode(sumVal % 10); // 取余数 // 更新指针 p = p->next; if (l1 != nullptr) l1 = l1->next; if (l2 != nullptr) l2 = l2->next; } return l3->next; } };
class Solution { public: // 给定单向链表的头指针和一个要删除的节点的值,删除该节点 // 使用双指针,一个遍历指针,一个遍历指针的前一个指针 ListNode* deleteNode(ListNode* head, int val) { // 如果要删除的是头结点,单独处理 if(head->val == val) return head->next; // 两个游标,一个定位删除节点的前一个节点,一个定位删除节点 ListNode *pre = head, *cur = head->next; // 遍历链表找到要删除的结点 while(cur != nullptr && cur->val!=val){ pre = cur; cur = cur->next; } // 找到要删除的节点后,将前后节点拼接 if(cur != nullptr) pre->next = cur->next; return head; } };
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) :val(x), next(NULL) {} }; */ class Solution { public: ListNode* deleteDuplication(ListNode* pHead) { if(pHead == nullptr || pHead->next == nullptr) return pHead; // 建立哨兵结点 ListNode* dummy = new ListNode(0); dummy->next = pHead; // 设立两个游标: pre和cur指针,pre指针指向当前确定不重复的那个节点,而cur指针相当于工作指针,一直往后面搜索。 ListNode* pre = dummy; ListNode* cur = dummy->next; // cur是真正工作的指针 while(cur){ // 找到重复的起始节点 if(cur->next != nullptr && cur->val == cur->next->val){ // 通过循环从重复的起始节点开始找到重复的倒数第一个节点(方便拼接) while(cur->next !=nullptr && cur->val == cur->next->val){ cur = cur->next; } // 找到倒一重复节点后,将前面不重复节点与倒一重复节点后面的节点拼接 pre->next = cur->next; cur = cur->next; // cur从倒一节点后面的节点重新出发 } // 当前节点不是重复节点 else{ pre = pre->next; cur = cur->next; } } return dummy->next; } };
class Solution { public: ListNode* deleteDuplication(ListNode* pHead) { if(pHead == nullptr || pHead->next == nullptr) return pHead; // 建立哨兵结点 ListNode* dummy = new ListNode(0); dummy->next = pHead; // 设立两个游标: pre和cur指针,pre指针指向当前确定不重复的那个节点,而cur指针相当于工作指针,一直往后面搜索。 ListNode* pre = dummy; ListNode* cur = dummy->next; // cur是真正工作的指针 while(cur){ // 找到重复的起始节点 if(cur->next != nullptr && cur->val == cur->next->val){ // 通过循环从重复的起始节点开始找到重复的倒数第一个节点(方便拼接) while(cur->next !=nullptr && cur->val == cur->next->val){ cur = cur->next; } // 找到倒一重复节点后,将前面不重复节点与倒一重复节点拼接 pre->next = cur; // 更新游标 pre = pre->next; cur = cur->next; // cur从倒一节点后面的节点重新出发 } // 当前节点不是重复节点 else{ pre = pre->next; cur = cur->next; } } return dummy->next; } };
class Solution { public: ListNode* getKthFromEnd(ListNode* head, int k) { // 两种思路 // 思路1:利用快慢指针 // 思路2:利用辅助栈 // 这里用快慢指针:让快指针先走k步,然后快、慢指针开始同速前进 // 当快指针走到链表末尾null时,慢指针所在的位置就是倒数第k个链表节点 ListNode* slow = head; ListNode* fast = head; while(k--){ fast = fast->next; } while(fast){ fast = fast->next; slow = slow->next; } return slow; } };
class Solution { public: // 使用头插法 // 三个指针:一个指向旧链表的头结点,一个指向当前要反转的节点head,一个指向新链表的头节点 ListNode* reverseList(ListNode* head) { if (!head || !head->next) return head; // 空节点或者只有一个节点 // head为工作指针 // new_head为新链表头结点指针 ListNode* new_head = nullptr; // old_head为旧链表头结点指针 ListNode* old_head = head; while(head){ old_head = old_head->next; // 旧链表头节点重新指向 head->next = new_head; // 当前遍历节点指向新链表头节点 new_head = head; // 新链表头节点重新指向 head = old_head; // 更新当前节点 } return new_head; } };
反转从位置 left 到 right 的链表。请使用一趟扫描完成反转。
class Solution { public: ListNode* reverseBetween(ListNode* head, int left, int right) { // 哨兵结点:因为头结点可能变化 ListNode *dummy = new ListNode(0); dummy->next = head; // pre用于寻找待反转节点的前一个节点 ListNode *pre = dummy; // 找到待反转节点的前一个节点 for (int i=1; i<left; ++i) // 题目称位置0元素为第1个节点。如m=1即第1个节点为反转起点,则无需循环 pre=pre->next; // 待反转起点 ListNode *curr = pre->next; // 当前工作指针 ListNode *old_head = pre->next; // 旧反转链表头 ListNode *new_head = nullptr; // 新已反转链表头 // 开始反转 for (int i = left; i<right+1; ++i) // 从第m开始到n,因为n+1停止 { old_head = old_head->next; // 旧头重新指向 curr->next = new_head; // 当前反转节点连接新链表 new_head = curr; // 新头重新指向 curr = old_head; // 更新工作指针 } // 将初始反转起点的next指向curr pre->next->next = curr; // 将反转起点的前一个节点指向反转链表的头结点 pre->next = new_head; return dummy->next; } };
class Solution { public: ListNode* reverseBetween(ListNode* head, int left, int right) { if(!head || !head->next) return head; // 链表没有节点或只有一个节点 int length = right-left+1; // 待反转部分链表的长度 ListNode* pre_head = nullptr; // 待反转部分链表的前一个节点 ListNode* old_head = head; // 待反转链表的头节点(旧部分链表) ListNode* new_head = nullptr; // 反转链表反转后的头节点(新部分链表) // 先找到开始反转的节点 while(--left){ pre_head = old_head; old_head = old_head->next; } ListNode* tmp = old_head; // 工作指针,指向待反转的节点 // 开始反转 while(length--){ old_head = old_head->next; tmp->next = new_head; new_head = tmp; tmp = old_head; } // 将反转后的部分链表与原链表的前后部分连接起来 if(pre_head){ // 不是从头节点开始反转 pre_head->next->next = old_head; pre_head->next = new_head; } else{ // 从头节点开始反转 head->next = old_head; head = new_head; // 整个链表头节点发生改变 } return head; } };
class Solution { public: ListNode* reverseKGroup(ListNode* head, int k) { // 先算链表长度 // 然后将链表长度/k 得到翻转的组数 // 再分别对每一组进行翻转 // 先创建一个临时头节点(因为原来的头节点要变) ListNode* dummy = new ListNode(); dummy->next = head; // 计算链表长度 int len = 0; while(head){ len++; head = head->next; } if(len<k) return dummy->next; // 链表长度小于分组长度时,无需反转 ListNode* pre = dummy; // 指向当前组的前一个节点 ListNode* old_head = dummy->next; head = dummy->next; // 遍历每一组 for(int i=0; i<len/k; ++i){ // 对该组节点翻转 ListNode* new_head = nullptr; // 新链表头结点 for(int j=0; j<k; ++j){ old_head = old_head->next; head->next = new_head; // head为工作指针 new_head = head; head = old_head; } pre->next->next = old_head; // 将该组初始反转节点即反转后末节点连接到下一个组的第一个节点 ListNode* tmp = pre->next; // 临时记录反转后末节点 pre->next = new_head; // 将当前组的前一个节点指向反转后头结点 pre = tmp; // 更新为反转后末节点:反转前第一个节点,下一个组的前一个节点 } return dummy->next; } };
class Solution { public: ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { // 返回的是一个全新的链表,所以创建哨兵结点(临时头节点) ListNode* dummy = new ListNode(0); // 创建该全新链表的游标指针 ListNode* tmp = dummy; // l1和l2分别为两个待排序链表的游标指针 while(l1 && l2){ if(l1->val < l2->val){ tmp->next = l1; l1 = l1->next; } else{ tmp->next = l2; l2 = l2->next; } tmp = tmp->next; // 记得更新新链表当前节点 } if(l1){ // 如果l1有剩余,接到tmp后 tmp->next = l1; } if(l2){ // 如果l2有剩余,接到tmp后 tmp->next = l2; } return dummy->next; } };
class Solution { public: // A和B同时从自身链表出发,遍历完自己的就遍历对方的,A到B的共同节点和B到A的共同节点所花费的步数是一样的 ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { ListNode* A = headA; ListNode* B = headB; while(A != B){ A = A != nullptr ? A->next:headB; B = B != nullptr ? B->next:headA; } return A; } };
class Solution { public: bool hasCycle(ListNode *head) { // 环形链表一般用快慢指针解决 // 慢指针slow每次走一步 // 快指针fast每次走两步 // 这样,如果链表有环,则它们必定在环内相遇(这是一个定理) ListNode* slow = head; ListNode* fast = head; while(fast && fast->next){ // 如果链表没有结点或者有尾结点(指向NULL),则链表肯定没有环,从而跳出循环 // 慢指针走一步 slow = slow->next; // 快指针走两步 fast = fast->next->next; // 如果存在环,则快慢指针必然相遇 if(slow == fast){ return true; } } // 链表没有环 return false; } };
class Solution { public: ListNode *detectCycle(ListNode *head) { if(head == nullptr || head->next == nullptr) return nullptr; unordered_map<ListNode*, int> unmp; while(head){ unmp[head]++; if(unmp[head] == 2) return head; head = head->next; } /* while(head){ if(unmp.find(head) != unmp.end()) return head; unmp[head]++; head = head->next; } */ return nullptr; } };
//先说个定理:两个指针一个fast、一个slow同时从一个链表的头部出发 //fast一次走2步,slow一次走一步,如果该链表有环,两个指针必然在环内相遇 //此时只需要把其中的一个指针重新指向链表头部,另一个不变(还在环内), //这次两个指针一次走一步,相遇的地方就是入口节点。 class Solution { public: ListNode *detectCycle(ListNode *head) { if(head == nullptr || head->next == nullptr) return nullptr; ListNode* slow = head; ListNode* fast = head; while(fast && fast->next){ slow = slow->next; fast = fast->next->next; if(slow == fast) break; } // 当快指针只是到达链尾退出循环时 if(fast == nullptr || fast->next == nullptr) return nullptr; slow = head; while(fast != slow){ fast = fast->next; slow = slow->next; } return fast; } };