Java数据结构 面试必看点

数据结构**

数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合**。
  通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
2.2.1.1 数据结构的基本功能
  ①、如何插入一条新的数据项
  ②、如何寻找某一特定的数据项
  ③、如何删除某一特定的数据项
  ④、如何迭代的访问各个数据项,以便进行显示或其他操作
2.2.1.2 常用的数据结构

常见的数据结构
1、数组Array
2、栈Stack
3、队列Queue
4、链表LinkedList
5、树Tree
6、哈希表Hash
7、堆Heap
8、图Graph
    
**数组:**采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
**线性链表:**对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
数据结构的物理存储结构只有两种:顺序存储结构
链式存储结构
(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组
2.2.2 算法
算法简单来说就是解决问题的步骤。
  在Java中,算法通常都是由类的方法来实现的。前面的数据结构,比如链表为啥插入、删除快,而查找慢,平衡的二叉树插入、删除、查找都快,这都是实现这些数据结构的算法所造成的。后面我们讲的各种排序实现也是算法范畴的重要领域。
2.2.2.1 算法的五个特征
  ①、有穷性:对于任意一组合法输入值,在执行又穷步骤之后一定能结束,即:算法中的每个步骤都能在有限时间内完成。
  
  ②、确定性:在每种情况下所应执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
  
  ③、可行性:算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现之。
  
  ④、有输入:作为算法加工对象的量值,通常体现在算法当中的一组变量。有些输入量需要在算法执行的过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中。
  
⑤、有输出:它是一组与“输入”有确定关系的量值,是算法进行信息加工后得到的结果,这种确定关系即为算法功能。
2.2.2.2 算法的设计原则
  **①、正确性:**首先,算法应当满足以特定的“规则说明”方式给出的需求。其次,对算法是否“正确”的理解可以有以下四个层次:
 一、程序语法错误。
 二、程序对于几组输入数据能够得出满足需要的结果。
 三、程序对于精心选择的、典型、苛刻切带有刁难性的几组输入数据能够得出满足要求的结果。
 四、程序对于一切合法的输入数据都能得到满足要求的结果。
  PS:通常以第 三 层意义的正确性作为衡量一个算法是否合格的标准。
  **②、可读性:**算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解;另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试。
  **③、健壮性:**当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且,处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理。
  **④、高效率与低存储量需求:**通常算法效率值得是算法执行时间;存储量是指算法执行过程中所需要的最大存储空间,两者都与问题的规模有关。
  前面三点 正确性,可读性和健壮性相信都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说“A算法比B算法快两倍”之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。所以描述算法的速度必须要和数据项的个数联系起来。也就是“大O”表示法,它是一种算法复杂度的相对表示方式,这里我简单介绍一下,后面会根据具体的算法来描述。
一个算法的效率越高越好,而存储量是越低越好。
2.2.2.3 时间复杂度的表示:o(1)、o(n)、 o(logn)、o(nlogn)
在描述算法复杂度时,经常用到O(1)、O(n)、O(logn)、O(nlogn)来表示对应算法的时间复杂度,O后面的括号中有一个函数,指明某个算法的耗时/耗空间与数据增长量之间的关系。其中的n代表输入数据的量。
**O(n):**时间复杂度为O(n),代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。再比如时间复杂度O(n2),就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n2)的算法,对n个数排序,需要扫描n×n次。
O(logn):当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。
**O(nlogn):**同理,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。
**O(1):**就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(哈希冲突不考虑)
2.2.3 JAVA常用数据结构
java.util包中三个重要的接口及特点:List(列表)、Set(保证集合中元素唯一)、Map(维护多个key-value键值对,保证key唯一)。其不同子类的实现各有差异,如是否同步(线程安全)、是否有序。

在这里插入图片描述
a) 集合用来解决数组长度不可变的问题;
b) Java中集合主要分为两种:单列的Collection和双列基于key-value的Map;
c) Collection有两个子接口:List和Set。其中List是有序可重复的集合;Set是无序不可重复的接口;
d) List常用的实现有ArrayList、LinkedList、Vector;
e) Set常用实现有HashSet、TreeSet;
f) Map主要实现有HashMap和HashTable;
2.2.3.1 ArrayList实现原理

ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。
每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。
ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。
当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
HashMap是线程不安全的,所以效率高;HashTable是基于synchronized实现的线程安全的Map,效率较低;ConCurrentHashMap线程安全,但是锁定的只是一部分代码,所以效率比HashTable高。

2.2.3.2 HashMap

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突
,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单说下HashMap的实现原理:
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率

在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)
2.2.3.3 ArrayList/Vector/LinkedList

