这里介绍一种简单但非常有效的方式一一双指针。所谓的双指针其实就是两个变量,不一定真的是指针双指针思想简单好用,在处理数组、字符串等场景下很常见。看个例子,从下面序列中删除重复元素[1,2,2,2,3,3,3,5,5,7,8],重复元素只保留一个。删除之后的结果应该为[1,2,3,5,7,8]。我们可以在删除第人2时将将其后面的元素整体向前移动一次,删除第二个2时再将其后的元素整体向前移动一次,外理后面63和5都一样的情况,这就导致我们需要执行5次大量移动才能完成,效率大低。如果使用双指针可以方便的解决这个问题,如图:
首先我们定义两个指针slow、fast。slow表示当前位置之前的元素都是不重复的,而fast则一直向后找,直到找到与slow位置不一样的,找到之后就将slow向后移动一个位置,并将arrifast]复制给arr[slow],之后fast继续向后找,循环执行。找完之后slow以及之前的元素就都是单一的了。这样就可以只用一轮移动解决问题。
上面这种一个在前一个在后的方式也称为快慢指针,有些场景需要从两端向中间走,这种就称为对撞型指针或者相向指针,很多题目也会用到,我们接下来会看到很多相关的算法题。还有一种比较少见的背向型,就是从中间向两边走。这三种类型其实非常简单,看的只是两个指针是一起向前走(相亲相爱一起走),还是从两头向中间走(冲破千难万险来爱你),还是从中间向两头走(缘分已尽,就此拜拜)。
在删除的时候,从删除位置开始的所有元素都要向前移动,所以这题的关键是如果有很多值为val的元素的时候,如何避免反复向前移动呢?
本题可以使用双指针方式,而且还有三种,我们都看一下:
整体思想就是2.1.1中个绍的双指针的图示的方式,定义两个指针slow和fast,初始值都是0.Slow之前的位置都是有效部分,fast表示当前要访问的元素。这样遍历的时候,fast不断向后移动:
/**
* 方法1:使用快慢型双指针
*
* @param nums
* @param val
* @return
*/
public static int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
//其实就是走在前的快指针不断的将值不等于val的元素往前放,打造出一个没有val的数组
//slow就是这个没val的数组的索引
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
//最后剩余元素的数量
return slow;
}
对撞指针,有的地方叫做交换移除,核心思想是从右侧找到不是val的值来顶替左侧是val的值。什么意思呢? 我们看图,我们以nums = [0,1,2,2,3,0,4,2], val = 2为例:
上图完整描述了执行的思路,当left==right的时候,left以及左侧的就是删除掉2的所有元素了。实现代码:
/**
* 方法2:使用对撞型双指针
*
* @param nums
* @param val
* @return
*/
public int removeElement(int[] nums, int val) {
int left=0;
int right=nums.length-1;
while(left<=right){
//当左边元素为val,右边不为val时两边交换元素,把不是val的保留下来
if(nums[left]==val&&nums[right]!=val){
int temp=nums[left];
nums[left]=nums[right];
nums[right]=temp;
}
//当前面的元素不等于val时,left++,去寻找val,找到val时left不动
//等满足上方条件时,才会接着移动
if(nums[left]!=val) left++;
//跟上方一样,当后面元素等于val时移动,寻找不为val的元素,换到前面去
if(nums[right]==val) right--;
}
return left;
}
拓展本题还可以进一步融合上面两种方式创造出:“对撞双指针+覆盖”法。当nums[left]等于val的时候,我们就将nums[right]位置的元素覆盖nums[left],继续循环,如果nums[left]等于val就继续覆盖,则才让left++,这也是双指针方法的方法,实现代码:
/**
* 方法3:使用对撞型双指针+覆盖
* 就是left位置元素等于val时,就让right元素覆盖过去,然后right--,如果覆盖过去的元素还等于val就接着覆盖接着right--,直到这个位置元素不等于val或不满足条件时
* @param nums
* @param val
* @return
*/
public int removeElement(int[] nums, int val) {
int left=0;
int right=nums.length-1;
while(left<=right){
if(nums[left]==val){
nums[left]=nums[right];
right--;
}else{
left++;
}
}
return left;
}
另外,我们可以发现快慢型双指针留下的元素顺序与原始序列中的是一致的,而在对撞型中元素的顺序和原来的可能不一样了。
/**删除重复元素,只保留一个
* @param nums
* @return int
*/
public int removeDuplicates(int[] nums) {
//slow表示可以放入新元素的位置,索引为0的元素不用管
int slow=1;
for(int fast=1;fast<nums.length;fast++){
//fast会找到不相等的元素,将fast位置元素赋予slow位置上元素
//进而保留了同样相等元素的[slow-1],保留了相同的元素的第一个
if(nums[fast]!=nums[slow-1]){
nums[slow]=nums[fast];
slow++;
}
}
return slow;
}
/**删除重复元素,保留两个,要是保留k个,就把2改成k
* @param nums
* @return int
*/
public int removeDuplicates(int[] nums) {
int slow=2;
for(int fast=2;fast<nums.length;fast++){
if(nums[fast]!=nums[slow-2]){
nums[slow++]=nums[fast];
}
}
return slow;
}
最直接的方式是使用一个临时数组,第一遍查找并将所有的偶数复制到新数组的前部分,第二遍查找并复制所有的奇数到数组后部分。这种方式实现比较简单,但是用双指针可以有空间复杂度为O(1)的方法。
我们可以采用对撞型双指针的方法,只不过比较的对象是奇数还是偶数。如下图所示:
维护两个指针left=0和 right=arrlength-1,left从0开始逐个检查每个位置是否为偶数,如果是则跳过如果是奇数则停下来。然后right从右向左检查,如果是奇数则跳过偶数则停下来。然后交换arraylleftl和array[right]。之后再继续巡循环,直到left> =right。
/**方法一:对撞型双指针
* @param nums
* @return int
*/
public int[] sortArrayByParity(int[] nums) {
int left=0;
int right=nums.length-1;
while(left<=right){
if(nums[left]%2>nums[right]%2){
int temp=nums[left];
nums[left]=nums[right];
nums[right]=temp;
}
if(nums[right]%2==1) right--;
if(nums[left]%2==0) left++;
}
return nums;
}
/**方法二:快慢型指针
* @param nums
* @return int
*/
public int[] sortArrayByParity(int[] nums) {
int slow=0;
for(int fast=0;fast<nums.length;fast++){
if(nums[fast]%2==0){
int temp=nums[fast];
nums[fast]=nums[slow];
nums[slow]=temp;
slow++;
}
}
return nums;
}
这里介绍一种简单的方法: 两轮翻转方法如下:
1.首先对整个数组实行翻转,例如 [1,2,3,4,5,6,7] 我们先将其整体翻转成[7,6,5,4,3,2,1].
2.从k 处分隔成左右两个部分,这里就是根据k将其分成两组 [7,6,5] 和[4,3,2,1].
3.最后将两个再次翻转就得到[5,6,7] 和[1,2,3,4],最终结果就是[5,6,7,1,2,3,4]
代码如下:
/**通过两轮翻转实现,第一轮翻转,将所有元素翻转,第二轮翻转从k处分隔两部分,再翻转回来
* @param nums
* @return int
*/
public void rotate(int[] nums, int k) {
k%=nums.length;
reverse(nums,0,nums.length-1);
reverse(nums,0,k-1);
reverse(nums,k,nums.length-1);
}
public void reverse(int[] nums,int start,int end){
while(start<end){
int temp=nums[start];
nums[start]=nums[end];
nums[end]=temp;
start++;
end--;
}
}
数组中表示的数据可能是连续的,也可能是不连续的,如果将连续的空间标记成一个区间,那么就出现了下题:
这个题使用双指针也可以非常方便的处理,慢指针指向每个区间的起始位置,快指针从慢指针位置开始向后遍历直到不满足连续递增(或快指针达到数组边界),则当前区间结束: 然后将 slow指向更新为 fast + 1,作为下一个区间的开始位置fast继续向后遍历找下一个区间的结束位置,如此循环,直到输入数组遍历完毕。
/**快慢指针
* @param nums
* @return {@link List}<{@link String}>
*/
public static List<String> summaryRanges(int[] nums) {
List<String> res = new ArrayList<>();
// slow 初始指向第 1 个区间的起始位置
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
// fast 向后遍历,直到不满足连续递增(即 nums[fast] + 1 != nums[fast + 1])
// 或者 fast 达到数组边界,则当前连续递增区间 [slow, fast] 遍历完毕,将其写入结果列表。
//当fast节点走到最后的时候,就不会走后面的判断,就不会数组越界
if (fast + 1 == nums.length || nums[fast] + 1 != nums[fast + 1]) {
// 将当前区间 [slow, fast] 写入结果列表
StringBuilder sb = new StringBuilder();
sb.append(nums[slow]);
if (slow != fast) {
sb.append("->").append(nums[fast]);
}
res.add(sb.toString());
// 将 slow 指向更新为 fast + 1,作为下一个区间的起始位置
slow = fast + 1;
}
}
return res;
}
/**
* 寻找缺失的区间
*
* @param nums
* @param lower
* @param upper
* @return {@link List}<{@link String}>
*/
public static List<String> findMissingRanges(int[] nums, int lower, int upper) {
List<String> res = new LinkedList<>();
// prev设为lower-1,判断是否有第一个区间lower->num[0]
int prev = lower - 1, cur = 0;
for (int i = 0; i <= nums.length; i++) {
// 当遍历到length时,设置cur为upper+1,判断是否存在最后一个区间
cur = i == nums.length ? upper + 1 : nums[i];
// 如果上一个数和当前数相差大于1,说明有区间
if (cur - prev > 1) {
int from=prev+1;
int to=cur-1;
if(from==to){
res.add(String.valueOf(from));
}else{
res.add(from+ "->" + to);
}
}
prev = cur;
}
return res;
}
首先要考虑用什么来存储字符串,如果是长度不可变的char数组,那么必须新申请一个更大的空间。如使用长度可变的空间来管理原始数组,或者原始数组申请得足够大,这时候就可能要求你不能申请O(n)小的空间
首先是如果长度不可变,必须新申请一个更大的空间,然后将原始数组中出现空格的位置直接替换成%20即可,代码如下:
/**
* 方法1:创建新的字符串
*
* @param str
* @return
*/
public static String replaceSpace1(StringBuffer str) {
String res = "";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == ' ')
res += "%20";
else
res += c;
}
return res;
}
对于第二种情况,首先想到的是从头到尾遍历整个字符串,遇到空格的时候就将其后面的元素向后移动2人位置,但是这样的问题在前面说过会导致后面的元素大量移动,时间复杂度为O(n^2),执行的时候非常容易超时。
比较好的方式是可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为:新串的长度=原来的长度+2*空格数目
接下来从字符串的尾部开始复制和替换,用两个指针fast和slow分别指向原始字符串和新字符串的末尾然后: slow不动,向前移动fast:
若指向的不是空格,则将其复制到slow位置,然后fast和slow同时向前一步:若fast指向的是空格,则在slow位置插入一个%20,fast则只移动一步循环执行上面两步,便可以完成替换。详细过程如下:
/**
* 方法2:在原数组基础上改
*
* @param str
* @return
*/
public static String replaceSpace2(StringBuffer str) {
if (str == null)
return null;
int numOfblank = 0;//空格数量
int len = str.length();
for (int i = 0; i < len; i++) { //计算空格数量
if (str.charAt(i) == ' ')
numOfblank++;
}
str.setLength(len + 2 * numOfblank); //设置长度
int fast = len - 1; //两个指针
int slow = (len + 2 * numOfblank) - 1;
while (fast >= 0 && slow > fast) {
char c = str.charAt(fast);
if (c == ' ') {
fast--;
str.setCharAt(slow--, '0');
str.setCharAt(slow--, '2');
str.setCharAt(slow--, '%');
} else {
str.setCharAt(slow, c);
fast--;
slow--;
}
}
return str.toString();
}