链表练习题
这段时间将会用这版面来记录一些链表的题目,脚踏实地去理解链表和使用链表。加油。(也许不是最优解,但是一定是符合题目要求的而且AC的)
1.移除链表元素
leetcode203题目:删除链表中等于给定值 val 的所有节点。
这是开始写的第一道,准备用3种方式来完成这道题目,记录自己的思路。
1.一般使用head节点完成
思路:既然是链表的删除元素,也只有next才能访问到其他元素,所以从head开始判断就好。
需要分别对head 和head.next进行判断.
其实就是遍历链表:时间复杂度O(N)
public ListNode removeElements(ListNode head, int val) { //如果头节点的val就是要找的val, while(head!=null && head.val ==val ){ head = head.next; } //链表的val全是val if(head ==null){ return head; } //删除head.next位置 ListNode prev = head; while(prev.next!=null){ if(prev.next.val == val){ prev.next = prev.next.next; }else{ prev = prev.next; } } return head; }
2.使用虚拟头结点
虚拟头结点的好处就在于可以不需要单独考虑head的情况,把head的情况也考虑在next里面去就好了。
时间复杂度O(N)
//使用虚拟头节点的方法:不需要单独判断头结点的情况了。 ListNode dummyHead = new ListNode(-1); dummyHead.next = head; //后面的和用head的方式一摸一样,但是要注意引用关系 ListNode prev = dummyHead; while(prev.next!=null){ if(prev.next.val == val){ prev.next = prev.next.next; }else{ prev = prev.next; } } return dummyHead.next;
3.递归的方式
由于递归具有天然的递归结构。如果不把链表看出一个一个节点,而是看成一个节点后面跟着比这个链表小一个规模的链表。null也可以当成最小规模的链表,这样就是一个递归的思想了。
所以对于这道移除严元素的题目来说,最终得到的是移除元素后的链表。
递归的关注点主要有2个。
1.最小规模的子问题的情况(即递归到底):最小规模子节点就是一个Null。 if(head == null) return head;
2.子问题怎么组合成比他大一个规模的问题:对于这个题目来说,当后面的链表已经是删除元素后的链表了(假设子链表头结点为res)。
与已知头结点的关系其实就是 如果head.val ==val,那头结点接上子链表就是所求解。head.next = res; 否则直接返回子链表就好了。 return res;
接下来看代码,思路理解后代码还是挺简单的。
递归的次数走遍整个链表,时间复杂度为O(N),比较相等为O(1),所以整体也是O(N)。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode removeElements(ListNode head, int val) { //使用递归的方式 //最小规模的情况 if(head == null) { return head; } //子问题组合成原问题的过程 ListNode res= removeElements(head.next,val); if(head.val ==val) { return res; }else { head.next = res; } return head; } }
2.反转一个单链表
leetcode206:反转一个单链表。
思路:单链表的反转其实比较好理解的。主要是要理解链表反转时放在断链的问题。
1.非递归的方式
非递归的方式就要想好如果防止断链。这里采用3指针来标识前置结点,当前结点,下一个节点。然后进行判断
具体非递归的方式在剑指offer分类里面写的挺清楚的。剑指Offr:反转链表 跳过去看看就好。这里重点分析一下递归的方式。
2.递归的方式
递归的方式还是从宏观上关注问题。
1.最小规模的问题的结果:当最小链表是null或者只有1个节点的时候,直接返回head就可以了。不需要反转,if(head == null) return head;
2.关注子问题的解怎么组合成前一个规模的问题的解。
其实比较好理解,因为子链表已经是反转完成的了。所以要解决的就是子链表怎么指向头结点,然后头结点指向null的问题.
head.next.next =head;//head的下一个元素是子链表的最后一个元素。所以子链表最后一个元素.next=head就反转成功了。
head =null;//将head的next也改变方向。最后会指向null
class Solution { public ListNode reverseList(ListNode head) { if(head==null||head.next ==null) return head;
//要注意问题的解是给出反转后链表的头结点,后面的处理是解决head和子链表的关系 ListNode res = reverseList(head.next); head.next.next = head; head.next = null; return res; } }
递归的思路其实都差不多。关键是从宏观理解问题.
3.删除排序链表中的重复元素
leetcode83:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
1.非递归
链表非递归的方式要关注头结点和next节点的关系,结合题目分情况讨论
这个题目的是要删除重复元素的,所以一个节点或者节点为空都可以直接返回。
而在多个元素的情况,抓住链表的是排序的。。所以其实当前节点只需要和下一个节点比较就好 了。重复的越过他就好了。(leetCode解题就不将无用的节点.next = null了,)
代码比较简单,就不注释了。看上面介绍结合代码就好了。
class Solution { public ListNode deleteDuplicates(ListNode head) { if(head ==null || head.next == null){ return head; } ListNode prev = head; while(prev.next!=null){ if(prev.val == prev.next.val){ prev.next = prev.next.next; }else{ prev = prev.next; } } return head; } }
2.递归的方式
老规矩思考,递归要从宏观去理解题目
1.问题的最小规模:当只有一个节点或者节点为空的时候,直接返回head就好
2.子问题的解怎么组合成原问题的解。问题的解是一个没有重复元素的链表
这里的思考重点其实和非递归一样,抓住已排序。
而子链表是全不重复的,所以只需要和子链表的头节点比较就好了。相同则越过,不相同则接上。这样就组合成原问题的解了
class Solution { public ListNode deleteDuplicates(ListNode head) {
//最小规模问题的解 if(head ==null || head.next == null){ return head; }
//要注意问题的解的意思。可以和上一道题目进行比较一下思考。 ListNode res = deleteDuplicates(head.next); ListNode prev =head; if(prev.val == res.val){ prev.next=prev.next.next; }else{ head.next =res; } return head; } }
4.排序链表
leetcode148: 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
思路:这道题目的重点就在于时间复杂度和空间复杂度都有要求,O(n log n)的算法有归并排序和快排。而由于一般都是练习的数组的排序算法的思维影响,归并和快排都是不能达到O(1)额外空间的。
但是对于链表来说,其实并不需要多余的空间来存储链表的节点。这是比较重要的。而且链表的merge的一端为Null以后,后面的就直接接上就可以了。
这道题目我选用归并来做。因为归并的3个要点:
1.选取中点位置。找到链表的中点位置也是一道题目。如果用遍历的方式,时间复杂度就是O(n+n/2)了.但是用快慢指针的方式(fast-slow)就可以用O(logn)的时间复杂度完成。
2.递归调用:还是老思路,递归一定要从宏观理解问题,明确终止条件和子问题的解是如何组成原问题的解的。
结合链表的结构和归并排序的要点。终止条件是只有一个元素的时候,而子问题组成原问题其实就是merge的过程。
3.merge:链表的归并过程和数组的归并并不一样。不需要O(n)空间。定义一个指针指向第一个结点,2个子链表的头结点比较大小向后移动,而定义的指针一直指向小的节点,最终2个链表都走完得出结果。
具体看代码:
/* 归并排序的3个要点: 1.划分中点:这里用快慢指针法来确定中点。快慢指针法的理解可以百度一下。(这里需要多一个节点记录中点的前一个节点,方便链表断开) 2.递归调用方法:理解递归子问题的返回值和原问题的意义一样,是已排序好的链表的头结点。 3.merge:归并操作就是将子问题组合成原问题的方法。可以理解一下。 */ class Solution { public ListNode sortList(ListNode head) { if(head ==null){ return head; } return mergeSort(head); } private ListNode mergeSort(ListNode head){ //递归的终止条件 if(head.next== null){ return head; } //利用快慢指针法找到中点 slow走1步,fast走2步,当fast走到终点slow刚好到达终点,以这个为中点,递归调用。 ListNode slow = head; ListNode fast = head; ListNode prev =null; while(fast!=null && fast.next!=null){//注意这个判断语句,fast!=null在前,避免出现空指针的情况 prev = slow; fast =fast.next.next; slow =slow.next; } //将2个节点断开 prev.next = null; ListNode res1 = mergeSort(head); ListNode res2 = mergeSort(slow); return merge(res1,res2); } private ListNode merge(ListNode res1,ListNode res2){ //用虚拟头结点的方式来简化对于头结点的判断 ListNode dummyHead = new ListNode(-1); ListNode prev = dummyHead; while(res1 !=null && res2 !=null){ if(res1.val >=res2.val){ prev.next = res2; res2 = res2.next; prev =prev.next; }else{ prev.next = res1; res1 = res1.next; prev =prev.next; } } if(res1==null){ prev.next =res2; } if(res2 ==null){ prev.next =res1; } return dummyHead.next; } }
这里如果懂归并排序的话,应该不难理解。我在merge的过程使用了虚拟的头结点,这样不需要对链表头结点进行单独的判断,很实用的操作。
注:题目还是有难度的。做了一定量的题目(找链表中点,快慢指针理解)和对排序算法的理解更深(基于原归并排序的改写)以后,还是有点用的。
对于一些提示就能很快有理解写出点东西,对很多基本的算法和数据结构都能比较快的写出来。感觉这才是最大的进步。不一定记得住全部,但是最根本的常用的记住了。以后看别人代码或者自己去写代码思路都会比较清晰,加油!!
5.判断链表是否有环
Leetcode141 :给定一个链表,判断链表中是否有环。
快慢指针法又用上了。如果链表有环的话,快指针就一定会追上慢指针的
public class Solution { public boolean hasCycle(ListNode head) { if(head ==null){ return false; } if(head.next ==null){ return false; } ListNode slow =head; ListNode fast =head.next; while(fast!=null && fast.next !=null){ //分2种情况的追上,当快慢指针都走一次,快指针在慢指针的前面,一个是快指针刚好跳到慢指针的位置, if(fast == slow || fast.next ==slow){ return true; } fast =fast.next.next; slow = slow.next; } return false; } }
6.两两交换链表的节点
leetcode24 :给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 注意:
- 你的算法只能使用常数的额外空间。
- 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
思路:两两交换其实和反转链表的思路是很相似的,但是要注意反转后链表的指向的变化情况。
所以一样是定义3个指针prev,cur,pNext来表示节点之间的关系。
由于是两两的交换,所以我定义一个变量来控制是否需要交换。
遍历链表是O(N),交换是O(1),整体是O(N)的时间复杂度。
看代码:
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode swapPairs(ListNode head) { //一样是虚拟头节点。 ListNode dummyHead = new ListNode(-1); dummyHead.next = head; //至少2个才能交换撒 if(head == null || head.next ==null){ return head; } //控制是否需要交换的变量 int count= 0; //3个指针标识节点关系 ListNode cur =dummyHead.next; ListNode pNext = dummyHead.next.next; ListNode prev = dummyHead; //遍历链表就好 while(pNext!=null){ if(count%2==0){ //节点反转 prev.next = cur.next; cur.next = pNext.next; pNext.next =cur; //节点反转完成后记得把指针也要交换 ListNode temp =cur; cur=pNext; pNext=temp; //反转完成后整体指针向前移动 prev = prev.next; cur=cur.next; pNext=pNext.next; //下一轮就不用交换了 count++; }else{ //不需要交换,指针往后走就可以了。 prev = prev.next; cur=cur.next; pNext=pNext.next; count++; } } //注意是虚拟头结点 return dummyHead.next; } }