链表面试简单题

 

目录

1. 删除链表中等于给定值 val 的所有结点。 

方法一:暴力遍历取出不同元素重新编排:

方法二:双指针

 方法三:递归

2. 反转一个单链表。 

方法一:利用三个指针

方法二:递归

链表内指定区间反转_牛客题霸_牛客网

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

方法一:暴力遍历

方法二:快慢指针

4. 输入一个链表,输出该链表中倒数第k个结点。

 方法一:暴力遍历

方法二:双指针

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的

方法一:双指针

方法二:递归

6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

方法一:开辟俩空间

7. 链表的回文结构。

方法一:快慢指针 反转链表

8. 输入两个链表,找出它们的第一个公共结点。

方法一:从两个链表相隔相同的距离开始一起走,判断相同,相同即为公共结点


1. 删除链表中等于给定值 val 的所有结点。 

方法一:暴力遍历取出不同元素重新编排:


typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    //首先遍历链表找到不是要删除节点的位置,创造哨兵位将其存储下来
    ListNode* phead = head;
    ListNode* newHead = NULL,*newTail = NULL;
    if(head == NULL)
    {
        return NULL;
    }
    while(phead)
    {
        if(phead->val != val)
        {
            if(newTail == NULL)
            {
                newHead = newTail = phead;
            }
            else{
                newTail->next = phead;
                newTail = newTail->next;
            }
        }
        phead = phead->next;
    }
    if(newTail != NULL)//可能链表里全部是val的情况,那么newTail就是野指针
    {
        newTail->next = NULL;//由于可能原链表最后一个即为删除元素
        //这样我们取出的元素就有可能造成->next依然指向val的情况
    }
                                   
    return newHead;              
}

注意事项:

由于可能原链表最后一个即为删除元素这样我们取出的元素就有可能造成newTail->next依然指向val的情况

于是我们就需要手动将最后一个newTail置为空,由于可能原链表里都是需要删除的值导致newTail为空,造成野指针,所以需要让newTail不为空再归零

方法二:双指针

引用两个指针一个负责遍历链表另一个则负责删除链表里的val元素(就是当cur遍历到val时,跳过,当不是val时,令他与其相等即可)注意开始的地方不能为val

可以结合lettcode上程序员小熊大佬配图进行理解

 typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    
    if(head == NULL)
        return NULL;
    while(head != NULL && head->val == val)
    {
        head = head->next;//将前面为val的全部驱逐
    }
    ListNode* pre = head,*cur = head;
    while(cur)
    {
        //pre记录要删除的前一个节点,cur正常遍历,来找到要删除的节点
        //如果找到了,pre就指向cur下一个节点,如果没找到,则记录下来
        if(cur->val == val)
        {
            pre->next = cur->next;       
        }
        else{
            pre = cur;
        }
        cur = cur->next;
    }
    return head;
}

 方法三:递归

递归就是将函数的最容易得到结果的拿到,然后反过来逆推,这题链表的最后一个肯定是空,然后倒过来判断倒过来的head是否为val , 如果不是则留下,是则找到上一个,然后直到找到头为止

20240623_164241

struct ListNode* removeElements(struct ListNode* head, int val) {
    if(head == NULL)//运行到尾节点,然后补到尾
        return NULL;
    head->next = removeElements(head->next,val);//执行递归,将其存起来正向next是向后走
    //但是反过来调用next就是在从后向前推进
    if(head->val == val)
        return head->next;//如果head是要删除的元素的话则不管他,找他的前一个
    else
        return head;//反之则正常插入
}

203. 移除链表元素 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/remove-linked-list-elements/submissions/541172741/


2. 反转一个单链表。 

206. 反转链表 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/reverse-linked-list/description/

方法一:利用三个指针

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
        return NULL;
    ListNode* n1 = NULL,*n2 = head , *n3 = head->next;
    while(n2)
    {
        n2->next = n1;//箭头反向
        n1 = n2;
        n2 = n3;//整体向后走,然后箭头反向
        if(n3)//最后一次可能造成空指针
            n3 = n3->next;
    }
    return n1;
}

方法二:递归

此题写递归一定要在三个指针的基础上来书写,最好要完全掌握三指针再来理解,众所周知,循环的迭代可以用递归的深度来解决

