面试必备好题【链表02】【LeetCode】【刷题】

前言🐱

hello,好久不见,这几天一直在刷蓝桥杯的题和英雄哥的九日集训的题,没什么时间更新了。忽略想起数据结构的复习还没完成呢,那就再次开始吧!

QQ截图20220304123200

NO1.合并两个有序链表🐕

image-20220316104028868

数组+排序:

先把2个链表链接起来,开辟一个数组,把所有的val放进去,并排序,重新遍历链表,把排好序的val赋值过去

注意排序的个数

注意考虑空链表的情况

注意,数组的大小尽量比最大的节点数大一点就行。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 
    {
        //考虑空链表,list1为空,list2为空,或者都为空
        if(list1 == nullptr)
            return list2;
        if(list2 == nullptr)
            return list1;
        //直接把两个链表链接起来
        ListNode* cur1 = list1;
        while(cur1->next)
        {
            cur1 = cur1->next;
        }
        //此时cur指向的是list1最后一个节点
        cur1->next = list2;
        //把链接好的链表的val放到数组里面并排序
        //由题目知,节点数目不超过50
        int arr[110];
        int i = 0, count = 0;//count记录节点个数
        cur1 = list1;
        while(cur1)
        {
            count++;
            arr[i++] = cur1->val;
            cur1 = cur1->next;
        }
        //重置cur1
        cur1 = list1;
        //把数组按升序排序
        sort(arr,arr+count);//数组没初始化,后面是随机值,整个排序不合理,只需排序所有的节点个数
        i = 0;//重置i
        while(count--)
        {
            cur1->val = arr[i++];
            cur1 = cur1->next;
        }
        return list1;
    }
};

尾插:

取小的节点尾插到新链表—》归并思想

单链表尾插,每次都得找尾,效率太低了(N^2),我们可以加一个tail指针,来随时标记尾。
让tail动起来就行
image-20220316103151731

当有一个链表插完了,直接链接到剩下那个链表的剩下节点。

第一次尾插时需要确定好头。
注意考虑空链表或单个节点的情况。

struct ListNode *mergeTwoLists(struct ListNode *list1, struct ListNode *list2)
{
    struct ListNode *head = NULL, *tail = NULL;
    //考虑空指针
    if (list1 == NULL && list2 != NULL)
        return list2;
    if (list2 == NULL && list1 != NULL)
        return list1;
    if (list1 == NULL && list2 == NULL)
        return NULL;
    //先取个小的去做头,方便后面尾插,这种情况下 head tail为空
    if (list1->val < list2->val)
    {
        head = tail = list1;
        list1 = list1->next;
    }
    else
    {
        head = tail = list2;
        list2 = list2->next;
    }
    while (list1 && list2)
    {
        if (list1->val < list2->val)
        {
            tail->next = list1;
            list1 = list1->next;
            tail = tail->next;
        }
        else
        {
            tail->next = list2;
            list2 = list2->next;
            tail = tail->next;
        }
    }
    // l1或者l2为空就出来了
    //list1为空时链接到list
    if (list1 == NULL)
        tail->next = list2;
    else if (list2 == NULL)
        tail->next = list1;
    return head;
}
if (list1 == NULL && list2 != NULL)
    return list2;
if (list2 == NULL && list1 != NULL)
    return list1;
if (list1 == NULL && list2 == NULL)
    return NULL;
//可以写得简洁一点
if (list1 == NULL)
    return list2;
if (list2 == NULL)
    return list1;

哨兵位:

哨兵位头结点不存储有效数据的

这样就不需要一开始就判断空链表的情况了。
也不需要担心tail head为空的情况。
image-20220316104409236

注意尾插完了之后,需要return head的下一个节点

struct ListNode *mergeTwoLists(struct ListNode *list1, struct ListNode *list2)
{
    struct ListNode *head = NULL;
    struct ListNode *tail = NULL;
    //带哨兵位的写法
    head = tail = (struct ListNode *)malloc(sizeof(struct ListNode));
    tail->next = NULL;
    while (list1 && list2)
    {
        if (list1->val < list2->val)
        {
            tail->next = list1;
            list1 = list1->next;
            tail = tail->next;
        }
        else
        {
            tail->next = list2;
            list2 = list2->next;
            tail = tail->next;
        }
    }
    // l1或者l2为空就出来了
    if (list1)
        tail->next = list1;
    else if (list2)
        tail->next = list2;
    //开辟的哨兵位用完要释放,但不能直接free,直接free就找不到了,先记录一下位置再free
    struct ListNode *node = head;
    head = head->next;
    free(node);
    node = NULL;//也可以不置空,反正没人访问得到,但置空是个好习惯。
    return head;
}

