链表
方法论
- 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度
- 对比面试,时间复杂度依然放在第一位,但是需要找到空间最省的方法
- 重要技巧
- 额外数据结构记录(哈希表等)
- 快慢指针
单链表、双链表基本结构
value next和value next last
反转单向链表
【如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)】
栈(若没有额外空间复杂度要求)
public ListNode reverseList(ListNode head){
Stack<ListNode> stack=new Stack<>();
while(head!=null){
stack.push(head);
head=head.next;
}
if(stack.isEmpty()){
return null;
}
ListNode node=stack.pop();
ListNode dummy=node;
while(!stack.isEmpty()){
ListNode tempNode=stack.pop();
node.next=tempNode;
node=node.next;
}
//最后一个结点就是反转前的头结点,一定要让他的next
//等于空,否则会构成环
node.next=null;
return dummy;
}
递归
public ListNode reverseListNode(ListNode head){
if(head==null||head.next==null){
return head;
}
ListNode newHead=reverseListNode(head.next);
head.next.next=head;
head.next=null;
return newHead;
}
左神
把原链表的结点一个个摘掉,每次摘掉的链表都让他成为新的链表的头结点,然后更新新链表
public ListNode reverseListNode(ListNode head){
ListNode pre=null;
ListNode next=null;
while(head!=null){
next=head.next;
head.next=pre;
pre=head;
head=next;
}
return pre;
}
反转单向链表Ⅱ
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
穿针引线
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummyNode=new ListNode(-1);//一般操作
dummyNode.next=head;
ListNode pre=dummyNode;
for(int i=1;i<left;i++){
pre=pre.next;
}
ListNode cur=pre.next;
ListNode next=null;
for(int i=0;i<right-left;i++){
next=cur.next;
cur.next=next.next;
next.next=pre.next;
pre.next=next;
}
return dummyNode.next;
}
}
反转双向链表
【如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)】
public DoubleNode reverseDoubleNode(DoubleNode head){
DoubleNode pre=null;
DoubleNode next=null;
while(head!=null){
next=head.next;
head.next=pre;
head.last=next;
pre=head;
head=next;
}
return pre;
}
打印两个有序链表的公共部分
要求:如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度为O(1)
谁小谁移动,相等就一起移动并打印(保存),一个越界了就停
public static void printCommonPart(Node head1, Node head2) {
System.out.print("Common Part: ");
while (head1 != null && head2 != null) {
if (head1.value < head2.value) {
head1 = head1.next;
} else if (head1.value > head2.value) {
head2 = head2.next;
} else {
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
判断一个链表是否是回文链表
笔试:栈
全部进栈,出栈的时候就相当于是反着读,与正着读的时候比较即可
public boolean isPalidromeList(ListNode head){
Stack<ListNode> stack=new Stack<>();
ListNode cur=head;
while(cur!=null){
stack.push(cur);
cur=cur.next;
}
while(head!=null){
if(head.value!=stack.pop().value){
return false;
}
head=head.next;
}
return true;
}
优化
快慢指针
快指针一次走两步,慢指针一次走一步,快指针走完的时候满指针在中点
只需要存一半进栈
1.奇数时慢指针出现在中点,偶数时满指针出现在第一个
ListNode fast=head;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
2.奇数时出现在中点的下一个,偶数时出现在第二个
ListNode fast=head.next;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
public boolean isPalindrome(ListNode head){
if(head==null||head.next==null){
return true;
}
ListNode right=head.next;
ListNode cur=head;
while (cur.next != null && cur.next.next != null) {
right = right.next;
cur = cur.next.next;
}
Stack<Node> stack = new Stack<Node>();
while (right != null) {
stack.push(right);
right = right.next;
}
while (!stack.isEmpty()) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
如何做到额外空间复杂度为O(1)
笔试的做法
快慢指针+反转+对比+恢复
public boolean isPalindrome(ListNode head){
if(head==null||head.next==null){
return true;
}
ListNode fast=head;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
fast=slow.next;
slow.next=null;// mid.next -> null
ListNode next=null;
//反转
while(fast!=null){
next=fast.next;
fast.next=slow;
slow=fast;
fast=next;
}
next=slow;//save last node
fast=head;
boolean res=true;//后面还要恢复链表
while(slow!=null&&fast!=null){
if(slow.value!=fast.value){
res=false;
break;
}
slow=slow.next;
fast=fast.next;
}
slow=next.next;
next.next=null;
while(slow!=null){
fast=slow.next;
slow.next=next;
next=slow;
slow=fast;
}
return res;
}
将单向链表按某个值划分成左边小,中间相等,右边大的形式
笔试:先将链表的每一个节点放到数组中,在数组上进行partition,再按链表串起来
public ListNode listPartition(ListNode head,int num){
if(head==null){
return head;
}
ListNode cur=head;
int i=0;
//记录长度
while(cur!=null){
i++;
cur=cur.next;
}
ListNode[] nodeArr=new Node[i];
i=0;
cur=head;
for(int i=0;i!=nodeArr.length;i++){
nodeArr[i]=cur;
cur=cur.next;
}
arrPartition(nodeArr,num);
for(i=1;i!=nodeArr.length;i++){
nodeArr[i-1].next=nodeArr[i];
}
nodeArr[i-1].next=null;
return nodeArr[0];
}
public void arrPartition(ListNode[] nodeArr,int num){
int small=-1;
int big=nodeArr.length;
int index=0;
while(index!=big){
if(nodeArr[index].value<num){
swap(nodeArr,++small,index++);
}else if(nodeArr[index].value==num){
index++
}else{
swap(nodeArr,--big,index);
}
}
}
public void swap(ListNode[] nodeArr,int a,int b){
Node tmp=nodeArr[a];
nodeArr[a]=nodeArr[b];
nodeArr[b]=tmp;
}
面试:在链表上直接partition(六个变量)可以不改变相对次序,即保持稳定性且没有额外的空间
注:最后将三个区域合并时,需要讨论清楚边界,避免出现该区域没值的情况
public ListNode listPartition(ListNode head,int num){
ListNode sH=null;
ListNode sT=null;
ListNode eH=null;
ListNode eT=null;
ListNode bH=null;
ListNode bT=null;
ListNode next=null;
while(head!=null){
next=head.next;
head.next=null;
if(head.value<num){
if(sH==null){
sH=head;
sT=head;
}else{
sT.next=head;
sT=head;
}
}else if(head.value==num){
if(eH==null){
eH=head;
eT=head;
}else{
eT.next=head;
eT=head;
}
}else{
if(bH==null){
bH=head;
bT=head;
}else{
bT.next=head;
bT=head;
}
}
head=next;
}
if(sT!=null){
sT.next=eH;
eT=eT==null?sT:eT;
}
if(eT!=null){
eT.next=bH;
}
return sH!=null?sH:eH!=null?eH:bH;
}
复制含有随机指针节点的链表
笔试:哈希表,key存老的node,value存新的node
public ListNode copyListNodeWithRan(ListNode head){
HashMap<ListNode,ListNode> hashmap=new HashMap<>();
ListNode cur=head;
while(cur!=null){
hashmap.put(cur,new ListNode(cur.value));
cur=cur.next;
}
cur=head;
while(cur!=null){
hashmap.get(cur).next=hashmap.get(cur.next);
hashmap.get(cur).rand=hashmap.get(cur.rand);
cur=cur.next;
}
return hashmap.get(head);
}
通过克隆节点的位置(放在原节点的下一个)可以把HashMap省掉
合并——>复制——>分离
public ListNode copyListNodeWithRand(ListNode head){
if(head==null){
return null;
}
ListNode cur=head;
ListNode next=null;
// copy node and link to every node
while(cur!=null){
next=cur.next;//recording
cur.next=new Node(cur.value);
cur.next.next=next;
cur=next;
}
cur=head;
ListNode curCopy=null;
// set copy node rand
while(cur!=null){
next=cur.next.next;
curCopy=cur.next;
curCopy.rand=cur.rand!=null?cur.rand.next:null;
cur=next;
}
ListNode res=head.next;
cur=head;
//split
while(cur!=null){
next=cur.next.next;
curCopy=cur.next;
cur.next=next;
curCopy.next=next!=null?next.next:null;
cur=next;
}
return res;
}
两个单链表相交的一系列问题
-
首先实现一个函数判断一个单链表是否有环,有的话返回环的入环节点loop,否则返回null(两条链表分别调用该函数)
-
用哈希表HashSet实现,有额外空间,遍历并插入,若在遍历结束之前有无法插入的,就返回
-
快慢指针,无额外空间
快指针一次走两步,慢指针一次走一步,如果无环,快指针会到null,如果有环,快慢指针必会相遇
相遇之后,快指针回到头节点,然后快慢指针同时,每次走一步,最终会在入环节点相遇
-
-
分类讨论
-
两个链表都无环,如果相交,相交的时刻到走到结尾都是共有的。分别遍历两个链表得到len1,end1和len2,end2,如果end1与end2内存地址不相等的话不可能相交,相等的话找出相交部分的第一个节点,让长度大的先走,直到和长度小的长度一致,然后一起走
-
一个有环,一个没环,这种情况两条链表不可能相交
-
两个链表都有环
- 当loop1和loop2内存地址相同,就是第二种情况,把入环节点当成终止节点,然后操作和1相同
- 让loop1继续往下走在回到自己之前如果能遇到loop2就是第三种情况(随便返回其中一个loop都行),否则就是第一种情况,返回null
-
public ListNode getIntersectNode(ListNode head1,ListNode head2){
if(head1==null||head2==null){
return null;
}
ListNode loop1=getLoopNode(head1);
ListNode loop2=getLoopNode(head2);
if(loop1==null&&loop2==null){
return noLoop(head1,head2);
}
if(loop1!=null&&loop2!=null){
return bothLoop(head1,loop1,head2,loop2);
}
//其他情况包括一个有环一个无环的情况d
return null;
}
//如果有环,获取入环节点
public ListNode getLoopNode(ListNode head){
if(head==null||head.next==null||head.next.next==null){
return null;
}
ListNode n1=head.next;
ListNode n2=head.next.next;
//防止后续进不去循环
while(n1!=n2){
if(n2.next==null||n2.next.next==null){
return null;
}
n2=n2.next.next;
n1=n1.next;
}
n2=head;
while(n1!=n2){
n1=n1.next;
n2=n2.next;
}
return n1;
}
//如果都没有环的情况
public ListNode noLoop(ListNode head1,ListNode head2){
if(head1==null||head2==null){
return null;
}
ListNode cur1=head1;
ListNode cur2=head2;
int n=0;
while(cur1.next!=null){
n++;
cur1=cur1.next;
}
while(cur2.next!=null){
n--;
cur2=cur2.next;
}
if(cur1!=cur2){
return null;
}
cur1=n>0?head1:head2;
cur2=cur1==head1:haed2:head1;
n=Math.abs(n);
while(n!=0){
n--;
cur1=cur1.next;
}
while(cur1!=cur2){
cur1=cur1.next;
cur2=cur2.next;
}
return cur1;
}
//如果都有环的情况
public ListNode bothLoop(ListNode head1,ListNode loop1,ListNode head2,ListNode loop2){
ListNode cur1==null;
ListNode cur2==null;
if(loop1==loop2){
cur1=head1;
cur2=head2;
int n=0;
while(cur1!=loop1){
n++;
cur1=cur1.next;
}
while(cur2!=loop2){
n--;
cur2=cur2.next;
}
cur1=n>0?head1:head2;
cur2=cur1==head1?head2:head1;
n=Math.abs(n);
while(n!=0){
n--;
cur1=cur1.next;
}
while(cur1!=cur2){
cur1=cur1.next;
cur2=cur2.next;
}
return cur1;
}else{
cur1=loop1.next;
while(cur1!=loop1){
if(cur1==loop2){
return loop1;
}
cur1=cur1.next;
}
return null;
}
}