数据结构与算法-数组(内含LeedCode练习)

一、 数组

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+isize 计算出索引 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状态包括以下几个常见的阶段:

  1. 可达状态(Reachable):对象处于可达状态表示它仍然可以被访问和使用,不会被垃圾回收器回收。例如,对象被引用变量引用、对象作为静态变量或局部变量存活等。
  2. 可回收状态(Reclaimable):对象处于可回收状态表示它已经不再被任何引用变量引用,但尚未被垃圾回收器回收。在可回收状态下,对象可以被垃圾回收器标记为待回收,并在下一次垃圾回收过程中回收。
  3. 不可达状态(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 0i<m,0j<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 函数则能充分利用局部性原理加载到的缓存数据

举一反三

  1. I/O 读写时同样可以体现局部性原理

  2. 数组可以充分利用局部性原理,那么链表呢?

    答:链表不行,因为链表的元素并非相邻存储

5.越界检查

java 中对数组元素的读写都有越界检查,类似于下面的代码

bool is_within_bounds(int index) const        
{ 
    return 0 <= index && index < length(); 
}
  • 代码位置:openjdk\src\hotspot\share\oops\arrayOop.hpp

​ 只不过此检查代码,不需要由程序员自己来调用,JVM 会帮我们调用

二、练习

E01. 合并有序数组-力扣88题

88. 合并两个有序数组 - 力扣(LeetCode)

​ 给你两个按非递减顺序排列的整数数组 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已经被排序的性质。为了利用这一性质,我们可以使用双指针方法。这一方法将两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。如下面的动画所示:

gif1

我们为两个数组分别设置一个指针 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题

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;
    }
}

步骤:

  1. 声明并初始化变量 n,其值为输入数组 nums 的长度。
  2. 检查 n 的值是否为 0。如果是,说明数组为空,直接返回 0。
  3. 声明并初始化两个指针 fastslow,它们都初始化为 1。这两个指针用于遍历数组。
  4. 进入循环 while (fast < n),循环条件是 fast 小于数组的长度。
  5. 在循环中,通过比较 nums[fast]nums[fast - 1] 的值来检查重复元素。
  6. 如果 nums[fast] 不等于 nums[fast - 1],表示当前元素不是重复的,将 nums[fast] 的值赋给 nums[slow],同时将 slow 自增 1。这样,slow 指向下一个非重复元素的位置。
  7. 无论是否执行赋值操作,fast 始终自增 1,用于继续遍历数组。
  8. 循环结束后,返回 slow 的值,即非重复元素的个数。

​ 这段代码的时间复杂度为 O(n),其中 n 是输入数组的长度。它通过维护两个指针来进行原地修改,将非重复的元素放在数组的前面,并返回非重复元素的个数。与之前的代码相比,该代码的逻辑更加简洁,但是实现的功能是相同的。

测试:
在这里插入图片描述

复杂度分析

时间复杂度: O(n),其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。

空间复杂度:O(1)。只需要使用常数的额外空间。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值