双指针 (doublePointer)

本文详细介绍了使用双指针解决数组和字符串问题的多种经典算法,包括寻找三数之和、最长回文子串、最长有效括号、盛最多水的容器、接雨水、反转字符串和下一个排列等。通过实例分析,展示了双指针在解决这些复杂问题时的高效性和简洁性。
摘要由CSDN通过智能技术生成

1 双指针

1.1 (lee-15) 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0,请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

/**
	 * 思路:双指针
	 * 1.首先,特例判断,如果数组为null或者数组长度小于3,返回null;
	 * 2.对数组进行排序,因为要求不能包含可以重复的三元组,因此排序后再操作;
	 * 3.对数组进行遍历:
	 *       如果nums[i] > 0,说明已经排好序,后面不会有三数相加和=0,直接返回结果;
	 *       对于重复元素,跳过,避免出现重复解;
	 *       令左指针为L,右指针为R,使用双指针法进行遍历。
	 * 时间复杂度:O(n^2)   
	 * 空间复杂度:O(n) 
	 * @param nums
	 * @return
	 */
	public List<List<Integer>> threeSum(int[] nums) {
		List<List<Integer>> res = new ArrayList<>();
		int n = nums.length;
		Arrays.sort(nums);
		for(int i = 0;i <n;i++) {
			if(nums[i] > 0) {                            // 后面的元素肯定不满足要求,都是大于0的
				return res;
			}
			if(i > 0 && nums[i] == nums[i-1]) {          // 遇到重复的跳过
				continue;
			}
			int left = i+1;
			int right = n-1;
			int cur = nums[i];
			while(left < right) {
				int temp = cur + nums[left] + nums[right];
				if(temp == 0) {
					List<Integer> list = Arrays.asList(cur,nums[left],nums[right]);
					res.add(list);
					 // 剔除重复元素
					while(left < right && nums[left+1] == nums[left]) {
						left++;
					}
					while(left < right && nums[right-1] == nums[right]) {
						right--;
					}
					// 同时移动left、right
					left++;
					right--;
				}else if(temp < 0) {
					left++;                               // 说明left太小,left右移
				}else {
					right--;                              // 说明right太大,right左移
				}
			}
		}
		return res;
    }

1.2 (lee-05) 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案

(1) DP

	/**
	 * 1.DP 
	 * 状态转移方程:P(i,j)=P(i+1,j−1)∧(Si​==Sj​)
	 * 边界条件:
	 * P(i,i)=true 子串的长度为1
	 * P(i,i+1)=(Si​==Si+1​) 子串的长度为2
	 * 答案: 子串长度最大值 j-i+1
	 * 
	 * 时间复杂度:O(n^2)
	 * 空间复杂度:O(n^2)​
	 * @param s
	 * @return
	 */
	public String longestPalindrome1(String s) {
		int len = s.length();
		if(len < 2) {
			return s;
		}
		int maxLen = 1;
		int start = 0;
		boolean[][] dp = new boolean[len][len]; // dp[i][j] 表示 s[i..j] 是否是回文串
		for(int i = 0;i <len;i++) {             // 初始化:所有长度为 1 的子串都是回文串
			dp[i][i] = true;
		}
		char[] c = s.toCharArray();
		for(int L = 2;L <= len;L++) {            // 先枚举子串长度
			for(int i = 0; i < len;i++) {        // 枚举左边界,左边界的上限设置可以宽松一些
				int j = L+i-1;                   // 由 L 和 i 可以确定右边界
				if(j >= len) {                   // 如果右边界越界,就可以退出当前循环
					break;
				}
				if(c[i] != c[j]) {
					dp[i][j] = false;
				}else {
					if(j - i < 3) {
						dp[i][j] = true;
					}else {
						dp[i][j] = dp[i+1][j-1];
					}
				}
				
				// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
				if(dp[i][j] && j-i+1 > maxLen) {
					maxLen = j-i+1;
					start = i;
				}
			}
		}
		return s.substring(start , start+maxLen);
	}

(2) 双指针

	/**
	 * 2.思路:双指针,注意奇偶
	 * 遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。
	 * 回文串在长度为奇数和偶数的时候,“回文中心”的形式是不一样的。
	 * left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
	 * left + 1 = right 的时候,此时回文中心是一个空隙,回文串的长度是偶数
	 * 时间复杂度:O(n^2)
	 * 空间复杂度:O(1)
	 * @param s
	 * @return
	 */
	public String longestPalindrome(String s) {
		int len = s.length();
		if(len < 2) {
			return s;
		}
		int maxLen = 1;
		String res = s.substring(0,1);
		
		for(int i = 0;i < len - 1;i++) {
			String oddStr = centerSpread(s,i,i);
			String eveStr = centerSpread(s,i,i+1);
			String maxLenStr = oddStr.length() > eveStr.length() ? oddStr : eveStr;
			if(maxLenStr.length() > maxLen) {
				maxLen = maxLenStr.length();
				res = maxLenStr;
			}
		}
		return res;
    }

	private String centerSpread(String s, int left, int right) {
		int len = s.length();
		while(left >= 0 && right < len) {
			if(s.charAt(left) == s.charAt(right)) {
				left--;
				right++;
			}else {
				break;
			}
		}
		return s.substring(left+1, right); //这里要注意,跳出 while 循环时,恰好满足 s.charAt(i) != s.charAt(j),因此不能取(i,j)
	}

