链表OJ
文章目录
1. 反转链表
好的,我们一起来看一下题目,题目是这样说的
思路一:可以新创建一个链表,然后采用头插的方法,从 1
一个接一个的头插数据,这种是最容易想到的方法,但是要开辟一块空间,需要考虑空间复杂度的问题
思路二:使用 n1
n2
n3
三个指针,首先我们让 n1
指向空, n2
指向头节点, n3
指向头节点的下一个节点
然后我们再让 n1
指向 n2
, n2
指向 n3
,就变成了这样
这样的话 n1
是不是指向反转链表之后的尾节点,那我们如果再让 n1
指向 n2
, n2
指向 n3
,把整个链表都给遍历一遍之后,是不是就把整个链表给反转了,对吧,像这样
然后我们在考虑遍历截止的条件,哎,是不是 n2
为空了之后遍历就截止了,最后再返回 n1
就好了
整体的思路是不是都比较清楚了,接下来就是代码实现了,这个就得多练习了
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
{
return NULL;
}
struct ListNode* n1 = NULL,*n2 = head,*n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
n3 = n3->next;
}
return n1;
}
2. 返回K值
OJ链接:面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)
好的,我们一起来看一下题目,题目是这样说的
思路一:我们可以创建一个数组,把链表中的数据存放在数组里,用数组来做,不过要再开辟一块空间,还要考虑空间复杂度的问题
思路二:反转链表遍历一下,再找第K个节点,这样也是可行的
思路三:使用快慢指针的方法,我们先让快指针走K步,然后再让快慢指针一起走,就能发现当快指针指向空的时候,我们的慢指针指向的就是倒数第K个节点,最后返回慢指针节点的值就行了
代码实现:
int kthToLast(struct ListNode* head, int k){
struct ListNode* fast = head,*slow = head;
while(k--)
{
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow->val;
}
3. 链表的中间节点
OJ链接:876. 链表的中间结点 - 力扣(LeetCode)
好的,我们一起来看一下题目,题目是这样说的
思路:主要的思路还是我们的快慢指针,这时就要分两种情况,一个是奇数的情况,另一个是偶数的情况
- 当链表为奇数时:首先我们要初始化两个快慢指针,然后通常让慢指针走一步,快指针走两步,像这样
像这样,当快指针指向尾节点的时候,慢指针就刚好指向中间节点,最后返回慢指针节点的值
- 当链表为奇数时:首先我们还要初始化两个快慢指针,然后还是让慢指针走一步,快指针走两步,像这样
就像这样,和前者不同的是,当快指针指向空时,慢指针才指向中间节点,最后返回慢指针节点的值
思路都清楚了吧,接下来就是代码实现:
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* fast = head,*slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
4. 回文链表
好的,我们一起来看一下题目,题目是这样说的
我们要想证明它是个回文结构,首先我们要先知道回文结构是什么,从前往后和从后往前是完全相同的以中间节点为中心这个链表是对称的,举个例子比如12321和1221这两个都是回文,如果是123123这个还是回文吗,这种就不是回文了
思路:我们可以先找中间节点,找中间节点有什么好处呢,当我们判断链表是否是回文链表,是不是首先寻找中间节点,中间节点断开然后再从两头对应,要是两头节点的值都能一一对应,我们就说这个链表是回文链表,我们找到中间节点之后,然后从中间节点断开,将中间节点后面的链表反转,这样就可以一一比较,怎么比呢,一个从头节点开始,另一个从中间节点开始,一一比较,就可以了,都相等就是回文链表,只要有一个不相等,就不是回文链表
现在我们思考一个问题,需要分情况吗,很简单,画个图就知道了
代码实现:
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* fast = head,*slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
{
return NULL;
}
struct ListNode* n1 = NULL,*n2 = head,*n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
n3 = n3->next;
}
return n1;
}
bool isPalindrome(struct ListNode* head) {
struct ListNode* mid = middleNode(head);
struct ListNode* rmid = reverseList(mid);
struct ListNode* cur = head;
while(rmid)
{
if(cur->val != rmid->val)
{
return false;
break;
}
cur = cur->next;
rmid = rmid->next;
}
return true;
}
5. 相交链表
好的,我们一起来看一下题目,题目是这样说的
思路一:我们首先要有一个共识就是,链表相等不一定相交,这个大家一定要清楚这一点,我们认为如果两个链表相交,那么他们的地址是相同的,而不是数据是相等的,像这样,我们需要注意一下
我们可以一个一个节点进行比较,将链表A的所有节点和链表B的所有节点,但是我们想想时间复杂度是多少,是不是O(n*2),不满足题意
思路二:首先我们要做的是判断两个链表是否相交,再找出交点,我们大致的思路就是这样,那我们应该怎么判断呢,怎么找呢
- 哎,我们把两个链表都遍历一下,如果它们的尾节点指向的位置相同是不是两个链表就一定会相交,这就解决了如何判断两个链表是否会相交的问题
- 那我们知道两个链表相交之后,如何找交点呢,我们想一想如果我们让两个指针都指向头节点,然后再同时遍历的话,是不是有可能会错过啊,就像这样
在这里插入图片描述
- 是不是啊,说的没有问题吧,那怎么办呢,看来大家都很聪明,已经发现了,如果我们让
pb
先走一步,让它们的起始位置都一样,然后再让它们一步一步地走就可以相遇了,像这样
哦,也就是说,我们先让长一些指针走,走它们的差值步,使两个指针指向的位置相同,再让两个指针一步一步往下走,就可以找到交点了,这个思路还是很清晰的,接下来就是代码实现了
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* pa = headA,*pb = headB;
int lenA = 1,lenB = 1;
while(pa)
{
pa = pa->next;
lenA++;
}
while(pb)
{
pb = pb->next;
lenB++;
}
if(pa != pb)
{
return NULL;
}
int gap = abs(lenA - lenB);
//假设法
struct ListNode* plong = headA,*pshort = headB;
if(lenA < lenB)
{
plong = headB;
pshort = headA;
}
while(gap--)
{
plong = plong->next;
}
while(plong != pshort)
{
plong = plong->next;
pshort = pshort->next;
}
return plong;
}
6. 带环链表
思路:主要的思路还是我们的快慢指针,也就是让慢指针走一步,快指针走两步,先让两个指针从头节点的位置出发如果带环的话,一定会在环中相遇,否则的话,快指针就先指向尾节点,画个图就能更好的理解
理顺思路之后,代码就很好写的
bool hasCycle(struct ListNode *head) {
struct ListNode* fast = head,*slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
return true;
}
}
return false;
}
好了,这个题还是非常简单的,那么在我们面试的时候,面试官可能会问我们两个问题
6.1 为什么一定会相遇,有没有可能会错过,或者出现永远追不上的情况,请证明
证明:假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,这是最好情况,那最最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动 一次之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇
6.2 slow一次走一步,fast如果一次走3步,走4步,走5步还能追上吗,请证明
证明:我们假设 fast
一次走3步,当 slow
进环时,和 fast
距离是N,追击过程距离变化如下图所示:
我们可以很明显的看到,当N为偶数时,就能追击上,当N为奇数时,最后值为-1就表明已经错过了,要开始进行新一轮的追击
接下来我们在考虑新一轮追击距离变成了多少,是不是C-1(假设C为环的长度)
我们想一想是不是也要分两种情况当C-1为偶数时,就能追击上,当C-1为奇数时,最后值还是为-1,就永远追不上
小结一下:
-
N是偶数,第一轮就追上了
-
N是奇数,第一轮追击会错过,距离变成C-1
a. 如果C-1是偶数,下一轮就追上了
b. 如果C-1是奇数,那么就永远追不上
那我们在思考一下,b成立的条件,是不是永远就追不上呢,我们发现当N为奇数且C为偶数时,结论成立,我们刚在是逻辑上想的,接下来证明一下,看看是不是就永远追不上
假设slow进环时,fast与slow的距离是N
假设slow进环时,fast已经走了x圈
设slow走过的距离为 L
fast走过的距离就是 L+x*C+C-N
那么我们又说了,fast走的距离是slow距离的3倍
就可以非常轻松地写出 3L = L+x*C+C-N
化简一下就是 *2L = (x-1)C-N
哦,这就非常明显了,等式左边一定是偶数,那就说明等式右边也一定是偶数
- 我们说当N为偶数的话,那么C只能为偶数,(x-1)*C才为偶数
- 当N为奇数的话,那么C只能为奇数,只有奇数-奇数才能是偶数
那就说明当N为奇数且C为偶数时,条件不成立,所以永远追不上的假设不成立,一定会追上的
结论:
- N是偶数,第一轮就追上了
- N是奇数,第一轮追不上,C-1是偶数第二轮就可以追上
7. 带环链表2
OJ链接:142. 环形链表 II - 力扣(LeetCode)
思路:主要的思路还是我们的快慢指针,slow走一步fast走两步,当它们在环里相遇时,让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。
思路很简单,看看代码:
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;
}
通常这个时候面试官会问一个问题,这个问题和我们一样
7.1 为什么它们最终肯定会在入口点的位置相遇,请证明
证明:假设slow和fast已经相遇了,设初始位置到入口点的距离为 L
设入口点到相遇点的距离为 N
,环的长度为 C
假设slow相遇时,fast已经走了x圈
那么fast走过的距离为 L+x*C+N
slow走过的距离为 L+N
思考一个问题我们slow与fast相遇的时候,slow一定走不完一圈
很简单,如果slow走完一圈的话,fast是不是就走完两圈了,是不是早就可以相遇了
fast走过的距离是slow走过距离的两倍
也就是 **L+x * C+N = 2 * (L+N) **
化简一下也就是 *L = (x-1)C + C-N
假如x=1,那么L=C-N,正好是相遇点到进环点的距离与入环之前的距离相等,让一个节点从头开始遍历,一个从相遇点开始,最终在入环的第一个节点相遇,证明了我们肯定会在入口点的位置相遇,如果x不唯1呢,那也会相遇,无非就是在环里多走了几圈,最后还是会在入环的第一个节点相遇
对大家来说这些都很EZ,对不对,接下来的最后一道OJ题就是比较综合的了
8. 复制链表
OJ链接:138. 随机链表的复制 - 力扣(LeetCode)
题干中提到了一个深拷贝的概念,深拷贝是拷贝一个值和指针指向跟当前链表一模一样的链表
思路一:这道链表题每个节点里多了个指针指向随机节点,也有可能指向空,然后深拷贝一份,如果我们直接遍历然后拷贝呢?硬拷贝是可以的,但是有个问题,随机指针(random)指向的值如何在新链表中实现呢,如果我们去链表中查找,那么万一有重复的值怎么办呢,这就会导致没拷贝成功,如果真要用这种暴力查找,就要算相对位置,而且它的时间复杂度为O(N^2),还需要考虑时间复杂度的问题
思路二:我们这个题的主要思路应该是先拷贝链表,然后把拷贝的链表插入到原节点的后面,每个拷贝节点和原节点建立了一个关联关系
接下来就是要考虑 random
的问题了,如下图所示,我们7的 random
指向的为空,那么我们拷贝节点7的 random
也要指问空,我们拷贝节点7就在原节点的后面,所以处理起来很方便,那么13的 random
指向的是7我们如何拷贝呢,拷贝节点的random指向的就是 cur->random>next
,最后再将拷贝节点拿下来尾插,成为一个新的链表,虽然我们破坏了原链表的结构,我们可以选择恢复原链表也可以不恢复,就要看看题目的要求
代码如下:
typedef struct Node Node;
struct Node* copyRandomList(struct Node* head) {
Node* cur = head;
// 拷贝节点插入在原节点后面
while (cur)
{
Node* copy = (Node*)malloc(sizeof(Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
//控制random
cur = head;
while (cur)
{
Node* copy = cur->next;
if (cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = copy->next;
}
//拷贝节点取下来尾插成为新链表,然后恢复原链表(不恢复也可以)
Node* copyhead = NULL, * copytail = NULL;
cur = head;
while (cur)
{
Node* copy = cur->next;
Node* next = copy->next;
if (copytail == NULL)
{
copyhead = copytail = copy;
}
else
{
copytail->next = copy;
copytail = copytail->next;
}
//恢复原链表
cur->next = next;
cur = next;
}
return copyhead;
}
结语
好了,感谢你能看到这里,本篇到这就结束了,希望对你有所帮助
溜了溜了,我们下篇文章再见吧