题目:环形链表
题目讲解:
判断环
要判断链表是否有环,可以使用快慢指针的方法。快指针每次走两步,慢指针每次走一步。如果链表有环,快慢指针最终会相遇;如果没有环,快指针会先到达链表末尾。
为什么快指针走两步,慢指针走一步?因为这样快指针会更快进入环,并在环中追上慢指针。具体来说,当快指针进入环后,它将在环中不断接近慢指针,最终相遇。即使快指针在环中比慢指针快,它们也不会永远错过,而是必然会在某一时刻相遇。
这个相遇的原因是因为在每一次循环中,快指针都比慢指针多走一步,这样慢指针总会缩小它与快指针之间的距离,直至两者相遇。
如果操场是一个环形的(即链表有环),那么即使快跑者一开始远远超过慢跑者,由于他们在同一条跑道上不断绕圈,快跑者最终一定会追上慢跑者。这是因为在每一圈中,快跑者都会比慢跑者多跑一圈的距离,最终两人会在某个点相遇。
代码实现:
class Solution
{
public:
bool hasCycle(ListNode *head)
{
//判断空链表及单节点
if(head == nullptr || head->next == nullptr)
{
return false;
}
ListNode* fast = head->next;
ListNode* slow = head;
while(fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
};
注意事项:
在处理链表时,必须仔细考虑 while
循环的判断条件。如果链表不是环形的,快指针每次移动两步可能会最终指向空引用,导致野指针问题。为了避免这种情况,你需要提前检查快指针的下一个节点是否为空。使用 &&
运算符可以确保两个条件都为真,这样可以防止当链表只有两个节点时发生错误——在这种情况下执行 fast = fast->next->next;
会导致野指针。
关于快指针最初指向第一个节点而慢指针指向空的做法,在这里并不适用。如果 slow
为空,执行 slow = slow->next;
会导致空指针引用错误。因此,必须根据具体问题定制你的方法,而不是盲目套用模式。
题目:环形链表||
题目讲解:
找环入口
数学解释
假设链表中从头到环入口点的距离是 a
环的长度是 b + c(
其中 b
是从环入口点到 slow
与 fast
相遇点的距离)
c
是从相遇点再回到入口点的距离。
相遇时slow走过的路程是 a + b
相遇时fast走过的路程是 a + (b+ c) + b
又因为fast走的路程是slow的两倍所以 2(a + b) == a + (b+ c) + b -> a = c
所以头到入口的距离就等于相遇点到入口的距离,这样的话让它们从头和快慢指针相遇的地点一起出发,等什么时候它们相遇那就什么时候遇到入口。
快慢指针相遇的理解
fast
指针比 slow
指针更早进入环,并且在环内移动得更快。当 slow
进入环时,fast
就开始追赶它。由于 fast
每次走两步,而 slow
只走一步,这就好像 fast
在不断地逼近一个“静止”的目标。你可以把这情况类比为 slow
不动,而 fast
每次向前移动一步。在这种情况下,fast
会很快追上 slow
。
因此,slow
并不需要走完整个环就会被 fast
追上。相遇是必然的,而 slow
没走完一整圈就和 fast
相遇的情况,正是因为 fast
在追击 slow
的过程中缩短了它们之间的距离。
代码实现:
class Solution
{
public:
ListNode *detectCycle(ListNode *head)
{
if(head == nullptr || head->next == nullptr)
{
return nullptr;
}
ListNode* fast = head;
ListNode* slow = head;
ListNode* Ptr = head;
while(fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
while(Ptr != fast)
{
Ptr = Ptr->next;
fast = fast->next;
}
return Ptr;
}
}
return nullptr;
}
};
题目:寻找重复数
题目讲解:
数组该怎样链表化?
你是否想过,数组中的每个元素其实可以看作链表中的一个节点?那么,这些节点该如何指向彼此呢?链表中的指向并不是随意的,它靠什么来决定呢?
答案就在数组的下标和元素之间的关系。下标永远是固定的,从 0 开始不变,而真正“变动”的是数组中的元素。每个元素的值决定了它指向的下一个位置。比如,数组的第一个元素是 1,它就会指向下标为 1 的位置。如果下标 1 的元素是 3,那么它再指向下标 3 的位置。如此循环下去,直到某个节点被指向两次,这就意味着找到了重复的数字。这样,你就把数组成功“链表化”了,利用这个方法轻松找出数组中的重复元素。
代码实现:
class Solution
{
public:
int findDuplicate(vector<int>& nums)
{
int fast = 0;
int slow = 0;
do
{
slow = nums[slow];
fast = nums[nums[fast]]; // 走两步
}while(slow != fast);
int tmp = 0;
while(tmp != fast)
{
tmp = nums[tmp];
fast = nums[fast];
}
return tmp;
}
};
题目:快乐水
题目讲解:
这个题目其实和之前的三道题思路类似。题目给出了两种情况:第一种是计算结果为 1,然后进入无限循环,循环的数字全是 1;第二种是永远算不出 1,进入一个与 1 无关的数字循环。这个循环可以被看作一个“环”,要么全是 1,要么是其他数。我们需要找到这个“环”的入口,也就是第一个 1 或者与 1 无关的数。
其实,这就是在找环的入口,完全可以用快慢指针来解决。快慢指针的核心思想是,一个走得快,一个走得慢,只要能满足这个条件,就可以称为快慢指针。在这里,我们让慢指针每次变化一次,快指针每次变化两次。如果它们相遇且值为 1,就返回 true
,否则返回 false
。
代码实现:
class Solution
{
public:
int qwe(int number)
{
int sum = 0;
int tmp = 0;
while(number)
{
tmp = number % 10;
sum += tmp * tmp;
number = number / 10;
}
return sum;
}
bool isHappy(int n)
{
int slow = n;
int fast = n;
do
{
slow = qwe(slow);
fast = qwe(fast);
fast = qwe(fast);
}
while(slow != fast);
if(slow == 1)
{
return true;
}
return false;
}
};
题目:删除链表的倒数第N个元素
题目讲解:
这道题可以先让快指针走n步然后再让慢指针和快指针一同走那这样快指针走到结尾那慢指针也就刚好移动到目标位置的前一个。
代码实现:
class Solution
{
public:
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* dummyHead = new ListNode(0 , head);
dummyHead->next = head;
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
for (int i = 0; i <= n; ++i)
{
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyHead->next;
}
};
这里定义哨兵位(虚拟头节点)的作用:
为什么要用虚拟头节点(哨兵位)?
- 当我们需要删除链表中的某个节点时,通常需要访问待删除节点的前一个节点,因为删除操作涉及到将前一个节点的
next
指针重新指向待删除节点的下一个节点。
2. 删除头节点的特殊情况
- 如果没有虚拟头节点,要删除头节点会比较麻烦,因为头节点前面没有其他节点,无法轻易访问它的前一个节点。因此,删除头节点通常需要特别处理,直接返回第二个节点来实现删除。
3. 虚拟头节点解决了什么问题?
- 引入虚拟头节点后,即使是删除头节点的操作,我们也可以按照统一的方式处理。虚拟头节点的
next
指向实际的头节点,这样我们就能在删除任何节点时,不需要再为删除头节点编写额外的特殊逻辑了。
4. 总结
- 统一的删除逻辑:有了虚拟头节点后,删除头节点就变得和删除其他节点一样,不需要再单独处理特殊情况。
- 代码简洁性:使用虚拟头节点让代码更简洁,因为所有节点的删除操作都可以通过相同的逻辑来处理,无需特别关注链表的第一个节点。
题目:链表的中间节点
题目讲解:
这题比较easy,所以我就贴代码算了。
代码实现:
class Solution
{
public:
ListNode* middleNode(ListNode* head)
{
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
题目:回文链表
题目讲解:
判断回文链表我们可以先利用快慢指针找到前部分的尾部然后然后再去反转后部分链表判断,然后再去判断它们的val是否一摸一样,我们唯一要注意的就是反转链表,
代码实现:
class Solution
{
public:
//反转中间节点后的数据
ListNode* reverse(ListNode* Newhead)
{
ListNode* PPfast = Newhead;
ListNode* PPslow = nullptr;
while(PPfast)
{
ListNode* next = PPfast->next;
PPfast->next = PPslow;
PPslow = PPfast;
PPfast = next;
}
Newhead = PPslow;
return Newhead;
}
bool isPalindrome(ListNode* head)
{
ListNode* dummyHead = new ListNode (0 , head);
dummyHead->next = head;
ListNode* fast = head;
ListNode* slow = head;
//
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
fast = reverse(slow);
slow = head;
while(fast && slow)
{
if(fast->val != slow->val)
{
return false;
}
fast = fast->next;
slow = slow->next;
}
return true;
}
};