什么是双向链表
双向链表是一种数据结构,是由若干个节点构成,每个节点由三部分构成, 分别是前驱节点,元素,后继节点,且双向链表中的节点在内存中是游离状态存在的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/e3deda4d7f3c4759a9e1ff78ab074e86.png
双向链表的术语:
头节点:第一个节点
尾节点:最后一个节点
添加元素 - 尾部添加元素 --add(E)
插入元素 – add(int index,E ele)
其实就是断开链,重新形成链的过程
问题:LinkedList中index是下标吗?
双向链表中没有下标,在LinkedList中涉及到的所有的index都表示从头节点开始的第几个元素,注意index是从0开始的。
查找元素 – get(int index)
双向链表查找元素方式:对半查找
判断查找的index与链表长度一半的关系
若小于,则从头节点开始顺序查找
否则,从尾节点开始逆序查找
这样做,查询效率更高,同时注意,这里明确了index并不是下标,而是表示从头节点或尾节点开始顺序/逆序迭代多少个元素。在双向链表中,节点并没有index属性。
如果查找的元素位于双向链表的头部或尾部,则查询效率很高;但是如果查询的元素位于链表的中间部分,查询效率是地下的。
删除元素 – remove(int index):E
同样是断开链,重新形成链的过程,注意,删除节点后,会将删除节点的元素对象返回
修改元素 – set(int index,E newEle):E
原理:修改节点中元素部分引用item的值
修改元素完成,会将节点中的原元素对象返回
LinkedList的数据结构在JDK1.8前后是不同的
关于双向循环链表:首尾相连的双向链表叫做双向循环链表。
双向循环链表查询操作:
和双向链表一样,遵循对半查找,即判断index与链表查询一半的大小关系:
小于size>>1,从header开始顺序查找
否则,从header开始逆序查找
位移运算:
带符号右移 表示除法运算 >>1 表示右移一位,除以2得效果
1 1 1 1 = 18+14+12+11=15
8 4 2 1 – 权重
1111 对应得十进制是多少,计算方式是:各个位上得值*权重之和即为对应得十进制结果
是除以得效果原因: 右移后,原来得某个二进制对应得权重会降低2n倍,所以整个数也是一个除以2n得效果
eg: 101100
8421
<< 带符号左移,是乘以2^n得效果
常见面试题:
ArrayList和LinkedList有什么区别
数据结构不同:ArrayList底层采用数组实现;LinkedList底层采用双向链表实现
导致性能不同:
查询元素:
ArrayList查询性能很高,
LinkedList中若查询的元素位于头部或尾部部分,查询性能也挺高,如果查询的元素偏中间部分,则查询性能低下(查询元素越在中间位置,性能越低)
增删元素:
ArrayList若在尾部增删元素,性能可能会很高,在头部或中间增删元素(因为后续所有元素都需要移动)性能比较低下
LinkedList:若在头尾部进行增删元素,性能很高;但是如果在中间位置进行增删元素,性能同样不高
结论:若在首尾进行增删元素,LinkedList性能高于ArrayList;否则在其他位置进行增删元素,ArrayList和LinkedList的性能是差不多的。查询元素Arraylist综合性能也是更好
结论:综合考虑,大部分情况选择ArrayList性能都是比较高的,若明确是在首尾增删,则选择LinkedList
什么是递归
递归是一种思想,应用在编程中体现为方法调用方法本身。
案例:要求用户输入一个正整数,写一段程序求出该整数的阶乘(求阶乘)
阶乘:某个数的阶乘=从这个数开始,依次乘以前一个数,直到乘以1为止
1!=1
2!=21 21!
3!=321 32!
4!=4321 43!
n! =n(n-1)*(n-2)…1 n(n-1)!
解法:
//该方法用于求出给出整数n的阶乘
public int f(int n){
if(n==1)
return 1;
return n*f(n-1);
}
递归的经典案例2:斐波那契数列
斐波那契数列是指这样一组数列
1 1 2 3 5 8 13 21 34 55 89.。。。。
规律为:第一位第二位固定为1,从第三位开始,其值为前两位之和,求出第13位的值是多少
思路:定义一个方法f(int pos),用于求出给定位置pos上的值,返回值即为对应的值
public int f(int pos){
if(pos1 || pos2)
return 1;
return f(pos-1)+f(pos-2);
}
递归注意事项:
栈内存和递归使用的关系:
栈内存会存在若干个栈帧区域(栈帧:每调用一个方法,在栈中会为该方法开辟一块栈帧区域,用于保存这个方法内存产生的所有局部遍历,方法调用结束,对应的栈帧区域随之销毁),若递归没有出口,则在栈中会一直开辟栈帧区域,而栈内存大小是有限则,则最终一定会出现栈内存溢出(SOF)
内存溢出问题:
栈内存溢出:StackOverFlowError - SOF 在递归中可能会出现
堆内存溢出:OutOfMemoryError - OOM
造成OOM的原因:
1. 创建的对象太大了,堆内存中的剩余内存不足以分配给请求的资源
2. 内存泄漏的不断累积,最终也会造成堆内存溢出。
内存泄漏:堆内存中分配出去的内存回收不回来,无法复用,这个现象叫做内存泄漏
内存溢出:剩余内存不足以分配给请求的资源,则会造成内存溢出。
内存泄漏和内存溢出的关系:
内存泄漏累积到一定程序才会造成内存溢出,并不是内存泄漏一旦出现,则立即出现内存溢出
出现内存溢出并不一定是由于内存泄漏造成的,还可能是因为创建了大对象造成的。
定义
度为2的树称为二叉树
定义
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
保存节点规则:
数据结构可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
查询效率为什么比链表结构要高
二叉排序树每次比较完成,都会排除将近一半的数据,所以查询效率会很高,而单向链表最次的情况,会查询n次。
如何保证二叉排序树中节点的元素是可比较大小的
二叉排序树中节点中保存的元素可以是任意引用类型。但是要求元素之间是必须可比较大小的,如何保证元素是可比较大小的?
让元素所属的类实现Comparable接口,定义比较规则。
即二叉排序树中的元素所属的类必须要求实现Comparable接口才可能。
保存字符串类型的二叉排序树
比较器接口
Comparable接口
内比较器(实现该接口的类在类的内部重写compareTo方法来定义比较规则-比较规则定义在类的内部)
-案例:实现Student对象之间的比较
-练习1:一个集合中保存有若干个Student对象,现要求按照学生的id进行降序排列,请用代码实现。
对集合进行排序:Collections.sort(List)
-练习2:在练习1的基础上(不改变Student原有的排序规则),对list集合中的student重新排序,按照年龄进行升序排列。
Comparator接口 – 外比较器–比较规则是定义在类的外部的
适用场景:
当不想使用原来的排序规则,想使用新的或临时的排序规则时,就可以使用外比较器来实现。
使用:实现Comparator接口,重写其中的抽象方法,在方法中定义新的比较规则接口。后续若对list进行排序,可使用api方法Collections.sort(List,Comparator)
练习3:以上案例中,要求在不改变Student默认排序规则的前提下,对集合中的学生重新排序,按照姓名降序排列。
注意点:若排序规则时根据某引用类型来比较,需要调用该类型的类中的compareTo方法来获取比较结果
手写二叉排序树
操作二叉排序树
遍历二叉排序树
遍历方式都有三种:
重写toString方法,将中序遍历的结果返回
若树中为空,输出引用,结果为“[]”;
若树中有元素,输出引用,结果为[12,23,45,90],[]中的内容为中序遍历的结果
思路:
1. 判断root是否为null,为null,返回[]
2. 否则,定义StringBuilder,初始值为[
2. 从root开始,调用中序遍历,将遍历到的元素拼接到builder中,且拼接上,
3. 遍历完成,需要将最后的,替换成],并将builder转换成String类型返回
中序遍历代码:
注意点:中序遍历实际上是对某个目标节点遍历
根据元素查询节点
判断root是否为null
-为null,说明目标元素不存在,返回null
-不为null,从root开始判断是否为要找的目标节点
-判断目标节点的元素是否与查找的元素相等
- 若相等,返回当前节点
- 查找的元素<目标节点元素
- 目标节点的left上继续查找
- 判断left是否为null
- 为null,查找元素不存在,返回null
- 不为null,继续判断left是否为目标节点
- 查找元素>目标节点元素
同理
删除节点
删除叶子节点:这种删除是最简单的删除,其操作为:
删除只有一颗子树的节点:
让删除的目标节点的父节点指向其孙子节点
删除有两颗子树的节点:
让删除节点的前驱节点/后继节点来替换掉目标节点即可。
前驱/后继节点是指中序遍历后,目标节点的前一个节点称为前驱节点;目标节点的后一个节点称为后继节点
前驱/后继节点不一定是叶子节点
极端情况下二叉排序树会发生什么?
极端情况下,会产生失衡二叉树
失衡二叉树不具备二叉排序树的优点(查询效率高)
所以不应该让失衡二叉树存在。
对失衡二叉树的优化
数据结构在设计时就考虑到失衡二叉树不应该存在的问题,所以数据结构中会对失衡二叉树进行了优化,不让失衡二叉树存在。
如何优化失衡二叉树?
对失衡二叉树的优化产生了另外两种新的数据结构,分别为
- AVL树(平衡二叉树)
- 红黑树
AVL树(平衡二叉树)
是绝对平衡的二叉排序树,绝对平衡是指左右子树的高度差<=1
AVL树是如何保证绝对平衡状态的
通过旋转(左旋或右旋),AVL树中整个树,包括每颗子树都是绝对平衡的(左右子树高度差<=1)
定义
红黑树是实现了自平衡的二叉排序树,红黑树达到的相对平衡状态,而不是绝对平衡状态。相对平衡是指左右子树的高度差可以大于1(可能2,3,4),且红黑树中所有的节点颜色要么是黑色要么是红色。
红黑树中节点的颜色
红黑树中有2类节点的颜色是确定的
1. 根节点一定黑色的
2. 新添加的节点一定是红色的
红黑树是如何保证树达到相对平衡状态的
红黑树是通过旋转(左旋,右旋)和改变节点的颜色来保持树的平衡的
红黑树满足以下5大原则,则认为红黑树达到了相对平衡的状态
面试问到红黑树描述答案:
红黑树是实现了自平衡的二叉排序树,通过旋转和改变节点颜色保持达到相对平衡状态。旋转和改变节点颜色遵循5大原则:12345.。。。
红黑树的应用类:
TreeMap:其实就是红黑树
– 通过Debug的方式观察数据结构即可
– 输出引用观察结果
TreeMap中的toString方法调用了中序遍历,输出结果为根据key升序排列的结果
TreeSet:底层调用TreeMap,实质上是占用了TreeMap中key的那一列
所有的set和map之间的关系:
HashMap – HashSet
TreeMap – TreeSet
LinkedHashMap – LinkedHashSet
注意点:set集合是不可重复集,不能说是无序的,因为有一些set的实现类是有序的,有一些是实现了排序的
HashSet – 无序的
TreeSet – 实现了排序效果的(升序/降序)
LinkedHashSet – 有序的
什么是散列表
散列表的底层其实是一个数组,这个数组叫做散列表,其核心原理为尽可能将元素分散开分布到数组的不同位置去。
在JDK1.8之前,散列表的底层为数组+链表结构
从JDK1.8开始,底层为数组+链表+红黑树
应用类:
HashMap – 就是散列表
hash 音译成哈希 翻译后:散列
相关面试题:描述HashMap,HashTable,ConcurrentHashMap的区别
HashTable是从JDK1.0开始的,是线程安全的
HashMap是从JDK1.2开始的,是非线程安全的
ConcurrentHashMap是JUC包下的,是线程安全的散列表,从JDK1.5版本开始的。
JUC(java.util.concurrent) 并发包
区别:
HashMap是非线程安全的散列表,HashTable和ConcurrentHashMap都是线程安全的散列表,其中HashMap的key和value均允许为null;而HashTable和ConcurrentHashMap的key和value均不允许为null
HashTable和ConcurrentHashMap的区别为保证线程安全的方式是不同。
JDK1.8之前ConcurrentHashMap保证线程安全的方式
通过分段锁机制来保证线程安全
ConcurrentHashMap在JDK1.8之前数据结构与HashMap是不一样。
保证线程安全的方式
JDK1.8之前,是通过分段锁机制来保证线程安全的
内部是一个长度为16的segment数组,每个segment数组中有一张散列表,当线程向某个segment进行update操作,会立即给segment加锁,从而保证线程安全。
这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率。
1.8中ConcruentHashMap是如何保证线程安全的
数据结构:从JDK1.8开始,其数据结构发生了改变,和1.8中的HashMap保持一致,为数组+链表+红黑树
保证线程安全的操作:是通过乐观锁+Synchronized配合作用来保证线程安全的。
乐观锁和悲观锁
乐观锁和悲观锁是两种思想,并不是真的锁,都是用于解决线程安全问题的。
悲观锁保证线程安全的方式:选择给目标对象加锁
乐观锁保证线程安全的方式:不给目标对象加锁,而是通过版本号机制来保证线程安全
悲观锁:
当多线程并发执行时,线程总是悲观的认为在自己执行期间一定有其他线程与之并发执行,会产生线程安全问题,所以为了保证线程安全,当线程访问目标资源时,选择立即给资源加锁,从而保证线程安全。
synchronized修饰同步资源(方法/代码块),会给对象加锁
synchronized是悲观锁的应用
乐观锁
乐观锁是指多线程并发执行时,线程总是乐观的认为,在自己执行期间,不会由其他线程与之并发执行,所以不会给目标对象加锁,但是实际上确实有可能有线程与之并发,产生线程安全问题,所以为了保证线程安全,采用版本号机制来保证线程安全。
HashMap,HashTable和ConcurrentHashMap的区别
定义
是指具有阻塞效果的队列,默认情况下,会从队尾入队操作,从队首进行出队操作
阻塞队列的作用
阻塞队列能够实现生产者和消费者的分离解耦,从而提高二者各自的执行效率
BlockingQueue(接口) – 阻塞队列
题目:
模拟生产者线程和消费者线程并发执行,生产者阻塞的情况
注意:上述的生产者会一直不停的产生数据向队列中保存,消费者会一直从队列中取数据
思路:让生产者和消费者的速度不一致,让生产者产生数据的速度>消费者取数据的速度,则会出现生产者阻塞
public class ProductorBlockingDemo {
public static void main(String[] args) {
//创建阻塞队列
BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);
//创建生产者线程
Thread productor = new Thread(){
@Override
public void run() {
while (true){
try {
queue.put("hello"+(int)(Math.random()*1000));
System.out.println(queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建消费者线程
Thread consumer = new Thread(){
@Override
public void run() {
while (true){