数据结构和算法分析学习心得-数组篇(持续更新)

          前面几节写了二分查找算法,写完后发现篇幅有些太长了,就决定把后面要写的内容单独写在一个篇章里,不然堆积在一块查看起来非常的不方便。

2025年9月23日

数组

定义:在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识。
        其中元素的类型必须是一致的,比如说所有元素都是 int 类型或者所有元素都是 double 类型,如果一个数组里的元素既有 int 类型又有 double 类型,这样的元素是无法用数组来管理的。

特点:数组中的元素都是连续存储的,数组的元素在进行存储时是一个挨着一个的。
        通过这个特点我们可以获得一个好处,好处是当我们要计算数组中某个元素的地址,我们可以通过它的索引来计算。我们举一个例子来说明,说现在我们有一个 int 整数数组,里面的元素是[1,2,3,4,5],他们对应的索引就是 01234,现在假设我们已经知道 0 索引的元素地址是 b,那我想要 1 索引的元素地址该怎么办?
        各位想一下,我们前面不是说过数组的元素都是连续存储的,那 0 元素后面是不是就是紧挨着 1 元素,因为这是一个 int 类型的数组,所以 0 元素占 4 个字节,而 1 元素地址就是 b+4,类似的,索引 2 的元素地址就是 b+8,索引 2 的元素地址就是 b+12,索引 2 的元素地址就是 b+16,由于数组的元素是连续存储,那我们就可以通过索引计算出元素的地址,由此我们就得出一个公式:BaseAddress + i * size

  • BaseAddress 数组的数组起始地址
  • i 即索引,在 java、C 等语言都是从 0 开始
  • size 是每个元素占用字节,例如 int 占 4 个字节,double 占 8 个字节

        有了公式,我们来做一个小测试,现在又有了一个数组,数组的元素还是 [1,2,3,4,5],但是这个数组是一个byte类型的数组,现在我想知道元素 3 的地址是什么,题目已给出数组的起始地址是 0x7138f94c8。
        套用一下公式:0x7138f94c8 + 2 * 1 得出结果是 0x7138f94ca,a 是 16 进制 10。

        上面说的就是在数组中怎么结算某个元素的地址,最后我们来分析一下它的空间占用,空间占用这一块实现的语言不一样,以 java 为例,java 本质上是一个对象,所以它也是有对象头部分,还有数据部分,主要是这两部分组成,对象头部分又分成了 markword 和 类指针这两个部分,markword 是记录对象的哈希码,包括垃圾回收时的分带年龄,给对象加锁时一些锁的信息都可以记录到 markword 里,而 markword 会占用到 8 个字节,这是第一部分。
        接下来对象里头还有四个字节的是类指针,类指针就是说你这个对象是什么类型就需要通过类指针去寻找对象它的实际类型,如果我现在是一个 int 数组,它的指针类型各位可以简单理解为它指向的就是我们 int [ ].class,这个占用 4 个字节。
        还有四个字节是记录数组的大小,这个也占4个字节,所以它就可以决定我们数组的最大容量是 2^{32},也就是数组内你能存的元素是不超过 2^{32},这是我们对象的头部分,一共是16个字节。
        接下来就是数组元素了,就以数组 int [1,2,3,4,5] 这个 5 个元素为例,由于这个是整数类型的数组,所以 int 数组里的每个元素都各占 4 个字节,一共是存了 5 个元素,最后还有一个叫对齐字节,什么是对其字节,因为我们前面存了5个元素,导致5后面缺了一个元素,而 java 里它这个对象大小都是 8 字节的整数倍,你看上面的 markword 是八个字节,类指针大小加起来也是 8 个字节,剩下的几个元素也都是 8 个字节,但是5后面没有元素了,所以就要补齐 8 个字节,所以要再加一个对齐字节,这是空间占用。

