【链表经典OJ-上】

★★★★★个人专栏《C语言》《数据结构-初阶》★★★★★

欢迎各位大佬交流!!!

通过对经典链表OJ题目的练习,不仅能加深对链表的理解,更能体会链表的精妙之处!

目录

1、移除链表元素

思路一:

思路二:

2、反转链表

思路一:

思路二:

3、合并两个有序链表

双指针法:

4、链表的中间节点

思路一:

思路二:


1、移除链表元素

看到题目之后,我们第一个思路就是遍历一遍链表,遇到某个节点的val满足条件就删除此节点;

思路一:

当我们要删除某一个节点时,肯定要暂存此节点的下一个节点;

那么不妨创建出cur、prev两个节点,一个用来遍历节点,另一个来记录cur的前一个节点地址

那如果头节点的val就等于val呢?显然要更改头节点,那么我们提前处理这种情况,保证cur和prev不受影响;

尝试写代码:

先提前处理头节点的情况

cpp 复制代码
if(head && head->val == val)
{
   //更改头节点
   head = head->next;
}  

接着创建两个指针用来遍历,同时分情况处理

cpp 复制代码
while(cur)
    {
        if(cur->val == val)
        {
            //删除cur
            struct ListNode* next = cur->next;
            prev->next = cur->next;
            free(cur);
            cur = next;
        }
        else
        {
            //同时向后走
            prev = cur;
            cur = cur->next;
        }
    }

整合一下

cpp 复制代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
    //思路一:创建两个指针
    //先处理头节点
    while(head && head->val == val)
    {
        //更改头节点
        head = head->next;
    }  

    struct ListNode* cur = head;
    struct ListNode* prev = head;

    while(cur)
    {
        if(cur->val == val)
        {
            //删除cur
            struct ListNode* next = cur->next;
            prev->next = cur->next;
            free(cur);
            cur = next;
        }
        else
        {
            //同时向后走
            prev = cur;
            cur = cur->next;
        }
    }
    return head;
}

现在来运行一下

程序出错了,提示也并不是很明显,我们再来思考一下;

首先我们的遍历节点这段代码肯定是没有问题的;

那么就要往前看,我们再思考一下;

如果给定的链表前两个节点的val都等于val呢?而我们的if只能判断一次,因此我们将if改为while再试一次

在此基础上,我们还可以适当优化一下;

我们可不可以将所有节点的判断都看成是普通节点的判断?

就是创建一个虚拟头节点

将此虚拟头节点当做给定链表头节点,后续所有节点都当做普通节点来处理

cpp 复制代码
    //创建虚拟头节点
    struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    dummyhead->next = head;

创建出虚拟头节点之后,依旧用双指针的方法进行遍历即可

在最终返回时,一定要返回虚拟头节点的下一个节点!!!

cpp 复制代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
    //创建虚拟头节点
    struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    dummyhead->next = head;
    struct ListNode* cur = dummyhead->next;
    struct ListNode* prev = dummyhead;


    while(cur)
    {
        if(cur->val == val)
        {
            //满足条件,删除
            prev->next = cur->next;
            free(cur);
            cur = prev->next;
        }
        else
        {
            prev = cur;
            cur = cur->next;
        }
    }
    return dummyhead->next;
}

提交一下:

思路二:

我们换一种角度来思考,重新新建一个链表怎么样?如果某个节点的val != val,此时就尾插到新链表中,最后返回新链表的头节点即可

首先创建出新链表的头节点、遍历原链表的cur节点

有了思路一的铺垫,思路二就显得简单的多

cpp 复制代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    struct ListNode* newtail = NULL;

    while(cur)
    {
        if(cur->val != val)
        {
            //加入到新链表中
            if(newhead == NULL)
            {
                //新链表的第一个节点,既是头节点,也是尾节点
                newhead = newtail = cur;
            }
            else
            {
                //尾插即可
                newtail->next = cur;
                newtail = cur;
            }
        }
        cur = cur->next;
    }
    return newhead;
}

此时,我们提交一下后发现测试用例一无法通过

