剑指 Offer 52. 两个链表的第一个公共节点
题目:
输入两个链表,找出它们的第一个公共节点。 如下面的两个链表:
在节点 c1 开始相交。
示例1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
解题思路:
设「第一个公共节点」为 node ,「链表 headA」的节点数量为 a ,「链表 headB」的节点数量为 b ,「两链表的公共尾部」的节点数量为 c ,则有:
- 头节点 headA 到 node 前,共有 a - c 个节点;
- 头节点 headB 到 node 前,共有 b - c 个节点;
考虑构建两个节点指针 A , B 分别指向两链表头节点 headA , headB ,做如下操作:
- 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:a+(b−c)
- 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:b+(a−c)
如下式所示,此时指针 A , B 重合,并有两种情况:a+(b−c)=b+(a−c)
1、若两链表 有 公共尾部 (即 c > 0 ) :指针 A , B 同时指向「第一个公共节点」node 。
2、若两链表 无 公共尾部 (即 c = 0 ) :指针 A , B 同时指向 null 。
因此返回 A 即可。
如下图所示,为 a = 6, b = 4, c = 2示例的算法执行过程。
复杂度分析:
- 时间复杂度 O(a + b): 最差情况下(即 ∣a−b∣=1 , c = 0),此时需遍历 a + b个节点。
- 空间复杂度 O(1): 节点指针 A , B 使用常数大小的额外空间。
代码:
public class Method{
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}
}
剑指 Offer 57. 和为 s 的两个数字
题目:输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
限制:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^6
解题思路:
利用 HashMap 可以通过遍历数组找到数字组合,时间和空间复杂度均为 O(N);
注意本题的 nums 是 排序数组 ,因此可使用 双指针法 将空间复杂度降低至 O(1)。
算法流程:
1、初始化: 双指针 i , j 分别指向数组 nums 的左右两端 (俗称对撞双指针)。
2、循环搜索: 当双指针相遇时跳出;
(1)计算和s=nums[i]+nums[j] ;
(2)若 s > target,则指针 j 向左移动,即执行 j=j−1;
(3)若 s < target,则指针 i 向右移动,即执行 i=i+1;
(4)若 s = target,立即返回数组 [nums[i],nums[j];
3、返回空数组,代表无和为 target 的数字组合。
正确性证明:
记每个状态为 S(i,j) ,即S(i,j)=nums[i]+nums[j] 。假设S(i,j)<target ,则执行i=i+1 ,即状态切换至S(i+1,j) 。
-
状态S(i,j) 切换至S(i+1,j) ,则会消去一行元素,相当于消去了状态集合 {S(i,i+1),S(i,i+2),…,S(i,j−2),S(i,j−1),S(i,j)} 。(由于双指针都是向中间收缩,因此这些状态之后不可能再遇到)。
-
由于 nums 是排序数组,因此这些 消去的状态 都一定满足 S(i, j) < target ,即这些状态都 不是解 。
-
结论: 以上分析已证明 “每次指针 i 的移动操作,都不会导致解的丢失” ,即指针 i 的移动操作是 安全的 ;同理,对于指针 j 可得出同样推论;因此,此双指针法是正确的。
复杂度分析: -
时间复杂度 O(N): N为数组 nums 的长度;双指针共同线性遍历整个数组。
-
空间复杂度 O(1): 变量 i, j 使用常数大小的额外空间。
代码:
class Method{
public int[] twoSum(int[] nums, int target) {
int i = 0, j = nums.length - 1;
while(i < j) {
int s = nums[i] + nums[j];
if(s < target) i++;
else if(s > target) j--;
else return new int[] { nums[i], nums[j] };
}
return new int[0];
}
}
总结:
今天又是下了一天的雨,当然学习也不能止步!接下来就是步入社会!互联网的世界说简单就俩字:“流量”;说复杂也复杂,不同的岗位,不同的需求,不同的代码,不同的实现方式!大型企业BATJ面试的要求高!当然,刚进入互联网的世界当中就先从中小型企业开始学习!于是就有正式工作和外包的区分!
但还没拿到毕业证的小伙伴们应该深有体会!大部分的企业都是要长期的程序员,没有毕业证就相当于还是实习生!社会上压榨的正是实习生!也不是实习生吃不了苦,而是实习生想找工作中小型企业根本就不给机会,大部分慢慢的就进入了外包公司,根本就接触不到公司内部的核心业务。后期去其他企业时,就会受到其他企业的歧视,所以学习的路并没有结束!反而学习的更多了!正所谓:“处处留心皆学问,人情练达即文章”!江湖险恶!正在找工作的程序员小伙伴们可要擦亮眼睛哦!一定要进入正规的中小型企业,尽量避免进入外包公司!接着就是不停的在正式的岗位上磨练,对项目经验绝对会有很大的帮助的!
最后,愿我们都能在各行各业中能够取得不同的成就,不负亲人、朋友、老师、长辈和国家的期望!能够用自身的所学知识为国家贡献出自己的一份力量!一起加油!
2021年5月15日夜