码迷,mamicode.com
首页 > 编程语言 > 详细

常见和链表相关的算法

时间:2015-02-02 07:05:12      阅读:211      评论:0      收藏:0      [点我收藏+]

标签:

一、 链表排序
    链表排序和数组排序的思路类似,只是链表操作起来比较麻烦,因为不能随机访问,所以只能借助于类似于前置或后置插入,添加等概念来完成。下面给出了链表排序的几种方法。
辅助代码:
//单链表节点的定义
typedef struct LinkNode{
    int val;
    struct LinkNode* next;
}LinkNode;
 
 
//由一个数组创建单链表
LinkNode* CreateList(int A[], int count)
{
    if(NULL == A)
        return NULL;
 
    LinkNode* head = (LinkNode*)malloc(sizeof(LinkNode));
    head->val = A[0];
    head->next = NULL;
    LinkNode* p = head;
   
    for (int i = 1; i < count; ++i)
    {
        p->next = (LinkNode*)malloc(sizeof(LinkNode));
        p->next->val = A[i];
        p->next->next = NULL;
        p = p->next;
    }
 
 
    return head;
}
 
 
//显示该单链表
void ShowList(LinkNode* L)
{
    LinkNode* p = L;
    printf("%d", p->val);
 
 
    while(p->next)
    {
        p = p->next;
        printf("-->%d", p->val);
    }
 
 
    printf("\n");
}
 
 
1. 插入排序(以从小到大排序为例)
    链表排序最容易想到的是插入排序,它的基本想法是从第一个节点开始,每次将一个节点放到结果链表,并保证每个节点加入到结果链表前后,结果链表都是有序的。每个节点在被链入结果链表时有三种情况:
1)该节点值比结果链表中的所有元素值都大,则将该节点追加到结果链表的最后;
2)该节点值比结果链表中的所有元素值都小,则将该节点插入到结果链表最前面;
3)该节点值在结果链表中处于中间位置,则将该节点插入到合适位置。
    下面是该算法思路的流程图:
 image
   
    代码实现如下:
 
LinkNode* SortLinkListInsertion(LinkNode* head)
{
    //链表空或链表只有一个节点,不必排序,直接返回原头结点
    if (NULL == head || NULL == head->next)
    {
        return head;
    }
 
 
    //我们从第二个节点进行处理,第一个节点作为结果链表的初始节点
    LinkNode* r = head->next;
    LinkNode* tmp;
    head->next = NULL; //将结果链表末尾标记为结束
 
 
    while(NULL != r) //依次处理每一个节点
    {
       
        if (r->val < head->val)
        {
            //将该节点插到结果链表最前,并修改链表头,同时注意辅助变量的使用
            tmp = r->next;
            r->next = head;
            head = r;
            r = tmp;
        }
        else
        {
            //否则从链表头部开始搜索,注意这里搜索结束的条件是当前被搜索节点不是最后一个节点
            LinkNode* p = head;
            while(NULL != p->next)
            {
                //注意只有当节点严格小于被搜索节点的下一节点的值是,才将其插入到被搜索节点后
                //这样能保证排序是稳定的!
                if (r->val < p->next->val)
                {
                    tmp = r->next;
                    r->next = p->next;
                    p->next = r;
                    r = tmp;
                    continue; //注意跳出,开始处理下一个节点
                }
                else
                {
                    p = p->next;
                }
            }
 
 
 
            //此时,p是结果链表中的末尾节点,将被处理节点追加到其后
            if (NULL == p->next)
            {
                tmp = r->next;
                r->next = NULL;
                p->next = r;
                r = tmp;
            }
        }//end else
    }//end while(NULL != r)
 
 
    return head;
}
 
2.  选择排序(以从小到大排序为例)
    选择排序的基本思想是每次从源链表中选取并移除一个最小的元素,追加到结果链表的尾部,直到源链表变空为止。因此本算法的关键点在于如何从源链表中选取并移除一个最小的元素。考虑到一般情况下,我们在移除链表中的某个元素时,需要知道它的前一个节点的指针(我知道你在想那个trick,但我想我们还是用正统的方法吧),于是我们可以按照最小元素的出现位置分为两种情况:最小元素是链表头部节点和最小元素不是链表头部节点。我们先找出链表中除头结点外的最小值节点,然后再和头节点的值比较,然后进行处理。寻找除头结点外的最小值节点的代码可以很简洁,这也是为什么要分成这两部分处理的原因。
 
 image
 
    代码实现如下:
