通过这6道初级链表题检验你的链表知识是否合格

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过这6道初级链表题检验你的链表知识是否合格相关的知识,希望对你有一定的参考价值。

前言


学习完毕链表(单双链表)后,博主更新了6道比较有意思的链表题,并且全部配以图解文字说明,对于大家有一定的帮助,谢谢支持哦~


01-删除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例:1

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例:2

输入:head = [], val = 1
输出:[]

示例:3

输入:head = [7,7,7,7], val = 7
输出:[]

迭代法(这种操作和我们学习链表增删查改时候一样):


比较明显,这是一个链表的增删改查里面的删除操作.大家还记得在单链表里面我们要删除一个结点的步骤是什么吗? 没错:

  • 第一步: 找到需要删除的结点前一个结点,即如果cur->next->val等于val,说明cur就是需要删除结点的前一个结点
  • 第二步: 让需要删除结点的前一个结点 与 需要删除结点的后一个结点 连接,即cur->next = cur->next->next
  • 第三步: 释放需要删除的结点.(博主知道不释放内存是一个很糟糕的习惯,但是这里只是在讲解算法题,所以博主就偷懒了)

而大致的迭达方法如下:


如果 cur的下一个节点的节点值不等于给定的 val,则保留下一个节点,将 val移动到下一个节点即可。

cur的下一个节点为空时,链表遍历结束,此时所有节点值等于val 的节点都被删除

但是我们还记得吗?在单链表的头删操作中,我们是分了情况进行讨论的,即只有一个结点和多个结点时候,这就会比较麻烦,有没有什么好的操作省去它呢? 有,这个操作就是给原来链表增加一个哑结点


示意图(假设有下面一个链表,需要删除的val是6,绿色移动点代表在判断是否等于val,橙色代表val结点后面一个结点):

题解:

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* dummynode = (struct ListNode*)malloc(sizeof(struct ListNode));  //创建哑结点
    dummynode->next = head; //连接链表
    struct ListNode* cur = dummynode; //迭代结点
    
    while(cur->next) //当下一个为空指针就结束
    {
        if(cur->next->val == val) //如果下一个结点的值等于val,就连接橙色结点
        {
            cur->next = cur->next->next;
        }
        else
        {
            cur = cur->next;
        }
    }
    return dummynode->next;//返回修改后的结点(不要哑结点哦~~)
}

02-反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表

示例1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例2:

输入:head = [1,2]
输出:[2,1]

示例3:

输入:head = []
输出:[]

我们的正常想法是,先给一个prev指针置为NULL,再给个cur指针赋值head,然后cur指向prev,最后prev和cur依次像下图这样迭代:

有问题吗?问题很大,问题出在哪里了?那就是当cur指向prev后,等下一次迭代时,cur就找不到他原来后面的结点,所以说,我们还差一个指针用来保存下一个结点,上代码:

struct ListNode* reverseList(struct ListNode* head) 
{
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;
    struct ListNode* next= NULL;
    while (cur) 
    {
        next = cur->next;
        cur->next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

03-查找链表中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])

示例2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])

博主这里介绍的是快慢指针法则,即快指针比慢指针多走一步,这样当快指针走完整个链表时候,慢指针就刚好是中间结点.

但有个情况是快指针的结束条件受到结点的奇偶影响.

下面我们分开进行演示:

  • 当结点数是偶数时候,发现fast为空:
  • 当结点数是奇数时候,发现fast为尾结点,即fast->next等于NULL:

代码:

struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast,*slow; //定义快慢指针
    fast = slow = head;
    while(fast && (fast->next))  // 当fast为空,或者fast->next为空,就结束循环,说明此时slow已经走到了中间结点
    {
        fast = fast->next->next;//fast走两步
        slow = slow->next; //slow走一步
    }
    return slow;
}

04-链表倒数第K个结点

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

示例1:

输入: 1->2->3->4->5 和 k = 2
输出: 4

博主这里介绍的还是快慢指针法,但是这里就需要一个变化了~~~,什么变化呢?大家想想上一个题目的快慢指针的差异是什么?

没错,上个题的差异是fast的速度是slow的两倍,而我们这个题目怎么弄呢?没错,可以先让fast走K步,然后slow与fast速度一样

fast走到末尾时候结束(fast->next等于NULL)

如图:

代码:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) 
{
    struct ListNode* slow,*fast;
    slow = fast = pListHead;   //创建快慢指针
    
    while(k--)   //fast先走K步
    {
        if(fast)        //注意!!!!这里必须要判断,为什么呢?因为题目并没有规定k是小于等于链表长度的哦
            fast = fast->next;
        else
            return NULL;
    }
    while(fast)  //同步
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

05-合并两个有序链表

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

示例1

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例2

输入:l1 = [], l2 = []
输出:[]

示例3

输入:l1 = [], l2 = [0]
输出:[0]

如果这个题是两个数组,大家有没有啥想法? 对,估计大部分的想法都是 新建立一个数组,然后数组上下两两比较,放进新数组.

而我们链表其实也可以这样,我们可以建立一个哑结点,然后让链表1和链表2每个结点进行比较,小的就依次放在哑结点后,如图:

代码:

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){

    struct ListNode* p1 = l1,*p2 = l2;
    struct ListNode* newhead = NULL,*tail = NULL;

    p1 == NULL ? (return p2):(;); //后面括号是空语句
    p2 == NULL ? (return p1):(;);
    
    newhead = tail = (struct ListNode* )malloc(sizeof(struct ListNode)); //创建新结点

    while(p1 && p2)  //开始一一比较并进行连接
    {
        (p1-> val) <= (p2->val) ?  //比较谁小
        (tail->next = p1,p1 = p1->next,tail = tail->next) :  //p1的值更小,则连接p1,然后tail和p1迭代
        (tail->next = p2,p2 = p2->next,tail = tail->next);   //p2的值更小,则连接p2,然后tail和p2迭代
    }

    p1 ? (tail->next = p1):(p1);  //如果p1还有值,则把剩下的p1连接
    p2 ? (tail->next = p2):(p2);  //如果p2还有值,则把剩下的p2连接

    struct ListNode* pp = newhead->next;  //保存哑结点后面的结点
    free(newhead); //释放哑结点
    newhead = NULL;
    return pp;
}

06-链表分割

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

思路:

同时创建两个哑结点,值小于x的结点放在第一个结点后,值大于等于x的结点放在第二个结点后,等原链表放完后,再把第二个哑结点链表后面的数据连接在第一个哑结点链表上,具体过程如图:

代码:

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) 
    {
        if(pHead == NULL) return NULL;  //如果原链表是空,就返回空
        
        ListNode* cur = pHead,*dummyone,*dummytwo,*tailone,*tailtwo,;
        tailone = dummyone = (ListNode*)malloc(sizeof(ListNode));//创建哑结点
        tailtwo = dummytwo = (ListNode*)malloc(sizeof(ListNode));
        
        while(cur) //进行迭代
        {
            cur->val < x ?
            (tailone->next = cur,tailone = tailone->next,cur = cur->next) : //小值放在dummyone链表
            (tailtwo->next = cur,tailtwo = tailtwo->next,cur = cur->next) ; //大值放在dummytwo链表
        }
        
        tailone->next = dummytwo->next;//连接
        tailtwo->next = NULL;
        
        return dummyone->next;
    }
};

以上是关于通过这6道初级链表题检验你的链表知识是否合格的主要内容,如果未能解决你的问题,请参考以下文章

几道简单的链表题

剑指offer链表题的双指针法总结

判断链表的回文结构

203. 移除链表元素

六种方法判断链表是否为回文序列

剑指offer 面试25题