目录
1.返回倒数第K个节点【链接】
题目描述:
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
思路:快指针先走k步,然后快指针和慢指针同时走,直到快指针走到NULL,此时慢指针的节点即为所求。
解析:
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int kthToLast(struct ListNode* head, int k){
struct ListNode*fast=head,*slow=head;
//快指针先走k步
while(k--)
{
fast=fast->next;
}
//快慢指针一起走
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow->val;
}
2.链表的回文结构【链接】
题目描述:
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
思路 :首先找到中间节点,将中间节点后半部分倒置,再分别从头结点和尾节点向中间遍历,看对应值是否相等。
这里需要用到曾经写过的找链表的中间节点函数和反转链表函数可参照【单链表的应用】
代码实现
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head) {
//创建快慢指针
struct ListNode* slow = head;
struct ListNode* fast = head;//如果有两个中间节点,则返回第二个中间节点
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
//此时slow刚好指向中间节点
return slow;
}
struct ListNode* reverseList(struct ListNode* head) {
// 重新创建一个链表,将之前的链表进行头插即可
struct ListNode* rphead = nullptr;
// 进行指针变换
struct ListNode* cur = head;
while (cur != nullptr) {
// 用于保存下一个节点地址
struct ListNode* newnode = cur->next;
// 头插
cur->next = rphead;
rphead = cur;
cur = newnode;
}
return rphead; //返回新链表的头rhead
}
bool chkPalindrome(ListNode* A) {
struct ListNode* mid = middleNode(A);
struct ListNode* rmid = reverseList(mid);
while (rmid && A) {
if (rmid->val != A->val) {
return false;
}
rmid = rmid->next;
A = A->next;
}
return true;
}
};
3.相交链表【链接】
题目描述:
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。
示例1相交节点不是1,而是8,注意这里不能比值,因为两个不同节点的值可能一样,要比较节点的地址。
思路1:暴力求解,对于链表A中的每个节点,我们都遍历一次链表B看B中是否有相同节点,第一个找到的就是第一个公共节点,假设A链表有M个节点,B链表有N个节点,时间复杂度太高了为O(M*N)即O(N^2)。
思路2:先判断两个链表是否相交,可以通过判断两个链表最后一个节点地址是否相同,如果尾节点相同,说明两个链表一定相交,如果不相等,直接返回空指针。计算出两个链表的长度,让长链表先走相差的长度,然后让两个链表同时走,直到遇到相同的节点就是第一个公共节点,返回指向这个节点的指针。
解析:
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *curA=headA,*curB=headB;
int lenA=0,lenB=0;
while(curA->next)
{
curA=curA->next;
++lenA;
}
while(curB->next)
{
curB=curB->next;
++lenB; //此时B链表的长度少算一个
}
//尾节点不相等就是不相交
if(curA!=curB)
{
return NULL;
}
lenA+=1;
lenB+=1;
//长的先走差距步,再同时走,第一个相等的就是交点
//假设法
int gap=abs(lenA-lenB);//求绝对值
struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
//如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
if(lenB>lenA)
{
longList=headB;
shortList=headA;
}
while(gap--)//走差距步
{
longList=longList->next;
}
while(longList!=shortList)
{
longList=longList->next;
shortList=shortList->next;
}
return shortList;//返回哪一个都可以
}
4.判断链表中是否有环【链接】
题目描述:
给你一个链表的头节点
head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回
true
。 否则,返回false
。
思路:首先,让快慢指针fast、slow指向链表的头节点head, 让快指针fast一次向后移动两个节点,慢指针一次向后移动一个节点, 判断fast和slow是否走到同一个节点上(数学上追击相遇问题),如果走到同一个节点上,就返回true。
代码实现
bool hasCycle(struct ListNode *head) {
struct ListNode*slow=head,*fast=head;
while(fast&&fast->next)//当一个链表中没有环时,fast一定会移动到链表的结尾
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
return true;
}
}
return false;
}
常见问题解析
1.快慢指针为什么会相遇,它们有没有可能错过,永远追不上?
不会错过。
假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:
N->N-1->N-2->......->3->2->1->0
每追击一次,二者之间距离缩小1,距离为0即相遇。
2.慢指针slow一次移动一个节点,快指针一次移动多个节点(3,4,5......n)可行吗?可行。
用快指针一次移动3个节点举例
假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:
情况1:N->N-2->N-4->......->4->2->0(N为偶数)
情况2:N->N-2->N-4->......->5->3->1->-1(N为奇数)N为-1表示错过了,距离变成C-1(C为环的长度)
- 如果C-1是偶数,新一轮追击就追上了
- 如果C-1是奇数,那么就永远追不上
如果N是奇数并且C是偶数,那么就永远追不上,那这种条件存在吗?
假设slow进环前走的距离为L,设slow进环时,fast已经在环中转了x圈
slow走的距离:L
fast走的距离:L+x*C+C-N
由fast走的距离是slow的三倍产生的数学等式:3L=L+x*C+C-N
化简得:2L=(x+1)*C-N
我们发现偶数=(x+1)*偶数-奇数不成立,反证出当N是奇数时,C也一定是奇数。
总结一下:一定能追上,N是偶数,第一轮就追上了;N是奇数,第一轮追不上,C-1是偶数,第二轮就追上了。
5.寻找环的入口点【链接】
题目描述:
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始 )。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改链表。
思路1**:** 用两个指针head、meet分别指向链表的头节点和快慢指针相遇的节点,同时移动两个指针,当两个指针指向同一个节点时,该节点就是环的入口点 。
解析:
假设环的长度为C,到达相遇点时,慢指针slow走过的距离为L+N(一圈之内肯定会被追上),快指针fast走过的距离为L+x*C+N (假设快指针走了x圈,N一定大于等于1),由fast走的距离是slow的2倍产生的数学等式:2*(L+N)=L+x*C+N 化简得:L=x*C-N
也就是说head到入口点的距离等于meet指针转x圈减去N的距离,head走到入口点,meet也刚好走到入口点,所以两个指针一起遍历,最终会同时到达入口点。
代码实现1
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow=head,*fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
struct ListNode *meet=slow;
while(meet!=head)
{
meet=meet->next;
head=head->next;
}
return meet;
}
}
return NULL;
}
**思路2:**让快慢指针相遇节点与下一个节点断开,然后将问题转化为两个链表的相交,环的入口点其实就是两个链表的相交点。
解析:
代码实现2
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *curA=headA,*curB=headB;
int lenA=0,lenB=0;
while(curA->next)
{
curA=curA->next;
++lenA;
}
while(curB->next)
{
curB=curB->next;
++lenB; //此时B链表的长度少算一个
}
//尾节点不相等就是不相交
if(curA!=curB)
{
return NULL;
}
lenA+=1;
lenB+=1;
//长的先走差距步,再同时走,第一个相等的就是交点
//假设法
int gap=abs(lenA-lenB);//求绝对值
struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
//如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
if(lenB>lenA)
{
longList=headB;
shortList=headA;
}
while(gap--)//走差距步
{
longList=longList->next;
}
while(longList!=shortList)
{
longList=longList->next;
shortList=shortList->next;
}
return shortList;//返回哪一个都可以
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow=head,*fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
struct ListNode *meet=slow;
struct ListNode *newhead=meet->next;
meet->next=NULL;
return getIntersectionNode(head,newhead);
}
}
return NULL;
}
6.随机链表的复制【链接】
题目描述:
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝 。 深拷贝应该正好由
n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点。
深拷贝就是拷贝一个值和指针指向都跟当前链表一模一样的链表。
**思路:**拷贝节点到原节点的后面,再寻找途径处理random指针,处理完后对复制的节点拿下来尾插成新链表,返回新链表的头。
解析:
代码实现
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
//拷贝节点插入在原节点的后面
struct Node* copyRandomList(struct Node* head) {
struct Node*cur=head;
while(cur)
{
struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
copy->next=cur->next;//注意这里顺序不能颠倒
cur->next=copy;
cur=copy->next;
}
//控制random
cur=head;
while(cur)
{
struct Node*copy=cur->next;
if(cur->random==NULL)
{
copy->random=NULL;
}
else
{
copy->random=cur->random->next;
}
cur=copy->next;
}
//把拷贝节点取下来尾插成为新链表,然后恢复原链表
struct ListNode*copyhead=NULL,*copytail=NULL;
cur=head;
while(cur)
{
struct ListNode*copy=cur->next;
struct ListNode*next=copy->next;
if(copytail==NULL)
{
copyhead=copytail=copy;
}
else
{
copytail->next=copy;
copytail=copytail->next;
}
cur->next=next;//恢复原链表,有没有这句代码都行!
cur=next;
}
return copyhead;
}