一、判断链表是否有环
单链表只有一个next指针,所以有环一定是下面的情况(所以不要脑补其他结构了😋):
方法一、使用HashSet
- 思路:遍历链表,并依次把节点放入HashSet中,放入前判断节点在Set中是否出现过,由此得到链表是否包含环。
方法二、使用快慢指针(比较魔性🤯)
- 思路:使用快慢指针遍历链表,快指针一旦走到
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;
}
slow = slow.next;
fast = fast.next.next;
}
// 快指针返回链表初始节点
fast = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
二、链表相交问题
链表相交的情况有多种,需要分开讨论
情况一、两个链表都无环
- 思路:首先对两个链表进行遍历,获得最后一个节点,如果两个链表得**最后一个节点不相同的话,说明两个链表不为空(相交只可能是Y字形);
在遍历的过程中同时获取两个链表的长度,两个链表的公共部分**长度一定相同,所以需要保证非公共部分长度一致:对长链表遍历,遍历的次数为两链表长度之差(切除过长部分);
之后两链表同时遍历,当两指针指向的节点相同时,返回该节点。
public static Node noLoop(Node head1, Node head2) {
// 先遍历到最后一个节点,因为两个链表最后一个节点不相同的话,一定不相交
// 同时获得两个链表各自的长度
Node cur1 = head1, cur2 = head2;
int n = 0;
while (cur1.next != null) {
cur1 = cur1.next;
n++;
}
while (cur2.next != null) {
cur2 = cur2.next;
n--;
}
if (cur1 != cur2) {
return null;
}
// cur1变成较长链表的头,cur2变成较短链表的头
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n > 0) {
cur1 = cur1.next;
n--;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur1.next;
}
return cur1;
}
情况二、两个链表一个有环、一个无环
不可能存在的结构
情况三、两个链表都有环
- 思路:判断两个链表的入环节点是否为同一个
- 若入环节点为同一个,则相交节点在入环节点之前(如图二),此时可将入环节点看作两个链表的终止节点,然后进行情况一的判断
- 若入环节点不为同一个,则设定两个指针
p1,p2
分别指向两个链表的入环节点,保持p1
不变,p2
继续向下走:- 当
p2
指向了p1
所指的节点(如图三),说明两个链表的入环节点不同,但环相同,此时返回两个入环节点之一 - 当
p2
回到了起点(即入环节点),则说明两链表不相交,如图1。
- 当
public class FindFirstIntersectNode {
/**
* 【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。
* 请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null
* 【要求】如果两个链表长度之和为N,时间复杂度请达到$O(N)$,额外空间复杂度请达到$O(1)$。
*/
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.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);
} else {
return bothLoop(head1, loop1, head2, loop2);
}
}
/**
* 找到链表的第一个入环节点,如果没有环,则返回空
* 使用快慢指针
*/
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;
}
slow = slow.next;
fast = fast.next.next;
}
// 快指针返回链表初始节点
fast = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
/**
* 如果两个链表都无环,返回第一个相交节点,如果不相交,返回null
*/
public static Node noLoop(Node head1, Node head2) {
// 先遍历到最后一个节点,因为两个链表最后一个节点不相同的话,一定不相交
// 同时获得两个链表各自的长度
Node cur1 = head1, cur2 = head2;
int n = 0;
while (cur1.next != null) {
cur1 = cur1.next;
n++;
}
while (cur2.next != null) {
cur2 = cur2.next;
n--;
}
if (cur1 != cur2) {
return null;
}
// cur1变成较长链表的头,cur2变成较短链表的头
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n > 0) {
cur1 = cur1.next;
n--;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur1.next;
}
return cur1;
}
/**
* 两个有环链表,返回第一个相交节点,如果不相交返回null
*
* @param head1 链表1的头节点
* @param loop1 链表1的入环节点
* @param head2 链表2的头节点
* @param loop2 链表2的入环节点
* @return 相交节点
*/
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null, 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) {
cur1 = cur1.next;
n--;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
cur2 = loop2;
while (cur1 != loop1) {
if (cur1 == cur2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
}
三、二叉树递归/非递归遍历
1、递归
递归遍历方法不难,代码也很好写,三者的区别在于输出的顺序不同。
递归先序遍历
public static void preOrderRecur(Node head) {
if (head == null) {
return;
}
System.out.print(head.value + " ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}
递归中序遍历
public static void inOrderRecur(Node head) {
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.value + " ");
inOrderRecur(head.right);
}
递归后序遍历
public static void posOrderRecur(Node head) {
if (head == null) {
return;
}
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.print(head.value + " ");
}
2、非递归
递归方式本质上是系统帮我们维护一个栈,并针对我们的递归操作进行弹栈/压栈处理;而非递归的方法就是由程序员手动弹栈/压栈。
非递归先序遍历
算法步骤:
-
从栈中弹出一个节点
cur
-
对该节点的
value
进行打印/处理 -
将
cur
节点的孩子按照先左后右的顺序依次压入栈(如果有的话) -
重复1~3
/**
* 非递归先序遍历
*/
public static void preOrderUnRecur(Node head) {
System.out.print("pre-Order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.push(head);
Node cur = null;
while (!stack.isEmpty()) {
cur = stack.pop();
System.out.print(cur.value + " ");
if (cur.left != null) {
stack.push(cur.left);
}
if (cur.right != null) {
stack.push(cur.right);
}
}
}
System.out.println();
}
非递归中序遍历
算法思想:对于每棵子树,整棵树左边界进栈,即不断将left
放入栈中,直到left
为null
,然后将栈顶节点弹出打印,并将弹出节点的right
子树(如果有的话)重复上述左边界进栈,直到栈为空
/**
* 非递归中序遍历
*/
public static void inOrderUnRecur(Node head) {
System.out.print("in-Order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.println(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
ps:确实抽象🤯
非递归后序遍历
算法思想:上述先序遍历打印的顺序是父左右
,可以将对左右孩子的处理顺序调换,这样打印的顺序是父右左
,可以发现这个顺序的逆序正是后序遍历的顺序,所以可以把打印操作换成压栈操作,即再准备一个收集栈,用于调转打印顺序。
/**
* 非递归后序遍历
*/
public static void posOrderUnRecur(Node head) {
System.out.println("pos-Order: ");
if (head != null) {
Stack<Node> stack1 = new Stack<>();
Stack<Node> stack2 = new Stack<>();
stack1.push(head);
while (!stack1.isEmpty()) {
head = stack1.pop();
stack2.push(head);
if (head.left != null) {
stack2.push(head.left);
}
if (head.right != null) {
stack2.push(head.right);
}
}
while (!stack2.isEmpty()) {
System.out.print(stack2.pop().value + " ");
}
}
System.out.println();
}
二叉树遍历属于必须要熟练掌握的内容,其中非递归遍历可能会在面试场合出现。