面试之数据结构

一、包括线性结构和非线性结构

线性结构是最常用的数据结构,其特点是数据元素之前存在一对一的线性关系。其中线性结构按存储结构划分又可分为顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表为顺序表,其存储的元素是连续的,链式存储的线性表为链表,其存储的元素不一定连续,元素节点中除了存放元素,还存放了相邻元素的地址。除了常见的数组和链表,队列和栈也属于线性结构。

非线性结构包括:二维数组、多维数组,广义表,树和图结构等

二、数据类型

Java虚拟机中,数据类型可以分为基本类型和引用类型。基本类型的变量保存原始值,他代表的值就是数值本身,引用类型的变量并不是保存对象本身,只是持有具体对象的引用地址。

基本类型包括:byte,short,int,long,char,float,double,boolean(都是小写,注意与封装类的区分)

引用类型包括:类类型,接口类型和数组。

三、java虚拟机内存模型

1.程序计数器

程序计数器是一块较小的内存空间,为线程私有内存,可以看作是当前线程所执行的字节码行号指示器。由于Java虚拟机的多线程是通过线程切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间计数器互不影响。程序计数器是在java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的区域。

2.虚拟机栈

虚拟机栈也是线程私有的,生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应这一个栈帧在虚拟机中从入栈到出栈的过程。虚拟机栈中的局部变量表存放了编译期可知的各种基本数据类型、对象引用以及returnAddress类型。其中64位长度的long和double类型的数据会占用2个局部变量空间,其余数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域规定了两种异常情况:

  • 如果线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常

  • 如果虚拟机可以动态扩展且扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常

3.本地方法栈

一些带native关键字的方法就是需要JAVA去调用本地的C或C++方法,因为Java有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带native关键字的方法。
本地方法栈区域会抛出StackOverflowError和OutOfMemoryError异常。

4.Java堆

Java堆是虚拟机所管理的内存中最大的一块。用于存放实例对象。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.方法区

方法区与Java堆一样,为线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区不需要连续的内存,可以选择固定大小或者可扩展,还可以选择不实现垃圾收集。方法区的内存回收的主要目标是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

6.运行时常量池

运行时常量池是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。当常量池无法再申请到内存时将抛出OutOfMemoryError异常。

7.直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在NIO类中,引入了一种基于通道Channel与缓冲区的I/O方式,可以使用Native方法直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。从而避免了在Java堆和Native堆中来回复制数据,能在某些场景中显著提高性能。当各个内存区域总和大于物理内存限制,会导致动态扩展时出现OutOfMemoryError异常。

四、数组

在内存中是连续的,一旦初始化后,类型和长度不可变

五、集合

 1、ArrayList、Vector、Stack

  • 对象按照插入的顺序存储
  • 同一个对象可以被重复插入,包括Null值
  • 支持根据下标随机快速访问,查询效率高,时间复杂度为:O(1)
  • 容量不够时,会自动扩容,通过内部成员变量Object[] elementData存储实际的数据,默认初始容量10,当容量不够时,每次扩容0.5倍,即:新容量= 旧容量 + ( 旧容量>>1 );最小存储容量: 0(创建对象时,指定容量为0)最大存储容量: Integer.MAX_VALUE 即:2的31次方 - 1
  • 删除跟插入元素时会涉及到数据的前移跟后移,效率不高,时间复杂度为:O(n)
  • 只能存储引用数据类型
  • 线程不安全
  • 通过成员变量int modCount记录此列表在结构上被修改的次数,在Iterator(迭代器)中对列表进行增删操作时,会抛出:ConcurrentModificationException 异常;如果要在循环中删除某个元素,只能通过for循环来删除
  • Vector与ArrayList一样,也继承了AbstractList,内部的实现逻辑也基本一致,不过它所提供的方法都被synchronized修饰,虽然是线程安全的,但性能也相对较差. 目前已不推荐使用
  • Stack继承Vector,只有一个无参构造函数,是一种先进后出的数据结构,内部提供了push(入栈),pop(栈顶出栈),peek(获取栈顶元素),empty(判断是否有元素)和search(判断元素是否存在,没有返回-1)等方法