LinkNode* SortLinkListSelection2(LinkNode* head)
{
    //我们这里即使不进行特殊情况处理,代码也能正常工作,可以代入检查
    //if (NULL == head || NULL == head->next)
    //{
    //    return head;
    //}
 
    LinkNode* p = NULL; //遍历辅助变量
    LinkNode* pminpre = NULL; //指向源链表中除头结点外的最小值节点的前驱节点
    LinkNode L = {0, NULL}; //我们这里用了一个哑节点,它能简化后面的代码
    LinkNode* Ltail = &L;  //Ltail用于指向结果链表的最后一个节点
 
    while (NULL != head && NULL != head->next) //循环处理源链表中节点个数不小于2个的情况
    {
        pminpre = head;
        p = head->next;
        while(NULL != p && NULL != p->next) //找出源链表中除头结点外的最小值节点的前驱节点
        {
            if (p->next->val < pminpre->next->val) //严格小于时才改变pminpre
                pminpre = p;
            p = p->next;
        }
        if (head->val <= pminpre->next->val) //和头结点值进行比较处理,值相等时,取头结点
        {
            Ltail = Ltail->next = head;
            head = head->next;
        }
        else
        {
            Ltail = Ltail->next = pminpre->next;
            pminpre->next = pminpre->next->next;
        }
    }
 
    Ltail = Ltail->next = head; //最后一个节点直接追加到结果链表的尾部
    Ltail->next = NULL; //设置结果链表的结束标记
 
    return L.next;
}
 
注意上面if语句中的 < 和 <= 判断,他们能使得链表的选择排序是稳定的排序。
 
3.  冒泡排序(以从小到大排序为例)
    链表的冒泡排序不太好想,主要是不太好控制比较边界。这里我们用一个标记指针end表示边界,每完成一趟冒泡后,end会被向前移一下,直到完成N-1趟冒泡。冒泡需要比较并交换相邻的节点,因此我们在实现中使用了pre,cur,n等指针分别表示当前处理节点的前驱,当前处理节点和下一节点。
 image
    链表冒泡排序的实现如下:
 
//asscending sort link list
LinkNode* SortLinkListBubble(LinkNode* head)
{
    if (NULL == head)
    {
        return head;
    }
 
 
    //init end pointer
    LinkNode* end = NULL;
 
 
    while(true)
    {
        LinkNode* n = head->next;
        if (n == end)
            break;
 
 
 
        //这里单独处理头结点和第二个节点的比较,这是没有哑头节点的链表的一个劣势
        if (n->val < head->val)
        {
            head->next = n->next;
            n->next = head;
            head = n;
        }
 
 
        LinkNode* pre = head;
        LinkNode* cur = head->next;
        LinkNode* tmp;
        n = cur->next;
 
 
        while(n != end)
        {
            if (n->val < cur->val)
            {
                tmp = n->next;
                cur->next = n->next;
                n->next = cur;
                pre->next = n;
                n = tmp;
            }
            else
            {
                n = n->next;
            }
            pre = pre->next;
            cur = cur->next;
        }
 
 
        end = cur;
    }
 
 
    return head;
}
 
4.  快速排序(从小到大排序)
    知道链表还可以快速排序是在笔试某公司时才发现的,当时只看到了个QsortList什么的,而且是个填空题,当时没有思路,也没有做出来,只记得那个QsortList带了3个参数,还返回了个链接点指针。回来后细想了一阵,发现原理和数组的快速排序是一样的,只是链表在处理指针时比较麻烦,而且要保证排序后链表还是有序地链接在一起,不能出现掉链等情况,有些绕人。
    思路这样,从链表中选取一个节点(一般简单地取头结点),将其值作为pivot,将链表中剩余的节点分成两个子链表,其中一个中的所有值都小于pivot,另一个中的所有值都不小于pivot,然后将这两条链表分别链接到pivot节点的两端。然后对于子链表分别进行递归该过程,直到子链表中只有一个节点为止。
    下面给出了链表快速排序的一种实现方法,该实现没有返回参数,链表头也是通过引用方式传入的。
