Leetcode刷题--数组
说明
本文的刷题顺序是按照 代码随想录 来进行的,在这里记录自己做题的思路。
基本内容
数组是一种数据结构,他和链表经常会被用作比较。他们使用的场景不同。数组需要注意的有:
(1)内存地址是连续的。数组一旦初始化完成之后,他的长度就固定了,不能修改。如果需要修改数组的长度,那么肯定就是建了一个新的数组,可以实现对数组里面的元素进行删除或者增加。
(2)数组是有下标的。下标从0开始,可以通过下标迅速的访问到数组的元素,因为是通过内存地址来访问的,非常快。
Leetcode 题目
类型一 二分查找相关
第1题: 704 二分查找
题目描述:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
二分法使用的前提:
(1)数组是有序的。只有数组是有序的,才能对整段数据进行对半分。
(2)元素不重复。如果元素重复,那么返回的结果可能不唯一。
题解:
class Solution {
public int search(int[] nums, int target) {
//二分查找
//定义左中右三个下标,用于下面的循环
int left = 0;
int right = nums.length - 1;
int mid = (left + right) / 2;
//这里的符号是 <= , 我们定义的区间是一个闭区间,[left,right],所以left == right 的时候是有意义的
while(left <= right){
//更新mid
mid = (left + right) / 2;
if(nums[mid] == target){
return mid;
}else if(target < nums[mid]){
//左边找,更新right
right = mid - 1;
}else{
//右边找,更新left
left = mid + 1;
}
}
return -1;
}
}
考虑使用二分查找的场景:数组是有序的!
第2题: 35. 搜索插入位置
题目描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
题解:
class Solution {
public int searchInsert(int[] nums, int target) {
//二分查找
//定义左中右下标
int left = 0;
int right = nums.length - 1;
int mid;
while(left <= right){
//更新mid
mid = (left + right) / 2;
//判断大小
if(target == nums[mid]){
return mid;
}else if(target < nums[mid]){
//目标数小,往左边找,更新right
right = mid - 1;
}else{
//目标数大,往右边找,更新left
left = mid + 1;
}
}
//找不到,那就要找插入位置 left > right 了
//这里的 right + 1要好好琢磨一下,直接返回left好像也是可以的
return right + 1;
}
}
第3题: 34. 在排序数组中查找元素的第一个和最后一个位置
题目描述:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
题解:
class Solution {
public int[] searchRange(int[] nums, int target) {
//二分查找
int left = 0;
int right = nums.length - 1;
int mid;
while(left <= right){
//更新mid
mid = (left + right) / 2;
if(nums[mid] == target){
//找到,现在要找左右边界
//左边界
int l = mid;
while(l-1 >= 0 && nums[l-1] == target){
l--;
}
//有边界
int r = mid;
while(r+1 <= nums.length - 1 && nums[r+1] == target){
r++;
}
return new int[]{l,r};
}else if(nums[mid] < target){
//右边找
left = mid + 1;
}else{
//左边找
right = mid - 1;
}
}
return new int[]{-1,-1};
}
}
第4题: 69. Sqrt(x)
题目描述:给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
题解:
关键点:找到 k * k <= x的最大的k。这就需要定义一个辅助变量用来保存这个值。
class Solution {
public int mySqrt(int x) {
//二分查找法 --> 找到 k * k <= x 的最大k
int left = 0;
int right = x;
int ans = -1; //更新答案
while(left <= right){
//更新mid
int mid = (left + right) / 2;
//转成long,防止越界
if((long)mid * mid > x){
//太大,左边
right = mid - 1;
}else if(mid * mid <= x){
//太小,往右边找,也有可能这就是那个数,因为小数被舍弃
//更新ans
ans = mid;
//更新left
left = mid + 1;
}
}
return ans;
}
}
第5题: 367. 有效的完全平方数
题目描述:给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
进阶:不要 使用任何内置的库函数,如 sqrt 。
题解:
解法和上一题类似。
class Solution {
public boolean isPerfectSquare(int num) {
//二分查找法 就是看 k * k = x有没有解
int left = 0;
int right = num;
int mid;
while(left <= right){
mid = (left + right) / 2;
if((long)mid * mid == num){
return true;
}else if((long)mid * mid < num){
//往右
left = mid + 1;
}else{
//往左
right = mid - 1;
}
}
return false;
}
}
类型二 移除元素
第1题: 27. 移除元素
题目描述:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
思路1:暴力破解。那就是使用双层for循环进行遍历,外层的for循环用来找到是val的值的下标,找到的时候,里面再套一层for循环,把所有的数都往前移动一个位置。
思路2:双指针法。只需一层遍历。快慢指针,快指针用来遍历整个数组,慢指针用来表示这个数组有多少个需要保留的数。 当快指针遍历到这个数是要保留的数时,那么需要将慢指针的值用快指针的值进行覆盖,然后下标都往后移动。 如果快指针遍历到val值时,那么快指针自己移动就行。
题解:
class Solution {
public int removeElement(int[] nums, int val) {
//双指针法 --> 慢指针代表数组的大小,快指针遍历数据
int slow = 0;
for(int fast = 0; fast < nums.length; ++fast){
//如果当前的指针指向不是val,那这个数就是要保留的,slow要移动,fast也移动
//同时,slow下标的值被fast的值覆盖
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}else{
//要删除的,那就只有fast进行移动,slow不移动,那就不操作
}
}
return slow;
}
}
第2题: 26. 删除有序数组中的重复项
题目描述:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
思路:双指针法。也是快慢指针,这里判断是否重复的逻辑有所改变,因为数组是有序的,看连续的两个数是否有序直接使用 fast 和 fast - 1就可以,不能同时使用 slow 和 fast,因为他们不一定相邻。
题解:
class Solution {
public int removeDuplicates(int[] nums) {
//双指针法 --> 快指针遍历数组,慢指针用来确定这个数组中需要保存的个数
if(nums == null || nums.length == 0) return 0;
int slow = 1;
for(int fast = 1; fast < nums.length; ++fast){
//什么时候是重复的,就是 fast 和 fast - 1 处值相同的时候
if(nums[fast] != nums[fast - 1]){
//不相同,此时fast的值是需要保留的
nums[slow] = nums[fast];
slow++;
}else{
//重复的值,那么让fast自己移动即可。
}
}
return slow;
}
}
第3题: 283. 移动零
题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
思路:双指针法。和第一题的思路一样,最后再多一个步骤,将最后的数赋值成0即可。
题解:
class Solution {
public void moveZeroes(int[] nums) {
//双指针法。 和删除元素这题类似,把0删除,然后将最后几个数赋值成0.
int slow = 0;
for(int fast = 0; fast < nums.length; ++fast){
if(nums[fast] != 0){
//是要保留的
nums[slow] = nums[fast];
slow++;
}else{
//fast自己移动
}
}
//将最后的数赋值成0。
for(int i = slow; i < nums.length; ++i){
nums[i] = 0;
}
}
}
第4题: 844. 比较含退格的字符串
题目描述:给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,请你判断二者是否相等。# 代表退格字符。
如果相等,返回 true ;否则,返回 false 。
思路:双指针法。一个指针处理一个字符串,每次处理完之后就进行比较,这样可以避免过多的处理。
题解:
class Solution {
public boolean backspaceCompare(String s, String t) {
//双指针法。 一个指针遍历一个字符串,并且从后向前进行遍历,因为 # 只会影响他前面的字符
int sIndex = s.length() - 1;
int tIndex = t.length() - 1;
//定义两个数,用来记录两个字符串分别需要跳过的字符数
int sSkip = 0;
int tSkip = 0;
while(sIndex >= 0 || tIndex >= 0){
//处理第一个字符串
while(sIndex >= 0){
if('#' == s.charAt(sIndex)){
sSkip++;
//当前字符处理完毕
sIndex--;
}else{
//不是#,判断还有没有跳过的字符串
if(sSkip > 0){
sSkip--;
sIndex--;
}else{
break;
}
}
}
//while跳出之后, sIndex指向的位置就是不为 # 的位置
//处理第二个字符串
while(tIndex >= 0){
if('#' == t.charAt(tIndex)){
tSkip++;
tIndex--;
}else{
//不是#,判断还有没有跳过的字符串
if(tSkip > 0){
tSkip--;
tIndex--;
}else{
break;
}
}
}
//判断 t 和 s当前的字符是不是相同的
if(sIndex >= 0 && tIndex >= 0){
if(s.charAt(sIndex) != t.charAt(tIndex)){
return false;
}
}else{
//可能一个遍历完,一个没有遍历完,这也是false
if(sIndex >= 0 || tIndex >= 0){
return false;
}
}
//两个都大于0,并且数都是相等的,那就处理下一个字符
sIndex--;
tIndex--;
}
return true;
}
}
第5题: 977. 有序数组的平方
题目描述:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
思路:双指针法。左右两边向中间进行遍历。左右的平方肯定是最大的数,因为数组是有序的。
题解:
class Solution {
public int[] sortedSquares(int[] nums) {
//双指针法,两边从中间进行遍历
int left = 0;
int right = nums.length - 1;
int index = nums.length - 1;
int[] ans = new int[index + 1];
while(left <= right){
if(nums[left] * nums[left] <= nums[right] * nums[right]){
//右边大
ans[index--] = nums[right] * nums[right];
right--;
}else{
//左边大
ans[index--] = nums[left] * nums[left];
left++;
}
}
return ans;
}
}
类型三 滑动窗口
第1题: 209. 长度最小的子数组
题目描述:给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0
思路1:暴力破解。那就是使用双层for循环进行遍历。
思路2:滑动窗口,也是双指针法。就是维护一个窗口,让这个窗口不断的向前移动,当这个窗口的大小 >= target 的时候,看是否更新我们的最终结果。
题解:
class Solution {
public int minSubArrayLen(int target, int[] nums) {
//子序列长度
int sub = 0;
//结果
int ans = Integer.MAX_VALUE;
//窗口数值之和
int sum = 0;
//窗口左指针
int left = 0;
//遍历nums,并且定义右指针
for(int right = 0; right < nums.length; ++right){
//将当前数加入串口
sum += nums[right];
//窗口是否达到要求
while(sum >= target){
//当前窗口长度
sub = right - left + 1;
//更新结果
ans = (sub <= ans) ? sub : ans;
//收缩窗口,左指针向前移动
sum -= nums[left++];
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
第2题: 904. 水果成篮
题目描述:
在一排树中,第 i 棵树产生 tree[i] 型的水果。
你可以从你选择的任何树开始,然后重复执行以下步骤:
把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。
移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。
你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。
用这个程序你能收集的水果树的最大总量是多少?
思路:滑动窗口法。这题翻译成人话就是问最长的连续子串是多少,这个子串中最多只能包含两种不同的数。和上一题非常类似,但是窗口的维护不同。这题的关键是在怎么确定这个窗口是否满足条件,可以使用HashMap来做这样的事。
题解:
class Solution {
public int totalFruit(int[] fruits) {
//滑动窗口,双指针法。现在的窗口里面维护的是 最多两个不同的数字。
//关键在怎么确定当前的窗口里面只有两种数 --> 使用HashMap
//创建HashMap维护窗口
Map<Integer,Integer> map = new HashMap<>();
//最终结果
int ans = 0;
//窗口左指针
int left = 0;
//窗口右指针,并遍历水果
for(int right = 0; right < fruits.length; ++right){
//将当前数加入
map.put(fruits[right], map.getOrDefault(fruits[right], 0) + 1);
//如果水果超过3种,需要处理,直至变成两种为止
while(map.size() > 2){
//最左边的数去除
map.put(fruits[left], map.get(fruits[left]) - 1);
//如果变成0,将把这个水果去除
if(map.get(fruits[left]) == 0){
map.remove(fruits[left]);
}
left++;
}
//此时窗口是满足要求的
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
第3题: 76. 最小覆盖子串
题目描述:
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
思路:滑动窗口法。关键还是在怎么去维护这样一个滑动窗口。能处理好窗口的关系就可以了。
题解:
class Solution {
public String minWindow(String s, String t) {
//滑动窗口方式 --> 使用HashMap来判断当前的窗是否已经满足条件了
HashMap<Character,Integer> need = new HashMap<>(); //需要包含的所有的字符及其数量
HashMap<Character,Integer> window = new HashMap<>(); //滑动窗口中的字符
//滑动窗口参数
int left = 0; int right = 0;
//最终结果
int len = Integer.MAX_VALUE; //字符串的长度
int start = 0; //满足条件字符串的起始位置
int valid = 0; //window中满足字符的个数
//先处理好 need 这个集合
for(int i = 0; i < t.length(); ++i){
Character cur = t.charAt(i);
need.put(cur, need.getOrDefault(cur, 0) + 1);
}
//遍历 s 这个字符串
while(right < s.length()){
//获取当前字符
Character cur = s.charAt(right);
right++;
//查看这个字符是不是在need里面,不在的话直接跳过
if(need.containsKey(cur)){
//加入窗
window.put(cur, window.getOrDefault(cur, 0) + 1);
//是否达到要求
if(need.get(cur).equals(window.get(cur))){ //这里使用equals,防止越界导致的判断出错
valid++;
}
}
//看valid是否已经满足
while(valid >= need.size()){
//更新结果
if(right - left <= len){
len = right - left;
start = left;
}
//更新窗口
Character lCur = s.charAt(left); //窗口最左边的字符串
left++;
if(need.containsKey(lCur)){ //只有这个字符是需要的,才需要被处理,不然直接扔了
window.put(lCur, window.get(lCur) - 1);
//看是否还满足要求
if(need.get(lCur) > window.get(lCur)){
valid--;
}
}
}
}
//返回字符串
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
类型四 边界、螺旋相关
第1题: 9. 螺旋矩阵 II
题目描述:
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
思路:就是顺时针遍历一个二维数组,然后把数放进去,遵循好相应的原则就行,比如:左闭右闭,左闭右开等。这里使用左闭右闭的原则。
题解:
class Solution {
public int[][] generateMatrix(int n) {
//边界法 --> 每次都必须遵循左闭右闭的原则
int[][] ans = new int[n][n];
//边界
int top = 0, bottom = n - 1, left = 0, right = n - 1;
//不断上升的数
int count = 1;
while(count <= n * n){
//上边界,左到右的循环
for(int i = left; i <= right; ++i){ // i < right 确定右边是开的
ans[top][i] = count++;
}
top++;
//右边界,上到下的循环
for(int i = top; i <= bottom; ++i){
ans[i][right] = count++;
}
right--;
//下边界,从右到左的循环
for(int i = right; i >= left; --i){
ans[bottom][i] = count++;
}
bottom--;
//左边界,从下到上的循环
for(int i = bottom; i >= top; --i){
ans[i][left] = count++;
}
left++;
}
return ans;
}
}
第2题: 54. 螺旋矩阵
剑指 Offer 29. 顺时针打印矩阵
题目描述:
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
思路:和上一题的区别在于,现在是矩阵,行和列不固定。但基本思想一致。遵循好相应的原则就行,比如:左闭右闭,左闭右开等。这里使用左闭右闭的原则。
题解:
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
//边界
int left = 0, right = matrix[0].length - 1, top = 0, bottom = matrix.length - 1;
int count = (right + 1) * (bottom + 1);
while(res.size() < count){
//上边界,从左到右
for(int i = left; i <= right && res.size() < count; ++i){
res.add(matrix[top][i]);
}
top++;
//右边界,从上到下
for(int i = top; i <= bottom && res.size() < count; ++i){
res.add(matrix[i][right]);
}
right--;
//下边界,从右到左
for(int i = right; i >= left && res.size() < count; --i){
res.add(matrix[bottom][i]);
}
bottom--;
//左边界
for(int i = bottom; i >= top && res.size() < count; --i){
res.add(matrix[i][left]);
}
left++;
}
return res;
}
}