一、 数组
1.概述
1.1定义
在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识
In computer science, an array is a data structure consisting of a collection of elements (values or variables), each identified by at least one array index or key
因为数组内的元素是连续存储的,所以数组中元素的地址,可以通过其索引计算出来,例如:
int[] array = {1,2,3,4,5}
知道了数组的数据起始地址 B a s e A d d r e s s BaseAddress BaseAddress,就可以由公式 B a s e A d d r e s s + i ∗ s i z e BaseAddress + i * size BaseAddress+i∗size 计算出索引 i i i 元素的地址
- i i i 即索引,在 Java、C 等语言都是从 0 开始
- s i z e size size 是每个元素占用字节,例如 i n t int int 占 4 4 4, d o u b l e double double 占 8 8 8
小测试
byte[] array = {1,2,3,4,5}
已知 array 的数据的起始地址是 0x7138f94c8,那么元素 3 的地址是什么?
答:0x7138f94c8 + 2 * 1 = 0x7138f94ca
1.2Java 中数组结构的空间占用(64位系统)
Java中的数组对象的空间占用由对象头、数组长度、数组元素和对齐字节组成,其中对象头部分包含markword和class指针,数组元素的大小取决于元素类型,而对齐字节用于满足对齐要求。
Java 中数组结构为:
-
8 字节 markword (markword用于存储对象的锁信息、GC状态等)
-
4 字节 class 指针(通过类指针找到对象实际类型)
注:开启了指针压缩(Compressed Oops),则class指针占用4字节,否则占用8字节。
-
4 字节 数组大小(决定了数组最大容量是 2 32 2^{32} 232)
-
数组元素 + 对齐字节(java 中所有对象大小都是 8 字节的整数倍,不足的要用对齐字节补足)
例如
int[] array = {1, 2, 3, 4, 5};
的大小为 40 个字节,组成如下
8 + 4 + 4 + 5*4 + 4(alignment)
Java中的数组是对象,因此还有额外的对象开销,包括对象头部、对齐填充等。
拓展:
GC状态(Garbage Collection State)指的是垃圾回收的状态,用于描述对象在内存管理过程中的状态。
在Java中,垃圾回收是自动进行的,用于回收不再使用的对象以释放内存空间。GC状态反映了对象在垃圾回收过程中所处的不同阶段。
Java中的垃圾回收器通常会根据对象的可达性来确定哪些对象是存活的,哪些对象是可以回收的。GC状态包括以下几个常见的阶段:
- 可达状态(Reachable):对象处于可达状态表示它仍然可以被访问和使用,不会被垃圾回收器回收。例如,对象被引用变量引用、对象作为静态变量或局部变量存活等。
- 可回收状态(Reclaimable):对象处于可回收状态表示它已经不再被任何引用变量引用,但尚未被垃圾回收器回收。在可回收状态下,对象可以被垃圾回收器标记为待回收,并在下一次垃圾回收过程中回收。
- 不可达状态(Unreachable):对象处于不可达状态表示它已经不再被任何引用变量引用,并且已经被垃圾回收器标记为待回收。在下一次垃圾回收过程中,不可达状态的对象将被垃圾回收器回收并释放内存空间。
GC状态的变化是由垃圾回收器的工作决定的。垃圾回收器根据可达性分析和对象引用关系等信息,对对象进行标记、清除和整理等操作,使得不再使用的对象被回收,释放内存空间,以便后续的对象分配和使用。
GC状态的正确理解对于了解Java内存管理、垃圾回收机制以及优化内存使用具有重要意义。它有助于开发人员更好地掌握内存管理和调优技术,提高程序的性能和稳定性。
1.3随机访问性能
即根据索引查找元素,时间复杂度是 O ( 1 ) O(1) O(1)
2.动态数组
java 版本
/**
* 动态数组
*/
public class DynamicArray implements Iterable<Integer>{
//逻辑大小
private int size = 0;
//Java中ArrayList容量是10
private int capacity = 8;
/*这里存在一个问题,如果这个数组没有用,这种初始化方式就会一直占用一个容量为8的数组。
我们这里可以使用懒汉模式的设计思想,一开始不给你那么大的数组,真正用到的时候我再给你那么大的数组。
*/
// private int[] array = new int[capacity];
//初始化数组
private int[] array = {};
/**
* 向最后位置 [size] 添加元素
*
* @param element 待添加元素
*/
public void addLast(int element){
add(size,element);
}
/**
* 向 [0 .. size] 位置添加元素
*
* @param index 索引位置
* @param element 待添加元素
*/
public void add(int index,int element) {
//1.容量检查,方法抽取快捷键Ctrl+Alt+M
checkAndGrow();
//2.添加逻辑
//索引错误判断
if (index < 0 && index > size) {
System.out.println("索引错误");
return ;
};
if (index>=0&&index <size){
//index后的元素向后挪动, 空出待插入位置,通过arraycopy()实现。
System.arraycopy(array, index, array, index + 1, size - index);
array[index] = element;
size++;
}
//相当于最后插入
array[size] = element;
size++;
}
/**
* 扩容
* 如果容量为0,给一个数组,如果size到了容量边界,进行扩容1.5倍。
*/
private void checkAndGrow() {
if (size == 0){
array = new int[capacity];
}else if (size == capacity){
//进行扩容,java中扩容1.5倍 有的用1.618倍 有的2倍
capacity += capacity >>1;
int[] newArray = new int[capacity];
//将旧数组元素复制到新数组
System.arraycopy(array,0,newArray,0,size);
array = newArray;
}
}
/**
* 获取索引元素
* @param index
* @return
*/
public int get(int index){
return array[index];
}
/**
* 删除元素
* @param index
* @return
*/
public int remove(int index){
int removed = array[index];
//最后一个元素不需要移
if (index<size-1){
System.arraycopy(array,index+1,array,index,size-index-1);
}
size--;
return removed;
}
/**
* 遍历方法一
* @param consumer 遍历要执行的操作, 入参: 每个元素
*/
public void foreach(Consumer<Integer> consumer){
for (int i = 0; i < size; i++) {
// System.out.println(array[i]);
//函数接口Consumer提供array[i],返回void。
consumer.accept(array[i]);
}
}
/**
* 遍历方法二:迭代器遍历
* @return
*/
@Override
public Iterator<Integer> iterator() {
//匿名内部类写法
return new Iterator<Integer>() {
//i指针指向初始位置
int i =0;
@Override
public boolean hasNext() {
//在遍历时有没有下一个元素
return i<size;
}
@Override
public Integer next() {
//返回当前元素,并移动到下一个元素
return array[i++];
}
};
}
/**
* 遍历方法三:Stream流遍历
* @return
*/
public IntStream stream(){
return IntStream.of(Arrays.copyOfRange(array,0,size));
}
总结:
动态数组实现步骤:
1.首先定义三个成员变量size,capacity,array数组。这里需要注意的是:
- 数组的容量为8,ArrayList的容量为10。
- 直接给数组容量存在一个问题,如果这个数组一直没有用到,这种初始化方式就会一直占用一个容量为8的数组。此时 我们这里可以使用懒汉模式的设计思想,给个空数组。即一开始不给你那么大的数组,真正用到的时候我再给你那么大的数组。
2.添加元素分为两种情况:
- 在数组尾部添加
- 在数组头部或中间添加。此时我们需要先扩容在插入,扩容的原理是先检查size是否已达容量边界,如果容量为0,给一个数组,如果size到了容量边界,进行扩容1.5倍。扩容通过System.arraycopy()方法将旧数组copy到新数组。插入的逻辑实现检查索引是否错误,然后在有效范围内通过拷贝原数组往后挪的方式进行插入。
3.索引、遍历元素,这里遍历方式有三种:
-
普通for循环遍历
可使用函数接口Consumer来进行返回指定array[i]。
优点: 效率最高,遍历快,可以根据自定计数器操作元素
缺点: 不适用所有集合,适用范围小
-
迭代器遍历
利用匿名内部类的hasNext与next方法实现遍历。
优点: 迭代器提供了操作元素的方法 可以在遍历中相应地操作元素
缺点: 运行复杂,性能稍差,效率相对其他两种遍历方式较低
-
Stream流遍历
利用IntStream.of(Arrays.copyOfRange(array,0,size))方法,进行遍历
优点: stream有并发流,可以处理高并发流量
缺点:数据量小的情况下,效率没有for循环高。
4.删除操作往前挪。
插入或删除性能
头部位置,时间复杂度是 O ( n ) O(n) O(n)
中间位置,时间复杂度是 O ( n ) O(n) O(n)
尾部位置,时间复杂度是 O ( 1 ) O(1) O(1)(均摊来说)
注:集合的底层就是数组。
3.二维数组
int[][] array = {
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25},
{31, 32, 33, 34, 35},
};
内存图如下
- 二维数组占 32 个字节,其中 array[0],array[1],array[2] 三个元素分别保存了指向三个一维数组的引用
- 三个一维数组各占 40 个字节
- 它们在内层布局上是连续的
更一般的,对一个二维数组 A r r a y [ m ] [ n ] Array[m][n] Array[m][n]
- m m m 是外层数组的长度,可以看作 row 行
- n n n 是内层数组的长度,可以看作 column 列
- 当访问
A
r
r
a
y
[
i
]
[
j
]
Array[i][j]
Array[i][j],
0
≤
i
<
m
,
0
≤
j
<
n
0\leq i \lt m, 0\leq j \lt n
0≤i<m,0≤j<n时,就相当于
- 先找到第 i i i 个内层数组(行)
- 再找到此内层数组中第 j j j 个元素(列)
小测试
Java 环境下(不考虑类指针和引用压缩,此为默认情况),有下面的二维数组
byte[][] array = {
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25},
{31, 32, 33, 34, 35},
};
已知 array 对象起始地址是 0x1000,那么 23 这个元素的地址是什么? 23->array[1] [2]
答:
- 起始地址 0x1000
- 外层数组大小:16字节对象头 + 3元素 * 每个引用4字节 + 4 对齐字节 = 32 = 0x20
- 第一个内层数组大小:16字节对象头 + 5元素 * 每个byte1字节 + 3 对齐字节 = 24 = 0x18
- 第二个内层数组,16字节对象头 = 0x10,待查找元素索引为 2
- 最后结果 = 0x1000 + 0x20 + 0x18 + 0x10 + 2*1 = 0x104a
4.局部性原理
这里只讨论空间局部性
-
cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
-
缓存的最小存储单位是缓存行(cache line),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其临近的数据,这就是所谓空间局部性
CPU(皮秒)
缓(64字节,缓存行 cache line)
内存(纳秒)
对效率的影响
比较下面 ij 和 ji 两个方法的执行效率
public static void main(String[] args) {
int rows = 1_000_000;
int columns = 14;
int[][] a = new int[rows][columns];
StopWatch sw = new StopWatch();
sw.start("ij");
ij(a, rows, columns);
sw.stop();
sw.start("ji");
ji(a, rows, columns);
sw.stop();
System.out.println(sw.prettyPrint());
}
ij 方法
public static void ij(int[][] a, int rows, int columns) {
long sum = 0L;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
sum += a[i][j];
}
}
System.out.println(sum);
}
ji 方法
public static void ji(int[][] a, int rows, int columns) {
long sum = 0L;
for (int j = 0; j < columns; j++) {
for (int i = 0; i < rows; i++) {
sum += a[i][j];
}
}
System.out.println(sum);
}
执行结果
0
0
StopWatch '': running time = 96283300 ns
---------------------------------------------
ns % Task name
---------------------------------------------
016196200 017% ij
080087100 083% ji
为什么 ij 的效率比 ji 快很多呢?
- 缓存是有限的,当新数据来了后,一些旧的缓存行数据就会被覆盖
- 如果不能充分利用缓存的数据,就会造成效率低下
以 ji 执行为例,第一次内循环要读入 [ 0 , 0 ] [0,0] [0,0] 这条数据,由于局部性原理,读入 [ 0 , 0 ] [0,0] [0,0] 的同时也读入了 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [0,1]...[0,13],如图所示
但很遗憾,第二次内循环要的是 [ 1 , 0 ] [1,0] [1,0] 这条数据,缓存中没有,于是再读入了下图的数据
这显然是一种浪费,因为 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [0,1]...[0,13] 包括 [ 1 , 1 ] . . . [ 1 , 13 ] [1,1] ... [1,13] [1,1]...[1,13] 这些数据虽然读入了缓存,却没有及时用上,而缓存的大小是有限的,等执行到第九次内循环时
缓存的第一行数据已经被新的数据 [ 8 , 0 ] . . . [ 8 , 13 ] [8,0] ... [8,13] [8,0]...[8,13] 覆盖掉了,以后如果再想读,比如 [ 0 , 1 ] [0,1] [0,1],又得到内存去读了
同理可以分析 ij 函数则能充分利用局部性原理加载到的缓存数据
举一反三
-
I/O 读写时同样可以体现局部性原理
-
数组可以充分利用局部性原理,那么链表呢?
答:链表不行,因为链表的元素并非相邻存储
5.越界检查
java 中对数组元素的读写都有越界检查,类似于下面的代码
bool is_within_bounds(int index) const
{
return 0 <= index && index < length();
}
- 代码位置:
openjdk\src\hotspot\share\oops\arrayOop.hpp
只不过此检查代码,不需要由程序员自己来调用,JVM 会帮我们调用
二、练习
E01. 合并有序数组-力扣88题
给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-10^9 <= nums1[i], nums2[j] <= 10^9
进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
方法一:直接合并后排序
算法
最直观的方法是先将数组 nums2 放进数组 nums1 的尾部,然后直接对整个数组进行排序。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i != n; ++i) {
nums1[m + i] = nums2[i];
}
Arrays.sort(nums1);
}
}
复杂度分析
时间复杂度: O((m+n)log(m+n))
排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O((m+n)log(m+n))。
空间复杂度: O(log(m+n))。
排序序列长度为 m+n,套用快速排序的空间复杂度即可,平均情况为 O(log(m+n))。
测试:
方法二:双指针
算法
方法一没有利用数组 nums 1与nums 2已经被排序的性质。为了利用这一性质,我们可以使用双指针方法。这一方法将两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。如下面的动画所示:
我们为两个数组分别设置一个指针 p1 与 p2 来作为队列的头部指针。代码实现如下:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = 0, p2 = 0;
int[] sorted = new int[m + n];
int cur;
while (p1 < m || p2 < n) {
if (p1 == m) {
cur = nums2[p2++];
} else if (p2 == n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
}
复杂度分析
时间复杂度: O(m+n)。
指针移动单调递增,最多移动
m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度: O(m+n)。
需要建立长度为 m+n 的中间数组 sorted。
测试:
方法三:逆向双指针
算法
方法二中,之所以要使用临时变量,是因为如果直接合并到数组nums1中,nums 1中的元素可能会在取出之前被覆盖。那么如何直接避免覆盖 nums 1中的元素呢?观察可知,nums 1的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进 nums 1的最后面。
严格来说,在此遍历过程中的任意一个时刻,nums 1数组中有
m−p1−1 个元素被放入 nums 1的后半部,nums2数组中有 n−p2−1 个元素被放入 nums1的后半部,而在指针 p1的后面,nums 1数组有 m+n−p1−1 个位置。由于
m+n−p1 −1≥m−p1−1+n−p2−1
等价于 p2≥−1
永远成立,因此 p1后面的位置永远足够容纳被插入的元素,不会产生p1的元素被覆盖的情况。
实现代码如下:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1, p2 = n - 1;
int tail = m + n - 1;
int cur;
while (p1 >= 0 || p2 >= 0) {
if (p1 == -1) {
cur = nums2[p2--];
} else if (p2 == -1) {
cur = nums1[p1--];
} else if (nums1[p1] > nums2[p2]) {
cur = nums1[p1--];
} else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
}
复杂度分析
时间复杂度: O(m+n)。
指针移动单调递减,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度:O(1)。
直接对数组 nums1原地修改,不需要额外空间。
测试:
E02.合并有序数组-力扣88题改
将数组内两个区间内的有序元素合并
例
[1, 5, 6, 2, 4, 10, 11]
可以视作两个有序区间
[1, 5, 6] 和 [2, 4, 10, 11]
合并后,结果仍存储于原有空间
[1, 2, 4, 5, 6, 10, 11]
方法1——递归法,利用归并
- 每次递归把更小的元素复制到结果数组
merge(left=[1,5,6],right=[2,4,10,11],a2=[]){
merge(left=[5,6],right=[2,4,10,11],a2=[1]){
merge(left=[5,6],right=[4,10,11],a2=[1,2]){
merge(left=[5,6],right=[10,11],a2=[1,2,4]){
merge(left=[6],right=[10,11],a2=[1,2,4,5]){
merge(left=[],right=[10,11],a2=[1,2,4,5,6]){
// 拷贝10,11
}
}
}
}
}
}
代码
/**
* 方法一 递归法 利用归并
* @param a1 原始数组
* @param i 第一个有序区间的起点
* @param iEnd 终点
* @param j 第二个有序区间的起点
* @param jEnd 终点
* @param a2 结果数组 (k)
* @param k 结果数组索引
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd,
int[] a2, int k) {
//1.判断区间是否还有元素,如果i区间没元素了,把j剩下的copy到结果数组,反之亦然
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
return;
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
return;
}
//2.比较第一个有序区间和第二个有序区间的起点,小的放入结果数组,然后再递归下一个
if (a1[i] < a1[j]) {
a2[k] = a1[i];
merge(a1, i + 1, iEnd, j, jEnd, a2, k + 1);
} else {
a2[k] = a1[j];
merge(a1, i, iEnd, j + 1, jEnd, a2, k + 1);
}
}
方法2 非递归法——利用指针思想
- 三个指针遍历
/*
i
1 5 6
j
2 4 10 11
k
1 2 4 5 6 10 11
*/
代码
/**
*方法二 非递归法 利用指针
* @param a1 原始数组
* @param i 第一个有序区间的起点
* @param iEnd 终点
* @param j 第二个有序区间的起点
* @param jEnd 终点
* @param a2 结果数组 (k)
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = 0;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
测试
public static void main(String[] args) {
int[] a1 = {1, 5, 6, 2, 4, 10, 11};
int[] a2 = new int[a1.length];
// merge(a1, 0, 2, 3, 6, a2, 0);
merge(a1, 0, 2, 3, 6, a2);
System.out.println(Arrays.toString(a2));
System.arraycopy(a2, 0, a1, 0, a2.length);
System.out.println(Arrays.toString(a1));
}
E03.删除有序数组中的重复项-力扣26题
给你一个升序排列的数组 nums ,请你原地删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
- 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
- 返回 k 。
判题标准:
系统会用下面的代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int k = removeDuplicates(nums); // 调用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被通过。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
- 1 <= nums.length <= 3 * 10^4
- -104 <= nums[i] <= 104
- nums 已按升序排列
方法一:双指针法
写法一:
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int fast = 1, slow = 1;
while (fast < n) {
if (nums[fast] != nums[fast - 1]) {
nums[slow] = nums[fast];
++slow;
}
++fast;
}
return slow;
}
}
步骤:
- 声明并初始化变量
n
,其值为输入数组nums
的长度。 - 检查
n
的值是否为 0。如果是,说明数组为空,直接返回 0。 - 声明并初始化两个指针
fast
和slow
,它们都初始化为 1。这两个指针用于遍历数组。 - 进入循环
while (fast < n)
,循环条件是fast
小于数组的长度。 - 在循环中,通过比较
nums[fast]
和nums[fast - 1]
的值来检查重复元素。 - 如果
nums[fast]
不等于nums[fast - 1]
,表示当前元素不是重复的,将nums[fast]
的值赋给nums[slow]
,同时将slow
自增 1。这样,slow
指向下一个非重复元素的位置。 - 无论是否执行赋值操作,
fast
始终自增 1,用于继续遍历数组。 - 循环结束后,返回
slow
的值,即非重复元素的个数。
这段代码的时间复杂度为 O(n),其中 n 是输入数组的长度。它通过维护两个指针来进行原地修改,将非重复的元素放在数组的前面,并返回非重复元素的个数。与之前的代码相比,该代码的逻辑更加简洁,但是实现的功能是相同的。
测试:
复杂度分析
时间复杂度: O(n),其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。
空间复杂度:O(1)。只需要使用常数的额外空间。