LeetCode--链表2-双指针问题

思考问题:

判断一个链表是否有环

列举几种情况:

graph LR
A-->B
B-->C
C-->D
D-->E
E-->C
graph LR
A-->B
B-->A

你可能已经使用哈希表提出了解决方案。但是,使用双指针技巧有一个更有效的解决方案。在阅读接下来的内容之前,试着自己仔细考虑一下。

想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。

这正是我们在链表中使用两个速度不同的指针时会遇到的情况:

如果没有环,快指针将停在链表的末尾。
如果有环,快指针最终将与慢指针相遇。

所以剩下的问题是:

这两个指针的适当速度应该是多少?

一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。

那其他选择呢?它们有用吗?它们会更高效吗?

题1 环形链表1(简单)

/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
// 当链表中没有节点或只有一个节点时 false
if(!head || !head->next)
return false;
// 两个节点的初始值都是head
ListNode* p1 = head;
ListNode* p2 = head;
//
while( p1->next && p2->next)
{
if(p1->next == p2->next->next)
return true;
p1 = p1->next;
p2 = p2->next->next;
if( !p1 || !p2 )
return false;
}
return false;
}
};

题2 环形链表2(中等题)

自己的思路:

巴拉巴拉

然而

自己的思路并不对...

看看人家的想法好了

graph LR
0-->1
1-->2
2-->3
3-->4
4-->5
5-->2

剑指offer上这道题的思路,主要就是运用双指针,起点不同。

设环内节点个数为n,那就一个从0节点出发,另一个从第n+1个节点出发。

相遇处,就是入口处。

说白了就是带环的相遇问题。

所以这道题需要解决几个问题

  1. 确定链表是否有环
  2. 确定链表内节点个数
  3. 确定入口节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
int num = counter(head);
if ( num == 0)
return NULL;
ListNode* pa = head;
ListNode* pb = head;
for (int i = 0 ; i < num; i++)
{
pb = pb->next;
}
while (pa->next && pa->next)
{
if(pa == pb)
return pb;
pa = pa->next;
pb = pb->next;
}
return NULL;
} int counter( ListNode* head )
{
// 链表为空或链表中只有一个节点-->不存在环-返回0
if( !head || !head->next )
return 0;
// 设置双指针
ListNode* p1 = head;
ListNode* p2 = head;
//
int count = 0;
while( p1->next && p2->next )
{
// 若p1和P2即将相遇,重新赋值,并开始计数
if( p1->next == p2->next->next)
{
p1 = p1->next;
p2 = p1->next;
count = 2;
while(p2->next)
{
if( p1 == p2 )
{
return count;
}
p2 = p2->next;
count ++;
}
}
p1 = p1->next ;
p2 = p2->next->next;
if(!p1||!p2)
return 0;
}
return 0;
}
};

超出时间限制-

修改为下面的代码,通过了测试;

修改内容:
  1. 避免了双重的while循环
  2. 避免while的循环的终止条件是真值
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 确定快慢指针相遇的节点
ListNode* pmeet = meeting(head);
if ( pmeet == NULL)
return NULL;
// 确定环内节点的个数
int count = 1 ;
ListNode* p1 = pmeet;
while( p1->next != pmeet )
{
p1 = p1->next;
count ++;
}
// 确定环的入口节点
ListNode* pa = head;
ListNode* pb = head;
for (int i = 0 ; i < count; i++)
{
pb = pb->next;
}
while ( pa != pb )
{
pa = pa->next;
pb = pb->next;
}
return pa;
} // 确定快慢指针相遇的节点
ListNode* meeting (ListNode* head )
{
// 链表为空或链表中只有一个节点-->不存在环-返回0
if( !head || !head->next )
return NULL;
// 设置双指针
ListNode* p1 = head;
ListNode* p2 = head;
//
ListNode* meet = head;
while( p1->next && p2->next )
{
// 若p1和P2即将相遇,重新赋值,并开始计数
if( p1->next == p2->next->next)
{
meet = p1->next;
return meet;
}
p1 = p1->next ;
p2 = p2->next->next;
if(!p1||!p2)
return NULL;
}
return NULL;
}
};

题3 相交链表

