数据结构与算法+JVM调优和GC常用算法+数据库高级+复杂sql手写

数据结构
  1. 双向链表
  2. 二叉排序树
  3. 红黑树
  4. 散列表
双向链表:
  • 什么是双向链表

    双向链表是一种数据结构,是由若干个节点构成,每个节点由三部分构成, 分别是前驱节点,元素,后继节点,且双向链表中的节点在内存中是游离状态存在的。

![在这里插入图片描述](https://img-blog.csdnimg.cn/e3deda4d7f3c4759a9e1ff78ab074e86.png

  • 应用:LinkedList

在这里插入图片描述

双向链表的术语:
头节点:第一个节点
尾节点:最后一个节点

操作双向链表
  • 添加元素 - 尾部添加元素 --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的值
    修改元素完成,会将节点中的原元素对象返回

双向链表注意事项:(常见面试题)
  1. LinkedList的数据结构在JDK1.8前后是不同的

    1. 从JDK1.8开始,其数据结构为双向链表
    2. 在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得效果

  1. 常见面试题:

    ArrayList和LinkedList有什么区别

    数据结构不同:ArrayList底层采用数组实现;LinkedList底层采用双向链表实现
    导致性能不同:
    查询元素:
    ArrayList查询性能很高,
    LinkedList中若查询的元素位于头部或尾部部分,查询性能也挺高,如果查询的元素偏中间部分,则查询性能低下(查询元素越在中间位置,性能越低)
    增删元素:
    ArrayList若在尾部增删元素,性能可能会很高,在头部或中间增删元素(因为后续所有元素都需要移动)性能比较低下
    LinkedList:若在头尾部进行增删元素,性能很高;但是如果在中间位置进行增删元素,性能同样不高

    结论:若在首尾进行增删元素,LinkedList性能高于ArrayList;否则在其他位置进行增删元素,ArrayList和LinkedList的性能是差不多的。查询元素Arraylist综合性能也是更好

    结论:综合考虑,大部分情况选择ArrayList性能都是比较高的,若明确是在首尾增删,则选择LinkedList

递归-recursion
  • 什么是递归

    递归是一种思想,应用在编程中体现为方法调用方法本身。

  • 案例:要求用户输入一个正整数,写一段程序求出该整数的阶乘(求阶乘)

    阶乘:某个数的阶乘=从这个数开始,依次乘以前一个数,直到乘以1为止
    1!=1
    2!=21 21!
    3!=321 32!
    4!=4
    321 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);
    }

  • 递归注意事项:

    1. 递归一定要有出口,否则会造成栈内存溢出 – StackOverflowError(SOF)
    2. 递归是有深度的,使用递归时,必须考虑到深度问题,若深度太深,可能会造成栈内存溢出。
  • 栈内存和递归使用的关系:

    栈内存会存在若干个栈帧区域(栈帧:每调用一个方法,在栈中会为该方法开辟一块栈帧区域,用于保存这个方法内存产生的所有局部遍历,方法调用结束,对应的栈帧区域随之销毁),若递归没有出口,则在栈中会一直开辟栈帧区域,而栈内存大小是有限则,则最终一定会出现栈内存溢出(SOF)
    在这里插入图片描述

  • 内存溢出问题:

    栈内存溢出:StackOverFlowError - SOF 在递归中可能会出现
    堆内存溢出:OutOfMemoryError - OOM
    造成OOM的原因:
    1. 创建的对象太大了,堆内存中的剩余内存不足以分配给请求的资源
    2. 内存泄漏的不断累积,最终也会造成堆内存溢出。

    内存泄漏:堆内存中分配出去的内存回收不回来,无法复用,这个现象叫做内存泄漏
    内存溢出:剩余内存不足以分配给请求的资源,则会造成内存溢出。

    内存泄漏和内存溢出的关系:
    内存泄漏累积到一定程序才会造成内存溢出,并不是内存泄漏一旦出现,则立即出现内存溢出
    出现内存溢出并不一定是由于内存泄漏造成的,还可能是因为创建了大对象造成的。

树:tree
  • 定义:

    树是指由若干个节点构成,其中有且仅有一个根节点。(每个节点中都会保存元素)
    在这里插入图片描述

二叉树
  • 定义

    度为2的树称为二叉树

在这里插入图片描述

二叉排序树
  • 定义

    二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。

  • 保存节点规则:

    1. 元素不能重复
    2. 左子树的值一定小于根节点
    3. 右子树的值一定大于根节点

