数据结构--链表进阶面试题

        在链表题目开始之前我们来复习一道数组元素的逆序问题:

        给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

提示:

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 0 <= k <= 10^5

 思路1:旋转k次,每一次项右轮转一个元素。时间复杂度O(N^2)。空间复杂度O(1)。

 思路2:创建一个新的数组,现将原数组的后k个元素放置到新数组中,然后再把剩余元素依次放入新数组中,最后把新数组的元素按顺序放回原数组。时间复杂度O(N)。空间复杂度O(N)。

上面两种思路各有优势,也有缺点。我们再来看看思路三。

思路3:先逆序前n-k个元素,再逆序后k个元素,最后整体逆序。这种方法在时间、空间复杂度上都是最优的,但是思路不好想到,这就要大家多多积累,我们来看看代码:

void revese(int* nums,int left ,int right)
{
    while(left <= right)
    {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }

}

void rotate(int* nums, int numsSize, int k) 
{
    k %= numsSize;
    revese(nums,0,numsSize-k-1);
    revese(nums,numsSize-k,numsSize-1);
    revese(nums,0,numsSize-1);
}

例1:返回链表的倒数第k个节点

        实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。给定的 k 保证是有效的。

解:

        这到题的思路和之前的快慢指针相似,我称之为先后指针,我们先创建两个指针(fast、slow),既然是找倒数第k个节点,那我们就先让fast走k个节点,然后让两个指针同时向后走,当fast指针走到尾节点时,slow节点就是倒数第k个节点。代码如下:

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k)
{
    ListNode* fast = head;
    ListNode* slow = head;
    while(k--)
    {
        fast = fast->next;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow->val;
}

 例2:链表的回文结构

        对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

解:

        这道题有些难度,不过我们使用之前学的解法思路可以很轻松的过这道题。既然是回文结构,那我们就先找到他的中间节点,然后我们在逆序后半段链表,然后再比较原头结点和逆序后的头结点的值,如果不相等就返回false,反之继续向后遍历,直到其中有一个指针指向为空,返回true。

 

bool chkPalindrome(ListNode* A) 
    {
        //寻找中间节点
        struct ListNode * fast = A;
        struct ListNode * slow = A;
        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        //翻转后半链表:头插法
        struct ListNode* pcur = slow;
        struct ListNode* newhead = NULL;
        while(pcur)
        {
            struct ListNode* next = pcur->next;
            pcur->next = newhead;
            newhead = pcur;
            pcur = next;
        }
        //向后遍历
        while(A && newhead)
        {
            if(A->val != newhead->val)
            {
                return false;
            }
             A = A->next;
             newhead = newhead->next;
        }
        return true;
    }

例3:相交链表

        给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 10^4
  • 1 <= Node.val <= 10^5
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listA 和 listB 没有交点,intersectVal 为 0
  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]

图示两个链表在节点 c1 开始相交

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。

 解:

        本道题在初见可能会没有思路,因为我们没有办法确定他们是否相交了或者走到哪个节点了。但是仔细思考,我们还是有办法去处理这些情况的。

        首先,我们要确定两个链表是否真的相交了,那我们可以先遍历两个数组,如果它们的尾节点为同一个,说明它们是相交的,反之即为不想交,返回空指针。

        如果是相交的话,我们就要重新去寻找它们的第一个相交节点,我们可以在第一次遍历判断他们是否相交时,顺便计算一下两个链表的节点个数,这样不需要重新再去计算,降低了时间复杂度。如果A链表长,那就让A先走len(A) - len(B)和节点,然后让两个节点从所在位置继续向后遍历,当两个节点相等时,就找到了第一个相交节点,返回该节点即可。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
        ListNode* cur1 = headA;
        ListNode* cur2 = headB;
        int len1 = 1;
        int len2 = 1;
        while(cur1->next)
        {
            cur1 = cur1->next;
            len1++;
        }

        while(cur2->next)
        {
            cur2 = cur2->next;
            len2++;
        }
        
        if(cur1 != cur2)
        {
            return NULL;
        }
        else
        {
            ListNode* pcur1 = headA;
            ListNode* pcur2 = headB;
            if(len1 >= len2)
            {
                int num = len1 - len2;
                while(num--)
                {
                    pcur1 = pcur1->next;
                }
            }
            else
            {
                int num = len2 - len1;
                while(num--)
                {
                    pcur2 = pcur2->next;
                }
            }

            while(pcur1 != pcur2)
            {
                pcur1 = pcur1->next;
                pcur2 = pcur2->next;
            }
            return pcur1;
        }
}

 例4:随机链表的复制

        给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

        构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

        例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

        返回复制链表的头节点。用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val,random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

 解:

        本题的难点在于节点的随机指针random难以确定指向的方位。所以需要我们另辟蹊径,我们可以直接在原链表的每个节点之后复制一个相同的节点,这样我们就可以直到该新节点的随机节点的位置在原节点的随机节点的下一个位置。怎么样,是不是很巧妙。

        首先我们还是先判断原链表是否为空,为空就返回NULL。反之,我们就要开始插入复制节点了,各位要注意,新复制的原点的随机节点在第一遍遍历的时候都赋值为NULL,因为,第一遍还没有创建好所有的节点,你可能找不到相应的随机节点。第二遍遍历就是单纯把所有的random都指向他们相应的节点。第三遍遍历为的是还原原链表并分离新链表。代码如下:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
