此页内容

数组

yirufeng

3436字约11分钟

2024-05-12

数组基本概念

特点

  1. 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
    • 第一是线性表(Linear List)。顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构
    • 非线性表,比如二叉树、堆、图,因为在非线性表中,数据之间并不是简单的前后关系
  2. 数组拥有连续的内存空间以及相同类型的数据,伴随而来的便是一个杀手级的特性:“随机访问呢”。引申出来一个面试题“数组和链表的区别”,区别是数组支持随机访问,根据下标随机访问时间复杂度是O(1)

数组插入和删除技巧

  • 技巧1:在部分情况下,数组插入的时间复杂度可以由O(N)降为O(1)。在数组中,如果想要插入元素,大部分情况下都需要在插入前将插入位置以及后面的元素向后挪动一个位置,但是在没有强烈要求的情况下,我们可以将插入位置的元素放到最后一个元素的下一个元素,然后将要插入的元素放置到插入位置即可。比如,数组长度为n,我们现在需要将元素 x 插入到第 3 个位置。我们只需要将c放入到a[5],将a[2]赋值为x即可。最后,数组中的元素如下: a,b,x,d,e,c
  • 技巧2:如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率是不是会提高很多呢?我们继续来看例子。数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。如果你了解 JVM,你会发现,这不就是 JVM 标记清除垃圾回收算法的核心思想吗?没错,数据结构和算法的魅力就在于此,很多时候我们并不是要去死记硬背某个数据结构或者算法,而是要学习它背后的思想和处理技巧,这些东西才是最有价值的。如果你细心留意,不管是在软件开发还是架构设计中,总能找到某些算法和数据结构的影子。

注意事项

  1. 警惕数组越界
    • 在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。这种情况下,一般都会出现莫名其妙的逻辑错误,就像我们刚刚举的那个例子,debug 的难度非常的大。而且,很多计算机病毒也正是利用到了代码中的数组越界可以访问非法地址的漏洞,来攻击系统,所以写代码的时候一定要警惕数组越界。并非所有语言都像C语言一样,把数组越界检查的工作丢给程序员来做
  2. 容器可否完全替代数组
    • ArrayList等容器 最大的优势就是可以将很多数组操作的细节封装起来。比如前面提到的数组插入、删除数据时需要搬移其他数据等。另外,它还有一个优势,就是支持动态扩容。除此之外,数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。如果我们申请了大小为 10 的数组,当第 11 个数据需要存储到数组中时,我们就需要重新分配一块更大的空间,将原来的数据复制过去,然后再将新的数据插入。如果使用 ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为 1.5 倍大小。不过,这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。比如我们要从数据库中取出 10000 条数据放入 ArrayList。我们看下面这几行代码,你会发现,相比之下,事先指定数据大小可以省掉很多次内存申请和数据搬移操作。作为高级语言编程者,是不是数组就无用武之地了呢?当然不是,有些时候,用数组会更合适些,我总结了几点自己的经验。
      • Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
      • 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
      • 当要表示多维数组时,用数组往往会更加直观。比如Object[][] array;而用容器的话则需要这样定义:ArrayList<ArrayList > array
    • 对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

一些疑问

  1. 为什么大多数编程语言中,数组要从0开始编号,而不是从1开始?
    • 从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。
    • 数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

总结

数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是支持随机访问,但插入、删除操作也因此变得比较低效,平均情况时间复杂度为 O(n)。在平时的业务开发中,我们可以直接使用编程语言提供的容器类,但是,如果是特别底层的开发,直接使用数组可能会更合适。

面试题:手撕顺时针打印二维矩阵

console.log("123")

面试题: 两个有序数组合成一个有序数组,不允许有重复数据

面试题:找出一个数组中长度等于k且阈值大于等于threshold的所有连续的子数组(遍历一遍就完事了)

两个有序数组的中位数

搜索旋转数组最小值

两个有序数组怎么求交集

一个数组,每个位置的值对应下标。重新排列,要求对应位置上不能有同下标相同的 值,即原先 a[0]=0,重排后 a[0]不可以等于 0。输出总共有多少种重新排列的方法。