在这里插入图片描述

  • 数据结构可视化网站: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方法来获取比较结果

  • 手写二叉排序树

    1. 定义二叉排序树类,通过泛型由用户来指定元素类型,
      注意: 泛型中的类型T一定是引用类型
    2. 二叉排序树中保存的元素所属类必须实现Comparable接口
    3. 定义内部类Node表示二叉排序树中的节点类
  • 操作二叉排序树

    • 添加节点

    在这里插入图片描述

    • 遍历二叉排序树

      遍历方式都有三种:

      1. 先序遍历:根左右 遍历结果:元素乱序
      2. 中序遍历:左根右 遍历结果:升序排列
      3. 后续遍历:左右根 遍历结果:元素乱序
    • 重写toString方法,将中序遍历的结果返回

      若树中为空,输出引用,结果为“[]”;
      若树中有元素,输出引用,结果为[12,23,45,90],[]中的内容为中序遍历的结果

      思路:
      1. 判断root是否为null,为null,返回[]
      2. 否则,定义StringBuilder,初始值为[
      2. 从root开始,调用中序遍历,将遍历到的元素拼接到builder中,且拼接上,
      3. 遍历完成,需要将最后的,替换成],并将builder转换成String类型返回

      中序遍历代码:
      注意点:中序遍历实际上是对某个目标节点遍历

    • 根据元素查询节点

      1. 判断root是否为null
        -为null,说明目标元素不存在,返回null
        -不为null,从root开始判断是否为要找的目标节点
        -判断目标节点的元素是否与查找的元素相等
        - 若相等,返回当前节点
        - 查找的元素<目标节点元素
        - 目标节点的left上继续查找
        - 判断left是否为null
        - 为null,查找元素不存在,返回null
        - 不为null,继续判断left是否为目标节点

         	- 查找元素>目标节点元素
         		同理
        
二叉排序树操作
  • 删除节点

    1. 删除叶子节点:这种删除是最简单的删除,其操作为:

      1. 查询到元素对应的节点
      2. 让目标节点的上级节点指向它的引用置为null
    2. 删除只有一颗子树的节点:
      让删除的目标节点的父节点指向其孙子节点

    3. 删除有两颗子树的节点:
      让删除节点的前驱节点/后继节点来替换掉目标节点即可。

      前驱/后继节点是指中序遍历后,目标节点的前一个节点称为前驱节点;目标节点的后一个节点称为后继节点
      前驱/后继节点不一定是叶子节点

在这里插入图片描述

  • 极端情况下二叉排序树会发生什么?

    极端情况下,会产生失衡二叉树
    失衡二叉树不具备二叉排序树的优点(查询效率高)
    所以不应该让失衡二叉树存在。

在这里插入图片描述

  • 对失衡二叉树的优化

    数据结构在设计时就考虑到失衡二叉树不应该存在的问题,所以数据结构中会对失衡二叉树进行了优化,不让失衡二叉树存在。
    如何优化失衡二叉树?
    对失衡二叉树的优化产生了另外两种新的数据结构,分别为
    - AVL树(平衡二叉树)
    - 红黑树

  • AVL树(平衡二叉树)

    • 定义

    是绝对平衡的二叉排序树,绝对平衡是指左右子树的高度差<=1

    • AVL树是如何保证绝对平衡状态的

      通过旋转(左旋或右旋),AVL树中整个树,包括每颗子树都是绝对平衡的(左右子树高度差<=1)

红黑树
  • 定义

    红黑树是实现了自平衡的二叉排序树,红黑树达到的相对平衡状态,而不是绝对平衡状态。相对平衡是指左右子树的高度差可以大于1(可能2,3,4),且红黑树中所有的节点颜色要么是黑色要么是红色。

  • 红黑树中节点的颜色

    红黑树中有2类节点的颜色是确定的
    1. 根节点一定黑色的
    2. 新添加的节点一定是红色的

    在这里插入图片描述

  • 红黑树是如何保证树达到相对平衡状态的

    红黑树是通过旋转(左旋,右旋)和改变节点的颜色来保持树的平衡的
    红黑树满足以下5大原则,则认为红黑树达到了相对平衡的状态

    1. 节点是红色或黑色
    2. 根节点是黑色的
    3. 所有叶子节点是黑色(叶子节点是NIL节点)
    4. 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点) --红色节点不能连续出现
    5. 从任意一个节点到其每个叶子所经历的简单路径上包含的黑色节点的个数相同(简称黑高相同)
  • 面试问到红黑树描述答案:

    红黑树是实现了自平衡的二叉排序树,通过旋转和改变节点颜色保持达到相对平衡状态。旋转和改变节点颜色遵循5大原则:12345.。。。

  • 红黑树的应用类:

    1. TreeMap:其实就是红黑树
      – 通过Debug的方式观察数据结构即可
      – 输出引用观察结果
      TreeMap中的toString方法调用了中序遍历,输出结果为根据key升序排列的结果

    2. TreeSet:底层调用TreeMap,实质上是占用了TreeMap中key的那一列

      • 输出引用,得到的是升序排列的结果
      • TreeSet和TreeMap之间的关系:
        TreeSet底层调用TreeMap
    3. 所有的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) 并发包

    区别:

    1. HashMap是非线程安全的散列表,HashTable和ConcurrentHashMap都是线程安全的散列表,其中HashMap的key和value均允许为null;而HashTable和ConcurrentHashMap的key和value均不允许为null

    2. HashTable和ConcurrentHashMap的区别为保证线程安全的方式是不同。

      • HashTable是通过给整张散列表加锁来保证线程安全的,这种方式可以保证线程安全,但是并发执行效率非常低下。
      • ConcurrentHashMap是如何保证线程安全的呢?
        • JDK1.8之前,是通过分段锁机制来保证线程安全的
        • 从JDK1.8开始,数据结构发生了,保证线程安全的方式也发生了改变,变为了通过乐观锁+Synchronized来保证线程安全