void QsortList(LinkNode*& head, LinkNode* end)
{
    if(head == NULL || head == end)
        return;
    LinkNode* pivot = head;
    LinkNode* p = head->next;
    LinkNode* head1 = NULL, *tail1 = NULL;
    LinkNode* head2 = NULL, *tail2 = NULL;
    while(p != end)
    {
        if(p->val < pivot->val)
        {
            if(head1 == NULL)
            {
                head1 = tail1 = p;
            }
            else
            {
                tail1->next = p;
                tail1 = p;
            }
        }
        else
        {
            if(head2 == NULL)
            {
                head2 = tail2 = p;
            }
            else
            {
                tail2->next = p;
                tail2 = p;
            }
        }
        p = p->next;
    }
    if (tail1)
        tail1->next = pivot;
    if (head2)
        pivot->next = head2;
    else
        pivot->next = end;
    if (tail2)
        tail2->next = end;
    if (head1)
        head = head1;
    else
        head = pivot;
    QsortList(head, pivot); //这里是传入head, 而不能传入head1,因为head还可能被子调用修改
   
    //同样这里传入pivot->next而非head2,这样才能保证最后链表是有序链在一起的    
    QsortList(pivot->next, end);
}
调用QsortList时采用这样的方式QsortList(L, NULL);
数组的快速排序是不稳定的,原因是其实现时采用了交换的机制;而链表的快速排序则是稳定的,其原因是,被扫描的节点是有序地依次添加到子链表的末尾,保证了两个等值节点的相对位置不变。
 
 
二、关于链表的其他笔试面试题
    很多公司都考这个东西,其实考来考去本质就是那几道题,比较基础的东西,弄懂了其他类似的稍微变一下就行了。
1. 单链表倒置
    就是倒插法了,假想现在有一个空链表(这是链表的一个很好的优点,定义一个指针,你就可以声称创建了一个链表了,很节省空间),对输入的链表,从头到尾进行扫描,把每个节点都插入到假想链表的头部,然后返回假想的链表就可以,唯一需要注意的就是,边界情况和结束标记的NULL指针。
下面是一种实现:
Node* reverse(Node *head)
{
         if(NULL == head)
                   return head;
         Node *p = head->next;
         head->next = NULL;
         Node *tmp;
         while(NULL != p){
                   tmp = p->next;
                   p->next = head;
                   head = p;
                   p = tmp;
         }
         return head;
}
关于单链表的倒置,还有个递归的算法,虽然该算法在实际工作中没有任何作用,除了应付某些考试或仅仅作为一种谈资之外(当然,也可以说是在启发思维):
Node *reverse(Node *head, Node *pre) {
    Node *p = head->next;
head->next = pre;
if(p)
    return reverse(p, head);
else 
    return head;
}
调用方式:reverse(head, NULL);
 
2. 求链表的倒数第K个节点,如果K大于链表长度则返回NULL。
    这也是某公司的笔试题之一,出题者的意图是不想让做题者遍历两次链表,即你不能先数链表中到底有几个节点。在不知道链表到底有几个节点的前提下找到倒数第K个节点,嗯,听起来很酷,虽然可以先数数到底有多少个,做做减法,再去找顺数第多少个(实际上这个笨笨的方法和那个酷酷的方法进行的机器操作数的差别基本可以忽略,只是在链表非常长,而K值很小的情况下,笨方法需要再从头来过,而酷方法可以每个节点过两次,因而占到了局部性原理的便宜)。说了这么多,还是看看实现吧,看了就明白了。
Node* BackKth(Node* head, int K)
{
    if(NULL == head || K <= 0)
        return NULL;
   
    Node* pK = head;
    while(NULL != pK && K > 1) //Note that head is the first Node
    {
        pK = pK->next;
        K--;
}
 
 
if(NULL == pK)          //K > LengthOfList
    return NULL;
 
Node* p = head;
 
 
while(NULL != pK->next) //Not the last Node
{
    p = p->next;
    pK = pK->next;
}
    return p;
}
 
3.  求链表的第K个节点
    “搞笑么?哪个公司会出这样的题啊?”呵呵,没有公司会出这样的题,我自己想的。只是为了下面可能用到,高手请跳过。
Node* KthNode(Node* head, int K)
{
    if(K <= 0)
        return NULL;
 
 
 
   
Node* p = head;
    while(NULL != p && K > 1)
    {
        p = p->next;
        K--;
    }
   
    return p;
}
 
4.  不知道单链表节点前驱的情况下,删除该节点
    被问到过这样的问题,我笑而不语。这只能算一个小小的trick,其方法就是将该节点的后继结点中的值数据拷到本节点中,然后删掉后继节点。要是节点中的数据很多很庞大,而且还需要深拷贝呢?=。=!
 
