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

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)。

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

posted @ 2020-02-08 20:42  longlongban  阅读(877)  评论(0编辑  收藏  举报