我们看到预期结果时1,2,3,4,5,而我们的输出结果却是1,2,3,4,5,6 最终多了一个节点

这是为何?

当我们处理到5节点时,newtail = cur,继续向后走,发现最后一个节点的val与题目所给val相等;此时往后走就退出了循环

而newtail的下一个节点不还是连接着节点6吗?

因此,只需将newtail的next置为空即可满足题意

cpp 复制代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    struct ListNode* newtail = NULL;

    while(cur)
    {
        if(cur->val != val)
        {
            //加入到新链表中
            if(newhead == NULL)
            {
                //新链表的第一个节点,既是头节点,也是尾节点
                newhead = newtail = cur;
            }
            else
            {
                //尾插即可
                newtail->next = cur;
                newtail = cur;
            }
        }
        cur = cur->next;
    }
    if(newtail != NULL) newtail->next = NULL;
    
    return newhead;
}

提交一下

没有问题!!!

AC了

2、反转链表

反转链表,就是将链表逆置

思路一:

首先能够想到的就是创建两个指针,遍历一遍链表;

同时在逆置的过程中创建next指针,用来控制cur及暂存节点

cpp 复制代码
struct ListNode* reverseList(struct ListNode* head)
{
    //直接将prev置为空,当处理第一个元素时,就会将其next置为空,恰好作为反转链表后的尾节点
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;

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

没有问题,这种迭代法很简洁高效

思路二:

不妨再想一想,如果用和上道题思路二的想法,新建一个链表;

同时将原链表的每一个节点都头插到新链表中,最终返回新链表的头节点

cpp 复制代码
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* newtail,*newhead = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        if(newhead == NULL)
        {
            //既是头节点,也是尾节点
            newhead = newtail = cur;
            newtail->next = NULL;
            cur = cur->next;
        }
        else
        {
            //将每个节点头插到新链表中即可
            struct ListNode* next = cur->next;
            cur->next = newhead;
            newhead = cur;
            cur = next;
        }
    }    
    return newhead;
}

此时我们提交一下

会发现出错了,看用例发现只输出了一个结果,说明没连接上;

而我们头插的逻辑又是没问题的,那么问题就在于这个既是头节点又是尾节点的处理中;

cpp 复制代码
newhead = newtail = cur;
newtail->next = NULL;
cur = cur->next;

我们将newtail->next = NULL之后,不就相当于cur->next = NULL,接着cur = cur->next,不就相当于直接将cur置为空了;

那么循环肯定进不来了,导致只会输出一个结果

因此我们只需暂存下一个节点的信息,然后再赋值给cur即可

cpp 复制代码
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* newtail,*newhead = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        struct ListNode* next = cur->next;
        if(newhead == NULL)
        {
            //既是头节点,也是尾节点
            newhead = newtail = cur;
            newtail->next = NULL;
            cur = cur->next;
        }
        else
        {
            //将每个节点头插到新链表中即可
            
            cur->next = newhead;
            newhead = cur;
        }
        cur = next;
    }    
    return newhead;
}

3、合并两个有序链表

双指针法:

题目中明确表示两个链表均递增,因此很容易想到双指针遍历两个链表,根据curA->val 与 curB->val来确定添加节点的先后顺序;

首先创建两个cur指针,接着创建newhead和newtail用来表示新链表的头节点和尾节点;

核心目的就是通过双指针的遍历,至少会将一个链表全部遍历完,后续再处理剩下的节点时直接挂上对应的cur节点即可

cpp 复制代码
    struct ListNode* curA = list1;
    struct ListNode* curB = list2;
    struct ListNode* newhead = NULL,*newtail = NULL;
    while(curA && curB)
    {
        //至少遍历完一个链表
        if(curA->val < curB->val)
        {
            struct ListNode* next = curA->next;
            //将curA尾插到链表中
            if(newtail == NULL)
            {
                //既是头节点也是尾节点
                newhead = newtail = curA;
            }
            else
            {
                //尾插即可
                newtail->next = curA;
                newtail = curA;
            }
            curA = next;
        }
        else
        {
            struct ListNode* next = curB->next;
            //将curB尾插到链表中
            if(newtail == NULL)
            {
                //既是头节点也是尾节点
                newhead = newtail = curB;
            }
            else
            {
                //尾插即可
                newtail->next = curB;
                newtail = curB;

            }
            curB = next;
        }
    }