5.  求单链表的中间节点,偶数个节点时返回中间两个节点的前一个
    这是一个很经典的问题。当然也可以先数数链表中有多少个节点,然后遍历一半,这很直接,但是不够巧妙,相信也不会是提问者想要的答案。我们可以这样考虑,设想两个人跑步,一个人的速度是另外一个人的两倍,当速度快的人到达了终点,速度慢的人就在赛程的正中间。同样地,我们设置两个游动的指针,慢的指针移动步长为1,快的指针移动的步长为2。一开始都指向链表的头部,当遍历开始时,进行这样的操作:如果快的指针可以向前移动两步并且没有到达链表的尾部的话,快指针就向前移动2个节点,同时慢的指针向前移动1个节点;否则退出,返回慢指针。该算法的实现如下,注意区别链表长度为偶数和奇数的情况:
Node *FindMid(Node* head)
{
    if(NULL == head || NULL == head->next)
        return head;
        
    Node* p1 = head;
    Node* p2 = head;
    while(NULL != p2->next && NULL != p2->next->next)
    {
        p1 = p1->next;
        p2 = p2->next->next;
    }
    return p1;
}
 
现在把问题稍微改变一下,变成偶数个节点时返回中间两个节点的后一个。上面的算法中的循环需要稍微调整一下。前面的算法中,快指针p2每次都移动2步,而本问题中,我们需要改变下策略,变成:只要快指针p2能向前移动一步,那么快指针和慢指针就都同时向前移动一步,再看看快指针是否还能补上一步,如果能补上一步,那么就补上,然后进行下一轮循环,否则就表示到达了链表尾部,返回慢指针即可。实现如下:
Node* FindMin2(Node* head)
{
    if(NULL == head || NULL == head->next)
        return head;
        
    Node* p1 = head;
    Node* p2 = head;
        
    while(NULL != p2->next)
    {
        p2 = p2->next;
        p1 = p1->next;
        if(NULL == p2->next)
            return p1;
        else
            p2 = p2->next;
    }
 
 
    return p1;
}
这种步长法的想法很巧妙,后面的题中还会使用。
 
6. 判断单链表是否有环,并求出环开始的节点,参考[1]。如果没有环,就返回NULL。如下图中所示,该单链表环开始的节点是3。
 image
    首先,我们判断单链表是否有环,这里我们也借用了步长法,即指针p1,p2都指向链表头,然后开始遍历,p1每次移动一步,p2每次移动两步,然后判断p2有没有遇到p1。如果遇到了p1,说明链表有环;如果遇到之前,p2就已经到达链表尾部(值为NULL),说明链表没有环。判断单链表是否有环算法的实现如下:
BOOL FindCirleStart(LinkNode* L)
{
    LinkNode* p1 = L;
    LinkNode* p2 = L;
 
 
 
    while (NULL != p2->next)
    {
        p2 = p2->next;
 
 
        if(NULL == p2->next) //Not a cyclic link list
        {
            return FALSE;
        }
 
 
        p2 = p2->next;
        p1 = p1->next;
 
 
        if (p1 == p2)
        {
           return TRUE;
        }
    }
 
 
    return FALSE;
}
如何证明这个算法的正确性?p2最多要绕环转几圈才能追上p1?关于这一点可以参考[1]中的证明,这里也再转述下。
clip_image002
    上面的图是链表有环时的示意图,原作者没有把它化成链表结构,而只是以连续的线条代替,我们在考虑问题的时候需要注意到它们是离散的节点。下面就详细分析该算法的思路:
a. 如果链表无环,我们的算法是能得到正确的结果的;
b. 这里我们考虑链表有环的情况。按照算法的流程,当慢指针p1,到达环开始节点A时,此时快节点必定在环中的某个节点,我们假设为B。这里我们只是随便假设B而已,B可能在环中的任意位置,也有可能就是节点A(那样的话,算法就直接得到结果了)。我们假设指针以顺时针方向在环中移动,B距离A的长度为y个节点,可以认为B处的快指针p2落后于A处的快指针p1的距离为y(0 <= y < LengthOfCircle)个节点。现在开始赛跑,每轮循环快指针p2向前跑2个节点,慢指针p1向前跑1个节点,两者综合起来的效果就是快指针和慢指针的距离减少1个节点,因此经过y轮循环之后,两指针将相遇,且相遇点为A向前y个节点的M点。因此证明了上述算法是正确的。
 