graph LR
A-->B
B-->C
C-->F
E-->F
D-->E
F-->G
G-->H
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 如果两个链表其中任意一个为空就不会有相交节点
if( !headA || !headB )
return NULL;
// 两个链表从头节点就相交了
if( headA == headB )
return headA;
ListNode* pa = headA;
ListNode* pb = headB;
// 求两个链表的长度
int numa = counter(headA);
int numb = counter(headB);
// 哪一个链表长,其指针就往前步进长度差的步长
int step = 0;
if ( numa >= numb )
{
step = numa - numb;
for(int i = 0; i < step ; ++i)
{
pa = pa->next;
}
}
else
{
step = numb - numa;
for(int j = 0 ; j < step; ++j)
{
pb = pb->next;
}
} // 定位第一个相同的节点
while ( pa && pb && (pa != pb) )
{
pa = pa->next;
pb = pb->next;
}
return pb; // 第二种循环的写法
/*
while ( pa && pb )
{
if ( pa == pb )
return pa;
pa = pa->next;
pb = pb->next;
}
return NULL;
*/
}
// 返回单链表中的节点数
int counter(ListNode* head)
{
ListNode* p = head;
int count = 1;
if( !p )
return 0;
if( !p->next )
return 1;
while( p->next )
{
p = p->next;
++count;
}
return count;
}
};

题4 删除链表中的倒数第n个节点

第一想法就是通过辅助栈求解

/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//
if( !head || n <= 0 )
return NULL;
// 建立一个辅助栈
stack<ListNode*> nodes;
// 遍历链表,依次放入栈中
ListNode* p = head;
while(p)
{
nodes.push(p);
p = p->next;
} if(n == 1)
{
nodes.pop();
ListNode* pend = nodes.top();
pend->next = nullptr;
return head;
}
// 遍历栈中的节点到第n-1个节点
int i = 1;
while ( i != n-1 && n > 1 )
{
if(nodes.empty())
return NULL;
nodes.pop();
++i;
}
ListNode* pe = nodes.top();
nodes.pop();
nodes.pop();
ListNode* ps = nodes.top();
ps->next = pe;
return head;
}
};

测试用例通过,但是提交解答报错

Line 152: Char 17: runtime error: reference binding to misaligned address 0xbebebebebebec0b6 for type 'struct ListNode *', which requires 8 byte alignment (stl_deque.h)

修改后代码如下:

/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//
if( !head || n <= 0 )
return NULL;
// 建立一个辅助栈
stack<ListNode*> nodes;
// 遍历链表,依次放入栈中
ListNode* p = head;
while(p)
{
nodes.push(p);
p = p->next;
}
// 倒数第1个节点
if(n == 1)
{
nodes.pop();
if(nodes.empty())
return NULL;
ListNode* pend = nodes.top();
pend->next = NULL;
return head;
}
// 倒数n-1个节点之前的节点出栈
int i = 1;
while ( i != n-1 && n >= 2 )
{
if(nodes.empty())
return NULL;
nodes.pop();
++i;
}
// 得到第n-1个节点,使其出栈
ListNode* pe = nodes.top();
nodes.pop();
// 第n个节点出栈
nodes.pop();
// 如果倒数第n个节点之前再无节点,head = pe
if(nodes.empty())
{
head = pe;
return head;
}
ListNode* ps = nodes.top();
ps->next = pe;
return head;
}
};

使用辅助栈的时候,代码的鲁棒性要十分注意

出栈后,栈是否为空一定要注意!!!

小结 - 链表中的双指针问题

代码模板:

// Initialize slow & fast pointers
ListNode* slow = head;
ListNode* fast = head;
/**
* Change this condition to fit specific problem.
* Attention: remember to avoid null-pointer error
**/
while (slow && fast && fast->next) {
slow = slow->next; // move slow pointer one step each time
fast = fast->next->next; // move fast pointer two steps each time
if (slow == fast) { // change this condition to fit specific problem
return true;
}
}
return false; // change return value to fit specific problem

提示

它与我们在数组中学到的内容类似。但它可能更棘手而且更容易出错。你应该注意以下几点:

  1. 在调用 next 字段之前,始终检查节点是否为空。

    获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。

  2. 仔细定义循环的结束条件。运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。

复杂度分析

空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数。

在前面的查找循环示例中,假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。

如果没有循环,快指针需要 N/2 次才能到达链表的末尾,其中 N 是链表的长度。
如果存在循环,则快指针需要 M 次才能赶上慢指针,其中 M 是列表中循环的长度。

显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)。

自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。