C++写法

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 
    {
        ListNode *head = nullptr, *tail = nullptr;
        //带哨兵位
        head = tail = new ListNode;
        tail->next = nullptr;
        while (list1 && list2)
        {
            if (list1->val < list2->val)
            {
                tail->next = list1;
                list1 = list1->next;
                tail = tail->next;
            }
            else
            {
                tail->next = list2;
                list2 = list2->next;
                tail = tail->next;
            }
        }
        // l1或者l2为空就出来了
        if (list1)
            tail->next = list1;
        else if (list2)
            tail->next = list2;
        //开辟的哨兵位用完要释放
        ListNode *node = head;
        head = head->next;
        delete node;
        node = nullptr;
        return head;
    }
};

NO.2链表分割🐺

现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

image-20220317075512940

哨兵位:

哨兵位方便尾插
设置两个链表头,遍历原链表,一个追加小数链表,一个追加大数链表,最后将小数链表粘到大数链表前边即为结果。

Pasted image 20220319090133

class Partition
{
public:
    ListNode *partition(ListNode *pHead, int x)
    {
        //借助哨兵位
        struct ListNode *lessHead = NULL;
        struct ListNode *lessTail = NULL;
        struct ListNode *greaterHead = NULL;
        struct ListNode *greaterTail = NULL;

        //开辟哨兵位
        lessHead = lessTail = (struct ListNode *)malloc(sizeof(struct ListNode));
        greaterHead = greaterTail = (struct ListNode *)malloc(sizeof(struct ListNode));
        lessTail->next = NULL;
        greaterTail->next = NULL;
        struct ListNode *cur = pHead;
        //把小于x的插到一个链表,大于x的插到一个链表
        while (cur != NULL)
        {
            if (cur->val < x)
            {
                lessTail->next = cur;
                cur = cur->next;
                lessTail = lessTail->next;
            }
            else
            {
                greaterTail->next = cur;
                cur = cur->next;
                greaterTail = greaterTail->next;
            }
        }

        //链接2个链表,注意带了哨兵位
        lessTail->next = greaterHead->next;
        //防止成环,要让最后的那个值指向NULL
        greaterTail->next = NULL;

        list = lessHead->next;
        //释放空间
        free(lessHead);
        lessHead = NULL;
        free(greaterHead);
        greaterHead = NULL;
        return list;
    }
};

牛客的调试太难了,只能靠猜测极端情况

1.空链表过得去
2.全大于/全小于x也过得去
3.有可能成环

成环了就陷入死循环,牛客就会报内存受限,其实不是真的内存不足,而是要考虑死循环的情况

N0.3链表的回文结构👻

image-20220319132344346

开数组:

开一个int[900]的数组,用数组判断,虽然也能通过牛客的判断,但实际不符合空间复杂度O(1)的要求

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        int arr[900];
        int count = 0, i = 0;
        ListNode* cur = A;
        while(cur)
        {
            count++;
            arr[i++] = cur->val;
            cur = cur->next;
        }
        //判断数组中的数是否为回文结构
        count--;
        i = 0;
        int tmp = count;
        while(tmp--)
        {
            if(arr[i] == arr[count])
            {
                i++, count--;
            }
            else
                return false;
        }
        return true;
    }
};

逆置+快慢指针:

  1. 先找到中间节点

  2. 翻转后半部分链表

  3. 一一比较

