双指针 (doublePointer)
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;
}