数据结构 - 数组、链表

目录

1、数组

2、链表


    最简单是数据结构就是线性表,包括数组、链表、栈和队列。线性表就是数据像线一样连城一条线,线上的每个节点都只有前后两个方向。其他的就是非线性结构,包括树(一个节点可以链接到多个节点)、图(节点可能成环),或者散列表、跳表等。用一个对比图展示:

 

1、数组

    数组的特点:

        1)、属于线性表

        2)、数组创建时会申请一片连续的内存空间,并且存储每个位置上的数据类型相同(即创建完成后,内存空间已定,不能再扩展)。

        3)、数组通过下标进行随机访问的时候可以直接计算出内存地址位置,所以时间复杂度为 O(1); 但是查询值等情况时需要遍历整个数组,所以此时最好时间复杂度为 O(1), 最坏时间复杂度为 O(N), 平均时间复杂度也为 O(N)

        4)、插入和删除时,需要将插入的下标位置后面的所有元素往后挪动一位,所以写(插入、删除)的时间复杂度为 O(N)。如果要提高插入和写入的性能,可以先进行标记删除,等到一定量时,直接将后续的所有节点都向后移动多位以提高性能。

    数据为什么能根据下标随机访问?数组下标为什么从0开始?

数组在创建时就确定了长度,也确定了存放的类型,那么当申请完成后,每个下标位置的内存地址就确定了。int类型占用4个字节,long类型占用8个字节,引用数据(对象)类型存放的是对象头信息长度也是确定的。反之通过下标就可以计算出对应的内存地址:

下标为 i 的内存地址 = 数组的启始内存地址 + i * 数据类型占用的长度; 

比如  int[] ,内存一般使用十六进制表示,当前为了方便使用十进制表示,并且该数组申请到的内存启始地址为 10 ,那么:

下标为 2 的内存地址 = 10 + 2 * 4 = 18

  但是如果数组的下标是从1开始的话,计算公式就变成了(CPU就需要多一次的 读数据 - 运算 - 写数据 ): 下标 i 内存地址 = 数组启始内存地址 + (i - 1)* 数据类型占用的长度

 

   利用数组下标可以随机访问的特点,有哪些扩展?

1)、二分查找 就是基于下标可以随机访问的特点,二分下标后进行向左或向右查询,从而实现了 O(logN) 的时间复杂度

2)、散列表 也是利用hash函数计算值再取余等方式,计算出下标位置, 从而实现了 读写的时间复杂度近似 O(1)

 

2、链表

    链表按照结构可分为单链表、双链表、循环链表、双向循环链表,其中项目工程上使用最广泛的还是双向链表。链表由数据节点(一般用Node表示),前驱指针指向前一个数据节点地址(一般用 prev 表示),后驱指针指向下一个数据节点地址(一般用 next 表示)。链表的第一个节点一般叫 头结点,使用 head(或first) 表示;最后一个节点叫尾节点,使用 tail(或last) 表示。

    链表不像数组一样创建的时候就确定大小,开辟连续的内存空间。链表的节点之间是由指针进行链接,所以可以认为链表本身就是动态扩容的数据结构,不像ArrayList(底层使用数组,默认长度为10)当数组长度不足时,需要新创建一个更大的数组,将原数据拷贝进去再将原数组释放(让jvm进行垃圾回收),扩容本身不仅影响程序的执行时间,对内存也有消耗。链表可以利用碎片的内存空间,支持动态扩容这也是该数据结构的优点。

Java中的 LinkedList 就是一个双向链表结构如下:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable {
    // 链表的长度
    transient int size;
    // 链表的头结点
    transient java.util.LinkedList.Node<E> first;
    // 链表的尾节点
    transient java.util.LinkedList.Node<E> last;

    // 表示节点的数据结构
    private static class Node<E> {
        // 节点存储数据
        E item;
        // 后继指针
        LinkedList.Node<E> next;
        // 前驱指针
        LinkedList.Node<E> prev;
    }
}

    根据链表结构的不同,节点间的结构关系如下图:

 

    链表和数组一样都是最基础的数据结构,并且他们很多情况刚好相反,从理论链表中查找一个元素则只能从头到尾查找,所以时间复杂度为 O(N)。如上链表中一般会存储头和尾节点,所以当需要插入数据时只需要用指针将两个节点连在一起即可;删除元素时只需要将删除元素的前后节点用指针链接,所以理论上链表的插入、删除的时间复杂度是O(1)。链表的删除、插入操作如下图:

将数组和链表进行比较,如下:

    但是实际语言开发过程就会发现,链表的插入和删除操往往伴随着查询操作。所以需要根据具体情况进行分析,删除操作一般有两种情况:

1、删除链表中值为 某个特点值的节点(那么则需要遍历整个链表,查找特定值的节点,那么时间复杂度为 O(N) )

2、已经有链表某个节点(Node)的指针了,需要删除该节点(那么需要将修改该节点的上一个节点 的后驱指针)

    所以这就是理论与实际的差距,也是基于此,实际使用双向链表的场景远多于单向链,比如Java中 LinkedList、LInkedHashMap容器,juc并且包的核心 AbstractQueuedLongSynchronizer(AQS)中的CLH队列。根据具体情况对单项链表和双向链表进行比较,就是空间换时间的设计思想,双向链表虽然增加了前驱节点的存储空间,但是可以在O(1)时间复杂度查询到当前节点的前一个节点。具体对比如下:

 

    最后想说的就是链表的数据结构虽然感觉比较简单,但是涉及的相关操作有大量的指针(引用)和边界判断条件,所以非常难写和读。之前在研究AQS源码、自己写单链表反转等时,深有体会。不管面试刷题还是基于自己逻辑思维的训练,或是对链表这种数据结构更深的理解,可能下面的练习都是少不了的:

反转链表

环路检测

合并两个排序的链表

链表中倒数第k个节点

链表的中间结点

【链表】其他更多算法训练

 

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值