下面我们再说一一下数组的随机访问性能,随机访问就是根据索引查找元素,前面我们说过了知道数组元素索引就间接知道了数组元素地址,因此它跟我们的这个数组内的元素个数是没有关系的,知道了起始地址,知道了索引,那我们就可以通过公式计算出它的地址了,因此它的时间复杂度是常数时间也就是O(1)

2025年8月24日更新

动态数组

        前面说的数组都是 Java 自带的静态数组,静态数组是不能够插入以及删除元素的,而它的大小在创建数组时就固定死了,无法改变;而与之对应的能够插入或删除元素,它的大小也能根据我的实际需求而发生变化的数组是动态数组。这里插入一个题外话 Java 里也是自带动态数组的,就是各位熟悉的 ArrayList。

        当然,在学习数据结构时最好是自己来实现一个动态数组,下面来说一下动态数组的一些特性。我们已经定义了一个数组如图 1。

图1

        首先看到一个 size,这个 size 我们称之为逻辑大小,它所代表的是动态数组里有效的元素个数,最开始是 0,就表示这个动态数组里还没有管理元素,虽然图 1 里展示的有一堆元素,但是这些元素都是不能用的,后面随着我往这个动态数组里添加元素,这个 size 就会增大,删除元素,size 就会缩小。
        我们举个例子,现在我要往图 1 这个动态数组里添加 1 2 3 4 5 这个几个元素,他的添加过程是加入元素 1 时,size 变成了 1,如图 2,再加一个元素 2,size 就变成了 2,如图 3,直到 5 个元素都加进去了,size 变成了 5,如图 4,这样就表示当前这个动态数组里,前面这 5 个元素都是有效的是可以使用的,但是后面这个几个灰色是无效的我们是不能访问的。

图2
图3
图4

        再看一个删除的例子,比如说我想把索引 2 位置这个元素 3 删掉,来看一下删除的过程,由于要保证数组元素的连续性,元素 3 没了,那 1 2 4 5 就要连起来,怎么连?我们先把元素 4 5 往前面移动,如图 5,元素 4 5 取代了原来 3 的位置,然后再把 size 减一,如图 6,这时候元素就变成 1 2 4 5 了,元素 3 就删掉了。

图5
图6

        我们看到了动态数组的逻辑大小,它就是管理了数组内有效的元素个数,添加就增大,删除就缩小,而动态数组还有一个重要的属性,就是它的容量,通过上面图片展示的动态数组元素的个数,我们可以看出这个动态数组的容量是 8 个,如果我想再往里放第九个元素怎么办?
        下面就要说到动态数组的一个重要功能,它还支持一个扩容,现在我要往这个动态数组里再添加 6 7 8 9 10 这四个元素,来看一下扩容的过程,我看到在添加 6 7 8 9 这几个元素时是没问题的,如图 7 8 9 10,在加元素 10 的时候,就发现这个动态数组的容量不够了,不够就要进行扩容,而扩容就是创建一个容量更大的动态数组,如图 11,下面的操作就是把旧数组里的元素给复制到新数组里去,然后就看到新数组把旧数组给替代掉了,如图 12。

图7
图8
图9
图10
图11
图12

        而代替之后旧可以把元素 10 给加进新数组里了,如图13。

图13

下面我们看一下这些操作是怎么在代码里实现的

public class DynamicArray {
    private int size = 0; // 逻辑大小
    private int capacity = 8; // 容量
    private int[] array = new int[capacity];
}

        通过上面的代码,我们来思考一下这个类里的这些属性是干什么的,先看 size 它代表的是我们动态数组里的一个逻辑大小,是用来控制我们数组内有效元素的个数,接着看 capacity,它代表的是容量,最后看 int[] array,从中不难看出,虽然类的名字叫动态数组,但是它的底层还是需要一个静态数组来存储元素,三个属性都有了,再看一下他们的初始值,先看 size,size 后面给个 0 表示这个数组里还没有有效元素,大小是 0,接着看 capacity,它就和上图的数组容量一致,给个 8,当然了,java 里的 ArrayList 的初始容量是 10,最后看 这个静态数组,他的初始值就是上面的 capacity,当这个空数组创建出来后,它的这些元素都被初始成了 0,这无所谓,只要这个 size在控制,就不用担心他访问到这些无效的元素。