2.LinkedList

  • LinkedList是基于双向循环链表实现的,除了可以当作链表操作外,由于实现了Deque,它还可以当作栈、队列和双端队列来使用。
  • 头结点中不存放数据,每个节点都存在一个指向前一个节点和指向后一个节点的指针
  • 新增节点默认添加到链表尾部,并修改之前last节点的指向为新节点
  • 删除指定节点,会同时修改前后节点的指向,根据删除元素是否为null分别处理,具体删除逻辑在unlink方法中
  • 通过node()方法返回节点信息,无法随机访问,只能整个遍历,为了提高效率,会根据节点位置与链表元素的中间位置比较,小于则从头部开始遍历,大于则从尾部开始遍历
  • LinkedList线程不安全,只在单线程下适合使用
  • 与arrayList相比,增删操作性能高,不需要扩容。随机访问效率差,不能根据下标直接获取元素,需要整体遍历。底层不是基于数组实现的,内存可以不连续

3.PriorityQueue

  • 优先队列,保证每次取出的数据都是队列中权值最小的
  • 不允许放入null元素,通过二叉小顶堆(完全二叉树)实现,即任意一个非叶子节点的权值,都不大于其左右子节点的权值
  • 基于数组实现,初始容量默认11,按照每层从左向右的顺序存储,节点下标为 n 的子节点分别(2n+1)和(2n+2)

4.ArrayQueue和ArrayDeque

  • ArrayDeque是Deque接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 可以作为栈来使用,效率要高于Stack;ArrayDeque 也可以作为队列来使用,效率比基于双向链表的LinkedList也要更好一些
  • ArrayDeque除了实现Deque,也继承AbstractCollection,使用head和tail来表示索引,但注意tail不是尾部元素的索引,而是尾部元素的下一位,即下一个将要被加入的元素的索引

5.LinkedBlockingQueue和ArrayBlockingQueue

  • 阻塞队列的实现类,ArrayBlockingQueue基于数组实现,LinkedBlockingQueue基于链表实现,都是线程安全的,依赖AQS实现并发操作
  • LinkedBlockingQueue对每一个lock锁都提供了一个Condition用来挂起和唤醒其他线程
  • LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能
  • 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
  • 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
  • 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,从而提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

6.SynchronousQueue

  • 通过CAS实现线程的安全访问,内部没有容器,capacity是0
  • 类似于生产者和消费者,每一次insert操作,必须等待其他线性的remove操作,而每一次remove操作也必须等待其他线程的insert操作
  • 常用于线程中需要传递对象的场景

7.DelayQueue

  • 是一个没有边界阻塞队列实现,加入其中的元素必需实现Delayed接口
  • 当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,队列中元素的顺序是按到期时间排序的,而非它们的入队顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚
  • 消费者线程查看队列头部的元素,然后调用元素的getDelay方法,如果返回值不大于0,则取出此元素;如果大于0,则消费者线程等待这个时间值,此时元素已到期,再从队列头部取出元素
  • 延时队列是主从模式的变种,消费者线程处于等待状态时,总是等待最先到期的元素,而不是长时间的等待。消费者线程尽量把时间花在处理任务上,最小化空等的时间,以提高线程的利用效率

8.HashSet

  • 是基于HashMap实现的,默认构造函数初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入HashSet中的集合元素实由HashMap的key来保存,HashMap的value存储了一个静态的 Object 对象
  • 内部存储元素无序不重复,允许一个null值,通过迭代器遍历不保证结果的顺序,线程不安全
  • 需要重写hashCode()和equals()方法,定义新增元素作为key存入时的比较的标准

9.TreeSet

  • 有序不重复的集合,基于TreeMap实现,支持2种排序方式:自然排序或自定义Comparator排序
  • TreeSet的性能比HashSet差,只能通过迭代器遍历,线程不安全
  • HashSet是用Hash表来存储数据,而TreeSet是用红黑树来存储数据

10.LinkedHashSet

  • 在HashSet的基础上,维护了一个双向链表,该链表定义了迭代顺序:插入顺序或是访问顺序
  • LinkedHashSet底层使用LinkedHashMap来保存所有元素,线程不安全
  • 继承HashSet,增删改查都是调用HashSet的方法,HashSet底层实现是调用HashMap的方法

