该篇文章主要梳理相关问题思路,强化巩固新方法,提出自己的理解感悟,形成自己的模板套路,解决非递归线性链表的绝大多数题目。
与此同时本篇文章也涉及部分集合相关内容,需要掌握集合所在的包名以及集合使用方法。
1.寻找链表公共节点四种处理方式
四种方式中前两种方式的使用主要是强化集合的使用方法;第三种方式开阔视野,属于新思路,需要强化理解记忆;至于第四种快慢指针,太简单了无需复习。
1.1使用哈希
第一种方法的思想非常简单,就是利用一个数据结构存储节点信息,一边遍历一边检索节点信息是否在给定的数据结构中存在。
为什么必须是哈希?(检索速度块!当然你可以写一个新的链表存储节点信息,再自己写个新的函数进行检索。重要的是速度,没有什么比HashSet和HashMap检索速度更快的了)
一个小tips:使用HashMap存储哈,key存ListNode,val随便存个数据结构就行,占位符罢了,上代码!
(What?HashSet实现代码也想要?我的建议:勤能补拙!)
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
//创建map存储链表1中元素,第二个位置为占位符
HashMap<ListNode, Integer> map = new HashMap<>();
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while(p1 != null){
map.put(p1, null);
p1 = p1.next;
}
while(p2 != null){
if(map.containsKey(p2)){
return p2;
}
p2 = p2.next;
}
return null;
}
1.2使用栈
第二种方法的思想我称其“逆过程”,它和物理中的一个场景非常相似,一个匀加速直线运动可以看成是一个匀减速直线运动的逆运动。那么存在公共节点的链表,逆向来看,它的尾部必然相同!什么数据结构是从尾部来观察数据的?答:栈!
需要注意的是,有着公共节点的链表其尾部相同,找到第一个不相同的元素cur,它的下一位就是公共节点!上代码!!
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
Stack<ListNode> stack1 = new Stack<>();
Stack<ListNode> stack2 = new Stack<>();
ListNode p1 = headA;
ListNode p2 = headB;
//链表AB压栈
while(p1 != null){
stack1.push(p1);
p1 = p1.next;
}
while(p2 != null){
stack2.push(p2);
p2 = p2.next;
}
//出栈比较
ListNode pNode = null;
while(!stack1.empty() && !stack2.empty()){
if(stack1.peek() == stack2.peek()){
pNode = stack1.pop();
stack2.pop();
} else {
break;
}
}
return pNode;
}
1.3拼接两个字符串
第三种方法的思想非常神奇!重点重点重点!
有公共节点的两个链表,再拼接之后,其尾部元素必定相同,第一个相同的尾部元素就是公共节点!(无论AB排列,还是BA排列)
很显然,我们可以构造两个新链表,第一个链表AB,第二个链表BA,然后设置两个指针,逐位比较节点是否相同即可!
可是这意味着空间浪费啊!我们不妨就用链表A和链表B,指针pANode用于遍历AB排列,当链表A遍历完了,接着就去遍历列表B(指针pBNode同理)大大缩减空间!直接上代码!(咳咳,这一段嘛,来自鱼骨头)
当心Leetcode提供的示例中,有两个链表没有公共节点存在的特殊情况(会进入死循环)!特殊情况特殊记忆!加上if(p1 != p2),比如链表1 2 3 和链表4 5 6,那么123456和456123在最后的时候p1 = 6,p2 = 3,紧接着p1 = 4,p2 = 1,陷入死循环!!
public static ListNode findFirstCommonNodeByCombine(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;
//防止出现p1 = null, p2 = null重置查询链表出现死循环
if(p1 != p2){
//某个链表访问完,访问另一个链表
if(p1 == null){
p1 = pHead2;
}
if(p2 == null){
p2 = pHead1;
}
}
}
return p1;
}
1.4差和双指针
第四种方法的思想就是分别遍历一遍链表A,链表B,统计它们的长度lenA,lenB
使用两个指针,长链表的指针先移动|lenA-lenB|,然后两个指针同时移动,指向元素相同就是公共节点。太简单了,代码不上了...
2.回文序列
回文,什么意思,说白了逆向看这个序列和顺序看这个序列一样,刚刚上面说了,什么数据结构用到逆向?栈!(幻听),ok直接上代码!
public static boolean isPalindromeByAllStack(ListNode head) {
Stack<ListNode> stack = new Stack<>();
ListNode p = head;
//压栈
while(p != null){
stack.push(p);
p = p.next;
}
p = head;
//出栈,逆向顺向比较
while(!stack.empty()){
if(stack.peek().val != p.val){
return false;
}
stack.pop();
p = p.next;
}
return true;
}
3.合并有序链表
这类题目变式很多,归根结底,在于如何合并两个有序链表。
搞两个指针,比较元素大小,插入新的链表,其中一个链表遍历完了,把另一个链表的剩余元素贴过去。上代码!
public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
p.next = l1;
l1 = l1.next;
}else{
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
ListNode res = l1 == null ? l2 : l1;
p.next = res;
return dummy.next;
}
变式训练1
如何合并K个有序链表?
简单点,搞一个空链表,先把空链表和第一个链表合并,合并之后的链表和第二个链表合并,依次...K个有序链表合并完成!(还是那句话,韩信带净化,勤能补拙!)
变式训练2
两个链表list1和list2,将list1中下标a到b的节点删除,把list2接在被删除的地方
关键:找到下标a节点的前驱,下标b节点的后继
如何变换出更多题目??
出题点1,开闭区间[a,b] (a,b) (a,b] [a,b)
出题点2,若[a,b]升序,list2升序,将[a,b]和list2合并按照升序排列
4.双指针
方法的本质在于设置双指针,两个指针移动步幅不同,找到中间/倒数第k位置的节点
令人振奋的是,找到第K位置节点有模板!我们只需要关注边缘条件,套用模板,就可以解决问题!
4.1寻找中间节点
思想很简单,那么为什么拎出来记一记,讲一讲?
对于一个链表 1 2 3 4 5 6 中间节点,有的题目说返回3,有的题目说返回4,那么你会精确控制嘛?这就是我要说的点!上代码!
当慢指针先移动,输出结果是4,当慢指针后移动,输出结果是3。这个现象如何记忆?
笨鸟先飞,跑得远!
public static ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
//注意这里先慢指针移动还是先快指针移动
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
4.2寻找倒数第K个元素
注意哈,这个是模板,直接套用模板,修改边界条件,大部分问题迎刃而解,不信接着看4.3旋转链表!
直接上模板代码!反复记忆,特别是第一个while循环条件
public static ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head, fast = head;
while(fast != null && k > 0){
fast = fast.next;
k--;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
4.3旋转链表
旋转链表,很简单,我们先找到倒数第k节点的前驱,然后修改链表头,链表尾,拼接链表就行!
问题来了,如何寻找倒数第k节点的前驱?答:模板
public static ListNode rotateRight(ListNode head, int k) {
//规范化到底旋转多少个元素
int count = 0;
ListNode p = head;
while(p != null){
p = p.next;
count++;
}
k = k % count;
//先找到倒数第k个节点
ListNode slow = head, fast = head;
ListNode oldTail = null, newTail = null;
while(fast != null && k >0){
fast = fast.next;
k--;
}
while(fast != null){
if(fast.next == null){
oldTail = fast; // 原来链表的表尾
newTail = slow; // 新链表的表尾,第k-1位置
}
fast = fast.next;
slow = slow.next;
}
//再完成拼接
oldTail.next = head;
newTail.next = null;
return slow;
}
5.删除元素
5.1删除特定节点
删除特定节点和删除第n个节点的关键在于找到前驱节点。前者可以使用cur.next == val来判断,cur是否是前驱节点,后者使用模板找到前驱节点cur(修改边界条件)这里不再花多余篇幅赘述。
5.2删除重复元素
题型1:重复元素仅保留一个
我们使用cur记录当前遍历的节点,用cur.val==cur.next.val来判断,后面节点是否和前面节点重复,重复则删除,直接上代码!
需要注意的,while条件,我的理解:因为使用cur.val==cur.next.val这一判断条件,因此while循环的判断条件是cur.next != null
public static ListNode deleteDuplicate(ListNode head) {
if(head == null){
return null;
}
ListNode cur = head;
while(cur.next != null){
if(cur.val == cur.next.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
题型2:重复元素全删除
因为重复元素全部删除,存在链表1 1 2 3删除重复元素,变成2 3,修改链表表头,因此使用一个dummy虚拟节点记录表头节点
我们设定需要比较的节点是cur节点之后的两个节点,如果这两个节点相同(cur.next.val == cur.next.next.val),则删除;如果两个节点不相同,继续往后遍历查找。
存在cur后面连着好几个元素都相同的情况,怎么办?
使用变量x记录第一次重复时,元素值,后面节点依次比较x,如果和x相同则删除,删除后cur.next指向节点如图中的红色线所示,当后继再次相同继续删除,直到全部删除干净为止。这个过程可以使用while迭代删除,当元素值不等表示删除干净,退出while。
如何判断while边界条件?
同上一题型所述,我的理解:因为使用cur.next.val == cur.next.next.val这一判断条件
厘清思路之后,直接上代码!
public static ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode cur = dummy;
int x;
while(cur.next != null && cur.next.next !=null){
//cur后面的两个元素相同
if(cur.next.val == cur.next.next.val){
//记录元素值
x = cur.next.val;
//删除后面可能的元素
while(cur.next != null){
if(cur.next.val == x){
cur.next = cur.next.next;
}else{
break;
}
}
}else{
cur = cur.next;
}
}
return dummy.next;
}
Ok,《算法通关村第一关——链表白银挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)
再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y...emmm还没想好,ok,拜拜,第一关第三幕见!