在这里插入图片描述

  • JDK1.8之前ConcurrentHashMap保证线程安全的方式

    通过分段锁机制来保证线程安全
    ConcurrentHashMap在JDK1.8之前数据结构与HashMap是不一样。

在这里插入图片描述#### ConcurrentHashMap数据结构

  • 保证线程安全的方式

    JDK1.8之前,是通过分段锁机制来保证线程安全的
    内部是一个长度为16的segment数组,每个segment数组中有一张散列表,当线程向某个segment进行update操作,会立即给segment加锁,从而保证线程安全。

    这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率。

  • 1.8中ConcruentHashMap是如何保证线程安全的

    数据结构:从JDK1.8开始,其数据结构发生了改变,和1.8中的HashMap保持一致,为数组+链表+红黑树

    保证线程安全的操作:是通过乐观锁+Synchronized配合作用来保证线程安全的。

  • 乐观锁和悲观锁

    乐观锁和悲观锁是两种思想,并不是真的锁,都是用于解决线程安全问题的。

    悲观锁保证线程安全的方式:选择给目标对象加锁
    乐观锁保证线程安全的方式:不给目标对象加锁,而是通过版本号机制来保证线程安全

  • 悲观锁:

    当多线程并发执行时,线程总是悲观的认为在自己执行期间一定有其他线程与之并发执行,会产生线程安全问题,所以为了保证线程安全,当线程访问目标资源时,选择立即给资源加锁,从而保证线程安全。

    synchronized修饰同步资源(方法/代码块),会给对象加锁
    synchronized是悲观锁的应用

在这里插入图片描述

  • 乐观锁

    乐观锁是指多线程并发执行时,线程总是乐观的认为,在自己执行期间,不会由其他线程与之并发执行,所以不会给目标对象加锁,但是实际上确实有可能有线程与之并发,产生线程安全问题,所以为了保证线程安全,采用版本号机制来保证线程安全。

    • 乐观锁最终可以保证线程安全,但是不会采取给对象加锁的方式来保证线程安全,而是采用版本号机制。

在这里插入图片描述

  • JDK1.8中ConcurrentHashMap的数据结构以及保证线程安全的方式

在这里插入图片描述

  • HashMap,HashTable和ConcurrentHashMap的区别

    1. HashMap是非线程安全的散列表,而另外两种都是线程安全的散列表,HashMap的key和value均可以为null,而另外两种的key和value均不可以为null
    2. HashTable和ConcurrentHashMap的区别为保证线程安全的方式不同:
      HashTable是通过给整张散列表加锁的方式来保证线程安全的,这种方式并发执行效率非常低下
      ConcurrentHashMap从JDK1.5版本开始出现的
      在JDK1.8之前是通过分段锁机制来保证线程安全的。这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,线程是完全并发的)
      从JDK1.8开始其数据结构发生了改变,和JDK1.8中的HashMap的数据结构保持一致,保证线程安全的方式为乐观锁+synchroinized,当多线程并发向同一个散列桶添加元素时,若散列桶为空,则触发乐观锁机制,若不为空,则选择给桶中的头节点/根节点加锁来保证线程安全
阻塞队列
  • 定义

    是指具有阻塞效果的队列,默认情况下,会从队尾入队操作,从队首进行出队操作

在这里插入图片描述

  • 阻塞队列的作用

    阻塞队列能够实现生产者和消费者的分离解耦,从而提高二者各自的执行效率

在这里插入图片描述

  • 阻塞队列应用2:记录日志

在这里插入图片描述

  • BlockingQueue(接口) – 阻塞队列

    1. 创建阻塞队列对象
      new LinkedBlockingQueue<>(int capacity)
      new LinkedBlockingQueue<>();
    2. 入队
      put(E):void 具有阻塞效果
    3. 出队
      take():E 具有阻塞效果
    • 注意:BlockingQueue是处于JUC(java.util.concurrent)包下的,是线程安全的
  • 题目:

    • 模拟生产者线程和消费者线程并发执行,生产者阻塞的情况

      注意:上述的生产者会一直不停的产生数据向队列中保存,消费者会一直从队列中取数据
          
      思路:让生产者和消费者的速度不一致,让生产者产生数据的速度>消费者取数据的速度,则会出现生产者阻塞
          
      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){
             
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值