学习日记第二期,整理了几道链表有关题目。复杂的链表操作确实容易乱,欢迎批评指正~
题目1:删除链表的倒数第N个节点(leetcode-19)
题目描述
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
样例输入
head = [1,2,3,4,5], n = 2
样例输出
[1,2,3,5]
题目分析
本题目要用到链表操作中的两个技巧:虚拟头节点法和快慢指针法。
1、利用虚拟头节点法,可以避免讨论所要删除的第一个节点是否是头节点。否则,如果要删除的不是头节点,则可以重新连接要删除节点的前后两个节点,如果要产出的是头节点,则修改头节点位置为第二个节点,需要分别讨论这两种情况。
2、利用快慢指针法,我们可以方便地定位到要删除节点的前一个节点。否则需要先通过遍历求出链表的节点个数,进而定位到要删除节点的位置,而遍历操作会增加程序运行的时间。
最终代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode newhead;
newhead.next = head;//定义虚拟头节点
ListNode *p = &newhead, *q = p;//定义快慢指针,并让快指针提前开始移动
for(int i = 0; i < n; i++){
q = q->next;
}
while(q->next){//快慢指针隔开一点距离后,开始同时移动
p = p->next;
q = q->next;
}
p->next = p->next->next;//跳过要删除的节点
return newhead.next;//注意此处要返回虚拟头节点的下一个节点
}
};
题目2:环形链表(leetcode-141)
题目描述
给你一个链表的头节点 head
,判断链表中是否有环。如果链表中存在环 ,则返回 true
。 否则,返回 false
。
样例输入
样例输出
true
题目分析
本题目可以利用快慢指针法进行求解,与上一题不同的是,我们令快慢指针同时出发,但是快指针一次前进两个节点,满指针一次前进一个节点。如果链表有环,则会出现套圈,即快指针追上满指针并相遇;如果链表没有环,则快指针会首先遇到空节点。
最终代码
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *p = head, *q = head;
while(p != NULL && p->next != NULL){
p = p->next->next;//快指针一次前进两个节点,满指针一次只前进一个阶段
q = q->next;
if(p == q) return true;//两指针相遇则证明有环
}
return false;
}
};
题目3:快乐数(leetcode-202)
题目描述
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。(1 <= n <= 2^31 - 1
)
样例输入
n = 19
样例输出
true
计算过程: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
题目分析
这道题目不是一道链表的题目,但是可以用题目2的快慢指针法的思路来解决。
1、n的所有位数的平方和是有限大的。首先n是在int范围内的,最大是一个十位数。而对于一个十位数来说,想要其所有位数的平方和最大,可以另每一位都是9,即n取9999999999。此时n的所有位数的平方和为810。
2、如果n不是一个快乐数,则经过有限次迭代后,会出现迭代结果与之前某次的结果相同。经过前面的分析可以得到,每次迭代的结果最大是810,根据抽屉原理,经过有限次迭代,会出现迭代结果与之前某次的结果相同。若不好理解,可以类比:若某个学校有367个学生,则至少有两个学生的生日相同。
3、因此,本题目可以采取与快慢指针法类似的思路。即设置两个数字,他们迭代的初始值都是n,运行中一个数字每次迭代两次,一个数字每次迭代一次。如果是快乐数,则迭代快的数会首先成为1;如果不是快乐数,则在有限次迭代后,两个数的迭代结果相同。
最终代码
class Solution {
public:
int Happy(int n){//迭代函数
int a = 0;
do{
a += (n % 10) * (n % 10);
n = n / 10;
} while(n);
return a;
}
bool isHappy(int n) {
int a = n, b = n;
while(1){
a = Happy(a);
b = Happy(Happy(b));
if(b == 1) return true;//如果迭代结果为1,则是快乐数
if(a == b) return false;//如果两个数字迭代结果相同,则不是快乐数
}
}
};
总结
在做链表有关题目时,一定要理清楚每一步的逻辑,可以用笔在纸上写出每一步的流程,否则容易出现数据的丢失,或者逻辑混乱。此外,链表中也有许多经典的方法,比如虚拟头节点法,快慢指针法,可以提高结题的效率。
对于题目3,虽然这并不是一道标准的链表题目,但是本题目的迭代关系与链表类似,本质都是一通过一系列的映射,把许多值串联起来。因此,在解其他题目的时候,也可以借鉴链表的思想,把快慢指针、虚拟头节点等方法活学活用。
最后分享一个我在写程序时犯的一个小错误。在利用虚拟头节点法解题目1时,我在定义虚拟头节点时,将虚拟头节点定义为一个指针,而不是一个实际的节点,代码如下:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *newhead;//注意这里定义了指针,但没有定义指针指向哪里
newhead->next = head;
ListNode *p = newhead, *q = p;
for(int i = 0; i < n; i++){
q = q->next;
}
while(q->next){
p = p->next;
q = q->next;
}
p->next = p->next->next;
return newhead->next;
}
};
运行后程序在“newhead->next = head;”了报错,后来经过思考明白了其中的逻辑:在上面的代码中,我定义了一个新的指针,想将其作为虚拟头节点,并让其指向链表的第一个节点。“newhead->next = head;”语句的意思是让newhead所指向节点的next指针等于head指针。但由于我并没有定义newhead指针指向的节点,因此程序会发生报错。
因此,为解决这个bug,需要首先定义newhead指针所指向的节点,修改程序如下:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *newhead = new ListNode;//注意这里定义了实际的节点
newhead->next = head;
ListNode *p = newhead, *q = p;
for(int i = 0; i < n; i++){
q = q->next;
}
while(q->next){
p = p->next;
q = q->next;
}
p->next = p->next->next;
return newhead->next;
}
};