数据结构与算法 - 数组

数据结构与算法 - 数组

数据结构

一、定义
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

线性表(Linear List)就是数据排成像一条线一样的结构。每个线性表上的数据最多只有两个方向。除了数组,链表、队列、栈也是线性表结构。

与线性表对立的是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。

二、操作

操作 时间复杂度
查找 \(O(1)\)
插入 开头\(O(1)\),末尾\(O(n)\),平均\(O(n)\)
删除 开头\(O(1)\),末尾\(O(n)\),平均\(O(n)\)

三、细节

  1. 为什么数据编号从0开始?

从数组的存储模型上来看,“下标”最确切的定义应该是“偏移量(offset)”。
如果用a代表数组的首地址,a[0]就是偏移量为0的位置,也就是首地址,a[k]就表示偏移量k个 type_size 的位置,所以计算 a[k] 的内存地址,以1开始,则多一次减法运算:

\[\begin{array}{l} \begin{array}{l} a[k]\_address = base\_address + k * type\_size \\ a[k]\_address = base\_address + (k-1)*type\_size \end{array} \end{array} \]

  1. 数组类型与大小如何选择?
    1. 数组类型根据实际的情况,预先估算数据量大小、存储大小等需求;
    2. 如果数据大小事先已知,并且对数据的操作非常简单,可以定义静态数组;
    3. 如果数据大小需要动态添加,并且需要反复操作,可以定义动态数组vector

静态与动态数组

一、 定义

1.静态数组:不可以更改数组长度的

2.动态数组:动态数组本质上就是数组,是由静态数组封装的一些扩容能力。

静态数组在内存中位于栈区,是在定义时就已经在栈上分配了固定大小,在运行时这个大小不能改变,在函数执行完以后,系统自动销毁;

动态数组是用户自己创建出来的,位于内存的堆区,它的大小是在运行时给定,并且可以改变其大小。同时,使用完必须由程序员自己释放,否则严重会引起内存泄露。

二、 动态扩容机制

vector(c++):面试题:C++vector的动态扩容,为何是1.5倍或者是2倍

  1. 扩容原理?当向vector中插入元素时,如果元素有效个数size与空间容量capacity相等时,vector内部会触发扩容机制。拷贝元素和释放旧空间,可以通过&vector[0]的方式来查看数据首地址改变情况。

  2. 扩容大小?每次扩容新空间不能太大,也不能太小,太大容易造成空间浪费,太小则会导致频繁扩容而影响程序效率。不同的的编译器实现方式不同,vs中以1.5倍扩容,GCC以2倍扩容。

  3. 如何避免扩容导致效率低?如果在插入之前,可以预估vector存储元素的个数,提前将底层容量开辟好即可。如果插入之前进行reserve,只要空间给足,则插入时不会扩容,如果没有reserve,则会边插入边扩容,效率极其低下。

  4. 为什么选择以倍数方式扩容?以等长个数k进行扩容,向vector插入n个元素,需要插入元素操作和搬移元素操作的总和:\(n + \sum_{i=1}^{\frac{n}{k}}ik= n + \frac{(1+n/k)*n}{2}\),平摊下来每次操作时间\((n + \frac{(1+n/k)*n}{2})/n=3/2+n/2*k = O(N)\)。以倍数方式m进行扩容,向vector插入n个元素,需要插入元素操作和搬移元素操作的总和:\(n + \sum_{i=1}^{\log_{m}{n}}m^i= n + \frac{m(n-1)}{m-1}=n+\frac{mn}{m-1}\),平摊每次操作的时间\((n+\frac{mn}{m-1})/n = O(\frac{m}{m-1}))=O(1)\),m为常量。

  5. 为什么选择1.5倍或者2倍方式扩容,而不是3倍、4倍?(斐波那契数,1.618)理想的分配方案是在第N次扩容时如果能复用之前N-1次释放的空间,因此按照小于2倍方式扩容,多次扩容之后就可以复用之前释放空间。如果倍数超过2倍(包含2倍)方式扩容会存在:(1)空间浪费可能会比较高,比如:扩容后申请了64个空间,但只存了33个元素,有接近一半的空间没有使用。(2)无法使用到前面已释放的内存。

List(python):list是以两倍进行扩容,并且会创建新的数组,然后拷贝,系统再回收久数组。

二维数组

一、定义

二维数组就是在一维数组上,多加一个维度;

二、声明与初始化

  1. c++ (静态数组)
