1.移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
此题相对较简单,其实我们只需要循环遍历数组,把满足条件的值(不等于目标值)依次放入数组即可
public int removeElement(int[] nums, int val) {
int index = 0;
for(int num : nums){
if(num != val){
nums[index++] = num;
}
}
return index;
}
上述这种方式,其实本质是双指针的思想,我们定义了后指针为我们需要从新赋予的值的下标,前指针为遍历元素时的下标。
使用前后指针,我们可以把12345(3)变成1245,满足题意
public int removeElement(int[] nums, int val) {
int back= 0;
for (int before= 0; before< nums.length; before++) {
if (nums[before] != val) {
nums[back++] = nums[before];
}
}
return back;
}
基于上述这种方式,我们还可以改为左右指针,
使用左右指针,我们可以把12345(3)变成12545,同样满足题意
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;
}
2.移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。
2.1.复制非零值
解题要素:
1、定义一个坐标,记录当前当前移动的元素的下标
2、当循环到某个元素不为0时,将该值赋值给需要移动的元素
伪代码示意
for(nums){
if( nums [i] 不等于0 ){
nums [move] = nums [i];
move ++;
}
}
move之后的元素设置为0
public void moveZeroes(int[] nums) {
if (nums==null) return;
int moveIndex = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[i] != 0) {
nums[moveIndex] = nums[i];
moveIndex ++;
}
}
for (int i = moveIndex; i < nums.length; i++) {
nums[i] = 0;
}
}
2.2.与零交换值
解题要素:定义一个坐标,记录当前执行交换操作的坐标。定义一个第三只手temp,用于两数交换。
伪代码示意
for(nums){
if( nums [i] 不等于0 ){
temp = 交换值;
交换值 = 循环值;
循环值 = temp;
move ++;
}
}
完整代码
public void moveZeroes(int[] nums) {
if(nums==null) return;
int exchange = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[i]!=0) {
int temp = nums[exchange];
nums[exchange ++] = nums[i];
nums[i] = temp;
}
}
}
3.删除排序数组中的重复项
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
解题思路:这里的解题思路和移动0类似,移动0中,我们如果不等于0就复制值到前面去,这里如果不等于前面的数,我们就复制值到前面去
伪代码示意
for(nums){
if( nums [move] 不等于 nums [i]){
move ++;
nums [move] = nums [i];
}
}
public int removeDuplicates(int[] nums) {
int moveIndex = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[moveIndex] != nums[i]) {
moveIndex ++ ;
nums[moveIndex] = nums[i];
}
}
return moveIndex + 1;
}
4.盛最多水的容器
给你 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。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
4.1.暴力双循环
通过如上图所示,我们不难得出,此题的关键我们可以等价于求上述长方形的最大面积。并且知道的长度(Y轴)由最短长度的决定,宽度(X轴)由两条左边相减决定。因此得出公式:面积 = Min(Y1,Y2)* (X2 –X1 );
通过双循环,穷举所有的面积,然后取最大面积值。
伪代码示意
for( 高1 ){
for( 高2 ){
求最小高;
求宽度;
S = 最小高 * 宽度
求最大S
}
}
public int maxArea(int[] height) {
int maxArea= 0;
for (int i = 0; i < height.length; i++) {
for (int j = i + 1; j < height.length; j++) {
int minheight = Math.min(height[i], height[j]);
int area = minheight* (j-i);
maxArea= Math.max(maxArea, area);
}
}
return max;
}
4.2.双指针:左右指针最短收缩
在双重for循环中,我们穷举了所有的面积,但是,有没有一些方法,可以过滤掉我们提前就能判断出该面积一定是小于之前的面积?
定义左右两个指针,这时候所能形成的最长的宽,就能确定了,此时的面积,作为初始最大面积,当我们宽度在缩小的同时,如果长度也在缩小,则面积也一定缩小;有且仅有在宽度缩短的时候,长度变长,才可能比初始面积大。
解题要素:
1、左右两边长度哪个小,就收缩哪个;
2、 area = length * (right - left + 1);
伪代码示意
while( left < right ){
minHitht : left right 谁小谁收缩
width
s = minHitht * width
max(s,maxs)
}
public int maxArea(int[] height) {
int max = 0;
int left = 0;
int right = height.length-1;
while (left < right) {
int length = height[left]>height[right]?height[right--]:height[left++];
int area = length * (right - left + 1);
max = Math.max(max, area);
}
return max;
}
5.旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的 原地算法。
5.1.暴力:k 次整体右移一个单位
我们先实现一个操作,就是将整个数组集体向右移动一个单位,
因此,可以得出如下代码实现 12345转变成51234
public void rotate(int[] nums, int k) {
int length = nums.length-1;
int temp = nums[length];
for (int i = length; i > 0; i--) {
nums[i] = nums[i-1];
}
nums[0]=temp;
}
那么接下来我们将上面的过程循环K次,
public void rotate(int[] nums, int k) {
for (int j = 0; j < k; j++) {
int length = nums.length-1;
int temp = nums[length];
for (int i = length; i > 0; i--) {
nums[i] = nums[i-1];
}
nums[0]=temp;
}
}
我们发现其实还可以继续优化,当K大于 nums.length时候,我们出现了重复的移动
如int [] nums = {1,2,3,4,5,6};此时K = 2 + n * nums.length(n>1),其实我们只需要做K次操作就可以了,因此我们真正的循环次数是:K % nums.length次。
完整代码示意
public void rotate(int[] nums, int k) {
for (int j = 0; j < k % nums.length; j++) {
int temp = nums[nums.length-1];
for (int i = nums.length -1; i >0; i--) {
nums[i] = nums[i-1];
}
nums[0] = temp;
}
}
**备注:**现在(20210924)暴力破解不行了,超出时间限制。
5.2.空间换时间:数据拷贝再还原
我们可以用一个额外的数组来将每个元素放到正确的位置上,也就是原本数组里下标为 i的我们把它放到 (i+k)%数组长度的位置。然后再把新的数组拷贝到原数组中。
伪代码示意
temp[];
将nums[i]元素放到temp[i+k)% lenth];
nums[] = temp[];
因此我们可以得到如下代码
public void rotate(int[] nums, int k) {
int length = nums.length;
int[] temp = new int[length];
for (int i = 0; i < length; i++) {
temp[(i + k) % length] = nums[i];
}
for (int i = 0; i < length; i++) {
nums[i] = temp[i];
}
}
5.3.反转-分段反转
首选将整个数组进行反转,然后,接着分成两段,前段部门和后段部再反转。
解决要素:
参考反转字符串,如何进行反转
public void rotate(int[] nums, int k) {
k = k % nums.length;
reverse(nums, 0, nums.length-1);
reverse(nums, 0, k-1);
reverse(nums, k, nums.length-1);
}
public static void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[end];
nums[end] = nums[start];
nums[start] = temp;
start ++;
end --;
}
}
5.4.环状替换:依次占位
如果我们直接把每一个数字(位置为:current)移动到位置(current + k) % nums.length),我们可以把被替换的数字保存在变量 temp里面,作为下一次移动的数据。运气好的情况下,一次轮询就能移动完成,运气不好的时候,就需要换个坐标重新轮询。
当nums.length和k的最大公约数等于1的时候,1次遍历就可以完成交换
当nums.length和k的最大公约数不等于1的时候:需要最大公约数次遍历,也就是换个坐位重新移动。
如下图所示。
public void rotate(int[] nums, int k) {
k = k % nums.length;
//移动次数等于数组长度,代表移动完成
int count = 0 ;
for (int start = 0; count < nums.length; start++) {
//当前移动的位置
int current = start;
//当前要移动的元素
int prev = nums[start];
do {
//需要霸占的元素的位置
int next = (current + k) % nums.length;
//将需要霸占位置的元素存储到temp中
int temp = nums[next];
//将需要移动的元素放到需要霸占的位置上
nums[next] = prev;
//将temp中的元素作为下一次需要移动的元素
prev = temp;
//下一次需要移动元素的位置
current = next;
count ++;
}while(start != current);
}
}
6.合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明:
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
提示:
-10^9 <= nums1[i], nums2[i] <= 10^9
nums1.length == m + n
nums2.length == n
6.1.遍历+合并+排序
循环nums2,将nums2的元素追加到nums1的尾部中,然后对nums1进行排序。
伪代码示意
for(第二个数组的数量){
第一个数组的最后一个位置 = 第二个数组的第一个元素
第一个元素的最后一个位置 ++;
}
完整代码
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < n; i++){
nums1[m++] = nums2[i];
}
Arrays.sort(nums1);
}
6.2.双指针:从前往后对比
我们创建一个和nums1一样大小的数组newNums,然后,对比nums1中和nums2中的元素,将小的元素放到newNums中,当我们结束对比,说明至少有一方已经对比完毕了,剩下的一方的元素都是比新数组中的元素要大,因此将剩下的元素拷贝到新数组即可。
public void merge2(int[] nums1, int m, int[] nums2, int n) {
int[] sorted = new int[m + n];
int pm= 0, pn = 0, ps = 0;
while (pm < m || pn < n) {
if(pm == m) {
//nums1中的元素已经比较完毕,剩下的都是nums2
sorted[ps ++] = nums2[pn ++];
} else if(pn == n) {
//nums2中的元素已经比较完毕,剩下的都是nums1
sorted[ps ++] = nums1[pm ++];
} else if(nums1[pm] < nums2[pn]) {
//nums1小,取nums1的元素
sorted[ps ++] = nums1[pm ++];
} else {
//nums2小,取nums2的元素
sorted[ps ++] = nums2[pn ++];
}
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
7.加一
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
示例 2:
输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。
解题思路:
1、尾数加1如果不等于10.则直接返回;
2、如果尾数加1等于10,则尾数变为0,并进入下一轮循环;
3、如果循环完毕,即{9,9,9}这种情况,则直接返回{1,0,0,0}
public int[] plusOne(int[] digits) {
for (int i = digits.length-1; i >= 0; i--) {
digits[i] = digits[i] +1;
if(digits[i] != 10) {
return digits;
}
digits[i] = 0;
}
int[] newDigits = new int[digits.length + 1];
newDigits[0] = 1;
return newDigits;
}
8.有效字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:
输入: s = “rat”, t = “car”
输出: false
说明:
你可以假设字符串只包含小写字母。
进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
借用外部空间排序对比
解题要素:
1、什么是异位词:两个字符串的组成元素一样,只是组成元素的位置不一样。
2、既然组成元素一样,只是位置不一样,则可以对两个字符串进行排序,这样组成元素和位置都一样了
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
char[] charArrayS = s.toCharArray();
char[] charArrayT = t.toCharArray();
Arrays.sort(charArrayS);
Arrays.sort(charArrayT);
//return Arrays.equals(charArrayS, charArrayT);
for (int i = 0; i < charArrayT.length; i++) {
if(charArrayS[i] != charArrayT[i]) {
return false;
}
}
return true;
}
hash比对
依据题意,只会出现小写字母,因此我们可以映射如下的hash,当然这里并不是直接使用hash表,而是借助hash的思想,使用当前小写字母减去a的ASCII的值,做Key,出现的次数做value。则我们可以形成如下索引映射表。
索引0映射a,索引1映射b…索引25映射z
。这样,我们设计一个计数器,当我们遍历s字符串的时候,记录对应索引中元素出现的次数,当我们遍历t字符串的时候,减去记录。如果两个字符串的组成元素相同的话,那么如下这个hash思想所建立的数组的,所有元素的值,都为0。(如果两个字符串的元素都相同,那么一边加,一边减,最后肯定减为0)
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i)-'a'] ++;
count[t.charAt(i)-'a'] --;
}
for (int i = 0; i < count.length; i++) {
if(count[i] != 0) return false;
}
return true;
}
9.螺旋矩阵
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
解题要素:
设定上下左右四个边界值:
左:从0开始,循环一轮后,左++,结束标志为是否需要从上到下循环;
右:从右开始(二维数组中,一维数组的最大长度),循环一轮后,右- -,结束标志为是否需要从右到左循环;
上:从0开始,循环一轮后,上++,结束标志为是否需要从下到上循环;
下:从下开始,循环一轮后(二维数组中,二维数组的总个数),下- -,结束标志为是否需要从左到右循环;
以左到有循环为例:保持上(top)不变,依次读取:matrix[top][0],matrix[top][1],matrix[top][2],当循环一轮完毕后,判断上(top)+1 的边界值和下边界值(bottom )是否相等,如果相等,说明所有元素已经读取完毕,则结束循环。重复读取。知道边界值相等。
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if(matrix.length == 0) {
return result;
}
int left = 0, right= matrix[0].length - 1, top = 0, bottom = matrix.length - 1, resultIndex = 0;
//int[] result = new int[(right + 1) * (bottom + 1)];
while (true){
//从左向右移动
for(int i = left; i <= right; i++) {
//result[resultIndex++] = matrix[top][i];
result.add(matrix[top][i]);
}
//右移边界值:向右移动完毕后,判断是否执行从上向下移动
if(top++ == bottom) break;
//从上向下移动
for(int i = top; i <= bottom; i++) {
//result[resultIndex++] = matrix[i][right];
result.add(matrix[i][right]);
}
//下移边界值:向下移动完毕后,判断是否执行从右向左移动
if(left == right--) break;
//从右向左移动
for(int i = right; i >= left; i --) {
//result[resultIndex++] = matrix[bottom][i];
result.add(matrix[bottom][i]);
}
//左移边界值:向左移动完毕后,判断是否执行从下向上移动
if(top == bottom-- ) break;
//从下向上移动
for(int i = bottom; i >= top; i --) {
//result[resultIndex++] = matrix[i][left];
result.add(matrix[i][left]);
}
//上移边界值:向上移动完毕后,判断是否执行从右向左移动
if(left ++ == right ) break;
}
return result;
}
总结 数组篇的题型解题思路
大多数数组的问题,都可以采用穷举的思路,也就是暴力破解。
在本章节中
两数之和,我们采用两次循环,将所有的元素组合都列举出来,然后从所有的结果中取最大值。
针对数组篇,做算法优化,常见的有:
1、hash优化,减少一层循环;
2、双指针移动,过滤无效循环