- 环形链表 II 206. 反转链表 912. 排序数组
大家好,文章题目是codeTop上的热门题目,本文章是京东面试题目热门题目。虽然算法在后端日常开发中应用比较少,但是它对思维的开发是很有益的,也是进中、大厂的必备的东西。文章致力于用最简单的语言阐述解题思路和解题方法,欢迎大家讨论更优秀的解题方案。
ps:对题目的讲解分为三部分,题意描述、思路描述和代码编写。众所周知有思路不一定能编写出代码,思路的完整性有助于一次AC。
强烈建议参考代码一起思考,有些东西用语言表达不出来,也就是脑子会了,手不会。题解地址
题目
142. 环形链表 II
题意:
给出一个链表,链表包含本身数值和下一个数据的地址。需要判断出此链表存不存在环形结构,返回入环的节点。
思路:
若链表有环,那么循环链表的话会永远找不到出口,可以利用这个特性,就像人在操场跑步,只要第一名足够快,第一名和最后一名一定会相遇,在链表中循环也是一样利用套圈会相遇的特点,使用两个循环若相遇(相同值)就是有环,否则无环。
但是有个问题,虽然能相遇,但是不一定是在开始循环的节点相遇,还需要计算出入环节点。
这个时候就需要公式推导了(为什么公式可以推导?因为这个是有规律的)
我们称快的为fast,慢的为slow。为了好计算,fast的速度是2,slow为1,即fast一次循环两个节点,slow为一个。
- 以路程为视角,S(fast) = 2S(slow)
- 以fast追slow为视角,追了n圈才相遇,S(fast)=S(slow) + n * S(环长度)
- 得出S(slow) = n * S(环长度)
- 以slow为视角,S(slow) = res + m * S(环长度)
res为从起点到入环的距离,即slow再走res步就是答案。换个说法,从head走res步也是入环节点,那么从head和slow位置同时走,走res步则会相遇,只要判断相遇,相遇的点就是入环点,就是答案。
代码解析:
有了思路,剩下的是转换为代码,参考
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
public static void main(String[] args) {
ListNode listNode1 = new ListNode(3);
ListNode listNode2 = new ListNode(2);
ListNode listNode3 = new ListNode(0);
ListNode listNode4 = new ListNode(-4);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode2;
ListNode res1 = detectCycle1(listNode1);
System.out.println(res1.val);
ListNode res2 = detectCycle2(listNode1);
System.out.println(res2.val);
}
/**
* 哈希法判断
* 利用哈希表进行判断是是否存在此节点来判断是否存在环形
* 每次循环时,首先判断是否在Set中
* - 若不存在则加入到Set中
* - 若存在则返回节点
* - 若节点为空,返回空
* <p>
* 复杂度分析:O(N) O(N)
*
* @param head 链表
* @return 结果
*/
public static ListNode detectCycle1(ListNode head) {
Set<ListNode> nodeSet = new HashSet<>();
while (true) {
if (head == null) {
return null;
}
if (nodeSet.contains(head)) {
return head;
}
nodeSet.add(head);
head = head.next;
}
}
/**
* 双指针
* AB两指针分别一次走2和1的距离,因为有距离差,若存在环形结构,一定会被套圈追上
* 首先计算出相遇的位置,这个位置可以用公式表示(起点到环形距离为lenA,环形长度为lenB):
* ∵ A走的速度快
* ∴ Sa = 2 * Sb(A走过的距离=2*B走过的距离)
* ∵ A走的多,在追B时,多走了很多圈
* ∴ Sa = Sb + n * lenB
* 整合后
* ∴ Sb = n * lenB Sa = 2 * n * lenB
* 所以AB走的距离都可以用lenB表示。
* <p>
* 若从起点出发,最终停留在循环入口处,期间转了m圈用公式表示:
* Res = lenA + m * lenB ①
* ∵ n m 都是基于链表的实际数字,圈数与位置没有关系
* ∵ Sb停留的位置一定在环上,但不一定在起点处,结合上面公式①
* ∴ Sb + lenA 的位置一定是起点,也就是结果
* <p>
* 当B走lenA后,即为答案,问题转换为求lenA
* Sb = n * lenB
* 令Sa = 0
* 当Sa = lenA时,Sb = lenA + n * lenB
*
* @param head 链表
* @return 结果
*/
public static ListNode detectCycle2(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (true) {
// 无环退出
if (fast == null || fast.next == null) {
return null;
}
// 走一次两个距离
fast = fast.next.next;
// 走一次一个距离
slow = slow.next;
// 相遇退出
if (fast == slow) {
break;
}
}
// 最后一步,相遇即可
fast = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
static class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
}
206. 反转链表
题意:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路:
既然是链表,进行循环即可,因为需要反向输出,循环的第一个节点就是最后一个节点,每次循环时,新建一个节点,使此节点的next指针指向循环节点的值。
题外话:程序是一个呆板的、写死的、只能按照预先设置好的执行,所以思考时需要往通用型方向思考,特殊情况特殊判断,即可完成一个健壮性程序。
代码解析:
public class ReverseList {
/**
* 题意:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
* 思路:既然是链表,进行循环即可,因为需要反向输出,循环的第一个节点就是最后一个节点,每次循环时,新建一个节点,使此节点的next指针指向循环节点的值。
*
* @param args arg
*/
public static void main(String[] args) {
ListNode listNode1 = new ListNode(1);
ListNode listNode2 = new ListNode(2);
ListNode listNode3 = new ListNode(3);
ListNode listNode4 = new ListNode(4);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
ListNode res = reverseList(listNode1);
while (res != null) {
System.out.print(res.val + " ");
res = res.next;
}
}
public static ListNode reverseList(ListNode head) {
// 结果链表
ListNode res = new ListNode();
// 第一个节点特殊判断,作为新链表最后一个节点的值
if (head != null) {
res.val = head.val;
} else {
return null;
}
while (true) {
if (head.next != null) {
head = head.next;
// 新节点作为父节点,t一直为循环至此的,结果的,第一个节点
ListNode t = new ListNode();
t.val = head.val;
t.next = res;
// res替换为新的第一个节点
res = t;
} else {
break;
}
}
return res;
}
static class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
}
912. 排序数组
题意:
给你一个整数数组 nums,请你将该数组升序排列。
思路:
快速排序。
快速排序是采用二分的思维方式,选中数组中的一个数字,以它为标准,左放比它小的数字,右边放大于它的数字,那么这个数字的位置就可以确定了,然后左边的数字、右边的数字重复这一操作,最终排序的数组为一个数字,那么排序也就完成了。
即每次只排一个数字。
优化:选取数字标准的时候,可以从数组中随机挑取,这样会加快排序速度。
代码解析:
import java.util.Arrays;
import java.util.Random;
public class QuickSort {
private static final Random RANDOM = new Random();
/**
* 题意:给你一个整数数组 nums,请你将该数组升序排列。
* 解析:快速排序。
* 快速排序是采用二分的思维方式,选中数组中的一个数字,以它为标准,左放比它小的数字,右边放大于它的数字,那么这个数字的位置就可以确定了,然后左边的数字、右边的数字重复这一操作,最终排序的数组为一个数字,那么排序也就完成了。
* 即每次只排一个数字。
* 优化:选取数字标准的时候,可以从数组中随机挑取,这样会加快排序速度。
*
* @param args arg
*/
public static void main(String[] args) {
int[] nums = new int[]{5, 1, 1, 2, 0, 0};
int[] ints = sortArray(nums);
System.out.println(Arrays.toString(ints));
}
public static int[] sortArray(int[] nums) {
// 快速排序
return quickSort(nums);
}
/**
* 快速排序
*
* @param nums 数组
* @return 结果
*/
private static int[] quickSort(int[] nums) {
// 递归处理
quickSortFun(nums, 0, nums.length - 1);
return nums;
}
private static void quickSortFun(int[] nums, int left, int right) {
// left,right分表代表着需要排序数组的区间,若不满足下属要求,说明已经不需要排序了
if (left >= right) {
return;
}
// 从数组中随机挑取一个数组与最左边的数字交换,即最左边为此次排序的标准
int randomIndex = left + RANDOM.nextInt(right - left + 1);
swap(nums, randomIndex, left);
// 为何+1,是因为left数字为此次排序标准数,不参与排序,只需要最后放到它应该在的地方即可
int i = left + 1;
int j = right;
int index = nums[left];
while (true) {
// 从左到右,比标准数小的话直接跳过,相等是不跳过的(原因如下),i是可以到达right的,
while (i <= right && nums[i] < index) {
i++;
}
// 从右到左,比标注数大的话直接跳过,相等是不跳过的(原因如下),j不允许到达left
while (j > left && nums[j] > index) {
j--;
}
// 限制左右循环不允许相遇(相遇说明排序完成了)
if (i >= j) {
break;
}
// i,j分别停留在了大于、小于标准数的位置,交换两个数字使数组满足要求
// 相等的元素通过交换,等概率分到数组的两边
swap(nums, i, j);
i++;
j--;
}
// 最终停留位置与标准数交换,那么标准数已经放到最终位置
swap(nums, left, j);
quickSortFun(nums, left, j - 1);
quickSortFun(nums, j + 1, right);
}
private static void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}