news 2026/10/10 6:53:29

数组进阶实战:切片、指针、去重与动态扩展全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数组进阶实战:切片、指针、去重与动态扩展全解析

数组这玩意儿,表面上看是每种语言入门第一课就教的东西,但真到项目里用起来,坑一个接一个。我在后端处理接口数据、前端操作表格、甚至用Excel VBA整理报表的时候,都吃过数组的亏。前一篇写了基础的定义、遍历和下标访问,这篇《数组(二)》咱们直接进入真实使用场景,聊聊切片取值、指针数组、对象去重、动态扩展,最后拆一道和数组配对相关的构造题。内容偏实战,适合已经能写for循环、但对数组的“进阶操作”还不太有把握的人。

1. 切片与取值:语言的设计哲学决定你的写法

1.1 Python切片的“半开区间+步长”到底怎么记

Python的切片语法是arr[start:stop:step],其中stop位置是不包含的。很多人一开始不习惯“取不到结尾”这个设定,但你只要记住一个生活类比:半开区间[start, stop)就像“从第start个人开始点名,点到第stop-1个人为止,第stop个人不点”。这个设计其实是为了让arr[:n]和arr[n:]能无缝拼成完整数组,也让len(arr[:i])天然等于i,写代码时少一堆+1、-1的纠结。

具体到实操,几个高频切片写法我直接列出来:

  • arr[::-1]:整个数组逆序,效果等同reversed(arr),但返回的是新列表。
  • arr[::2]:取偶数下标元素,也就是每隔一个取一个。
  • arr[1:7:2]:从下标1到下标6(注意7取不到),步长为2。
  • 二维数组取列:matrix[:, 1]是NumPy的写法,原生Python列表只能写成[row[1] for row in matrix]。

这里有一个特别容易踩的坑:Python列表切片出来的新列表,里层元素还是引用。比如b = a[1:3],如果a里存的是可变对象(比如子列表),那b里改子元素,a也会跟着变。而NumPy的切片更狠,它直接返回原数组的视图,连新数组都不算,改视图就是改原数组。我当年刚接触NumPy时在这个上面翻过车,所以现在凡是做切片后要原地修改的,都会先确认一下是视图还是副本,不行就显式调用.copy()。

1.2 JS的slice、splice、split三兄弟,别再混了

JavaScript里跟“切数组”相关的有三个方法,名字长得像,行为差很多:

  • slice(start, end):不修改原数组,返回一个从start到end-1的新数组,end缺省时取到末尾。
  • splice(start, count, ...items):直接修改原数组,从start开始删掉count个元素,还可以插入新元素。返回值是被删掉的元素组成的数组。
  • split:这是字符串的方法,不是数组的,作用是把字符串按分隔符拆成数组。

“js怎么取出数组”这种需求,实际项目里最常见的是提取数组的一部分。正确姿势是arr.slice(1, 4),而不是arr.splice(1, 4),后者会把原数组改掉,容易引发连锁副作用。ES6之后,还经常用解构来取数组对象里的某些字段,比如从对象数组里提取所有id组成新数组:

const ids = arr.map(item => item.id); const firstThree = arr.slice(0, 3);

如果要把数组转成字符串,也值得单独说一下。arr.join(',')是按指定分隔符拼字符串,String(arr)等价于arr.join(','),但嵌套数组转出来会带逗号,格式可能不是你要的。想要调试时看全貌,JSON.stringify(arr)最靠谱,尤其是数组里套对象的时候。我见过有人用arr.toString()去拼SQL查询参数,结果是对象数组全变成[object Object],排查半天才发现是这里出了问题。

1.3 MATLAB取多列:一种矩阵思维的降维打击

Python和JS都是“行优先”视角,取一行很容易,但取一列就得想办法。MATLAB不一样,它天生就是矩阵思维,A(:, 2:4)直接取出所有行的第2到第4列,返回一个子矩阵。这个语法用起来是真的爽,但移植到别的语言时容易水土不服——Python里没有对应的原生语法,NumPy里则是arr[:, 1:3],底层是切片视图。

