> 文档中心 > 冰冰学习笔记:这些链表练习题,你会吗?

冰冰学习笔记:这些链表练习题,你会吗?


欢迎各位大佬光临本文章!!!

 

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


目录

前言

1.移除链表元素

(1)遍历链表删除元素

(2)无哨兵尾插

(3)带哨兵尾插

2.反转链表

(1)头插

(2)三指针轮转

 3.链表的中间结点

(1)遍历找中间结点

(2)快慢指针



前言

前面说过单链表不会单独的作为数据存储的结构来使用,多半会作为基础讲解对象,还有就是0J 练习题中的题目多半都是单链表结构,下面我们就来盘一盘这些题!!

1.移除链表元素

题目连接:203. 移除链表元素 - 力扣(LeetCode) (leetcode-cn.com)

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

简单来说,这个题类似于在数组中将与给定的数值val相同的元素全部删除,并返回数组本身。只不过这里是链表而已,所以我们自然想到了一种解法,遍历链表然后删除元素。本题举例分析中的删除元素为6。

(1)遍历链表删除元素

首先我们将头指针head赋值给cur,然后cur对链表进行遍历,当cur->val==给定的数值val时,将cur所指向的结点释放,然后将cur赋值给cur->next。如果不相同则cur直接赋值给cur->next。

上面这种只是一般情况,我们还要考虑一些特殊的情况,例如如果head指向的是空链表怎么办,链表元素全是给定的val怎么办?

如果链表为空,我们就直接返回NULL,如果链表元素开头为val我们就让head往后走,直到head指向的val不为所给定的val。然后就像分析的那样进行删除,最后返回head指针。

但是我们发现,我们释放掉结点后,需要将前后的结点连接起来,后面的结点就是cur->next,前面的结点并不好找,所以我们要将前面的结点保存到指针pre之中。

下面是代码以及逐句分析:

具体代码:

struct ListNode* removeElements(struct ListNode* head, int val){    struct ListNode* pre = NULL;    struct ListNode* cur = head;    while ( cur != NULL )    { if ( cur->val == val ) {     if ( cur == head )     {  head = cur->next;  free(cur);  cur = head;     }     else     {  pre->next = cur->next;  free(cur);  cur = pre->next;     } } else {     pre = cur;     cur = cur->next; }    }    return head;}

(2)无哨兵尾插

上面的解法最容易想到,那有没有其他的方法呢?有,例如我们使用尾插的方法,将链表中结点元素不为val 的结点拿下来进行尾插,释放掉val相等的结点,然后将新连接的链表赋值给head。

等cur遍历一遍原链表后,新的链表也尾插完毕,然后返回新链表即可 

值得注意的是,我们需要将尾插的最后一个结点的next要置为NULL,如果不置为NULL,假设原链表的最后一个元素val的值与删除元素相等,那么我们尾插的就是上一个结点,其结点中的next还保存指向原链表最后一个元素的地址,我们将其释放后,必然会造成野指针的问题。

代码如下:

struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* newnode=NULL;    struct ListNode* cur = head;    head = NULL;    //尾插    while ( cur != NULL )    { if ( cur->val == val ) {     struct ListNode* del = cur;     cur = cur->next;     free(del); } else {     if ( newnode == NULL )     {  newnode = head = cur;     }     else     {  newnode->next = cur;  newnode = newnode->next;     }     cur = cur->next; }    }    if ( newnode )    { newnode->next = NULL;    }return head;}

下面我们开始分析这个代码:

注意一下图中黄色框圈起来的代码:head==NULL;这有什么用呢?

如果结点元素val全都是需要删除的元素,例如链表中的val都是6的情况,此时我们会发现,代码进入循环后只会走if语句,将原链表全部删除,然后head保存的还是原来头节点的位置,并非我们要返回的NULL。所以要将其置空处理。

(3)带哨兵尾插

我们发现上面的尾插的方法也没有想象中的这么简单,指向相对好理解了一下。例如我们在处理尾插的时候就要处理两步,判断是否为第一次尾插或者不是第一次尾插,能不能有简单点的尾插方法呢?

答案是有的,就是创建一个哨兵位,不存任何元素,只需要将需要尾插的数据直接连接在后面即可,最后返回哨兵位的next。这样我们就不用在判断是否为第一次尾插,或者是否为全都是删除元素的情况了。

