文章目录
- 27. 移除元素
- 26. 删除有序数组中的重复项
- 283. 移动零
- 844. 比较含退格的字符串
- 977. 有序数组的平方
27. 移除元素
我的思路:
简单来说,将要删除的元素放到数组的最后
当数组中的元素和val的值相同时,就和数组末尾的值进行交换。
所以,需要一个变量last指向数组的末尾。当last的位置已经放了和val相等的数值时,last向前移一位,即last–
在提交的过程中,发现还有一些特殊情况,比如:
测试用例1:
[]
0
测试用例2:
[1]
1
测试用例3
[0, 4, 4, 0, 4, 4, 4, 0, 2]
4
class Solution {
public int removeElement(int[] nums, int val) {
if (nums.length == 0) {
return 0;
}
int last = nums.length - 1;
while (last >= 0 && nums[last] == val) {
last--;
}
for (int i = 0; i <= last; i++) {
if (nums[i] != val) {
continue;
}
while (last >= 0 && nums[last] == val) {
last--;
}
if (last + 1 == i) {
break;
}
int tmp = nums[last];
nums[last] = nums[i];
nums[i] = tmp;
last--;
}
return last + 1;
}
}
官方解答,方式一:双指针
分析,题目要求删除数组中等于val的元素,因此输出数组的长度一定小于等于输入数组的长度,而且,题目要求不使用额外的数组空间,那么就可以把数组直接写在输入数组上。
可以使用双指针:右指针right指向当前将要处理的元素,左指针left指向下一个将要赋值的位置。
- 如果右指针指向的元素不等于val,那么,它一定是输出数组中的元素,就将右指针指向的元素复制到左指针位置,然后将左右指针同时右移
- 如果右指针指向的元素等于val,那么,它就不能在输出数组里,此时左指针不动,右指针右移一位
最后,在[0,left)中的元素都不等于val。
当右指针遍历完输入数组以后,left的值就是输出数组的长度。
这样的算法,最坏的情况是,输入的数组中没有等于val的值,左右指针各遍历了数组一次。
class Solution {
public int removeElement(int[] nums, int val) {
if (nums.length == 0) {
return 0;
}
int left = 0;
int right = 0;
while (right < nums.length) {
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}
}
官方解答 方法二:双指针优化(和我的思路差不多)
在题目中提到,元素的顺序可以改变,当要移除的元素恰好在数组的开头,可以将数组中的最后一个元素移动到数组的开头,取代要删除的元素。
实现方面,使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列。
如果左指针left指向的元素等于val,此时将右指针right指向的元素赋值到左指针left的位置,然后右指针right左移一位。如果赋值过来的元素恰好也等于val,可以继续把右指针指向的元素赋值过来(左指针left指向的等于val的元素的位置继续被覆盖),直到左指针指向的元素的值不等于val为止。
当左指针left和右指针right重合的时候,左右指针遍历完数组中的所有元素。
这样的方法两个指针在最坏的情况下合起来只遍历了数组一次。与上面的方法一不同的是,方法二避免了需要保留的元素的重复赋值操作。
class Solution {
public int removeElement(int[] nums, int val) {
if (nums.length == 0) {
return 0;
}
int left = 0;
int right = nums.length - 1;
//如果right初始化为nums.length,那么while循环的条件是<,赋值时是nums[left]=nums[right-1]
//如果哦right初始化为nums.length-1,那么while循环条件是<=,赋值时是nums[left]=nums[right]
//可以用nums=[1],val=1这个比较特殊的测试用例来思考
while (left <= right) {
if (nums[left] == val) {
nums[left] = nums[right];
right--;
} else {
left++;
}
}
return left;
}
}
26. 删除有序数组中的重复项
我的思路:使用LinkedHashSet去重,并且,LinkedHashSet可以记录添加元素的顺序
import java.util.LinkedHashSet;
class Solution {
public int removeDuplicates(int[] nums) {
LinkedHashSet<Integer> set = new LinkedHashSet<>();
for (int num : nums) {
set.add(num);
}
int index = 0;
for (Integer i : set) {
nums[index] = i;
index++;
}
return set.size();
}
}
其他思路:双指针
首先,题目中说明了数组是有序的,那么重复的元素一定会相邻。
要求删除重复的元素,实际上就是将不重复的元素移到数组的左侧。
考虑用两个指针,一个在前记作p,一个在后记作q,算法流程如下:
- 比较p和q为止的元素是否相等
- 相等,q后移一位
- 不相等,将q位置的元素复制到p+1位置上,p后移一位,q后移一位
- 重复1-3步,直到q等于数组长度
- 返回p+1,即为新数组的长度
class Solution {
public int removeDuplicates(int[] nums) {
int left = 0;
int right = 0;
int n = nums.length;
while (right < n) {
if (nums[left] != nums[right]) {
nums[left + 1] = nums[right];
left++;
}
right++;
}
return left + 1;
}
}
优化:如果数组中没有重复的元素,按照上面的算法流程,每次比较时nums[p]都不等于nums[q],因此,会将q指向的元素原地复制一遍,这个操作其实是不必要的
因此,可以添加一个小判断,当q-p>1时,才进行复制
class Solution {
public int removeDuplicates(int[] nums) {
int left = 0;
int right = 1;
int n = nums.length;
while (right < n) {
if (nums[left] != nums[right]) {
if (right - left > 1){
nums[left+1] = nums[right];
}
left++;
}
right++;
}
return left + 1;
}
}
283. 移动零
我的思路:
p指针指向当前要操作的位置,如果当前位置的数值不等于0,那么p后移一位,如果p指向的当前位置为0,那么就需要找出在p后面的不为0的数的位置q,将其值赋值到p的位置,并将q的位置赋值为0
根据题目所给的提示,如果数组的长度是 1 0 4 10^4 104,假设一个比较极端的情况,只有第一个数字是非零,后面的9999个数字都是零,那么,就会做很多无用功,导致执行用时花费很长时间
class Solution {
public void moveZeroes(int[] nums) {
int p = 0;
int q = 0;
int n = nums.length;
while (p < n && q < n) {
if (nums[p] != 0) {
p++;
} else {
q = p + 1;
while (q < n && nums[q] == 0) {
q++;
}
if (q < n) {
nums[p] = nums[q];
nums[q] = 0;
}
}
}
}
}
我的另一种思路
双指针p和q,一开始都指向数组位置0
- 如果p位置的值为0,那么就要看q位置的值是否为0
- 如果q位置的值为0,则,q后移一位
- 如果q位置的值不为0,则将q位置上的值赋值到p的位置上,然后q位置上的值设置为0,p和q都后移一位
- 如果p位置的值不为0,则,p和q都后移一位
class Solution {
public void moveZeroes(int[] nums) {
int p = 0;
int q = 0;
int n = nums.length;
while (p < n && q < n) {
if (nums[p] == 0) {
if (nums[q] != 0) {
nums[p] = nums[q];
nums[q] = 0;
p++;
}
} else {
p++;
}
q++;
}
}
}
其他思路
创建两个指针i和j,第一遍历的时候,指针j用来记录当前有多少非0元素。即遍历的时候每遇到一个非0元素,就将其往数组左边挪,第一次遍历完成后,j指针的下标就指向了最后一个非0元素的下标。
第二次遍历的时候,其实位置从k开始到结束,将剩下的这段区域内的元素全部置为0。
class Solution {
public void moveZeroes(int[] nums) {
int j = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[j++] = nums[i];
}
}
for (int i = j; i < nums.length; i++) {
nums[i] = 0;
}
}
}
844. 比较含退格的字符串
我的思路:使用栈来模拟
import java.util.Stack;
class Solution {
public boolean backspaceCompare(String s, String t) {
Stack<Character> ss = new Stack<>();
Stack<Character> st = new Stack<>();
char[] charS = s.toCharArray();
char[] charT = t.toCharArray();
for (char c : charS) {
if (c == '#' && !ss.isEmpty()) {
ss.pop();
} else if (c != '#') {
ss.push(c);
}
}
for (char c : charT) {
if (c == '#' && !st.isEmpty()) {
st.pop();
} else if (c != '#') {
st.push(c);
}
}
if (ss.size() != st.size()) {
return false;
}
while (!ss.isEmpty()) {
Character c1 = ss.pop();
Character c2 = st.pop();
if (!c1.equals(c2)) {
return false;
}
}
return true;
}
}
官方思路,方法一:重构字符串
看到这个题目,最容易想到的就是将给定的字符串中的退格符和应当被删除的字符都去除,还原给定字符串的一般形式。然后直接比较两个字符串是否相等即可。
用栈来处理遍历过程,每次遍历一个字符:
- 如果它是退格符,那么将栈顶弹出
- 如果是普通字符,将其压入栈中
说是用栈,但是,使用的StringBuilder来模拟栈
class Solution {
public boolean backspaceCompare(String s, String t) {
return build(s).equals(build(t));
}
public String build(String str) {
StringBuilder stringBuilder = new StringBuilder();
int length = str.length();
for (int i = 0; i < length; i++) {
char c = str.charAt(i);
if (c != '#') {
stringBuilder.append(c);
} else {
if (stringBuilder.length() != 0) {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
}
}
return stringBuilder.toString();
}
}
复杂度分析:
- 时间复杂度:O(N+M),其中N和M分别为字符串S和T的长度。因为需要遍历两个字符串各一次
- 空间复杂度:O(N+M),其中N和M分别是字符串S和T的长度。主要是使用StringBuilder还原出字符串的开销。
官方思路,方法二:双指针
一个字符是否会被删除,只取决于该字符后面的退格符,而与该字符前面的退格符无关。因此,可以逆序地遍历字符串,就可以立即确定当前字符是否会被删除掉。
具体地,定义skip表示当前待删除地字符的数量。每次我们遍历到一个字符:
- 如果该字符为退格符,则需要多删除一个普通字符,skip+1
- 若该字符是普通字符:
- 若skip为0,则说明当前字符不需要删去
- 若skip不为0,则说明当前字符需要删去,skip-1
这样,定义两个指针,分别指向两字符的末尾。每次我们让两指针逆序地遍历两字符串,直到两字符串能够各自确定一个字符,然后将这两个字符进行比较。重复这一过程直到找到的两个字符不相等,或者遍历完字符串为止。
class Solution {
public boolean backspaceCompare(String s, String t) {
int i = s.length() - 1;
int j = t.length() - 1;
int skipS = 0;
int skipT = 0;
while (i >= 0 || j >= 0) {
while (i >= 0) {
char c = s.charAt(i);
if (c == '#') {
skipS++;
i--;
} else if (skipS > 0) {
skipS--;
i--;
} else {
break;
}
}
while (j >= 0) {
char c = t.charAt(j);
if (c == '#') {
skipT++;
j--;
} else if (skipT > 0) {
skipT--;
j--;
} else {
break;
}
}
if (i >= 0 && j >= 0) {
if (s.charAt(i) != t.charAt(j)) {
return false;
}
} else {
if (i >= 0 || j >= 0) {
return false;
}
}
i--;
j--;
}
return true;
}
}
977. 有序数组的平方
我的思路:因为数组是按照非递减的顺序排列的,所以,数组两端的数的平方大于等于中间数的平方,所以从两头开始比较,然后从后往前向新数组中填数
class Solution {
public int[] sortedSquares(int[] nums) {
int[] res = new int[nums.length];
int left = 0;
int right = nums.length - 1;
int i = nums.length - 1;
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
res[i] = nums[left] * nums[left];
left++;
} else {
res[i] = nums[right] * nums[right];
right--;
}
i--;
}
return res;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是数组nums的长度
- 空间复杂度:O(1)。除了存储答案的数组以外,需要维护常量空间。
其他思路,双指针
如果给定数组中的数都是非负数,那么每个数平方后,数组仍然保持升序;如果数组nums中的所有数都是负数,那么将每个数平方后,数组会保持降序。
这样,就可以找到数组nums中负数与非负数的分界线neg,就可以用类似【归并排序】的方法。nums[0]到nums[neg]均为负数,nums[neg+1]到nums[n-1]均为非负数。当将数组nums中的数平方后,那么nums[0]到nums[neg]单调递减,nums[neg+1]到nums[n-1]单调递增。
由于得到了两个已经有序的子数组,因此可以使用归并的方法进行排序。使用两个指针分别指向neg和neg+1,每次比较两个指针对应的数,选择较小的那个放入答案并移动指针。当某一指针移至边界时,将另一指针还未遍历到的数一次放入答案。
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int neg = -1;
for (int i = 0; i < n; i++) {
if (nums[i] < 0) {
neg = i;
} else {
break;
}
}
int[] ans = new int[n];
int i = neg;
int j = neg + 1;
int index = 0;
while (i >= 0 || j < n) {
if (i < 0) {
ans[index] = nums[j] * nums[j];
j++;
} else if (j >= n) {
ans[index] = nums[i] * nums[i];
i--;
} else if (nums[i] * nums[i] > nums[j] * nums[j]) {
ans[index] = nums[j] * nums[j];
j++;
} else {
ans[index] = nums[i] * nums[i];
i--;
}
index++;
}
return ans;
}
}