LeetCode24 两两交换链表中的节点
题目链接:25.两两交换链表中的节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* swapPairs(struct ListNode* head) {
struct ListNode *DummyHead;//定义虚拟头结点,并且必须申请空间
DummyHead = (struct ListNode *)malloc(sizeof(struct ListNode));
DummyHead->next = head;//初始话虚拟头结点,使其连接到原链表上
struct ListNode *curp = DummyHead;
while(curp->next && curp->next->next){
struct ListNode *temp = curp->next->next; //记录2 //临时指针变量 记录第二个有效节点地址
curp->next->next = curp->next->next->next; //1->3 //将第一个有效节点对的指向第三个有效节点
temp->next = curp->next; //2->1 //第二个有效节点指向原来的第一个有效节点
curp->next = temp; //0->2 //第一个有效节点的前一个节点的指向第二个有效节点
//以上交换完毕
curp = curp->next->next;//继续向后遍历
}
head = DummyHead->next;
free(DummyHead);
return head;
}
上面那个是我根据自己的理解写的代码,没有什么特别固定的思路,所以在解题时很慢,也容易出很多错,跟Carl哥学完之后,对这种类型的思路立马有了比较清楚的理解,大体过过程是这样的(模拟,假设是第一次交换,0代表虚拟头结点):
0->2 2->1 1->3 curp = 1 (原链表的 1 实际上交换之后的2)
struct ListNode* swapPairs(struct ListNode* head) {
struct ListNode *DummyHead, *curp;
DummyHead = (struct ListNode *)malloc(sizeof(struct ListNode));
DummyHead->next = head;
curp = DummyHead;
while(curp->next && curp->next->next){
struct ListNode *temp1 = curp->next;//记录第一个有效节点地址
struct ListNode *temp2 = curp->next->next->next;//记录第三个有效节点
curp->next = curp->next->next; //0->2
curp->next->next = temp1; //2->1
curp->next->next->next = temp2; /*1->3 或者这一语句可以替换为这个:temp1->next = temp2;(本质上都是让1->3)*/
curp = temp1;
}
head = DummyHead->next;
free(DummyHead);
return head;
}
通过设置虚拟指针和临时变量储存当前循环中的第二个有效值来衔接使链表中的数据不丢失。这是通过迭代法实现的。下面是递归版本解题方法:
struct ListNode* swapPairs(struct ListNode* head){
//递归结束条件:头节点不存在或头节点的下一个节点不存在。此时不需要交换,直接返回head
if(!head || !head->next)
return head;
//创建一个节点指针类型保存头结点下一个节点
struct ListNode *newHead = head->next;
//更改头结点+2位节点后的值,并将头结点的next指针指向这个更改过的list
head->next = swapPairs(newHead->next);
//将新的头结点的next指针指向老的头节点
newHead->next = head;
return newHead;
}
LeetCode19 删除链表的倒数第N个节点
题目链接:19.删除链表中的倒数第N个节点
我现在发现,很多题目,简单的用暴力解法都能AC,但是现在追求的就是用更优质的解法,降低时间复杂度,降低空间复杂度。
暴力解法的代码(时间复杂度为 O(n * n)):
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
//原链表为空
if(head == NULL) return head;
//构建虚拟头结点
struct ListNode *p;
p = (struct ListNode *)malloc(sizeof(struct ListNode));
p->next = head;
//遍历链表,记录链表节点个数
struct ListNode *curp = p;
int cnt = 0;
while(curp->next != NULL){
cnt++;
curp = curp->next;
}
//个数小于n
if(cnt < n) return head;
//不小于n
curp = p;
cnt -= n;
while(cnt-- && curp->next != NULL){
curp = curp->next;
}
curp->next = curp->next->next;
head = p->next;
free(p);
return head;
}
看到思路之后,真的没想到这个题竟然可以用双指针的算法求解,这样看来,很多看似只能用暴力法求解的问题,都可以尝试着使用双指针法求解。
首先讲一下双指针解法的思路,依然是用一个快指针,一个慢指针,使得整体遍历一次链表就找到倒数第n个节点。让fast先自己移动n + 1步,之后,slow和fast再一起移动,当fast指向末尾的时候,slow指向的值也正是要删除的节点。这里的重点就是注意 fast 要先移动 n + 1 次,而不是 n 次。
这是我自己编写风格的代码(更喜欢使用 fast->next 作为循环条件):
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
if(!head) return head;
struct ListNode *DummyHead = malloc(sizeof(struct ListNode));
DummyHead->next = head;
struct ListNode *fast = DummyHead, *slow = DummyHead;
while(fast->next != NULL){
n--;
if(n < 0){
slow = slow->next;
}
fast = fast->next;
}
slow->next = slow->next->next;
head = DummyHead->next;
free(DummyHead);
return head;
}
这里没有使用 n <= 0 时就开始让slow更新的原因就是为了让 fast 能多走一步,使其走 n + 1次。这样才能使得当循环结束的时候,slow->next 指向的是要删除节点(如果循环条件为fast->next 指向最后一个节点,就不需要多走一步,这是数学性质,但是拿到编程中,循环结束的条件只能为NULL)。
LeetCode面试题02.07.链表相交
题目链接:面试题02.07.链表相交
简单的暴力解法就不在这里解释了,下面介绍时间复杂度为O(m + n),并且空间复杂度为O(1)的解法——双指针。
首先要考虑多种情况,headA和headB为空,为一种情况直接返回NULL。
接下里是双指针核心部分,(对于这个题而言,双指针法只是实现的方法,重要的是对思想上的理解)创建两个指针pa和pb,初始化他们分别为headA和headB,接下来开始遍历。
当两链表长度相等时,很好理解,如果两个链表有相交的部分,在循环到某个节点肯定有存在pa == pb(同步更新),循环结束,return其中任意一个指针都可以;如果没有相交最后都会等于NULL符合题意返回。
当两个链表长度不相等时,这时就体现出这个思想的核心了,假设链表A的不相交的长度为a,链表B的不相交的长度为b,那么在更新寻找相同节点的过程中,两个指针依次会进入指向另一个链表的循环中,如果没有交点 a + b == b + a 到最后同时到达NULL处返回。如果有相同节点,又因为此题性质是相交之后的节点也相同,设链表A、B相交的长度为c,a + c + b == b + c + a 所以也会同时到达第一个相交节点。
(我看了一些其他的方法,但是根本思想都是根据这个长度的性质)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *pA = headA, *pB = headB;
while (pA != pB) {
pA = pA == NULL ? headB : pA->next;
pB = pB == NULL ? headA : pB->next;
}
return pA;
}
LeetCode142 环形链表Ⅱ
题目链接:142.环形链表Ⅱ
同样暴力解法省略,接下来讲一下时间复杂度为O(n)的解法,与上题相交链表相同的是,都用了 双指针 和 严密的数学逻辑推理的公式证明。
首先判断环形链表是否存在:先来研究用双指针时环形链表的性质,如果环形链表存在,fast和slow一定会相遇,因为我们规定的是fast一次走两步,slow一次走一步,当进入环形链表开始循环时,fast 与 slow一圈的路径相同,再结合对两者的速度考虑,相当于slow不动,fast每次走一步来追赶slow,因为是一步就不可能出现跳过slow的情况。
再来推理如何找到环形链表的入口,
那么相遇时: 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时,得到x = z。
实际上,这个n的数目是不影响实际上的性质,因为n只是代表fast在环中走几圈才遇到slow,跟第一圈就遇到slow一样。
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *fast = head, *slow = head;
while(fast && fast->next){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
struct ListNode *index1 = head, *index2 = fast;
while(index1 != index2){
index1 = index1->next;
index1 = index2->next;
}
return index1;
}
}
return NULL;
}
链表部分总结:
不管是数组还是链表的章节,双指针法都是非常有用处,还有在链表这一章节的题中,令我感触很深的就是有了很多的数学逻辑推导证明的过程,这在我以前遇到的题目中是很少见的。链表中的重要技巧也就是附加头结点,还有虚拟头结点的应用会让遍历更加方便。在许多题目中,暴力法可以解决,但是时间空间复杂度都很高,为了降低,很多时候都可以用双指针的办法,把多重遍历和循环,降成 n 阶的时间复杂度( O(n) )。