[Leetcode刷题]——链表
一、找出两个链表的交点
160.相交链表(easy)2021-01-05
编写一个程序,找到两个单链表相交的起始节点
如下面的两个链表,在c1 处相交:
public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode l1 = headA; ListNode l2 = headB; while(l1 != l2){ l1 = (l1 == null) ? headB : l1.next; l2 = (l2 == null) ? headA : l2.next; } return l1; } }
解法:如果两个链表有相交,设第一个链表长度为 a+c, 第二个链表长度为b+c。
让两个链表指针从头开始遍历,第一个链表遍历a+c后接着从头开始遍历第二个链表的b,第二个链表遍历b+c后从头开始遍历第二个链表的a。
最后两个指针会同时到达相交的地方。
注意,条件判断时判断的是(l1 == null)而不是 (l1.next == null) 。其目的是为了如果两个链表没有相交,那么在遍历两个链表后连个指针均指向了链表最后的null,而非陷入死循环。
此解法的复杂度为O(n)。
二、反转链表
206.反转链表(easy)2021-01-06
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
我的原始解法,错误示范:
class Solution { public ListNode reverseList(ListNode head) { ListNode newHead = head; newHead.next = null; ListNode traverseNode = head; while(traverseNode.next != null){ traverseNode = traverseNode.next; ListNode temp = traverseNode; temp.next = newHead; newHead = temp; } return newHead; } }
错误分析:上述代码理论推导似乎可行,但是编译器运行不通过。
debug后发现,第2-5行,将head结点赋值给newhead结点,然后将newhead结点的next指针改为null,发现原始的head结点的next指针自动也变成了null。
在idea中可以看到,虽然head的值赋值给newHead 和traverseNode, 但是并没有开辟新空间,所以这里不能当一般变量处理。
标准解法:
1.递归,yyds
public static ListNode reverseList(ListNode head) { if(head == null || head.next == null){ return head; } ListNode next = head.next; ListNode newHead = reverseList(next); next.next = head; head.next = null; return newHead; }
2.头插法
public ListNode reverseList(ListNode head) { ListNode newHead = new ListNode(-1); while (head != null) { ListNode next = head.next; head.next = newHead.next; newHead.next = head; head = next; } return newHead.next; }
三、合并两个有序链表
21、合并两个有序链表(easy) 2021-01-07
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
我的代码:
常规思路,需要注意的是在进行循环后的第二个if 语句时,又重新判断了( l1 != null )。 如果不加判断条件有可能为null就直接写l1.val会产生NullPointerException异常。
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode newHead = new ListNode(-1); ListNode temp = newHead; while(l1 != null && l2 != null){ if(l1.val < l2.val){ temp.next = l1; temp = l1; l1 = l1.next; } if(l1 != null && l2.val <= l1.val){ temp.next = l2; temp = l2; l2 = l2.next; } } if(l1 == null){ temp.next = l2; } if(l2 == null){ temp.next = l1; } return newHead.next; } }
递归的写法,真简洁:
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) return l2; if (l2 == null) return l1; if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } }
四、从有序链表中删除重复结点
83、删除排序链表中的重复元素 (easy) 2021-01-08
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:输入: 1->1->2->3->3
输出: 1->2->3
我的解法:需要注意的一点是在没有判断结点是否为空的情况下之直接就使用.val 调用结点的数值会报NullPointerException异常
class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode tempNode = head; while(tempNode != null){ if(tempNode.next != null && tempNode.val == tempNode.next.val){ tempNode.next = tempNode.next.next; }else{ tempNode = tempNode.next; } } return head; } }
递归的解法,妙呀,三行就搞定!唯一不足的就是内存消耗较多。
class Solution { public ListNode deleteDuplicates(ListNode head) { if (head == null || head.next == null) return head; head.next = deleteDuplicates(head.next); return head.val == head.next.val ? head.next : head; } }
五、删除链表的倒数第N个结点
19、给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。(medium) 2021-01-09
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
我的解法,遍历两次,内存击败29.8%,还是太菜
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode tempNode = head; int len = 1; while(tempNode.next != null){ len++; tempNode = tempNode.next; } tempNode = head; if(n == len){ return head.next; }else{ while(tempNode != null && tempNode.next != null){ if(len == n + 1){ tempNode.next = tempNode.next.next; } len--; tempNode = tempNode.next; } return head; } } }遍历一次的原理,设置两个指针,让一个指针先走n长度,然后两个指针一起走,当快的指针走到链表结尾时,慢的指针就在倒数第n个位置
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode fast = head; while (n-- > 0) { fast = fast.next; } if (fast == null) return head.next; ListNode slow = head; while (fast.next != null) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return head; } }
六、两两交换链表中的相邻结点
24、给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。(medium) 2021-01-10
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
解法:力扣里的链表的头节点都是第一个结点,在这个题中,设置第一个结点的前一个结点为头结点,在判断后两个结点是否为空,然后进行交换即可。
class Solution { public ListNode swapPairs(ListNode head) { ListNode preHeadNode = new ListNode(-1); preHeadNode.next = head; ListNode traverseNode = preHeadNode; while(traverseNode.next != null && traverseNode.next.next != null){ ListNode n1 = traverseNode.next; ListNode n2 = traverseNode.next.next; traverseNode.next = n2; ListNode temp = n2.next; n2.next = n1; n1.next = temp; traverseNode = n1; } return preHeadNode.next; } }
递归的写法,需要系统的学习递归,链表和树的大多数题目太适合递归了。
参考博客https://lyl0724.github.io/2020/01/25/1/
递归三部曲:1.找终止条件 2.找返回值 3.单次过程
class Solution { public ListNode swapPairs(ListNode head) { if(head == null || head.next == null){ return head; } ListNode next = head.next; head.next = swapPairs(next.next); next.next = head; return next; } }
七、两数相加
445、两数相加 (medium) 2021-01-11
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
进阶:
如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
示例:
输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7
我的解法:我尝试用最简单的解法解这个问题,先将链表转成数字,相加后再将数组转成链表。遇到的问题是链表太长,转成int型数字溢出了,换成long型居然还溢出了。此路不通。
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { int sum = listToInteger(l1) + listToInteger(l2); return integerToList(sum); } public int listToInteger(ListNode head){ ListNode node = head; int sum = 0; while(node != null){ sum = sum * 10 + node.val; node = node.next; } return sum; } public ListNode integerToList(int sum){ if(sum == 0){ return new ListNode(0); } ListNode head = new ListNode(-1); while(sum != 0){ int val = sum % 10; ListNode node = new ListNode(val); node.next = head.next; head.next = node; sum = sum / 10; } return head.next; } }
使用另一个常规解法,翻转两个链表,然后一个结点一个结点的进行相加并设置进位。代码冗长,逻辑简单。
需要注意的是,在解此题时,翻转链表函数时,又出现了与前几天相似的错误。即复制一个结点并对他的next指针进行修改时,原始的结点也发生了改变。方法是在修改前保存下next指针。
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode node1 = reverseList(l1); //链表的头结点 ListNode node2 = reverseList(l2); ListNode head = new ListNode(-1); //求和链表的头结点 ListNode temp = new ListNode(-1); //求和链表的结点 int flag = 0; //进位标志 int sum = 0; while(node1 != null || node2 != null || flag != 0){ //要考虑求和后进位 if(node1 == null && node2 == null){ sum = 0; }else if(node1 == null){ sum = node2.val; }else if(node2 == null){ sum = node1.val; }else{ sum = node1.val + node2.val; } if(sum + flag >= 10){ temp = new ListNode(sum - 10 + flag); flag = 1; }else{ temp = new ListNode(sum + flag); flag = 0; } if(node1 != null){ node1 = node1.next; } if(node2 != null){ node2 = node2.next; } temp.next = head.next; head.next = temp; } return head.next; } public ListNode reverseList(ListNode head){ //头插法 ListNode node = new ListNode(-1); ListNode traverseNode = head; while(traverseNode != null){ ListNode next = traverseNode.next; //这里先保存next指针 traverseNode.next = node.next; node.next = traverseNode; traverseNode = next; } return node.next; } }
如果不对链表进行翻转操作,使用栈进行处理。大体都相似,翻转后取出和入栈后再取出作用相同。使用栈耗时更久但是内存占用较小。
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { Stack<Integer> stack1 = listToStack(l1); Stack<Integer> stack2 = listToStack(l2); int flag = 0; //设置一个进位的标志 ListNode head = new ListNode(-1); while(!stack1.isEmpty() || !stack2.isEmpty() || flag == 1){ int sum; //新链表结点的值 ListNode node; if(stack1.isEmpty() && stack2.isEmpty()){ sum = flag; }else if(stack1.isEmpty()){ sum = stack2.pop() + flag; }else if(stack2.isEmpty()){ sum = stack1.pop() + flag; }else{ sum = stack1.pop() + stack2.pop() + flag; } if(sum < 10){ node = new ListNode(sum); flag = 0; }else{ node = new ListNode(sum - 10); flag = 1; } node.next = head.next; head.next = node; } return head.next; } public Stack<Integer> listToStack(ListNode head){ ListNode node = head; Stack<Integer> stack = new Stack<>(); while(node != null){ stack.push(node.val); node = node.next; } return stack; } }
八、回文链表
234.回文链表 (easy) 2021-01-12请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
我的解法:先计算链表长度,再遍历链表,前一半遍历时存储到栈中,后一半遍历时就出栈进行比较是否一致。使用栈那么空间复杂度变成O(n)。
如果不使用栈,将后半段链表分割下来翻转作用一样。
好不容易来个题一遍过, 时间击败37% ,空间击败26%也太难顶了吧。
class Solution { public boolean isPalindrome(ListNode head) { Stack<Integer> stack = new Stack<>(); ListNode node = head; int length = length(head); for(int i = 0; i < length / 2; i++){ stack.push(node.val); node = node.next; } if(length % 2 == 1){ node = node.next; } while(node != null){ int pre = stack.pop(); int rear = node.val; if(pre != rear){ return false; } node = node.next; } return true; } //返回链表的长度 public int length(ListNode head){ ListNode tempNode = head; int length = 0; while(tempNode != null){ length++; tempNode = tempNode.next; } return length; }
九、分割链表
725、分隔链表 (medium)2021-01-13
给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。
这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。
返回一个符合上述规则的链表的列表。
举例: 1->2->3->4, k = 5 // 5 结果 [ [1], [2], [3], [4], null ]
解法:此题代码较长但是逻辑简单,分类讨论除法的三种结果。
class Solution { public ListNode[] splitListToParts(ListNode root, int k) { int length = length(root); ListNode[] arr = new ListNode[k]; //初始化长度为k的数组 ListNode traverseNode = root; ListNode temp = null; if(length == 0){ //0除了啥都等于0,都算除尽,单独考虑 return arr; }else if(length % k == 0){ //正好除尽 for(int i = 0; i < k; i++){ arr[i] = traverseNode; for(int j = 0 ; j < length / k - 1; j++){ traverseNode = traverseNode.next; } temp = traverseNode.next; traverseNode.next = null; traverseNode = temp; } }else if(length % k == length){ //说明k大于length for(int i = 0; i < length; i++){ arr[i] = traverseNode; temp = traverseNode.next; traverseNode.next = null; traverseNode = temp; } for(int i = length; i < k; i++){ arr[i] = null; } }else{ //k不能除尽length,前几个链表会比后面的长1 for(int i = 0; i < length % k; i++){ arr[i] = traverseNode; for(int j = 0 ; j < length / k ; j++){ //多移了一个 traverseNode = traverseNode.next; } temp = traverseNode.next; traverseNode.next = null; traverseNode = temp; } for(int i = length % k; i < k; i++){ arr[i] = traverseNode; for(int j = 0 ; j < length / k - 1; j++){ traverseNode = traverseNode.next; } temp = traverseNode.next; traverseNode.next = null; traverseNode = temp; } } return arr; } //返回链表的长度 public int length(ListNode root){ ListNode node = root; int length = 0; while(node != null){ length++; node = node.next; } return length; } }
十、奇偶链表
328、奇偶链表 (medium) 2021-01-13
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
常规思路。
class Solution { public ListNode oddEvenList(ListNode head) { if(head == null || head.next == null ){ return head; } ListNode evenNode = head; ListNode oddNode = head.next; ListNode oddNodeHead = oddNode; while(evenNode.next.next != null && oddNode.next.next != null){ evenNode.next = evenNode.next.next; oddNode.next = oddNode.next.next; evenNode = evenNode.next; oddNode = oddNode.next; } if(oddNode.next != null){ evenNode.next = evenNode.next.next; evenNode = evenNode.next; oddNode.next = null; } evenNode.next = oddNodeHead; return head; } }