单链表的常见笔试题(2)

        本文中接着列举一些常见的有关单链表的笔试题。

1. 判断单链表是否带环

        如果一个链表带环,则该链表的尾节点的next必不指向空,而是指向在他之前的某一节点,类似这样的:


        那么该如何判断呢?

方法一:

        首先将头结点的地址存放在一个顺序表中。从头开始遍历链表,比较每个节点的next域是否在顺序表中出现,如果出现了,说明该链表带环,如果没有出现,将该next域的地址再存放在顺序表中。依次往后遍历,如果某一节点的next域为空了,说明到达链表结尾了,在这之前都没在数组中找到相同的地址,说明该链表没有环。

        由于实现该算法需遍历链表和顺序表,所以时间复杂度为O(n^2),而又额外开辟了顺序表所占的内存来存放各节点的地址,所以空间复杂度为O(n)。所以,采用以下方法对其进行优化。

方法二:

        定义一个快慢指针,使快指针从头开始每次走两步,慢指针每次走一步。

        如果没环,则快指针必先走到尾节点处,因为快指针一次走两步,此时结束条件是快指针的next域或者快指针的next域的next域为空。

        如果有环,则快指针必先进入环中,慢指针后进入环中。因为快指针比慢指针多走一步,所以,每走一步,它两之间的距离就缩短一步,最终,快指针一定可以追上慢指针即相遇。此时,便可以说明该链表带环。

        当慢指针刚进入环时,快指针已经在环中。若慢指针正好位于快指针之后时。快指针要追上慢指针需要走两个环长,而慢指针需要走一个环长。所以,此时,是最坏情况下慢指针需要走的步数。所以慢指针最坏需遍历一个带环链表的长度。此时时间复杂度为O(n),因为没有开辟额外的空间,所以空间复杂度为O(1)。

代码如下:

//判断链表是否带环:1表示有环,0表示没有
//方法2:定义快慢指针,快指针走两步,满指针走一步,当快慢指针可以相遇,说明有环
int LinkListHasCircle(LinkListNode* head)
{
    if(head == NULL)
    {
        //空链表
        return 0;
    }
    LinkListNode* fast = head;//定义快指针
    LinkListNode* slow = head;//定义满指针
    while(fast->next != NULL && fast->next->next != NULL)//如果有环该条件不可能成立
    {
        fast = fast->next->next;//快指针一次走两步
        slow = slow->next;//满指针一次走一步
        if(fast == slow)//如果快慢指针相遇了,说明有环
        {
            return 1;
        }
    }
    return 0;//while循环的条件不成立,说明没环                                                                                                                          

}

思考:如果快指针一次走三步,慢指针一次走一步,可以判断链表是否带环?

        不可以,假如链表的环中有两个结点1和2,当慢指针指向1时,快指针指向2。接下来,慢指针走一步指向2,快指针走三步指向1。再接下来,慢指针指向1,快指针指向2。又回到上述的循环中,所以,快慢指针永远不会相遇。但是该链表确实有环。所以不能用该方法判断。

结论:

(1)环长为1时,快慢指针不论怎么走都会相遇。

(2)环长不为1时,当快指针的绝对速度(快指针一次走的步数减去环的节点个数)与慢指针的速度(慢指针一次走的步数)相等时,快慢指针永远不会相遇。

2. 判定是否带环,如果带环,求环长

        可以将上述代码进行改进,如果链表带环的话。快慢指针一定会相遇。则从相遇点开始遍历,绕环一周,再回到相遇点时,走过的节点数即为环长。

代码如下:

//球带环链表的环长
size_t LinkListCircleLength(LinkListNode* head)
{
    if(head == NULL)
    {    
        //链表为空
        return 0;
    }    
    LinkListNode* fast = head;
    LinkListNode* slow = head;
    while(fast->next != NULL && fast->next->next != NULL)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(slow == fast)//如果带环
        {
            LinkListNode* cur = slow->next;//从相遇点开始
            int count = 1;//设置计数器变量
            while(cur != slow)//绕环一周,再次回到相遇点,即为环长                                                                                                      
            {
                count++;
                cur = cur->next;
            }    
            return count;
        }    
    }    
    return 0;
}

