在之间的两篇文章中,我们着重讲了顺序表及顺序表的实现。今天这篇文章我们将简单讲解关于顺

序表的三个算法题。这三个题也都属于力扣上的经典例题。

1.例题1:移除元素

 例题来源(力扣) : https://leetcode.cn/problems/remove-element/description/
这是一道数组操作算法题,核心任务是:

可以简单理解为 : 给你一个装着数字的“盒子”(数组  nums )和一个特定数字( val ),要你在这
个“盒子”里直接把所有等于 val 的数字去掉,并且把剩下的数字往前排,最后告诉别人剩下多少个

不等于 val 的数字(返回  k )。

比如盒子里是  [3,2,2,3] ,要去掉 3,操作后盒子前两位变成  [2,2] ,返回剩下 2 个。



方法一 : 嵌套循环移位法

int removeElement(int* nums, int numsSize, int val) 
{int i,j;for(i = 0;i<numsSize; ){if(nums[i]==val){// 当找到要移除的元素时,将后面元素逐个向前移位for(j = i;j<numsSize-1;j++) {nums[j] = nums[j+1];}numsSize--; //覆盖掉当前要移除的元素,同时将数组有效长度numsSize减1}else{i++; }}return numsSize;
}

思路:

- 外层 for 循环遍历数组,循环条件中 i 不直接自增,根据元素是否等于 val 决定后续操作。

- 当发现 nums[i] 等于 val 时,通过内层 for 循环将 i 位置之后的所有元素依次向前移动一

,覆盖掉当前要移除的元素,同时将数组有效长度 numsSize 减 1;若 nums[i] 不等于

val,则  i  正常自增,继续遍历下一个元素。

- 最终返回的 numsSize 就是数组中不等于 val 的元素数量,且数组前 numsSize 个元素已更

新为不含 val 的结果。

时间复杂度:

- 最坏情况下,根据两个 for 循环, 可以得出时间复杂度为 O(n^2) ,其中 n 是数组 nums 的初

始长度 。

- 最好情况下,数组中没有等于 val 的元素,外层循环只需遍历一次数组,时间复杂度为

O(n) 。

取最坏的结果,所以第一种解法的时间复杂度为 O(n^2)。

空间复杂度:

- 整个过程只使用了有限的几个额外变量( i 、 j  等 ),没有额外开辟大量数据存储空间,

空间复杂度为 O(1) ,属于原地操作。

方法二 : 借助数组法

int removeElement(int* nums, int numsSize, int val) 
{// 动态分配一个与原数组大小相同的临时数组,处理 numsSize 为 0 的情况避免非法int* tmp = (int*)malloc(numsSize * sizeof(int));   if(tmp == NULL){return 0;}int index = 0;  // 记录新数组 tmp 的下标,用于存放不等于 val 的元素for(int i = 0;i<numsSize;i++){if(nums[i]!=val){tmp[index] = nums[i];index++;}}// 将临时数组中有效元素(不等于 val 的元素)拷贝回原数组前 index 个位置for(int i = 0;i<index;i++){nums[i] = tmp[i];}free(tmp);  // 释放临时数组内存,避免内存泄漏return index;
}

思路:

- 首先动态分配一个和原数组 nums 大小相同的临时数组 tmp,用于存储不等于 val 的元

素。若内存分配失败( tmp == NULL  ),直接返回 0。


- 遍历原数组  nums  ,当遇到元素不等于  val  时,将其存入临时数组 tmp 中,同时

用 index  记录  tmp  中有效元素的个数(即下标 )。


- 遍历结束后,把临时数组  tmp  中存储的有效元素(共  index  个 )拷贝回原数组nums 的

前 index 个位置,最后释放临时数组 tmp 的内存,避免内存泄漏,返回 index  ,即原数组中

不等于 val 的元素数量。

时间复杂度:

- 遍历原数组  nums  是一个 O(n) 的操作( n  为数组初始长度  numsSize  ),将临时数组元

素拷贝回原数组也是一个 O(k) 的操作( k  为不等于  val  的元素数量,最大为  n  ),总的

时间复杂度为 O(n) ,因为 O(n + k) 中  k  最大不超过  n  ,可简化为 O(n)

空间复杂度:

- 额外动态分配了一个大小为  numsSize  的临时数组  tmp  ,所以空间复杂度为 O(n) 

我们可以明确的观察到,第二种方法的时间复杂度比第一种方法的时间复杂度更简单,但是空间复
杂度比第一种解法的空间复杂度更复杂,所以第二种方法比第一种方法就是采用了空间换时间的做

方法三 :  双指针法(推荐的高效原地算法 )

int removeElement(int* nums, int numsSize, int val) 
{int dst = 0,src = 0; while(src < numsSize){// 当 src 指向元素不等于 val 时,将其赋值到 dst 位置,dst 后移if(nums[src] != val) {nums[dst] = nums[src];dst++;}src++; }return dst;
}

思路:

- 定义两个指针(这里用变量 dst 和 src 模拟指针功能 ),src 用于遍历原数组 nums  ,逐个

检查元素; dst  用于标记新数组(即处理后不含 val 的数组部分 )的最后一个位置。


- 遍历过程中,当  nums[src]  不等于  val  时,说明该元素是需要保留的,将其赋值到

nums[dst] 的位置,然后 dst 自增 1,指向下一个要填充的位置;不管 nums[src] 是否等于

 val ,src 都要自增 1 ,继续遍历下一个元素。


- 当  src  遍历完整个数组( src >= numsSize  )时, dst  的值就是原数组中不等于  val  的

元素数量,且原数组前  dst  个元素已经是处理后不含  val  的结果。

时间复杂度:

- 只需遍历一次原数组  nums  , src  从 0 走到  numsSize - 1  ,循环执行  numsSize  次,

所以时间复杂度为 O(n) ,其中  n  是数组  nums  的初始长度 。

空间复杂度:

- 只使用了常数个额外变量( dst 、 src  ),没有额外开辟大量数据存储空间,空间复杂度

为 O(1) ,属于高效的原地算法。

通过第三种方法,我们可以观察到第三种方法比前两种方法都更加简单。时间复杂度比第一种方法
更简单,空间复杂度比第二种方法更简单。
在实际开发中,优先推荐 双指针法 ,它在时间和空间复杂度上都更优,能高效解决问题;嵌套循
环移位法在数据量小的时候可以用,但数据量大时性能不佳;借助额外数组法一般不是最优选择,
除非有特殊场景需求(比如不能修改原数组内容,只是临时拷贝处理等,但本题要求原地操作,它
其实不太契合题目 “原地” 要求,只是一种思路 ) 。

2.例题2:删除有序数组中的重复项

例题来源(力扣) : https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/
这道题也可以使用双指针的解题方法来写。并且使用双指针能够最大程度的简化时间和空间复杂
度。下面我们来看一下双指针的具体解法。
给非严格递增有序数组 nums ,原地删除重复元素,使每个元素仅出现一次,保持相对顺序,返回
新长度 k ,且需保证 nums  前 k  个元素为去重后的唯一元素。

方法 : 双指针解法

int removeDuplicates(int* nums, int numsSize) {// dst 初始指向第 1 个唯一元素应在的位置(初始为 0)int dst = 0, src = dst + 1; // src 遍历整个数组,找后续元素while (src < numsSize) { // 条件 1:当前 src 元素与 dst 元素不同(需保留 src 元素)// 条件 2:++dst != src(尝试移动 dst 后,判断是否与 src 位置重叠)if (nums[src] != nums[dst] && ++dst != src) { // 将 src 元素放到 dst 位置,保证前 dst+1 个元素唯一nums[dst] = nums[src]; }// src 继续向后遍历src++; }// dst 是最后一个唯一元素的下标,长度为下标 + 1return dst + 1; 
}

思路(以  nums = [1,1,2]  为例 ):

1. 初始状态: dst = 0 (指向第一个  1  ), src = 1 (指向第二个  1  )。

2. 第一次循环: nums[src] == nums[dst] (都是 1 ),足 nums[src]!=nums[dst] , src++ →

src = 2 。

3. 第二次循环: nums[src] = 2  ≠  nums[dst] = 1 ,执行  ++dst ( dst = 1  ),判断  dst !=

src ( 1 != 2  → 成立 ),执行  nums[1] = 2 ,数组变为  [1,2,2] ; src++  →  src = 3 (退出

循环 )。

4. 返回结果: dst + 1 = 2 ,与题目要求一致。

复杂度分析:

时间复杂度:

- 核心逻辑: src  从  1  遍历到  numsSize - 1 ,仅遍历数组一次。

- 无论数组多长,循环次数为  numsSize - 1 ( src  移动次数 ),因此时间复杂度为

 O(n)( n  为numsSize ,即数组长度 )。

空间复杂度:

- 函数仅使用  dst 、 src  两个额外变量,未动态分配内存(如  malloc  )。

- 额外空间占用与数组长度无关,始终为常数,因此空间复杂度为  O(1) (原地算法 )。

对于本地来说,双指针法就是最优的解法。无论是从时间复杂度还是从空间复杂度来说,都是最优
答案。希望大家能够好好理解这道题,好好理解双指针的方法。

3.例题3:合并两个有序数组

例题来源(力扣) : https://leetcode.cn/problems/merge-sorted-array/description/
这道题的大概思路 : 给你两个已经排好序(非递减,即从小到大)的数组 nums1  和 nums2 ,但
nums1  后面有额外的空间(题目里说 nums1  初始长度是 m + n ,前 m  个是有效元素,后 n  个
是闲置的 0  )。你需要把 nums2  的元素合并到 nums1  里,并且合并后整个 nums1  仍然保持非
递减顺序,最后结果直接存在 nums1  里,不用返回新数组。
这道题有三种解法。下面我们一一来看。

方法一 : 先合并再冒泡排序

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) 
{// 1. 合并 nums2 到 nums1 中int k = 0;for (int i = m; i < m + n; i++) {nums1[i] = nums2[k];k++;}// 2. 冒泡排序实现升序排列int len = m + n;for (int i = 0; i < len - 1; i++) {for (int j = 0; j < len - i - 1; j++) {if (nums1[j] > nums1[j + 1]) {// 交换元素int temp = nums1[j];nums1[j] = nums1[j + 1];nums1[j + 1] = temp;}}}
}

思路:

- 合并阶段:利用循环,把  nums2  中的元素依次放到  nums1  中原本后面闲置的位置(即

从下标  m  开始的位置 ),完成两个数组合并为一个数组的操作。


- 排序阶段:采用冒泡排序,对合并后的  nums1  数组(长度为  m + n  )进行升序排序,通

过相邻元素比较和交换,让整个数组有序。

复杂度分析:

- 时间复杂度:合并操作是  O(n) ( n  是  nums2  的长度 ),冒泡排序的时间复杂度是

 O((m + n)²)  。总的时间复杂度由冒泡排序主导,为 O((m + n)²)  ,因为  (m + n)²  增长速度

比  n  快得多。


- 空间复杂度:整个过程只用到了几个临时变量(如  k 、 i 、 j 、 temp  等 ),没有额外开

辟大量数据存储空间,空间复杂度是  O(1)  。

如果大家不懂冒泡排序的话,小编之前在c语言的讲解中详细讲解了冒泡排序的逻辑以及代码内

容。有兴趣的可以去看一下。

方法二 : 先合并再快速排序(借助 qsort  排序法)

// 比较函数,用于 qsort,实现升序排序
int compare(const void *a, const void *b)
{return (*(int *)a - *(int *)b);
}
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) 
{// 1. 合并 nums2 到 nums1 中int k = 0;for (int i = m; i < m + n; i++) {nums1[i] = nums2[k];k++;}// 2. 使用 qsort 对合并后的 nums1 进行升序排序qsort(nums1, m + n, sizeof(int), compare);
}

思路:

- 合并阶段:和方法一一样,通过循环将  nums2  的元素放到  nums1  后面的闲置位置,完

成合并。


- 排序阶段:调用标准库函数  qsort  对合并后的  nums1  数组进行升序排序。 qsort  内部一

般采用快速排序算法(优化版本,平均情况高效 ),根据自定义的  compare  比较函数(返

回  a - b  实现升序 )来排序。

复杂度分析:

- 时间复杂度:合并操作时间复杂度是  O(n)  。 qsort  基于快速排序,平均时间复杂度是

 O((m + n)log(m + n))  ,最坏情况(比如数组完全有序等极端情况,快速排序退化为冒泡排

序逻辑,但实际  qsort  有优化,一般不考虑 )时间复杂度是  O((m + n)²)  ,这里按平均情

况,总的时间复杂度由  qsort  主导,为  O((m + n)log(m + n))  。


- 空间复杂度:合并阶段空间复杂度  O(1)  , qsort  内部实现可能会用到递归栈或者额外的

辅助空间,不过空间复杂度平均情况是  O(log(m + n)) (递归栈深度 ),最坏情况是  O(m +

n)  ,但整体相对于方法一在排序环节更高效。

qsort排序小编之前也在c语言的相关内容中讲解过,有兴趣的可以去了解一下。

方法三 : 双指针(从后往前合并)

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{int l1 = m - 1;  // nums1 中有效元素(即原本要合并的元素)的最后一个下标int l2 = n - 1;  // nums2 中元素的最后一个下标int l3 = m + n - 1;  // 合并后 nums1 数组的最后一个下标// 同时遍历 nums1 和 nums2 的有效元素(从后往前)while (l1 >= 0 && l2 >= 0) {if (nums1[l1] > nums2[l2]) {// nums1 当前元素更大,放到合并后数组的当前最后位置nums1[l3--] = nums1[l1--];} else {// nums2 当前元素更大或相等,放到合并后数组的当前最后位置nums1[l3--] = nums2[l2--];}}// 如果 nums2 还有剩余元素(说明 nums1 有效元素已经处理完了),直接放到 nums1 前面位置while (l2 >= 0) {nums1[l3--] = nums2[l2--];}
}

思路:

- 利用三个指针, l1  指向  nums1  中原有有效元素的末尾, l2  指向  nums2  元素的末

尾, l3  指向合并后  nums1  数组的末尾。


- 从后往前比较  nums1  和  nums2  中的元素,把较大的元素放到  nums1  中  l3  指向的位

置,然后对应的指针向前移动。


- 当  nums1  原有有效元素处理完( l1 < 0  ),如果  nums2  还有剩余元素( l2 >= 0  ),

直接把这些元素依次放到  nums1  中剩下的位置,因为  nums2  本身是有序的,这样就能保

证合并后整体有序。

复杂度分析:

- 时间复杂度:整个过程中, l1  从  m - 1  往前遍历到  -1  , l2  从  n - 1  往前遍历到

 -1  , l3  从  m + n - 1  往前遍历到  -1  ,总的操作次数是  m + n  次,所以时间复杂度是

 O(m + n)  。


- 空间复杂度:只用到了几个指针变量( l1 、 l2 、 l3  ),没有额外开辟大量数据存储空

间,空间复杂度是  O(1)  。

在实际应用中,双指针法(方法三) 是最优解,充分利用了数组有序的条件,时间和空间复杂都
更优;方法一仅适合理解排序和合并逻辑,实际很少用;方法二借助库函数简化了排序实现,但效
率不如方法三且依赖库 。
好了,以上就是关于顺序表部分有关算法题的内容。这些题都运用了一个共同的方法,那就是双指
针法。双体执法在后面的算法题中属于非常重要且非常常见的一种算术方法,大家一定要了解并且
熟练掌握它。

以后小编也会更新一些常见的算法题和一些比较重要的算法方法。喜欢的可以给小编留个关注,感
谢大家的观看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/913993.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/913993.shtml
英文地址,请注明出处:http://en.pswp.cn/news/913993.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

逆向入门(9)汇编篇-bound指令的学习

看程序的时候碰到这么一行没见过的代码&#xff0c;简单记录一下 00427AC8 |. 6215 3C7B4200 |bound edx,qword ptr ds:[0x427B3C]这里是用到了bound指令&#xff0c;这是 x86 汇编中的指令&#xff0c;用于检查数组索引是否在有效范围内。 指令解析 bound edx, qword ptr ds…

【web应用】若依框架中,使用Echarts导出报表为PDF文件

文章目录前言一、Echarts准备工作1、查看是否安装了Echarts2、Echarts导入script 中3、使用Echarts创建图表二、报表制作打印html2canvas和jsPDF准备工作1、安装html2canvas和jsPDF依赖包2、html2canvas和jsPDF引用到script中3、制作并打印报表三、导出结果前言 若依框架前端中…

优选算法 --(双指针算法 1~8)

引言&#xff1a;此专栏为记录算法学习&#xff0c;本专题作为算法学习的第一部分&#xff0c;优选算法专题共计100题&#xff0c;分为不同小模块进行&#xff0c;算法学习需坚持积累&#xff0c;时代不会辜负长期主义者&#xff0c;仅以此句&#xff0c;与君共勉。 讲解算法分…

XRDMatch代码复现与分析报告

XRDMatch代码复现与分析报告 1. 项目概述 XRDMatch是一个用于X射线衍射(XRD)数据匹配和分析的开源工具,由zhengwan-chem开发并托管在GitHub上。本项目旨在复现XRDMatch的核心功能,并对其实现进行详细分析。 X射线衍射是材料科学中用于确定晶体结构的重要技术,通过分析衍射…

SpringAI×Ollama:Java生态无缝集成本地大模型实践指南

摘要 随着大语言模型(LLM)的普及,数据隐私和技术栈统一性成为企业级AI应用的核心挑战。本文系统阐述如何通过SpringAI框架与Ollama本地化模型引擎的结合,构建安全高效的生成式AI应用。通过实战案例解析配置优化、流式响应、工具调用等关键技术,为Java开发者提供零Python依…

从采购申请到报废核销:如何用数字化缝合企业物资管理的“断点”?

在企业的日常运营中&#xff0c;物资管理是一项至关重要的工作。从采购申请到物资的入库、使用&#xff0c;再到最终的报废核销&#xff0c;这一系列流程就像一条长长的链条&#xff0c;环环相扣。然而&#xff0c;在传统管理模式下&#xff0c;这条链条上却存在着诸多“断点”…

AVL平衡二叉树

01. 初始AVL树 AVL树是最早发明的自平衡二叉搜索树。在AVL树中&#xff0c;任何节点的两个子树的高度差&#xff08;平衡因子&#xff09;最多为1&#xff0c;这使得AVL树能够保持较好的平衡性&#xff0c;从而保证查找、插入和删除操作的时间复杂度都是O(log n)。包含n个节点…

教育行业可以采用Html5全链路对视频进行加密?有什么优势?

文章目录前言一、什么是Html5加密&#xff1f;二、使用Html5对视频加密的好处三、如何采用Html5全链路对视频进行加密&#xff1f;四、教育行业采用Html5全链路视频加密有什么优势&#xff1f;总结前言 面对优质课程盗录传播的行业痛点&#xff0c;教育机构如何守护核心知识产…

Vue3 tailwindcss

1、安装tailwindcsspnpm i -D tailwindcss postcss autoprefixer # yarn add -D tailwindcss postcss autoprefixer # npm i -D tailwindcss postcss autoprefixer2、 创建TailwindCSS配置文件npx tailwindcss init -ptailwind.config.js/** type {import(tailwindcss).Config}…

提示工程:解锁大模型潜力的核心密码

以下是对Lilian Weng的提示工程权威指南&#xff08;原文链接&#xff09;的深度解析与博客化重构&#xff0c;融入最新行业实践&#xff1a; 提示工程&#xff1a;解锁大模型潜力的核心密码 ——从基础技巧到工业级解决方案全解析 一、重新定义人机交互范式 传统编程 vs 提示…

Python3邮件发送全指南:文本、HTML与附件

在 Python3 中&#xff0c;使用内置的 smtplib 库和 email 模块发送邮件是一个常见的需求。以下是更详细的实现指南&#xff0c;包含各种场景的解决方案和技术细节&#xff1a;一、发送纯文本邮件的完整实现准备工作&#xff1a;确保已开通 SMTP 服务&#xff08;各邮箱开启方式…

CSS和CSS3区别对比

CSS&#xff08;层叠样式表&#xff09;与CSS3&#xff08;CSS的第三个版本&#xff09;的区别主要体现在功能扩展、语法特性以及应用场景等方面。以下是两者的核心对比&#xff1a; 一、核心概念与版本关系CSS&#xff1a;是基础样式表语言&#xff0c;用于分离网页内容与样式…

JVM--监控和故障处理工具

一、命令行工具 1. jps (Java Process Status) 作用&#xff1a;列出当前系统中所有的 Java 进程 常用命令&#xff1a; jps -l # 显示进程ID和主类全名 jps -v # 显示JVM启动参数 输出示例&#xff1a; 1234 com.example.MainApp 5678 org.apache.catalina.startup.Bootstra…

推荐 7 个本周 yyds 的 GitHub 项目。

01.开源的 CRM 软件这是一个开源的客户关系管理&#xff08;CRM&#xff09;系统&#xff0c;现在又 32.5K 的 Star。为企业和团队提供比肩 Salesforce 等商业产品的功能&#xff0c;同时强调用户自主权、数据自由与高度可定制性。开源地址&#xff1a;https://github.com/twen…

linux网络编程之单reactor模型(一)

Reactor 是一种事件驱动的设计模式&#xff08;Event-Driven Pattern&#xff09;&#xff0c;主要用于处理高并发 I/O&#xff0c;特别适合网络服务器场景。它通过一个多路复用机制监听多个事件源&#xff08;如 socket 文件描述符&#xff09;&#xff0c;并在事件就绪时将事…

浏览器重绘与重排

深入解析浏览器渲染&#xff1a;重排(Reflow)与重绘(Repaint)的性能陷阱与优化策略作为一名前端开发者&#xff0c;你是否遇到过界面突然卡顿、滚动时页面抖动或输入框响应迟钝&#xff1f;这些常见性能问题背后&#xff0c;往往是重排与重绘在作祟。本文将深入剖析浏览器渲染机…

day049-初识Ansible与常用模块

文章目录0. 老男孩思想-人脉的本质1. Ansible1.1 密钥认证1.2 安装ansible1.3 添加ansible配置文件1.4 配置主机清单文件&#xff08;Inventory&#xff09;1.5 测试1.6 ansible的模块思想1.7 command模块1.8 需求&#xff1a;每台服务器的密码都不同&#xff0c;怎么批量执行业…

力扣网编程134题:加油站(双指针)

一. 简介 前面两篇文章使用暴力解法&#xff0c;或者贪心算法解决了力扣网的加油站问题&#xff0c;文章如下&#xff1a; 力扣网编程150题&#xff1a;加油站&#xff08;暴力解法&#xff09;-CSDN博客 力扣网编程150题&#xff1a;加油站&#xff08;贪心解法&#xff09…

XPath 语法【Web 自动化-定位方法】

&#x1f9ed; XPath 语法简介&#xff08;Web 自动化核心定位手段&#xff09;一、XPath 是什么&#xff1f;XPath&#xff08;XML Path Language&#xff09;是用于在 XML/HTML 文档中定位节点的语言&#xff0c;由 W3C 标准定义。浏览器支持的是 XPath 1.0。应用场景广泛&am…

记一次 Linux 安装 docker-compose

一.下载 1.手动下载 下载地址&#xff1a;https://github.com/docker/compose/releases 下载后&#xff0c;放在/usr/local/bin/目录下&#xff0c;命名为&#xff1a;docker-compose 2.命令下载 sudo curl -L "https://github.com/docker/compose/releases/download/…