2025年8月26日更新

动态数组-插入

public class DynamicArray {
    private int size = 0; // 逻辑大小
    private int capacity = 8; // 容量
    private int[] array = new int[capacity];

    public void addLast(int element) {
        // array[size] = element;
        // size++;
        add(size, element);
    }

    public void add(int index, int element) {
        if (index >= 0 && index < size) {
            System.arraycopy(array,index,
                    array,index + 1,size - index);
            array[index] = element;
            size++;
        } else if (index == size) { // addLast
            array[index] = element;
            size++;
        }
        array[index] = element;
        size++;
    }
}

        接着我们来实现第一个功能 addLast,即新添加的元素是作为最后一个元素加入到动态数组里的,这样的例子其实在前面的更新部分里就说过了,就比如添加 12345 这几个元素,加入 1 的时候,1 被加到最后的位置,再加 2 的时候,2 也是作为最后一个元素加进去了。
        在实现功能之前,我们先想一下,这个 addLast 是怎么确定最后的位置的?其实就是 size,最开始 size 是不是 0?那我添加了元素,是不是就添加到 0 索引上了,再添加第二个元素,这时候 size 是 1,那就应该添加到索引 1 的位置。
        所以这个方法其实不难,这里就不需要考虑返回值了,方法名就是刚才说的 addLast,方法的参数就是我们需要添加新元素,我们把它叫 int element,而方法里面就是我们找到底层 array 数组索引 size 的元素,它就是我们要添加的最后的位置,他的取值等于我们的 element ,添加完之后,size的值要加 1,也就是 size ++,这样第一个功能就实现了。

        接下里就要做第二个功能 add 方法,这个 add 方法就像我们这个数组内某一个索引位置加入新的值,这里就不是加入到最后了。比如我们要向索引 2 的位置加入一个新的元素 6,那原本的 12345 是不是要变成 126 然后 345 向后移,我们先看一下这个效果怎么样,大体的思路就是先把 345 向后移动一位,如图 14,然后索引 2 的位置就空出来了,新的元素 6 就可以插入到索引 2 的位置了,如图 15,这既是 add 方法的含义。

图14
图15

        下面就来实现一下它,方法是void,方法名是 add,但这里要接收两个参数,第一个参数是要插入插入新的元素的索引位置,就叫 index ;第二个参数就是我们要插的值,就叫 element,方法内该写什么,各位想一下,刚才从 index 开始,后面这些元素都要向后移一位,这个就需要用 System.arraycopy 来实现,它可以实现数组间或数组内元素的复制,而它接触的第一个参数即是 array 数组,从 index 这个位置开始拷贝,而参数二就是 index,那么要把它复制到那里?其实还是在同一个数组内进行复制,所以第三个参数,也就是要复制的目标数组,其实就是同一个 array;第四个参数就是起始目标位置 index + 1,因为你要把复制的目标位置的起始位置向后移位,所以目标位置的起始位置就是index + 1;最后一个元素就是说你要复制几个元素?各位看我们是不是应该把从 index 开始到后面这所有的元素都要右移一位,也就是当前这个 size 是 5,index 是 2,是不是 5 - 2 = 3?也就是剩下这 3 个元素都要往后移动一位,因此我们最后一个就代表的是要复制的元素的个数,也就是前面说过的 3。
        把它们汇总在一块就是:我要从初始数组 array 的 index 开始进行一个复制,然后复制到同一个数组,不过要向后移动一位,复制到 index + 1 这个起始位置,复制 size - index 这么多个元素,这样就完成了一个复制,复制完后,索引 2 的位置就空出来,如果我们需要往这个位置加入新的元素,也就是先通过 array 定位到 index 这个索引位置,它的新值就是我们的 element,然后size 要加一,也就是 size ++,这样方法就实现了。

        实现完之后,又要考虑一下边界的问题,这个 index 的有效值是什么?各位看一下index有效值,我们到插入之前,应该是从 0 开始到 size - 1 这个位置,如图 16,从 0 ~ size - 1,如果你小于 0 就没有 1 了(毕竟都不在数组里了);如果是等于 size 是可以的,但是等于 size 相当于他的情况跟我们这个 addLast 是一样的,就是把它作为最后一个元素加进去;如果大于 size 也不行,比如说我们在 6 的索引位置加一个元素,那新加的元素就和前面有效的元素就断了,中间是不能断开的,必须要保证新加的元素跟原来的元素都连在一起。

