《算法通关村第一关——链表经典问题笔记》
链表的常见的增删改查问题---注意指针位置。
1.两个链表第一个公共子节点
查找问题
分析问题:相交问题--》如何相交?
同时到一个地方?分为两个问题:1.链表相同长度2.不同长度
如果相同则直接比较,不同长度则要转换成一样的长度。------》拼接
解决思路:
1.常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、递归、迭代、分治、贪心、回溯和动态规划等等。
可以用集合,hashset------>唯一不重复,快速查找是否存在
public ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) { Set<ListNode> set = new HashSet<>(); while (headA != null) { set.add(headA); headA = headA.next; } while (headB != null) { if (set.contains(headB)) return headB; headB = headB.next; } return null; }
2.可以使用栈---》因为相交后面相同元素,从后往前比较
import java.util.Stack; public ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) { Stack<ListNode> stackA=new Stack(); Stack<ListNode> stackB=new Stack(); while(headA!=null){ stackA.push(headA); headA=headA.next; } while(headB!=null){ stackB.push(headB); headB=headB.next; } ListNode preNode=null; while(stackB.size()>0 && stackA.size()>0){ //比较栈顶 if(stackA.peek()==stackB.peek()){ preNode=stackA.pop(); stackB.pop(); }else{ break; } } return preNode; }
3.变成相同的长度----》拼接法ab与ba
public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) { if(pHead1==null || pHead2==null){ return null; } ListNode p1=pHead1; ListNode p2=pHead2; while(p1!=p2){ p1=p1.next; p2=p2.next; if(p1!=p2){ //一个链表访问完了就跳到另外一个链表继续访问 if(p1==null){ p1=pHead2; } if(p2==null){ p2=pHead1; } } } return p1; }
4.双指针法:---》快慢指针----》长度变为一致
public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) { if(pHead1==null || pHead2==null){ return null; } ListNode current1=pHead1; ListNode current2=pHead2; int l1=0,l2=0; //分别统计两个链表的长度 while(current1!=null){ current1=current1.next; l1++; } while(current2!=null){ current2=current2.next; l2++; } current1=pHead1; current2=pHead2; int sub=l1>l2?l1-l2:l2-l1; //长的先走sub步 if(l1>l2){ int a=0; while(a<sub){ current1=current1.next; a++; } } if(l1<l2){ int a=0; while(a<sub){ current2=current2.next; a++; } } //同时遍历两个链表 while(current2!=current1){ current2=current2.next; current1=current1.next; } return current1; }
2.判断链表是否为回文序列
什么是回文---》就是前后读都相同----》对称
查找问题:
前后查找
解决思路:
1.将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较两者元素值,只要有一个不相等,那就不是。
栈是倒序的好办法--》可倒序解决问题
在遍历的时候使用递归来反转一半链表可以吗?
可以,因为对称的
public boolean isPalindrome(ListNode head) { ListNode temp = head; Stack<Integer> stack = new Stack(); //把链表节点的值存放到栈中 while (temp != null) { stack.push(temp.val); temp = temp.next; } //之后一边出栈,一遍比较 while (head != null) { if (head.val != stack.pop()) { return false; } head = head.next; } return true; }
3.合并有序链表
查找,增添问题
查找比较---------》注意指针问题
public ListNode mergeTwoLists(ListNode list1, ListNode list2) { ListNode prehead = new ListNode(-1); ListNode prev = prehead; while (list1 != null && list2 != null) { if (list1.val <= list2.val) { prev.next = list1; list1 = list1.next; } else { prev.next = list2; list2 = list2.next; } prev = prev.next; } // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方 prev.next = list1 == null ? list2 : list1; return prehead.next; }
4.寻找中间结点
查找问题
1.双遍历去中间
2.双指针法
这个问题用经典的快慢指针可以轻松搞定,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
偶数的时候该返回哪个?想一想为什么会这样?
原因是先判断后做,因此在后面的时候会多走一步。
为了避免这个原因-----------》fast.next != null
public ListNode middleNode(ListNode head) { ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; }
寻找倒数第K个元素
一样的,可倒序k(栈)
双指针---》
public ListNode getKthFromEnd(ListNode head, int k) { ListNode fast = head; ListNode slow = head; while (fast != null && k > 0) { fast = fast.next; k--; } while (fast != null) { fast = fast.next; slow = slow.next; } return slow; }
旋转链表
关注点:可能旋转的时候大于链表长度
因为k有可能大于链表长度,所以首先获取一下链表长度len,如果然后k=k % len,如果k == 0,则不用旋转,直接返回头结点。否则: 1.快指针先走k步。 2.慢指针和快指针一起走。 3.快指针走到链表尾部时,慢指针所在位置刚好是要断开的地方。把快指针指向的节点连到原链表头部,慢指针指向的节点断开和下一节点的联系。 4.返回结束时慢指针指向节点的下一节点。
代码可以由反转链表得出。
5.删除特定结点
遇到的问题---》并不是判断节点相同,而是里面的元素相同
虚拟节点可以解决头节点问题
public ListNode removeElements(ListNode head, int val) { ListNode dummyHead = new ListNode(0); //注意 dummyHead.next = head; ListNode cur = dummyHead; while (cur.next != null) { if (cur.next.val == val) { cur.next = cur.next.next; } else { cur = cur.next; } } return dummyHead.next; }
双指针也可以解决------》记得不是慢指针是真节点,快指针是探寻节点
注意:跳出循环后最后面的慢指针的下一个指向null。
这里最好不用,因为要改变原来的数组,所以直接用单节点改变原来的就行了
删除倒数第n个结点
查找第倒数n+1个节点
public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next=head; int length = getLength(head); ListNode cur = dummy; for (int i = 1; i < length - n + 1; ++i) { cur = cur.next; } cur.next = cur.next.next; ListNode ans = dummy.next; return ans; } public int getLength(ListNode head) { int length = 0; while (head != null) { ++length; head = head.next; } return length; }
public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next=head; ListNode first = head; ListNode second = dummy; 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; }