一、 两两交换链表中的节点
题目链接:力扣
我的思路:
下面的代码也是经过我很多次调试检验以后才得到的一个不会报地址错误的代码。
//我觉得首先要判断链表结点数为奇数还是偶数 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: ListNode* swapPairs(ListNode* head) { dummyHead->next = head; int count = 1; ListNode* countpiont = head; while (countpiont->next != nullptr) { count++; countpiont = countpiont->next; } if (count % 2 == 0)//链表有偶数个结点 { //初始化 int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; cout << "val of back" << "=" << back->val<<endl; cout << "val of temp" << "=" << temp->val<<endl; for (int i = 0; i < loop; i++) { //交换指针_>没有问题 back->next = pre->next; pre->next->next = temp; pre->next = back; //打印交换后的结果 cout << "loop=" << i << endl; ListNode* temp1 = dummyHead; cout << "交换后的结果:" << endl; while (temp1->next!= nullptr) { temp1 = temp1->next; cout << temp1->val; } cout << endl; //结束一轮交换,指针往前走 pre = back; if (back->next->next != nullptr) { back = back->next->next->next; temp = temp->next->next; } } } else//链表有奇数个结点 { int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; for (int i = 0; i < loop; i++) { back->next = pre->next; pre->next = temp; pre->next = back; //结束一轮交换,指针往前走 pre = back; if (back->next->next != nullptr) { back = back->next->next; temp = temp->next->next; } } } return dummyHead->next; } ListNode* dummyHead = new ListNode(); }; int main() { std::cout << "Hello World!\n"; ListNode x1(1); ListNode x2(2); x1.next = &x2; ListNode x3(3); x2.next = &x3; ListNode x4(4); x3.next = &x4; ListNode x5(5); x4.next = &x5; ListNode x6(6); x5.next = &x6; x6.next = nullptr; ListNode* temp = &x1; while (temp != nullptr) { cout << temp->val; temp = temp->next; } cout << endl; Solution swap; ListNode* result; result=swap.swapPairs(&x1); temp = result; cout << "交换后的结果:"<<endl; while(temp->next!=nullptr){ cout << temp->val; temp = temp->next; } cout << endl; }
后来发现是因为第一次两两交换后,back指针指向的元素已经发生了变化,不能再依靠交换前的相对位置来判断指针移动到下一轮要交换的元素上要使用多少个->next!!所以后面几轮的交换结果才会那么诡异。
经过修改后的代码如下,基础的测试用例已经可以通过了:
class Solution { public: ListNode* swapPairs(ListNode* head) { dummyHead->next = head; //if (head == nullptr||head->next==nullptr) // { // return head; // } //加上上面这几行就是为了增加代码健壮性,可以通过一些特殊的测试用例 int count = 1; ListNode* countpoint = dummyHead; while (countpoint->next != nullptr) { count++; countpoint = countpoint->next; } if (count % 2 == 0)//链表有偶数个结点 { //初始化 int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; cout << "val of back" << "=" << back->val<<endl; cout << "val of temp" << "=" << temp->val<<endl; for (int i = 0; i < loop; i++) { //交换指针_>没有问题 back->next = pre->next; pre->next->next = temp; pre->next = back; //打印交换后的结果 cout << "loop=" << i << endl; cout << "检查本轮中pre指针的位置:" << pre->val << endl; cout << "检查本轮中back指针的位置:" << back->val << endl; ListNode* temp1 = dummyHead; cout << "交换后链表的输出结果:" << endl; while (temp1->next!= nullptr) { temp1 = temp1->next; cout << temp1->val; } cout << endl; //结束一轮交换,指针往前走 pre = back->next; if (back->next->next != nullptr) { back = back->next->next->next; temp = temp->next->next; } } } else//链表有奇数个结点 { //初始化 int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; cout << "val of back" << "=" << back->val << endl; cout << "val of temp" << "=" << temp->val << endl; for (int i = 0; i < loop; i++) { //交换指针_>没有问题 back->next = pre->next; pre->next->next = temp; pre->next = back; //打印交换后的结果 cout << "loop=" << i << endl; cout << "检查本轮中pre指针的位置:" << pre->val << endl; cout << "检查本轮中back指针的位置:" << back->val << endl; ListNode* temp1 = dummyHead; cout << "交换后链表的输出结果:" << endl; while (temp1->next != nullptr) { temp1 = temp1->next; cout << temp1->val; } cout << endl; //结束一轮交换,指针往前走 pre = back->next; if (back->next->next != nullptr) { back = back->next->next->next; temp = temp->next->next; } } } return dummyHead->next; } ListNode* dummyHead = new ListNode(0); };
最后调试与修改的要点:
-
count为奇数和偶数两种情况下,临界条件应该如何设置才能让最后一轮两两元素交换结束时back和temp指针不移动
-
考虑测试用例中输入为[]和[1]的情况,这两种情况都是不需要交换的情况,所以需要在一开始进行条件判断,如果时这两种情况直接return
经过修改,最后正确通过的题解:
class Solution { public: ListNode* swapPairs(ListNode* head) { dummyHead->next = head; if (head == nullptr || head->next == nullptr) { return head; } int count = 0; ListNode* countpoint = dummyHead; while (countpoint->next != nullptr) { count++; countpoint = countpoint->next; } if (count % 2 == 0)//链表有偶数个结点 { //初始化 int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; for (int i = 0; i < loop; i++) { //交换指针_>没有问题 back->next = pre->next; pre->next->next = temp; pre->next = back; //结束一轮交换,指针往前走 pre = back->next; if (temp != nullptr)//在最后一轮的时候如果back和temp还是继续往前走,那么就会非法访问没有定义的内存空间 { back = back->next->next->next; temp = temp->next->next; } } } else//链表有奇数个结点 { //初始化 int loop = count / 2; //ListNode *cur=head; ListNode* pre = dummyHead; ListNode* back = pre->next->next; ListNode* temp = back->next; for (int i = 0; i < loop; i++) { //交换指针_>没有问题 back->next = pre->next; pre->next->next = temp; pre->next = back; //结束一轮交换,指针往前走 pre = back->next; if (temp->next != nullptr) { back = back->next->next->next; temp = temp->next->next; } } } return dummyHead->next; } ListNode* dummyHead = new ListNode(0); };
从结果看效果还是很不错的。
心得&总结:
-
自己画图写解题思路的时候,一定不要为了图好看,就保留很多前一步的痕迹;该修改的指针指向就擦掉修改,不要想着留给自己之后看————》此刻的思路清晰才是最重要的!!
-
-
特殊情况自己还是考虑的太少了
-
临界条件确实可以最后敲定,但一定要记得去思考,别最后就忘记了
-
像这种需要多次交换(变换)的题目,一定要用第一次变换结束后的结果来考虑第二次变换的代码要怎么写!
卡子哥的写法(更简洁了):
class Solution { public: ListNode* swapPairs(ListNode* head) { ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作 ListNode* cur = dummyHead; while(cur->next != nullptr && cur->next->next != nullptr) { ListNode* tmp = cur->next; // 记录临时节点 ListNode* tmp1 = cur->next->next->next; // 记录临时节点 cur->next = cur->next->next; // 步骤一 cur->next->next = tmp; // 步骤二 cur->next->next->next = tmp1; // 步骤三 cur = cur->next->next; // cur移动两位,准备下一轮交换 } return dummyHead->next; } };
卡子哥的写法和我的写法思路上一样,不同的地方也就值得反思学习的地方:(重点)
-
这个代码没有对链表结点总数是奇数还是偶数进行分类——因为他的while条件是
cur->next != nullptr && cur->next->next != nullptr
,其实相等于把我上面的奇数情况和偶数情况的指针迭代停止条件给放到一起了——》当链表结点总数为偶数时,迭代的最后结果的cur满足cur->next==nullptr&&cur->next->next==未定义
;当链表结点总数为奇数时,迭代的最后结果的cur满足cur->next->next==nullptr
; -
这个代码简洁最主要的原因是每次迭代只维护一个cur指针的更新——》而其余两个指针都是在cur更新完后,根据与cur指针的位置关系来直接确定的
二、删除链表的倒数第N个节点
题目链接:Loading Question... - 力扣(LeetCode) :给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
我的思路:先算出链表的结点总数,然后用结点总数减去n,就是待删除结点从头节点开始数起的序号。
<!--tip:链表删除的常用操作----》要删除从正向开始数的x号结点,应该将指针指向序号为(x-1)的结点。-->
卡子哥的思路(真的很巧):定义一个快指针一个慢指针——》先让快指针向前移动(n+1)个单位,然后再让快慢指针一起同步向前移动,指针快指针到达边界——》这样很巧妙的让slow指针指向了待删除结点的前一个结点(也就是slow指针会指向倒数第n+1个结点);最巧的是这种计算倒数第n个结点的思路!!
class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode* dummyHead = new ListNode(0);//千万不要把dummyhead定义为一个类内元素 dummyHead->next = head; ListNode* slow = dummyHead; ListNode* fast = dummyHead; n++;// fast再提前走一步,因为需要让slow指向删除节点的上一个节点 while(n-- && fast != NULL) { fast = fast->next; } while (fast != NULL) { fast = fast->next; slow = slow->next; } ListNode *tmp = slow->next; C++释放内存的逻辑 slow->next = slow->next->next; delete tmp; return dummyHead->next;//返回head } };
三、 链表相交
题目链接:面试题 02.07. 链表相交 - 力扣(LeetCode)
我的思路:因为如果两个链表相交,那么分别为两个列表创造一个temp1、temp2指针沿着两个链表的头节点一直走下去暴力匹配(有点想排序算法里面的暴力排序算法的感觉),肯定可以找到两个指针指向的地址相同的时候。
卡子哥的思路(一开始有点难理解):
class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { ListNode* curA = headA; ListNode* curB = headB; int lenA = 0, lenB = 0; while (curA != NULL) { // 求链表A的长度 lenA++; curA = curA->next; } while (curB != NULL) { // 求链表B的长度 lenB++; curB = curB->next; } curA = headA; curB = headB; // 让curA为最长链表的头,lenA为其长度 if (lenB > lenA) { swap (lenA, lenB); swap (curA, curB); } // 求长度差 int gap = lenA - lenB; // 让curA和curB在同一起点上(末尾位置对齐) while (gap--) { curA = curA->next; } // 遍历curA 和 curB,遇到相同则直接返回 while (curA != NULL) { if (curA == curB) { return curA; } curA = curA->next; curB = curB->next; } return NULL; } };
问:为什么直接让curA移动到和curB对齐的位置了,链表A中跳过的那些元素都没有可能和链表B相交吗?
这个问题的原因是我对力扣题目里面的示例理解和记忆的不到位:
示例里面A链表和B链表的交点后面的结点值都完全一样!
四、环形链表II(难)
题目链接:力扣
我的思路:
1.定义temp=head,如果循环让head=head->next,最后head==nullptr,那么说明没有环
2.在循环让head=head->next的过程中标记经过的结点的地址,然后每next到一个新的结点,就将其与过去记录的结点地址进行比较,发现相同则说明有环。
卡子哥思路: 代码随想录 (programmercarl.com)
这道题涉及到数学计算!
1.判断有环的方法:
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
st和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
2.如果有环,找到这个环的入口的方法 (难点)
设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点再到环形入口节点所需经过的节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2(这是一个非常数学的关系,也是这一题的关键所在):
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点 到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z
,
这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
所以:在相遇节点处定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的:回顾公式:x = (n - 1) (y + z) + z
——》说明n>1时,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
整体代码:
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *detectCycle(ListNode *head) { ListNode* fast = head; ListNode* slow = head; while(fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 if (slow == fast) { ListNode* index1 = fast; ListNode* index2 = head; while (index1 != index2) { index1 = index1->next; index2 = index2->next; } return index2; // 返回环的入口 } } return NULL; } };
-
时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
-
空间复杂度: O(1)
补充:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
即文章链表:环找到了,那入口呢? (opens new window)中如下的地方:
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在slow开始走的那一环已经和fast相遇了。
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。
好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对链表:环找到了,那入口呢? (opens new window)的补充。
五、链表题目总结
1.力扣上全部的链表题目都没有给我写上虚拟头节点,但是链表的题目定义虚拟头节点是很有必要的(会非常省事),所以最好自己在链表的题目中都首先定义一个虚拟头节点。
2.自己以后在想对一个分类讨论的时候,先想想能不能自己认为添加一些东西让这些需要分类的情况可以归为一类、用相同的代码和方式去讨论——》刚开始写题的时候可以有思路就写,写完以后在想能不能简化;对于像”链表相交“那题的分类情况归一化(让链表A永远为长度最长的那个链表),我希望以后遇到的时候我可以在第一次写题的过程中就能捕捉到这个简便之处;对于像”两两交换链表结点“那道题里面,链表长度为奇数还是偶数的分类希望我在写完第一次后复盘时能够发现!
3.鉴于时间紧迫和是一刷这些题目,所以有些题目我可以先写下自己的初步思路和伪代码;然后就去看卡子哥的思路和代码,比较我们的思路和代码实现之间的异同。二刷的时候希望自己能独立回忆起每道题最妙的思路和一些可以合并、简化代码的小细节!然后二刷的时候自己纯上手敲!