X Tutup
### 数组 数组可以说是最基本最常见的数据结构。数组一般用来存储相同类型的数据,可通过数组名和下标进行数据的访问和更新。数组中元素的存储是按照先后顺序进行的,同时在内存中也是按照这个顺序进行连续存放。数组相邻元素之间的内存地址的间隔一般就是数组数据类型的大小。 因为 `字符串` 是由字符数组形成的,所以二者是相似的。大多数面试问题都属于这个范畴。 ## 前言 具体介绍数组之前,我们先来了解一下集合、列表和数组的概念之间的差别。 ### 集合 集合一般被定义为:由一个或多个确定的元素所构成的整体。 通俗来讲,集合就是将一组事物组合在一起。你可以将力扣的题库看作一个集合: ![1.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozoy8b55j30xq047tbg.jpg) 也可以将力扣商店里的礼品看作一个集合: ![2.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozpjcnmvj31dv0lngxp.jpg) 甚至可以将桌面上的物品当作一个集合。 集合有什么特性呢? 首先,**集合里的元素类型不一定相同。** 你可以将商品看作一个集合,也可以将整个商店看作一个集合,这个商店中有人或者其他物品也没有关系。 其次,**集合里的元素没有顺序。** 我们不会这样讲:我想要集合中的第三个元素,因为集合是没有顺序的。 事实上,这样的集合并不直接存在于编程语言中。然而,实际编程语言中的很多数据结构,就是在集合的基础上添加了一些规则形成的。 ### 列表 列表(又称线性列表)的定义为:是一种数据项构成的有限序列,即按照一定的线性顺序,排列而成的数据项的集合。 列表的概念是在集合的特征上形成的,它具有顺序,且长度是可变的。你可以把它看作一张购物清单: ![3.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozqdf70vj30dg0cwmxx.jpg) 在这张清单中: - 购物清单中的条目代表的类型可能不同,但是按照一定顺序进行了排列; - 购物清单的长度是可变的,你可以向购物清单中增加、删除条目。 在编程语言中,列表最常见的表现形式有数组和链表,而我们熟悉的栈和队列则是两种特殊类型的列表。除此之外,向列表中添加、删除元素的具体实现方式会根据编程语言的不同而有所区分。 ### 数组 数组是列表的实现方式之一,也是面试中经常涉及到的数据结构。 正如前面提到的,数组是列表的实现方式,它具有列表的特征,同时也具有自己的一些特征。然而,在具体的编程语言中,数组这个数据结构的实现方式具有一定差别。比如 C++ 和 Java 中,数组中的元素类型必须保持一致,而 Python 中则可以不同。Python 中的数组叫做 list,具有更多的高级功能。 那么如何从宏观上区分列表和数组呢?这里有一个重要的概念:**索引**。 首先,数组会用一些名为 `索引` 的数字来标识每项数据在数组中的位置,且在大多数编程语言中,索引是从 `0` 算起的。我们可以根据数组中的索引,快速访问数组中的元素。 ![4.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozrdvktdj30iy06qmx4.jpg) **而列表中没有索引,这是数组与列表最大的不同点**。 其次,数组中的元素在内存中是连续存储的,且每个元素占用相同大小的内存。 ![5.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozrke8wuj30ux0gr409.jpg) 相反,列表中的元素在内存中可能彼此相邻,也可能不相邻。比如列表的另一种实现方式——链表,它的元素在内存中则不一定是连续的。 ## 数组的操作 ### 读取元素 读取数组中的元素,即通过数组的索引访问数组中的元素。 这里的索引其实就是内存地址,值得一提的是,计算机可以跳跃到任意的内存地址上,这就意味着只要计算出数组中元素的内存地址,则可以一步访问到数组中的元素。 可以形象地将计算机中的内存看作一系列排列好的格子,这些格子中,每一个格子对应一个内存地址,数据会存储在不同的格子中。 ![1.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozs98zimj30zk0k0ab5.jpg) 而对于数组,计算机会在内存中申请一段 **连续** 的空间,并且会记下索引为 `0` 处的内存地址。例如对于一个数组 `['oranges', 'apples', 'bananas', 'pears', 'tomatoes']`,为了方便起见,我们假设每个元素只占用一个字节,它的索引与内存地址的关系如下图所示。 ![2.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozslroucj30zk0akdgn.jpg) 当我们访问数组中索引为 `3` 处的元素时,计算机会进行如下计算: - 找到该数组的索引 `0` 的内存地址: `2008`; - `pears` 的索引为 `3`,计算该元素的内存地址为 `2008 + 3 = 2011`; 接下来,计算机就可以在直接通过该地址访问到数组中索引为 `3` 的元素了,计算过程很快,因此可以将整个访问过程只看作一个动作,因此时间复杂度为 $O(1)$。 ### 查找元素 前面我们谈到计算机只会保存数组中索引为 `0` 处元素的内存地址,因此当计算机想要知道数组中是否包含某个元素时,只能从索引 `0` 处开始,逐步向后查询。 还是上面的例子,如果我们要查找数组中是否包含元素 `pears`,计算机会从索引 `0` 开始,逐个比较对应的元素,直到找到该元素后停止搜索,或到达数组的末尾后停止。 ![3.gif](https://tva1.sinaimg.cn/large/008i3skNly1gqozxdyr51g316o0dce89.gif) 我们发现,该数组的长度为 `5`,最坏情况下(比如我们查找元素 `tomatoes` 或查找数组中不包含的元素),我们需要查询数组中的每个元素,因此时间复杂度为$ O(N)$,*N* 为数组的长度。 ### 插入元素 假如我们想在原有的数组中再插入一个元素 `flowers` 呢? 如果要将该元素插入到数组的末尾,只需要一步。即计算机通过数组的长度和位置计算出即将插入元素的内存地址,然后将该元素插入到指定位置即可。 ![4.gif](https://tva1.sinaimg.cn/large/008i3skNly1gqozvafsuog30uh0dcu0x.gif) 然而,如果要将该元素插入到数组中的其他位置,则会有所区别,这时我们首先需要为该元素所要插入的位置`腾出` 空间,然后进行插入操作。比如,我们想要在索引 `2` 处插入 `flowers`。 ![5.gif](https://tva1.sinaimg.cn/large/008i3skNly1gqozvjtbgtg30uh0dcu0z.gif) 我们发现,如果需要频繁地对数组元素进行插入操作,会造成时间的浪费。事实上,另一种数据结构,即链表可以有效解决这个问题。 ### 删除元素 删除元素与插入元素的操作类似,当我们删除掉数组中的某个元素后,数组中会留下 `空缺` 的位置,而数组中的元素在内存中是连续的,这就使得后面的元素需对该位置进行 `填补` 操作。 以删除索引 `1` 中的元素 `apples` 为例,具体过程如图所示。 ![6.gif](https://tva1.sinaimg.cn/large/008i3skNly1gqozvs74g7g30uh0dcu0x.gif) 同样地,数组的长度为 `5`,最坏情况下,我们删除第一个元素,后面的 `4` 个元素需要向前移动,加上删除操作,共需执行 `5` 步,因此时间复杂度为 $O(N)$,*N* 为数组的长度。 ## 二维数组 二维数组是一种结构较为特殊的数组,只是将数组中的每个元素变成了一维数组。 ![1.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozw53udoj30zk0ftdgm.jpg) 所以二维数组的本质上仍然是一个一维数组,内部的一维数组仍然从索引 `0` 开始,我们可以将它看作一个矩阵,并处理矩阵的相关问题。 ### 示例 类似一维数组,对于一个二维数组 `A = [[1, 2, 3, 4],[2, 4, 5, 6],[1, 4, 6, 8]]`,计算机同样会在内存中申请一段 **连续** 的空间,并记录第一行数组的索引位置,即 `A[0][0]` 的内存地址,它的索引与内存地址的关系如下图所示。 ![2.png](https://tva1.sinaimg.cn/large/008i3skNly1gqozwtoa8xj30zk0ftq6g.jpg) 注意,实际数组中的元素由于类型的不同会占用不同的字节数,因此每个方格地址之间的差值可能不为 `1`。 实际题目中,往往使用二维数据处理矩阵类相关问题,包括矩阵旋转、对角线遍历,以及对子矩阵的操作等。 ## 刷题 ![leetcode-hot100-array](https://cdn.jsdelivr.net/gh/Jstarfish/picBed/algorithms/leetcode-hot100-array.png) ### [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) > 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 > > 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 > > ``` >输入:nums = [2,7,11,15], target = 9 > 输出:[0,1] >解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 > ``` ### 寻找两个正序数组的中位数(4) > 给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。 > > 请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。 > > 你可以假设 nums1 和 nums2 不会同时为空。 > > > > 示例 1: > > nums1 = [1, 3] > nums2 = [2] > > 则中位数是 2.0 > 示例 2: > > nums1 = [1, 2] > nums2 = [3, 4] > > 则中位数是 (2 + 3)/2 = 2.5 > 二分查找 给定两个有序数组,要求找到两个有序数组的中位数,最直观的思路有以下两种: - 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。 - 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 00 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。 ### [215. 数组中的第K个最大元素](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) > 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。 > > 请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 > > > > 示例 1: > > 输入: [3,2,1,5,6,4] 和 k = 2 > 输出: 5 > > 输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 > 输出: 4 ### [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) > 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 > > 说明:你不能倾斜容器。 > > > > 示例 1: > > ![img](https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg) > > 输入:[1,8,6,2,5,4,8,3,7] > 输出:49 > 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 > > 输入:height = [1,1] > 输出:1 > > 输入:height = [4,3,2,1,4] > 输出:16 > > 输入:height = [1,2,1] > 输出:2 双指针,,从两头开始内卷,先卷了挫的那头 ### [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) > 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 > > 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 > > 此外,你可以假设该网格的四条边均被水包围。 > > > > 示例 : > > ``` > 输入:grid = [ > ["1","1","1","1","0"], > ["1","1","0","1","0"], > ["1","1","0","0","0"], > ["0","0","0","0","0"] > ] > 输出:1 > ``` > > ``` > 输入:grid = [ > ["1","1","0","0","0"], > ["1","1","0","0","0"], > ["0","0","1","0","0"], > ["0","0","0","1","1"] > ] > 输出:3 > ``` ### 移动零(283) > 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 > > 示例: > > 输入: [0,1,0,3,12] > 输出: [1,3,12,0,0] > 说明: > > 必须在原数组上操作,不能拷贝额外的数组。 > 尽量减少操作次数。 ### 排序算法() ### 买卖股票的最佳时机(121) > 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 > > 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 > > 注意:你不能在买入股票前卖出股票。 > > 示例 1: > > 输入: [7,1,5,3,6,4] > 输出: 5 > 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 > 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 > 示例 2: > > 输入: [7,6,4,3,1] > 输出: 0 > 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 ### [15. 三数之和](https://leetcode-cn.com/problems/3sum/) > 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 > > 注意:答案中不可以包含重复的三元组。 > > > > 示例: > > 给定数组 nums = [-1, 0, 1, 2, -1, -4], > > 满足要求的三元组集合为: > [ > [-1, 0, 1], > [-1, -1, 2] > ] ### 最小路径和(64) > 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 > > 说明:每次只能向下或者向右移动一步。 > > 示例: > > 输入: > [ > [1,3,1], > [1,5,1], > [4,2,1] > ] > 输出: 7 > 解释: 因为路径 1→3→1→1→1 的总和最小。 ### 双索引技巧-对撞指针 ### 双索引技巧-滑动窗口 ### 稀疏数组
X Tutup