Node* copyRandomList(Node* head) 
{
     if (head == NULL) 
            return NULL;

    // 第一遍遍历:在每个原节点后面插入复制节点
    struct Node *cur = head;
    while (cur != NULL) 
    {
        struct Node *copyNode = (struct Node *)malloc(sizeof(struct Node));
        //固定位置插入
        copyNode->val = cur->val;
        copyNode->next = cur->next;
        //随机指针先置为空,后续节点完全创建好之后再进行复制
        copyNode->random = NULL;
        cur->next = copyNode;
        cur = copyNode->next;
    }

    // 第二遍遍历:设置复制节点的 random 指针
    cur = head;
    while (cur != NULL) 
    {
        //NULL已经赋值过了,所以无需再次复制
        if (cur->random != NULL) 
        {
            cur->next->random = cur->random->next;
        }
        cur = cur->next->next;
    }

    // 第三遍遍历:将复制节点从原链表中分离出来
    //分离新链表的同时,还原原链表
    cur = head;
    struct Node *newHead = head->next;
    struct Node *newCur = newHead;
    while (cur != NULL)     
    {
        cur->next = cur->next->next;
        if (newCur->next != NULL) 
        {
            newCur->next = newCur->next->next;
        }
        cur = cur->next;
        newCur = newCur->next;
    }

    return newHead;
}

例5:环形链表

        给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

提示:

  • 链表中节点的数目范围是 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos 为 -1 或者链表中的一个 有效索引 。

解:

         这道题不是很难,但是用到了很多数理逻辑思维。首先我们要确定链表中是否存在环,我们先假设有环,那如何确定环的存在也是一个问题。我们来看一张图:

        我们依旧使用快慢指针来解题,不了解的可以看前两篇文章。slow走一步,fast走两步,直到slow进入环中,fast开始追击slow,因为fast每次都比slow多走一步,那是不是说明fast早晚都可以追上slow呀。既然能追上,是不是就说明有环存在,这道题就解了。如果没有环,两个指针就不可能相遇,我们在里面判断一下就可以了。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) 
{
    //判断是否为空链表
    if(head == NULL)
    {
        return false;
    }
    ListNode* fast = head;
    ListNode* slow = head;
    //循环中如果fast和slow相等说明成环,不成环在循环结束后会返回fasle
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return false;
}

例5拓展:

        我们要讨论的远远不止这些,如果slow固定为走一步,但是fast不是走两步,而是走三步、四步、五步……呢?下面我们来一个一个讨论:

        我们假设进环前的节点个数为L,环的节点个数为C,当slow进环时,fast与slow相距N。

        我们先来看fast走三步的情况, 每移动一次指针,fast和slow的距离就会减少2,如果N为偶数,过程为N、N-2、N-4、……、2、0,最后两个指针会相遇;如果N为奇数,过程为N、N-2、N-4、……、3、1、-1,我们会发现两个指针错过了,并且越来越远,我们就需要重新计算它们的距离。错过之后它们的距离变成了C-1,当C-1为偶数时,它们最终会相遇对吧;但是当C-1为奇数时,它们就永远不会相遇了。那这里是不是就无解了呢,我们继续往下去探讨:

        我们假设slow走的路程是L,那么fast走的就是L + x*C + C-N(x为正整数)。我们还知道fast走的路程是slow的三倍,那么我们就可以写出关系式:3L = L + x*C + C-N。化简之后我们可以得到2L = (x+1)*C -N。由于C-1是奇数,仔细看你会发现:偶数 = 任意整数 * 偶数 - 奇数。这是一个恒不成立的等式,也就是说这种情况不存在。那我们是不是就可以证明,fast必定可以追上slow。

        看完三步后我们再来看看四步的,每移动一次指针,fast和slow的距离就会减少3,这时我们要分三种情况去看:N%3==0时,过程为N、N-3、N-6、……、3、0,可以相遇;N%3==1时,过程N、N-3、N-6、……、4、1、C-2;当N%3==2时,过程为N、N-3、N-6、……、5、2、C-1。后面的过程和三步的相似,各位感兴趣可以自己去算一下,这里不过多着笔。

        从这两种情况中我们要学的是数理逻辑思维,在正式工作时很少用到,但是在面试时可能会被问,大家注意一下就可以了。

例6:环形链表二

        给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

        如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

提示:

  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • -10^5 <= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

解:

        这道题用上面的思路可以很快就解决, 我们使用两步fast的快慢指针来解决。

 

        首先,我们让fast和slow走到相遇节点,记为meet。我们来算一下,假设slow走了L+N ,fast走了L + x*C + N。由关系可得,2(L+N) = L + x*C + N。化简得L = (x-1)*C + C-N。其中C是环的节点数,C-N就是meet到入环节点的距离,因为(x-1)是可以等于0的,我们惊喜地发现,L = C-N。也就是说在头指针和meet一起向后走,会同时到达我们要的节点。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
ListNode *detectCycle(struct ListNode *head) 
{
        ListNode* fast = head;
        ListNode* slow = head;
        ListNode* meet = NULL;
        //相遇
        while(fast && fast->next)
        {
            slow =  slow->next;
            fast = fast->next->next;
            if(fast == slow)
            {
                meet = fast;
                break;
            }
        }
        //判断链表是否无环
        if(fast == NULL || fast->next == NULL)
        {
            return NULL;
        }
        //返回入环的第一个节点
        ListNode* pcur = head;
        while(pcur != meet)
        {
            pcur = pcur->next;
            meet = meet->next;
        }
        return meet;
    
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值