赞
踩
目录
上篇文章是有关于两个较常用链表的结构的实现。实现完链表过后,我们还需要会熟练运用链表这种数据结构,所以这里将会有十道链表OJ题目,本篇文章将会先分析解决七道题目,剩下三道留在下一篇文章。每道题都题目后都有链接,还会有大量的图示,帮助你理解题目解法,附上解题代码。
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。链接--力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
- //题目结构题定义如下
- struct ListNode
- {
- int val;
- struct ListNode *next;
- };
注意:之后题目若没有详细说明,都是以上面单链表结构体为准!!!
示例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
输出:[ ]
从题目和示例来看,就是去除链表中存储数据相同的结点时,跟单链表的删除操作十分相似。需要注意的是特殊情况,链表为空的时候直接返回空链表即可。在删除的过程中要分两种情况,分别是头部删除和中间删除,这是因为改题目要求返回新的头结点,中间的删除不会影响头结点的指向。
下面分析这两步操作,在执行前,我会定义prev和cur这两个指针,prev指向空指针,cur指向头结点。prev指向的是cur指针的前一个结点,但是一开始cur指向头结点,所以指向空。
- struct ListNode* removeElements(struct ListNode* head, int val) {
- struct ListNode* prev = NULL;
- struct ListNode* cur = head;
- while(cur)
- {
- if (cur->val == val)
- {
- //1.头部珊除
- //2.中间删除
- if (cur == head)
- {
- head = cur->next;
- free(cur);
- cur = head;
- }
- else
- { //删除
- prev->next = cur->next;
- free(cur);
- cur = prev->next;
- }
- }
- else
- { //往后走
- prev = cur;
- cur = cur->next;
- }
- }
- return head;
- }
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
示例1:
输入:head = [ 1,2,3,4,5 ]
输出:[ 5,4,3,2,1 ]
示例2:
输入:head = [ 1,2,3,4,5 ]
输出:[ 5,4,3,2,1 ]
示例3:
输入:head = [ ]
输出:[ ]
这个题目意思比较明显,我们需要把链表整个指向反转过来。有以下几种思路:
1. 正常来说,最平常的思路就是自己开辟与题目示例所给相同的个数的结点,并先遍历链表找到尾结点,依次寻找前面的结点,但是十分麻烦。倘若有五个节点需要遍历4+3+2+1+0次次,时间复杂度相当于O(N^2),空间复杂度为O(N)。
2. 在第一种做法的基础上,进一步优化,还是开辟示例所给相同个数的结点,但是这次我们只需要遍历一次链表即可。我们需要三个指针,是cur,newCur,newPrev,分别指向原链表,新链表。
这个做法只需遍历链表一次,时间复杂度为O(N),空间复杂度还是O(N)。代码如下:
- struct ListNode* reverseList(struct ListNode* head) {
- struct ListNode* cur = head;
- struct ListNode* newCur= NULL;
- struct ListNode* newPrev= NULL;
- while(cur)
- {
- newCur = (struct ListNode*)malloc(sizeof(struct ListNode));
- if (cur == head)
- { //赋值
- newCur->val = cur->val;
- newCur->next = NULL;
- //迭代往后
- newPrev = newCur;
- cur = cur->next;
- }
- else
- { //赋值
- newCur->val = cur->val;
- newCur->next = newPrev;
- //迭代往后
- newPrev = newCur;
- cur = cur->next;
- }
- }
- return newCur;
- }
3. 不过大家可以仔细想想,既然我们创建新链表的时候可以反转,为什么不直接在原链表进行反转。这就是此题的最优解,在空间上,只需要创建几个指针变量,空间复杂度为O(1),在时间上,只需要遍历一次链表即可,时间复杂度为O(N)。
在开始反转前,需要创建三个指针变量,cur,newhead和next,其中next是放在while循环中创建的指针变量。cur先指向头结点,newhead指向空指针。
代码如下:
- struct ListNode* reverseList(struct ListNode* head) {
- struct ListNode* cur = head;
- struct ListNode* newhead = NULL;
- while(cur)
- {
- struct ListNode* next = cur->next;
- //头插
- cur->next = newhead;
- newhead = cur;
-
- //迭代往后走
- cur = next;
- }
- return newhead;
- }
给你单链表的头结点 head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
示例1:
输入:head = [ 1,2,3,4,5 ]
输出:[ 3,4,5 ]
解释:链表只有一个中间结点,值为3.
示例2:
输入:head = [ 1,2,3,4,5 ]
输出:[ 3,4,5 ]
解释:链表只有两个中间结点,值分别为3和4,返回第二个结点。
寻找中间结点需要分奇数和偶数两种情况。
解法一:
正常来说,因为单链表不知道结点个数,需要利用一个指针变量遍历整个链表,记下链表长度,然后再次从头节点开始,让指针指向链表长度的一半。总的来说,在时间消耗上,如果有N个结点,需要执行N + N / 2 + 1次。
解法二:
那有没有更好的解法呢?那就得请出快慢指针了,在第一种解法上进一步优化,只需遍历一遍链表。怎么做呢?快慢指针顾名思义有两个指针,一个走的快,一个走得慢。
综上所述,while循环结束条件是fast指针为空或者fast的next指针为空。
- struct ListNode* middleNode(struct ListNode* head)
- {
- struct ListNode* slow, *fast;
- slow = fast = head;
- while (fast && fast->next)
- {
- slow = slow->next;
- fast = fast->next->next;
- }
- return slow;
- }
输入一个链表,输出该链表中倒数第k个结点。链接:链表中倒数第k个结点_牛客题霸_牛客网
示例1:
输入:1, [ 1,2,3,4,5 ]
输出:[ 5 ]
解释:倒数第一个结点是5,返回5字后的链表
这道题跟中间结点类似,只不过是求倒数第k个结点,下面我提供两种思路
解法一:
正常来说,大家首先想到的应该是遍历知道链表节点个数,然后再从头结点开始,走n-k步。在时间消耗上,最坏的情况是要走2 * n步。
解法二:
这里会使用快慢指针,但与找中间结点不同的是,这次是fast指针先走,然后slow指针和fast指针都走一步。为什么是这样子呢?我们以链表 [ 1,2,3,4,5 ],K = 2为例。
所以说在时间消耗上,只需要遍历一次链表,是第一种解法的优化。我们已经分析完了过程,代码怎么写呢?首先用一个while循环让fast指针先走K步。其次,再用while循环让两个指针一起走,结束条件是fast的指针为空。不过得注意的是,如果是空链表或者k小于零的情况,直接返回空指针。
- struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
- {
- if (pListHead == NULL || k <= 0)
- return NULL;
- struct ListNode* slow, *fast;
- slow = fast = pListHead;
- while(k--)//先走K步
- {
- if (fast == NULL)
- return NULL;
- fast = fast->next;
- }
-
- while(fast != NULL)//同时走一步
- {
- slow = slow->next;
- fast = fast->next;
- }
- return slow;
- }
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
- struct ListNode* mergeTwoLists
- (struct ListNode* list1,
- struct ListNode* list2) {
- }
示例1:
输入:l1 = [ 1,2,4 ],l2 = [ 1,3,4 ]
输出:[ 1,1,2, 3,4,4 ]
示例2:
输入:l1 = [ ],l2 = [ ]
输出:[ ]
示例3:
输入:l1 = [ ],l2 = [ 0 ]
输出:[ 0 ]
这道题有递归和迭代两种方式,这里只详解迭代的方式。迭代思路还是比较好想的,难的是代码实现。思路就是链表一和链表二从第一个结点开始比较,较小的结点就成为新链表的结点,较大的继续跟下一个结点比较,直到所有结点比较完。在写代码上,有两种形式,就是带不带哨兵位的结点。自己创建一个指针head表示新链表的头,还有一个tail指针,为插入后续节点服务,将这两个指针都赋值为空指针。
在这之前还要判断链表有没有空的情况。代码如下:
- struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
- if (list1 == NULL)
- return list2;
- if (list2 == NULL)
- return list1;
- struct ListNode* head = NULL, *tail = NULL;
- while (list1 && list2)
- {
- if (list1->val < list2->val)
- {
- if (head == NULL)
- {
- head = tail = list1;
- }
- else
- {
- tail->next = list1;
- tail = list1;
- }
- list1 = list1->next;
- }
- else
- {
- if (head == NULL)
- {
- head = tail = list2;
- }
- else
- {
- tail->next = list2;
- tail = list2;
- }
- list2 = list2->next;
- }
- }
- if (list1)
- {
- tail->next = list1;
- }
- if (list2)
- {
- tail->next = list2;
- }
- return head;
- }
- struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
- if (list1 == NULL)
- return list2;
- if (list2 == NULL)
- return list1;
- struct ListNode* head = NULL, *tail = NULL;
-
- head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
-
- while (list1 && list2)
- {
- if (list1->val < list2->val)
- {
- tail->next = list1;
- tail = list1;
- list1 = list1->next;
- }
- else
- {
- tail->next = list2;
- tail = list2;
- list2 = list2->next;
- }
- }
- if (list1)
- {
- tail->next = list1;
- }
- if (list2)
- {
- tail->next = list2;
- }
-
- struct ListNode* list = head->next;
- free(head);
- return list;
- }
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。链接:链表的回文结构_牛客题霸_牛客网
- class PalindromeList {
- public:
- bool chkPalindrome(ListNode* A) {
- }
- };
这道题在牛客网中没有提供C语言版,有C++函数,可以直接在此函数内部写C语言版代码。因为C++兼容C语言。
测试样例:
输入:A = [ 1->2->2->1 ]
输出:true
这道题目要判断是否为回文结构,我们可以观察回文链表,发现回文链表从中间结点断开,两边是对称的,那我们可以找到中间结点,并把中间结点之后的链表逆置,再进行比较存储的整型值。因此,之前写的两道题反转链表和找中间结点就可以派上用场了。不过得注意奇数偶数的情况。
要反转中间结点后的链表,有的人会直接断开,就像下面图例的情况,如果是偶数个结点,从头开始比较,到空指针的时候结束了。如果是奇数个结点,断开的时候会多出一个中间结点,只要其中一个链表走到空指针就结束,所以比较判断结束条件是其中一个链表为空。但是反转链表后,还需要断开链表,比较麻烦,我们可以看看不断开的情况。
如下图所示,这就是不断开的情况。如果是奇数个结点,从两边开始比较,最后都会走向空指针。如果是偶数个结点,从两边开始比较,其中一个会先走到空指针,就结束对比的过程。
你可以把寻找中间结点函数和反转链表函数再重新写一遍,也可以直接借用,再进行比较。
- //寻找中间结点
- struct ListNode* middleNode(struct ListNode* head)
- {
- struct ListNode* slow, *fast;
- slow = fast = head;
- while (fast && fast->next)
- {
- slow = slow->next;
- fast = fast->next->next;
- }
- return slow;
- }
-
- //反转链表
- struct ListNode* reverseList(struct ListNode* head)
- {
- struct ListNode* cur = head;
- struct ListNode* newhead = nullptr;
- while(cur)
- {
- struct ListNode* next = cur->next;
- //头插
- cur->next = newhead;
- newhead = cur;
- //迭代往后走
- cur = next;
- }
- return newhead;
- }
-
- class PalindromeList {
- public:
- bool chkPalindrome(ListNode* A)
- { //找到中间结点
- struct ListNode* mid = middleNode(A);
- //中间结点的头
- struct ListNode* rHead = reverseList(mid);
- //原链表的左右结点
- struct ListNode* curLeft = A;
- struct ListNode* curRight = rHead;
- while (curLeft && curRight)
- {
- if (curLeft->val != curRight->val)
- {
- return false;
- }
- else
- {
- curLeft = curLeft->next;
- curRight = curRight->next;
- }
- }
- return true;
- }
- };
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构 。
示例1:
输入:intersectVal = 8, listA = [ 4,1,8,4,5 ],listB = [ 5,6,1,8,4,5 ],skipA = 2,skipB = 3
输出:IntersectVal at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例1:
输入:intersectVal = 0, listA = [ 2,6,4 ],listB = [ 1,5 ],skipA = 3,skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
我们要判断是否为相交链表,并找出相交点,而且不能破坏原链表的结构。我们先要清楚相交链表的概念,题目给的图示就很直观,有两个链表的最后一个节点不指向空指针,但是同时指向一个结点。还要注意的是,不能形成环形链表,就是一个链表出现头尾相连的情况。
这道题我会提供两个思路。
思路一:
我们观察相交链表,会发现不管从A链表出发,还是B链表出发,在指向空指针之前的结点是相同的,我们可以根据这个判断是否为相交链表。并在遍历AB链表的时候,记录链表从A出发和从B出发的长度,算出差距多少步。定义两个指针变量,让长的链表指针变量先走差距步,然后两个链表指针同时走,当走的过程中,两个指针相同时,必然是相交的节点。
代码如下:
- struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
- {
- struct ListNode* tailA = headA;
- struct ListNode* tailB = headB;
- int lenA = 1;
- while(tailA->next)
- {
- lenA++;
- tailA = tailA->next;
- }
- int lenB = 1;
- while (tailB->next)
- {
- lenB++;
- tailB = tailB->next;
- }
-
- if(tailA != tailB)
- {
- return NULL;
- }
- //abs时绝对值
- int gap = abs(lenA - lenB);
- //先假设A链表是长链表,再比较,如果不是再重新赋值
- struct ListNode* longlist = headA;
- struct ListNode* shortlist = headB;
- if (lenA < lenB)
- {
- shortlist = headA;
- longlist =headB;
- }
- // 长的先走差距步,再同时走交点
- while(gap--)
- {
- longlist = longlist->next;
- }
-
- while(longlist != shortlist)
- {
- longlist = longlist->next;
- shortlist = shortlist->next;
- }
-
- return longlist;
- }
思路二:
思路二更加巧妙,我用图示展示更直观。A链表结点总个数用totalA表示,在相交结点之前A链表节点个数用lenA表示,相交结点之后公共部分用comlenth表示。B链表同理。
totalA = lenA + comlenth
totalB = lenB + comlenth
totalA = lenA + comlenth
totalB = lenB + comlenth
将其带入到curAlen和curBlen中,
curAlen = totalA + lenB = (lenA + comlenth) + lenB = (len A + lenB) + comlenth
curBlen = totalB + lenA = (lenB + comlenth) + lenA = (len A + lenB) + comlenth
那么代码怎么写呢,先要注意如果有其中一个链表为空,就无法形成相交链表,直接返回空。先创建两个指针curA和curB,写一个while循环,结束条件是curA = curB的时候,最后返回其中一个指针变量即可。
- struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
- {
- if (headA == NULL || headB == NULL)
- {
- return NULL;
- }
-
-
- struct ListNode *curA = headA,
- struct ListNode *curB = headB;
- while (curA != curB)
- {
- pA = pA == NULL ? headB : pA->next;
- pB = pB == NULL ? headA : pB->next;
- }
- return curA;
- }
-
这次七道链表OJ题目的解析可以说是干货满满,建议收藏。每做一道题,可以参考上面的图示来分析理解思路,多画图有助于你思路顺畅,后期写代码不卡壳。事后可以尝试自己总结其中用到的方法,话不多说,练起来!
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。