/**
* 方法1:通过Hash辅助查找
*
* @param pHead1
* @param pHead2
* @return
*/
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode current1 = pHead1;
ListNode current2 = pHead2;
//使用HashMap,结点作为key,value作为值
HashMap<ListNode, Integer> map = new HashMap<ListNode, Integer>();
while (current1 != null) {
map.put(current1, null);
current1 = current1.next;
}
//遍历另一个链表,判断每个结点是否有包含相同的结点
while (current2 != null) {
if (map.containsKey(current2))
return current2;
current2 = current2.next;
}
return null;
}
/**
* 方法2:通过集合来辅助查找
*
* @param headA
* @param headB
* @return
*/
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
if (headA==null||headB==null){
return null;
}
//利用set集合遍历headA保存结点
Set<ListNode> set = new HashSet<>();
ListNode cur=headA;
while (cur!=null){
set.add(cur);
cur=cur.next;
}
cur=headB;
//遍历headB,判断是否有公共子节点
while (cur!=null) {
if (set.contains(cur)){
return cur;
}
cur=cur.next;
}
return null;
}
/**
* 方法3:通过栈
* 思路:因为栈是先进后出,但我们要拿到的是第一个公共子节点,所以要等两个栈弹出的元素不相同时,返回结果
*/
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
if (headA==null||headB==null){
return null;
}
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 pNode= null;
//当长度都大于0时,继续判断
while (stackA.size()>0&&stackB.size()>0){
//peek()函数返回栈顶的元素,但不弹出该栈顶元素。
//pop()函数返回栈顶的元素,并且将该栈顶元素出栈。
//如果弹出的元素相同,则为公共子节点,但因为栈是先进后出,所以要找到最后一个,即为第一个公共子节点
//为什么这样写?因为链表的next只会指向一个元素,所以只会出现两个链表走着走着合并成一条链
if (stackA.peek()==stackB.peek()){
pNode=stackA.pop();
stackB.pop();
}else {
break;
}
}
return pNode;
}
但是上述方法都要额外去创建栈或者集合,空间复杂度为O(N)
先看下面的链表A和B
A:0-1-2-3-4-5
B:a-b-4-5
如果分别拼接成AB和BA会怎么样呢?
AB: 0-1-2-3-4-5-a-b-4-5
BA: a-b-4-5-0-1-2-3-4-5
我们发现拼接后从最后的4开始,两人链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式来寻找交点。这么做的道理是什么呢?
我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交点为中心,可以将两人链表分别分为left a和right a,left b和right b这样四个部分,并且right a和right b是一样的,这时候我们拼接AB和BA就是这样的结构:
right a和right b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的点了?
这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每人链表访问完了之后,调整到下链表的表头继续遍历就行了
/**
* 方法4:通过字符串拼接
*/
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;
//如果序列不存在交集的时候陷入死循环,例如 list1是1 2 3,list2是4 5,
// 这样的话遍历到结尾的时候p1和p2都为null,就会跳出循环
if (p1!=p2){
//一个链表访问完了跳到另一个链表继续访问,就达成了拼接的效果
if (p1==null){
p1=pHead2;
}
if (p2==null){
p2=pHead1;
}
}
}
return p1;
}
使用差和双指针来解决问题的方法。假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1| 就是两个的差值。第二轮遍历,长的先走|L2-L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了
/**
* 方法5:通过差值来实现
*
* @param pHead1
* @param pHead2
* @return
*/
public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode p1 = pHead1;
ListNode p2 = pHead2;
int len1 = 0;
int len2 = 0;
//获取长度
while (p1 != null) {
p1 = p1.next;
len1++;
}
while (p2 != null) {
p2 = p2.next;
len2++;
}
ListNode fast = pHead1;
ListNode slow = pHead2;
//获取差值
int cha = len1 > len2 ? len1 - len2 : len2 - len1;
//使长的一方先走掉差值步,只是走掉差值,链表不会为空,无需判
while (len1 > len2 && cha > 0) {
fast = fast.next;
cha--;
}
while (len1 < len2 && cha > 0) {
slow = slow.next;
cha--;
}
//同时遍历到结束
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
/** 方法1.全部压栈
思路:栈先进后出,所以就将链表中的元素倒转了,然后再一边出栈、一边遍历链表进行比较,一旦有不相同的就直接返回false
* @param head
* @return boolean
*/
public static boolean isPalindrome(ListNode head) {
if (head == null)
return true;
Stack<Integer> stack=new Stack<Integer>();
ListNode cur=head;
//将链表结点全部压入栈
while(cur!=null){
stack.push(cur.val);
cur=cur.next;
}
//一边出栈,一边比较
while(head!=null){
if(stack.pop()!=head.val) {
return false;
}
head = head.next;
}
return true;
}
/**
* 方法2:只将一半的数据出栈
* 思路:只要前一半和倒着的后一半相同,就代表是回文链表,只需要在压栈的时候顺带计算链表长度,然后长度减半比较即可
*
* @param head
* @return
*/
public static boolean isPalindromeByHalfStack(ListNode head) {
if (head == null)
return true;
Stack<Integer> stack=new Stack<Integer>();
ListNode cur=head;
//将链表结点全部压入栈
//计算链表的长度
int len=0;
while(cur!=null){
stack.push(cur.val);
cur=cur.next;
len++;
}
//len长度除2
len>>=1;
//一边出栈,一边比较
while(len-->0){
if(stack.pop()!=head.val) {
return false;
}
head = head.next;
}
return true;
}
/**
* 方法3:通过双指针+链表反转的方式来判断
*
* @param head
* @return
*/
public static boolean isPalindromeByTwoPoints(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
ListNode pre = head, prepre = null;
//pre是反转存储了前半段倒置的链表
while (fast != null && fast.next != null) {
//存储slow遍历的每个结点,直到中间
pre = slow;
slow = slow.next;
fast = fast.next.next;
//指向前一个结点
pre.next = prepre;
//存储前一个结点
prepre = pre;
}
//如果奇数情况,需要跳过中间元素
if (fast != null) {
slow = slow.next;
}
//遍历判断值是否相同
while (pre != null && slow != null) {
if (pre.val != slow.val) {
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}
链表合并的思路:
1.新建一个链表,然后分别遍历两个链表,每次都选最小的结点接倒新链表上,直到结束
2.将一个链表结点拆下来,逐个合并倒另外一个对应位置上去
/**
* 方法1:新建一个链表,然后分别遍历两个链表,每次都选最小的结点接倒新链表上,直到结束
*
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//创建一个新链表
ListNode newHead = new ListNode(-1);
//记录新链表的头结点,res.next为两个链表合并的头结点
ListNode res = newHead;
//遍历两个个链表,当两个链表都不为空时,有三种情况
while (list1 != null && list2 != null) {
//大于
if (list1.val > list2.val) {
newHead.next = list2;
list2 = list2.next;
//小于
} else if (list1.val < list2.val) {
newHead.next = list1;
list1 = list1.next;
//等于时,两个链表的结点都拼接到新链表
} else if (list1.val == list2.val) {
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
newHead.next = list1;
list1 = list1.next;
}
//保持新链表遍历
newHead = newHead.next;
}
//判断某个链表还没遍历结束的情况,将该链表所有结点拼接到新链表
while (list1 != null) {
newHead.next = list1;
list1 = list1.next;
newHead = newHead.next;
}
while (list2 != null) {
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
}
return res.next;
}
/**
* 方法2:优化1:方法1的大while有三种情况,可以合并成两个,当存在相同元素时,第一次出现使用
* if(list1.val<=list2.val)处理,第二次使用else处理
* 因为,当A和B两个链表结点相同时,新链表先接上A的结点,A链表结点向后走了,如果A链表结点元素大于B链表结点,就会走else,就节省了判断
*优化2:当其中一个链表结束后,新链表可以直接拼接还有元素的链表,无需遍历
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoListsMoreSimple(ListNode list1, ListNode list2) {
ListNode newHead=new ListNode(-1);
ListNode res=newHead;
while(list1!=null&&list2!=null){
if(list1.val<=list2.val){
newHead.next=list1;
list1=list1.next;
}else{
newHead.next=list2;
list2=list2.next;
}
newHead=newHead.next;
}
newHead.next=list1==null?list2:list1;
return res.next;
}
/**
* 合并K个链表
*
* @param lists
* @return
*/
public ListNode mergeKLists(ListNode[] lists) {
//只要合并两个写的出来,合并k个直接薄纱
ListNode res=null;
for(ListNode list:lists){
res=mergeTwoLists(res,list);
}
return res;
}
这里需要留意题目中是否有开闭区间的情况,例如如果是从a到b,那就是闭区间[a,b]。还有的会说一人开区间(a,b),此时是不包括a和b两个元素,只需要处理a和b之间的元素就可以了。比较特殊的是进行分段处理的时候,例如K个一组处理,此时会用到左闭右开区间,也就是这样子[a,b),此时需要处理a,但是不用处理b,b是在下一个区间处理的。此类题目要非常小心左右边界的问题
/**合并两个链表,[a,b]
* @param list1
* @param a
* @param b
* @param list2
* @return {@link ListNode}
*/
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode nodeA=list1;
ListNode nodeB=list1;
for(int i=0;i<=b;i++){
//找到a-1结点位置
if(i<a-1){
nodeA=nodeA.next;
}
//找到b+1结点位置
nodeB=nodeB.next;
}
//拼接list2
nodeA.next=list2;
//遍历list2
while(list2.next!=null){
list2=list2.next;
}
//拼接后续
list2.next=nodeB;
return list1;
}
用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步fast 一次走两步。那么当 fast 到达链表的未尾时,slow必然位于中间。
这里还有个问题,就是偶数的时候该返回什么,例如上面示例2返回的是4,而3貌似也可以,那该使用哪个呢?如果我们使用标准的快慢指针就是后面的4,而在很多数组问题中会是前面的3,想一想为什么会这样。
//利用快慢指针
public ListNode middleNode(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
//当偶数时,遍历到倒数第二个时,仍满足循环条件,只不过fast指针最后会为null
//但是中间指针会走到中间结点两个中的后一个
//而数组因为会导致数组越界异常,所以slow会是中间两个元素的前一个
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
/** 快慢指针,fast指针与slow指针保持k的距离,当fast走完,slow指向的就是倒数第k个
* @param head
* @param k
* @return {@link ListNode}
*/
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast=head;
ListNode slow=head;
//遍历fast指针到k+1结点
while(fast!=null&&k>0){
fast=fast.next;
k--;
}
//这时候slow指针没动,fast和slow相差k个结点
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
观察链表调整前后的结构,我们可以发现从旋转位置开始,链表被分成了两条,例如上面的(1,2,3)和(4.5,这里我们可以参考上一题的倒数K的思路,找到这个位置,然后将两个链表调整一下重新接起来就行了。
第一种是将整个链表反转变成(5,4,3,2,1},然后再将前K和N-K两个部分分别反转,也就是分别变成了14.5)和f1,2,3],这样就轻松解决了。通过链表反转
第二种思路就是先用双指针策略找到倒数K的位置,也就是1,2,3和4.5两个序列,之后再将两人链表拼接成(4.5,1,2,.3}就行了。
具体思路:
因为k有可能大于链表长度,所以首先获取一下链表长度len,如果然后k=k % len,如果k ==0,则不用旋转,直接返回头结点。否则:
1.快指针先走k步
2.慢指针和快指针一起走
3.快指针走到链表尾部时,慢指针所在位置刚好是要断开的地方。把快指针指向的节点连到原链表头部,慢指针指向的节点断开和下一节点的联系。
4.返回结束时慢指针指向节点的下一节点。
/**依旧使用快慢指针,通过保持fast指针和slow指针k个结点距离,使fast指针移到末尾,让slow移动到倒数第k个结点位置
* @param head 头结点
* @param k 移动k个位置
* @return {@link ListNode}
*/
public ListNode rotateRight(ListNode head, int k) {
if(head==null||k==0){
return head;
}
ListNode fast=head;
ListNode slow=head;
ListNode temp=head;
int len=0;
//统计链表的元素个数
while(head!=null){
head=head.next;
len++;
}
if(k%len==0){
return temp;
}
//从这里开始fast从头结点向后走
//这里使用取模,是为了防止k大于len的情况
//例如,如果len=5,那么k=2和7,效果是一样的
while((k%len)>0){
k--;
fast=fast.next;
}
//快指针走了k步,然后快慢指针一起向后走
//当fast到尾结点时,slow刚好在倒数第k个位置上
while(fast.next!=null){
fast=fast.next;
slow=slow.next;
}
//得到k个要移动的后一段
ListNode res=slow.next;
//将链表中某一结点的地址值赋值为空,会将原先的链表拆成两份
slow.next=null;
//因为fast已经在尾巴了,将前一段拼接,这时候的temp虽然是头结点,但是它只是原先链表的前一段
fast.next=temp;
return res;
}
事实上,不用这么麻烦,直接暴力点,假如我们要找倒数第K个,那就是要找正数第Len-k+1个。因此可以先遍历遍,计算出LEN,然后直接通过计算得到需要走的步数,只会从头开始遍历,到第Len-k+1即可。当然也要通过取模来计算防止K越界。
删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next。这时候cur就脱离链表了,cur节点会在某个时刻被gc回收掉。
对于删除,注意首元素的处理方式与后面的不一样。为此,可以先创建一个虚拟节点dummyHead,使其指向head,也就是dummyHead.next=head,这样就不用单独外理首节点了完整的步骤是:
1.我们创建一个虚拟链表头dummyHead,使其next指向head
2.开始循环链表寻找目标元素,注意这里是通过curnext.val来判断的
3.如果找到目标元素,就使用cur.next = cur.next.next;来删除.
4.注意最后返回的时候要用dummyHead.next,而不是dummyHead
/**关键:整一个dummyHead指向head,使头结点可以一起处理
* @param head
* @param val
* @return {@link ListNode}
*/
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return head;
}
ListNode dummyHead=new ListNode(-1);
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;
}
/**方法1.倒数第n个结点=len+1-n
* @param head
* @param n
* @return {@link ListNode}
*/
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null){
return null;
}
ListNode dummyHead=new ListNode(0,head);
int len=0;
while(head!=null){
head=head.next;
len++;
}
ListNode cur=dummyHead;
for(int i=1;i<len-n+1;i++){
cur=cur.next;
}
cur.next=cur.next.next;
return dummyHead.next;
}
//通过快慢指针
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null){
return null;
}
ListNode dummyHead=new ListNode(0,head);
ListNode fast=head;
//这里慢指针是指向虚拟结点,因为可能会出现删除第一个结点
//如果指向head,会导致slow.next空指针
ListNode slow=dummyHead;
while(fast!=null&&n>0){
fast=fast.next;
n--;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
slow.next=slow.next.next;
return dummyHead.next;
}
/**通过栈反向遍历得到结点,不过空间复杂度O(N),还要考虑链表和栈的关系
* @param head
* @param n
* @return {@link ListNode}
*/
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null){
return null;
}
ListNode dummyHead=new ListNode(0,head);
Stack<ListNode> stack=new Stack<>();
//可能删除第一个结点,所以拿虚拟结点开始压栈
ListNode cur=dummyHead;
while(cur!=null){
stack.push(cur);
cur=cur.next;
}
//弹出倒数第k个
for(int i=0;i<n;i++){
stack.pop();
}
//这里拿到的就说第k个结点的前一个
ListNode pre=stack.peek();
pre.next=pre.next.next;
return dummyHead.next;
}
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此只需要对链表进行次遍历,就可以删除重复的元素。
思路:我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与curnext 对应的元素相同,那么我们就将curnext 从链表中移除,否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 curnext。当遍历完整链表之后,我们返回链表的头节点即可。
另外要注意的是 当我们遍历到链表的最后一个节点时,cur next 为空节点,此时要加以判断
/**
* @param head
* @return {@link ListNode}
*/
public ListNode deleteDuplicates(ListNode head) {
if(head==null){
return head;
}
ListNode dummy=new ListNode(0,head);
ListNode cur=dummy.next;
while(cur.next!=null){
if(cur.val==cur.next.val){
cur.next=cur.next.next;
}else{
cur=cur.next;
}
}
return dummy.next;
}
当一个都不要时,链表只要直接对curnext 以及 cur.next.next两个node进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。
/** cur.next和cur.next.next比较,并判断它们的值是否相同,如果相同的话,记录值
并且内部进行遍历判断。如果cur.next不为空且值相同,则cur.next=cur.next.next;
* @param head
* @return {@link ListNode}
*/
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy=new ListNode(0,head);
ListNode cur=dummy;
while(cur.next!=null&&cur.next.next!=null){
if(cur.next.val==cur.next.next.val){
int temp=cur.next.val;
//判断重复元素,当值不同时跳出循环
while(cur.next!=null&&cur.next.val==temp){
cur.next=cur.next.next;
}
}else{
cur=cur.next;
}
}
return dummy.next;
}