链表相关题解

以下题目皆来自力扣和牛客,可以通过搜索题目名找到对应题目
前期最好配合画图进行解题,要进行特殊情况的考虑。
在进行链表题训练时,最好自己先画图,然后再进行代码的书写,卡壳了或者完全没有思路再看解析。只看完本篇文章不代表你能从容解决链表题。
1. 删除链表中等于给定值 val 的所有节点。 //这里的头结点是链表实现的一种 方式,后面会介绍到。
画图分析,因为返回头结点,所以可以不用考虑头指针的因素。在实现单链表时也可以这样创建节点,但是要额外创建变量接受指针(返回值),每次创建节点都要创建指针接收,没有直接传二级指针方便。
基本思路和之前单链表的任意插差不多,都是防止信息丢失创建两个节点指针,当con位置的值不为val时,就跳过,prev指向con,con指向下一个位置。当con位置值为val时,要进行free操作,free后free指针的值会发生改变,所以不能先free,再找下一位。先记录下一个节点的位置,可以通过创建临时变量来保存下一位的地址,这种方式更稳妥:struct  ListNode* next = con->next;也可以通过prev的next成员变量保存下一个节点的位置:prev->next = con->next;然后free(con);再将con指向下一个节点:con = prev->next;
考虑特殊情况,就是开头为要删除的值和连续出现要删除的值。
代码:
struct ListNode* removeElements(struct ListNode* head, int val)
{
    if(head == NULL)
    {
        return NULL;
    }
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
    while(cur)
    {
        if(cur->val == val)//删除的情况
        {
            struct ListNode* next = cur->next;
            if(prev == NULL)//头结点要删除的情况
            {
                free(cur);
                cur = next;
                head = next;
            }
            else
            {
                prev->next = next;
                free(cur);
                cur = next;
            }
        }
        else//不删除的情况
        {
            prev = cur;
            cur = cur->next;
        }
    }
    return head;
}
2.反转一个单链表(相当经典的题)
简单思路:将指针指向的方向互换。
链表的本质就是节点中的next储存下一个节点的地址,只要将next的值进行修改就能完成链表的方向的转换。创建指针来完成对节点方向的修改。
就像这样:
通过这幅图也发现一个问题,1节点的next被修改了,2节点找不到了,所以要创建三个指针,prev,cur,next分别保存前一个节点,当前节点,下一个节点。
代码如下:
struct ListNode* reverseList(struct ListNode* head){
    if(head == NULL)
        return NULL;
    struct ListNode*  prev = NULL;
    struct ListNode*  cur = head;
    struct ListNode*  next = head->next;
    while(cur)
    {
        cur->next = prev;
        prev = cur;
        cur = next;
        if(next != NULL)
            next = next->next;
    }
    return prev;
}
还有一个思路:创建一个新链表,将原链表的值赋给新链表,就相当于对一个空链表不断进行头插,如图:
代码如下:
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是指向NULL的
        newhead = cur;//newhead指向新的头节点
        cur = next;
    }
    return newhead;
}
3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点
这题看起来很简单,遍历一遍,计算出节点个数再除2就可以得到中间节点的下标了,再根据下标遍历出中间节点。但是如果加个附加要求:只允许遍历一遍。应该如何去做呢?
这里我们介绍一个新的方法:快慢指针,快慢指针开始都指向第一个节点,快指针走2步,慢指针走1步,在快指针走到头时,慢指针刚好走一半。再根据奇数偶的情况写出条件判断就可以了。
最后附上代码:
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast != NULL && fast->next != NULL)
//这里fast的判断顺序不能改变,如果fast为偶数,最后一次fast将在next两次后指向NULL这时先判断fast->next != NULL就不合适了
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}
4.输入一个链表,输出该链表倒数第k节点
这题是牛客网上的(虽然LeetCode上也有),目前牛客在公司面试的比例也很高,所以在牛客上的练习也是很有必要的。
同样,这题也可以通过遍历两边做,同样有没有只遍历一遍的方法呢?同样,还是快慢指针。
这就涉及了快慢指针的另一个用法了,快指针先走k步,之后一起遍历,快指针结束时,慢指针就刚好在倒数第k个节点。
下面是代码:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
        struct ListNode* slow = pListHead;
        struct ListNode* fast = pListHead;
        while(k--)
        {
            if(fast == NULL) //这里是一个坑,如果k的值比链表节点个数大,如果不加if判断就会发生越界访问。
                return NULL;
            fast = fast->next;
        }
        while(fast != NULL)
        {
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
}
5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
依据前面的思路,可以将两个链表的节点进行比较,创建一个新的链表,每次用比较后节点进行尾插,但这样每次找尾,执行次数分别为1,2,3,4……n,累加起来时间复杂度为O(n^2)。有没有办法用O(n)的时间复杂度。
也很简单,定义一个尾指针就可以,每次尾插就放到尾指针后面,但为什么不在但链表中定义尾指针呢?因为单链表中不止有尾插,定义位置没有太大帮助,本题只需要进行尾插就可以了,所以定义一个尾指针可以更高效解决问题。
下面是代码: //逻辑简单,实现的结构比较复杂,具体过程就不阐述了
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* head = NULL;
    struct ListNode* tail = NULL;
    while(list1 && list2)
    {
        if(list1->val > list2->val)
        {
            if(head == NULL)
            {
                head = list2;
                tail = list2;
            }
            else
            {
                tail->next = list2;
                tail = tail->next;
            }
            list2 = list2->next;
        }
        else
        {
            if(head == NULL)
            {
                head = list1;
                tail = list1;
            }
            else
            {
                tail->next = list1;
                tail = tail->next;
            }
            list1 = list1->next;
        }
    }
