算法 - Day4
快慢指针
上次我们了解了链表并且学习了链表经典的例题,今天我们一起来看看快慢指针相关的链表习题。
删除链表的倒数第N个结点
题目链接:19.删除链表的倒数第N个结点
第一种解法:
删除链表的倒数第N个结点,本题我们可以使用快慢指针来解决,我们可以先让快指针走上N步后在让慢指针和快指针同时开始走,当快指针走到空时,慢指针走到倒数第N个结点,但是本题不是找到倒数第N个结点而是要求我们删除倒数第N个节点,所以我们可以设置一个哨兵位,让慢指针和快指针都从哨兵位出发,这样当快指针走到空时,慢指针走到倒数第N个结点的前一个结点,这样我们更方便删除倒数第N个结点,下面我们来看下本题具体的代码。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* newnode = new ListNode(0, head);
ListNode* fast = newnode, *slow = newnode;
while(n-- && fast != nullptr)
{
fast = fast->next;
}
fast = fast->next;//因为快慢指针都是从哨兵位出发循环结束相当于走了N-1个结点,故出了循环还在要再走一步。
//快慢指针一起走直到快指针为空
while(fast != nullptr)
{
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow->next = slow->next->next;
delete tmp;
return newnode->next;
}
};
第二种解法:
上面这种解法利用了快慢指针的做法删除了倒数第N个结点,同样我们可以利用另外一种方法解决本题,我们在数据结构中非常常见的一种first in last out 的数据结构就是栈结构,我们可以将链表放入栈中,然后从栈顶弹出N个元素,当弹出N个元素后,栈顶元素即为倒数第N个元素的前一个元素,这样我们就可以删除倒数第N个元素。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
stack<ListNode*> st;
ListNode* newnode = new ListNode(0, head);
ListNode* cur = newnode;
while(cur)
{
st.push(cur);
cur = cur->next;
}
while(n--)
{
st.pop();
}
ListNode* prev = st.top();
ListNode* tmp = prev->next;
prev->next = prev->next->next;
delete tmp;
return newnode->next;
}
};
上面代码中使用哨兵位的原因是因为如果链表中只有一个结点时,当我们pop后栈中为空st.top()为空此时如果在使用prev->next会发生空指针解引用的错误,所以我们添加一个哨兵位为了防止类似情况发生。
链表相交
题目链接:160.链表相交
本题是判断两个单链表是否相交,但是这个相交和我们在数学上学的相交是有区别的,下面我们通过下面的图片来看下题目中的相交是什么意思。
有的人可能会问为什么链表相交后的结点是一样的,这里是因为我们通常使用的链表都是只有一个数据域和一个指针域(双链表除外),所以本题中链表相交后的结果一定是如图中第二个链表所示。
现在我们知道了链表相交后的结构,那么我们如何判断链表是否相交呢,这时我们可能会想要是链表相交前的长度一样就好了,这样两个指针一边判断是否相等一边移动,当两个指针相等时就可以确定两个链表相交否则则无交点,那么我们可以通过让长度长的链表先走n步和短的那个链表一样长之后就达到了我们想要的前提,具体代码可以参考下面的。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lengthA = 0, lengthB = 0;
ListNode* curA = headA, *curB = headB;
//先求出两个链表的长度
while(curA)
{
lengthA++;
curA = curA->next;
}
while(curB)
{
lengthB++;
curB = curB->next;
}
curA = headA;
curB = headB;
//我们假定A是长的那一个。
//如果A的长度小于B则A与B交换。
if(lengthA < lengthB)
{
swap(lengthA, lengthB);
swap(curA, curB);
}
//算出长度差值
int gap = lengthA - lengthB;
//让A和B一样长
while(gap--)
{
curA = curA->next;
}
//一起遍历并且判断是否相等,如果相等则返回相交结点,否则返回NULL。
while(curA)
{
if(curA == curB)
{
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
环形链表II
题目链接:142.环形链表
环形链表是一个比较有深度的问题,本题要求我们找到入环第一个结点,首先我们要判断该链表是否存在环,如果存在环我们需要返回第一个入环节点,但是如果不存在环的话,我们去寻找如入环结点也是没有意义的,那么我们如何判断一个链表是否有环呢,我们可以通过设置快慢指针来判断一个链表是否存在环,让快指针每次移动两个结点,慢指针每次移动一个结点,如果存在环的话,最后快指针一定是可以追上慢指针的,我们通过判断快慢指针是否相等来判断链表是否存在环结构,那么有同学可能会问为什么快指针一定会追上慢指针呢?有没有可能是快指针超过慢指针但是不相遇呢?这个问题我们可以很容易就回答首先如果存在环的话快指针一定首先进入环,而且快指针是每次走两个结点,慢指针是每次一个结点,也就是说快指针是一个结点一个结点的靠近慢指针,这样快指针和慢指针一定可以相遇。
上面我们解决了相遇的问题之后我们怎么找到入环第一个结点呢,这里需要我们使用数学公式来推导一下:
假设从头结点到入环结点的结点数为x,从入环结点到fast和slow相遇结点,结点数为y,从fast和slow相遇结点到入环结点的结点数为z,那么相遇时slow走过的结点个数为x + y, fast指针走过的结点个数为x + y + n(y + z),其中n为fast在环中走过的圈数,因为fast指针是一次走两个结点,slow指针是一次走一个结点,所以相遇时fast走过的结点 数 = slow走过结点数*2;
即 (x + y) * 2 = x + y + n(y+ z)化简可得: x = (n-1)(y+z) + z;这里的n一定是大于1的因为快指针追上慢指针至少要走一圈,当n = 1时x = z,这表明从头结点出发的指针到入环结点与从快慢指针相遇到入环结点走过的结点数相同,也就是说从头结点出发的指针一定会和从相遇点出发的指针在入环点相遇同样的当n大于1时也是这个道理,只不过是在相遇点的指针多走了(n-1)圈,所以我们可以判断是否相遇并找到相遇点之后,依据上面的结论建立两个指针一个指向头结点另一个指向相遇点,然后同时移动直到相等为止,即找到了入环结点,下面看一下具体的代码。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while(fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
//判断是否存在环,如果存在找到入环节点。
if(fast == slow)
{
//两个指针一个指向头结点,另一个指向相遇点。
ListNode* index1 = head;
ListNode* index2 = fast;
while(index1 != index2)
{
index1 = index1->next;
index2 = index2->next;
}
//当两个指针相等时为入环结点
return index1;
}
}
return NULL;
}
};
环形链表还是很值得思考的,希望小伙伴们认真思考并亲手推导一下数学公式,这样可以更深刻的理解环形链表。
觉得有帮助的小伙伴可以一键三连哟!!!