a) 相同点
这三个类都是List的实现,都是有序可重复的集合
b) 底层实现
ArrayList和Vector是基于数组的,存放在连续的内存块,LinkedList底层基于链表的可以分散存储;
所以ArrayList和Vector查询快(通过索引也就是下标直接访问),增删慢(需要移动后面的元素);而LinkedList查询慢(需要基于指针从头一个一个查找,最坏的情况要遍历整个集合才能找到),增删快(指针移动到指定的位置之后);
c) 线程安全性
ArrayList线程不安全,Vector线程安全(使用synchronized关键字锁定方法),然而,Vector性能较低,所以一般不怎么使用(并没有废弃),建议使用下面的方式代替Vector:
Collections.synchronizedList(List);
LinkedList线程不安全,如果需要使用线程安全的链表List,有两种处理方法:

  1. 调用List list = Collections.synchronizedList(new LinkedList());
  2. 将LinkedList全部换成ConcurrentLinkedQueue
    d) 大小以及增长方式
    ArrayList构造的时候,大小为0,第一次添加元素的时候,容量变成为默认大小10(构造没有指定容量的情况下);当元素个数超过集合容量的时候进行扩容,扩容方式为当前容量的1.5倍。
    Vector构造的时候容量就是10;当元素个数超过集合容量的时候进行扩容,扩容为原来的2倍(如果构造的时候指定了增加元素个数,就按这个进行增长);
    e) 怎么遍历LinkedList
    2.2.3.4 HashMap/HashTable/ LinkedHashMap /ConcurrentHashMap/
    a) 相同点
    这三个类都是都是Map的实现,也就是说,都是双列的集合,都是存放键值对的,底层实现都是基于数组+链表的结构,也就是hash表。
    b) 底层结构
    除了LinkedHashMap之外,都是基于数组+链表的结构,而LinkedHashMap还在此基础上维护了一个双向链表,用来保证元素插入的顺序,所以LinkedHashMap可以保证元素的插入顺序,其他的几个则不可以
    c) null值要求
    HashMap的key和值都可以为null;HashTable的键和值都不能为null;ConcurrentHashMap的键和值都不能为null;
    d) 容量和增长方式
    HashMap : 默认大小16,负载因子0.75,当hash表的容量超过负载因子的时候开始扩容,扩容为原始容量的2倍。
    HashTable:初始容量为11,负载因子为0.75。超过负载因子容量开始扩容,扩容为旧的容量2+1。
    e) 安全性及性能

2.2.3.5 List集合里面的元素怎么排序
如果要对List集合里面的元素排序,有如下两种方式:
a) 修改被排序的类
让定义的类实现Comparable接口,重写compareTo方法;然后调用java.util.Collections.sort(List)方法。这种方法的缺点在于要修改被排序的类;
b) 编写一个排序比较器,Comparator
先如下编写一个排序器:
static class MyComparator implements Comparator{
@Override
public int compare(User o1, User o2) {
return o1.getId() - o2.getId();
}
}
然后调用
Collections.sort(List,Comparator),如:
public static void main(String[] args) {
List uList = new ArrayList<>();
Collections.sort(uList, new MyComparator());

}

2.2.3.6 HashSet底层实现,
HashSet底层其实存放了一个HashMap,往set里面添加的元素放在了HashMap的key里面
为什么Set是无序的集合
以HashSet为例,因为底层实现是HashMap,而HashMap保证不了元素插入的顺序(key通过计算hashcode放在了hash表),所以,HashSet也保证不了元素顺序。
2.2.3.7 String/StringBuffer/StringBuilder
a) 相同点
这三个类都是用来处理字符串的。
b) 是否可变
String是不可变字符串,StringBuffer和StringBuilder是可变字符串。
c) 安全性
StringBuffer是线程安全的,效率较低;StringBuilder是线程不安全的,效率高一些。
d) String是否有length()方法,数组呢?
String有length()方法,数组没有,有lenth属性。
e) new String(“123”)会产生几个对象
1个或者2个,因为new,一定会在堆中开辟空间,如果”123”在字符串常量池已经存在,就不会再字符串常量池中创建对象了,这样就只会有1个;如果串池(字符串常量池)中没有,那么就会在串池中创建一个对象,这样,就有两个对象。
2.2.3.8 List集合里面的元素怎么排序
如果要对List集合里面的元素排序,有如下两种方式:
c) 修改被排序的类
让定义的类实现Comparable接口,重写compareTo方法;然后调用java.util.Collections.sort(List)方法。这种方法的缺点在于要修改被排序的类;
d) 编写一个排序比较器,Comparator
先如下编写一个排序器:
static class MyComparator implements Comparator{
@Override
public int compare(User o1, User o2) {
return o1.getId() - o2.getId();
}
}
然后调用
Collections.sort(List,Comparator),如:
public static void main(String[] args) {
List uList = new ArrayList<>();
Collections.sort(uList, new MyComparator());

}
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值