公主请阅
- [1. 返回倒数第 k 个节点](#1. 返回倒数第 k 个节点)
-
- [1.1 题目说明](#1.1 题目说明)
- [1.2 题目分析](#1.2 题目分析)
- [1.3 解法一代码以及解释](#1.3 解法一代码以及解释)
- [1.3 解法二代码以及解释](#1.3 解法二代码以及解释)
- 2.相交链表
-
- [2.1 题目说明](#2.1 题目说明)
-
- [示例 1](#示例 1)
- [示例 2](#示例 2)
- [示例 3](#示例 3)
- [2.2 题目分析](#2.2 题目分析)
- [2.3 代码部分](#2.3 代码部分)
- [2.4 代码分析](#2.4 代码分析)
1. 返回倒数第 k 个节点
1.1 题目说明
题目名称:
面试题 02.02. 返回倒数第k
个节点
题目要求:
实现一种算法,找出单向链表中倒数第 k
个节点,并返回该节点的值。
注意:
本题相对原题稍作改动。
示例:
输入:1 -> 2 -> 3 -> 4 -> 5 和 k = 2
输出:4
说明:
给定的k
保证是有效的。
1.2 题目分析
这个题的话需要我们将倒数第k
个节点进行返回,那么我们先想到的是什么呢?怎么找打倒数第k
个节点呢?
解法一
那么这个时候我就想着将这个链表先逆置了,然后遍历k
次我们就可以找到原先链表的倒数第k个节点了
这个是一种解法
解法二
要解决这个问题,我们可以使用 双指针 (又称为 快慢指针)来找到链表中的倒数第 k 个节点。具体思路如下:
-
初始化两个指针,都指向链表的头节点。
- 快指针
fast
- 慢指针
slow
- 快指针
-
移动快指针 ,先让
fast
指针前进k
步。 -
同时移动快慢指针 ,当
fast
指针到达链表末尾时,slow
指针的位置就是倒数第k
个节点的位置。
详细步骤:
- 定义
fast
和slow
两个指针,初始时都指向链表头部。 - 让
fast
先走k
步,这样在之后的同时移动中,fast
和slow
之间的距离始终保持k
。 - 然后同时移动
fast
和slow
,每次fast
向后移动一格,直到fast
到达链表的末尾。 - 此时,
slow
就是倒数第k
个节点。
那么下面我们会将两个解决方案的代码呈现出来的
1.3 解法一代码以及解释
C
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int kthToLast(struct ListNode* head, int k)
{
struct ListNode*prev=NULL;
struct ListNode*cur=head;
while(cur!=NULL)//遍历整个链表,将链表反转了
{
struct ListNode*tmp=cur->next;
cur->next=prev;
prev=cur;
cur=tmp;
}
//到这里prev就是反转链表的头结点了
while(--k)
{
prev=prev->next;
}
return prev->val;
}
//先将链表进行反转的操作,然后我们就可以直接利用循环来找到第k个节点了
先定义两个指针
-prev
指向当前节点的前一个节点,初始化为NULL
cur
指向当前节点,初始化为头结点
然后我们使用while
循环进行遍历操作,直到我们遍历到尾节点我们就结束了
我们先定义一个指针tmp
将当前节点的下一个节点进行保存了
然后我们现在开始将相邻的两个节点的指向进行改变的操作了
我们让当前节点的下个节点指向我们的prev
,然后对prev
这个指向进行更新,现在指向了我们当前的节点了,然后我们的cur
也进行了更新了,指向当前节点的下个节点了,下个节点我们一开始使用tmp
进行保存了,然后我们直接进行赋值就行了
等循环结束了,我们的链表的遍历操作就结束了,那么就说明我们的链表已经逆置成功了,那么现在我们的prev
就是我们的头结点了
然后我们从这个头结点进行遍历操作,我们遍历k次,所以我们的循环条件是--k
,前置--,返回的是-
之前的值
等循环结束了,我们的perv
就是我们原先链表的倒数第k
个节点了,我们直接将这个节点的值进行返回就行了
1.3 解法二代码以及解释
c
int kthToLast(struct ListNode* head, int k){
struct ListNode* first = head;
struct ListNode* last = head;
for(int i = 1; i < k ;i++)
first = first->next;
while(first->next)
{
first = first->next;
last=last->next;
}
return last->val;
}
我们先创建了两个指针fast
和last
,然后都初始化指向头结点,但是我们让块指针先走k
步,然后我们继续使用while
循环进行遍历操作,但是是快慢指针一起进行遍历,然后循环的条件就是快指针走到了尾节点的时候我们直接将尾节点的值进行返回就行了
但是这个原理是为什么呢?
双指针法的原理可以通过以下几点进行解释:
-
快慢指针之间的距离固定为 k
双指针法的核心思想是利用两个指针:一个"快指针"(
fast
)和一个"慢指针"(slow
)。我们首先让 快指针先走k
步 ,这样快指针和慢指针之间的距离就正好是k
。 -
同时移动快指针和慢指针
接下来,当我们同时移动快慢指针时,快指针每走一步,慢指针也走一步。因为快指针最开始已经领先k
步,因此当快指针走到链表末尾时,慢指针的位置就是倒数第k
个节点。
- 具体来说,假设链表的长度为
n
,当快指针走到末尾时,它已经走了n
步。而由于快指针最开始比慢指针多走了k
步,因此慢指针只走了n - k
步,此时slow
就恰好指向倒数第k
个节点。
- 保持链表的一次遍历
通过这种方式,我们只需要遍历链表一次,而不是两次就可以得到倒数第k
个节点。相比于朴素方法(先遍历链表得到链表长度,再遍历到目标节点位置)要遍历两次链表的时间复杂度,双指针法将时间复杂度从 O(2n) 优化到了 O(n)。
总结原理:
- 步数差 :快指针和慢指针在一开始保持
k
步的差距,这样当快指针到达末尾时,慢指针正好停在倒数第k
个节点的位置。 - 一次遍历:只需要遍历链表一次就能完成任务,避免了多次遍历的低效。
- 指针同步移动 :当快指针到达链表末尾时,慢指针正好位于我们想要的倒数第
k
个节点。
2.相交链表
2.1 题目说明
从图中提取的题目信息如下:
题目描述
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,则返回 null
。
注意:
- 整个链表结构中不存在环。
- 你不能更改链表的结构,链表必须保持其原始结构。
示例 1
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
示例 2
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
示例 3
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入参数:
intersectVal
:相交的起始节点的值。如果不存在相交节点,则该值为 0。listA
:链表 A。listB
:链表 B。skipA
:在链表 A 中(从头节点开始)跳到交叉节点的节点数。skipB
:在链表 B 中(从头节点开始)跳到交叉节点的节点数。
输出:
- 如果链表相交,返回相交的起始节点的值;如果不相交,返回
null
。
2.2 题目分析
这个题给我们两个链表,让我们找出两个链表的相交链表,然后将相交节点进行返回的操作,如果没有相交的节点的话我们直接返回NULL
那么这个题我们应该怎么进行设置呢?
可能给的两个链的长度不一样啊,那么我们应该怎么解决呢?
我们可以找出长链,然后计算出两个链的节点之差k,然后让长链走k次,那么我们的长链和短链就是同一个起点了,然后一起进行遍历的操作,边遍历边进行大小的比较的操作,如果两个链表遍历的指针相遇了的话,那么当前的指针所处的节点就是相交的节点了
那么思路就说到这里了,下面就是我们的代码的实践了
2.3 代码部分
C
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//我们需要找到两个链表节点的差值
//长链表先走差值数
//两个链表开始遍历操作,看看是不是同一个节点
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
if(headA==NULL||headB==NULL)
{
return NULL;
}
//创建两个指针分别指向A和B两个链表
ListNode*l1=headA;
ListNode*l2=headB;
//计算两个链表的节点的个数
int sizeA=0,sizeB=0;
while(l1!=NULL)//先遍历l1
{
sizeA++;
l1=l1->next;
}
while(l2!=NULL)//先遍历l1
{
sizeB++;
l2=l2->next;
}
//求差值
int gap=abs(sizeA-sizeB);//绝对值
//我们需要判断下谁是长链表
//我们先假设
ListNode*longlist=headA;
ListNode*shortlist=headB;
if(sizeA<sizeB)
{
longlist=headB;
shortlist=headA;
}
//到这里我们就知道长短链表了
while(gap--)
{
longlist=longlist->next;
}
//上面的代码是找到长链表然后让长链表的指针走差值
while(longlist&&shortlist)//两个链表不为空的话我们就往后面走
{
if(longlist==shortlist)//说明相交了
{
return longlist;
}
//如果没有相交的话,那么我们的两个指针接着走
longlist=longlist->next;
shortlist=shortlist->next;
}
//到这里的话据说明没有相交的节点,我们直接返回空就行了
return NULL;
}
2.4 代码分析
我们先将特殊情况进行分析下,如果两个链表都是空的话,我们直接返回NULL
就行了
然后我们就开始处理正常情况了
我们先创建两个指针分别指向我们的A
和B
两个链表,分别是l1
和l2
,指向对应链表的头结点
我们然后创建两个变量进行记录两个链表的头结点到尾节点的节点个数
然后我们就可以算出差值gap
了,然后我们进行长短链表的判断假设,我们先假设长链表是A
,然后加链表是B
,如果sizeA<sizeB
的话,那么长链表就是B
,短链表就是A
了
然后我们就可以将长短链表判断出来了
我们利用sizeA<sizeB
算出了节点差值 gap
,我们利用abs
进行绝对值的操作
直到长链表是谁之后,我们让长链表走gap
次
走完之后我们的两个链表一起进行遍历,使用while
循环,条件是两个链表不为空我们就往后走
然后在循环里面进行判断,如果长链表的指针等于短链表的指针,那么就说明两个指针相遇了,那么这个节点就是我们想要的相交节点了
在这个条件判断之后我们进行两个指针的往后走一位的操作
如果出了循环还没有返回的话,那么就说明这两个链表是不存在相交的节点的,我们直接返回NULL
就行了