class PalindromeList
{
public:
    //先找到中间节点
    struct ListNode *midNode(struct ListNode *head)
    {
        struct ListNode *slow = head;
        struct ListNode *fast = head;
        while (fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
    //翻转后半部分
    struct ListNode *ReverseList(struct ListNode *head)
    {
        //头插法
        struct ListNode *newHead = NULL;
        struct ListNode *cur = head;
        while (cur != NULL)
        {
            struct ListNode *next = cur->next;
            cur->next = newHead;
            newHead = cur;
            cur = next;
        }
        head = newHead;
        return head;
        //再一一比较
    }
    bool chkPalindrome(ListNode *A)
    {
        struct ListNode *mid = midNode(A);
        struct ListNode *rHead = ReverseList(mid);
        while (A && rHead)
        {
            if (A->val == rHead->val)
            {
                A = A->next;
                rHead = rHead->next;
            }
            else
                return false;
        }
        return true;
    }
};

奇数个是怎么过的呢?奇数个理论上不是应该让中间的前一个指向NULL才行吗?

其实是虽然后半部分逆置了,但2还是指向3的,3的next为NULL

只要奇数个偶数个的情况能过就行。

NO.4相交链表🐹

image-20220319134533361

依次比较

A链表每个节点依次跟B链表每个节点依次比较,如果有相等,就是相等,第一个相等的就是交点。

时间复杂度O(M*N)

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        //考虑空链表,恰好最后返回的也是nullptr
        ListNode *curA = headA, *curB = headB;
        while(curA)
        {
            ListNode *TmpCurA = curA, *TmpCurB = curB;
            //先挨个遍历B链表
            while(curB)
            {
                if(curA == curB)
                    return curA;
                curB = curB->next;
            }
            //B回到初始,A回到初始下一个
            curB = TmpCurB;
            curA = TmpCurA->next;
        }
        return nullptr;
    }
};

快慢指针走长度差:

先遍历到尾结点,判断是否相交。
如果相交,让长的链表先走差距步,再同时走找交点。

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    //考虑空链表
    if(headA == NULL || headB == NULL)
    {
        return NULL;
    }
    //先算2个链表长度
    int lenA = 0;
    int lenB = 0;
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    while(curA->next != NULL)
    {
        curA = curA->next;
        lenA++;
    }
    //各自都少计算一步,相减没影响,这样方便后面判断是否相交
    while(curB->next != NULL)
    {
        curB = curB->next;
        lenB++;
    }
    //不相交,此时curA->nxet == NULL
    if(curA != curB)
    {
        return NULL;
    }
    //注意要把curA curB的位置调整回到一开始
    curA = headA;
    curB = headB;
    //让长的链表先把多出来的长度走了,再一起走
    if(lenA > lenB)
    {
        int len = lenA - lenB;
        while(len--)
        {
            curA = curA->next;
        }
    }
    if(lenA < lenB)
    {
        int len = lenB - lenA;
        while(len--)
        {
            curB = curB->next;
        }
    }
    //一起走,看谁的指针相同
    while(curA != curB)
    {
        curA = curA->next;
        curB = curB->next;
    }
    return curA;
}

写法二:

用abs判断差距步

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    //考虑空链表
    if(headA == NULL || headB == NULL)
    {
        return NULL;
    }
    //先算2个链表长度
    int lenA = 0;
    int lenB = 0;
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    while(curA->next != NULL)
    {
        curA = curA->next;
        lenA++;
    }
    //各自都少计算一步,相减没影响,这样方便后面判断是否相交
    while(curB->next != NULL)
    {
        curB = curB->next;
        lenB++;
    }
    //不相交,此时curA->nxet == NULL
    if(curA != curB)
        return NULL;
    //另一种写法
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    if(lenB > lenA)//假设错了就及时修改
    {
        longList = headB;
        shortList = headA;
    }
    int gap = abs(lenB - lenA);
    while(gap--)
    {
        longList = longList->next;
    }
    while(longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return longList;
}

不先判断是否相交也行,如果不相交最后返回的也是NULL

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    //考虑空链表
    if(headA == NULL || headB == NULL)
        return NULL;
    //先算2个链表长度
    int lenA = 0;
    int lenB = 0;
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    while(curA != NULL)
    {
        curA = curA->next;
        lenA++;
    }
    //各自都少计算一步,相减没影响,这样方便后面判断是否相交
    while(curB != NULL)
    {
        curB = curB->next;
        lenB++;
    }
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    if(lenB > lenA)
    {
        longList = headB;
        shortList = headA;
    }
    int gap = abs(lenB - lenA);
    while(gap--)
    {
        longList = longList->next;
    }
    while(longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return longList;
}

NO5. 环形链表🐜

image-20220319134457032

快慢指针

判断是否带环很简单,利用快慢指针即可。如果带环,快指针必然会追上慢指针。

