本文内容基于《漫画算法 小灰的算法之旅》,魏梦舒著。
1. 如何判断链表有环?
1.1 双重遍历
首先从头结点开始,依次遍历单链表中的每一个节点,每遍历一个新节点,就从头检查新节点之前的所有节点,如果发现新节点和之前的某个节点相同,则说明该节点被遍历过两次,链表有环。
public class IsCycle {
private static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static boolean isCycle(Node node) {
Node node1 = node.next;
int i = 1;
while (node1 != null) {
Node node2 = node;
int j = 0;
while (node2 != null && j < i) {
if (node2 == node1) {
return true;
}
j++;
node2 = node2.next;
}
i++;
node1 = node1.next;
}
return false;
}
public static void main(String[] args) {
Node node1 = new Node(5);
Node node2 = new Node(3);
node1.next = node2;
Node node3 = new Node(7);
node2.next = node3;
Node node4 = new Node(2);
node3.next = node4;
Node node5 = new Node(6);
node4.next = node5;
Node node6 = new Node(8);
node5.next = node6;
Node node7 = new Node(1);
node6.next = node7;
node7.next = node4;
System.out.println(isCycle(node1));
}
}
假设链表的节点数量为n,则方法1的时间复杂度为:O(n^2),空间复杂度为O(1)。
1.2 哈希表
首先创建一个HashSet用来存储曾经遍历过的节点,每遍历一个节点,都用新节点同HashSet中存储的节点进行比较,如果相同,则说明链表有环。
public class IsCycle {
private static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static boolean isCycle1(Node node) {
HashSet<Node> set = new HashSet<>();
Node node1 = node;
while (node1 != null) {
if (set.contains(node1)) {
return true;
}
set.add(node1);
node1 = node1.next;
}
return false;
}
public static void main(String[] args) {
Node node1 = new Node(5);
Node node2 = new Node(3);
node1.next = node2;
Node node3 = new Node(7);
node2.next = node3;
Node node4 = new Node(2);
node3.next = node4;
Node node5 = new Node(6);
node4.next = node5;
Node node6 = new Node(8);
node5.next = node6;
Node node7 = new Node(1);
node6.next = node7;
node7.next = node4;
System.out.println(isCycle1(node1));
}
}
假设链表的节点数量为n,则方法1的时间复杂度为:O(n),空间复杂度为O(n)。
1.3 追及问题
首先创建两个对象引用,让它们同时指向这个链表的头节点,然后开始一个大循环,让引用1每次向后移动1个节点,引用2每次向后移动2个节点,然后比较两个引用指向的节点是否相同,如果相同,则说明链表有环。
类似于数学上的追及问题,因为是环形的,如果一个比另一个的速度快,那么它们肯定会相遇。
public class IsCycle {
private static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static boolean isCycle2(Node node) {
Node node1 = node;
Node node2 = node;
while (node2 != null && node2.next != null) {
node1 = node1.next;
node2 = node2.next.next;
if (node1 == node2) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Node node1 = new Node(5);
Node node2 = new Node(3);
node1.next = node2;
Node node3 = new Node(7);
node2.next = node3;
Node node4 = new Node(2);
node3.next = node4;
Node node5 = new Node(6);
node4.next = node5;
Node node6 = new Node(8);
node5.next = node6;
Node node7 = new Node(1);
node6.next = node7;
node7.next = node4;
System.out.println(isCycle2(node1));
}
}
假设链表的节点数量为n,则方法1的时间复杂度为:O(n),空间复杂度为O(1)。
1.4 如果链表有环,如何求出环的长度?
当两个引用首次相遇,证明链表有环的时候,让两个引用从相遇点继续前进,并统计前进的次数,直到两个引用第二次相遇,此时这个前进次数就是环的长度。
public static int getCycleLength(Node node) {
Node node1 = node;
Node node2 = node;
boolean firstMeet = false;
int length = 0;
while (node2 != null && node2.next != null) {
node1 = node1.next;
node2 = node2.next.next;
if (node1 == node2) {
if (!firstMeet) {
//第一次相遇
firstMeet = true;
} else {
//第二次相遇
return length;
}
}
if (firstMeet) {
length++;
}
}
return -1;
}
1.5 如果链表有环,如何求出入环节点?
只需要记住:从链表头节点到入环点的距离,等于从首次相遇点回到入环点的距离。
所以,当两个引用首次相遇,让一个引用回到头节点继续前进,一个节点从相遇点继续前进,都每次只前进一个节点,直到两个引用第二次相遇,第二次相遇点就是入环点。
public static Node getInCycleNode(Node node) {
Node node1 = node;
Node node2 = node;
while (node2 != null && node2.next != null) {
node1 = node1.next;
node2 = node2.next.next;
if (node1 == node2) {
//第一次相遇
node1 = node;
break;
}
}
while (node1 != null && node2 != null) {
if (node1 == node2) {
//第二次相遇
return node1;
}
node1 = node1.next;
node2 = node2.next;
}
return null;
}
2. 最小栈的实现?
【问题】实现一个栈,该栈有出栈、入栈、取最小元素3个方法,要保证这3个方法的时间复杂度都是O(1)。
【思路】用两个栈来实现,栈A存所有元素,栈B存栈A当前的最小元素。
public class MinStack {
private static Stack<Integer> mainStack = new Stack<>();
private static Stack<Integer> minStack = new Stack<>();
public static void push(int element) {
mainStack.push(element);
if (minStack.isEmpty() || minStack.peek() >= element) {
minStack.push(element);
}
}
public static int pop() {
if (mainStack.peek().intValue() == minStack.peek()) {
minStack.pop();
}
return mainStack.pop();
}
public static int getMin() {
return minStack.peek();
}
public static void main(String[] args) {
MinStack.push(4);
MinStack.push(9);
MinStack.push(7);
MinStack.push(3);
MinStack.push(8);
MinStack.push(5);
System.out.println(MinStack.getMin());
MinStack.pop();
MinStack.pop();
MinStack.pop();
System.out.println(MinStack.getMin());
}
}
3. 如何求出最大公约数?
3.1 暴力枚举法
public class GreatestCommonDivisor {
public static int getGreatestCommonDivisor(int a, int b) {
int big = a > b ? a : b;
int small = a < b ? a : b;
if (big % small == 0) {
return small;
}
for (int i = small / 2; i > 1; i--) {
if (small % i == 0 && big % i == 0) {
return i;
}
}
return 1;
}
public static void main(String[] args) {
System.out.println(getGreatestCommonDivisor(25, 5));
System.out.println(getGreatestCommonDivisor(100, 80));
System.out.println(getGreatestCommonDivisor(27, 14));
}
}
该方法的问题是:如果传入的整数是10000和10001,就需要循环10000 / 2 - 1次,效率比较低。时间复杂度为O(min(a, b))。
3.2 辗转相除法
两个正整数a和b(a > b),它们的最大公约数等于a % b和b的最大公约数。
public class GreatestCommonDivisor {
public static int getGreatestCommonDivisor1(int a, int b) {
int big = a > b ? a : b;
int small = a < b ? a : b;
if (big % small == 0) {
return small;
}
return getGreatestCommonDivisor1(big % small, small);
}
public static void main(String[] args) {
System.out.println(getGreatestCommonDivisor1(25, 5));
System.out.println(getGreatestCommonDivisor1(100, 80));
System.out.println(getGreatestCommonDivisor1(27, 14));
}
}
该方法的问题是:当两个整数比较大时,%的效率比较低。时间复杂度可以近似为O(log(max(a, b)))。
3.3 更相减损术
两个正整数a和b(a > b),它们的最大公约数等于a - b和b的最大公约数。
public class GreatestCommonDivisor {
public static int getGreatestCommonDivisor2(int a, int b) {
if (a == b) {
return a;
}
int big = a > b ? a : b;
int small &#