我把三种语言的常用取值方式整理成了一张表,做跨语言开发时对照着看很方便:

场景PythonJavaScriptMATLAB
取前n个元素arr[:n]arr.slice(0, n)A(:, 1:n)(按列)
逆序arr[::-1]arr.slice().reverse()flip(A)
每隔一个取arr[::2]arr.filter((_, i) => i % 2 === 0)A(:, 1:2:end)
取第2列[row[1] for row in arr]arr.map(row => row[1])A(:, 2)

这里有一个通用经验:切片的本质是对“数组的视图”进行操作,而不同语言的视图机制完全不同。Python原生切片是副本,NumPy切片是视图,MATLAB切片多数是值拷贝。写跨语言脚本时,第一步就该确认目标语言是值语义还是引用语义,否则后面全是在debug。

2. 数组与指针:C语言里你避不开的那些细节

2.1 指针数组、数组指针和函数指针数组,谁是谁

C语言的数组一旦和指针搅在一起,就变成劝退现场。先说最常见的两个:指针数组和数组指针。

  • 指针数组:int *p[3],它是一个数组,数组里每个元素是int*。[]的优先级高于*,所以p先和[3]结合成数组,再和*结合表示元素是指针。
  • 数组指针:int (*p)[3],它是一个指针,指向一个长度为3的int数组。因为加了括号,p先和*结合成指针,再指向一个“有3个int的数组”。

指针数组最常见的用途是存放字符串。比如const char *names[] = {"Alice", "Bob", "Cindy"};,这里names是一个指针数组,每个元素指向一个字符串字面量。遍历的时候就是:

for (int i = 0; i < 3; i++) { printf("%s\n", names[i]); }

函数指针数组也是同样的思路,它本质是一个元素为函数指针的数组,常用来实现“转移表”,替代一堆if-else。举个例子:

int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int (*ops[2])(int, int) = {add, sub}; int result = ops[0](3, 5); // 调用add(3, 5)

实际项目里,命令解析器特别喜欢用这种结构:命令编号作为下标,直接定位处理函数,比一长串switch清爽得多。不过写之前一定要记得数组下标越界就是未定义行为,转移表的边界检查要做在入口。

2.2 字符数组与字符串初始化:char s[]和char *s不是一回事

“c++字符串数组初始化”是新手高频问题,但很多人没搞清底层差异。看这两行:

char s1[] = "hello"; // 字符数组,内容可修改 const char *s2 = "hello"; // 指针指向字符串字面量,内容只读

s1是一个有6个元素的数组(结尾隐含'\0'),sizeof(s1)等于6,修改s1[0]没问题。s2是一个指针,sizeof(s2)在64位系统上是8,指向的字符串字面量位于只读区,试图修改*s2会导致运行时错误。我在教学时经常说:char *s是“拿着纸条找到别人的家门”,char s[]是“把字刻在自己家的墙上”。前者方便指向和传递,后者才是真正拥有这份数据。

二维字符数组也很常用,比如存储一批姓名:

char names[3][10] = {"Alice", "Bob", "Cindy"}; // 每行最多9个有效字符

它和const char *names[3]的区别在于:二维字符数组在连续内存里存放,每行长度固定,适合排序、交换行;指针数组每行可以指向不同长度的字符串,适合只读场景,交换元素只换指针,效率更高。

再说说“如何输入char数组”。用scanf("%s", buf)最省事,但它会读到空白字符就停,而且不校验长度,输入一长就缓冲区溢出。推荐用fgets(buf, sizeof(buf), stdin),它能限制读取长度,把安全边界控制住。实测中很多人不读fgets的返回值,导致输入失败时还在处理旧数据,这个小细节能在关键时刻省你两小时调试时间。

2.3 数组名的衰减:为什么arr和&arr[0]长得一样

