单链表刷题小结
- 注意头节点的使用
- java中没有直接访问内存地址的用法,每个节点都是引用,可以 .val 或者 .next进行访问
- 如果是需要返回结果,最好是返回一个新建的链表,如果返回旧的结果那么在head长度为1时是无论怎样都消不掉的,我遇到这样的情况比如说,p2.next = p1; p2.next = p2.next.next; return head; 那么我这样操作目的是想通过p2来删除链表中的某个节点,大但是当head长度为1时,p2的改变是改变不了head的,就算p2=null,head仍然长度为1
- 学会了优先级队列这个概念
- 还有一些比较新奇的思路,比如补全链表,双指针,都需要常常回顾
21. 合并两个有序链表
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1), p = dummy;
ListNode p1 = l1, p2 = l2;
while (p1!=null && p2!=null){
if (p1.val<p2.val){
p.next = p1;
p1 = p1.next;
}
else{
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
if (p1!=null){
p.next = p1;
}
if (p2!=null){
p.next = p2;
}
return dummy.next;
}
}
86. 分隔链表
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode p0 = head;
ListNode dummy1 = new ListNode(-1), p1 = dummy1;
ListNode dummy2 = new ListNode(-1), p2 = dummy2;
while(p0 != null){
if(p0.val < x){
p1.next = p0;
p1 = p1.next;
p0 = p0.next;
p1.next = null;
}
else{
p2.next = p0;
p2 = p2.next;
p0 = p0.next;
p2.next = null;
}
}
p1.next = dummy2.next;
return dummy1.next;
}
}
23. 合并 K 个升序链表
- 这个题直接看的时候,思路就是给k个链表都安排一个指针,取三个指针中最小的值给到新的列表,需要注意读取链表的数量,或者将链表指针存到数组里边;要不就是递归?
- 但是难点是在于求k个节点的最小节点???答案中是用优先级队列(二叉堆)数据结构。每次移动一个指针到下一个位置,堆的数据结构可以直接给出最小的值而不需要重新排列,原先的时间复杂度是M序列N总序列长度,现在的复杂度是O(NlogM)
- lists.length 求长度不需要加括号
- // 优先队列的用法
- PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length,(a,b)->(a.val-b.val));
- // 定义方式
- ListNode dummy = new ListNode(-1);
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0){
return null;
}
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length,(a,b)->(a.val-b.val)); // 如果是负数,a小
for (ListNode head : lists){
if(head!=null){
pq.add(head);
}
}
while(!pq.isEmpty()){
ListNode temp = pq.poll();
p.next = temp;
if(temp.next!=null){
pq.add(temp.next);
}
p = p.next;
p.next = null;
}
return dummy.next;
}
}
19. 删除链表的倒数第 N 个结点(被困住一晚上的结果返回问题)
- 考虑的方法是:在进行正向读取的时候,就将整个列表逆序,要么就是新读取一个节点就插入到前边,读取结束之后从头节点开始读取k个即可(反思:这种情况只能读到k节点的值,无法读到next)
- 如果是删除该节点,那么就是设置成双向节点,读到尾巴之后,返回来修改倒数第k个(很繁琐)
- 正确答案是两个人赛跑,其中一个人先放到k处,然后两人同时开始,在两人速度一致的情况下,一人到达
我的方法:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode p1 = head;
ListNode p2 = new ListNode(0, head);
ListNode p3 = p2;
// 安全,这里我的思路是只有走n-1步骤,才会正确达到第n个数,所以我的p1一直指向了安全区域
for (int i=0;i<(n-1);i++){
p1 = p1.next;
}
// 当p1的下一个是null,那说明p1到了结尾节点
while(p1.next!=null){
p1 = p1.next;
p2 = p2.next;
}
// 在此过程中 p1 和 p2 的长度一直为n+1,所以当p1到达结尾,p2在倒数n+1上停着,这时将p2后边的数字删除掉即可
// 在删除的时候尤其注意,不能直接返回head,否则当只有一个值的时候,永远无法被删除,所以需要重新开启一个链表即p2,最后的返回结果也是p2
p2.next = (p2.next).next;
return p3.next;
}
}
答案结果
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode first = head;
ListNode second = dummy;
// first可以指向n个数的后边null,first和second之间的距离是n+2,所以需要first到达null,second才到达n+1
for (int i = 0; i < n; ++i) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
ListNode ans = dummy.next;
return ans;
}
}
876. 链表的中间结点
class Solution {
public ListNode middleNode(ListNode head) {
ListNode p1 = head;
ListNode p2 = head;
while(p2.next!=null){
p1 = p1.next;
p2 = p2.next;
if(p2.next!=null){
p2 = p2.next;
}
}
return p1;
}
}
142. 环形链表 II (一遍AC)
- 这个思路之前有学过,所以简单推导一下就得到了:简单来说就是环形操场跑步的问题,同时出发,一个速度为v另一个速度为2v,两人相遇的时候,说明快的人比慢的人多走了一圈,相遇的点也正好是快的人一半路程的位置。
- 这对比环形链表问题也是同理的,即开始节点到相遇节点距离 = 相遇节点环形到下次该节点 = 环形的长度
- 有了以上推断,那么在开始位置设置指针 p1, 相遇位置设置指针p2, 两个指针相遇的地方就是这个豁口啦
- 引用labuladong的图解
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode p1 = head;
ListNode p2 = head;
if (head==null) return null;
while(true){
if(p2.next == null){
return null;
}
else if(p2.next.next == null){
return null;
}
else{
p1 = p1.next;
p2 = p2.next.next;
}
if (p1 == p2){
break;
}
}
ListNode p3 = head;
while(p2!=p3){
p2 = p2.next;
p3 = p3.next;
}
return p2;
}
}
160. 相交链表
- 直接想法是,遍历一个链表存为hash,遍历另一个链表检查是否有重复,但是这样的时间复杂度为 O(m+n).,空间复杂度为O(n).
- 答案的思路是补齐,因为现在的问题是两个链表不同步,如果在每个链表的前边加上与另一链表相同的长度(里边的值无所谓),那么两个链表在评估的时候就是对齐的,这里不用担心前边加的链表会影响结果,因为前边的链表是在不对齐的状态下,如果真的有相同位置的值相同,那么说明两条链表的长度是一样的,因此不会对结果产生影响。
- leecode评论区:对的人错过了还是会相遇, 错的人相遇了也是NULL
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA;
ListNode p2 = headB;
int flag1 = 0;
int flag2 = 0;
if (headA == null || headB == null) return null;
while(p1 != null || flag1==0){
if (flag1==0 && p1 == null){
p1 = headB;
flag1 = 1;
}
if(flag2==0 && p2 == null){
p2 = headA;
flag2 = 1;
}
if (p1 == p2) return p1;
p1 = p1.next;
p2 = p2.next;
}
return null;
}
}
-
官方题解
-
最开始不太理解,为什么没有处理 第一次为null和第二次为null的情况,
- 第一次:如果在过程中二者同时为null,那么其实不需要再遍历拼接段之后的链表,因为两个链表的长度一致,遍历过一次没有重复那么返回null即可
- 第二次:如果前期都没有匹配,那么最后Pa = null Pb= null一定是同时到达的,满足截至条件
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
- 看到题解中还有一种很巧妙的思路就是把某一链表的首尾连接起来,变成上述寻找豁口的问题,但是这样会改变链表原有的结构