双指针的一些题
给跪了,重复指针我一直很菜,如果还涉及字符串就更不会了。。。字符串一生之敌😕
双指针一些练习题,正在摸索双指针的解题思路——————————
双指针优秀就优秀在,一旦可以用双指针做,那么时间复杂度就是O(n),空间复杂度就是O(1)。
27. 移除元素
难度简单
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100
双指针:
class Solution {
public int removeElement(int[] nums, int val) {
int len = nums.length;
//right是哨兵,left是有效数组的最终位置(不是最后一个元素,是最后一个元素下标+1)。找到第一个符合题意的下标开始,右指针不断赋给左指针直到右指针到达边界。
int right = 0,left = right;
for(;right < len;){ //遍历数组,只要==val就覆盖,只要!=val就right++
if(nums[right] == val){
while(right < len && nums[right] == val){
right++;
}
}
else{
nums[left] = nums[right];
left++;
right++;
}
}
return left;
}
}
双指针一般不要for()因为很容易误++了(for(int i = 0;i<len;i++)。
42. 接雨水
难度困难
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length0 <= n <= 3 * 1040 <= height[i] <= 105
一开始想到的是动态规划,后来看到了双指针解法:
class Solution {
public int trap(int[] height) {
//动态规划的意思是每次都左边最大值和右边最大值的较小值,减去当前高度,最后相加结果就是答案
//这题双指针是动态规划的优化版,left是待计算雨水的左边界,right是待计算雨水的右边界。
//然后由短板决定高低的原理,只要知道i以及i左边的最高高度比右边的小,当前i能接的雨水就等于leftMax-height[left]。
int len = height.length;
int left = 0,right = len - 1;
int leftMax = 0,rightMax = 0;
int res = 0;
while(left < right){//双指针不建议for!
leftMax = Math.max(leftMax,height[left]);
rightMax = Math.max(rightMax,height[right]);
if(height[left] < height[right]){ //height[left]和height[right]谁比较小就是当前进行到的i,而较高的那位是暂时不管的,毕竟你也无法确定中间会不会有更高的从而决定当前的接水高度,所以就先计算较矮的那一位(由于已经存在比它高的了所以用当前这个较矮的高度计算的雨水是一定能接到的)。
res += leftMax - height[left];
left++;
}
else{
res += rightMax - height[right];
right--;
}
}
return res;
}
}
动态规划解法,比较好理解:
class Solution {
public int trap(int[] height) {
//动态规划不一定要出现dp[][]然后最终结果是dp[0][n-1]之类,只要是动态维护数组就是动态规划
//leftMax[i]表示下标为i的左边最大高度(包括i本身),rightMax[i]同理。
int len = height.length;
if(len == 0){
return 0;
}
int[] leftMax = new int[len];
int[] rightMax = new int[len];
int res = 0;
leftMax[0] = height[0];
rightMax[len-1] = height[len-1];
for(int i = 1;i < len;i++){
leftMax[i] = Math.max(height[i],leftMax[i-1]);
}
for(int j = len-2;j >= 0;j--){
rightMax[j] = Math.max(height[j],rightMax[j+1]);
}
for(int i = 0;i < len;i++){
int temp = Math.min(leftMax[i],rightMax[i]);
res += temp - height[i];
}
return res;
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
难度中等
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"输出: 3解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
s.length <= 40000
注意:本题与主站 3 题相同:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
这题莽着做很容易没考虑全或者边界问题出错,所以建议用哈希表,有containsKey和get方法何乐而不为呢?
遍历字符串,一旦发现遍历过的字符串部分出现了当前遍历到的字符(用哈希表.containsKey方法发现),就检查res长度是否需要更新,同时更新无重复区间的左边界i(判断i和当前字符哈希表里最大下标,取较大者,因为i只会变大不会变小),以及哈希表里当前字符的最新下标(哈希表维护该字符最大的下标)。
class Solution {
public int lengthOfLongestSubstring(String s) {
int len = s.length();
if(len==0){
return 0;
}
if(len==1){
return 1;
}
//双指针
int j = 1,i = -1;//j是遍历指针,i是s[j]左边最靠近j的相同字符的下标
Map<String,Integer> dic = new HashMap<>();//记录每个字符的最后一个出现的下标
int res = 0;
dic.put(s.charAt(0)+"",0);
while(j < len){
if(dic.containsKey(s.charAt(j)+"")){
i = Math.max(i,dic.get(s.charAt(j)+""));//这不是为了重复取最右边,而是因为i只会增大不会减少,而dic这里没有删除元素,所以会有些已经超出[i,j]的重复元素也在dic里而且还与当前s.charAt(j)相同时,就不能更新i了,故应该取自己和dic中相同元素下标的max者。
}
dic.put(s.charAt(j)+"",j);//无论有没有containsKey,都得覆盖或者添加
res = res > j-i ? res : j-i;//无重复字符串是[i,j),长度是j-i不是j-i+1
j++;
}
return res;
}
}
java里也有char的包装类Character,可以直接作为HashMap<Character,Integer>这样的集合存在,其实也可以直接<String,Integer>存储。
这里使用了哈希表,所以查找的时间复杂度只有O(n)。
此题还有动态规划+哈希表的解法。
75. 颜色分类
难度中等
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
示例 1:
输入:nums = [2,0,2,1,1,0]输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]输出:[0,1,2]
示例 3:
输入:nums = [0]输出:[0]
示例 4:
输入:nums = [1]
输出:[1]
提示:
n == nums.length1 <= n <= 300nums[i]为0、1或2
话说这题都不用双指针的,直接一行sort或者自己写个排序算法搞定:
class Solution {
public void sortColors(int[] nums) {
Arrays.sort(nums);
}
}
然后想到只要将0全部挪到前面,2全部挪到后面即可。
这其实属于硬要双指针:
class Solution {
public void sortColors(int[] nums) {
//将所有的0交换到前面,将所有的2交换到后面————其实就是冒泡排序
//可以简化为:将所有的0交换到前面,1交换到较前面————原理还是冒泡排序。
int len = nums.length;
int ptr = 0;//ptr是当前要被交换的下标
for(int i = 0;i < len;i++){
if(nums[i] == 0){
int temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
ptr++; //保持当前0的最终位置+1
}
}
for(int i = 0;i < len;i++){
if(nums[i] == 1){
int temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
ptr++; //保持当前1的最终位置+1
}
}
}
}
234. 回文链表
难度简单
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2输出: false
示例 2:
输入: 1->2->2->1输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
想了许久还是想不出来,看了题解——原来只需要翻转后半部分链表然后两个指针同步比较是否一致就行了。
一开始怎么都想不到O(n)时间和O(1)空间的算法:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
//第一时间想到的是放到一个数组里,然后左右边界往中间靠拢,依次比较是否相等,一旦不等就是false,left和right相等时就true了。
//但是想要O(n)时间复杂度和O(1)空间复杂度实现,得原地双指针。
//然后就想先计算出链表长度,然后i对应len-i,左指针对应的右指针相等就行,不相等立马false。但是这样是O(n^2)。
//然后想到,先计算出链表长度,然后是偶数的话,先加到len/2;然后减去后面的,如果减到负数就不是回文串;如果是奇数,忽略那个中间值,加到len/2-1,再从len/2+1开始减,减到负数就false。但是卡在用例[1,3,2,4,3,2,1]
ListNode temp = head;
int len = 0;
while(temp != null){
len++;
temp = temp.next;
}
if(len == 1){
return true;
}
if(len == 2){
return head.val==head.next.val;
}
// System.out.println(len);
temp = head;
if(len % 2 == 0){
int sum = 0;
for(int i = 0;i < len/2;i++){
sum += temp.val;
temp = temp.next;
}
for(int j = len/2;j < len;j++){
sum -= temp.val;
temp = temp.next;
}
if(sum != 0){
return false;
}
else{
return true;
}
}
else{
int sum = 0;
for(int i = 0;i < len/2;i++){
sum += temp.val;
temp = temp.next;
}
temp = temp.next;
for(int j = len/2+1;j < len;j++){
sum -= temp.val;
temp = temp.next;
}
if(sum != 0){
return false;
}
else{
return true;
}
}
}
}
反转后半部分链表,然后从表头和表中间开始比较即可:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
//翻转后半部分然后同步比较
if(head == null){
return true;
}
// if(head.next == null){//只有1位
// return true;
// }
// if(head.next.next == null){//只有2位
// return head.val == head.next.val;
// }
// if(head.next.next.next == null){//只有3位
// return head.val == head.next.next.val;
// }
ListNode mediumNode = medium(head); //找到中间节点
ListNode right = reseverselist(mediumNode);//翻转后面链表
ListNode left = head;
while(right != null){
if(right.val != left.val){
return false;
}
right = right.next;
left = left.next;
}
return true;
}
//翻转链表
ListNode reseverselist(ListNode node){
ListNode res = new ListNode();//头结点,不存数据
ListNode temp = node;//要交换的节点
ListNode temp2 = node.next;//遍历指针
while(temp != null){
temp.next = res.next;
res.next = temp;
temp = temp2;
if(temp2 != null)
temp2 = temp2.next;
}
return res.next;
}
//找到单向链表的中间节点。也可以用计算出长度后直接len/2的循环找到那个节点。这里是快慢指针法。
//1 2 3 4返回3
ListNode medium(ListNode node){
ListNode quick = node;
ListNode slow = node;
while(quick != null && quick.next != null){
quick = quick.next.next;
slow = slow.next;
}
return slow;
}
}
翻转链表原理是将每次待翻转链表的节点取出插入到新表的头结点后面,这样O(n)时间复杂度,O(1)空间复杂度。
找到中间节点原理是快指针慢指针一开始一个位置,然后快指针走两步慢指针走一步,快指针到达结尾时慢指针就到达中间节点了,链表长度是偶数时慢指针就到达后半部分的第一个节点,是奇数时就是那个中间节点。这样O(1)空间复杂度,O(n)时间复杂度。
另外,反转链表并不是原地翻转,而是将原来的链表断开了要翻转的部分,使其后面的后面是null(保留了一位节点),然后新建了一个节点反向指向链表,从而实现了链表翻转(具体看reseverselist代码就知道了)。