下面我们再来看看作者是怎样求出环开始的节点A,顺便计算出环的长度的。
在前面的证明中,我们是从慢指针到达A点开始证明的,而忽略了前面从链表头到A点的x个距离。现在假设慢指针从链表头head到达A点时,快指针p2已经移动到了B点,设环长为s个节点,那么有关系式:2x = x + n * s + s - y。即x = (n+1)s–y。当慢指针p1和快指针p2同时到达M点时,即到达前面证明的最后时,我们再增加下面一些操作:另起一个指针p从链表头head开始移动,每次移动一个节点,同样慢指针p1也继续从M点向前移动。当p经过x步到达A时,p1也经过n*s + s - y到达A点,和p相遇,这样我们就找到了A点。在此过程中,我们可以同时记下p1反复经过M点的次数n,然后利用s = (x + y)/ (n + 1)计算环的长度。注意,我们无法直接单独得到x或y的值,但却能统计从head开始到慢指针p1和快指针p2第一次在M点相遇时经过的循环次数x + y。下面的实现中体现了这一点:
LinkNode* FindCirleStart(LinkNode* L, int &nCircleLen)
{
    LinkNode* p1 = L;
    LinkNode* p2 = L;
 
 
    int xy = 0; //x + y
 
 
    while (NULL != p2->next)
    {
        p2 = p2->next;
 
 
        if(NULL == p2->next) //Not a cyclic link list
        {
            return NULL;
        }
 
 
        p2 = p2->next;
        p1 = p1->next;
        xy++;
 
 
        if (p1 == p2)
        {
            LinkNode* M = p2;
            LinkNode* p = L;
            int n = 0;
            while (p != p1)
            {
                p = p->next;
                p1 = p1->next;
                if (p1 == M)
                {
                    n++;
                }
            }
 
 
            nCircleLen = xy/(n+1);
            return p; //此时的p即为A点
        }
    }
 
 
    return NULL;
}
 
另一种求环长的方法:当p1和p2第一次在M相遇时,我们已经知道了链表是有环的了,所以还可以通过一种简单的计数方法求环的长度,即用指针遍历环一圈,直到重新回到M点。只是这样,我们是无法得到环开始的节点A的。
 
不难证明,以上算法的复杂度是线性的。下面给出一个引申的问题,该问题也是类似,可以在线性时间内解决:
如果一个单链表可能有环,如何计算该链表的长度?
可以通过前面的方法判断是否有环,找到环开始节点A,并求出环长度。然后再计算从链表头head到A的距离,然后做计算。
 
7.  判断两个单链表是否相交,如果相交则返回交点的指针,否则返回NULL。
    这是《编程之美》上面的一个题,关于这个问题,一般的讨论都是基于无环单链表的,即第1)种情况,这里我们也讨论第2)种情况,即单链表存在环的情况,最后我们也讨论下,不知道到底属于那种情况下的解决方法。
1)无环情况
 image无交点
 image
有交点
    如上两图中分别显示了两单链表无环的情况下,无交点和有交点的情况。无环情况的判断和求交点都比较简单。判断的思路如下:分别找到L1链表和L2链表的最后一个节点,判断它们是不是同一个节点,如果是同一个节点,那么就两链表就是相交的;如果是不同的两节点,就是不相交的。求交点思路有些巧妙:在判断是否存在交点遍历两链表的时候,我们可以顺便分别计算两链表的长度,然后计算其长度差d,再分别从短链表和长链表的第d个节点开始遍历并判断两者是否相等,第一个相等的节点指针就是交点指针。实现如下:
//Return NULL if there no crossing node
Node* FindCrossingNodeNoLoop(Node* head1, Node* head2)
{
         if(NULL == head1 || NULL == head2)
                   return NULL;
 
 
         int Len1 = 1;
         Node* p1 = head1;
         while(NULL != p1->next){
                   Len1++;
                   p1 = p1->next;
         }
 
 
         int Len2 = 1;
         Node* p2 = head2;
         while(NULL != p2->next){
                   Len2++;
                   p2 = p2->next;
         }
 
 
         if(p1 != p2)         //Different ending Node, no crossing node
                  return NULL;
        
         p1 = head1;
         p2 = head2;
         if(Len1 > Len2){
                   int K = Len1 – Len2;
                   while(K > 0){
                            p1 = p1->next;
                            K--;
                   }
         }
         else if (Len1 < Len2){
                   int K = Len2 – Len1;
                   while(k > 0){
                            p2 = p2->next;
                            K--;
                   }
         }
        
         while(p1 != p2){
                   p1 = p1->next;
                   p2 = p2->next;
         }
        
         return p1;
}
 
2) 有环情况
    无交点情况(1)和无交点情况(2)分别显示了一条及两条链表有环时,不相交的情况,这两种情况在设计算法时需要做一些考虑。
 image 无交点情况(1)
 image 无交点情况(2)
   
    有交点情况(1)显示了环在交点后面的情况。
 image
有交点情况(1)
    有交点情况(2)显示了交点是环开始的节点的情况。
 image