3. 求带环链表的入口点

        如图所示:


        红线表示的是整个链表。快慢指针相遇点为meet_node,环的入口点是entry_node,环长为circle。

        (1)当慢指针走到相遇点时,假设快指针已经绕环一周与慢指针相遇。因为快指针是慢指针速度的二倍,所以,快慢指针走过的路程满足:

2(x+y)= x+y+circle  => x+y = circle  =>  x = circle-y

有上述表达式可知,当慢指针从头开始走到到入口点的距离与快指针从相遇点走到入口点(快指针没有绕圈)的距离L相同。

        (2)当慢指针走到相遇点时,假设快指针已经绕环n周再与慢指针相遇。同样得到下列表达式:

2(x+y) = x+y+n*circle  =>  x+y = n*circle  =>  x=n*circle-y  =>  x=(n-1)*circle + circle-y

有上述表达式可知,当慢指针从头开始走到到入口点的距离与快指针从相遇点绕了n圈后再从相遇点走到入口点(快指针有绕圈)的距离L相同。

        所以,可以根据上述表达式来编写代码。慢指针从头开始走,快指针从相遇点开始走。要使快慢指针在相同时间内分别走L后,来到相遇点。则他们的速度必须相同。所以,使快慢指针一次走一步。

代码如下:

//求环的相遇点
LinkListNode* LinkListHasCircle1(LinkListNode* head)
{
    if(head == NULL)
    {   
        //空链表
        return NULL;
    }
    LinkListNode* fast = head;
    LinkListNode* slow = head;
    while(fast->next != NULL && fast->next->next != NULL)
    {   
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {   
            return slow;
        }
    }
    return NULL;
}

//求环的入口点
LinkListNode* LinkListCircleEntry(LinkListNode* head)
{
    if(head == NULL)
    {
        //链表为空
        return NULL;
    }
    LinkListNode* meet_node = LinkListHasCircle1(head);//求快慢指针的相遇点
    if(meet_node == NULL)
    {
        return NULL;//如果相遇点为空,说明链表不带环
    }
    LinkListNode* cur1 = head;//使cur1从头开始走
    LinkListNode* cur2 = meet_node;//cur2从相遇点开始走
    while(cur1 != cur2)//当两指针未相遇时,即为走到入口点
    {
        cur1 = cur1->next;//一次一步往前走
        cur2 = cur2->next;
    }
    return cur1;//两指针相遇的地方即为相遇点                                                                                                                            
}

4. 判定两链表是否相交,若相交则求出交点(假设两链表均不带环)

        两链表均不带环,如果相交,只可能出现以下左侧的情形,而不会出现右侧的情形(因为一个节点的next域只能指向唯一一个确定的节点):


        所以,当两链表的尾节点重合时,就说明两链表相交。如下图:


        如果链表1比链表2长,假设多出的部分为x(x可由两链表的长度相减得到)。使cur1和cur2分别指向链表1和链表2的头结点处。因为链表1比链表2长,是cur1先走x,走完之后,再使cur1和cur2同时走,一次走一步。在同时走了相同距离y之后,必能相遇,相遇点即为两链表的交点。

        代码如下:

//判相交,求交点(假设两链表不带环)
LinkListNode* LinkListCrossPos(LinkListNode* head1,LinkListNode* head2)
{
    if(head1 == NULL || head2 == NULL)
    {
        //任意一个链表为空,都没有交点
        return NULL;
    }
    size_t size1 = LinkListSize(head1);//求链表1的长度
    size_t size2 = LinkListSize(head2);//求链表2的长度
    LinkListNode* cur1 = head1;//使cur1初始指向链表1的头节点
    LinkListNode* cur2 = head2;//使cur2初始指向链表2的头节点
    size_t i = 0;
    if(size1 > size2)//如果链表1比链表2长
    {
        for(;i < size1 - size2;++i)
        {
            cur1 = cur1->next;//如果链表1比链表2长,让链表1先走多余的部分
        }

    }
    else 
    {
        for(i = 0;i < size2 - size1;++i)
        {
            cur2 = cur2->next;//如果链表2比链表1长,让链表2先走多余的部分
        }
    }                                                                                                                                 
    while(cur1 != NULL && cur2 != NULL)
    {    
        if(cur1 == cur2)//相遇点即为两链表相交的点
        {
            return cur1;
 }
        cur1 = cur1->next;//两个指针同时后移
        cur2 = cur2->next;
    }
    return NULL;//两连表不相交
}

5. 判定两链表是否相交,若相交则求出交点(假设两链表可能带环)

        根据两链表的带环情况,可分为以下几种情形:

(1)两链表均不带环,此时,可使用上述4中的算法判断相交情况

(2)一个带环,一个不带环,此时,必不相交。因为如果相交的话,两链表必然都有环

(3)两链表都带环:

1)两带环链表不相交,如下图:


2)两带环链表环外相交,如下图:


3)两带环链表环内相交,如下图:


        若两带环链表环外相交,则两链表的环入口点必然相同。而两链表的交点可由上述不带环链表的交点类似可得。

        如不相同,则有可能是环内相交或不想交。如果是环内相交,则从一个链表的入口点出发,绕环一周,必然可以找到另一链表的入口点。两环的入口点即为两链表的两交点。若找不到,说明两链表不想交。

        根据以上情况,编写代码如下:

//判相交,求交点(两链表可能带环)
LinkListNode* LinkListCrossPosWithCircle(LinkListNode* head1,LinkListNode* head2)
{
    if(head1 == NULL || head2 == NULL)
    {
        return NULL;
    }
    //求两链表的环的入口点
    LinkListNode* entry1 = LinkListCircleEntry(head1);
    LinkListNode* entry2 = LinkListCircleEntry(head2);

    //如果两链表均无环,则按照上述无环的情况处理
    if(entry1 == NULL && entry2 == NULL)
    {
        return LinkListCrossPos(head1,head2);
    }
    //若一个链表有环,一个没有环,则肯定不相交
    if((entry1 == NULL && entry2 != NULL)||(entry1 != NULL && entry2 == NULL))
    {
        return NULL;
    }
    //如果两链表都带环
    if(entry1 == entry2)//如果两环的入口点相同,则环外相交
    {
        //求两环从头到环入口点的长度
        LinkListNode* cur1 = head1;
        LinkListNode* cur2 = head2;
        int length1 = 1;                                                                                                              
        int length2 = 1;
        while(cur1 != entry1)
        {
            length1++;
            cur1 = cur1->next;
        }
        while(cur2 != entry2)
        {
            ++length2;
            cur2 = cur2->next;
        }
        cur1 = head1;//两当前位置指针要从头开始
        cur2 = head2;
        //使长链表先走偏移量步
        int i =0;
        if(length1 > length2)
        {
            for(i = 0;i < length1-length2;++i)
            {
                cur1 = cur1->next;
            }
        }
        else
        {
            for(;i < length2-length1;++i)
            {
                cur2 = cur2->next;
            }
        }
        //走完后,一个指针走一步,直到走到相遇点
        while(cur1 != cur2)
        {                                                                                                                             
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        return cur1;
    }
else//如果两环的环入口点不相同
    {
        LinkListNode* cur1 = entry1->next;
        while(cur1 != entry1)//从一个链表的环入口点出发,绕环一周,若能找到另一链表的环入口点,则两入口点即两交点
        {
            if(cur1 == entry2)
            {
                return entry2;//此时,entry1和entry2均为两交点,这里,只返回一个
            }
            cur1 = cur1->next;
        }
        return NULL;//绕环一周,没找到另一链表的环入口点,则两带环链表不相交
    }
}                                                                              

6. 求两个已排序链表中相同的元素(求交集)

        该算法思想与合并两有序链表相似。定义两指针,遍历两链表,比较两当前结点的数据域,如果某个节点的数据域小,则该结点一定不是交集中的元素,所以,指针后移。继续比较。遇到相同的元素,则插入到新链表中,同时两指针均后移。直到至少遍历完一个链表。而另一链表则不再考虑。注意,如果交集中有多个相同的元素,只取一个即可。

代码如下:

//求两排序链表中相同的数据(即求交集,若有多个相同元素,只取一个)
//不破坏原链表,并且一个链表中允许有多个相同的元素
LinkListNode* LinkListUnionSetEx(LinkListNode* head1,LinkListNode* head2)
{
    //如果任一链表为空,则交集为空
    if(head1 == NULL || head2 == NULL)
    {
        return NULL;
    }
    LinkListNode* cur1 = head1;//定义cur1遍历链表1
    LinkListNode* cur2 = head2;//定义cur2遍历链表2
    LinkListNode* new_head = NULL;//定义新链表的头指针
    LinkListNode* new_tail = NULL;//定义新链表的尾指针
    while(cur1 != NULL && cur2 != NULL)//如果两链表都没遍历完
    {
        if(cur1->data < cur2->data)//如果cur1的数据域小于cur2的
        {
            cur1 = cur1->next;//则cur1后移
        }
        else if(cur1->data > cur2->data)//如果cur1的数据域大于cur2的
        {
            cur2 = cur2->next;//则cur2后移
        }
        else//如果两指针的数据域相同,将相同的数据插入到新链表中
        {
            if(new_tail == NULL)//如果链表为空
            {
                new_head = new_tail = CreateNode(cur1->data);
            }
            else//如果链表不为空
            {
                if(new_tail->data != cur1->data)//如果交集中没有相同的元素,则执行插入操作
                {
                    new_tail->next = CreateNode(cur1->data);
                    new_tail = new_tail->next;
 }
            }
            cur1 = cur1->next;//使两指针同时后移
            cur2 = cur2->next;
        }
    }
    return new_head;//当遍历完其中一个链表后,另一链表中的剩余元素不可能为交集中的元素,所以直接返回新链表
}

7. 复杂链表的复制

        首先介绍一下复杂链表:复杂链表与普通链表的不同之处在于:它的节点中不仅有data域和next域,这两个域与普通结点相同,还有一random指针,该指针可以指向任意位置,既可以指向其他节点,也可以指向自身和空指针。

复杂链表结点的结构体:

//复杂链表的结构题定义
typedef char ComplexType;
typedef struct ComplexNode
{
    ComplexType data;
    struct ComplexNode* next;
    struct ComplexNode* random;
}ComplexNode;

创建复杂链表的结点:

//创建复杂链表节点
ComplexNode* CreateComplexNode(ComplexType data)
{                                                                                                                                                                       
    ComplexNode* new_node = (ComplexNode*)malloc(sizeof(ComplexNode));
    new_node->data = data;
    new_node->next = NULL;
    new_node->random = NULL;
    return new_node;
}

        下面介绍两种方式来实现:

方法一:

        (1)先不管random指针,将各节点的data域和next域复制到一个新链表上。

        (2)然后,根据原链表的random指针得到它所指向的位置距头结点的偏移量

        (3)根据上述得到的偏移量,从新链表的头结点开始移动“偏移量”次,便是新链表结点的random域

        (4)对新旧链表中的每一个节点进行(2)(3)操作,便可以将所有结点的random域复制过来。

代码如下:

//求random域据头节点的偏移量
int Diff(ComplexNode* head,ComplexNode* random)
{
    ComplexNode* cur = head;
    int offset = 0;
    while(cur != random)
    {
        ++offset;
        cur = cur->next;
    }
    return offset;
}

//设置复制后的节点的random值
ComplexNode* Step(ComplexNode* new_head,int offset)
{
    ComplexNode* cur = new_head;
    int i = 0;
    for(;i < offset;++i)
    {
        cur = cur->next;
    }
    return cur;
}
//复杂链表的复制                                                                                                                                                        
ComplexNode* CopyComplexList(ComplexNode* head)
{
    //原链表为空,则复制后的链表也为空
    if(head == NULL)
    {
        return NULL;
    }
    ComplexNode* cur = head;
    ComplexNode* new_head = NULL;
    ComplexNode* new_tail = NULL;
    //先复制各节点的数据域及next域
    while(cur != NULL)
    {
        if(new_head == NULL)
        {
            new_head = new_tail = CreateComplexNode(cur->data);
        }
        else
        {
            new_tail->next = CreateComplexNode(cur->data);
            new_tail = new_tail->next;
        }
        cur = cur->next;
    }
 //在再使两链表从头开始遍历,求原链表各节点的random域的偏移量,再设置新链表各节点的random的值
    cur = head;
    ComplexNode* new_cur = new_head;
    while(cur != NULL)
    {
        if(cur->random == NULL)
        {
            new_cur->random = NULL;
        }
        else
        {
            int offset = Diff(head,cur->random);//求偏移量
            new_cur->random = Step(new_head,offset);//设置新链表中各节点的random域
        }
        cur = cur->next;
        new_cur = new_cur->next;
    }
    return new_head;
}

        在该方法中,因为第一个结点的random域可能指向最后一个结点,此时,计算偏移量的时间复杂度为O(n),而每个节点的random域都要进行设置,所以算法的时间复杂度为O(n^2)。

        所以,通过以下方法来进行优化。

方法二:

        (1)首先从头开始遍历原链表,遇到一个节点,在其后插入一个与之相同的新节点,遍历完后,整个链表是原链表的2倍

        (2)根据奇数位的结点的random域设置紧随其后的偶数位的结点的random域。如奇数位的结点由cur指向,则满足:

cur->next->random = cur->random->next

        (3)将偶数位的各节点拆分出来组成新链表,新链表即为复制过后的复杂链表。

代码如下:

//复杂链表的复制(简单)
ComplexNode* CopyComplexListEx(ComplexNode* head)
{
    if(head == NULL)
    {
        //原链表为空,则新链表也为空
        return NULL;
    }
    //首先,在原链表每个节点后面插入相同的节点
    ComplexNode* cur = head;
    for(;cur != NULL;cur = cur->next->next)//如果cur不为空,则一定可以在其后插入一个新元素,所以cur->next也一定不为空
    {
        ComplexNode* new_node = CreateComplexNode(cur->data);
        new_node->next = cur->next;
        cur->next = new_node;
    }
    //然后,根据原有节点的random域设置新插入的各节点的random域
    cur = head;
    for(;cur != NULL;cur = cur->next->next)
    {
        if(cur->random == NULL)
        {
            cur->next->random = NULL;
        }
        else
        {
            cur->next->random = cur->random->next;
        }
    }

    //最后,提取出插入的所有新节点
    ComplexNode* new_head = NULL;
    ComplexNode* new_tail = NULL;
    cur = head;
    while(cur != NULL)
    {
        ComplexNode* new_cur = cur->next;
        cur->next = new_cur->next;

        if(new_head == NULL)
        {
            new_head = new_tail = new_cur;
        }
        else
        {
            new_tail->next = new_cur;
            new_tail = new_tail->next;
        }
        cur = cur->next;
    }
    return new_head;
}
        此时,算法的时间复杂度为O(n).
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值