struct ListNode* reverse(struct ListNode* n1,struct ListNode*n2)
{
    if(n2 == NULL)//结束条件
        return n1;
    struct ListNode* n3 = n2->next;
    n2->next = n1;
    return reverse(n2,n3);//相当于把n1 = n2 n2 = n3 放在递归里进行
}
struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
        return NULL;
    return reverse(NULL,head);    
}

进阶题:在链表指定位置翻转单链表

链表内指定区间反转_牛客题霸_牛客网

 #include <stdlib.h>
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    
    if(!head||!head->next)
        return head;
    
    if(m==n)
        return head;
    
    struct ListNode* phead=head,*p2=head,*pcur=head,*pm=head;
    
    //找到要反转的头尾两个节点
    for(int i=1;i<m;i++){
        pcur=phead;
        phead=phead->next;
        pm=phead;//不同点
    }
    for(int j=1;j<n;j++){
        p2=p2->next;
    }
    
    
    //反转,利用p1,p3,p4三个指针
    struct ListNode*p3=phead->next,*p4=phead->next->next;
    while(phead!=p2){
        p3->next=phead;
        phead=p3;
        p3=p4;
        if(p4)
        p4=p4->next;
    }
    
    //将反转后的部分链表与首尾连接
    pcur->next=phead;
    pm->next=p3;
    
    //特殊情况,直接返回p2
    if(m==1)
        return p2;
    
    return head;
}


3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

方法一:暴力遍历

算出非空链表的长度,然后只遍历一半长度即可

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* phead = head;
    int n = 0;
    while(phead)
    {
        phead = phead->next;
        n++;
    }
    int m = n / 2;
    while(m--)
    {
        head = head->next;
    }
    return head;
}

方法二:快慢指针

分别定义两个指针,一个走一步,一个走两步,当快指针走到头的时候,慢指针就为中间节点

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* fast = head, *slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}


4. 输入一个链表,输出该链表中倒数第k个结点。

 方法一:暴力遍历

找到正序的节点的位置,然后返回所在的val

int kthToLast(struct ListNode* head, int k){
    //暴力遍历
    int n = 0;
    struct ListNode* phead = head;
    while(phead)
    {
        phead = phead->next;
        n++;
    }
    int m = n - k;
    while(m)
    {
        head = head->next;
        m--;
    }
    return head->val;
}

方法二:双指针

目前已知:头节点以及倒数K节点的位置,我们可以设两个指针它们之间相隔K个单位长度,那么当后面指针指向尾时,前面的即为倒数K的位置

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


5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的

方法一:双指针

思路:分别给两个有序链表定义一个头指针,比较这两个指针的大小,将小的一方存到新开辟的全新节点

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode* l1 = list1;//定义两个有序链表的头
    struct ListNode* l2 = list2;//来遍历得到小的一方然后尾插
    //创建哨兵位
    struct ListNode* list = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* newhead = list;//保留头节点的指针
    //单独判断有一方为零的情况
    if(l1 == NULL)
        return l2;
    if(l2 == NULL)
        return l1;    
    while(l1 && l2)//有一方为空即跳出循环,小的就尾插
    {
        if(l1->val < l2->val)
        {
            list->next = l1;
            l1 = l1->next;
        }
        else
        {
            list->next = l2;
            l2 = l2->next;
        }
        list = list->next;
    }
    if(l1)//当只剩l1
        list->next = l1;
    if(l2)//当只剩l2
        list->next = l2;
    return newhead->next;
}

方法二:递归

 在这里引用lettcode上CQQ大佬的一段解释

关于return L1的个人理解: 递归的核心在于,我只关注我这一层要干什么,返回什么,至于我的下一层(规模减1),我不管,我就是甩手掌柜.

好,现在我要merge L1,L2.我要怎么做?

  • 显然,如果L1空或L2空,我直接返回L1或L2就行,这很好理解.
  • 如果L1第一个元素小于L2的? 那我得把L1的这个元素放到最前面,至于后面的那串长啥样 ,我不管. 我只要接过下级员工干完活后给我的包裹, 然后把我干的活附上去(令L1->next = 这个包裹)就行
  • 这个包裹是下级员工干的活,即merge(L1->next, L2)

我该返回啥?

  • 现在不管我的下一层干了什么,又返回了什么给我, 我只要知道,假设我的工具人们都完成了任务, 那我的任务也就完成了,可以返回最终结果了
  • 最终结果就是我一开始接手的L1头结点+下级员工给我的大包裹,要一并交上去, 这样我的boss才能根据我给它的L1头节点往下找,检查我完成的工作

