【算法】双指针算法(全),题目详解,图文并茂。
这篇文章介绍了双指针算法的概念和应用。作者首先说明了双指针算法通过使用两个指针在数组或链表中按特定方式移动,来解决多种问题的优势,并且具有较低的时间复杂度,能够提高算法效率。接着,文章详细介绍了三类常见的双指针算法:快慢指针、左右指针和对撞指针,每种指针移动方式都适用于不同类型的问题,需要根据具体情况选择合适的方式。最后,文章承诺将详细讲解八道双指针算法相关题目,图文并茂地解释题目内容,并且提供代
🌠个人主页 : @赶路人- -
🌌个人格言 :要努力成为梧桐,让喜鹊在这里栖息。
要努力成为大海,让百川在这里聚积。
双指针算法
前言
欢迎来到我的算法栏目!
从今天开始,我将分享各种有关算法的知识和技巧,希望能帮助大家提升算法设计和分析的能力。
如果你也对算法感兴趣,欢迎加入我的算法栏目。让我们一起开始这个令人兴奋的学习之旅吧!
↖(▔^▔)↗
0.双指针算法是什么
双指针算法(Two Pointers Algorithm)通过使用两个指针,分别从数组或链表的头部和尾部开始向中间移动,常可以用来解决数组或链表问题中的多种问题。其中包括:判断是否存在满足某条件的两个数、寻找满足某条件的连续子序列、判断一个字符串是否为回文串以及将一个数组或链表按照某种方式重新排序等。双指针算法的时间复杂度通常为 O(n),因此在处理大规模数据时,使用双指针算法可以有效提高算法的效率。根据指针移动的方式,双指针算法可以分为三类:快慢指针、左右指针和对撞指针。不同类型的双指针算法适用于不同类型的问题,使用时需要根据问题的具体情况来选择合适的指针移动方式。
1.快慢指针(Fast and Slow Pointers) 通常用于链表问题中,如判断链表是否有环或找到链表的中间节点等。通过使用两个指针,一个快指针和一个慢指针,快指针每次移动2步,慢指每次移动1步,当快指到达链表尾部时,慢指针就到达了链表中间位置。
2. 左右指针(Left and Right Pointers) 通常用于数组问题中,如在有序数组中查找目标元素等。通过使用两个指针,一个左指针和一个右指针,左指针从数组头部开始向右移动,右指针从数组尾部开始向左移动,根据具体问题来移动指针,最终得出结果。
3. 对撞指针(Two Pointers Approach) 通常用于有序数组或链表问题中,如判断回文字符串或找到两个数的平方和等。通过使用两个指针,一个指向数组或链表的头部,另一个指向尾部,根据具体问题来移动指针,最终得出结果。
不同类型的双指针算法适用于不同类型的问题,使用时需要根据问题的具体情况来选择合适的指针移动方式。
1.移动零
题目来源LeetCode : https://leetcode.cn/problems/move-zeroes/submissions/497790642/
1.1题目解析
题目描述: 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
1.2解题思路
两个指针:
current(最近) destination(目的地)
cur : 从左向右扫描数组, 遍历数组 .
dest : 已处理的区间内 , 非零元素的最后一个位置 .
在cur移动一次处理一个位置 , 即cur左边为处理过的 , 右边为待处理的 .
在cur处理过的区间 , 用dest分为非零元素和零元素 , 即dest左边为非零元素右边为零元素 .
由此在处理的过程中 , 可以用两个指针分成三个区间
[0 , dest] 非零元素 , [dest+1 , cur - 1 ] 零元素 , [cur , n - 1]待处理 .
这里可能有小土豆有疑问了 , 第二个区间右边为啥是 cur - 1 呢 , 为啥不是cur , 没事我必给你讲懂 . 首先回到cur指针的含义上 , cur是从左向右扫描的 , 因此cur的左边表示处理过的 , 那么处理过的最右边不就是cur - 1 了吗 . 然后待扫描的就是[cur , n - 1] .
所以当cur移动到n 位置时即处理完毕 .
当cur到了n位置时那么 [ cur , n - 1]位置是不是已经没有待处理元素.
那么具体怎么处理呢 下面我以一个例子来讲 .
初始化 :
cur初始化为0 , 应该是比较好理解的 , cur从左往右扫描嘛 , 那就从第一个元素开始 . 那么dest为什么初始化为 - 1 呢 . 区间[0 ,dest]不是表示非零元素吗 , 那么一开始还没有非零元素 , 那么就可以初始化为 - 1
首先cur移动 , 那么就会有两种情况cur的位置元素是 0 和非 0 , 当cur位置是 0 时为了保证前面区间符合条件cur向右移动一位即可(移动完区间[dest , cur - 1]都是 0元素) , 当元素是非 0 时 那么这个元素就要移动到区间 [0,dest] 里面 , 即移动到dest + 1指向的位置 , 因为区间元素多了一个即dest要 dest++ .
当cur位置元素为 0 时 , 区间无需处理 cur++即可 .
当cur位置元素非零时 , 首先dest++ (dest指向非零区间的最后一位元素 , 此时非零区间要多加一位元素) ,
然后交换cur元素和dest元素 , 最后cur++(cur在向后移动) .
总结
cur从左往右遍历过程中
1. 遇到 0 元素 : cur++;
2. 遇到非 0 元素 :
dest++;
swap(dest , cur);
cur++;
相关知识点 : 双指针的思想是快速排序里面最核心的 , 仅需把第一个区间改为 <=tmp , 第二个区间为 >tmp 即可 .
1.3代码实现
java代码示例
public void moveZeroes(int[] nums) {
for(int cur = 0,dest = -1;cur<nums.length;cur++){
if(nums[cur] != 0){
dest++;
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
}
}
2.复写零
题目来源LeetCode : https://leetcode.cn/problems/duplicate-zeros/solutions/1604450/fu-xie-ling-by-leetcode-solution-7ael/
2.1题目解析
题目描述 : 给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。
注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。
示例 1:
输入: arr = [1,0,2,3,0,4,5,0]
输出:[ 1,0,0,2,3,0,0,4]
解释: 调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]
示例 2:
输入: arr = [1,2,3]
输出:[1,2,3]
解释: 调用函数后,输入的数组将被修改为:[1,2,3]
2.2解题思路
cur指针 : 移动 , 遍历数组 .
dest指针 : 执行复写操作 .
解法:先更据"异地"操作 , 然后化为双指针下的"就地"操作 . 在新建一个数组时:
当cur位置为非零则复写一次 , 即 arr[++dest] = arr[cur++] ; 当cur位置为零时则复写两次 ,
即arr[++dest] = arr[cur] ;arr[++dest] = arr[cur++] ;
但是本题在进行"就地"操作时 , 会发生dest指针走的比cur , 这就会发生cur指针还没有遍历到的位置就已经被复写了 . (这句话比较绕 , 请看下面例子)
这个时候2的位置还没有复写就已经被覆盖了
哎 ! 所以这道题双指针不可以用了吗? 可以写在双指针文章这里欸.哈哈哈
不是的 , 既然咱们从左到右不可以 , 那么就可以试试从右到左 .
初始化dest指向最后一位数即0 , cur指向最一位复写的数在这个例子中就是4啦 .
这样下去就会发现 , 是可以实现复写的 ,
**那么 , 为什么呢 , 为什么从左到右不可以 , 从右到左又可以了呢? **
可以发现在从左到右复写时 , dest指针会跑到了cur指针的前面去了 , 这样就会发生某个数还没扫描到就已经被复写操作覆盖了 , 改变了原来题目的复写内容 . 而从右到左则不会因为我们已经确定了cur指向最后一个需要复写的数 , 这样dest指针就不会跑到cur指针的前面去 , 这样就不会改变需要复写的内容 .
所以这道题的解题步骤是
1.找到最后一位需要复写的位置 .
2.从右向左复写 .
第2步其实就是上面模拟的从右到左的过程 , 所以下面就不在讲解 , 相信经过第一题大家已经可以轻松的搞定 , 下面我们就开始第1步步骤如下
1.1判断cur位置 的值 .
1.2决定dest向右移动一步还是两步
(cur位置值为非零移动一步 零则移动一步)
1.3判断一下dest是否已经到结束位置
1.4 cur++ ;
初始化:
cur = 0 ;
dest = -1 ;
//为什么dest要等于-1 呢 ,因为当dest到达数组最后一位时就可以停止 , 此时cur就指向了需要复写的最后一位数
注意! 有特殊情况 , 请看例子 .
例子中可以看到cur指针没有问题 , 指向了最后一位需要复写的数 , 但是dest指针因为最后一位需要复写的数是0最后移动两步导致越界了 .
所以我们要处理一下这个边界情况 .
在发生dest越界的情况 , 一定是因为最后一位是0 . 所以只要最后一位复写一次0 , 然后cur向左移动一步 , dest向左移动两步即可.
以下是处理代码 .
arr[ n-1 ] = 0 ;
cur -= 1 ;
dest -= 2 ;
至于第2步从右向左复写0 , 相信看过前面之后难度不大 ,以下直接给过程.
2.1判断cur位置的值
2.2
cur值为0 , arr[dest–] = 0;arr[dest] = 0;
cur值不为0 , arr[dest–] = arr[cur]
2.3 cur–;dest–;
2.3代码实现
java代码示例
public void duplicateZeros(int[] arr) {
int cur = 0,dest = -1,n = arr.length;
//找最后一位需要复写的数
while(cur < n){
if(arr[cur] == 0)
dest += 2;
else
dest ++;
if(dest >= n-1) break;
cur ++;
}
//处理边界情况
if(dest == n){
arr[n - 1] = 0;
cur -= 1; dest -= 2;
}
//从右向左完成复写操作
while(cur >= 0){
if(arr[cur] == 0){
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
else
arr[dest--] = arr[cur--];
}
}
3.快乐数
题目来源LeetCode : https://leetcode.cn/problems/happy-number/description/
3.1题目解析
题目描述 : 编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
题目解释
示例一
示例二
如示例二图所示 在到达20的时候 又回到了之前写过的4 , 即后面会一直在这个环里进行死循环 .
如果这个数陷入了死循环 , 即不是快乐数 ,返回false.
3.2 解题思路
其实两种情况可以抽象成一种情况 , 即这个数在经过操作后 , 一定会进入到一个环里面 , 只不过是快乐数时那个环里面的数全部都是1 ,
相信学过数据结构的土豆 , 做过一道判断链表是否有环的题目 , 和这道很类似 , 即那道是判断是否有环 , 这里是判断环里的数字是否是1 .
在环形链表的题目中 , 我们可以用快慢双指针的办法来解决 , 那么这道题目可能也可以 , 下面我们来试一下 .
解法: 快慢双指针
(双指针是一种思想 , 并不一定要是指针来实现 ,在数组中可以用下标来表示 , 甚至可以直接用一个数来充当指针)
1. 定义快慢双指针 .
2. 慢指针每次向后移动一步 , 快指针每次向后移动两步 .
3. 判断相遇时候的值是否为
3.3代码实现
java代码示例
public int bitSum(int n)//返回n每一位数上的平方和
{
int sum = 0;
while(n != 0)
{
int t = n % 10; // 取n的最后位数
sum += t * t; // 将最后位数的平方加到sum上
n /= 10; // 将n除以10,去掉最后位数
}
return sum ;
}
public boolean isHappy(int n) {
int slow = n, fast = bitSum(n); //slow为线中的第一个数 , fast为第二个数
while(slow != fast){ // 当slow和fast相等时,退出循环(找到了一个重复数)
slow = bitSum(slow); // slow更新数线中的后一个数
fast = bitSum(bitSum(fast)); // fast更新为数线中的后两位数
}
return slow == 1; // 判断这个重复数是否是1
}
3.4拓展知识(不重要)
以下内容仅为扩展 , 对于数学证明兴趣不大的同学可以直接跳过 ,没有影响 .
在快乐数的定义中第二句话是"然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。".
这道题目在开始的时候是没有这句话的 , 如果没有这句话 ,大家可以细想一下是不是题目还会有第三种情况 ,即如果这个数没有成环是一条直线下去呢 , 如果需要考虑这种情况的话那么这道题目的难度会直接飙升 .
下面我们来证明一下为什么一定会有环 .
首先要用到的是 鸽巢原理(抽屉原理) : n个巢 , n+1个鸽子 ,那么至少有一个巢里面的鸽子数大于1 .
在本题中数据的最大范围是int的 即 2 ^ 31-1
231-1= 2147483648≈2.1 ✖109 << 9999999999(10个9)
9999999999经过 f 操作后(题目中每个位置上的数字的平方和) 等于9^2 ✖10 = 810
因为2.1 ✖109 << 9999999999所以题目中输入的n经过一次f变化后一定会落在区间[ 1, 810 ]中的 , 并且以后的每次变化也落在[ 1, 810 ]区间中 .
区间[1 , 810]里面一共有810个数 , 那么当n经过811次f变化后一定会出现重复的(原因:鸽巢原理) , 所以后面一定会出现环 .
4.盛最多水的容器
题目来源LeetCode : https://leetcode.cn/problems/container-with-most-water/description/
4.1题目解析
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:输入:height = [1,1]
输出:1
4.2解题思路
解法一:暴力枚举
这个方法比较简单,就是通过双重 for 循环来固定每一个数,并计算其与后面每个数的体积。然而,这种暴力枚举的方法会导致超时,因此需要寻找更优的解法。
时间复杂度O(n2)
解法二:利用规律,使用双指针减少计算次数。
我们可以通过找规律的方式,进一步减少计算次数。例如,在小区间 [6,2,5,4] 中,根据公式体积 V = H(高度)× W(宽度),首先可以计算最外面的两条边 V(6,4)= 4×4。然后,我们固定小的 4 来向内枚举,计算 V(2,4)、V(5,4)。在向内枚举的过程中,会出现两种情况:一种是向内枚举的另一边小于 4,另一种是大于等于 4。这里我们分别探讨一下这两种情况下体积和一开始的 V(6,4)有什么关系。
在况一 V(2,4)时,由于要选择小的一边,所以 H 一定小于 4,即 V(2,4)的高度 H 一定小于 V(6,4)的高度。同时,宽度 W 也会减小,因为是向内枚举嘛,宽度肯定会减小。因此,可以得到结论:在固定小的一边(6 和 4 中 4 较小)时向内枚举,H 和 W 都减小,所以 V 一定减小,即 V(2,4) < V(6,4)。
在况二V(5,4)时,由于向内枚举的数大于等于 4,那么小的一边就是 4,即向内枚举的高度 H = 4,和 V(6,4)的高度一样。同时,宽度 W 也会减小。因此,一个相等一个减小最后相乘得到的 V 一定也变小。即 V(5,4)<V(6,4)。
综合上述情况,我们可以得出结论:在固定小的一边时向内枚举都是比一开始用两边求的体积小的,因此小的一边就不用再向内枚举了。
在处理题目给定的数组 [1,8,6,2,5,4,8,3,7] 时,我们可以采用双指针法来减少计算次数。首先,我们计算最外侧两条边形成的容器的体积,即 V(1,7)。接着,我们观察到根据之前的结论,在向内枚举时,固定较小的一边并向内移动指针会得到比最初计算的体积更小的结果。因此,我们可以将较小的边1去掉,因为无论与数组中的哪个数字组合计算,得到的体积都会小于 V(1,7)。
接下来,我们将第一个指针移动到8处,计算 V(8,7),然后移动较小的边7以计算 V(8,3)。继续移动指针,计算 V(8,8),在这种情况下,移动哪个边都不会影响最终的体积结果,因为两个边向内枚举得到的结果是相同的。
通过这种双指针法,我们遍历了数组中的数字一次,时间复杂度O(n),有效降低了计算的复杂度,使得问题的解决更加高效。这种方法在处理类似容器盛水的问题时非常实用,避免了暴力枚举带来的时间复杂度过高的问题。
因为这里是用两个指针把里面的数一起走过了一遍,所以时间复杂度为O(n)。
4.3代码实现
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, ret = 0;
while (left < right) {
// 计算当前左右边界形成的容器的体积
int v = Math.min(height[left], height[right]) * (right - left);
// 更新最大的容器体积
ret = Math.max(ret, v);
// 根据高度的大小移动指针
if (height[left] < height[right]) left++;
else right--;
}
return ret;
}
}
5.有效三角形的个数
题目来源Leecode:https://leetcode.cn/problems/valid-triangle-number/description/
5.1题目描述
给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。
示例 1:
输入: nums = [2,2,3,4]
输出: 3
解释:
有效的组合是:
2,3,4 (使用第一个 2)
2,3,4 (使用第二个 2)
2,2,3
示例 2:
输入: nums = [4,2,3,4]
输出: 4
5.2解题思路
数学知识:判断三个数是否能构成三角形。
法一:
{
a
+
b
>
c
a
+
c
>
b
b
+
c
>
a
\begin{cases} a+b >c \\ a+c>b \\ b+c>a \end{cases}
⎩
⎨
⎧a+b>ca+c>bb+c>a
法二:
{
a
≤
b
≤
c
a
+
b
>
c
\begin{cases} a\leq b \leq c\\ \\a+b >c \end{cases}
⎩
⎨
⎧a≤b≤ca+b>c
解法一: 暴力枚举
暴力枚举所有的三位数,在判断是否构成三角形。
伪代码:
for(i = 0 ; i < n ; i++)
for(j = i + 1 ; j < n;j++)
for(k = j + 1;k < n;k++)
check(i , j , k);//判断是否构成三角形
时间杂度O(N3)
考虑常数级 ,在用法一判断三角形时,最后一行代码需要判断三次,即3N3。
用法二判断三角形时,需要先排序,排序的时间复杂度为NlogN , 即最后的复杂度为N logN+N3。
优化选择
考虑到时间复杂度和常数级因素,我们发现在使用法一判断三角形时,最后一行代码需要判断三次,即3N3。而使用法二判断三角形时,需要先对数组排序,排序的时间复杂度为NlogN,最后的复杂度为N logN+N3。
显而易见,3N3 > N logN+N3,因此我们选择第二种判断方法,即先对数组排序。并且在数组有序时,我们的操作空间就变得很大,可以利用单调性进行优化
解法二:利用单调性,使用双指针算法
步骤:
1.先固定最大的数。
2.在最大的数的左区间内,使用双指针算法,快速统计出符合要求的三元组的个数。
3.固定最大数的指针向左移动,重复步骤二。
步骤二详细,固定最大的数(下面记c),在用指针left指向最左边的最小数(a),用指针right指向第二大的数(即c的左边的数b)。
判断a,b,c,三数能否构成三角形
有两种情况即
1. a + b > c 2. a + b ≤ c 1. \quad a+b>c \\ 2.\quad a+b\leq c 1.a+b>c2.a+b≤c
如果是情况1,那么说明b加上任意一个中间数都会大于c(中间的数大于a,a+b已经大于c了)。因此,我们找到了a到b左边一个数的三角形组合,即右指针的位置到左指针的位置之间存在right - left种组合可能。在这种情况下,我们将右指针向左移动(即right–)
如果是情况2,说明当前左指针指向的数太小了(中间的任意一个数加上a都会小于c),需要增大最小数a的值。此时,将左指针右移一位(即left++)
总结
1.
a
+
b
>
c
→
r
i
g
h
t
−
l
e
f
t
,
r
i
g
h
t
−
−
2.
a
+
b
≤
c
→
l
e
f
t
+
+
1. \quad a+b>c\quad\rightarrow\quad right-left,\quad right-- \\ 2.\quad a+b\leq c\quad\rightarrow\quad left++\qquad\qquad\qquad\quad\;
1.a+b>c→right−left,right−−2.a+b≤c→left++
时间复杂度: 最大的数c要一直循环到第三小的数(即需要n次),中间使用双指针算法判断,两个指针相向移动遍历一次(即也是n次),所以总共O(N2)。
5.3代码实现
public int triangleNumber(int[] nums) {
//优化排序
Arrays.sort(nums);
int ret = 0;
for(int c = nums.length - 1;c >= 2; c--){//固定最大的数
//利用双指针快速统计出符合要求的三元组的个数
int left = 0,right = c - 1;
while(left < right ){
if(nums[left]+nums[right] > nums[c]){
ret += right-left; //统计个数
right--;
}
else
left++;
}
}
return ret;
}
6.和为S的两个数字
题目来源牛客网:https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&ru=/exam/oj
6.1 题目描述
输入一个升序数组 array 和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字和等于S,返回任意一组即可,如果无法找出这样的数字,返回一个空数组即可。
数据范围: 0≤len(array)≤105 , 1≤array[i]≤106.
示例1
输入:[1,2,4,7,11,15],15
返回值:[4,11]
说明:返回[4,11]或者[11,4]都是可以的
示例2
输入:[1,5,11],10
返回值:[]
说明:不存在,返回空数组
示例3
输入:[1,2,3,4],5
返回值:[1,4]
说明:返回[1,4],[4,1],[2,3],[3,2]都是可以的
示例4
输入:[1,2,2,4],4
返回值:[2,2]
6.2 解题思路
解法一:暴力枚举策略
for(i = 0 ; i < n ; i++)
{
for(j = i + 1; j < n; j ++ )
{
check(nums[i] + nums[j] == t)//判断两数是否等于目标值
}
}
时间复杂度O(N2)
! 为什么暴力枚举会慢。
注意看题目,有个升序的信息,而这个暴力枚举中没有用到。
所以这个升序要怎么用呢?
解法二:利用单调性,使用双指针算法解决问题。
首先left指向第一个数,right指向末尾的数。
sum表示为left指向的数和right指向的数的和。
所以sum有三种可能即
- 当sum > s 时,表示左右指针指向的两数之和大于目标值s。这意味着即使将右指针指的数加上区间内最小的数(左指针的数),仍然大于目标值s,因此,右指针加上区间[left, right]内任意一个数都会大于s,所以需要舍弃右指针的数,将右指针左移,即执行right–操作。
- 当sum < s时,即左右指针指向的两数之和小于目标值s,这意味着即使将左指针的数加上区间内最大的数(右指针的数),仍未达到目标值s。因此,左指针加上区间内的任意一个数也不会大于s。在这种情况下需要舍弃左指针的数,将左指针右移,即执行left++操作。
- 当sum=s时,即找到了我们需要的数返回数即可。
所以总结如下
{
1.
s
u
m
>
s
→
r
i
g
h
t
−
−
;
2.
s
u
m
<
s
→
l
e
f
t
+
+
;
3.
s
u
m
=
s
→
r
e
t
u
r
n
;
\begin{cases} 1.sum>s\rightarrow right--;\\ 2.sum<s\rightarrow left++;\\ 3.sum = s\rightarrow return; \end{cases}
⎩
⎨
⎧1.sum>s→right−−;2.sum<s→left++;3.sum=s→return;
每一步的图解如下,
需要注意的细节时循环判断的条件是(left<right)。
6.3 代码实现
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int[] array, int s) {
ArrayList<Integer> result = new ArrayList<>();
int left = 0, right = array.length - 1;
while (left < right) {
int sum = array[left] + array[right];
if (sum > s) {
right--;
} else if (sum < s) {
left++;
} else {
result.add(array[left]);
result.add(array[right]);
return result;
}
}
return result;
}
}
7.三数之和
题目来源LeetCode : https://leetcode.cn/problems/3sum/
7.1 题目描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。
请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例三
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
数据范围
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
7.2解题思路
对于这道题目,我们可以使用双指针的方法来解决。首先对数组进行排序,然后遍历数组,对于每个元素 nums[i],我们使用双指针 left 和 right 分别指向 i 后面的两端,通过不断调整 left 和 right 的位置,找到所有满足条件的三元组。
具体步骤如下:
1. 首先对数组进行排序,方便处理重复元素和确定双指针移动的方向。
2. 遍历数组,对于每个元素 nums[i],设定一个目标值 target = -nums[i]。
3. 使用双指针 left 和 right 分别指向 i 后面的两端。 在 left < right 的情况下,计算 nums[left] + nums[right] 的值:
4. 如果等于目标值target,则找到一个满足条件的三元组,将其加入结果集,并继续向中间移动 left 和 right,同时跳过重复元素。如果小于目标值target,说明需要增大和,移动 left 指针。 如果大于目标值
target,说明需要减小和,移动 right 指针。
5. 继续遍历下一个元素,直至遍历完所有元素。
具体例子如下:[-4 , -4 , -1 , 0 ,1 , 1 , 4 , 4 , 5 , 6 ]
细节处理:去重
具体来说,在找到一个满足条件的三元组后,我们将左指针向右移动一位,同时跳过所有重复的元素,直到遇到一个不同的元素为止。同样地,我们将右指针向左移动一位,同时跳过所有重复的元素,直到遇到一个不同的元素为止。
这样做可以确保我们在移动指针的过程中不会得到重复的解。在代码实现中,我们通过 while 循环和判断相邻元素是否相同来跳过重复元素,以此保证结果中不包含重复的三元组。
7.3代码实现
import java.util.*;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums); // 1. 对数组进行排序以便使用双指针方法
List<List<Integer>> result = new ArrayList<>(); // 存储结果的列表
for (int i = 0; i < nums.length - 2; i++) {
if (i == 0 || (i > 0 && nums[i] != nums[i - 1])) { // 避免重复解
int left = i + 1, right = nums.length - 1, target = -nums[i]; // 定义左右指针和目标值
while (left < right) {
if (nums[left] + nums[right] == target) { // 找到符合条件的三元组
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 跳过重复元素
while (left < right && nums[left] == nums[left + 1]) left++; // 左指针右移,跳过重复元素
while (left < right && nums[right] == nums[right - 1]) right--; // 右指针左移,跳过重复元素
left++;
right--;
} else if (nums[left] + nums[right] < target) {
left++; // 调整左指针
} else {
right--; // 调整右指针
}
}
}
}
return result;
}
}
8. 四数之和
题目来源LeetCode : https://leetcode.cn/problems/4sum/description/
8.1 题目描述
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入: nums = [1,0,-1,0,-2,2], target = 0
输出: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入: nums = [2,2,2,2,2],target = 8
输出:[[2,2,2,2]]
8.2解题思路
- 对输入的整数数组 nums 进行排序。
- 遍历数组,在每一次遍历中选定第一个数字 nums[i]。为了避免重复解,当 i大于0且当前数字与上一个数字相同时,我们应该跳过这个数字。
- 在内层循环中,遍历剩余的数字并选定第二个数字nums[j]。同样地,为了避免重复解,当 j 大于 i + 1 且当前数字与上一个数字相同时,我们应该跳过这个数字。
- 然后,我们使用双指针方法来寻找剩余数组中的两个数字,使得它们的和等于 target - nums[i] - nums[j]。
- 左指针left 初始化为 j + 1,右指针 right 初始化为 nums.length - 1。
- 我们不断移动左右指针,直到找到所有满足条件的四元组。如果找到符合条件的四元组,则将其加入结果列表中。
- 在添加之前,需要检查是否有重复的情况,如果有则跳过重复元素。
- 继续遍历直到找到所有满足条件的四元组。 最终返回结果列表。
这道题是6.和为S的两个数字 7.三数之和 两题的延续,解题思路很多延续了前面两道题的思想,所以如果你是小白强烈建议看完前面两题在看此题。
8.3代码实现
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 对数组进行排序
List<List<Integer>> ret = new ArrayList<>(); // 用于存储结果的列表
int n = nums.length; // 数组的长度
for (int i = 0; i < n - 3; ) {
for (int j = i + 1; j < n - 2; ) {
int left = j + 1;
int right = n - 1;
while (left < right) {
long sum = (long) target - nums[i] - nums[j];
if (nums[left] + nums[right] < sum) { // 如果两数之和小于目标值,左指针右移
left++;
} else if (nums[left] + nums[right] > sum) { // 如果两数之和大于目标值,右指针左移
right--;
} else {
// 找到符合条件的四元组,加入结果列表中
ret.add(Arrays.asList(nums[i], nums[j], nums[left++], nums[right--]));
// 去重处理
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
j++;
while (j < n - 2 && nums[j] == nums[j - 1]) {//对j去重
j++;
}
}
i++;
while (i < n - 3 && nums[i] == nums[i - 1]) {//对i去重
i++;
}
}
return ret; // 返回结果列表
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)