C语言里,数组名在绝大多数表达式里会“衰减”成指向首元素的指针。所以sizeof(arr)能算出整个数组的大小,但一旦把arr作为参数传给函数,在函数内部sizeof(arr)就变成指针的大小了,因为参数的数组写法本质上还是指针。

void print_len(int arr[]) { int n = sizeof(arr) / sizeof(arr[0]); // 错误!arr已经衰减成指针 }

正确的做法是额外传一个长度参数。这就是为什么C语言里凡是“数组作为函数参数”的标准写法都是void f(int *arr, int n),你写int arr[]只是给人看的,编译器眼里是一样的。

关于“c语言数组指针移动指定位输出字符”,核心就是指针算术:char *p = str + offset;然后从p开始输出。这里有个容易错的地方,指针加法是按元素大小来移动的,char类型移动1就是1个字节,int指针移动1就是4个字节。我当年做协议解析时想跳过N个字节,直接int *p = base + N,结果跳了4N个字节,数据全错位了。后来就养成了习惯:先确认指针的基类型,再决定偏移量。

还有数组变量的类型转换。C语言里数组没有直接的赋值语义,你不能写arr1 = arr2,但可以用指针指向它,也可以把数组强制转换成字节指针来做序列化:

int arr[] = {1, 2, 3}; unsigned char *bytes = (unsigned char *)arr; // 按字节访问底层存储

这种用法在嵌入式通信、网络协议栈里很常见。但要注意机器字节序的问题:大端和小端机器上,同一个int的字节排列是不同的。跨平台发送数据时,最好用显式的位运算组装字节,而不是靠强转。

顺带提一句“结构体定义数组”和“宏定义数组”。结构体数组很简单:struct Student stu[10];就是数组里每个元素都是一个结构体,初始化可以写成struct Student stu[2] = {{"Alice", 18}, {"Bob", 20}};。宏定义数组就是#define MAX_SIZE 10配合int arr[MAX_SIZE];,好处是改一个宏定义就能全局调整容量,坏处是宏不检查类型,#define SIZE 10u这种写法更稳妥。

3. 去重这项基本功:从Set到对象数组

3.1 简单数组去重与Set的适用边界

数组去重大概是前端面试里出现频率最高的题。最简单的方式是用Set:

const unique = [...new Set(arr)];

Set底层是哈希结构,长度n的去重复杂度接近O(n),比filter加indexOf的O(n^2)好得多。“数组去重”看着简单,但有两个边界问题:

  • NaN在Set里被认为等于它自己,所以能被正确去重;而indexOf查找NaN永远返回-1,filter方案会保留多个NaN。
  • 两个内容相同但引用不同的对象,在Set里是不相等的,所以Set对对象数组去重无效。

Python里类似的去重就是list(set(arr)),但同样只对可哈希元素有效。如果元素是列表(不可哈希),就得写成[dict(t) for t in {tuple(d.items()) for d in arr}]之类的式子,或者自己写循环。

3.2 对象数组去重:Map才是正解

“对象数组去重”是真正业务里最常见的需求:后端返回一批数据,每一条有一个唯一的id,要根据这个id去重。这时候Set派不上用场,因为每个对象引用都不同。正确姿势是用Map:

const arr = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 1, name: 'Alice2' } ]; const map = new Map(); arr.forEach(item => { if (!map.has(item.id)) { map.set(item.id, item); } }); const unique = [...map.values()];

这个思路的本质是:用一个“主键”把对象映射成唯一标识,再靠Map的哈希能力去重。如果数据量小,也可以写成一行:

const unique = [...new Map(arr.map(item => [item.id, item])).values()];

我在实际开发中更推荐前一种写法,因为后面大概率还要加“保留最后一条还是第一条”之类的规则,写循环更好维护。ES6里提取数组对象的一部分字段也常用到Map,比如arr.map(item => ({ id: item.id, name: item.name })),这本质就是“投影”。

3.3 接口数据里的数组处理:JSON数组与PHP对象数组

