数组
基础知识
数组是有限个相同类型的变量所组成的有序集合,数组中的每一个变量被称为元素。数组是最简单、最常用的数据结构。
数组中的每个元素都有自己的下标(从零开始计数),在内存中是顺序存储(因此有非常高效的随机访问能力)。
数组的基本操作:
- 读取元素:通过数组的下标 获取元素, 如 array[3];
- 更新元素:通过数组下标将新值赋给改元素,如 array[3] = 5;
- 插入元素:插入时需要先将插入位置之后的每元素向后移动,腾出空位,再把要插入的元素放到对应位置上;
- 删除元素:与插入相反,要删除的元素之后的所有元素向前移动即可;
适用于读多写少的场景。
实现数组插入/删除操作的完整实现代码:
class MyArray {
private int[] array;
private int size;
public MyArray(int capacity) {
this.array = new int[capacity];
size = 0;
}
// 插入
public void insert(int element, int index) throw Exception{
// 1、判断访问的下标是否超出数组范围
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出数组的实际元素范围!");
}
// 2、因为是插入,增加元素,需考虑数组容量是否够用,如果实际元素达到数组容量上限,则需要对数组进行扩容
if (size >= array.length) {
// 2.1、数组扩容
resize();
}
// 3、从右向左循环,将元素逐个向右挪 1位
for (int i = size - 1; i >= index; i--) {
array[i+1] = array[i];
}
// 4、腾出的位置放置新元素,完成插入
array[index] = element;
// 5、当前数组大小加一
size++;
}
// 2.1、数组扩容
public void resize() {
int[] newArray = new int[array.length * 2];
// 从旧数组复制到新数组
System.arraycopy(array, 0, newArray, 0, array.length);
array = newArray;
}
// 方便显示,对数组进行遍历输出
public void output() {
for (int i = 0; i < size; i++) {
System.out.println(array[i]);
}
}
// 删除
public int delete(int index) throws Exception {
// 是否出界
if (index < 0 || index >= size) {
throw new IndexOutOfBoundException("超出数组实际元素范围!");
}
int deletedElement = array[index];
// 从左向右 循环,将元素逐个向左移动一位
for (int i = index; index < size - 1; i++) {
array[i] = array[i+1];
}
// 数组计数减一
size--;
return deletedElement;
}
public static void main(String[] args) throws Exception{
MyArray ma = new MyArray(4);
ma.insert(3, 0);
ma.insert(7, 1);
ma.insert(9, 2);
ma.insert(5, 3);
ma.insert(6, 1);
ma.output();
}
}
删除排序数组中的重复项
leetcode 第 26 题 (简单)
要求在原地删除重复出现的元素使得每个元素只出现一次,返回移除后数组的新长度。
@Test
public void removeDuplicates() {
// 测试样例
int[] nums = new int[]{0, 0, 1, 1, 1, 2, 2, 3, 3, 4};
// 慢指针 j,该指针指向的元素前是无重复数据
int j = 0;
for (int i = 0; i < nums.length; i++) {
// 循环快指针 i,由于是有序数组,发现与最后一个元素不同的元素即与前面 所有元素不同,即可添加在 第 j 个元素后
if (nums[j] != nums[i]) {
nums[++j] = nums[i];
}
}
System.out.println(Arrays.toString(nums));
}
复杂度分析:
- 时间复杂度:
O(n)
,假设数组长度为n
, 那么i
和j
最多遍历n
步; - 空间复杂度:
O(1)
, 在原数组中更新,无额外空间占用;
旋转数组
leetcode 第 189 题 (中等)
给定一个数组,将数组中的元素向右移动
k
≥
0
k \geq 0
k≥0 个位置;
@Test
public void rotate() {
int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7};
int k = 3;
int n = nums.length;
int count = gcd(k % n, n);
int next;
int tmp;
// 4、该循环过程可能未遍历到所有元素,应该从下一个数字开始,重复过程(1-3), 重复 count 次(为什么 count = gcd(k % n, n)次?参考代码后解释)
for (int i = 0; i < count; i++) {
int prev = nums[i];
next = i;
do {
// 1、从零开始,将 第i个元素放置在 (i+k)% n 的位置,由于 向后移动超过原数组长度 `n` 以后要补在原数组的开头,所以取 模 n;
next = (next + k) % n;
tmp = nums[next];
nums[next] = prev;
// 2、将 这轮的 next 的元素暂存,用于替换 下一轮 next 元素
prev = tmp;
// 3、退出条件,当回到初始位置(即 i == next)时,退出循环
} while (i != next);
}
// 输出: 5671234
System.out.println(Arrays.toString(nums));
}
// 最大公约数,递归实现的辗转相除
private int gcd(int x, int y) {
return y > 0 ? gcd(y, x % y) : x;
}
注:
- 为什么 count = gcd(k % n, n)次?
从一个位置开始,向后更新,最终回到该位置,故该过程恰好走了整数数量的圈,不妨设为
a
a
a 圈,不难发现,每圈 有
n
n
n 个元素(数组的长度),则
a
a
a 圈掠过
a
n
an
an个元素;
每替换一个元素掠过
k
k
k个元素,设回到起始位置时替换了
b
b
b个元素,此时掠过
b
k
bk
bk个元素;
因此有
a
n
=
b
k
an=bk
an=bk,即
a
n
an
an一定是
n
,
k
n, k
n,k的公倍数,又因为是第一次回到起始位置,因此
a
a
a要尽可能的小,故
a
n
an
an一定是
n
,
k
n, k
n,k的最小公倍数
l
c
m
(
n
,
k
)
lcm(n,k)
lcm(n,k),所以有
b
=
l
c
m
(
n
,
k
)
/
k
b = lcm(n,k)/k
b=lcm(n,k)/k,也就是单次遍历访问到
l
c
m
(
n
,
k
)
/
k
lcm(n,k)/k
lcm(n,k)/k个元素,因而遍历所有的元素需要的次数:
c
o
u
n
t
=
n
l
c
m
(
n
,
k
)
/
k
=
n
k
l
c
m
(
n
,
k
)
=
g
c
d
(
n
,
k
)
count = \frac{n}{lcm(n,k)/k} = \frac{nk}{lcm(n,k)} = gcd(n,k)
count=lcm(n,k)/kn=lcm(n,k)nk=gcd(n,k)
gcd 为最大公约数。