代码随想录第一天 | 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 ...
随机推荐
- Mac安装Zookeeper
软件版本:3.4.10 一.软件下载 下载链接:http://archive.apache.org/dist/zookeeper/ 解压缩:tar -zxf zookeeper-3.4.10.t ...
- 揭秘 Sdcb Chats 如何解析 DeepSeek-R1 思维链
在上一篇文章中,我介绍了 Sdcb Chats 如何集成 DeepSeek-R1 模型,并利用其思维链(Chain of Thought, CoT)功能增强 AI 推理的透明度.DeepSeek-R1 ...
- JavaDoc文档的介绍及生成方法
javaDoc命令是用来生成自己的API文档的 参数信息 @author 作者名 @version 版本号 @since 指明需要最早使用的jdk版本 @param 参数名 @return 返回值情况 ...
- 【译】HTTP 文件更新了请求变量
许多用户都要求在 Visual Studio 的 HTTP 文件中添加对请求变量的支持.使用请求变量,您可以发送 HTTP 请求,然后在从 HTTP 文件发送的任何后续请求中使用响应或请求中的数据.我 ...
- C# 获取计算机唯一标识
C# 获取计算机唯一标识 原文链接 private static string _sFingerPrint { get; set; } /// <summary> /// 计算机唯一标识 ...
- [BZOJ3160] 万径人踪灭 题解
首先正难则反,想到答案即为满足第一条要求的回文子序列数量,减去回文子串数量.回文子串数量 \(hash+\) 二分即可,考虑前半部分. 假如我们将一个回文子序列一层层剥开,就会发现它其实是由多个相同的 ...
- 4个Sprint目标的挑战以及解决的技巧
1. Sprint 目标太大 有时,您的团队可能会尝试将过多的任务塞进冲刺中.抵制在冲刺中承担太多的诱惑,因为这会损害你的速度和持续交付的能力. 2. Sprint目标是模糊的 冲刺目标通常是不确定的 ...
- C# 将list进行随机排序
private List<T> RandomSortList<T>(List<T> ListT) { Random random = new Random(); L ...
- Selenium KPI接口 附件上传
实现功能 拖拽图片到百度上传图片搜索功能区域. 定位.send_keys(r'图片路径') 导入相关包 from selenium import webdriver from time import ...
- Caused by: java.lang.IllegalArgumentException: invalid comparison: java.util.Date and java.lang.String 解决办法
使用MyBatis 更新数据库数据的时候 遇到了这个错误: Caused by: java.lang.IllegalArgumentException: invalid comparison: jav ...