//数据类型 数组名[行数][列数];
int a1[3][3];
//数据类型 数组名[行数][列数] = {{数据1,数据2,数据3},{数据4,数据5}};
int a2[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
//数据类型 数组名[行数][列数] ={数据1,数据2,数据3,数据4}
int a2[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
//数据类型 数组名[  ] [列数] = {数据1,数据2,数据3,数据4};
int a3[][3] = { 1,2,3,4,5,6 };
//查询二维数组所占内存空间
cout << "二维数组占用内存空间为:" << sizeof(arr) << endl;
cout << "二维数组第一行占用内存为:" << sizeof(arr[0]) << endl;
cout << "二维数组第一个元素所占内存空间" << sizeof(arr[0][0]) << endl;
// 行列数
cout << "二维数组行数为:" << sizeof(arr) / sizeof(arr[0]) << endl;
cout << "二维数组列为:" << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;
//可以查看二维数组的首地址
cout << "二维数组首地址为:" << (int)arr << endl;
cout << "二维数组第一行首地址为:" << (int)arr[0] << endl;
cout << "二维数组第二行首地址为:" << (int)arr[1] << endl;

cout << "二维数组第一个元素首地址:" << (int)&arr[0][0] << endl;
  1. c++ (动态数组)
vector<vector<int>> a(m, vecotr<int>(n, 0));
  1. python (list)
li = [[0 for i in range(m)] for j in range(n)]

山脉数组

一、 定义

山脉数组:\(arr.length >= 3\),在 \(0 < i < arr.length - 1\)条件下,存在\(i\)使得:

  • \(arr[0] < arr[1] < ... arr[i-1] < arr[i]\)
  • \(arr[i] > arr[i+1] > ... > arr[arr.length - 1]\)

二、 题型

序号 题目 难度
—— ———————————————————————————————————————————————— —————
0845 845. 数组中的最长山脉 中等
0852 852. 山脉数组的峰顶索引 简单
0941 941. 有效的山脉数组 简单
1095 1095. 山脉数组中查找目标值 困难

旋转数组

一、 定义

旋转数组:nums在预先未知的某个下标 \(k(0 <= k < nums.length)\)上进行了 旋转,使数组变为\([nums[k], ..., nums[n-1], nums[0], ..., nums[k-1]]\)(下标 从 0 开始 计数)。例如, \([0,1,2,4,5,6,7]\) 在下标 3 处经旋转后可能变为 \([4,5,6,7,0,1,2]\)

注意:

1.旋转数组,无论选择多少次都是二分有序。

2.元素可以重复出现, 因此旋转点一定是最小值, 但最小值不一定是旋转点,如\([2,0,2,2,2]\)。因此需要判断左右端点,当左右端点与中点相等,左递增1右递减1,然后再根据单调性来判断。最小值可以根据右端点来比较。

二、 题型

序号 题目 难度
—— ———————————————————————————————————————————————— —————
189.轮转数组
33. 搜索旋转排序数组 中等
81. 搜索旋转排序数组 II 中等
153. 寻找旋转排序数组中的最小值 中等
154. 寻找旋转排序数组中的最小值 II 困难
剑指 Offer 11. 旋转数组的最小数字 中等
面试题 10.03. 搜索旋转数组 中等

环形数组

一、 定义

数组是 环形 的,所以可以假设从最后一个元素向前移动一步会到达第一个元素,而第一个元素向后移动一步会到达最后一个元素。

二、 题型

序号 题目 难度
—— ———————————————————————————————————————————————— —————
457. 环形数组是否存在循环
剑指 Offer II 090. 环形房屋偷盗

子数组

一、 定义

子数组 是数组的连续子序列。

思路:滑动窗口(可变滑窗)和动态规划。

二、 题型

序号 题目 难度
—— ———————————————————————————————————————————————— —————
152. 乘积最大子数组
209. 长度最小的子数组
525. 连续数组
560. 和为 K 的子数组
581. 最短无序连续子数组
643. 子数组最大平均数 I
644. 子数组最大平均数 II
659. 分割数组为连续子序列
713. 乘积小于K的子数组
718. 最长重复子数组
795. 区间子数组个数
1423. 可获得的最大点数
1248. 统计「优美子数组」
795. 区间子数组个数
1630. 等差子数组
2104. 子数组范围和
978. 最长湍流子数组
1246. 删除回文子数组
1310. 子数组异或查询
LCP 14. 切分数组
898. 子数组按位或操作
1574. 删除最短的子数组使剩余数组有序
992. K 个不同整数的子数组
1524. 和为奇数的子数组数目
2090. 半径为 k 的子数组平均值
930. 和相同的二元子数组
1157. 子数组中占绝大多数的元素
974. 和可被 K 整除的子数组
1186. 删除一次得到子数组最大和
1856. 子数组最小乘积的最大值
862. 和至少为 K 的最短子数组
1191. K 次串联后最大子数组之和
915. 分割数组
剑指 Offer II 010. 和为 k 的子数组
剑指 Offer II 009. 乘积小于 K 的子数组
剑指 Offer II 011. 0 和 1 个数相同的子数组
剑指 Offer II 011. 0 和 1 个数相同的子数组
剑指 Offer 42. 连续子数组的最大和
剑指 Offer II 012. 左右两边子数组的和相等
posted @ 2022-04-07 15:42  yangly1  阅读(64)  评论(0)    收藏  举报