//下面部分的代码是为了解决两个链表中有空链表的存在,有些冗余。
//可以在开头判断有没有空链表,如果有直接返回另一个链表
    if(list1 || list2)
    {
        if(list1 == NULL)
        {
            if(tail != NULL)
            {
                tail->next = list2;
            }
            else
            {
                tail = list2;
                head = list2;
            }
        }
        else
        {
            if(tail != NULL)
            {
                tail->next = list1;
            }
            else
            {
                tail = list1;
                head = list1;
            }
        }
    }
    return head;
}
下面讲一下链表带头结点和不带头结点的区别
比如同样存3个值的链表,不带头结点的由指针指向第1个节点,3个值3个节点;带头结点的由节点指向第1个节点,3个值4个节点,这样的头结点称为哨兵位的头节点,这个节点不存储有效值。哨兵位的节点中的val也有值,但是是随机值,部分参考书会将头结点中的值设为有效节点数,但这样是不合适的,因为val中存储的值不不止有整型。
无头单链表是从head往后遍历,带头结点单链表是从head->next往后遍历。
在oj题中除非特别说明,否则都是不带头节点的链表。
带头结点和不带头结点的区别在上一题中的体现为:尾插
有图中可以看出,不带头结点要尾插的话要先判断是否为空,为空:tail = list1,不为空:tail->next = list1。而有哨兵位的链表则不管是否为空,都是直接tail->next = list1。在程序中不带头结点意味着要多个判断条件。
下面是上一题使用头结点的代码:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* head = NULL, *tail = NULL;
    head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    head->next = NULL;
    while(list1 && list2)
    {
        if(list1->val > list2->val)
        {
            tail->next = list2;
            tail = tail->next;
            list2 = list2->next;
        }
        else
        {
            tail->next = list1;
            tail = tail->next;
            list1 = list1->next;
        }
    }
    if(list1 || list2)
    {
        if(list2)
        {
            tail->next = list2;
        }
        else
        {
            tail->next = list1;
        }
    }
    return head->next;//返回值要稍微注意一下,原来不是哨兵位单链表,返回时也不要是哨兵位单链表