1.3 (lee-32) 最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
输入:s = “”
输出:0

(1) 栈

	/**
	 * 1.栈
	 * 思路:利用栈进行括号匹配,每次遇到“(”就入栈,遇到“)”就出栈,为了计算括号的数量,每次当栈空时,将未被匹配的“)”的索引入栈。
	 * 时间复杂度:O(n)
	 * 空间复杂度:O(n)
	 */
	public int longestValidParentheses(String s) {
		Deque<Integer> stack  = new LinkedList<>();
		int maxLen = 0;
		stack.offerFirst(-1);
		char[] c = s.toCharArray();
		for(int i= 0;i < c.length;i++) {
			if(c[i] == '(') {
				stack.offerFirst(i);
			}else {
				stack.pollFirst();
				if(stack.isEmpty()) {
					stack.offerFirst(i);
				}else {
					maxLen = Math.max(maxLen, i-stack.peekFirst());
				}
			}
		}
		return maxLen;
	}

(2) 双指针

	/**
	 * 2.双指针遍历两遍
	 * 思路:从左向右遍历,遇到每个‘(’,left++,遇到每个‘)’,right++; 
	 * 当 left计数器与 right计数器相等时,计算当前有效字符串的长度,并记录目前为止找到的最长子字符串;
	 * 当 right计数器比 left计数器大时,将 left和 right计数器同时变回 0;
	 * 从右往左遍历用类似的方法计算.
	 * 时间复杂度:O(n)
	 * 空间复杂度:O(1)
	 */
	public int longestValidParentheses1(String s) {
		int left = 0;
		int right = 0;
		int maxLen = 0;
		for(int i= 0;i <s.length();i++) {
			if(s.charAt(i) == '(') {
				left++;
			}else {
				right++;
			}
			if(left == right) { 
				maxLen = Math.max(maxLen, 2 * right);
			}else if(right > left) {
				left = right = 0;
			}
		}
		
		left = right = 0;//注意,否则变为二倍
		
		for(int i = s.length() - 1;i >=0;i--) {
			if(s.charAt(i) == '(') {
				left++;
			}else {
				right++;
			}
			if(left == right) {
				maxLen = Math.max(maxLen, 2*left);
			}else if(left > right) {
				left = right = 0;
			}
		}
		return maxLen;
	}

1.4 (lee-11) 盛最多水的容器

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。

输入:[1,8,6,2,5,4,8,3,7]
输出:49
该题目与(lee-42)接雨水问题有一点区别,接雨水问题在栈的部分已经总结,包括动态规划、单调栈以及双指针三种方法,这里的求的是构成的整个容器水量,比较简单,使用双指针很快完成。

	/**
	 * (lee-11)
	 * 思路:双指针
	 * 首尾各一个指针,每次向内移动短板,可以得到最大值。
	 * 时间复杂度:O(n)
	 * 空间复杂度:O(1)
	 * @param height
	 * @return
	 */
	public int maxArea(int[] height) {
		int left = 0;
		int right = height.length-1;
		int res = 0;
		while(left < right) {
			if(height[left] < height[right]) {
				res = Math.max(res, (right-left) * height[left]);
				left++;
			}else {
				res = Math.max(res, (right-left) * height[right]);
				right--;
			}
		}
		return res;
    }

1.5 (lee-42) 接雨水

给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。

(1) DP

	/**
	 * 1.动态规划
	 * 时间复杂度O(n)
	 * 空间复杂度O(n)
	 * 需要维护两个数组 leftMax 和 rightMax
	 * @param height
	 * @return
	 */
	public int trap(int[] height) {
		int n = height.length;
		if(n==0) {
			return 0;
		}
		
		int[] leftMax = new int[n];
		leftMax[0] = height[0];
		for(int i = 1;i < n;++i) {
			leftMax[i] = Math.max(leftMax[i-1], height[i]);
		}
		
		int[] rightMax = new int[n];
		rightMax[n-1] = height[n-1];
		for(int i = n-2;i >=0;--i) {
			rightMax[i] = Math.max(rightMax[i+1], height[i]);
		}
		
		int res = 0;
		for(int i= 0;i<n;i++) {
			res += Math.min(leftMax[i], rightMax[i]) - height[i];
		}
		return res;
	}

(2) 单调栈

	/**
	 * 2.单调栈
	 * 时间复杂度O(n)
	 * 空间复杂度O(n)
	 * @param height
	 * @return
	 */
	public int trap2(int[] height) {
		int res = 0;
		Deque<Integer> stack = new LinkedList<Integer>();
		int n = height.length;
		for(int i = 0;i < n;i++) {
			while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
				int top = stack.pop();
				if(stack.isEmpty()) {
					break;
				}
				int left = stack.peek();
				int curWidth = i-left-1;
				int curHeight = Math.min(height[left], height[i]) - height[top];
				res += curWidth * curHeight;
						
			}
			stack.push(i);
		}
		return res;
	}