当上述代码执行完之后,再处理未遍历完的链表;

此时我们可以选择用两个while循环,一个处理curA,一个处理curB

cpp 复制代码
    if(newtail == NULL)
    {
        //说明至少一个链表是空的
        //因此直接返回那个非空的链表即可
        return curA ? curA : curB;
    }
    if(curA)
    {
        //尾插到newtail中
        newtail->next = curA;
    }
    if(curB)
    {
        //尾插到newtail中
        newtail->next = curB;
    }

这样就将剩下的节点处理完了;

既然逻辑是如果curA不为空,就将curA挂在newtail的后面,否则就将curB挂在newtail的后面;

不妨也用三目操作符进行优化

cpp 复制代码
newtail->next = curA ? curA : curB;

简洁、高效

再此基础上,如果我们增加虚拟头节点呢?

是不是会让逻辑更加清晰?

来尝试写代码

cpp 复制代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* cur = dummyhead;
    struct ListNode* newtail = NULL;    

    while(list1 && list2)
    {
        if(list1->val < list2->val)
        {
            cur->next = list1;
            list1 = list1->next;
        }
        else
        {
            cur->next = list2;
            list2 = list2->next;
        }
        cur = cur->next;
    }
    cur->next = list1 ? list1 : list2;
    return dummyhead->next;
}

和之前不同的是,我们不再创建curA curB 而是直接使用list1、list2;

创建虚拟头指针,并用cur遍历新的链表;

最终返回dummyhead的下一个节点即可

4、链表的中间节点

思路一:

遍历一遍链表,同时利用cnt记录总的节点个数

然后循环cnt次,直接返回即可

cpp 复制代码
struct ListNode* middleNode(struct ListNode* head)
{
    int cnt = 0;
    struct ListNode* cur = head;
    //获取总节点个数
    while(cur)
    {
        cnt++;
        cur = cur->next;
    }
    int ans = cnt / 2 + 1;
    //由于直接指向头节点,因此会少走一步
    ans -= 1;
    struct ListNode* ret = head;
    while(ans--)
    {
        ret = ret->next;
    }
    return ret;
}

还有没有其他方法了?

下面我们用一种新的方法,也是利用两个指针;

通过快慢指针,只需遍历一遍链表即可

思路二:

定义两个指针,一个快指针,一个慢指针;

快指针一次走两步,慢指针一次走一步;

当快指针走到终点时,慢指针正好到达链表中间的位置

由于快指针一次走两步,因此循环结束的条件应该是fast && fasr->next != NULL;

下面来实现代码

cpp 复制代码
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }  
    return slow;
}

如有不足之处恳请指出!!!

谢谢大家!!!

相关推荐
riNt PTIP2 小时前
在21世纪的我用C语言探寻世界本质——字符函数和字符串函数(2)
c语言·开发语言
programhelp_2 小时前
TikTok 26 Summer SDE Intern 面经分享|两轮技术面 + Timeline 复盘
数据结构·经验分享·算法·面试
无限进步_2 小时前
二叉树的前序遍历(非递归实现)
开发语言·数据结构·c++·windows·git·visual studio
01二进制代码漫游日记2 小时前
【C语言数据结构】之解锁双向链表(头插、头删等操作)
c语言·数据结构·学习·链表
C++ 老炮儿的技术栈2 小时前
工业视觉检测:用 C++ 和 Snap7 库快速读写西门子 S7-1200
c语言·c++·git·qt·系统架构·visual studio·snap
hipolymers2 小时前
C语言是什么
c语言·嵌入式开发·编程范式·高效性·系统级编程
j_xxx404_2 小时前
Linux C 语言编译链接全解析:静态库与动态库从原理到实战
linux·运维·服务器·c语言·编辑器
WL_Aurora2 小时前
每日一题——自然倍树
数据结构·python·算法·深度优先
Lazionr2 小时前
【链表经典OJ-中】
c语言·数据结构·链表