数组与链表

本文深入探讨了数组和链表两种基本数据结构的特点与应用场景,分析了数组的快速访问优势及链表的动态扩展能力,对比了两者在内存管理、访问效率上的优劣。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数组

什么是数组?

数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下标,可以在常数时间内访问数组元素的这么一个结构;

为什么能在常数时间内访问数组元素? 

为了访问一个数组元素,该元素的内存地址需要计算其距离数组基地址的偏移量。需要用一个乘法计算偏移量,再加上基地址,就可以获得某个元素的内存地址。首先计算元素数据类型的存储大小,然后将它乘以元素在数组中的索引,最后加上基地址,就可以计算出该索引位置元素的地址了;整个过程可以看到需要一次乘法一次加法就完成了,而这两个运算的执行时间都是常数时间,所以可以认为数组访问操作能在常数时间内完成;

数组的优点

  • 简单且易用;
  • 访问元素快(常数时间);

数组的缺点

  • 大小固定:数组的大小是静态的(在使用前必须制定数组的大小);
  • 分配一个连续空间块数组初始分配空间时,有时候无法分配能存储整个数组的内存空间(当数组规模太大时);
  • 基于位置的插入操作实现复杂:如果要在数组中的给定位置插入元素,那么可能就会需要移动存储在数组中的其他元素,这样才能腾出指定的位置来放插入的新元素;而如果在数组的开始位置插入元素,那么这样的移动操作开销就会很大

关于数组的一些问题思考

1)在索引没有语义的情况下如何表示没有的元素?

我们创建的数组的索引可以有语义也可以没有语义,比如我现在只是单纯的想存放100,98,96这三个数字,那么它们保存在索引为0,1,2的这几个地方或者其他地方都可以,无论它们之间的顺序怎样我都不关心,因为它们的索引是没有语义的我只是想把它们存起来而已;但是如果它们变成了学号为1,2,3这几个同学对应的成绩,那么它们的索引就有了语义,索引0对应了学号为1的同学的成绩,索引1对应了学号2的同学,索引2对应了学号3的同学,因为数组的最大的优点是访问元素是在常数时间,所以我们使用数组最好就是在索引有语义的情况下;

好了,那么如果在索引没有语义的情况下,我们如何表示没有的元素呢?例如上图中,对于用户而言,访问索引为3和4的数组元素是违法的,因为它们根本就不存在,我们如何表示没有的元素呢?

表示为0或者-1?

2)如何添加元素和删除元素呢?

我们知道,数组的明显缺点是在创建之前需要提前声明好要使用的空间,那么当我们空间满了该如何处理呢?又该如何删除元素呢?在Java中提供给我们的默认数组是不支持这些功能的,我们需要开发属于自己的数组类才行;

Java中的ArrayList的扩容

这里提出来一个grow()的方法,来看看ArrayList是怎么实现动态扩容的:

从上面的源码我们可以看到ArrayList默认增容是增加当前容量的0.5倍(>> 1即乘以0.5)

链表

什么是链表

链表是一种用于存储数据集合的数据结构,它是最简单的动态数据结构,我们在上面虽然实现了动态数组,但这仅仅是对于用户而言,其实底层还是维护的一个静态的数组,它之所以是动态的是因为我们在add和remove的时候进行了相应判断动态扩容或缩容而已,而链表则是真正意义上动态的数据结构;

链表的优点

  • 真正的动态,不需要处理固定容量的问题;
  • 能够在常数时间内扩展容量;

对比我们的数组,当创建数组时,我们必须分配能存储一定数量元素的内存,如果向数组中添加更多的元素,那么必须创建一个新的数组,然后把原数组中的元素复制到新数组中去,这将花费大量的时间;当然也可以通过给数组预先设定一个足够大的空间来防止上述时间的发生,但是这个方法可能会因为分配超过用户需要的空间而造成很大的内存浪费;而对于链表,初始时仅需要分配一个元素的存储空间,并且添加新的元素也很容易,不需要做任何内存复制和重新分配的操作;

链表的缺点

  • 丧失了随机访问的能力;
  • 链表中的额外指针引用需要浪费内存;

链表有许多不足。链表的主要缺点在于访问单个元素的时间开销问题;数组是随时存取的,即存取数组中任一元素的时间开销为O(1),而链表在最差情况下访问一个元素的开销为O(n);数组在存取时间方面的另一个优点是内存的空间局部性,由于数组定义为连续的内存块,所以任何数组元素与其邻居是物理相邻的,这极大得益于现代CPU的缓存模式;

链表和数组的简单对比

  • 数组最好用于索引有语意的情况,最大的优点:支持快速查询
  • 链表不适用于索引有语意的情况,最大的优点:动态

链表虚拟头结点的作用

  • 为了屏蔽掉链表头结点的特殊性;
    因为头结点是没有前序结点的,所以我们不管是删除还是增加操作都要对头结点进行单独的判断,为了我们编写逻辑的方便,引入了一个虚拟头结点的概念;

LeetCode相关题目参考

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int [] res = new int[2];
        if(numbers==null||numbers.length<2)
            return res;
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i = 0; i < numbers.length; i++){
            if(!map.containsKey(target-numbers[i])){
                map.put(numbers[i],i);
            }else{
                res[0]= map.get(target-numbers[i]);
                res[1]= i;
                break;
            }
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值