介绍
本篇介绍的是标记元素的使用,很多需要找到正确元素的问题都可以通过将正确元素应该插入的位置单独放置一个标记来记录,这样可以达到原地解决问题的效果。
Start
27.RemoveElement 删除指定元素
描述
-
Given an array and a value, remove all instances of that value in place and return the new length.
Do not allocate extra space for another array, you must do this in place with constant memory.
The order of elements can be changed. It doesn't matter what you leave beyond the new length.
Example:
Given input array nums = [3,2,2,3], val = 3Your function should return length = 2, with the first two elements of nums being 2.
翻译: 给定一个数组一个目标值,移除那个数组中所有等于这个目标值的元素,然后返回剩下元素的个数
不要分配额外的空间给另一个数组,你需要在常数级别的内存空间完成这些操作,元素的顺序可以被改变并且在你留下什么元素并不重要
只要在你返回的元素的个数前面就好
举个例子, 给定的数组是 [3,2,2,3],目标值是 3 ,那么返回的元素个数就应该是2,原来数组的前2位也应该是2
参考思路
- 思路: 使用介绍所说的设立一个标记来标记正确元素的位置,只有碰到正确的元素了(value != target)时,标记位置才会更新元素并且移动
代码如下
public int removeElement(int[] nums, int val) {
// i表示当前正在遍历的元素的位置,j表示下一个正确元素应该插入的位置的位置
int j = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == val){
}else{
nums[j] = nums[i];
j++;
}
}
return j;
}
这是最直观的思维写出的,使用j来标记,如果碰到3了(example中的目标值3),那么什么也不做直接移动i,如果没碰到3,那么就把j位置的元素更新一下,然后移动j,最后返回的时候直接返回j即可(因为j是下一个元素应该插入的下标,假设最后遍历结束了,有5个正确元素,那么j的当前下标就是5,代表下一个应该插入的位置是数组第6个元素)
使用标记之后,时间复杂度是\(O(n)\),空间复杂度是\(O(1)\),符合要求
细微优化与简化后的代码如下
增加了一个 i != j
,意思是当i和j是一样的时候就没有必要再赋值了(i = j 时是自己给自己赋值),这样可以减少无意义赋值的操作
public int removeElement(int[] nums, int val) {
int j = 0;
for (int i = 0; i < nums.length; i++)
if (nums[i] != val && i != j++) nums[j-1] = nums[i];
return j ;
}
283. Move Zeroes 移动0元素
描述
-
Given an array nums, write a function to move all 0's to the end of it while maintaining the relative order of the non-zero elements.
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].
翻译, 给定一个数组nums,编写一个函数将所有0移动到它的末尾,同时保持非零元素的相对顺序。
例如,给定nums =[0,1,0,3,12],在调用函数后,nums应该是[1,3,12,0,0]。
参考思路
- 思路一,最直观的解法,使用一个等大的数组空间,进行一轮遍历,把非0的数字都放进去,剩下的元素补0,而java int类型的数组开辟出来的元素默认就是0,连补0的操作都可以省去了。时间复杂度为\(O(n)\),空间复杂度为\(O(n)\)
代码 第一步将值赋到aux数组中,第二步将aux数组的内容再赋回给nums
public void moveZeros(int[] nums) {
int[] aux = new int[nums.length];
for (int i = 0, j = 0; i < nums.length; i++)
if (nums[i] != 0) aux[j++] = nums[i];
for (int i = 0; i < nums.length; i++)
nums[i] = aux[i];
}
- 思路2,使用我们前面提到的标记元素,使用一个标记来记录正确的位置,最后将标记后面的元素全部赋成0,这样代码时间复杂度为\(O(n)\),空间复杂度为\(O(1)\)
代码如下
public void moveZero(int[] nums){
int k = 0;//nums[0,k) 都不为0
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) nums[k++] = nums[i];
}
for (int i = k; i < nums.length; i++) {
nums[i] = 0;
}
}
同样的上面解决思路大体的是对的,然而细节之处还可以优化
- 例如
[0,1,0,3,12]
这组数据,我们的做法是将前面三位赋值成为[1,3,12]
,然后将后面两位赋值为0,然而真的一定要这样吗,显然我们需要的0本身已经存在了,所以只需要交换相应的元素即可。 - 再有上面的交换操作,大家都知道交换操作需要三步和一个中间变量,即使是使用位运算可以减免一个变量但是也依旧逃不过需要三步,所以算法中一个常见的小技巧就是用赋值来代替交换,例如将1和0进行交换,只需要将0赋值成1,再将1赋值成0即可(因为我们交换的某一个元素永远是0)
- 还有就是上面第一题中的类似优化,如果i和k的下标是相同的,那么显然这次赋值就是自己给自己赋值,也是没有必要的,因此在这里也可以进行优化
最终代码如下
public void moveZeroes(int[] nums) {
int k = 0; //nums[0,k)范围内元素都不等于0
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
//如果k和i相等,那么就不必进行无意义的赋值,直接将k往后推一位即可
if (k != i) {
nums[k] = nums[i];
nums[i] = 0;
k++;
} else k++;
}
}
}
简化后如下
public void moveZeroes(int[] nums) {
int k = 0; //nums[0,k)范围内元素都不等于0
for (int i = 0; i < nums.length; i++)
if (nums[i] != 0 && k++ != i) {//如果k和i相等,那么就不必进行无意义的赋值
nums[k - 1] = nums[i];
nums[i] = 0;
}
}
26.RemoveDuplicatesFromSortedArray 移除有序数组的重复元素
描述
-
Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length.
Do not allocate extra space for another array, you must do this in place with constant memory.
For example,
Given input array nums = [1,1,2],Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively. It doesn't matter what you leave beyond the new length.
翻译 给定一个排序的数组,移除里面重复的元素这样每一个元素只出现一次,并且返回新的长度
不要使用为新的数组开辟额外的内存空间,你必须在原数组的内存空间上操作
举个例子,给定一个输入数组[1,1,2]
你的方法应该返回 2 ,并且nums数组的前两位应该是[1,2]
ps:对于java语言来说,在不使用额外的内存空间的情况下数组长度是不能改变滴,所以这道题
可以转变为例如给定数组为[1,2,3,3,5,5],返回结果长度应该为4,数组可以变为[1,2,3,5,x,x] x代表任意字符即可
参考思路
- 注意题目中的条件是有序数组,这条信息很重要,要充分利用,依旧是使用标记非重复的元素的位置来完成
- k为正确的元素应该插入的位置,i为当前正在遍历的元素,如果当前遍历的元素等于应该插入的元素的前一个位置的元素(因为数组是有序的,所以相同的数字是连在一起的),那么就证明当前遍历的元素已经在前面存在过了,直接跳过,去看下一个,否则的话就赋值,然后看下一个
代码如下
public int removeDuplicates(int[] nums) {
//特殊情况的判断,如果数组为null或者0,直接返回0,如果是1个元素显而易见肯定是不重复的返回1
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return 1;
int k = 1; //arr[1,k)正确元素应当插入的位置,这里从1开始算,从0开始算也应该维护0的定义
for (int i = 1; i < nums.length; i++) {
//k是应当插入的位置,所以k-1就是应当插入的位置的前一个元素,k和k-1位置的元素不应当相同
if (nums[i] != nums[k - 1]) {
//如果判断通过,说明当前遍历的nums[i]是一个独立的数字(因为数字是有序的),将nums[k]的值变为nums[i]
nums[k] = nums[i];
k++;//k标记往后推一位
}
}
return k;
}
简化后代码如下
public int removeDuplicates(int[] nums) {
if (nums == null || nums.length ==0 ) return 0;
int k = 1;
for (int i = 1; i < nums.length; i++)
if (nums[i] != nums[k - 1]) nums[k++] = nums[i];
return k;
}
80.RemoveDuplicatesFromSortedArrayII 移除有序数组的重复元素II
描述
-
Follow up for "Remove Duplicates":
What if duplicates are allowed at most twice?For example,
Given sorted array nums = [1,1,1,2,2,3],Your function should return length = 5, with the first five elements of nums being 1, 1, 2, 2 and 3. It doesn't matter what you leave beyond the new length.
翻译:承接移出重复的数字
如果每个数字要求保留最多出现两次呢?例如,给定一个排序好的数组 nums=[1,1,1,2,2,3]
你的方法应该返回长度为5 , 并且nums前5个元素是 [1,1,2,2,3]
参考思路
- 这道题是上一道题的一点升级,意思是每个元素要保留2次,其余还是一样,其实懂得了上面那面的思路十分容易,加一个计数的变量来计算重复元素出现的次数,第一次重复出现了就继续赋值,第二次或者更多的次数出现了就跳过,直到遇到一个新的元素清空这个计数器即可。
代码如下
public int RemoveDuplicatesfromSortedArrayII(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int k = 1;
int count = 1;//思路与移出重复数字I的题目一样,只是这里多加一个变量用于计数,默认为1,因为我们是从第二个数开始遍历的
for (int i = 1; i < nums.length; i++) {
if ( nums[i] != nums[k - 1]) {
nums[k++] = nums[i];
count = 1;//每次寻找到新的数字的时候将count赋值为1
} else {
if (count >= 2) {//允许重复的数字出现2次,如果超过2次了,就跳过
continue;
}
nums[k++] = nums[i];
count++;
}
}
return k;
}
简化后如下
public int RemoveDuplicatesfromSortedArrayII(int[] nums) {
if (nums == null) return 0;
int k = 0;
int count = 0;
for (int i = 0; i < nums.length; i++) {
if ( k==0 || nums[i] != nums[k - 1]) {
nums[k++] = nums[i];
count = 1;
} else if(++count <= 2) nums[k++] = nums[i];
}
return k;
}
End
- 总结一下,本篇讲述的思想是使用一个标记来标记需要当前需要插入的元素的位置,并且举了几道题的例子来讲述如何使用,为了方便理解,举的例子都比较简单,都使用一个标记,但是相信如果理解了这种解决思路,遇到复杂的需要使用多个标记的,只要定义清楚,解决起来也不会困难。
- 另外中间有几道还讲了一些简单的优化,例如看看能不能将交换变成赋值, 或者减少无意义的赋值,这样也可以对算法进行一些常数级别的优化,也是不错的。下篇再见。