LeetCode题解:双指针专栏
自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。
参考教材: LeetCode 101 - A LeetCode Grinding Guide
参考资料: LeetCode社区官方提供的思路/题解 以及 评论区/题解区各路大神提供的思路/答案
目录标题
- LeetCode题解:双指针专栏
- [167. 两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
- [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/)
- [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
- [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)
- [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)
- [633. 平方数之和](https://leetcode-cn.com/problems/sum-of-square-numbers/)
- [680. 验证回文字符串 Ⅱ](https://leetcode-cn.com/problems/valid-palindrome-ii/)
167. 两数之和 II - 输入有序数组
难度: 简单
给定一个已按照 升序排列 的整数数组 numbers
,请你从数组中找出两个数满足相加之和等于目标数 target
。
函数应该以长度为 2
的整数数组的形式返回这两个数的下标值*。*numbers
的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length
。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
题解:
因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最 小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。 如果两个指针指向元素的和等于给定值,那么它们就是我们要的结果。如果两个指针指向元 素的和小于给定值,我们把左边的指针右移一位,使得当前的和增加一点。如果两个指针指向元 素的和大于给定值,我们把右边的指针左移一位,使得当前的和减少一点。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left=0,right=numbers.length-1;
while (left<right){
if (numbers[left]+numbers[right]==target){
return new int[]{left + 1, right + 1};
}
if (numbers[left]+numbers[right]<target){
left++;
}else {
right--;
}
}
return new int[]{-1,-1};
}
}
88. 合并两个有序数组
难度: 简单
给你两个有序整数数组 nums1
和 nums2
,请你将 nums2
合并到 nums1
中*,*使 nums1
成为一个有序数组。
初始化 nums1
和 nums2
的元素数量分别为 m
和 n
。你可以假设 nums1
的空间大小等于 m + n
,这样它就有足够的空间保存来自 nums2
的元素。
示例:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
题解:
- 方法一
一般而言,对于有序数组可以通过 双指针法 达到O(n + m)O(n+m)的时间复杂度。
最直接的算法实现是将指针p1 置为 nums1的开头, p2为 nums2的开头,在每一步将最小值放入输出数组中。
由于 nums1 是用于输出的数组,需要将nums1中的前m个元素放在其他地方,也就需要 O(m) 的空间复杂度。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] nums1_copy=new int[m];
System.arraycopy(nums1,0,nums1_copy,0,m);
int p1=0;
int p2=0;
int p=0;
while ( (p1<m) && (p2<n)){
nums1[p++]=(nums1_copy[p1]<nums2[p2])?nums1_copy[p1++]:nums2[p2++];
}
if (p1<m){
System.arraycopy(nums1_copy,p1,nums1,p1+p2,m+n-p1-p2);
}
if (p2<n){
System.arraycopy(nums2,p2,nums1,p1+p2,m+n-p1-p2);
}
}
}
- 方法二
方法一已经取得了最优的时间复杂度O(n+m),但需要使用额外空间。这是由于在从头改变nums1的值时,需要把nums1中的元素存放在其他位置。
如果我们从结尾开始改写 nums1 的值又会如何呢?这里没有信息,因此不需要额外空间。
这里的指针 p 用于追踪添加元素的位置。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1=m-1;
int p2=n-1;
int p=m+n-1;
while (p1>=0 && p2>=0){
nums1[p--]=(nums1[p1]<nums2[p2])?nums2[p2--]:nums1[p1--];
}
System.arraycopy(nums2,0,nums1,0,p2+1);
}
}
141. 环形链表
难度: 简单
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true
。 否则,返回 false
。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
题解:
经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast,slow;
fast=slow=head;
while (fast!=null && fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if (fast==slow){
return true;
}
}
return false;
}
}
142. 环形链表 II
难度: 中等
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。注意,pos
仅仅是用于标识环的情况,并不会作为参数传递到函数中。
**说明:**不允许修改给定的链表。
进阶:
- 你是否可以使用
O(1)
空间解决此题?
示例:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
题解:
当快慢指针相遇时,让其中任一个指针重新指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢?
第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。
设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。
巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。
所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。
class Solution {
public ListNode detectCycle(ListNode head) {
boolean flag=false;//标记是否有环
ListNode fast,slow;
fast=slow=head;
while (fast!=null && fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if (fast==slow){
flag=true;
break;
}
}
if (!flag){
return null;//没有环
}
slow=head;
while (slow!=fast){
fast=fast.next;
slow=slow.next;
}
return slow;
}
}
76. 最小覆盖子串
难度: 困难
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
**注意:**如果 s
中存在这样的子串,我们保证它是唯一的答案。
示例:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
题解:
滑动窗口算法,利用左右指针维护窗口中的数据,窗口区间是左闭右开区间,[left,right)。
滑动窗口算法通用流程:
初始化:初始化左右指针,left, right = 0, 0;初始化窗口window={},最初时window不包含数据;
寻找可行解:不断扩大窗口的右边界,右指针right+=1,将新数据加入窗口,并检测此时窗口中的字串是否满足题目要求;
优化可行解:当窗口包含的字串满足题目要求时,开始收缩窗口的左边界,left+=1,直至不再满足题目要求;
滑动窗口过程中,注意数据的实时更新,找到保存结果的位置。
class Solution {
public String minWindow(String s, String t) {
HashMap<Character,Integer> need = new HashMap();
HashMap<Character,Integer> window = new HashMap();
//need存放的不重复的字符出现的次数
for(char c:t.toCharArray())
need.put(c,need.getOrDefault(c,0)+1);
//left,right 表示滑动窗口的左右指针
int left = 0 , right = 0;
//valid表示是否满足了t中的字符,不算重复的
int valid = 0;
//记录最小覆盖子串的起始索引及长度
int start = 0 , len = Integer.MAX_VALUE;
while(right < s.length()){
char c = s.charAt(right);
right++;
//判断取出的字符是否在需要的Map中
if(need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(need.get(c)))
valid++;
}
//判断是否需要收缩(即已经找到了合适的覆盖串)
while(valid == need.size()){
//更新最小覆盖子串
if(right - left < len){
start = left;
len = right - left;
}
char c1 = s.charAt(left);
//左移窗口
left++;
//进行窗口内数据的一系列更新
//如果当前要移动的字符是包含在need中,我们需要进行讨论,如果该字符的次数刚好与我们需要的次数相等,则valid--,并同时更新window中这个值出现的次数
if(need.containsKey(c1)){
if(window.get(c1).equals(need.get(c1)))
valid--;
window.put(c1,window.getOrDefault(c1,0)-1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
633. 平方数之和
难度: 中等
给定一个非负整数 c
,你要判断是否存在两个整数 a
和 b
,使得 a2 + b2 = c
。
示例:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
题解:
对于从 left=0 到 right=sqrt© 之间使用双指针,逐渐找到可行解。
class Solution {
public boolean judgeSquareSum(int c) {
int l=0;
int r=(int) Math.sqrt(c);
while (l<=r){
if (l*l==c-r*r){
return true;
}
if (l*l<c-r*r){
l++;
}else {
r--;
}
}
return false;
}
}
680. 验证回文字符串 Ⅱ
难度: 简单
给定一个非空字符串 s
,最多删除一个字符。判断是否能成为回文字符串。
示例:
输入: "aba"
输出: True
题解:
双指针+简单递归
class Solution {
public boolean validPalindrome(String s) {
int left = 0, right = s.length() - 1, count = 0;
char lc, rc;
while (left <= right) {
if (s.charAt(left) != s.charAt(right)) {
return validPalindrome(s,left+1,right) || validPalindrome(s,left,right-1);
}
left++;
right--;
}
return true;
}
public boolean validPalindrome(String s,int left,int right) {
while (left<right){
if (s.charAt(left) != s.charAt(right)) {
return false;//再一次出现不等
}
left++;
right--;
}
return true;
}
}
作者:耿鬼不会笑
时间:2021年2月