本着快速积极复习数据结构和算法基础的目的,按照LeetCode的标签,一个个过。根据标签复习总结相关的知识点,每个标签保证10道左右的刷题量(简单程度),目的是确保掌握这个知识点的基本使用方式。
数组
知识点
数组:把具有相同类型的若干元素按有序的形式组织起来,线性表的实现方式之一(另外一个是链表)
数组利用索引来记录每个元素在数组中的位置,索引从
0
开始,即数组A[m]
的索引值范围是[0,m-1]
,可通过索引快速访问数组中的元素,索引指向的是内存地址
数组中的元素在内存中连续存储,每个元素占用相同大小的内存(一开始数组定义好之后即每个元素就有相同大小的内存,即便这个时候还没有具体的值)
如上图可以看到
apples
的内存地址就是oranges
的内存地址加索引值
题867. 转置矩阵
题目
给定一个矩阵 A, 返回 A 的转置矩阵。
矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。
示例 1:
输入:[[1,2,3],[4,5,6],[7,8,9]]
A[3][3]
输出:[[1,4,7],[2,5,8],[3,6,9]]B[3][3]
示例 2:输入:[[1,2,3],[4,5,6]]
A[2][3]
输出:[[1,4],[2,5],[3,6]]B[3][2]
提示:
1 <= A.length <= 1000
1 <= A[0].length <= 1000来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/move-zeroes
题目解析
转置矩阵:
B[i][j] = A[j][i]
此处需要用到二维数组,对于一个二维数组
A[i][j]
而言,其行长度width为
A.length
,也称一维长度列长度height为
A[i].length
,一般使用A[0].length
,也称二维长度(某个一维数组的数组元素个数)转置之后得到的也是一个二维数组B,与A相比,刚好是行长度和列长度颠倒,即B的行长度为
A[0].length
,列长度为A.length
,例如A[2][3]
,那么转置后得到B[3][2]
只要用到二维数组,肯定会使用两个for循环,在两个for循环中遍历A的元素将其复制给B数组,其中有一个技巧点,当A是方阵即
width==height
的时候,可以简化循环,先遍历i到width,然后遍历j从i开始到height,然后直接B[i][j] = A[j][i]``B[j][i] = A[i][j]
即可(因为width==height,所以无需担心数组越界问题)
代码实现
class Solution {
public int[][] transpose(int[][] A) {
// 行
int width = A.length;
// 列
int height = A[0].length;
// 返回第一个元素大小为A[0]的长度,第二个大小为A的长度
int[][] B = new int[height][width];
// 考虑到方阵的话无需循环遍历,直接ij=ji
if (width == height) {
for (int i = 0; i<width; i++) {
for (int j = i; j<height; j++) {
B[i][j] = A[j][i];
B[j][i] = A[i][j];
}
}
} else {
for (int i = 0; i< height; i++) {
for (int j = 0; j < width; j++) {
B[i][j] = A[j][i];
}
}
}
// 另一种方法
// for (int i = 0; i< width; i++) {
// for (int j = 0; j < height; j++) {
// B[j][i] = A[i][j];
// }
// }
return B;
}
}
总结
1,二维数组的定义
int[][] array = new int[m][n]
2,二维数组获取数组大小
array.length
3,二维数组获取二维长度大小,即每个一维数组中有几个元素(多个一维数组组成的二维数组)
array[0].length
4,二维数组的遍历使用双层for循环:时间复杂度为
width * height
for (int i = 0; i< width; i++) { for (int j = 0; j < height; j++) { // ... } }
根据实际情况可以通过概念
i
和j
的范围来减少遍历次数
题283. 移动零
题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/move-zeroes
题目解析
题目很清晰,就是遍历数组将所有0元素往后挪,并且保证其他非0元素相对顺序不变。
我的思路这块想麻烦了,想的是直接二重循环,然后每次比较元素是否为0,如果不是的话,让这个元素和后面的元素进行置位,但这个里面存在多次冗余置换位置的操作,所以性能较低
class Solution { public void moveZeroes(int[] nums) { // 定义一个临时变量temp int temp = 0; for (int i=0; i<nums.length; i++) { for (int j=0; j<nums.length-1; j++) { if (nums[j] == 0) { // 前后元素进行位置交换 temp = nums[j+1]; nums[j+1] = nums[j]; nums[j] = temp; } } } } } // `[0,1,0,3,12]`,第一次内循环结束后为`[1,0,3,12,0]` 遍历4次 // 第一次内循环结束为`[1,3,12,0,0]` 遍历4次 // 继续遍历一直到i=nums.length-1=4为止,每次都遍历4次,总共遍历4*5=20次 // 但是实际上不需要j从0开始遍历,0元素已经挪动了;另外也不需要每个元素都遍历
优秀题解
双指针法
给两个指针,左指向已处理好的序列的尾部,右指向待处理的头部。
不断移动右指针,当右指向非零数的时候,都将左右指针指向的数进行交换,同时左指针右移
左指针左边均为非零数;右指针左侧到左指针都是零
每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变
class Solution { public void moveZeroes(int[] nums) { // 定义一个临时变量temp int temp = 0; // 定义左右指针 int left = 0; int right = 0; for (int i=0; i<nums.length; i++) { if (nums[right] != 0) { temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; left++; } right++; } } }
优化:移动的肯定是一个非0数和0进行交换,可以直接将非零数赋值给前面的零数,然后直接将right位的值为0(left!=right) 时间复杂度为O(n),空间复杂度为O(1)
另外注意当数组为空的时候,直接返回即可(注意临界值)
class Solution { public void moveZeroes(int[] nums) { // 数组为空,直接返回 if (nums == null) { //不等同于nums.length=0(如果nums为null,取length会报空指针异常) return; } // 定义一个临时变量temp int temp = 0; // 定义左右指针 int left = 0; int right = 0; for (int i=0; i<nums.length; i++) { if (nums[right] != 0) { // 移动的肯定是一个非0数和0进行交换,可以直接将非零数赋值给前面的零数,然后直接将right位的值为0(left!=right) // temp = nums[left]; // nums[left] = nums[right]; // nums[right] = temp; nums[left] = nums[right]; // 注意一定要加判断,否则如果left=right,结果为直接为0 if (left != right) { nums[right] = 0; } left++; } right++; } } }
总结
1,即使是Java语言没有指针,但也有指针思维去解决问题
2,变量交换:给一个临时变量完成值的转移操作
int temp = a; a = b; b = temp;
3,数组为null和数组长度为0的区别:
int[] array = null; 数组为空,此时array不指向任何对象;
int[] array = new array[0]; 定义一个长度为0的数组;
int[] array = new array[2]; 定义一个长度为2的数组,此时如果数组没有被初始化,默认的值为null;
一般先判断 nums 是否为空,然后再判断长度是否为0,因为可能报空指针异常。
if(array == null || array.length == 0) // array.length == 0 || array == null 这种写法不恰当,因为可能会由于array为null而导致空指针异常
题27. 移除元素
题目
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-element
题目解析
遍历数组将所有非val的值返回/去除数组中值等于val的元素
仔细想想将val换为0不就是题283,只不过这个题是去除0而非移动0,但是我们可以通过移动的方式将所有需要去除的元素放在数组最后,然后记录最新的一个val元素的下标即可(数组下标从0开始,最新的一个val下标即表示移除val后数组的大小)
复盘下283题,发现变量i没啥用,right的作用同i,所以优化了下
代码实现
class Solution {
public int removeElement(int[] nums, int val) {
// 若nums为空,直接返回0
if (nums == null) {
return 0;
}
int left = 0;
// 定义中间变量,用于元素互换
int temp = 0;
for (int right = 0; right< nums.length;) {
if (nums[right] != val) {
temp = nums[right];
nums[right] = nums[left];
nums[left] = temp;
// left即为最新的一个val的下标
left++;
}
right++;
}
return left;
}
}
// 移动n次,时间复杂度为O(n);空间复杂度为O(1)
优秀题解
双指针法——移动元素法(和本人解法一致,只不过没有用temp中间变量,也无需进行元素交换,直接赋值即可)
当 nums[j]与给定的值相等时,递增 j 以跳过该元素。
只要 nums[j] 不等于 val,我们就复制 nums[j]到 nums[i]并同时递增两个索引。
重复这一过程,直到 j 到达数组的末尾,该数组的新长度为 i
class Solution { public int removeElement(int[] nums, int val) { if (nums == null) { return 0; } int left = 0; for (int right = 0; right < nums.length;) { if (nums[right] != val) { nums[left] = nums[right]; left++; } right++; } return left; } } // 移动n次,left和right至少各遍历n次。时间复杂度为O(n);空间复杂度为O(1)
双指针法——当删除的元素很少的时候
当遇到要 nums[j] 等于 val的时候,将nums[i]与最后一个元素交换,并释放最后一个元素,实际上可使数组大小减1
当然交换过来的那个最后一个元素也有可能等于val,所以下一次迭代中还会进行检查(while循环,不断遍历数组直到数组中的每个元素都不等于val)
public static int removeElement(int[] nums, int val) { // 交换元素法 if (nums == null) { return 0; } int i = 0; int n = nums.length; while (i < n) { if (nums[i] == val) { // 将数组最后一个元素与其进行交换 nums[i] = nums[n-1]; // 减少数组大小 n--; // 不改变i的大小,继续判断是否nums[i] == val,从而避免交换过来的这个最后元素也等于val } else { i++; } } return n; } // i和n最多遍历n次,时间复杂度为O(n),赋值操作的次数==要删除元素的个数,如果删除的元素少的话效率会更高;空间复杂度为O(1)
总结
学会从283的思路解开27的题,给自己的赞!
同时通过两个优秀题解对于双指针的解法, 更深入理解双指针的应用。对于移动元素法,无需多余的交换,直接赋值即可。对于交换最后一个元素法,勿忘判断最后一个元素是否为val
题26. 删除排序数组中的重复项
题目
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 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。
你不需要考虑数组中超出新长度后面的元素。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
题目解析
道理同前两题,只不过val是变化的,根据left下标的值而定,left初始值为0,right初始值为1
如果right对应的值等于left对应的值,则不作任何操作,继续往后遍历
如果right对应的值不等于left对应的值,则将right对应的值覆盖掉++left对应的值
代码实现
class Solution {
public int removeDuplicates(int[] nums) {
int left = 0;
for (int right = 1; right < nums.length;) {
if (nums[right] != nums[left]) {
// 拷贝覆盖
nums[++left] = nums[right];
}
right++;
}
return left+1;
}
}
总结
此题忽略了一个点:数组是个排序数组,即意味着数组中的元素是有序排列的,那么其实可以直接使用nums[right] > nums[left]进行判断
要善于从题目中发现可攻破点
题80. 删除排序数组中的重复项II
题目
给定一个增序排列数组 nums ,你需要在 原地 删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 你不需要考虑数组中超出新长度后面的元素。
示例 2:输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 你不需要考虑数组中超出新长度后面的元素。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii
题目解析
一个排序数组
每个重复的元素只留两个
不重复的元素保持不变
可继续使用上述的双指针拷贝覆盖法
对于重复的元素要有两个,可使用一个计数器进行记录,当值为2的时候再遇到相同的元素就跳过
代码实现
class Solution {
public int removeDuplicates(int[] nums) {
int left = 0;
// 与nums[left]相同的元素个数,最多为1
int num = 0;
for (int right = 1; right < nums.length;) {
if (nums[right] == nums[left] && num != 1) {
nums[++left] = nums[right];
num++;
}
// 数组是排序数组,从小到大排序,不相同的元素即比之前的元素大
if (nums[right] > nums[left]) {
nums[++left] = nums[right];
// 遇到下一个不同的元素,计数器重新置0
num = 0;
}
right++;
}
return left+1;
}
}
优秀题解
利用排序数组
两种情况:
- left<2,则直接移动left
- right-2对应的值>left对应的值,拷贝覆盖
class Solution { public int removeDuplicates(int[] nums) { int left = 0; // 使用foreach,避免fori中不必要的临时变量i的消耗 for (int n: nums) { // left=0,1。0,1对应的值相同和不同都符合要求 // 第三个元素之后如果不相同,即大于第一个元素,则left=第三个元素下标,left++ if (left < 2 || n > nums[left-2]) { // 先赋值后++ nums[left++] = n; } } return left; } }
总结
擅于从题中发现技巧点