76. 最小覆盖子串
难度困难
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"输出:"BANC"
示例 2:
输入:s = "a", t = "a"输出:"a"
提示:
1 <= s.length, t.length <= 105s和t由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
一开始的暴力思路:遍历s,一旦有s.contains(t.charAt(i))就开始扫描t,对每个t的字符检查是否有s.contains(i),并记录下标,如果匹配串t都能在s中找到,就计算这些下标能给出的最大差值(找出最大者,减去最小者)。这样无法做到O(n)时间——但是看了题解好像也没有O(n)解法???
题解是给出滑动窗口(也是双指针,但是双指针不一定是滑动窗口):只要滑动窗口没有包含t全部元素,右指针就右移;只要滑动窗口包含了全部元素就记录窗口长度,左指针右移。直至右指针到达右边界。但是要保证O(n)的时间,就不能每次用扫描判断是否包含全部元素,而要借助哈希表空间换时间,哈希表记录字符和滑动窗口之间字符的出现次数(次数是为了防止窗口内重复字符出现误删,这也是不能用集合的原因)。
双指针一生之敌。
141. 环形链表
难度简单
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入:head = [1,2], pos = 0输出:true解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1输出:false解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104] -105 <= Node.val <= 105pos为-1或者链表中的一个 有效索引 。
龟兔赛跑判断环形链表很妙:兔子是快指针,乌龟是慢指针,一开始快指针就在慢指针前面(quick = slow.next)——这样可以避免因为输入链表长度小于2导致无法进入后面要说的循环——然后快指针走两步慢指针走一步,不断循环知道快指针的下一个或者快指针为null,如果是环链表就永远不会为null,而快指针迟早会多跑几圈“追上”慢指针,所以有相等的一天,此时返回true即可,而为null肯定是没有环的情况,因为环压根不会退出循环。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
//题解的龟兔赛跑解法是真的妙
if(head == null){
return false;
}
// if(head.next == null){
// return false;
// }
// if(head.next.next == null){
// return head.next.next == head;
// }
ListNode slow = head,quick = slow.next;
while(quick != null && quick.next != null){
if(quick == slow){
return true;
}
quick = quick.next.next;
slow = slow.next;
}
return false;
}
}
O(n)时间,O(1)空间,很完美。
注意java对象都是引用比较,除非你重写了equals和hashCode方法。
这题思路简记为:永远跑的比你快的人要和你相遇,只能是在环形跑道上。
142. 环形链表 II
难度中等
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
- 你是否可以使用
O(1)空间解决此题?
示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]内 -105 <= Node.val <= 105pos的值为-1或者链表中的一个有效索引
这题用集合就很容易做出来,直接存储各个节点(注意不是存储节点值),一旦发现指针遍历到集合已经有的节点时就是答案。
但是这题进阶说要用O(1)的空间解出来。
这题要理解快慢指针的数学规律,当快慢指针处于相同起始点时,快指针走的路永远都是慢指针的两倍。
那么就有:a+n(b+c)+b = 2(a+b)
从而得出:a = (n-1)(b+c)+c
也就是说a是c加上若干环长度。那么相遇时(b)设置一个初始位置在表头的指针ptr,那么让快慢指针继续循环直到慢指针(因为慢指针每次都只走一步,a和c是1:1的关系)跟ptr相遇,那么ptr就是答案。
633. 平方数之和
难度中等
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c 。
示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
示例 2:
输入:c = 3
输出:false
示例 3:
输入:c = 4
输出:true
示例 4:
输入:c = 2
输出:true
示例 5:
输入:c = 1
输出:true
提示:
0 <= c <= 2^31 - 1
这题是不能用常规模板的双指针:
class Solution {
public boolean judgeSquareSum(int c) {
int left = 0,right = (int)Math.sqrt(c);
while(left <= right){
int curr = left * left + right * right;
if(curr < c){
left++;
}
if(curr == c){
return true;
}
if(curr > c){
right--;
}
}
return false;
}
}
【待完结】

浙公网安备 33010602011771号