那么我自己写的又错在哪?

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if (list1 == NULL)
        return list2;
    if (list2 == NULL)
        return list1;
        if(list1->val < list2->val)
            return mergeTwoLists(list1->next,list2);
            //相当于上一种方法中的list负责往下尾插填充链表
        else{
            return mergeTwoLists(list1,list2->next);
        }
}

 按照上面的解释,我的代码缺少了什么?

就相当于缺少了甩手掌柜,导致新链表的头节点未保存,只是尾插了

真正的代码应该把return的递归函数式赋予给l1->next

这种方式相当于将每次递归存下来,并且l1就是甩手掌柜,l2同理

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if (list1 == NULL)
        return list2;
    if (list2 == NULL)
        return list1;
        if(list1->val < list2->val)
            {
                list1->next = mergeTwoLists(list1->next,list2);
            //相当于上一种方法中的list负责往下尾插填充链表
                return list1;//返回甩手掌柜即可
            }
        else{
            list2->next = mergeTwoLists(list1,list2->next);
            return list2;
        }
}

6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

方法一:开辟俩空间

思路:给大的开辟一块空间来专门存大的,给小的开辟一块空间专门存小的

struct ListNode* partition(struct ListNode* head, int x){
    if(head == NULL)
    {
        return NULL;
    }
    //定义两个链表一个负责存大的一个负责存小的
    struct ListNode* bigHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* big = bigHead; 
    struct ListNode* smallHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* small = smallHead;
    while(head)
    {
        if(head->val < x)//存小的
        {
            small->next = head;
            small = small->next;
        }
        else//存大的
        {
            big->next = head;
            big = big->next;
        }
        head = head->next;
    }
    small->next = bigHead->next;//小的和大的连接
    big->next = NULL;//最后得置为空,要不然big可能指向别的随机值
    return smallHead->next;
}


7. 链表的回文结构。

面试题 02.06. 回文链表 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/palindrome-linked-list-lcci/

方法一:快慢指针 反转链表

思路:回文结构为对称的链表形式,所以我们需要四个步骤 1.找到对称点 2.将对称点前后分为两部分,3.翻转其中一个 4。将这两个进行比较 

bool isPalindrome(struct ListNode* head){
    if(head == NULL|| head->next == NULL)//防止head为空和链表里只有一个
        return true;
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    struct ListNode* mid = head;
    while(fast &&fast->next)//利用快慢指针找到中间节点
    {
        fast = fast->next->next;
        mid = slow;
        slow = slow->next;
    }
    mid->next = NULL;//将前半部分结尾赋值为零
    struct ListNode* n2 = slow, *n3 = slow->next,*n1 = NULL;
    while(n2)//将后半部分链表反转
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
            n3 = n3->next;
    }

    while(head && n1)//同时遍历两个链表,并且比较
    {
        if(head->val != n1->val)
            return false;
        head = head->next;
        n1 = n1->next;
    }
    return true;
}


8. 输入两个链表,找出它们的第一个公共结点。

方法一:从两个链表相隔相同的距离开始一起走,判断相同,相同即为公共结点

bool getInter(struct ListNode *headA, struct ListNode *headB)
{
    while(headA->next)
    {
        headA = headA->next;
    }
    while(headB->next)
    {
        headB = headB->next;
    }
    if(headA == headB)
        return true;
    return false;
}
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    int countA = 0;
    int countB = 0;
    struct ListNode* heada = headA,*headb = headB;
    if(getInter(headA,headB))
    {
        while (headA)
        {
            headA = headA->next;
            countA++;
        }
        while (headB)
        {
            headB = headB->next;
            countB++;
        }
        int num = countA - countB;
        if(countA < countB)
        {
            num = countB - countA;
            while(num--)
            {
                headb = headb->next;
            }
            while(heada != headb)
            {
                heada = heada->next;
                headb = headb->next;
            }
            return heada;
        }
        else{
            while(num--)
            {
                heada = heada->next;
            }
            while(heada != headb)
            {
                heada = heada->next;
                headb = headb->next;
            }
            return heada;
        }
    }
    else
        return NULL;
}

思路:我们首先需要判断链表是否相交,如果不相交,则不用判断,若相交,则先算出它们相隔的距离,然后调整到同一起点,同时遍历即可

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值