求数组的中位数。数组由一个升序数组翻转形成,如 1 2 3 4 5 6 7 可以从 5 处翻转,形成 5 6 7 1 2 3 4,求 5 6 7 1 2 3 4 的中位数。要求时间复杂度低于 O(n)。

给一个数组,定义 X 为某个区间的最小值乘上这个区间内所有元素的和,求最大的 X。如数组为 3 1 6 4 5,则最大的 X=4*(6+4+5)=60

面试题:无序数组中找出连续的数中第一个缺失的数字

两个倒序数组找最第 k 大的(框架差不多,最后发现漏了一种情况,感觉还在想洗牌的事 情)

算法题:找出数组B里面比数组A大的所有数

算法题 偏转顺序数组 二分

算法:两个区间数组,求彼此区间的交集

谈谈二分查找(讲解并手写)

算法 数组内打印三个数,加起来等于指定的值

第一道原题,数据流的中位数。这道题有点吃亏,虽然见过原题,但是没有去网上找过最 优的解法。然后现场面,面试官就硬要让优化,最后才想出来用两个堆实现。

面试题:无序中位数找第 K 大的数字-> 内存是只能存 1/3 的数据怎么办

三数之和

x^2=n 怎么求 x?

股票买入时机,限制最多两次。

random(n)可以生成 0 到 n-1 的数,用 random(n)实现 random(m),m 为任意值

求分位数,相当于手撕快排

01 矩阵求最大正方形

Fib数列

有一个序列,知道 a[1]>a[0], a[n-1] > a[n],求在小于 O(n)的复杂度下,求这个序列的波峰。卡了很久,然后面试官稍微提醒了下,代码写出来了.

手写代码:从长序列中找出前 K 大的数字,堆排序

算法 12322121343434 1232212134343*4 数字必须在 0-600 之间,有几种插入方法

给两个数组(长度可能不等),要求循环输出

A=1,2,3

B=A,B,C,D,

就要输出 1A2B3C1D2A3B...

这个很简单的,维护两个下标变量 ai,bi,自增时取模就行了,如

ai = (ai+1)%A.length

要求输出重复时循环跳出

一开始想到最小公倍数去了,面试官提示从下标考虑,当 ai 与 bi 相等且为 0 的时候跳出 就行了

如果数组自己内部有重复,求出重复的部分

如 A=1,2,3,1,2,3,1,2,3

就求出 1,2,3

找名人

所有人都认识 TA,但是 TA 不认识任何其他人

a [i] [j] = 1, i 认识 j

a [i] [j] = 0, i 不认识 j

a [i] [i] 置空

给定 n*n 的二维数组,有多少个名人?具体都是谁?

我的思路就是大力出奇迹,遍历美滋滋,结果当场去世……

实际上有技巧的,i 认识 j,则 i 不是名人,i 不认识 j,则 j 不是名人,以此可以进行推 理

代码是一个 1-13 张牌分牌(队列)

零钱问题(有零钱1,3,5 求有多少种可能性)

算法题:一堆纸牌,哥哥和妹妹都按照最优策略轮流随机抽取一张,最后返回哥哥手中牌的分数之和与妹妹手中牌的总和的差

一个环上有 10 个点,编号为 0-9,从 0 点出发,每步可以顺时针到下一个点,也可 以逆时针到上一个点,求:经过 n 步又回到 0 点有多少种不同的走法

给你一个数字 n(n < 1e9),再给你一个数字 k(k < n),要求你找到 1,2,3,...,n 按照字典序排序后,第 k 小的数字;

选了第二道,但是不建议真的排序后再输出,最后用的递归,但是写的有点 bug😂😂,面 试官说主要还是考察思路和逻辑

算法题, M*N 横向纵向均递增的矩阵找指定数

算法题: N 场演唱会, 以 [{startTime, endTime}…] 的形式给出, 计算出最多能听几 场演唱会。用你最熟悉的语言把这个算法实现

No Pains, No Gains