有交点情况(2)
    有交点情况(3)显示了链表交点在环中间的情况。这种情况比较复杂,此时我们既可以认为交点是A节点,也可以认为交点是B节点。如果我们把环看成是L2的,那么就是L1交L2于A点;如果我们把环看成L1的,那么就是L2交L1于B点。实际上,这个环也是L1和L2共有的。因此此种情况下A和B都是链表的交点,我们只能通过距离远近来分辨了。距离L1较近的交点是A,距离L2较近的焦点是B。这个问题具有对称性,后面我们会利用该性质。
 image
有交点情况(3)
   
    如何判断有环情况下链表是否存在交点,并求出交点,这个问题似乎比较复杂,我们不能直接使用无环情况下的算法了,因为我们无法进行链表结束判断。不过,如果我们将其转化无环情况的话,就能使用已有的算法了。想想我们之前找到有环链表的环开始节点,以及求环长度的算法,这里我们可以直接利用。知道环开始节点和环长度后,我们就可以找到环的末节点(理论上,环是没有末节点的,这里的末节点就是环开始节点的前驱节点),然后断开环。下面是有交点情况(1)和(2)断开环后的示意图,实际上除了环开始节点和交点的相对位置不同外,这两种情况可以归为一种类型:两链表环开始节点相同。image
有交点情况(1)断开后
 image
有交点情况(2)断开后
    对于有交点情况(3),它属于另一种类型:两链表的环开始节点不同。对于该种类型,我们有两种断开方法,一种是选择断开L1的环,另一种选择是断开L2的环。实际上,由于两链表是共享环的,所以随便从哪个链表断开环,另外一个链表中的环也就自动断开了。下面是分别从L1和L2断开环的情况:
 image 有交点情况(3),从L1断开环
 image
有交点情况(3),从L2断开环
    这里我再给出有交点情况(3)的另一种表达方式,它和前面的图没有什么区别,只是可能看起来舒服些:
 image 有交点情况(3)的等效图
    因此我们不难看出,既可以说L1交L2于A,也可以说L2交L1于B。如果我们选择断开L1,那么所求的交点为B,如果我们从L2断开,那么所求交点为A。
    另外一点需要注意的是,我们再断开环后,还要在适当的时候对其进行还原,即重置End的指针为Start节点的地址。
    基于问题的全面性考虑,下面是无交点情况下经过断开处理后的示意图:
 image
无交点情况(1)断开后
 image
无交点情况(2)断开后
    最后给出有环情况下的求链表交点的算法,注意该算法的前提是链表有环:
Node* FindCrossingNodeLoop(Node* L1, Node* L2)
{
    int nCircleLen1;
    Node* Start1 = FindCircleStart(L1, nCircleLen1);
    Node* Last1 = NULL;
    Node* cNode = NULL; //crossing node
 
    if(NULL == Start1)       
    {
        //L1中不存在环,结合前提,L2中必存在环,那么L1和L2肯定不相交,参考无交点情况(1)
        return NULL;
    }
    else
    {
        Last1 = KthNode(Start1, nCircleLen1); //找到环的末节点,这是前面的一个小算法
        Last1->next = NULL;  //断开L1中的环
    }
   
    //执行到这里时,说明L1中存在环,并且已经被断开了   
 
    int nCircleLen2;
    Node* Start2 = FindCircleStart(L2, nCircleLen2);
    Node* Last2 = NULL;
    if(NULL != Start2)       
    {
        //L2中仍然存在环路,那么必然是不同的环,L1和L2也肯定不相交,参考无交点情况(2)
        goto end;
    }
            
    //从L1的末尾断掉环,按(1)处理
    cNode = FindCrossingNodeNoLoop(L1, L2);
 
end:
    Last1->next = Start1;  //注意还原环
    return cNode;
}
    此函数是断开第一条链表,以上面的有交点情况(3)为例子,如果我们调用FindCrossingNodeLoop(L1, L2),那么我们将得到B节点;如果我们调用FindCrossingNodeLoop(L2, L1),那么将得到A点。对于有交点情况(1)和(2),两种调用得到的A和B将为同一个点。
 
3) 不知道是否有环情况
    FindCrossingNodeNoLoop和FindCrossingNodeLoop是分别已知链表无环和有环情况下对应的解决方法。如果我们事先并不确定链表是否有没有环,那么我们该如何写FindCrossingNode函数呢?我们可以用下面的方法解决:
Node* FindCrossingNode(Node* L1, Node* L2)
{
    int nCircleLen1;
    Node* Start1 = FindCircleStart(L1, nCircleLen1);
    Node* Last1 = NULL;
    int nCircleLen2;
    Node* Start2 = FindCircleStart(L2, nCircleLen2);
    Node* Last2 = NULL;
    Node* cNode = NULL; //crossing node
 
    if(NULL == Start1 && NULL == Start2)
    {
        //L1和L2都无环
        return FindCrossingNodeNoLoop(L1, L2);
    }
   
    if(NULL != Start1)
    {
        Last1 = KthNode(Start1, nCircleLen1);
        Last1->next = NULL;//断开L1的环
        Start2 = FindCircleStart(L2, nCircleLen2);
        if(NULL != Start2)
            goto end; //仍然能找到环,那就属于有环无交点情况(2)
        cNode = FindCrossingNodeNoLoop(L1, L2);
    }
    else
    {
        //根据判断条件,此时必然是L1中无环,L2中有环,此时两链表不可能相交
        //这里我们可以什么都不做
    }
 
end:
    if(NULL != Last1)
        Last1->next = Start1;
    if(NULL != Last2)
        Last2->next = Start2;
   
    return cNode;
}
   
    以上是算法的初步实现,相信该算法还可以做一些化简和调整方面的工作,不过暂时我就写到这里吧。
 
参考:
[1] 判断单链表是否有环,求环长和环路起始节点 http://tan1019.spaces.live.com/blog/cns!1880820F8EF99DDA!466.entry?wa=wsignin1.0&sa=185164612
之前写了篇关于链表的博客,突然想起以前遇到的一些关于链表环问题的情况。所以赶紧百度了一下,现在作一下整理。
给定一个单链表,只给出头指针h:
1、如何判断是否存在环?
2、如何知道环的长度?
3、如何找出环的连接点在哪里?
4、带环链表的长度是多少?

1、如何判断是否存在环?
对于问题1,使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
bool IsExitsLoop(slist *head)  
{  
    slist *slow = head, *fast = head;  
  
    while ( fast && fast->next )   
    {  
        slow = slow->next;  
        fast = fast->next->next;  
        if ( slow == fast ) break;  
    }  
  
    return !(fast == NULL || fast->next == NULL);  
}  

2、如何知道环的长度?
对于问题2,记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。

3、如何找出环的连接点(入口)在哪里?
设环的长度为r,链表长度为L,节点相遇时slow走了s步,fast在环中转了n圈,入口环与相遇点距离为x,起点到环入口点的距离为a。slow走一步,fast走两步。
因此
2s = nr + s
s = nr

s = a + x
L = a + r => r = L – a

a + x = nr
a = nr - x

由上式可知:若在头结点和相遇结点分别设一指针,同步(单步)前进,则最后一定相遇在环入口结点。
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
slist* FindLoopPort(slist *head)  
{  
    slist *slow = head, *fast = head;  
  
    while ( fast && fast->next )   
    {  
        slow = slow->next;  
        fast = fast->next->next;  
        if ( slow == fast ) break;  
    }  
  
    if (fast == NULL || fast->next == NULL)  
        return NULL;  
  
    slow = head;  
    while (slow != fast)  
    {  
         slow = slow->next;  
         fast = fast->next;  
    }  
  
    return slow;  
}  

4、带环链表的长度是多少?
问题3中已经求出连接点距离头指针的长度,加上问题2中求出的环的长度,二者之和就是带环单链表的长度。

参考:
http://blog.sina.com.cn/s/blog_725dd1010100tqwp.html
http://blog.csdn.net/liuxialong/article/details/6555850
http://snprintf.net/archives/575

  

问题:
        给定一个有环的链表,写一个算法,找出环的起点。
例如:
输入:A->B->C->D->E->C[与前面的C是同一个节点]
输出:C

判断一个链表是否存在环有一个简单的方法,就是使用一个快指针、和一个慢指针,快指针每次走两步,慢指针每次走一步,则如果有环,它们最后必然会相遇的。本题的难点在于要找出环的起点。其实也不难,与判断是否有环类似,用两个步长分别为1和2的指针遍历链表,直到两者相遇,此时慢指针走过的长度就是环的长度。另外相遇后把其中指针重新设定为起始点,让两个指针以步长1再走一遍链表,相遇点就是环的起始点。
证明也很简单,证明如下:

我们注意到第一次相遇时

慢指针走过的路程S1 = 非环部分长度 + 弧A长

快指针走过的路程S2 = 非环部分长度 + n * 环长 + 弧A长

S1 * 2 = S2,可得 非环部分长度 = n * 环长 - 弧A长