跨语言处理数组,最典型的就是接口返回JSON数组。前端拿到[{"id":1,"name":"Alice"}, ...],要提取某一列、去重、分组。JS里的map、filter、reduce就是干这个的。分组需求用reduce很顺手:

const grouped = arr.reduce((acc, item) => { (acc[item.category] = acc[item.category] || []).push(item); return acc; }, {});

服务端如果用的是PHP,入口是json_decode($json, true),得到的就是关联数组。PHP的数组其实是个“有序字典”,既能当下标数组用,也能当对象用。从数据库查出来的记录集,配合array_column($list, 'name')可以直接提取某一字段为数组,array_map则可以对数组做批量回调处理。Java那边则常用Collectors.groupingBy配合Stream流做分组:

Map<String, List<Item>> grouped = list.stream() .collect(Collectors.groupingBy(Item::getCategory));

“json数组”、“php接口数组对象”、“group()+数组java”这几个热词背后其实是同一个场景:跨语言处理结构化的数组数据时,先确定数组元素的“形状”,再选择对应的批量操作方法。数据形状变了,操作方式立马跟着变。

4. 让数组活起来:动态数组、VBA数组与树状数组

4.1 VBA数组为什么总被吐槽“长度写死”

VBA里的数组,默认你得先声明大小:

Dim arr(1 To 10) As Integer

这个固定大小让很多人头疼,因为Excel的数据行数是不确定的。解决办法是ReDim Preserve:

ReDim arr(1 To n)

但ReDim Preserve有个限制:只能改变最后一维的大小。想往数组里动态追加元素,每次都得手动检查当前容量、Preserve重新声明,再填值。这就是为什么VBA写数组操作总是又臭又长。原因在于VBA的数组是静态内存模型,不像Python的列表会自动扩容。动态数组的本质是“容量不足时重新申请一块更大的内存,把旧数据搬过去”,只是高级语言把这一步封装好了。

Oracle里的变长数组VARRAY也是类似的思路:它在定义时给一个上限,但使用时长度可以变化,相当于数据库层面的“动态数组”。和VBA不同的是,VARRAY主要存在数据库里,适合存储固定顺序的小规模数据,不适合做大量增删。

4.2 动态数组在Python、C++、JS中的表现

真正要高效地“往数组里增加元素”,还得看现代语言的内置结构:

  • Python:list.append()是摊销O(1)时间,因为它内部有成倍扩容策略。pop()、insert()也都很方便。
  • C++:std::vector<T>,push_back()在容量不足时会重新分配内存并把旧元素拷贝或移动过去。C++11之后移动语义可以减少拷贝,性能提升很明显。
  • JavaScript:Array本身就是动态的,push()、splice()随便用,不需要声明长度。
  • C语言:没有内置动态数组,要么用malloc+realloc自己管理内存,要么借用第三方的容器库。

数组初始化这里也值得多说一句。Python里[0] * n会生成n个0的列表,注意里层元素如果是对象,[obj] * n出来的n个元素是同一个引用,修改任意一个等于改全部。C++里vector<int> v(n, 0)就是n个0。JS里new Array(n).fill(0)才是安全的,光写new Array(n)会得到n个空位,map都不会遍历空位。

4.3 树状数组:用一个数组做区间查询

讲一讲“树状数组模板”,这算是数组操作里的算法进阶。树状数组(Fenwick Tree)用一个数组tree来维护原数组a的前缀信息,支持两类操作:单点修改、区间查询。

核心是lowbit(x) = x & (-x),它取出x最低位的1所表示的数值。修改时,从下标i开始,每次i += lowbit(i)更新父节点;查询前缀和时,从下标i开始,每次i -= lowbit(i)累加。模板如下(C++):

int n; int tree[100005]; void add(int idx, int val) { while (idx <= n) { tree[idx] += val; idx += idx & (-idx); } } int prefixSum(int idx) { int res = 0; while (idx > 0) { res += tree[idx]; idx -= idx & (-idx); } return res; } // 区间[l, r]的和 int rangeSum(int l, int r) { return prefixSum(r) - prefixSum(l - 1); }

