链表类问题
1.7 链表题目
1.7.0 链表生成器
自己写了一个链表生成器,包括一个可以按照格式打印的链表结点类。
【使用方法】调用链表生成器,传入一个数组,即可按照数组数据生成链表
//可以根据一个数组生成对应数值的链表,并且可以根据链表的样子打印出来
public static class Node{
int val;
Node next;
public Node(int val) {
this.val = val;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
Node temp = this;
while(temp!=null){
result.append(temp.next==null?temp.val:temp.val+" -> ");
temp = temp.next;
}
return result.toString();
}
}
public static Node linkGenerator(int[] nums){
Node head = new Node(nums[0]);
Node temp = head;
for (int i = 1; i < nums.length; i++) {
temp.next = new Node(nums[i]);
temp = temp.next;
}
return head;
}
使用方法:
public static void main(String[] args) {
Node head = linkGenerator(new int[]{1,2,3,4,5,5,4,3,2,1});
System.out.println(head);
//1 -> 2 -> 3 -> 4 -> 5 -> 5 -> 4 -> 3 -> 2 -> 1
}
1.7.1 找到链表中的相同节点
题目:给定两个有序链表头指针head1和head2,打印两个链表的公共部分
算法:外部排序(Merge过程)
1.7.2 判断一个链表是不是回文结构
题目:
算法1:
遍历链表,将node加入栈中,再遍历栈,和原始链表比对,如果相等,说明是回文链表
算法1增强版:
- 快慢指针:当快指针走到结尾的时候,慢指针走到中点的下一个位置
- 将慢指针和之后的节点压栈
- 弹栈,和链表的前半部分比较
算法2:
- 快慢指针:当快指针走到结尾的时候,慢指针走到中点
- 逆序从慢指针开始的后半部分
- 新建两个指针从头和尾遍历,到终点node为止
- 如果都相等,返回true,否则返回false
- 最后再将后半部分恢复回来
public class Palindrome {
public static void main(String[] args) {
Node head = linkGenerator(new int[]{1,2,3,4,5,5,4,3,2,1});
System.out.println(head);
System.out.println(isPalindrome3(head));
}
//空间复杂度为n
public static boolean isPalindrome1(Node head){
Stack<Integer> stack = new Stack<>();
Node pointer = head;
while (pointer!=null){
stack.push(pointer.val);
pointer = pointer.next;
}
pointer = head;
while (!stack.isEmpty()){
if (stack.pop()!=pointer.val){
return false;
}
pointer = pointer.next;
}
return true;
}
//空间复杂度为n/2
public static boolean isPalindrome2(Node head){
if (head == null || head.next == null){
return true;
}
Node slow = head.next;//注意这里慢指针的初始位置
Node fast = head;
//当循环终止时,fast在最后一位(奇数),或者在倒数第二位(偶数)
//slow在中间位置的下一位(奇数)1->2->3->(2)->1,或者在中间相等两数的第二个数(偶数)1->2->3->(3)->2->1
while(fast.next!=null&&fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
//将此时从slow开始的链表压栈
Stack<Integer> stack = new Stack<>();
while (slow!=null){
stack.push(slow.val);
slow = slow.next;
}
while(!stack.isEmpty()){
if (head.val != stack.pop()){
return false;
}
head = head.next;
}
return true;
}
//空间复杂度为O(1)
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;
}
//开始反转后半部分的链表
n2 = n1.next;
n1.next = null;//mid.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.val!=n2.val){
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;
}
}
1.7.3 链表的荷兰国旗问题
要求:给定一个链表和一个数字,将比这个数字小的节点放在左边,比这个数字大的节点放在右边,和这个数字相等的节点放在中间
时间复杂度为O(N),空间复杂度为O(1)
1.7.4 复制含有随机指针节点的链表
题目:在链表的结点中,除了val,next之外,还有一个类型为Node的随机指针,它可能指向链表中的任何一个节点,也可能指向null
算法1:
使用HashMap :
- 将原链表中的节点作为key,将copy后的节点作为value,放入HashMap中。
- 重建新的链表
- 通过映射关系,重建rand关系:1’和 2’之间的关系可以通过1和2之间的关系得到
需要额外空间
代码:
public class DeepCopy {
public static void main(String[] args) {
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.rand = head;
head.next.next.rand = null;
}
//需要额外的存储空间
public static Node copyListWithRand1(Node head){
HashMap<Node, Node> map = new HashMap<>();
Node cur = head;
while(cur!=null){
map.put(cur, new Node(cur.val));
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);
}
}
class Node{
int val;
Node next;
Node rand;
public Node(int val) {
this.val = val;
}
}
算法2:不用HashMap
空间复杂度为O(1)
- 通过第一次遍历,将原来的链表:
每个copy节点都连接在原节点的后面
- 当确定rand或者next关系的时候,首先通过原来链表的rand关系找到(1找到2),因为2’就在2的下一位,所以1’可以直接找到2’(通过这种结构关系,巧妙地避免了HashMap的使用)
- 最后,分离混合的链表(在这里面重新确定next关系)
代码:
//不需要额外的存储空间
public static Node copyListWithRand2(Node head){
Node cur = head;
Node next = null;
//构造出 1 -> 1 -> 2 -> 2 -> 3 -> 3 这种结构
while(cur!=null){
next = cur.next;
cur.next = new Node(cur.val);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
//设置 “复制Node”的rand指针
while(cur!=null){
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand == null? null:cur.rand.next;
cur = next;
}
Node res = head.next;
cur = head;
//分离混合在一起的链表
while(cur!=null){
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next==null?null:next.next;
cur = next;
}
return res;
}
1.7.5 两个链表相交的一系列问题
题目:
算法:
因为是单链表,所以右图的情况是不可能的
-
首先判断两个链表是否有环(用下面问题的方法)
-
分别求出他们的入环节点loop1和loop2
-
如果没有环(loop1 == null ,loop2 == null),通过遍历每个节点,维护两个变量:len和endNode
- 如果endNode1!=endNode2,两个链表不相交;否则相交
- 如果相交,则比较len
( 如果len1-len2 = 10,则先让链表1遍历10个node,然后两个链表一起遍历,相遇的节点就是交叉的第一个节点)
-
如果两个一个链表有环一个链表无环,不可能相交(loop1,loop2中有一个为null)
-
如果两个链表都有环(loop!=null, loop2!=null):
- 各自成环,不相交
- 先相交,再共享一个环(loop1 == loop2)
- 从环上两个地方切入
public static Node getIntersectNode(Node head1, Node head2){
if (head1 == null || head2 == null){
return null;
}
if (head1 == head2){
return head1;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null){
return noLoop(head1, head2);
}else if(loop1 != null&&loop2 != null){
return bothLoop(head1, head2, loop1, loop2);
}else {
return null;
}
}
public static Node getLoopNode(Node head){
if (head == null || head.next == null || head.next.next == null){
return null;
}
//两个只针的位置要错开
Node fast = head.next.next;
Node slow = head.next;
//第一次重合
while(fast != slow){
if (fast.next == null||fast.next.next==null){
return null;
}
fast = fast.next.next;
slow = slow.next;
}
//重合后,快指针放回head
fast = head;
//寻找第二次重合
while (fast!=slow){
fast = fast.next;
slow = slow.next;
}
//返回当前重合节点
return fast;
}
public static Node noLoop(Node head1, Node head2){
if (head1 == null || head2 == null){
return null;
}
//遍历head1
int len1 = 0;
Node endNode1 = null;
Node cur = head1;
while(cur != null){
if (cur.next == null){
endNode1 = cur;
}
len1++;
cur = cur.next;
}
//遍历head2
int len2 = 0;
Node endNode2 = null;
cur = head2;
while(cur != null){
if (cur.next == null){
endNode2 = cur;
}
len2++;
cur = cur.next;
}
//结尾节点不相等,肯定不相交
if (endNode1!=endNode2){
return null;
}
//结尾节点相等
//长的链表先遍历sub步
Node longerHead = len1>len2?head1:head2;
Node shorterHead = len1>len2?head2:head1;
int sub = len1>len2?len1-len2:len2-len1;
for (int i = 0; i < sub; i++) {
longerHead = longerHead.next;
}
while(longerHead!=shorterHead){
longerHead = longerHead.next;
shorterHead = shorterHead.next;
}
return longerHead;
}
public static Node bothLoop(Node head1, Node head2, Node loop1, Node loop2){
if (loop1 == loop2){
int len1 = 0;
Node cur1 = head1;
while(cur1 != loop1){
len1++;
cur1 = cur1.next;
}
//遍历head2
int len2 = 0;
Node cur2 = head2;
while(cur2 != loop1){
len2++;
cur2 = cur2.next;
}
//结尾节点相等
//长的链表先遍历sub步
Node longerHead = len1>len2?head1:head2;
Node shorterHead = len1>len2?head2:head1;
int sub = len1>len2?len1-len2:len2-len1;
for (int i = 0; i < sub; i++) {
longerHead = longerHead.next;
}
while(longerHead!=shorterHead){
longerHead = longerHead.next;
shorterHead = shorterHead.next;
}
return longerHead;
}
Node cur = loop1.next;
while (cur!=loop1){
if (cur == loop2){
return loop2;
}
cur = cur.next;
}
return null;
}
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
1.7.6 判断一个单链表是否有环(若有环,将环的起点返回)
算法1:快慢指针:
-
如果两个指针重合,说明这个链表有环;如果其中一个指针遇到了null,则必然无环。
-
当两个指针重合后,快指针回到起点,速度变为1步每次,则两个指针一定会在环的起点相遇
-
注意:快慢指针的起点:
-
要么都在起点(推荐)
Node fast = head; Node slow = head; while(true){ fast = fast.next.next; slow = slow.next; if (fast == slow){ break; } }
-
要么在起点的之后两个位置
Node fast = head.next.next; Node slow = head.next; while(fast != slow){ fast = fast.next.next; slow = slow.next; }
-
否则:在寻找环入口的时候可能会陷入死循环
-
算法2:HashMap:遍历链表,每次将node存进表中,如果第一次出现了重复的node,就返回这个node