代码随想录第一天 | Leecode 704 二分查找、27 移除元素、977 有序数组的平方
前言
今天是我开始刷Leecode的第一天,同时这也是开通博客园第一篇博客。我希望能在每篇博客中记录下我做出每一道题的过程,为此我想先说明一下我的博客内容的结构。
- 题目描述:首先说明题目的要求以及测试用例,以及对应的题目的地址,以便于文章读者可以清楚知道当前题目的需求;
- 正确解法:为了不误导读者(如果有读者的话),我会先展示我的最终版提交代码,并解释实现过程的思路。同时如果有多个解法,会将多个解法都给出;
- 本人的错误解法:从错误的解法来展现本人在实现过程中遇到的问题,记录自己的思维过程,发现不足以便于更好地进步。
话不多说,接下来开始第一天的两道题目。
Leecode 704 二分查找
题目链接:https://leetcode.cn/problems/binary-search/
题目描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
- 输入:
nums= [-1,0,3,5,9,12],target= 9 - 输出: 4
- 解释: 9 出现在
nums中并且下标为 4
- 输入:
示例 2:
- 输入:
nums= [-1,0,3,5,9,12], target` = 2 - 输出: -1
- 解释: 2 不存在
nums中因此返回 -1
- 输入:
题目解法
二分查找思路解析
对于有序数组进行二分查找本身是非常简单一个算法,尚且记得我此前第一次写了一个猜数字的游戏(在一个范围内生成随机数,每次提交猜测结果后提示比当前更大还是更小,直至猜到正确答案),拿给我没有任何编程基础并且高考数学28分的女朋友(无贬义,但希望她不会看到这里)玩的时候,她也能直接通过二分查找来进行搜索。但尽管算法思想很简单,想要使用编程语言来进行实现还是具有一定的难度,因为不管再简单的算法都需要将其抽象转换为计算机能够读懂的编程语言,而这个过程中需要明确、清晰地定义其中的每一个细节(例如本题二分查找中每一次搜索中的区间范围定义),只有如此计算机才能正常运行并准确通过所有测试算例。
而对于本题,每次搜索时需要比较中点值与目标值的大小,随后根据比较结果来决定直接返回中点値,或是二分区间进一步搜索,而进一步搜索的这个过程可以考虑使用递归来实现。因此可以得到下面所示的算法结构:
- 处理特殊情况(当目标值不存在于数组中时,返回-1)
- 结束条件(当数组越分越小,最后只有一个元素的数组,此时如果该值等于目标值则返回)
- 递归搜索
- 如果中点值大于目标值,则在左侧递归搜索
- 如果中点值小于目标值,则在右侧递归搜索
对于上面思路,有一个关键点在于需要保存每一次搜索区间的范围,即区间的最左和最右两个序号(相当于是两个指针),并需要不断根据每一次中点的比较结果来更新这两个指针。如果使用递归来搜索,需要保证每次递归能够传递当前搜索区间的左右两个指针,因此需要添加这两个参数。而题目中只提供了nums和target输入,因此我通过默认参数的方式,可以在初次调用时输入这两个参数的默认值。
正确代码展示
class Solution {
public:
int search(vector<int>& nums, int target, int &start = 0, int &end = -5) { // 增加参数start, end;用于在递归过程中更新搜索区间
if(end == -5){
end = nums.size() - 1; // 初始化末尾下标
}
if (start > end){ // 如果当前数组为空,返回-1
return -1;
}
int mid = start + (end - start)/2; // 计算区间中点,特别地,当数组中只有一个元素的时候,start = mid = end
if (nums[mid] == target){ // 若区间中点恰好是搜索目标,则返回中点序号
return mid;
}
else if(nums[mid] > target){ // 若中点大于搜索目标值,则对区间进行二分,在左侧一半区间继续递归搜索
return search(nums, target, start, mid-1);
}
else{ // 若中点小于搜索目标值,则对区间进行二分,在右侧一半区间继续递归搜索
return search(nums, target, mid+1, end);
}
}
};
本人的错误解法
别看我上面分析得头头是道,但其实刚上手的时候也是一脸懵逼。此前基本没怎么刷过算法题,而且平时写作业过程中经常都借助Copilot自动补全,很多时候知道思路也能读懂代码,但是真的自己写的时候根本不记得标准库里方法的名称。就例如一上来需要求vector的长度需要使用.size()方法,我用了.length()无法通过。而在询问DeepSeek之后才知道:
- 求
vector的长度需要使用.size()方法,而求string类型的长度使用.length()方法.
第一版错误代码
我这里给出我的第一版代码,甚至不能通过编译的版本。如果有读者看到这里可以选择直接跳过,或是简单看一下看个乐子。本人只是为了记录我的思路和错误过程,把错误也记录下来可以让我避免下次再犯一样的错误。
class Solution {
public:
int start = 0; // 没有想到默认参数的方式,试图通过局部变量的方式来定义初始start指针
int search(vector<int>& nums, int target) {
int len = nums.size(); // 没有想到用维护左右两个指针的方式,而是希望递归数组,每一次递归截取一部分数组作为新的数组
if (nums[(len-1)/2] == target){
return start+(len-1)/2; // 终止条件,没有考虑目标值不存在于数组中的情况
}
else if (nums[(len-1)/2] < target){
start += (len+1)/2; // 当需要继续搜索右半部分数组时,更新左侧起始指针start
vector<int> subvec(start,len-1); // 我也不知道我为什么这样写。。太蠢了。。这里是想要新建子数组来进行递归搜索,但是这语法完全不对
return search(subvec, target);
}
else{
len = len/2-1;
vector<int> subvec(start, len); // 和上面一样,太蠢了。。不解释
return search(subvec,target);
}
}
};
第二版错误代码
再第一遍编译不能通过之后,我又重新进行了编写。首先是考虑到了使用默认参数的方式输入用于递归的参数;其次是考虑使用更新左右两个指针,而不是更新数组的方式来搜索(否则如果在子数组中找到了目标值,而需要返回在原数组中的序号会比较麻烦);最后又考虑了目标值不存在的情形,添加了新的终止条件。由此,得到了第二版代码:
class Solution {
public:
int search(vector<int>& nums, int target, int start = 0, int end = -1) {
if(end == -1){ // 由于数组长度不知道,考虑将end指针默认取到一个不可能取到的取值,在第一次执行时进行初始化
end = nums.size() - 1;
}
int mid = start + (end - start)/2;
if (start > end){ // 对终止条件进行抽象,具体解释见下文
return -1;
}
if (nums[mid] == target){
return mid;
}
else if(nums[mid] > target){
return search(nums, target, start, mid-1);
}
else{
return search(nums, target, mid+1, end);
}
}
};
对于上面代码中目标值不存在的终止条件start > end,分析过程如下:
- 如果维护的指针变成起始start指针大于end的情形时,说明在start = end = mid时,也即数组中只有一个元素的情况下,又进行了一次搜索,进而才会导致start > end。说明此时目标值一定不在数组中
上面代码看似没有什么问题了,但是实际还是有问题。使用该代码运行时,会遇到一些测试案例发生超出时间限制的情况。进一步分析可以知道,在一开始对于end进行默认初始化的时候,令end = -1,看似是不可能取到的值。但是若当start = end = 0的时候,数组中只有一个元素,而此时该元素大于目标值;那么接下来end会减1,此时会使得end又变成-1,再次递归会使得end再次变成数组长度-1,从而进入死循环。因此,考虑将初始默认参数从-1变成其他更小的任何负整数之后,就不会再出问题了。将其中的默认参数从-1变成-5即得到一开始所展示的正确代码。
Leecode 27 移除元素
题目链接:https://leetcode.cn/problems/remove-element/
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
- 更改
nums数组,使nums的前k个元素包含不等于val的元素。nums的其余元素和nums的大小并不重要。 - 返回
k。
正确解法
思路分析
这道题使用暴力解法非常简单,只需要从头到尾遍历一遍,遍历过程中删去目标值val即可。此时遍历的时间复杂度为 \(O(n)\),而每删去一个元素都会使得后面的元素往前进一位,这个操作隐含在了num.erase()方法中,这个删除操作的时间复杂度也是\(O(n)\),二者for循环叠加到一起那么该算法的总时间复杂度为\(O(n^2)\)。现在可以直接给出正确解法1,代码如下:
正确解法1 代码展示
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
for (int i = 0; i < nums.size();){
if(nums[i] == val){
nums.erase(nums.begin() + i); // 隐含了将nums.size()-1的过程,此时如果再i++,会跳过一个元素
}
else{
i++; // 只有当当前元素不等于要删去的值时,才进行i++
}
}
return nums.size();
}
};
思路优化 —— 解法2
上面算法中,从头到尾遍历一遍数组中的所有元素的过程不可避免,必须每个元素都验证一遍才可以确保可以删去其中所有待删除的元素。但是在原本数组上进行删去操作的时间复杂度过高,能否使用更好的算法来规避这个.earse()操作呢?为此我们设计下面算法:
- 使用一个指针遍历原数组,记录初始数组长度
- 如果当前元素不需要删去,则在另外一个数组中
.push_back()一个该元素 - 如果当前元素需要删去,则只需使得记录数组长度的变量-1
- 如果当前元素不需要删去,则在另外一个数组中
- 最后将令原数组等于新数组,再返回记录的数组长度即可
通过上面这个算法,就可以得到一个只需要\(O(n)\)时间复杂度的算法来进行移除元素的操作
解法2 代码展示
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
vector<int> newNums; // 新建数组,用于记录删去目标值之后的数组
int length = nums.size(); // 初始化长度
for(int i = 0; i < nums.size(); i++){
if(nums[i] == val){ // 值要删去
length--; // 更新长度
}
else{ // 值不用删
newNums.push_back(nums[i]); // 将该值放到新数组中
}
}
nums = newNums; // 令原数组等于新数组
return length;
}
};
解法3 双指针法
解法2虽然的确降低了时间复杂度,但是新建一个数组又会增大空间复杂度。因此可以采用双指针的方式,即使用快慢指针,将在原数组扫描的工作和新数组更新插入的工作都放到同一个数组中进行,由此我们可以得到双指针法的代码如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int length = nums.size();
int j = 0; // 初始化慢指针j
for(int i = 0; i < nums.size(); i++){ // i是快指针,每一次for循环都要++
if(nums[i] == val){ // 当前值需要删去
length--; // 无需操作数组,因为后续会被慢指针覆盖,此时只要使长度-1即可
}
else{ // 当前值无需删去
nums[j] = nums[i]; // 用快指针的值去覆盖慢指针的值
j++; // j是慢指针,只在不删值时++
}
}
return length;
}
};
这样我们使用双指针的方式就完美解决当前移除元素的问题。
Leecode 977 有序数组的平方
题目链接:https://leetcode.cn/problems/squares-of-a-sorted-array/
题目描述
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
- 输入:nums = [-4,-1,0,3,10]
- 输出:[0,1,9,16,100]
- 解释:平方后,数组变为 [16,1,0,9,100]排序后,数组变为 [0,1,9,16,100]
示例 2:
- 输入:nums = [-7,-3,2,3,11]
- 输出:[4,9,9,49,121]
题目解法
解法1 暴力解法思路
将上面题目拆分成两个部分,首先是遍历一遍数组,对每个元素取平方。随后再对数组进行排序操作,此时可以选择不同的排序算法(快速排序、插入排序等)。为了简单起见,我首先采用了冒泡排序。
解法1 代码展示
将算法拆分成取平方+冒泡排序,即可得到代码如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
nums[i] = nums[i] * nums[i]; //计算平方
}
for(int i = nums.size()-1; i > 0; i--){ //冒泡排序
for(int j = 0; j < i; j++){
if(nums[j] > nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
return nums;
}
};
分析这个算法的时间复杂度,首先计算平方的过程中时间复杂度为\(O(n)\),随后的冒泡排序时间复杂度为\(O(n^2)\),故总的时间复杂度为\(O(n) + O(n^2) = O(n^2)\)。可以看出这是一个时间复杂度很高的算法,提交之后所用时间为1129ms。如果将其中的冒泡排序换成是时间复杂度更低的排序算法(比如快速排序)可以将时间复杂度降低到\(O(n\log n)\),由此,可以得到下面解法2.
解法2 平方+快排
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
nums[i] = nums[i] * nums[i]; //计算平方
}
sort(nums.begin(), nums.end()); // 快速排序
return nums;
}
};
实测之后可知,如果使用快速排序,提交之后所用时间为11ms,已经大大降低了时间复杂度。但是我们接下来会考虑使用时间复杂度更低的算法。
解法3 思路
原本给定的数组已经是有序数组,但是在进行平方操作之后,会使得数组排序变成先减后增的趋势排列。但即便如此数组也仍然有一定的顺序,我们接下来考虑利用这种顺序来设计更好的算法。
- 先计算数组中每个数的平方,并新建一个数组用于存放新的排序好的数组
- 采用相向双指针的方式,从最左和最右开始遍历数组(数组中的最大值一定在最左或最右,现在比较两个指针指向的数据)
- 若最左大于最右,则在新数组中插入原数组最左的值,并使得最左指针+1
- 若最右大于最左,则在新数组中插入原数组最右的值,并使得最右指针-1
根据上面算法可以得到代码如下
解法3 代码(双指针法)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
nums[i] = nums[i] * nums[i]; //计算平方
}
vector<int> newNums = nums; // 新建一个数组,并进行拷贝构造
int i = 0; int j = nums.size()-1; // 初始化两个指针
for(int k = nums.size()-1; k >= 0; k--){ // 遍历更新新数组中所有的元素,从最后一个(序号nums.size()-1)一直到第一个(序号0)
if(nums[i] >= nums[j]){ // 将指针中数更大的放到新数组中,并同时移动指针
newNums[k] = nums[i];
i++;
}
else{
newNums[k] = nums[j];
j--;
}
}
return newNums;
}
};
使用上面双指针的算法,即可得到将时间复杂度降低到\(O(n)\),提交运行只需要0ms,可见时间复杂度降低明显.
今日小结
今天刷了3道Leecode数组简单题,除了因为对c++不太熟悉而导致第一题卡了大概一个小时以外,其他两道题都是只花了不到半小时都能想到暴力解法和双指针的解法(其实第一道应该最简单才对)。花了更多时间在写博客上,但是今天这博客写下来感觉还是很爽的,希望接下来每天都能坚持下去。
总结一下今天在这三道题学习到的地方
- 704 二分查找
vector的长度使用.size()方法- 复习了递归的写法(先写终止条件,随后再处理递归)
- 复习了默认参数的写法
- 27 移除元素
- 学习了双指针算法
- 977 有序数组的平方
- 复习了冒泡排序、快速排序
- 对于有一定顺序(或规律)的数据,在设计算法时最好充分考虑利用其规律,有可能大大提高算法效率
代码随想录第一天 | Leecode 704 二分查找、27 移除元素、977 有序数组的平方的更多相关文章
- 代码随想录训练营day 1 |704 二分查找 27移除算法
LeetCode 704.二分查找(C++) 题目链接 704.二分查找 题目描述:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 ...
- 【算法训练营day1】LeetCode704. 二分查找 LeetCode27. 移除元素
[算法训练营day1]LeetCode704. 二分查找 LeetCode27. 移除元素 LeetCode704. 二分查找 题目链接:704. 二分查找 初次尝试 看到题目标题是二分查找,所以尝试 ...
- 代码随想录训练营day 2 |977有序数组的平方 209.长度最小的子数组 (C++)
977.有序数组的平方 题目链接:977.有序数组的平方 题目描述:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序. 例子如下: 输入 ...
- 代码随想录第二天| 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
2022/09/22 第二天 第一题 这题我就直接平方后排序了,很无脑但很快乐啊(官方题解是双指针 第二题 滑动窗口的问题,本来我也是直接暴力求解发现在leetCode上超时,看了官方题解,也是第一次 ...
- 代码随想录算法训练营day23 | leetcode 669. 修剪二叉搜索树 ● 108.将有序数组转换为二叉搜索树 ● 538.把二叉搜索树转换为累加树
LeetCode 669. 修剪二叉搜索树 分析1.0 递归遍历树时删除符合条件(不在区间中)的节点-如何遍历如何删除 如果当前节点大于范围,递归左树,反之右树 当前节点不在范围内,删除它,把它的子树 ...
- 代码随想录算法训练营第二天| 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
977.有序数组的平方 :https://leetcode.cn/problems/squares-of-a-sorted-array/ 心得:周末再写... public class Solutio ...
- Leetcode之二分法专题-704. 二分查找(Binary Search)
Leetcode之二分法专题-704. 二分查找(Binary Search) 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 t ...
- LeetCode 704. 二分查找(Binary Search)
704. 二分查找 704. Binary Search 题目描述 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果 ...
- Java实现 LeetCode 704 二分查找(二分法)
704. 二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1. 示例 1 ...
- LeetCode 704.二分查找(C++)
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1. 示例 1: 输入: num ...
随机推荐
- Luogu P4933 大师 题解 [ 绿 ] [ 线性 dp ] [ dp 细节处理 ] [ 限制转移条件优化 ]
依据值域的 \(O(n^2)\) 做法 这种做法只适用于这种值域小的题,下一种做法才是求等差数列的通解. 我们定义 \(f[i][j]\) 表示以 \(h_i\) 为最后一个数,公差为 \(j\) 的 ...
- 支付宝 v3 自签名如何实现
今天在看文档的时候,发现支付宝新出了一个 v3 版本的接口调用方式,感觉有点意思,花了点时间研究了下这个版本要怎么实现自签名,大家有兴趣可以看看. 什么是支付宝 API v3 版本? 官网上给的解释是 ...
- datawhale-leetcode打卡:001-012题
这次这十二个题目属于是极限肝出来的,有两个参考了一下题解,还是很有意思.我会按照我个人的感觉去写这个东西. 螺旋矩阵(leetcode 054) 这个题目比较恶心的就是跑圈的过程怎么描述.首先,顺时针 ...
- 洛谷B4038 [GESP202409 三级] 平衡序列 题解
原题传送门 前言 当我以一种十分激动的心情参加了GESP的2024-9的三级考试时. 打开了此题,然后--自以为是的拿着暴力一顿乱写!然后TLE. 直到结束我还是没有想出来! (太菜了!!!) 以一种 ...
- 探秘Transformer系列之(6)--- token
探秘Transformer系列之(6)--- token 0x00 概述 语言是人类特有的概念.作为一个抽象符号,人是可以理解每个语言单词的意义的,但是现在的NLP语言模型无法直接的从感知中抽象出每个 ...
- Emacs 的优点及与 DE 的比较
一.引言 在编程领域,对于工具的选择一直是开发者们热议的话题.今天,我们来探讨一下 Emacs 及其所具有的优点,并思考使用 Emacs 写程序是否真的比使用集成开发环境(IDE)更方便. 二.Ema ...
- AGC015D题解
简要题意 给定一个区间 \([l,r]\),从中选出若干整数按位或,求可能出现的数的方案数. 数据范围:\(1\le l\le r\le2^{60}\). 思路 首先对于 \([l,r]\) 里的数全 ...
- centos 8 编译*.cpp文件
1.安装g++ yum -y install gcc-c++ 2.编译*.cpp文件 g++ -o test_app_name test_source_file.cpp 3.运行编译结果 ./test ...
- MyBatis与其使用方法讲解
ORM 在讲解Mybatis之前,我们需了解一个概念ORM(Object-Relational Mapping)对象关系映射,其是数据库与Java对象进行映射的一个技术.通过使用ORM,我们可以不用编 ...
- 记一次QT的QSS多个控件设置同一个样式的问题
文章目录 Qt样式表的格式问题 问题的引入 qss 选择器 问题所在 Reference Qt样式表的格式问题 问题的引入 最近在进行样式设计的时候,发现了一个问题,具体如下: 我是将所有样式写到.q ...