用树状数组做单点修改+区间查询,单次操作是O(log n),比普通数组O(n)求区间和快得多。我第一次看树状数组时觉得特别反直觉,后来想通了一个类比:它就像你记账时把账目分成几叠,每叠的金额已经汇总好放在文件夹上,你查总账时不用一张张数,只需要翻几个文件夹加一下。需要统计某个区间的数据动态变动的场景,比如求逆序对、实时排行榜、频率统计,树状数组都很好用。

5. 一道构造题说透数组的“奇偶配对”:2n个正整数求gcd>1

5.1 题面拆解与为什么答案一定存在

来看一道比较完整的数组构造题,热词里“小美的数组删除”也是类似风格:

给定一个由2n个正整数组成的数组a。你要先恰好舍弃其中两个元素,再将其余元素两两配对。每对元素的和构成新数组b的一个元素,因此b恰好有n-1个元素。构造一种配对方式,使b中所有元素的最大公约数大于1。可以证明,答案一定存在。

这道题乍一看吓人,本质上考查的是奇偶性分析。两个整数相加的奇偶性规律只有三条:

  • 奇数 + 奇数 = 偶数
  • 偶数 + 偶数 = 偶数
  • 奇数 + 偶数 = 奇数

要让每一项的和都有公约数,最简单的方法是让每一项都是偶数,这样每一项都能被2整除,整个数组的最大公约数至少是2。这就把问题转化成了“如何舍弃两个元素,使剩下的所有数能两两配对成奇-奇或偶-偶”。

关键在于剩余元素里奇数的个数必须是偶数。如果剩余奇数个数为奇数,无论怎么配对,必然有一对是奇-偶组合,配对结果是奇数,就破坏“全都为偶数”的构造了。

5.2 奇偶分离构造法

先统计原数组中奇数的个数cnt_odd,偶数的个数cnt_even = 2n - cnt_odd。分两种情况处理:

  • cnt_odd是偶数:只要保证舍弃两个元素后奇数个数仍是偶数即可。最简单的是舍弃两个偶数(如果偶数数量足够);如果偶数不够2个,即数组几乎全是奇数,那就舍弃两个奇数,剩余奇数个数依旧为偶数。
  • cnt_odd是奇数:舍弃一个奇数和一个偶数,剩余奇数个数就从奇数减1变成偶数。

舍弃完成后,把剩余元素分成两个集合:奇数集合和偶数集合。然后在每个集合内部两两配对。奇数+奇数是偶数,偶数+偶数也是偶数,所以新数组b的所有元素都是偶数,最大公约数至少是2,条件达成。

这个构造里最妙的一点是:我们根本不需要关心具体谁和谁配对,只要保证“奇数个数为偶数”这个前提。因为同一个集合内部元素数量是偶数时,总能全部两两配对,不会剩下落单的。手算几个例子验证,比如a = [1, 2, 3, 4](n=2,cnt_odd=2是偶数),舍弃两个偶数2和4,剩下[1, 3],配对和是4,偶数,gcd=4>1。如果舍弃两个奇数,剩下[2, 4],配对和6,一样成立。

5.3 完整代码与类似题的延伸

代码实现很直接,Python版本如下:

def solve(a): odd = [x for x in a if x % 2 == 1] even = [x for x in a if x % 2 == 0] if len(odd) % 2 == 0: if len(even) >= 2: a.remove(even[0]) a.remove(even[1]) else: a.remove(odd[0]) a.remove(odd[1]) else: a.remove(odd[0]) a.remove(even[0]) odd = [x for x in a if x % 2 == 1] even = [x for x in a if x % 2 == 0] pairs = [] for i in range(0, len(odd) - 1, 2): pairs.append((odd[i], odd[i + 1])) for i in range(0, len(even) - 1, 2): pairs.append((even[i], even[i + 1])) return pairs

