链表相关问题
一、快慢指针简单问题
1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点
A.推演过程
1.设置快慢指针指向第一个元素
2.只要快指针后面的两个元素都不为空,就让快指针每次走两步,慢指针每次走一步
3.返回慢指针所指向的节点,这里是C节点,这里仅列出了偶数长度返回上中点的情况,奇数长度同理。
B.代码实现:
public static Node midOrUpMidNode(Node head){
if (head == null||head.next == null||head.next.next == null){
return head;
}
// 链表有2个有值的点及以上
Node slow = head.next; // 慢指针
Node fast = head.next.next; // 快指针
while (fast.next!=null&&fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点
A.推演过程
1.将快慢指针指向头节点的下一个节点
2.快指针每次走两步,慢指针每次走一步
3.返回慢指针对应的节点,这里注意是认为头节点也算一个节点的,而上一题没算,可能造成理解困难,但是我想多维化思考
B.代码实现
public static Node midOrDownMidNode(Node head){
if (head==null||head.next==null){
return head;
}
Node slow = head.next;
Node fast = head.next;
while (fast.next!=null&&fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
A.推演过程
1.设置慢指针指向头节点,快指针指向第三个节点
2.只要快指针后面有两个节点不为空,快指针一次两步,慢指针一次一步
3.返回slow节点
B.代码实现
public static Node midOrUpMidPreNode(Node head){
if (head==null||head.next==null||head.next.next==null){
return head;
}
Node slow = head;
Node fast = head.next.next;
while (fast.next!=null&&fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个
A.推演过程
1.快指针指向头节点,慢指针指向第二个节点
2.只要快指针后两个节点不为空,快2慢1
3.返回慢指针对应的节点
B.代码实现
public static Node midOrDownMidPreNode(Node head){
if (head == null||head.next == null){
return null;
}
if (head.next.next==null){
return head;
}
Node slow = head;
Node fast = head.next;
while (fast.next!=null&&fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
二、稍微难一点的问题
1)判断给定头节点的链表是否为回文结构
链表的回文结构:
链表回文结构和普通回文结构略有不同
比如如下这个链表
这个链表显然是回文结构,但是如果将链表各个节点的值拼接在一起是1202020201,显然对于一个字符串1202020201不是回文结构
方法一:带容器的,用一个栈解决
推理过程
1.准备一个栈
2.将链表元素依次入栈,但不破坏原结构
3.依次遍历链表元素,并且栈中弹出一个元素,将他们的值进行比较,如果全部相等,则该链表为回文链表,下图是一次比较过程
代码实现
public static boolean isPalindrome1(Node head){
Stack<Node> stack = new Stack<>();
Node 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;
}
不难发现本方法是可以进行改进的,改进只需要将后半部分压入栈中即可,结合快慢指针法,可得如下代码,这里希望读者能够将图像画出来,自己推演一下过程,加深对快慢指针的理解:
public static boolean isPalindrome2(Node head){
if (head==null||head.next==null){
return true; // 0个或者1个节点,规定是回文串
}
Node right = head.next;
Node cur = head;
while (cur.next!=null&&cur.next.next!=null){
right = right.next;
cur = cur.next.next;
}
Stack<Node> stack = new Stack<>();
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;
}
方法二:有限变量实现,非常考研编程能力,所以在面试题中频繁出现
推理过程
1.利用快慢指针找到中点,奇数找中点,偶数找上中点
2.调整链表,考验代码细节能力,调整成如下模样
这里调整过程如果看不懂,可以对着下面的代码自己画一画,基本上也就能明白
3.向中间移动头尾指针,进行比较,直到有一个指针为空,如果全部相等,则为回文子串,返回true前应该恢复原状,这里也是编程细节处理问题,对着代码痛苦的搞一遍,以后就不用再痛苦了。
代码实现
public static boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
while (n2.next != null && n2.next.next != null) {
n1 = n1.next;
n2 = n2.next.next;
}
// n1 是 中点
n2 = n1.next;
n1.next = null;
Node n3 = null;
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1;
n2 = head;
boolean res = true;
while (n1 != null && n2 != null) {
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
2)将单向链表按某值划分成左边小、中间相等、右边大的形式
方法一、利用数组做Partition(复习一下快排就懂)
推理过程
- 将链表放入等长的数组中
- 利用Partition算法进行分区
- 将数组中的节点串成链表返回
注:如果Partition有困难的话,可以先学习一下数据结构有关快速排序的内容
代码实现
public static Node listPartition1(Node head,int pivot){
if (head == null){
return head;
}
Node cur = head;
int i = 0;
while (cur!=null){
i++;
cur = cur.next;
}
Node[] nodeArr = new Node[i]; // 建立一个等长的数组
i = 0;
cur = head;
for (i = 0; i != nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr,pivot);
for (i = 1; i!=nodeArr.length ; i++) {
nodeArr[i-1].next = nodeArr[i];
}
nodeArr[i-1].next = null;
return nodeArr[0];
}
public static void arrPartition(Node[] nodeArr,int pivot){
int small = -1;
int big = nodeArr.length;
int index = 0;
while (index!=big){
if (nodeArr[index].value<pivot){
swap(nodeArr,++small,index++);
}else if (nodeArr[index].value==pivot){
index++;
}else {
swap(nodeArr,--big,index);
}
}
}
public static void swap(Node[] nodeArr,int a,int b){
Node tmp = nodeArr[a];
nodeArr[a] = nodeArr[b];
nodeArr[b] = tmp;
}
方法二、利用有限变量,考验编程能力
推理过程
1.分三个区,小于指定值pivot区,等于pivot区,大于pivot区,分别设置三个区域的头尾指针(sh,st,eh,et,mh,mt)如下图
2.遍历单链表按照各个节点值的大小与pivot的关系将其插入指定的队列,结果如下图
3.将st指向eh,将et指向mh
代码实现
public static Node listPartition2(Node head, int pivot) {
Node sH = null;
Node sT = null;
Node eH = null;
Node eT = null;
Node mH = null;
Node mT = null;
Node next = null;
while (head != null) {
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (mH == null) {
mH = head;
mT = head;
} else {
mT.next = head;
mT = head;
}
}
head = next;
}
if (sT != null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
if (eT != null) {
eT.next = mH;
}
return sH != null ? sH : (eH != null ? eH : mH);
}
3)一种特殊的单链表节点类描述如下
class Node{
int value;
Node next;
Node rand;
Node(int value){
this.value = value;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。现给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点
方法一:用容器的方法,哈希表实现
推理过程
0.假设复制如下链表,曲线为rand指针
1.准备一个哈希表,然后遍历链表,哈希表的key存放原节点,哈希表的value存放新的copy节点
然后再次遍历链表,我们由A的的源节点找到next和rand然后参照哈希表对新A进行连线以此类推,便可实现复制功能,如果不太清楚,可以尝试看看代码
代码实现
public static Node copyListWithRand1(Node head){
HashMap<Node,Node> map = new HashMap<>();
Node cur = head;
while (cur!=null){
map.put(cur,new Node(cur.value));
cur=cur.next;
}
cur=head;
while (cur!=null){
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
方法二:利用有限的变量实现,考验编程能力
推理过程
1.遍历链表将自己复制一份插在自己后面,暂不不设置rand指针
3.再次遍历链表设置新节点的rand指针,想一想为什么这个时候可以做到这一步?
3.实现分离链表,得到复制的链表
代码实现
public static Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next = null;
// copy node and link to every node
// 1 -> 2
// 1 -> 1' -> 2
while (cur != null) {
// cur 老 next 老的下一个
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
// set copy node rand
// 1 -> 1' -> 2 -> 2'
while (cur != null) {
// cur 老
// cur.next 新 copy
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = next;
}
// head head.next
Node 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;
}
4)给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null,要求如果两个链表长度之和为N,时间复杂度请达到O(N),空间复杂度O(1)
推理过程
实现这个较为复杂的问题,我们往往要进行问题的拆解,所以我们首先要实现一个子函数完成如下功能
给定一个单链表,如果有环返回入环的第一个节点,如果无环返回null
问题推理过程如下:
1.给定一个单链表,我们设置快慢指针,往后走,如果快指针先为空,一定无环,返回null,如果快慢指针相遇,则一定有环,将快指针回到头节点处,快慢指针同时以一步往后走,则下一次相遇位置就是入环节点,返回即可
第一步代码如下
//找到链表第一个入环节点,如果无环返回null
public static Node getLoopNode(Node head){
if (head==null||head.next==null||head.next.next==null){
return null;
}
Node slow = head.next;
Node fast = head.next.next;
while (slow!=fast){
if (fast.next==null||fast.next.next==null){
return null;
}
fast=fast.next.next;
slow=slow.next;
}
fast=head;
while (slow!=fast){
slow=slow.next;
fast=fast.next;
}
return slow;
}
2.分类讨论,当链表1和链表二都无环时:
遍历两个链表求各自长度,判断null,然后做差,设置两个初始分别指向两链表头部的指针,让长链表指针先走差值然后长短链表同时走,知道为空或者第一次相等时停止,返回相等节点。
代码如下
//如果两个链表都无环,返回第一个相交节点,如果无,则返回null
public static Node noLoop(Node head1, Node head2){
if (head1==null||head2==null){
return null;
}
Node cur1 = head1;
Node 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?head2:head1;
n=Math.abs(n);
while (n!=0){
n--;
cur1=cur1.next;
}
while (cur1!=cur2){
cur1=cur1.next;
cur2=cur2.next;
}
return cur1;
}
3.分类讨论:一个链表无环一个链表有环这种情况是不存在的所以不必讨论,至于为什么不存在可以画图试一试。
4.分类讨论:两个链表均有环的情况:
总共只有三种结构的可能:
(一)两个链表不相交各自独立
(二)两链表相交入环节点是同一个,方法其实同2.,区分出长短链表,长链表先走差值步,最终同步移动知道第一次相等返回交点
(三)两链表相交入环节点不同
代码如下:
//两个有环节点,返回第一个相交节点,如果不相交,返回null
public static Node boothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node 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;
}
}
总体代码实现
public static class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1==null&&loop2==null){
return noLoop(head1,head2);
}
if (loop1!=null&&loop2!=null){
return boothLoop(head1,loop1,head2,loop2);
}
return null;
}
//找到链表第一个入环节点,如果无环返回null
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node slow = head.next;
Node fast = head.next.next;
while (slow != fast) {
if (fast.next == null || fast.next.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
//如果两个链表都无环,返回第一个相交节点,如果无,则返回null
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node 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 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
//两个有环节点,返回第一个相交节点,如果不相交,返回null
public static Node boothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node 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;
}
}