引言
数组是线性数据结构之一,即将相同类型的元素存储于连续内存空间。本文主要总结了数组的特点,以及推荐了针对性的练习题,涉及到的算法包括二分法、双指针、滑动窗口和模拟。
数组的定义
数组是将相同类型的元素存储于连续内存空间的数据结构,其长度不可变。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
数组的数据结构,如图所示(int array[] = {2, 3, 1, 0, 2};):
数组特点
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
- 数组在删除或者增添元素的时候,要频繁移动其他元素的地址。
- 二维数组每种语言的实现不同,但是一般每一行内的元素的地址是连续的。C++中二维数组的元素从左到右、从上至下在地址空间上是连续的。 Go和Java可能是二维数组可能是如下排列的方式:
可变数组
可变数组 是经常使用的数据结构,其基于数组和扩容机制实现,相比普通数组更加灵活。常用操作有:访问元素、添加元素、删除元素。
C++:
// 初始化可变数组
vector<int> arr;
// 向尾部添加元素
arr.push_back(2);
Go:
var arr []int
arr = append(arr,1)
Java:
List<Integer> arr = new ArrayList<>();
arr.add(2);
二分法
二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
- 第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] 。
- for left <= right 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if nums[mid] > target 的分支 right 要赋值为 middle - 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
for left <= right {
mid := left + (right-left)>>1
if target == nums[mid] {
return mid
} else if target < nums[mid] {
right = mid - 1
} else {
left = mid + 1
}
}
- 第二种写法,定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么:
- for left < right ,这里使用 < , 因为left == right在区间[left, right)是没有意义的
- if nums[mid] > target 的分支 right 更新为 mid,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为mid,下一个查询区间是[left, mid)
for left < right {
mid := left + (right-left)>>1
if target == nums[mid] {
return mid
} else if target < nums[mid] {
right = mid
} else {
left = mid + 1
}
}
双指针法
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
滑动窗口
滑动窗口,需要想清楚两个指针怎么更新,还要维护关于窗口的状态值
滑动窗口可用于解决一些列的字符匹配问题,典型的问题包括:在字符串 s 中找到一个最短的子串,使得其能覆盖到目标字符串 t。对于目标字符串 t,我们可以在字符串 s 上滑动窗口,当窗口包含 t 中的全部字符后,我们再根据题意考虑能否收缩窗口。
在窗口滑动的过程中,我们可以暴力地统计出窗口中所包含的字符是否满足题目要求,但这没有利用到滑动窗口的基本性质。事实上,窗口的滑动过程可分解为以下两步基础操作:
窗口右边界往右滑动一位:窗口右端新加入一个字符,但窗口中的其他字符没有发生变化; 窗口左边界往右滑动一位:窗口左端滑出一个字符,但窗口中的其他字符没有发生变化。
模拟法
模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家边界的掌控能力。
相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。
这三个题基本一样