两两交换链表中的节点
题干
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路
两两交换节点,让我联想到昨天做的反转链表的题目,两两交换节点相当于反转一次指针,只不过不是连续反转,而是每隔两个节点再反转。或许这道题也可以使用双指针法,同时为了保证操作统一,继续使用虚拟头节点dummyHead。画草图的时候发现,原来反转链表只需改变两个指针,而交换节点需要改变三个指针。
代码
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr){
return head;
}
// 新建虚拟头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
// 新建三个指针
ListNode* prepre = dummyHead;
ListNode* pre = head;
ListNode* cur = pre->next;
while (pre != nullptr && cur != nullptr){ // pre 和 cur 分别指向两个需要交换的节点,都不能为空
ListNode* tmp = cur->next; // cur->next有可能为 null
// 两两交换
cur->next = pre;
prepre->next = cur;
pre->next = tmp;
// 移动到下一对节点
prepre = pre;
pre = prepre->next; // 此时 pre 有可能为空指针,所以接下来要判空
if (pre != nullptr){
cur = pre->next;
}
}
return dummyHead->next;
}
};
删除链表倒数第 N 个结点
题干
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
思路
要删除倒数第 n 个结点,关键在于如何找到倒数第 n 个结点,即尾结点的前 (n-1) 个结点。我们可以设置两个指针 del 和 cur,指针 cur 指向当前结点,用于遍历链表找到尾节点;指针 del 则指向 cur 的前 (n-1)个结点,用于找到待删除的结点。只要 cur 指向了尾结点,则 del 就指向了倒数第 n 个结点。同时为了能够方便删除结点 del,可以再设置一个指针 pre 指向 del 的前一个结点,删除时只需要 pre->next = del->next。
代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 新建虚拟头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
for (int i = 0; i < n; ++i) {
cur = cur->next;
}
ListNode* del = head;
ListNode* pre = dummyHead;
// 查询倒数第 n 个结点
while (cur->next != nullptr){
cur = cur->next;
pre = del;
del = del->next;
}
// 删除倒数第 n 个结点
pre->next = del->next;
delete del;
return dummyHead->next;
}
};
链表相交
题干
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。注意结点的指针相同才意味着相交,如果只是数值相同而指针不同不意味着相交。
思路
方法一:暴力解法,用两个不同的指针 curA 和 curB 遍历两个链表,对比 curA 和 curB 指针是否相同,时间复杂度O(n^2)。之后发现自己想复杂了,本质上是没有完全理解题意中的结点相交到底代表着什么意思。
当两个链表存在相交结点,都说明了什么?
(1)两个链表自相交结点后的所有结点都相同。
因为链表结点的结构只定义了一个 next 指针,一旦两个链表中间存在相交结点,则从相交结点开始一直跟踪 next 指针往后的所有结点都相同。我看到 ”相交“ 两个字就想到了数学里两直线交叉的情况,先入为主觉得两个链表相交会存在以下这种情况,犯傻了。。
(2)若长链表的长度为 sizeA,短链表长度为 sizeB,长链表中相交结点最靠前的位置只会出现于第 (sizeA - sizeB) 个节点处,所以长链表的遍历并不需要从头节点开始。
由第(1)个结论可以知道,两个链表如果相交,则他们的末尾结点应该都相同,所以最极端的情况是整个短链表刚好就是长链表的末尾部分,此时会出现相交结点最靠前的位置 sizeA - sizeB。另一方面,也说明了两个链表的结点指针进行比较的时候,为什么当各自结点的指针都不同时就可以同步前进,即当 curA != curB 时,curA = curA->next,curB = curB->next,而不是 curA 保持不变,curB = curB->next。
方法二修正后:先计算两个链表的长度 sizeA,sizeB;设长链表为headA,如果不是则交换。再分别用两个指针 curA 和 curB 遍历链表,其中 curA 初始为长链表中第(sizeA - sizeB)的位置,curB 初始则为headB,之后两两比较,curA 和 curB 不同则一起前进。时间复杂度 O(n)。
代码
方法一:暴力解法(虽然能通过,但逻辑有问题)
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 结点数目有可能为 0
if (headA == nullptr || headB == nullptr){
return nullptr;
}
// 只是查找,不需要新建虚拟头节点
ListNode* curA = headA; // 遍历链表 A
ListNode* curB = headB; // 遍历链表 B
while (curA != nullptr){
while (curB != nullptr){
if (curA == curB){
return curA;
} else{
curB = curB->next;
}
}
curA = curA->next;
curB = headB;
}
return nullptr;
}
};
方法二:改正后的解法
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int sizeA=0,sizeB=0; // 记录链表A和链表B的长度
ListNode* curA = headA; // 用于遍历链表 A
ListNode* curB = headB; // 用于遍历链表 B
while (curA != nullptr){
sizeA++;
curA = curA->next;
}
while (curB != nullptr){
sizeB++;
curB = curB->next;
}
// 将长链表固定设置为 链表 A
if (sizeA < sizeB){
ListNode* tmp = headB;
headB = headA;
headA = tmp;
// delete tmp;
// 此处不可以释放掉 tmp 结点,如果释放掉 tmp,则相当于 headA 所指结点被释放
// 如果delete,后续访问指针 headA 会报错
int tmpSize = sizeB;
sizeB = sizeA;
sizeA = tmpSize;
}
curA = headA;
curB = headB;
int n = sizeA - sizeB;
for (int i = 0; i < n; ++i) {
curA = curA->next;
// 如果前面 delete tmp,则此处会报错 ERROR: AddressSanitizer: heap-use-after-free on address
// 此报错表明程序试图访问已经被释放的堆内存
}
while (curA != nullptr && curB!= nullptr){
if (curA == curB){
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};
环形链表
题干
题目:给定一个链表的头节点 head,返回链表开始入环的第一个节点。 如果链表无环,则返回 null 。不允许修改 链表。
如何判定存在环?
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 - 1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
思路
先确定链表有环,再找到环的入口结点。
1)如何判断是否有环?
不知道有什么方法,直接看的题解。
快慢指针法:一个快指针 fast,一个慢指针 slow,fast 每次移动两个结点,slow 每次移动一个结点。如果链表中没有环,则快慢指针不可能相遇;如果链表有环,则 fast 和 slow 会相遇。为什么链表有环,快慢指针就一定会相遇呢?不会错过吗?
首先 fast 肯定会比 slow 先进入环,而 fast 的速度是每次移动两个结点,slow 的速度是每次移动一个结点,fast 相对于 slow 每次前进( 2 - 1 = 1 )个结点,则进入环以后,fast 肯定会以每次移动一个结点的速度追上 slow。
2)如何找到环的入口结点?
观察得到,环的入口节点将有两个指针指向它,其余结点都只有一个指针指向自身。(个人想法,本来想用这个方法做题的,但是好像做不出来)
看了题解,根据快慢指针法,fast 和 slow 相遇的结点位置和环的入口位置有什么关系呢?
时间不够,剩下的明天再学。