图16

        所以这里的 index 应该做一个条件的检查,让他是大于等于 0,然后小于 size,所以我们加一个条件,if index 是大于等于 0,并且 index 是小于 size,然后把它用一个括号括起来,如果它不满足这个条件,我们就什么事都不做,或提示一个错误,告诉他您输入的这个 index 不合法,这是这个方法的实现。

        接下来我们对这两个 add 方法的功能进行一个合并,先看这下面这个 add 方法,他处理的实际上是从 index 大于等于 0 到 index 小于 size 这个范围内他的一个插入操作,上面这个addLast方法他处理的恰好是index 等于 size,也就是这个时候是最后一个元素的插入操作,那我们能不能让下面这个 add 的方法的处理能力更大一些,让它涵盖我们这个处理最后一个元素插入的操作?
        当然是可以的,我们给他多加一个 else if 的条件,在这个 else if 里,我们判断一下如果这时候 index 等于 size,这就意味着它即将往最后一个位置插入新的元素,这个条件是不是就跟我们 addLast 实际上是一回事,它就相当于我们能要执行的 addLast,那 addLast 要做什么操作?是不是上面说过的两行代码,如图17,我们把它复制过来,复制过来后你会发现,既然这种情况下 index 都等于 size 了,说明他进入这个条件以后,是不是这里可以替换成 array[index] 了。

图17

        替换之后就会发现,这两行代码跟下面这两行代码重复了,既然重复了就没必要把它放在 if 条件里头,所以我们就把它抽出来放在 if 条件外头,总之你不管是进 if 还是进 else,if 是都会执行这两行代码,所以放在外面会好些,那么重复的那几个代码就可以去掉了,这样这个 else if 就没用了,也可以去掉了,最后的结果就是这个样子。

public class DynamicArray {
    private int size = 0; // 逻辑大小
    private int capacity = 8; // 容量
    private int[] array = new int[capacity];

    public void addLast(int element) {
        // array[size] = element;
        // size++;
        add(size, element);
    }

    public void add(int index, int element) {
        if (index >= 0 && index < size) {
            System.arraycopy(array,index,
                    array,index + 1,size - index);
        }
        array[index] = element;
        size++;
    }
}

        这是做了一个简化,简化其实就是合并,我们这个 add 方法相当于就处理了两种情况,一种情况是在 size 范围内这个插入操作,一种情况是最后也就是 size 等于 size 时的插入操作,就都执行它就可以了,那上面的 addLast 就可以这样写,直接去调用下面这个 add 方法,只不过这个 index 我们就传入 size 作为它的参数就可以了,element 还是刚才的 element,这就是把两个方法它的功能做了个合并,当然我们在进入这个方法之前,其实最好还是做一个 index 的判断,如果它是小于 0 了,或者是大于 size 了,我们都应该报这个错误,就不要让它往下执行。

public class DynamicArray {
    private int size = 0; // 逻辑大小
    private int capacity = 8; // 容量
    private int[] array = new int[capacity];

    public void addLast(int element) {
        // array[size] = element;
        // size++;
        add(size, element);
    }

    public void add(int index, int element) {
        //校验参数
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index 必须在 0 到" + size + "之间");
        }
        
        if (index >= 0 && index < size) {
            System.arraycopy(array,index,
                    array,index + 1,size - index);
        }
        array[index] = element;
        size++;
    }
}

所以完整代码是上面这样的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值