话不多说,直接上代码开始分析:

struct ListNode* removeElements(struct ListNode* head, int val){    //哨兵结点    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));    newnode->next = NULL;    struct ListNode* cur = head;    struct ListNode* tail = newnode;    //尾插    while ( cur != NULL )    { if ( cur->val == val ) {     struct ListNode* del = cur;     cur = cur->next;     free(del); } else {     tail->next = cur;     cur = cur->next;     tail = tail->next; }    }    tail->next = NULL;    struct ListNode* del = newnode;    newnode = del->next;    free(del);    return newnode;}

代码分析:

 使用该方式一定不要忘了释放掉哨兵的结点空间,不然会造成内存泄漏的情况。

2.反转链表

题目链接:206. 反转链表 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

题目意思就是让原链表12345经过翻转后变为54321,返回指向新链表的头节点

经过上一个题的具体介绍,我想我们应该很自然的想到开辟新结点进行插入的办法。

(1)头插

这次是头插而不是尾插。我们将原链表中的元素拿过来进行头插,得到的就是翻转的链表。

当然我们要注意空链表的情况,这时我们应该返回空指针。头插解法是最简单的一种解法,直接上代码。

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

代码分析:

(2)三指针轮转

当然除了头插还有就是在原链表的基础上进行逐步翻转,这就是三指针轮转法。

为什么需要三个指针呢?

首先我们需要一个指针cur对链表进行遍历,然后将cur->next进行翻转,翻转指向cur之前的元素,前面的元素又是谁呢,所以我们需要用pre来记录。随后我们需要将cur变为cur后面的结点,但是next已经被更改为前面的了,谁来记录下一个结点呢?这就需要tmp来记录。

当然我们要先判断head是否为空链表,如果是直接返回NULL。然后在进行翻转。当然cur也不一定指向head,可以指向head->next。

但是cur=cur->next并不是一直进行的,当cur已经移动到NULL 的位置时,对其进行访问操作必然会照成空指针解引用的问题,所以我们要判断,只有当cur不为NULL的时候才能进行移动cur指针。

最后tmp将会指向最后一个元素,也就是反转后的第一个元素。

 

 代码:

struct ListNode* reverseList(struct ListNode* head){    if ( head == NULL )   {    return head;   } struct ListNode* tmp = NULL; struct ListNode* pre = head; struct ListNode* cur = head->next; while ( pre != NULL ) {     pre->next = tmp;     tmp = pre;     pre = cur;     if(cur!=NULL)     cur = cur->next;      } return tmp;}

 3.链表的中间结点

题目链接:876. 链表的中间结点 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

看到该题目我们最先想到的就是遍历找中间位置。没错,第一种解法就是这样,但是我们还有一种更绝妙的解题方法。

(1)遍历找中间结点

我们从题目可以得知,链表要分为两种情况考虑,奇数个数和偶数个数。

首先我们需要知道结点的个数count,然后在遍历一遍找到中间位置pos,并将其返回,如果为空链表则返回空指针,如果链表有一个元素,则返回该链表头指针。

代码便很容易写出来:

struct ListNode* middleNode(struct ListNode* head){    if ( (head == NULL)||(head->next==NULL) )    { return head;    }    else    { int count = 0; struct ListNode* cur = head; while ( cur ) {     count++;     cur = cur->next; } int pos = count / 2 + 1; cur = head; count = 1; while ( count != pos ) {     cur = cur->next;     count++; } return cur;    }}

代码分析:

(2)快慢指针

上面的方法虽然好想但是还不够好,因为我们遍历了两遍链表,如果说只能遍历一遍链表呢?

那就得用快慢指针了。我们创建两个指针,一个每次走一步,一个每次走两步,当走两步的走完时,走一步的恰巧为中间位置,然后返回此指针即可。

我们发现奇数个结点的结束标志是fast->next==NULL,偶数个结点则为fast为NULL。

所以代码为:

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

 

冰冰学习笔记:这些链表练习题,你会吗? 创作打卡挑战赛 冰冰学习笔记:这些链表练习题,你会吗? 赢取流量/现金/CSDN周边激励大奖