LeetCode--链表2-双指针问题的更多相关文章

  1. Leetcode链表

    Leetcode链表 一.闲聊 边学边刷的--慢慢写慢慢更 二.题目 1.移除链表元素 题干: 思路: 删除链表节点,就多了一个判断等值. 由于是单向链表,所以要删除节点时要找到目标节点的上一个节点, ...

  2. [LeetCode] [链表] 相关题目总结

    刷完了LeetCode链表相关的经典题目,总结一下用到的技巧: 技巧 哑节点--哑节点可以将很多特殊case(比如:NULL或者单节点问题)转化为一般case进行统一处理,这样代码实现更加简洁,优雅 ...

  3. LeetCode 链表题 ( Java )

    leetcode 237. 删除链表中的节点 链接:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/ 示例 : 输入: he ...

  4. C#LeetCode刷题-双指针

    双指针篇 # 题名 刷题 通过率 难度 3 无重复字符的最长子串   24.5% 中等 11 盛最多水的容器   43.5% 中等 15 三数之和   16.1% 中等 16 最接近的三数之和   3 ...

  5. [LeetCode] 链表反转相关题目

    暂时接触到LeetCode上与链表反转相关的题目一共有3道,在这篇博文里面总结一下.首先要讲一下我一开始思考的误区:链表的反转,不是改变节点的位置,而是改变每一个节点next指针的指向. 下面直接看看 ...

  6. LeetCode链表解题模板

    一.通用方法以及题目分类 0.遍历链表 方法代码如下,head可以为空: ListNode* p = head; while(p!=NULL) p = p->next; 可以在这个代码上进行修改 ...

  7. LeetCode链表相加-Python<二>

    上一篇:LeetCode两数之和-Python<一> 题目:https://leetcode-cn.com/problems/add-two-numbers/description/ 给定 ...

  8. leetcode 链表类型题总结

    链表测试框架示例: // leetcodeList.cpp : 定义控制台应用程序的入口点.vs2013 测试通过 // #include "stdafx.h" #include ...

  9. leetcode链表相关

    目录 2/445两数相加 综合题(328奇偶链表, 206反转链表, 21合并两个有序链表 ) 92反转链表 II 链表排序(148排序链表, 876链表的中间结点) 142环形链表 II 160相交 ...

  10. 【Leetcode链表】移除链表元素(203)

    题目 删除链表中等于给定值 val 的所有节点. 示例: 输入: 1->2->6->3->4->5->6, val = 6 输出: 1->2->3-&g ...

随机推荐

  1. MySQL不能通过127.0.0.1访问

    检查权限都是正确的,最后想到是防火墙的问题 -A INPUT -d 127.0.0.1/32 -j ACCEPT-A INPUT -s 127.0.0.1/32 -j ACCEPT 搞定

  2. eclipse配置svn若干点

    eclipse 或者针对java的,或者eclipse for php ,都行. 可以直接在线安装svn插件,也可以下载好插件后自己配置. ------------ 一下转载自http://blog. ...

  3. MVP简要示例

      MVP即Model-View-Presenter,是从经典的MVC演变而来的,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理.Model提供数据.View负责显 ...

  4. 工作常见的git命令

     Git创建项目仓库: 1.git init 初始化   2.git remote add origin url 关联远程仓库   3.git pull  拉取远程仓库到本地  相当于(git fet ...

  5. class.forName() 和 classLoader 的区别

    相同点:        java中class.forName() 和 classLoader 都可用来对类进行加载 不同店:        1.class.forName()除了将类的 .class ...

  6. spring cloud解决eureka的client注册时默认使用hostname而不是ip

    eureka的client注册到server时默认是使用hostname而不是ip.这样的话使用feign client通过eureka进行服务间相互调用时也会使用hostname进行调用,从而调用失 ...

  7. mac vmware fusion10 nat 模式网络配置

    mac vmware fusion10 nat 模式网络配置 1.虚拟机选择 nat 模式 虚拟机-->网络适配器-->网络适配器设置-->连接网络适配器(对勾)-->与我的 ...

  8. 被这个C程序折腾死了

    The C programming language 的第13页,1.5.3 行计数的那里,那个统计换行符个数的程序我好像无法运行,无论输入什么,按多少下enter,什么都出不来. #include& ...

  9. [LC] 159. Longest Substring with At Most Two Distinct Characters

    Given a string s , find the length of the longest substring t  that contains at most 2 distinct char ...

  10. 数字签名和数字证书等openssl加密基本术语

    openssl 算法基础 1.1 对称算法 : 密钥相同,加密解密使用同一密钥 1.2 摘要算法:无论用户输入的数据什么长度,输出的都是固定长度的密文:即信息摘要,可用于判断数据的完整性:目前常用的有 ...