数组双指针直接秒杀七道题目
学算法认准 labuladong
后台回复打卡参与刷题挑战
读完本文,可以去力扣解决如下题目:
26. 删除有序数组中的重复项(简单)
83. 删除排序链表中的重复元素(简单)
167. 两数之和 II - 输入有序数组(中等)
27. 移除元素(简单)
283. 移动零(简单)
344. 反转字符串(简单)
5. 最长回文子串(中等)
双指针技巧在处理数组和链表相关问题时经常用到,主要分为两类:左右指针和快慢指针。
所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。
对于单链表来说,大部分技巧都属于快慢指针,前文 单链表的六大解题套路 都涵盖了,比如链表环判断,倒数第K
个链表节点等问题,它们都是通过一个fast
快指针和一个slow
慢指针配合完成任务。
在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧,本文主要讲数组相关的双指针算法。
一、快慢指针技巧
数组问题中比较常见且难度不高的的快慢指针技巧,是让你原地修改数组。
比如说看下力扣第 26 题「删除有序数组中的重复项」,让你在有序数组去重:
函数签名如下:
int removeDuplicates(int[] nums);
简单解释一下什么是原地修改:
如果不是原地修改的话,我们直接 new 一个int[]
数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。
但是现在题目让你原地删除,不允许 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。
由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难。但如果毎找到一个重复元素就立即原地删除它,由于数组中删除元素涉及数据搬移,整个时间复杂度是会达到O(N^2)
。
高效解决这道题就要用到快慢指针技巧:
我们让慢指针slow
走在后面,快指针fast
走在前面探路,找到一个不重复的元素就赋值给slow
并让slow
前进一步。
这样,就保证了nums[0..slow]
都是无重复的元素,当fast
指针遍历完整个数组nums
后,nums[0..slow]
就是整个数组去重之后的结果。
看代码:
int removeDuplicates(int[] nums) {if (nums.length == 0) {return 0;}int slow = 0, fast = 0;while (fast < nums.length) {if (nums[fast] != nums[slow]) {slow++;// 维护 nums[0..slow] 无重复nums[slow] = nums[fast];}fast++;}// 数组长度为索引 + 1return slow + 1;
}
算法执行的过程如下 GIF 图:
再简单扩展一下,看看力扣第 83 题「删除排序链表中的重复元素」,如果给你一个有序的单链表,如何去重呢?
其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已,你对照着之前的代码来看:
ListNode deleteDuplicates(ListNode head) {if (head == null) return null;ListNode slow = head, fast = head;while (fast != null) {if (fast.val != slow.val) {// nums[slow] = nums[fast];slow.next = fast;// slow++;slow = slow.next;}// fast++fast = fast.next;}// 断开与后面重复元素的连接slow.next = null;return head;
}
算法执行的过程请看下面这个 GIF:
这里可能有读者会问,链表中那些重复的元素并没有被删掉,就让这些节点在链表上挂着,合适吗?
这就要探讨不同语言的特性了,像 Java/Python 这类带有垃圾回收的语言,可以帮我们自动找到并回收这些「悬空」的链表节点的内存,而像 C++ 这类语言没有自动垃圾回收的机制,确实需要我们编写代码时手动释放掉这些节点的内存。
不过话说回来,就算法思维的培养来说,我们只需要知道这种快慢指针技巧即可。
除了让你在有序数组/链表中去重,题目还可能让你对数组中的某些元素进行「原地删除」。
比如力扣第 27 题「移除元素」,看下题目:
函数签名如下:
int removeElement(int[] nums, int val);
题目要求我们把nums
中所有值为val
的元素原地删除,依然需要使用快慢指针技巧:
如果fast
遇到值为val
的元素,则直接跳过,否则就赋值给slow
指针,并让slow
前进一步。
这和前面说到的数组去重问题解法思路是完全一样的,就不画 GIF 了,直接看代码:
int removeElement(int[] nums, int val) {int fast = 0, slow = 0;while (fast < nums.length) {if (nums[fast] != val) {nums[slow] = nums[fast];slow++;}fast++;}return slow;
}
注意这里和有序数组去重的解法有一个细节差异,我们这里是先给nums[slow]
赋值然后再给slow++
,这样可以保证nums[0..slow-1]
是不包含值为val
的元素的,最后的结果数组长度就是slow
。
实现了这个removeElement
函数,接下来看看力扣第 283 题「移动零」:
给你输入一个数组nums
,请你原地修改,将数组中的所有值为 0 的元素移到数组末尾,函数签名如下:
void moveZeroes(int[] nums);
比如说给你输入nums = [0,1,4,0,2]
,你的算法没有返回值,但是会把nums
数组原地修改成[1,4,2,0,0]
。
结合之前说到的几个题目,你是否有已经有了答案呢?
题目让我们将所有 0 移到最后,其实就相当于移除nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
所以我们可以复用上一题的removeElement
函数:
void moveZeroes(int[] nums) {// 去除 nums 中的所有 0,返回不含 0 的数组长度int p = removeElement(nums, 0);// 将 nums[p..] 的元素赋值为 0for (; p < nums.length; p++) {nums[p] = 0;}
}// 见上文代码实现
int removeElement(int[] nums, int val);
到这里,原地修改数组的这些题目就已经差不多了。数组中另一大类快慢指针的题目就是「滑动窗口算法」。
我在另一篇文章 滑动窗口算法核心框架详解 给出了滑动窗口的代码框架:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0; while (right < s.size()) {char c = s[right];// 右移(增大)窗口right++;// 进行窗口内数据的一系列更新while (window needs shrink) {char d = s[left];// 左移(缩小)窗口left++;// 进行窗口内数据的一系列更新}}
}
具体的题目本文就不重复了,这里只强调滑动窗口算法的快慢指针特性:
left
指针在后,right
指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。
二、左右指针的常用算法
1、二分查找
我在另一篇文章 二分查找框架详解 中有详细探讨二分搜索代码的细节问题,这里只写最简单的二分算法,旨在突出它的双指针特性:
int binarySearch(int[] nums, int target) {// 一左一右两个指针相向而行int left = 0, right = nums.length - 1;while(left <= right) {int mid = (right + left) / 2;if(nums[mid] == target)return mid; else if (nums[mid] < target)left = mid + 1; else if (nums[mid] > target)right = mid - 1;}return -1;
}
2、两数之和
看下力扣第 167 题「两数之和 II」:
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节left
和right
就可以调整sum
的大小:
int[] twoSum(int[] nums, int target) {// 一左一右两个指针相向而行int left = 0, right = nums.length - 1;while (left < right) {int sum = nums[left] + nums[right];if (sum == target) {// 题目要求的索引是从 1 开始的return new int[]{left + 1, right + 1};} else if (sum < target) {left++; // 让 sum 大一点} else if (sum > target) {right--; // 让 sum 小一点}}return new int[]{-1, -1};
}
我在另一篇文章 一个函数秒杀所有 nSum 问题 中也运用类似的左右指针技巧给出了nSum
问题的一种通用思路,这里就不做赘述了。
3、反转数组
一般编程语言都会提供reverse
函数,其实这个函数的原理非常简单,力扣第 344 题「反转字符串」就是类似的需求,让你反转一个char[]
类型的字符数组,我们直接看代码吧:
void reverseString(char[] s) {// 一左一右两个指针相向而行int left = 0, right = s.length - 1;while (left < right) {// 交换 s[left] 和 s[right]char temp = s[left];s[left] = s[right];s[right] = temp;left++;right--;}
}
4、回文串判断
首先明确一下,回文串就是正着读和反着读都一样的字符串。
比如说字符串aba
和abba
都是回文串,因为它们对称,反过来还是和本身一样;反之,字符串abac
就不是回文串。
现在你应该能感觉到回文串问题和左右指针肯定有密切的联系,比如让你判断一个字符串是不是回文串,你可以写出下面这段代码:
boolean isPalindrome(String s) {// 一左一右两个指针相向而行int left = 0, right = s.length() - 1;while (left < right) {if (s.charAt(left) != s.charAt(right)) {return false;}left++;right--;}return true;
}
那接下来我提升一点难度,给你一个字符串,让你用双指针技巧从中找出最长的回文串,你会做吗?
这就是力扣第 5 题「最长回文子串」:
函数签名如下:
String longestPalindrome(String s);
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。所以我们可以先实现这样一个函数:
// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
String palindrome(String s, int l, int r) {// 防止索引越界while (l >= 0 && r < s.length()&& s.charAt(l) == s.charAt(r)) {// 双指针,向两边展开l--; r++;}// 返回以 s[l] 和 s[r] 为中心的最长回文串return s.substring(l + 1, r);}
这样,如果输入相同的l
和r
,就相当于寻找长度为奇数的回文串,如果输入相邻的l
和r
,则相当于寻找长度为偶数的回文串。
那么回到最长回文串的问题,解法的大致思路就是:
for 0 <= i < len(s):找到以 s[i] 为中心的回文串找到以 s[i] 和 s[i+1] 为中心的回文串更新答案
翻译成代码,就可以解决最长回文子串这个问题:
String longestPalindrome(String s) {String res = "";for (int i = 0; i < s.length(); i++) {// 以 s[i] 为中心的最长回文子串String s1 = palindrome(s, i, i);// 以 s[i] 和 s[i+1] 为中心的最长回文子串String s2 = palindrome(s, i, i + 1);// res = longest(res, s1, s2)res = res.length() > s1.length() ? res : s1;res = res.length() > s2.length() ? res : s2;}return res;
}
你应该能发现最长回文子串使用的左右指针和之前题目的左右指针有一些不同:之前的左右指针都是从两端向中间相向而行,而回文子串问题则是让左右指针从中心向两端扩展。
不过这种情况也就回文串这类问题会遇到,所以我也把它归为左右指针了。
到这里,数组相关的双指针技巧就全部讲完了,希望大家以后遇到类似的算法问题时能够活学活用,举一反三。
————————————
本文就讲到这里,后台回复「目录」可查看精选文章目录,回复「PDF」可下载最新的刷题三件套,回复「打卡」可参与刷题打卡活动。更多高质量课程见公众号菜单!
数组双指针直接秒杀七道题目相关推荐
- LeetCode:数组刷题(17道经典题目)
LeetCode 数组刷题(17道经典题目) 本文带来的是以数组为主题的经典题目,主要实现是C++,部分题目也用Python实现了. 704. 二分查找 35.搜索插入位置 34. 在排序数组中查找元 ...
- c语言超级经典400道题目,C语言超级经典400道题目.doc
C语言超级经典400道题目1.C语言程序的基本单位是____ A) 程序行 B) 语句 C) 函数 D) 字符.C.1 2.C语言程序的三种基本结构是____ A.顺序结构,选择结构,循环结构 B.递 ...
- vb treeview 展开子节点_详解最长公共子序列问题,秒杀三道动态规划题目
学算法认准 labuladong 后台回复进群一起力扣? 读完本文,可以去力扣解决如下题目: 1143.最长公共子序列(Medium) 583. 两个字符串的删除操作(Medium) 712.两个字符 ...
- 数组双指针之快慢指针
数组双指针 数组双指针分为两类: 左右双指针:左指针在最左侧,右指针在最右侧,它们相向而行 快慢双指针:在单链表中经常使用fast.slow指针去判断链表中是否成环.单链表的终点.单链表倒数第k个结点 ...
- HDUnbsp;动态规划(46道题目)倾…
原文地址:HDU 动态规划(46道题目)倾情奉献作者:奔跑的乌龟 Robberies OJ地址: http://acm.hdu.edu.cn/showproblem.php?pid=2955 背包;第 ...
- hdu 动态规划(46道题目)倾情奉献~ 【只提供思路与状态转移方程】(转)
HDU 动态规划(46道题目)倾情奉献~ [只提供思路与状态转移方程] Robberies http://acm.hdu.edu.cn/showproblem.php?pid=2955 背包 ...
- 1数组中重复的数字-面试题目3
题外话:找算法,在线编程在面试中占比50%. 听说上面的题外话,依旧重拾艰难困苦之心,翻开剑指offer按照题目类型以及随机抽取,还是决定开始好好的看看剑指offer的在线编程66道题.没有信心的跟我 ...
- 第一道题目:一个美国人在菜市场上做生意。第一次,8美元买了一只鸡,9美元卖掉了;第二次,10美元买了同样的一只鸡,11美元又卖掉了。
第一道题目:一个美国人在菜市场上做生意.第一次,8美元买了一只鸡,9美元卖掉了:第二次,10美元买了同样的一只鸡,11美元又卖掉了.那么,这个美国人到底是亏了,还是赚了?如果亏了,应该是亏多少?如果赚 ...
- 【疯狂诗词大会小程序2.0】功能模块+前端+诗词答题小程序+内置数千道题目+开箱即用
源码简介与安装说明: 模块介绍: 诗词答题小程序,支持单项选择题.文字线索题.看图猜诗词.读诗句猜谜等题目类型. 内置数千道题目,开箱即用.随机出题,先易后难. 诗词同步学,每一道诗题都配备了优质的诗 ...
最新文章
- python是动态编程语言吗-python是一种跨平台、开源、免费的高级动态编程语言,对么...
- 第五章:系统困境之 你的努力忽略了关键限制因素
- jzoj4282-[NOIP2015模拟10.29B组]平方数游戏【构造】
- android aoa 串口,沁恒股份USB Android AOA转接概述
- mysql数据库比较,各数据库不同之处
- Java进阶篇(六)——Swing程序设计(上),java面试题,java基础笔试题,BAT
- java让弹窗在最上层_layer弹出层显示在top顶层的方法
- Lumen开发:结合Redis实现消息队列(3)
- BeanFactory和ApplicationContext的异同
- (一文读懂社交网络分析(附应用、前沿、学习资源)学习笔记)
- 中台架构的未来在哪—开放式架构
- Spring Cloud Euraka( 服务注册中心)
- mp4 box linux,MP4Box
- 计算机医学应用的前景,未来这3大专业,不仅就业前景广阔,而且不容易被外行人轻易取代...
- 浅析Trafodion体系结构
- 硬件工程师入门基础知识(一)基础元器件认识(一)
- Python中使用正则表达式以及正则表达式匹配规则
- 云虚拟服务器登录,云虚拟服务器登录
- vi 命令 用法 (vi 方向鍵及 backspace 的問題 )
- 查找论文和代码实现的网站