主页 > 电脑硬件  > 

【算法】双指针(上)

【算法】双指针(上)

目录

双指针

左右指针(对撞指针)

快慢指针

移动零

双指针解题

复写零

暴力解题

双指针解题(快慢指针)

快乐数

双指针解题(快慢指针)

盛最多水的容器

暴力解题(会超时)

双指针解题(左右指针)

有效三角形的个数

暴力解题

双指针解题(左右指针)


双指针

常见的双指针有两种形式,一种是左右指针(对撞指针),一种是快慢指针

左右指针(对撞指针)

·左右指针从顺序结构的两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近

·左右指针的终止条件无非就只有两种情况(也可能在循环内部找到结果直接跳出循环)

·left==right(两个指针指向同一个位置)

·left>right(两个指针错开)

快慢指针

又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动

这种方法对于处理环形链表或数组非常有用

其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想

快慢指针的实现方式有很多种,最常用的一种就是:

·在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢

【数组分两块】是非常常见的一种提醒,主要就是根据一种划分方式,将数组的内容分成左右两部分。这种类型的题,一般就是使用双指针来解决

移动零

题目链接:283. 移动零 - 力扣(LeetCode)

题目描述:

给定⼀个数组 nums ,编写⼀个函数将所有 0 移动到数组的末尾,同时保持⾮零元素的相对顺

序。

请注意 ,必须在不复制数组的情况下原地对数组进⾏操作。

⽰例 1:

输⼊: nums = [0,1,0,3,12]

输出: [1,3,12,0,0]

⽰例 2:

输⼊: nums = [0]

输出: [0]

双指针解题

解题思路(快排的思想:数组划分区间--数组分成两块):

使用双指针。用一个cur指针来遍历整个数组,另一个dest指针用来记录非零数序列的最后一个位置。然后根据cur指针遍历到的数进行分类处理。

大致流程为:

1.初始化cur=0,dest=-1。

注意:我们这里初始化dest=-1。这里不将dest设置为0的原因是:如若将dest设置为0,dest的指向便不再是非零数序列的最后一个位置了。(因为下标为0的话,也占一个元素啊)

2.令cur遍历数组,遍历到的元素有两种情况:

·遇到的元素不是0,dest++。并交换cur和dest位置处的元素,之后让cur++。

这里dest++有两个含义:首先是因为cur指针找到一个非0数,所以dest后移一位,表示非0数的序列+1。其次是因为dest++之后,指向的元素就是0元素(因为非0元素区间末尾的后一个元素就是0)。

·遇到的元素是0,cur直接++。

解题代码:

class Solution {     public void moveZeroes(int[] nums) {         for(int cur=0,dest=-1;cur<nums.length;cur++){             if(nums[cur]!=0){                 dest++;                 //交换cur和dest位置处的元素                 int temp=nums[cur];                 nums[cur]=nums[dest];                 nums[dest]=temp;             }         }     } }

我们上述代码是初始化dest= -1。让dest指向非0序列的最后一个位置。

现在我们初始化dest=0。看看代码有哪里不同

class Solution {     public void moveZeroes(int[] nums) {         for(int cur=0,dest=0;cur<nums.length;cur++){             if(nums[cur]!=0){                 int temp=nums[cur];                 nums[cur]=nums[dest];                 nums[dest]=temp;                 dest++;             }         }     } }

我们发现,表面来看,不过是dest++ 一个放前面和一个后面。但我们要清楚里面的门道。将dest初始为0的,则令dest指向非0序列末尾的后一个元素。

复写零

题目链接:1089. 复写零 - 力扣(LeetCode)

题目描述:

给你⼀个⻓度固定的整数数组 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]

暴力解题

思路:让指针L1遍历数组,一旦遇到元素0,便从此处开始,一直到数组末尾,每个元素后移一位。由于元素后移一位,实现复写0,所以指针L1需要格外进行一次++,跳过复写的0,不然会导致后续元素都为0.

注意:如果我们只用一个指针L1无法【从前向后】实现所有元素后移一位。因为,如果【从前向后】实现元素后移的话,会把未移动的元素给覆盖掉。所以我们这里【从后向前】实现元素后移

解题代码:

class Solution {     public void duplicateZeros(int[] arr) {         int l1=0;         int length=arr.length;         for(;l1<length;l1++){             if(arr[l1]==0){                 for(int j=length-1;j>l1;j--){                     arr[j]=arr[j-1];                 }                 l1++;             }         }     } }

双指针解题(快慢指针)

解题思路:

由于复写0会使元素+1,然而数组长度不变,数组便从末尾开始舍弃元素。所以我们只需找到 新复写数组的最后一个元素(即旧数组中最后一个复写的数)。然后进行复写

核心步骤:

·先找到最后一个复写的数

·然后【从后向前】进行复写

注意:由于【从前向后】进行原地复写操作,会导致没有复写的数【被覆盖掉】。所以这里选择【从后往前】进行复写

大致流程为:

a.初始化两个指针cur=0,dest=-1

b.找到最后一个复写的数

·当cur<数组长度,执行下面循环

1.若cur位置处的元素为0,dest后移两位,否则后移一位。注意:这里的dest指针可以看作记录元素个数(每移动一步相等于记录一个元素)。(保证移动步数=元素个数即可,但当最后一个复写的数为0时,移动步数会多1步,dest指针会越界)

2.判断dest指针此时是否已经到结束位置(数组末尾亦或者是越界),如果到了便终止循环

3.如果没有结束,cur++,继续循环判断

c.判断dest是否越界(最后一个复写的数是否为0),如果越界,便执行下面三步:

将数组最后一个元素的值修改成0cur指针向前移动一步(跳过最后一个复写的数(0)dest指针向前移动两步(指向倒数第二个数--跳过数组最后一个元素)

注意:我们要知道,数组最后一个元素和最后一个复写的数,这两个名词并不等同

d.从cur位置开始往前遍历原数组,还原出复写后的数组

解题代码:

class Solution {     public void duplicateZeros(int[] arr) {         int cur=0;         int dest=-1;         int n=arr.length;         //找到最后一个复写的数         while(cur<n){             if(arr[cur]!=0){                 dest++;             }else{                 dest+=2;             }             if(dest>=n-1)break;//结束条件             cur++;         }         //处理越界情况--即最后一个复写的数为0         if(dest==n){             arr[n-1]=0;             cur--;             dest-=2;         }         //从后往前还原出复写后的数组         for(;cur>=0;cur--){             if(arr[cur]!=0){                 arr[dest--]=arr[cur];             }else{                 arr[dest--]=0;                 arr[dest--]=0;             }         }     } }

快乐数

题目链接:202. 快乐数 - 力扣(LeetCode)

题目描述:

编写⼀个算法来判断⼀个数 n 是不是快乐数。

「快乐数」 定义为:

◦ 对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和。

◦ 然后重复这个过程直到这个数变为 1,也可能是⽆限循环但始终变不到 1 。

◦ 如果这个过程 结果为 1 ,那么这个数就是快乐数。

◦ 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

⽰例 1:

输⼊: n = 19

输出: true

解释:

19 -> 1 * 1 + 9 * 9 = 82

82 -> 8 * 8 + 2 * 2 = 68

68 -> 6 * 6 + 8 * 8 = 100

100 -> 1 * 1 + 0 * 0 + 0 * 0 = 1

⽰例 2:

输⼊: n = 2

输出: false

解释:(这⾥省去计算过程,只列出转换后的数)

2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> 16

往后就不必再计算了,因为出现了重复的数字,最后结果肯定不会是 1

题目解析:

我们将 【对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和】这一操作用 function方法来解决。

重点是:题目定义中的【无限循环】。

意思是:

1.如果这个数n不是快乐数,就会【无限循环】但始终变不到1。

2.然而,如果这个数是快乐数,即最终会变到1,此时1便会【无限循环】。

所以,无论这个数n是不是快乐数,都会【无限循环】

所以,当我们不断循环function方法,计算一定会【无限循环】,【无限循环】的方式有两种:

情况1:在旧的数据中【无限循环】,但始终变不到1。

情况2:一直在1中进行【无限循环】

【无限循环】原理解释:

由于int最大值2^31-1=2147483647,哪怕是9999999999这样大的数据,各个位置上的数字的平方和为810。所以在int范围内的数n,其各个位置上的数字的平方和一定在[1,810]区间内。极限情况下,一个数变化811次之后,必然会形成一个循环。因此使用【快慢指针】来解决本题

双指针解题(快慢指针)

解题思路:

当我们重复使用function方法时,数据会陷入【无限循环】中。

【快慢指针】的一个特性就是,在一个圆圈中,快指针总是会追上慢指针的。也就是说,它们总会相遇在一个位置上。如果这个位置是1,那么这个数一定是快乐数;否则,这个数便不是快乐数

重点:相遇位置以及该数是不是1

大致流程为:

写一个function方法:用来求数n每个位置上的数字的平方和让slow每次走一步,fast每次走两步,判断相遇位置处的元素是否为1。如果是1,输出true;否则,输出false

题目代码:

class Solution {     public static boolean isHappy(int n) {         int slow=n         int fast=function(n);//注意,这里初始化fast!=slow是因为,我们要求它俩相遇的位置         //当fast指针追上slow指针,并且都为1时,返回true         while(slow!=fast){             slow=function(slow);//走一步             fast=function(function(fast));//走两步         }         return slow==1;     }     private static int function(int n) {         int sum=0;         while(n!=0){             int g=n%10;             sum+=g*g;             n/=10;         }         return sum;     } }

盛最多水的容器

题目链接:11. 盛最多水的容器 - 力扣(LeetCode)

题目描述:

给定⼀个⻓度为 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

暴力解题(会超时)

思路:一个一个进行遍历,找出其中容积(V)最大的值

容积公式=长*宽

设两指针i,j,初始化i=0,j=i+1,我们知道两指针之间的距离=宽度=j-i。由于容器的高度由两板中的短板决定,所以容积公式为:v=(j-i)*min(height[i],height[j])

解题代码:

class Solution {     public int maxArea(int[] height) {         int n=height.length;         int v=0;//v=(j-i)*min(height[i],height[j])         int sum=0;         for(int i=0;i<n;i++){             for(int j=i+1;j<n;j++){                 v=(j-i)*Math.min(height[i],height[j]);                 if(v>=sum){                     sum=v;                 }             }         }         return sum;     } }

双指针解题(左右指针)

解题思路:

设两个指针left,right分别指向容器的左右两个端点。

此时容器的容积v=(right-left)*min(height[left],height[right])

容器的【左边界】为height[left],【右边界】为height[right]

我们先假设【左边界】小于【右边界】

如果我们固定一个边界,改变另一个边界,容积v会有以下变化形式:

   1.容器的宽度一定变小

   2.由于【左边界】小,所以决定了容器的高度。如果改变【左边界】,容器的高度便不确定(相较于原先的【左边界】),但是一定不会超过【右边界】的高度,因此容器的容积v可能会增大

   3.如果改变右边界,无论【右边界】移动到哪里,新容器的高度一定不会超过【左边界】,也就是容器的高小于等于【左边界】,但是容器的宽度随着【右边界】的移动而减少,因此容器的容积v一定会变小

我们这里可以把【左边界】替换为较小值,【右边界】替换为较大值。

根据上述假设,移动【右边界】会导致容积v变小,所以,我们这里只移动【较小值】,并比较容积v,便能得到最大值

解题代码:

class Solution {     public static int maxArea(int[] height) {         int left=0;         int right=height.length-1;         int v=0;         int max=0;         while(left<right){             v=(right-left)*Math.min(height[left],height[right]);             if(v>=max){                 max=v;             }             if(height[left]<height[right]) {                 left++;             }else{                 right--;             }         }         return max;     } }

有效三角形的个数

题目链接:611. 有效三角形的个数 - 力扣(LeetCode)

题目描述:

给定⼀个包含⾮负整数的数组 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

解释:

4,2,3

4,2,4

4,3,4

2,3,4

暴力解题

解题思路:三层for循环枚举出所有的三元组

构成三角形的条件:需要满足任意两边之和大于第三边。但是实际上只需让较小的两条边之和大于第三边即可。因此我们这里先排序,再枚举。

解题代码:

lass Solution {     public static int triangleNumber(int[] nums) {         Arrays.sort(nums);         int n=nums.length;         int count=0;         for(int i=0;i<n;i++){             for(int j=i+1;j<n;j++){                 for(int t=j+1;t<n;t++){                     if(nums[i]+nums[j]>nums[t]){                         count++;                     }                 }             }         }         return count;     } }

双指针解题(左右指针)

由于我们左右指针只能表示两个元素,但为了找出这个三元组,可以尝试固定一条边,这样只需要找到一个二元组即可。

解题思路:

1.先将数组排序

2.我们这里选择固定一个【最长边】,然后在比【最长边】小的有序数组中找出一个二元组,使这个二元组之和大于这个【最长边】。由于数组是有序的,我们这里采用【左右指针】。

  假设最长边在下标i处,区间[left,right]是i位置左边的区间--也就是比i小的区间:

  如果nums[left]+nums[right]>nums[i]:

   ·说明[left,right-1](因为left处的元素是最小的)区间上的所有元素都可以和nums[right]构成一个二元组,使其之和大于nums[i]

   ·满足条件的有(right-1-left)+1=right-left种

   ·此时right处的元素的所有情况相当于全部考虑完毕,right--,进入下一轮判断

 如果nums[left]+nums[right]<nums[i]:

   ·说明left处的元素是不能和[left+1,right](因为right处的元素已经是最大的了)区间上的元素构成一个二元组,使其之和大于nums[i]

   ·left处的元素舍去,left++

解题代码:

class Solution { public static int triangleNumber(int[] nums) { Arrays.sort(nums); int n=nums.length; int count=0; //左右指针:固定一个最长边(i)---下标[2,n-1] for(int i=n-1;i>=2;i--){ int left=0; int right=i-1; while(left<right){ if(nums[left]+nums[right]>nums[i]){ //[left,right-1]区间上的元素 和nums[right]的和都大于nums[i] count+=right-left;//注意:这里不用count++,因为 right--;//选择较小的数 判断是否大于固定的最长边 }else{ left++; } } } return count; } }
标签:

【算法】双指针(上)由讯客互联电脑硬件栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【算法】双指针(上)