(3) 双指针

思路:注意到下标 i 处能接的雨水量由 leftMax[i] 和 rightMax[i] 中的最小值决定。由于 leftMax 是从左往右计算,数组 rightMax 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。
维护两个指针 left 和 right,以及两个变量 leftMax 和 rightMax,初始时left=0, right=n−1, leftMax=0, rightMax=0。指针 left 只会向右移动,指针 right 只会向左移动,在移动指针的过程中维护两个变量 leftMax 和 rightMax 的值。

当两个指针没有相遇时,进行如下操作

  • 使用 height[left] 和 height[right] 的值更新 leftMax 和 rightMax 的值;
  • 如果 height[left] < height[right],则必有 leftMax < rightMax,下标 left 处能接的雨水量等于 leftMax−height[left],将下标 left 处能接的雨水量加到能接的雨水总量,然后将 left 加 1(即向右移动一位);
  • 如果 height[left] ≥ height[right],则必有 leftMax ≥ rightMax,下标 right 处能接的雨水量等于 rightMax−height[right],将下标 right 处能接的雨水量加到能接的雨水总量,然后将 right 减 1(即向左移动一位)。
  • 当两个指针相遇时,即可得到能接的雨水总量。
	/**
	 * 3.使用双指针
	 * 时间复杂度O(n) ,其中 n 是数组 height 的长度。两个指针的移动总次数不超过 n。
	 * 空间复杂度O(1) ,只需要使用常数的额外空间
	 * 维护两个指针 left 和 right,以及两个变量 leftMax 和 rightMax
	 * @param height
	 * @return
	 */
	public int trap3(int[] height) {
		int res = 0;
		int left = 0;
		int right = height.length-1;
		int leftMax = 0;
		int rightMax = 0;
		while(left < right) {
			leftMax = Math.max(leftMax, height[left]);
			rightMax = Math.max(rightMax, height[right]);
			if(height[left] < height[right]) {
				res += leftMax - height[left];
				++left;
			}else {
				res += rightMax - height[right];
				--right;
			}
		}
		return res;
	}

1.6 (lee-344) 反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]

(1) 递归

	/*
	 * 1.递归
	 * 基本操作:交换一次对称位置的字符
	 * 结束条件:交换的位置重叠或者超过i>=j
	 */
	public void reverseString(char[] s) {
		int len = s.length;
		int start = 0;
		int end = len-1;		
		reverse(start,end,s);	
		System.out.print(s);
	}	
	private void reverse(int start, int end, char[] s) {
		if(start >= end) {
			return;
		}
		reverse(start+1, end-1, s);
		swap(start,end,s);
	}
	private void swap(int start, int end, char[] s) {
		char temp = s[start];
		s[start] = s[end];
		s[end] = temp;		
	}

(2) 双指针

思路:对于长度为 N 的待被反转的字符数组,我们可以观察反转前后下标的变化,假设反转前字符数组为 s[0] s[1] s[2] … s[N - 1],那么反转后字符数组为 s[N - 1] s[N - 2] … s[0]。比较反转前后下标变化很容易得出 s[i] 的字符与 s[N - 1 - i] 的字符发生了交换的规律,因此我们可以得出如下双指针的解法:

将 left 指向字符数组首元素,right 指向字符数组尾元素。
当 left < right:
    交换 s[left] 和 s[right];
    left 指针右移一位,即 left = left + 1;
    right 指针左移一位,即 right = right - 1。
当 left >= right,反转结束,返回字符数组即可。
	/*	
	 * 2.这道题使用双指针更快
	 * 时间复杂度O(n),,其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
	 * 空间复杂度O(1)
	 */
	public void reverseString1(char[] s) {
		int len = s.length;
		int start = 0;
		int end = len-1;
		while(start <= end) {
			char c = s[start];
			s[start++] = s[end];
			s[end--] = c;
		}
		System.out.print(s);
    }

1.7 (lee-31) 下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

输入:nums = [1,2,3]
输出:[1,3,2]

	/* 
	 * 两遍扫描:双指针、交换、反转
	 * 时间复杂度:O(n)
	 * 空间复杂度:O(1)
	 */
	public void nextPermutation(int[] nums) {
		int i = nums.length - 2;
		while(i >= 0 && nums[i+1] <= nums[i]) { //从后向前查找第一个顺序对
			i--;
		}
		if(i >= 0) {
			int j = nums.length - 1;
			while(j >= 0 && nums[j] <= nums[i]) {
				j--;
			}
			swap(nums,i,j); //交换nums[i] nums[j]
		}
		reverse(nums,i+1); //使用双指针反转  降序区间[i+1,n)
    }

	private void reverse(int[] nums, int start) {
		int i = start;
		int j = nums.length-1;
		while(i < j) {
			swap(nums,i,j);
			i++;
			j--;
		}	
	}

	private void swap(int[] nums, int i, int j) {
		int temp = nums[i];
		nums[i] = nums[j];
		nums[j] = temp;		
	}

2 练习链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值