让指针A回到起始点后,走过一个非环部分长度,指针B走过了相等的长度,也就是n * 环长 - 弧A长,正好回到环的开头。

算法如下:


<span style="font-size:18px;color:#3366ff;background-color: rgb(255, 255, 255); ">LinkedListNode FindBeginning( LinkedListNode head)  
{  
    LinkedListNode n1 = head;  
    LinkedListNode n2 = head;  
  
    //find meeting point  
    while( n2.next != NULL )  
    {  
        n1 = n1.next;  
        n2 = n2.next.next;  
        if (n1 == n2)   
        {  
            break;  
        }  
    }  
  
    //error check--there is no meeting point  
    if (n2.next == null)   
    {  
        return null;  
    }  
  
    /*Move n1 to head, Keep n2 at meeting point*/  
  
    n1 = head;  
    while (n1 != n2)   
    {  
        n1 = n1.next;  
        n2 = n2.next;  
    }  
    //Now n2 points to the start of the loop  
    return n2;  
}</span>  

  

【摘要】有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。1、如何判断一个链表是不是这类链表?2、如果链表为存在环,如果找到环的入口点?扩展:判断两个单链表是否相交,如果相交,给出相交的第一个点。
有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。
问题:
1、如何判断一个链表是不是这类链表?
2、如果链表为存在环,如果找到环的入口点?
解答:
一、判断链表是否存在环,办法为:
设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当然,fast先行头到尾部为NULL,则为无环链表)程序如下:
bool IsExitsLoop(slist * head)
{
    slist * slow = head ,  * fast = head;

    while  ( fast  &&  fast -> next ) 
    {
        slow  =  slow -> next;
        fast  =  fast -> next -> next;
        if  ( slow  ==  fast )  break ;
    }

    return   ! (fast  ==  NULL  ||  fast -> next  ==  NULL);
}
二、找到环的入口点
当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:
2s = s + nr
s= nr
设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a。
a + x = nr
a + x = (n – 1)r +r = (n-1)r + L - a
a = (n-1)r + (L – a – x)
(L – a – x)为相遇点到环入口点的距离,由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是我们从链表头、与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。
程序描述如下:
slist *  FindLoopPort(slist * head)
{
    slist * slow  =  head,  * fast  =  head;

    while  ( fast  &&  fast -> next ) 
    {
        slow  =  slow -> next;
        fast  =  fast -> next -> next;
        if  ( slow  ==  fast )  break ;
    }

    if  (fast  ==  NULL  ||  fast -> next  ==  NULL)
        return  NULL;

    slow  =  head;
    while  (slow  !=  fast)
    {
         slow  =  slow -> next;
         fast  =  fast -> next;
    }

    return  slow;
}
 
附一种易于理解的解释:
 
一种O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然): 
关于这个解法最形象的比喻就是在操场当中跑步,速度快的会把速度慢的扣圈

可以证明,p2追赶上p1的时候,p1一定还没有走完一遍环路,p2也不会跨越p1多圈才追上

我们可以从p2和p1的位置差距来证明,p2一定会赶上p1但是不会跳过p1的

因为p2每次走2步,而p1走一步,所以他们之间的差距是一步一步的缩小,4,3,2,1,0 到0的时候就重合了

根据这个方式,可以证明,p2每次走三步以上,并不总能加快检测的速度,反而有可能判别不出有环

既然能够判断出是否是有环路,那改如何找到这个环路的入口呢? 

解法如下: 当p2按照每次2步,p1每次一步的方式走,发现p2和p1重合,确定了单向链表有环路了

接下来,让p2回到链表的头部,重新走,每次步长不是走2了,而是走1,那么当p1和p2再次相遇的时候,就是环路的入口了。

这点可以证明的:

在p2和p1第一次相遇的时候,假定p1走了n步骤,环路的入口是在p步的时候经过的,那么有

p1走的路径: p+c = n;         c为p1和p2相交点,距离环路入口的距离

p2走的路径: p+c+k*L = 2*n;   L为环路的周长,k是整数

显然,如果从p+c点开始,p1再走n步骤的话,还可以回到p+c这个点

同时p2从头开始走的话,经过n步,也会达到p+c这点

显然在这个步骤当中p1和p2只有前p步骤走的路径不同,所以当p1和p2再次重合的时候,必然是在链表的环路入口点上。
 
 
 
扩展问题:
判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。
比较好的方法有两个:
一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。
二、如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同样的结尾点,则两个链表相交。
这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。

  

常见和链表相关的算法

标签:

原文地址:http://www.cnblogs.com/x113/p/4266710.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!