注意代码里要先根据奇偶个数决定舍弃谁,舍弃之后必须重新收集奇偶分组,因为原来的列表引用已经变了。这道题能推广到一类构造题:题目给一个数组,要求经过某些操作后满足某个整体性质,优先往奇偶性、同余、整除这些数论特征上想。类似风格的还有“2的幂数组”问题,本质也是找一个统一的整除因子;“是否同构”类的题目则往往先把条件写成数学表达式,再判断是否存在某个整数x满足关系,核心思路是化简条件后用取模或差分统一处理。

这类构造题在实际开发中可能不会天天遇到,但做一道能帮你建立“从宏观性质入手,而不是急着模拟过程”的思维习惯。我在公司带新人时发现,很多人拿到题就开始模拟配对流程,各种边界条件处理不完;反过来先问“目标是什么、什么情况下必然满足”,思路一下子就清晰了。数组这个东西,基础语法只是入口,真正拉开差距的恰恰是这种“站在性质层面看待数据”的视角。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 6:52:58

海康威视ISAPI协议对接实战:从鉴权翻车到稳定取流

简介&#xff1a;海康威视ISAPI协议文档是一份面向安防设备开发者、平台集成工程师及物联网应用开发者的技术参考资料&#xff0c;用于解决摄像机、NVR、门禁等设备与平台或客户端软件之间的通信对接问题。ISAPI全称Intelligent Security API&#xff0c;是基于HTTP并采用REST架…

作者头像 李华
网站建设 2026/10/10 6:52:49

多智能体协作系统实战:从任务编排到工程化落地

很多人做 AI Agent 开发时&#xff0c;最容易掉进去的坑&#xff0c;是以为把一堆 Agent 凑在一起&#xff0c;让它们各自发挥&#xff0c;事情就成了。真到了落地阶段你会发现&#xff0c;单个 Agent 再聪明&#xff0c;一旦放进一个多人协作的场景里&#xff0c;立刻会出现任…

作者头像 李华
网站建设 2026/10/10 6:51:55

5G信令流程解析实战:从抓包到排错,快速上手核心网与网优

简介&#xff1a;这份文档面向移动通信初学者、通信工程专业学生及希望系统梳理5G信令流程的从业者&#xff0c;围绕5G信令解析所需的核心概念与学习路径展开&#xff0c;帮助读者建立从网络架构到流程环节的整体认知框架。内容涵盖用户终端、基站、核心网等关键网元之间的通信…

作者头像 李华
网站建设 2026/10/10 6:51:18

Cursor免费额度实战指南:避开无限续杯陷阱,高效使用AI编辑器

看到“无限续杯”这四个字&#xff0c;我就知道很多人又被网上的标题党带偏了方向。作为一个从 VS Code 全家桶时代一路用过来、几乎把主流 AI 编程工具都摸过一遍的开发者&#xff0c;我最初也是抱着“白嫖”的心态去搜 Cursor 的免费额度攻略&#xff0c;结果发现网上那些“无…

作者头像 李华
网站建设 2026/10/10 6:51:12

浏览器编程工具全解析:云端IDE、在线编辑器与协作平台选型指南

1. 浏览器编程的现状与核心价值1.1 本地IDE的三大痛点做过开发的人都有体会&#xff0c;本地环境搭建这件事&#xff0c;说多了都是泪。一台新电脑到手&#xff0c;从零开始配一套能跑项目的开发环境&#xff0c;三小时能搞定都算快的。我见过太多这样的情况&#xff1a;新同事…

作者头像 李华
网站建设 2026/10/10 6:51:12

磁盘管理实战:用tree命令快速定位目录结构与空间占用

刚接手一台告警的服务器&#xff0c;磁盘使用率飙到97%&#xff0c;SSH上去之后我没有急着du -sh到处刨&#xff0c;而是先敲了一条tree -L 2 --du /&#xff0c;把根目录下面的大结构一次性铺开。很多人觉得 tree 就是个"画目录树"的小玩具&#xff0c;但在磁盘管理…

作者头像 李华