//其实还有一个问题,没有释放malloc开辟的内存,创建一个临时变量记录head->next的值后再释放head就可以了。
//ps:内存泄漏(没释放malloc开辟的内存)是C/C++中很大的一个问题。
}
6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
这题在牛客上是没有C语言编译的版本的,用c++正常编写即可,c++兼容c。
这题没有给具体的示例,具体意思是,一个链表为3-5-1-4-6-7,给定的x是4时,链表变为3-1-5-4-6-7。
基本思路也很简单:创建两个链表,遍历原链表,if判断大于等于x就放入链表1,小于就放入链表2,遍历结束后将链表2的尾节点和链表1的头节点连接即可。这题就很适合使用哨兵位的链表,不使用哨兵位会比较麻烦,下面给有哨兵位的和没有哨兵位的两组代码,试着比较分析一下。
不带哨兵位的代码:
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) 
{
    //c++在这下面直接写就行了
    //变量名一开始最好取好,我最开始是用n1,n2,list1,list2来做变量名的,后来稀里糊涂的不知道哪跟哪,图也不好看,除非重画,很搞心态。
    struct ListNode* lesstail = NULL;
    struct ListNode* greatertail = NULL;
    struct ListNode* lesshead = NULL;
    struct ListNode* greaterhead = NULL;
    while (pHead)
    {
        if (pHead->val >= x)
        {
            if (greaterhead == NULL) //这里和下面的if判断就是和哨兵位链表区别,要多考虑一个可能性。
            {
                greatertail = pHead;
                greaterhead = greatertail;
            }
            else
            {
                greatertail->next = pHead;
                greatertail = greatertail->next;
            }
        }
        else
        {
            if (lesshead == NULL)
            {
                lesstail = pHead;
                lesshead = lesstail;
            }
            else
            {
                lesstail->next = pHead;
                lesstail = lesstail->next;
            }
        }
        pHead = pHead->next;
    }
     //下面两个链表尾置空的操作是为了防止形成带环链表,比如lesstail指向greaterhead,greatertail又指向lesstail,这就形成了一个死循环。
    if(greaterhead != NULL)
        greatertail->next = NULL;
    if(lesshead != NULL)
        lesstail->next = NULL;
    //考虑极端情况,链表数据都大于x
    if(lesshead != NULL)
    {
        lesstail->next = greaterhead;
    }
    else
    {
        lesshead = greaterhead;
    }
    return lesshead;
    }
};
带哨兵位的代码:
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
    struct ListNode* lesstail = NULL;
    struct ListNode* greatertail = NULL;
    struct ListNode* lesshead = NULL;
    struct ListNode* greaterhead = NULL;
    //下面两行头和尾要指向同一块空间,如果先开辟出的空间给了头,然后赋值给尾,那么在下面的步骤中,pHead给tail的操作不会连接链表,因为这时的尾是临时变量,修改的不是链表空间
    greaterhead = greatertail = (struct ListNode*)malloc(sizeof(struct ListNode));
    lesshead = lesstail = (struct ListNode*)malloc(sizeof(struct ListNode));
    while (pHead)
    {
        if (pHead->val >= x)
        {
            greatertail->next = pHead;
            greatertail = greatertail->next;
        }
        else
        {
            lesstail->next = pHead;
            lesstail = lesstail->next;
        }
        pHead = pHead->next;
    }
    if (greaterhead != NULL)
        greatertail->next = NULL;
    if (lesshead != NULL)
        lesstail->next = NULL;
    lesstail->next = greaterhead->next;
    struct ListNode* ret = lesshead->next; //动态开辟的内存要free掉,防止内存泄漏
    free(lesshead);
    free(greaterhead);
    return ret;
    }
};
7.链表的回文结构
所谓回文结构就是从头读从尾读的结果是一样的,这题在输入测试用例时要按结构输入。
思路是:利用之前的题,找中间数和链表的倒置,将链表分为两个,然后比较就可以了,当链表节点数为奇数个时,在最后的比较中,会指向同一个节点,如图(参考链表倒置的代码,是有在尾节点的下一个置NULL的):
在进行倒置的过程过程中,链表A已经被修改了
下面是代码:
class PalindromeList {
public:
    struct ListNode* findMid(struct ListNode* A)
    {
        struct ListNode* slow = A;
        struct ListNode* fast = A;
        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
    struct ListNode* reserveList(struct ListNode* A)
    {
        if(A == NULL)
        {
            return NULL;
        }
        struct ListNode* cur = A;
        struct ListNode* next = cur->next;
        struct ListNode* rHead = NULL;
        while(cur)
        {
            cur->next = rHead;
            rHead = cur;
            cur = next;
            if(next != NULL)
                next = next->next;
        }
        return rHead;
    }
    bool chkPalindrome(ListNode* A)
{
        struct ListNode* mid = findMid(A);
        struct ListNode* rHead = reserveList(mid); //这里传A直接倒置链表,然后比较效果是一样的,而且不用写findMid函数
        //如果像上面这样想就犯了一个离谱的错,牛客在这题上有bug,倒置链表后,链表是会发生改变的,所以rHead和A所指向的链表和期望的不一样,如果传A,那么链表1,3,3,2,1是回文链表(rHead链表变成12331NULL,A链表变成1NULL,用这个算法会返回true),明显错误,而在牛客上如果传A会通过
        while(A && rHead)
        {
            if(A->val == rHead->val)
            {
                A = A->next;
                rHead = rHead->next;
            }
            else
            {
                return false;
            }
        }
        return true;
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值