一. 双指针
283. 移动零
解决这个问题的一个有效方法是使用双指针技巧。这个技巧可以帮助我们在一次遍历中完成任务,同时保持非零元素的相对顺序,并将所有0移动到数组末尾。这种方法的关键思想是维护一个“慢指针”(left
)和一个“快指针”(right
)。快指针用于遍历数组,而慢指针用于指示下一个还没有被处理的0
具体步骤如下:
- 初始化两个指针,
left
和right
,都指向数组的开始。 - 遍历数组,使用
right
指针作为遍历指针。 - 当
right
指针指向的元素不为0时,我们检查left
和right
是否指向相同的元素:- 如果不是,那么我们交换
left
和right
指针所指向的元素,然后移动left
指针到下一个位置。 - 如果是,这意味着到目前为止我们还没有遇到0,所以只需要移动
left
指针。
- 如果不是,那么我们交换
- 无论是否进行了交换,只要
right
指针指向的元素被处理过(不管是交换还是确认为非0),right
指针都需要向前移动。 - 重复这个过程,直到
right
指针遍历完整个数组。
class Solution {
public void moveZeroes(int[] nums) {
int left = 0;
for(int right = 0; right < nums.length; right ++){
if(nums[right] != 0) {
//如果不是指向同一位置,证明还有0没有处理,left还指向的是没有处理的0
if(left != right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
//left和right在一个位置,证明当前的元素不是0, left和right一起移动
left ++;
}
}
}
}
11. 盛最多水的容器
这种方法的基本思想是在数组的两端设置两个指针,分别表示容器的左右边界。然后,逐步移动两个指针,每次移动较短的一边,因为容器的容量是由较短的边界决定的,而移动较长的边界不会增加容器的容量。
这里是具体步骤:
- 初始化两个指针,
left
指向数组的开始,right
指向数组的末尾。 - 初始化一个变量
maxArea
来记录遇到的最大面积。 - 当
left < right
时,执行循环:- 计算当前左右边界形成的容器的面积,并更新
maxArea
。 - 比较
left
和right
指向的高度,移动较短的一边(如果左边较短,增加left
;如果右边较短,减少right
)。
- 计算当前左右边界形成的容器的面积,并更新
- 返回
maxArea
作为结果。
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while(left < right) {
int curArea = Math.min(height[left], height[right]) * (right - left);
maxArea = Math.max(curArea, maxArea);
if(height[left] < height[right]) {
left ++;
} else{
// ?? right ++ ??
right --;
}
}
return maxArea;
}
}
15.三数之和
思路:
-
排序: 首先对数组进行排序。排序能够让相同的元素聚在一起,从而使我们可以轻松跳过重复的元素,还能让双指针更高效地运作。
-
固定一个元素,转化为两数之和问题: 遍历排序后的数组,固定其中一个元素,将问题转化为在剩余的元素中找到两个数之和等于一个固定值的问题(即:
-nums[i]
)。这里需要注意跳过重复的元素,以避免得到重复的三元组。 -
双指针法寻找两数之和: 使用两个指针,一个从左边开始,一个从右边开始,在固定元素的右侧查找两个数的和等于固定值的元素。如果和小于目标值,移动左指针;如果和大于目标值,移动右指针;如果找到和等于目标值的两个元素,将它们与固定元素一起加入结果列表,并同时移动左右指针跳过重复的元素。
-
返回结果: 返回找到的所有满足条件的三元组。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
if (i > 0 && nums[i] == nums[i - 1]) { // 跳过重复的数字
continue;
}
int target = -nums[i];
int left = i + 1, right = n - 1;
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;
}
}
二. 滑动窗口
3.无重复字符的最长子串长度
思路:
该问题的核心是要找到一个不含重复字符的最长子串。我们可以使用一个滑动窗口来实现这个目的。窗口是字符串中的一个连续部分,窗口内的字符不重复。我们的目标是找到最大可能的这样的窗口。
1. **初始化**:
- `left` 和 `right` 是窗口的左右边界,初始化为 0。
- `set` 用于存储窗口中的字符,确保窗口内的字符不重复。
- `ans` 用于存储最长子串的长度,初始化为 0。
2. **滑动窗口**:
- **右指针移动**: 如果 `right` 指向的字符不在 `set` 中,表示这个字符可以加入当前窗口,不会导致重复。将该字符加入 `set`,并将 `right` 右移一位,扩大窗口。
- **左指针移动**: 如果 `right` 指向的字符已经在 `set` 中,说明加入这个字符会导致窗口内有重复字符。这时需要移动左指针 `left`,并从 `set` 中移除 `left` 指向的字符,缩小窗口,直到窗口内的字符不再重复。
- **更新结果**: 每次窗口大小变化后,都比较当前窗口大小(`right - left`)与 `ans`,并将较大值存储在 `ans` 中。
3. **结束**: 当 `right` 到达字符串末尾时,停止滑动,返回 `ans`。
通过这样的滑动窗口机制,我们确保了窗口内的字符始终不重复,同时还能找到最大可能的窗口大小,即最长的不重复字符子串。
这个算法的关键之处在于使用 `set` 来快速判断窗口是否可以扩展,以及使用两个指针来维护窗口的大小。
举例:
用字符串 `s = "pwwkew"`,来演示这个算法。
1. **初始化**: `left = 0`, `right = 0`, `ans = 0`, `set` 是空的。
2. **第一步**: `right` 指向 'p',将其添加到 `set`,然后将 `right` 移动到下一个字符。窗口现在包含 "p",长度为 1。`ans` 更新为 1。
3. **第二步**: `right` 指向 'w',将其添加到 `set`,然后将 `right` 移动到下一个字符。窗口现在包含 "pw",长度为 2。`ans` 更新为 2。
4. **第三步**: `right` 再次指向 'w',这个字符已经在 `set` 中,所以左指针 `left` 需要移动。从 `set` 中移除 'p',然后将 `left` 移动到下一个字符。窗口现在包含 "w",长度为 1。
5. **第四步**: `right` 再次指向 'w',这个字符还在 `set` 中,所以 `left` 需要继续移动。从 `set` 中移除 'w',然后将 `left` 移动到下一个字符。窗口现在为空。
6. **第五步**: `right` 指向 'k',将其添加到 `set`,然后将 `right` 移动到下一个字符。窗口现在包含 "k",长度为 1。
7. **第六步**: `right` 指向 'e',将其添加到 `set`,然后将 `right` 移动到下一个字符。窗口现在包含 "ke",长度为 2。
8. **第七步**: `right` 指向 'w',将其添加到 `set`,窗口现在包含 "kew",长度为 3。`ans` 更新为 3。
9. **结束**: `right` 已经到达字符串的末尾,算法结束,返回 `ans = 3`。
通过这个例子,我们可以看到滑动窗口是如何逐步扩展和收缩的,以确保窗口内的字符始终不重复,同时尽量增大窗口的大小。希望这个具体的例子能够帮助你理解这个算法!
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, left = 0, right = 0;
while (right < n) {
if (!set.contains(s.charAt(right))) {
set.add(s.charAt(right++));
ans = Math.max(ans, right - left);
} else {
set.remove(s.charAt(left++));
}
}
return ans;
}
总结:
1. 滑动窗口用while 不用for,因为用for的话i是fast,那么fast一直在动,如果用while的话,可以有时动fast,有时动left。
2.滑动窗口和双指针的循环问题:有以下几种方式:
(1).
int slow = 0;
for (int fast = 0; fast < arr.length; fast++){
.......}
(2).
int left = 0;
int right = arr.length;
while (left < right){
........}
(3). 滑动窗口
int left = 0;
int right = 0;
while ( right < arr.length) {
.......}
438. 找到字符串中所有字母异位词
思路:
1. 预处理
首先,我们计算字符串 ppp 的字符频率,并将其存储在数组 pCount
中。每个字符 ccc 的频率存储在索引 c−’a’c - \text{'a'}c−’a’ 处。这样,我们可以在常数时间内查找每个字符的频率。
2. 初始化滑动窗口
我们初始化一个大小为 0 的窗口,并定义两个边界:左边界 left
和右边界 right
。同时,我们使用一个名为 windowCount
的数组来跟踪窗口内字符的频率。
3. 滑动窗口
我们通过以下步骤移动窗口:
a. 扩展窗口: 将右边界的字符添加到 windowCount
计数器,并将右边界向右移动。 b. 收缩窗口: 如果窗口的大小超过 ppp 的长度,则我们需要移动左边界以收缩窗口,并从 windowCount
计数器中减去左边界字符的计数。 c. 检查异位词: 我们比较窗口计数器 windowCount
和 ppp 的计数器 pCount
。如果两者完全相同,则窗口内的字符构成 ppp 的异位词,我们将左边界添加到结果列表中。
4. 结果
我们继续移动窗口,直到右边界达到 sss 的末尾。在此过程中,每次找到 ppp 的异位词时,我们都将其起始索引添加到结果列表中。
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
public List<Integer> findAnagrams(String s, String p) {
// 结果列表
List<Integer> result = new ArrayList<>();
// 如果 s 的长度小于 p 的长度,直接返回空列表
if (s.length() < p.length()) return result;
// 用于存储 p 中字符的频率,26代表26个单词
int[] pCount = new int[26];
// 用于存储当前窗口中字符的频率
int[] windowCount = new int[26];
// 计算 p 中字符的频率
for (char c : p.toCharArray()) {
//a-'a'是0,b-'a'是1
pCount[c - 'a']++;
}
// 定义窗口的左右边界
int left = 0, right = 0;
// 滑动窗口
while (right < s.length()) {
// 将右边界的字符添加到窗口计数器
char rightChar = s.charAt(right);
windowCount[rightChar - 'a']++;
right++;
// 如果窗口大小大于 p 的长度,则需要缩小窗口
if (right - left > p.length()) {
char leftChar = s.charAt(left);
windowCount[leftChar - 'a']--;
left++;
}
// 如果窗口计数器与 p 的计数器匹配,则找到一个异位词
if (Arrays.equals(pCount, windowCount)) {
result.add(left);
}
}
return result;
}
5. 最长回文子串
思路:
中心扩展法的基本思想是:回文串是对称的,我们可以从它的中心展开,并检查两边的字符是否相同。
- 对于长度为
n
的字符串,可能的回文中心有2n-1
个(包括n
个字符和n-1
个字符之间的位置),因为回文中心可以是一个字符(对于奇数长度的回文),也可以是两个字符之间的空隙(对于偶数长度的回文)。 - 从每一个可能的回文中心开始扩展,向两边比较,如果两边的字符相同,则继续扩展;如果不同,则停止扩展,并记录当前的最长回文串。
- 遍历所有可能的回文中心,更新并记录最长的回文子串。
- 方法
expandAroundCenter
是核心,它负责以left
和right
为中心扩展,直到不能形成回文为止。这里的left
和right
允许相同(针对奇数长度的回文)或相邻(针对偶数长度的回文),从而处理了回文长度奇偶的问题。 - 在主方法
longestPalindrome
中,通过循环遍历每一个字符(以及每两个字符间的位置)作为可能的回文中心,调用expandAroundCenter
方法获取以该中心扩展得到的最长回文串的长度。 - 若找到更长的回文子串,则更新记录的起始和结束位置,最终通过这些位置获取并返回最长回文子串。
class Solution {
public String longestPalindrome(String s) {
if(s.length() == 0 && s == null){
return null;
}
// 记录最长回文子串的起始和结束位置
int start = 0;
int end = 0;
for(int i = 0; i < s.length(); i ++){
// 分别取以s[i]为中心和以s[i]和s[i+1]为中心的最长回文串长度
//奇数长度子串
int len1 = getLength(s,i,i);
//偶数长度子串
int len2 = getLength(s,i,i+1);
int len= Math.max(len1,len2);
// 如果找到了更长的回文串,更新起始和结束位置
if(len > end - start){
//注意此处,不能用i - max/2 + 1. 因为这样会考虑不到奇数的时候,因为如果是.5 会舍去
start = i - (len-1)/2;
end = i + len/2;
}
}
//注意要end + 1
return s.substring(start, end + 1);
}
public int getLength(String s, int left, int right){
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
++right;
--left;
}
//注意此时为什么要减去1,因为移动后的left和right不符合要求,所以总长度要去掉移动后的,求长度是r-l+1,减去移动的2位,所以是减去1
return right - left - 1;
}
}