文章目录
141.环形链表
给你一个链表的头节点head
,判断链表中是否有环.
如果链表中有某个节点,可以通过连续跟踪next
指针再次到达,则链表中存在环.为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从0开始)。如果pos
是-1
,则在该链表中没有环.注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。如果链表中存在环,则返回true
。否则,返回false
。
进阶:
你能用O(1)
空间解决此题吗?
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
- 思路:快慢指针
public boolean hasCycle(ListNode head) {
boolean flag = false;
if (head == null || head.next == null) {
return flag;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (fast != null) {
if (slow == fast) {
flag = true;
break;
}
}
}
return flag;
}
142.环形链表II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。
为了表示给定链表中的环,我们使用整数pos
来表示链表尾连接到链表中的位置(索引从0开始)。 如果pos
是-1
,则在该链表中没有环。注意,pos
仅仅是用于标识环的情况,并不会作为参数传递到函数中。
**说明:**不允许修改给定的链表。
进阶:
你是否可以使用O(1)
空间解决此题?
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
public ListNode detectCycle(ListNode head) {
ListNode fast = head, 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 (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
环形链表的数学证明:
设链表长度为
n
n
n,环的长度为
m
m
m,则
n
−
m
n-m
n−m个节点从head
开始,
m
m
m个节点从相遇点开始,设现在有快慢指针slow
和fast
,初始都指向head
,假设他们在某点重合,这个点与相遇点之间一共有
c
c
c个节点.
接下来证明他们一定可以重合:
设slow
走了
x
x
x步,那么fast
就走了
2
x
2x
2x步,他们应该满足下面的等式
{
x
=
c
+
(
n
−
m
)
−
1
+
k
1
m
2
x
=
c
+
(
n
−
m
)
−
1
+
k
2
m
\left\{\begin{aligned} x& = c+(n-m)-1+k_1m\\ 2x & = c+(n-m)-1+k_2m \\ \end{aligned}\right.
{x2x=c+(n−m)−1+k1m=c+(n−m)−1+k2m
其中
k
1
,
k
2
∈
N
k_1,k_2\in\mathbb{N}
k1,k2∈N.
化简上面的式子就可以得到:
{
x
=
(
k
1
−
k
2
)
m
n
+
c
−
1
=
(
k
2
−
2
k
1
+
1
)
m
\left\{\begin{aligned} x&=(k_1-k_2)m\\ n+c-1&=(k_2-2k_1+1)m \end{aligned}\right.
{xn+c−1=(k1−k2)m=(k2−2k1+1)m
注意到,
k
1
,
k
2
,
c
k_1,k_2,c
k1,k2,c是未知数,只要他们存在且符合逻辑,就代表两个指针一定可以重合.
首先看第一个等式,当 k 1 , k 2 k_1,k_2 k1,k2确认的时候,由于 m m m是已知数,所以 x x x可以直接确定.
然后看第二个等式,由于 n + c − 1 n+c-1 n+c−1一定是大于 n n n的,所以一定大于 m m m.那么这就代表我们可以通过调整 k 1 , k 2 , c k_1,k_2,c k1,k2,c的值让右边等于左边!举一个很简单的例子,令 k 2 = 3 , k 1 = 0 k_2=3,k_1=0 k2=3,k1=0,那么右边就变成了 4 m 4m 4m,这代表快指针已经走了4圈环,而慢指针一圈都没有走完!那么这种情况是合理的吗?答案:
只要能够取到一个合适的 c c c就是合理的!( 0 < c ≤ m 0<c\le m 0<c≤m).
综上所诉,两个指针是一定能够相遇的!这就解释了第一个问题.
接下来我们来解释第二个问题,首先还是考虑下面这个式子
x
=
(
k
1
−
k
2
)
m
x=(k_1-k_2)m
x=(k1−k2)m
可以注意到什么?如果x再加上
(
n
−
m
)
(n-m)
(n−m),那么就等于
n
+
k
3
m
n+k_3 m
n+k3m!!!
k
3
k_3
k3是一个整数,是多少不重要,重要的是:这说明此时此刻slow
指针走了一整个链表的长度加上若干圈环的长度!!!
假如说链表是有环的,那么slow
指针在走了
n
n
n步以后,必然会出现在相遇点!!!之后每次再走
m
m
m步,都会出现在相遇点!!!
而如果此时此刻一点指针从head
节点开始出发,那么在它走了
n
−
m
n-m
n−m步之后,是不是也会出现在相遇点?!
所以两个指针一定会在 n − m n-m n−m步之后重合!!!
2.两数相加
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
注意几个if判断的条件,不要随意更改
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null;
int count = 0;
while (l1 != null || l2 != null) {
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
int sum = n1 + n2 + count ;
if (head == null) {
head = tail = new ListNode(sum % 10);
} else {
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
count = sum / 10;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
if (count > 0) {
tail.next = new ListNode(count);
}
return head;
}
19.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
**进阶:**你能尝试使用一趟扫描实现吗?
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head.next == null){
return null;
}
ListNode dummy = new ListNode(0,head);
ListNode slow = dummy;
ListNode fast = head;
for(int i = 0;i<n;i++){
fast = fast.next;
}
while(fast!=null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
11.盛水最多的容器
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
public int maxArea(int[] height) {
int i=0,j=height.length - 1,res=0;
while(i<j){
res = height[i]<height[j]?
Math.max(res,(j-i)*height[i++]):
Math.max(res,(j-i)*height[j--]);
}
return res;
}
15.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for(int k=0;k<nums.length-2;k++){
if(nums[k] > 0) break;
if(k>0 && nums[k]==nums[k-1]) continue;
int i=k+1,j=nums.length-1;
while(i<j){
int sum = nums[k]+nums[i]+nums[j];
if(sum<0){
while(i < j && nums[i] == nums[++i]);
}else if(sum > 0){
while(i < j && nums[j] == nums[--j]);
}else{
res.add(new ArrayList<Integer>(Arrays.asList(nums[k],nums[i],nums[j])));
while(i < j && nums[i] == nums[++i]);
while(i < j && nums[j] == nums[--j]);
}
}
}
return res;
}
138.随机链表的复制
给定一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
for (Node node = head; node != null; node = node.next.next) {
Node nodeNew = new Node(node.val);
nodeNew.next = node.next;
node.next = nodeNew;
}
for (Node node = head; node != null; node = node.next.next) {
Node nodeNew = node.next;
nodeNew.random = (node.random != null) ? node.random.next : null;
}
Node headNew = head.next;
for (Node node = head; node != null; node = node.next) {
Node nodeNew = node.next;
node.next = node.next.next;
nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
}
return headNew;
}
53. 最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组
是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
- 思路:用动态规划的思想,
定义状态(定义子问题)
dp[i]
:表示以nums[i]
结尾的连续子数组的最大和.
状态转移方程
根据状态的定义,有dp[i]=dp[i-1]+nums[i]
但是dp[i-1]
有可能是负数,此时nums[i]
加上前面的dp[i-1]
值不会变大,所以dp[i]=nums[i]
否则dp[i-1]
为正数,那么dp[i]=dp[i-1]+nums[i]
总结来说那就是:
d
p
[
i
]
=
max
{
n
u
m
s
[
i
]
,
d
p
[
i
−
1
]
+
n
u
m
s
[
i
]
}
dp[i]=\max\{nums[i],dp[i-1]+nums[i]\}
dp[i]=max{nums[i],dp[i−1]+nums[i]}
初始值
显然dp[0]=nums[0]
public int maxSubArray(int[] nums) {
int pre=0;
int res=nums[0];
for(int num:nums){
pre=Math.max(pre+num,num);
res=Math.max(res,pre);
}
return res;
}
56.合并区间
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
- 思路:
首先对原来的数组的第一个数字进行排序,这里排序需要用到比较器
,Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]);
的含义就是根据数组第[0]个元素进行降序排序.
排序完成后就是建立答案的集合,如果集合为空,就先将数组第一个元素放进去,如果不为空就进行比较:若当前区间的左边比答案最右边区间的右边还大(例如interval为[3,5],但是res最右边的元素为[1,2]),那么就添加这个区间进入集合
否则说明区间一定有交叉(例如interval为[2,4],res最右边元素为[1,3]),此时就更新res区间,右边元素等于max(3,4)
,最后返回res即可
public int[][] merge(int[][] intervals) {
if (intervals.length < 2) {
return intervals;
}
Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]);
List<int[]> res = new ArrayList<>();
for (int[] interval : intervals) {
if (res.size() == 0 || res.get(res.size() - 1)[1] < interval[0]) {
res.add(interval);
} else {
res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], interval[1]);
}
}
return res.toArray(new int[res.size()][2]);
}
189.轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
方法一:使用额外的数组(时间复杂度为O(n),空间复杂度为O(n))
public void rotate(int[] nums,int k){
int n = nums.length;
int[] newArr = new int[n];
for(int i=0;i<n;i++){
newArr[(i+k)%n] = nums[i];
}
System.arraycopy(newArr,0,nums,0,n);
}
方法二:数组翻转(时间复杂度为O(n),空间复杂度为O(1))
public void reverse(int[] nums,int start,int end){
while(start<end){
int temp = start;
nums[start] = nums[end];
nums[end] = temp;
start++;
end++;
}
}
public void rotate(int[] nums,int k){
k = k%nums.length;
reverse(nums,0,nums.length-1);
reverse(nums,0,k-1);
reverse(nums,k,nums.length-1);
}
238.除自身以外数组的乘积
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在O(n)
时间复杂度内完成此题。空间复杂度可以做到O(1)
吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)
- 思路:
我们不必将所有数字的乘积除以给定索引处的数字得到相应的答案,而是利用索引左侧所有数字的乘积和右侧所有数字的乘积(即前缀与后缀)相乘得到答案。具体的答案我们可以直接在输出数组上面运算.
扫描一次不行,我们扫描两次!
public int[] productExceptSelf(int[] nums){
int length = nums.length;
int[] ans = new int[length];
ans[0] = 1;
//ans[i]表示索引i左侧所有元素的乘积
for(int i=1;i<length;i++){
ans[i]=ans[i-1]*nums[i-1];
}
//R为右侧所有元素的乘积
int R=1;
for(int i=length-1;i>=0;i--){
ans[i]=ans[i]*R;
R = R*nums[i];
}
return ans;
}