六、Map

 1.HashMap

  • 哈希表(散列表),增删改查效率高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)
  • hashMap基于数组和链表实现,链表用于解决hash冲突时元素的存放问题,如果散列算法的结果在数组中没有链表,则直接存入,这种情况下的查找效率高,只需一次寻址即可;如果存在冲突,需要遍历链表并和链表中的元素比对(equals),存在就覆盖,不存在就新增,时间复杂度为O(n)。线程不安全
  • jdk1.8以后,hashMap中链表长度超过8会转换为红黑树,长度小于6会还原回链表

2.ConcurrentHashMap和HashTable

  • HashTable是线程安全的,基于HashMap实现。它在所有涉及并发操作的代码块上都加了synchronized关键字,效率很低
  • ConcurrentHashMap在jdk1.7版本利用锁分离的技术实现,由一个Segment数组和多个HashEntry组成。Segment数组的意义就是将一个大的table分成很多小table来进行加锁,每一个Segment元素存储的是HashEntry数组+链表。插入元素时需要进行两次定位,第一次是找Segment,第二次找HashEntry。Segment继承了ReentrantLock,它每次找HashEntry使用重入锁的tryLock(),如果获取锁成功则直接插入相应位置,若是失败会以自旋方式继续尝试,直到超过指定次数,被挂起。get()操作同set()操作一样也是两次。size()操作使用不加锁的模式去尝试多次计算size,最多3次,比较前后两次计算的结果,结果一致就认为没有元素插入,当前计算结果为正确;不一致,就给每个Segment加上锁,然后计算size
  • 在jdk1.8之后,摒弃了Segment的概念,直接用Node数组+链表+红黑树实现,并发控制使用synchronized和CAS操作,其数据结构为:
    1. Node:ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。其本质是一个链表,只能查找,不能修改
    2. TreeNode:继承于Node,数据结构换成了二叉树,用于红黑树中存储数据,当链表的结点数大于8时会转换成红黑树结构
    3. TreeBin:封装TreeNode的容器,提供转换红黑树的一些条件和锁的控制

3.TreeMap

  • TreeMap存储键值对,通过红黑树实现,线程不安全
  • TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法
  • TreeMap有序,通过Key值的自然排序

七、树

1.二叉树

  • 比链表查询效率高,一个父节点至多有两个子节点,只有一个根节点
  • 数据量多时,树高度过高,遍历时多次进行I/O操作,读写磁盘效率低,时间长
  • 根据插入数据的大小生成树,最坏情况下结构可能无限趋近链表

2.多叉树

  • 一个父节点可以有多个子节点,减小树高,减少与磁盘的I/O交互,从而提高效率

3.B树

  • 可以看做是对2-3叉树的优化,允许一个节点有多于2个的元素,进一步减少树高

4.B+树

  • 非叶结点不存放具体元素,仅具有索引作用,与记录有关的信息均存放在叶结点中
  • 树的所有叶结点构成一个有序链表,可以按照排序的次序遍历全部元素
  • 在内存中能够存放更多的key,访问叶子节点上关联的数据可以更好的命中缓存
  • B+树遍历整个树和范围查找效率高于B树,但由于总是在叶子节点命中数据,在单一查找时效率有可能不如B树,但性能稳定,B树如果能在根节点附近命中,则效率优于B+
  • B树和B+树也常用于数据库的索引

5.B*树

  • B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针
  • B+树当一个结点满时,会分裂出一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针。B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟节点的指针
  • B*树当一个结点满时,如果它的下一个相邻结点未满,就将一部分数据移到相邻结点中,再在原结点插入关键字,最后修改父结点中相邻结点的关键字范围。如果相邻节点也满了,则在原结点与相邻结点之间分裂出一个新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。
  • B*树分配新结点的概率比B+树要低,空间使用率更高

6.平衡二叉树

  • 平衡二叉树又叫平衡二叉查找树,特点是它左右两个子树的高度差不超过1,并且左右两个子树都是一棵平衡二叉树,可以保证高效率的查询
  • 当新增或删除节点后,导致树高度差超过1而失衡,会进行左旋或右旋,保证最终结果依然是平衡二叉树
  • 遍历方式:前序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)

7.红黑树

  • 具有二叉树的所有特性,是一个自平衡二叉树
  • 根节点是黑色
  • 每个叶子节点都是黑色
  • 如果一个节点是红色的,则它的子节点必须是黑色的
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
  • 从根节点遍历的最长路径不会超过最短路径的2倍,从而保证最坏情况下也是高效的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值