image-20220319134055347

//快慢指针
bool hasCycle(struct ListNode *head) {
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return true;
        }
    }
    return false;
}

仅仅是这样就够了吗?

如果让你求环形入口点该怎么办呢?

NO6.环形链表 II🐎

image-20220319134832482

NO.7环形链表衍生问题🦏

1.

为什么slow走一步,fast走两步,他们一定会在环内相遇呢,会不会永远追不上?请证明:

不会。假设slow进环的时候,fast跟slow的距离是N,紧接着追击的过程中,slow每走一步,fast走2步,也就是他们自己距离缩减1,迟早会追上。

2.

slow走一步,fast走3步,4步,x步呢?一定会相遇吗?

假设slow进环的时候,fast和slow距离为N,环的长度为C。

当fast走3步时,他们每走一次,距离缩小2步,如果N是偶数就会相遇,奇数就不会相遇,变成-1时,实际相差C-1,如果C-1也是奇数,那么永远追不上。

image-20220319135128267

N为奇数时,假设环的长度为C,当他们差距为-1时,其实也就是差距为C-1

1、C-1为偶数时,那么他们最终就能相遇。
2、C-1为奇数时,那么就永远也追不上了。

当fast走4步时,slow走1步,每走一次,距离缩小3。
image-20220319135227808

如果C-1 是3的倍数,那么就能追上。如果不是3的倍数,那么最终就始终在C-1或C-2循环,追不上。

C-2同理。

3.

求环的入口点

image-20220319135321883

由图推出公式 :2*(L+X) = L+N * C+X

L是起点到入口点的距离,X是相遇点到入口点的距离,环的长度是C
注意如果环比较小,快指针可能会走了好多圈他们才相遇,因此是N*C
也就是一个指针从head走,一个从meet走,他们会在入口相遇

上面3个问题都弄清楚了,那么代码不就也手到擒来吗。

快慢指针:

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            //相遇
            //通过证明,一个指针从head走
            //一个从meet走,他们会在入口相遇
            struct ListNode* meet = fast;
            while(meet != head)
            {
                meet = meet->next;
                head = head->next;
            }
            return head;
        }
    }
    return NULL;
}

当然,快慢指针代码看似简单,实际原来却很复杂,还有没有其他思路呢?

当然有啦,结合前面的题,有没有什么想法呢?链表相交问题。

转换成相交问题:

找到相遇点后,让fast的下一个节点作为B的头,并把fast的next置空,然后再去找相交点即可:
无需翻转链表找到入口后再翻转回来。
Pasted image 20220319122803

class Solution {
public:
    ListNode *getIntersectionNode(ListNode* headA, ListNode* headB)
    {
        if(headA == nullptr || headB == nullptr)
            return nullptr;
        //先算两个链表长度
        int lenA = 0, lenB = 0;
        ListNode *curA = headA, *curB = headB;
        //2个长度都少算1不影响,方便后面算相交
        while(curA->next)
        {
            ++lenA;
            curA = curA->next;
        }
        while(curB->next)
        {
            ++lenB;
            curB = curB->next;
        }
        //算完长度后,恰好指向的是各自最后一个节点,进而判断是否相交
        if(curA != curB)
            return nullptr;
        //假设A是长的,如果不是就反过来
        ListNode *longList = headA, *shortList = headB;
        if(lenB > lenA)
        {
            longList = headB;
            shortList = headA;
        }
        int gap = abs(lenA - lenB);
        //让长的先走差距步
        while(gap--)
            longList = longList->next;
        while(longList != shortList)
        {
            longList = longList->next;
            shortList = shortList->next;
        }
        return longList;
    }
    ListNode *detectCycle(ListNode *head) {
        ListNode *slow = head, *fast = head;
        while(fast && fast->next)
        {
            //slow走一步,fast走2步,如果有环必会相遇
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
            {
                //相遇了把环断开,转换为判断链表是否相交问题
                ListNode* headB = fast->next;
                fast->next = nullptr;
                return getIntersectionNode(head, headB);
            }
        }
        //出循环说明不成环
        return nullptr;
    }
};

尾声🦊

🌹🌹🌹

写文不易,如果有帮助烦请点个赞~ 👍👍👍

Thanks♪(・ω・)ノ🌹🌹🌹

😘😘😘

👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接

0

  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值