2020Java开发工程师面试知识储备(一)JVM+数据结构+算法

功利性得去找了下面试题库,发现一个讲面试题的教程,觉得讲得不错,教程地址
在这里总结一下学到的知识,打一下Java基础,算是挖一个新坑。
代码地址:源码地址
发现了个刷题的网站,题量大,有解析,暂未发现收费现象,社区氛围也不错,地址:网站地址
正在根据错题内容,逐渐重构完善本系列文章结构。

第一章 JVM

1. JVM结构

JVM是Java Virtual Machine(Java虚拟机)的缩写,跨平台。

执行程序的过程:①加载.class文件②管理并分配内存③执行垃圾收集
三大任务:加载代码(类加载器),校验代码(字节码校验器),执行代码(解释器)
JVM 将内存区域划分为 Method Area(Non-Heap)(方法区) ,Heap(堆) , Program Counter Register(程序计数器) , VM Stack(虚拟机栈,也有翻译成JAVA 方法栈的),Native Method Stack ( 本地方法栈 ),其中Method AreaHeap 是线程共享的 ,VM Stack,Native Method StackProgram Counter Register 是非线程共享的。
堆溢出:短时间内创建大量对象
栈溢出:调用一个没有退出条件的递归方法
方法区溢出:创建大量动态代理(生成大量Class并加载)

off-heap叫做堆外内存,堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。不属于老年代和新生代。不受垃圾收集(JVM GC)

1)程序计数器

几乎不占有内存。用于取下一条执行的指令。

2)堆

所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成。
新生代:新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例旧生代。用于存放新生代中经过多次垃圾回收仍然存活的对象。
类加载问题:
①类中的static代码块只在第一次初始化的时候加载一次
用ClassLoader加载类,是不会导致类的初始化(也就是说不会执行方法).Class.forName(…)加载类,不但会将类加载,还会执行会执行类的初始化方法.
②子类访问父类的static final常量成员是不会触发类的初始化的,如果static final后面的常量是new赋值的,则和普通情况一样,会初始化父类,执行父类的static代码块。
③new一个类时先执行普通代码块再执行构造方法

2. 垃圾回收机制

1.jvm gc判断对象是根据可达性分析方法判断的.
2.GC ROOT 可达对象.
①.虚拟机栈中引用的对象.
②.方法区静态属性,常量引用的对象.
③.JNI引用的对象.
3.对象赋值为null,是将栈帧局部变量的slot记录除掉.,即虚拟机栈中引用的对象.
4.gc是在全局safepoint或安全区域,对所有线程设置中断位主动中断线程,并对垃圾对象进行回收.
5.终结器中对象可以进行自救.

第二章 数据结构

1. ArrayList、Vector、LinkedList

相同:ArrayList,LinkedList,Vestor这三个类都实现了java.util.List接口,同在java.util包下,他们都允许null值。
区别:ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要设计到数组元素移动等内存操作所以索引数据快插入数据慢
Vector由于使用了synchronized方法,是线程安全的,所以性能上比ArrayList要差。 LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快! ArrayList和LinkedList都不是线程安全的。

2. HashMap和Hashtable

2.1 HashMap和Hashtable的区别

1)Hashtable继承自Dictionary,HashMap继承自AbstractMap,二者都实现了Map接口。
2)Hashtable不允许null key和null value,HashMap允许。
3)HashMap的使用几乎和Hashtable相同,不过Hashtable是synchronize同步的。

2.2 HashMap的put方法

java1.7版本:数据结构是链表+数组。在新元素插入,触发Map扩容的时候,会涉及到计算新元素在新Map中的位置,需要用到头插法(线程不安全)。多线程下可能会造成环形引用,进入死循环。
java1.8版本:数据结构是链表+数组+红黑树。
当一个链表的长度大于8的时候,put会进行红黑树的操作(如果总容量小于64先扩容)
当红黑树中元素个数小于6的时候,再从红黑树转回链表。
多线程进行put操作的时候,可能会出现数据被覆盖的情况。

3. B树

应用场景:在数据库用作索引。
B-树:m阶表示一个节点上最多有m-1个关键字。
每个节点上的关键字都顺序排列,当关键字个数多于m时,取中间的关键字作为根,分裂出去,中间的关键字合并到上层节点上。查找时间复杂度O(logn)

第三章 算法(一)

1. 大整数相关

1.1 大整数相加

①将两个字符串转为两个char[]数组,toCharArray()方法。
②定义sums变量,int[]类型的数组,用第①步得到的长度较长的数组,逆序存入sums里面,sums[i]=larger[larger.length-i-1]-'0';
③较小的char数组与sums数组逆序相加,存到sums数组中。sums[i]+=smaller[smaller.length-i-1]-'0';
④遍历sums数组(遍历到sums.length-2)把大于9的位进行进位操作

for(int i = 0;i<sums.length-1;i++){
	if(sums[i]>9){
		sums[i+1]+=1;
 		sums[i]-=10;
	}
}

⑤用StringBuilder的append方法,逆序把sums的元素拼成string字符串。判断字符串的第一位是否为0,如果是,则把0剔除,这个字符串既是结果。

1.2 大整数乘法

整体思路:遍历小数的每一位,然后和大数的每一位相乘,把得到的数字加到新建的数组里面。例:小数个位与大数的个位相乘得到结果放到新建的数组multi的0下标下,小数的个位与大数的十位相乘,得到的结果放到multi的1下标下,小数的个位与大数的百位相乘,得到的结果放到multi的2下标下……小数个位处理完了继续小数的十位:小数的十位与大数个位相乘得到的结果,与multi的1下标内容相加后放到multi的1下标里……
multi下标与两个数组下标对应关系:数组长度-下标=multi下标
实例代码:

		for(int j=small.length-1;j>=0;j--){//小数乘大数
            for(int i=large.length-1;i>=0;i--){
                int num1 = small[j]-'0';
                int num2 = large[i]-'0';
                multi[large.length-1-i+small.length-1-j]+=num1*num2;
            }
        }

2. 字符串相关

2.1 求字符串中字符出现次数

整体思路:定义一个Map,key是Character,value是Integer,用于存储每一个字符出现的次数。遍历每一个字符,查询Map中有没有该key对应的value,如果没有则put(key,value)key是字符,value是1,如果有,则把value自增。更新Map的同时将value和事先定义的全局max变量比较,不断更新max的值。
核心代码:

		for (int i = 0;i<str.length();i++){
            char temp = str.charAt(i);
            Integer count = map.get(temp);
            if(count==null){
                count=1;
            }
            else {
                count++;
            }
            map.put(temp,count);
            if(count>max){
                max=count;
                res=temp;
            }
        }
2.2 求字符串中最先重复的字符

Set的add方法,返回Boolean类型的值,如果为true插入成功,原set没有该元素,如果为false插入失败,原set有该元素,遍历字符串,add字符,当add为false时break,当前add的值第一次重复。核心代码:

		for (int i = 0; i < str.length(); i++) {
            if(!set.add(str.charAt(i))){
                System.out.println(str.charAt(i));
                break;
            }
        }
2.3 求字符串中第一个只出现过一次的字符

在上面3的基础上,改装一下:先求出Map,然后再遍历一遍字符串,查询该字符串出现的次数,第一个次数为1的既是结果。
另一种暴力求法:利用String的indexOf和lastIndexOf方法。

    @Test
    public void test4(){
        String str = "azbgccbdae";
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if(str.indexOf(c)==str.lastIndexOf(c)){
                System.out.println(c);
                break;
            }
        }
    }
2.4 给出一个数字串,求数字出现的次数,并且从小到大排序。

应用桶排序,使用数字本身当做数组中的角标。减去’0’即可获得数字的值,新建count数组,将上面得到的数字作为角标,数组元素自增。核心代码:

		for (int i = 0; i < str.length(); i++) {//桶排序
            char c = str.charAt(i);
            count[c-'0']++;
        }
2.5 输入字符串和字节数,截取带汉字的字符串,不能把汉字截断(不够截完整的汉字就不截)

利用String.valueOf(字符).getBytes().length,获取字符的字节数,遍历字符串,一个一个字符截,每截之前判断已经截的字节数+将要截的字节数是否超出范围,没超出更新以截的字符串和字节数,继续循环,超出了break。
核心代码:

		StringBuilder builder = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            int len = String.valueOf(str.charAt(i)).getBytes().length;
            if(sum+len<=count){
                sum+=len;
                builder.append(str.charAt(i));
            }else {
                break;
            }
        }
2.6 截取指定字符串

利用String的substring方法截:首先用indexOf获取目标串的下标,然后str.substring(index,reg.length())截取子串。
利用正则表达式:首先编写正则式,然后Pattern.compile方法编译上面正则式,然后pattern.matcher方法生成Matcher对象,用Matcher对象的find方法判断是否存在符合正则式的部分,Matcher对象的group输出结果(用括号括起正则式的各个部分,可以自定义分组)

2.7 将字符串以某一分隔符隔开,并且倒序拼接各个部分

例:输入:“i love the game”," "
输出game the love i
利用String的split方法,将字符串分成字符数组,并定义变量接收,逆序遍历数组,将字符串拼接在一起,拼的时候每次循环都拼分隔符,最后返回结果将StringBuilder的子串从0到length-分隔符length。

2.8 给出一个字符串,判断该串是否为ipv4地址

首先把字符串用split方法分割,注意:".“需要转译split("\\.")
截成四段,每段进行判断,把字符串转成int类型的数,要在0~255之间,并且字符串若以"0"开头,只能为"0”

2.9 子集和问题

子集和问题的一个实例为<S,c>。其中S={x1,x2,…,xn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,使得S1中所有元素的和为c。
算法:定义一个函数,参数为原集合,目标sum数值,将要执行操作的元素下标。
在函数的前部分设置判断语句,结束继续递归下去的条件:①sum值等于目标sum值②将要操作的元素下标大于原集合的长度③sum值大于目标值
判断语句通过以后执行加法操作,把原集合的将要操作元素加到sum上,然后递归调用函数。调用结束后执行减操作,把刚才加的部分减掉。最后再递归调用函数。

    public static int count = 0;
    public static int sum = 0;
    public static void kSum(List<Integer> numbers, int target, int i) {
        if(sum == target){
            count++;
            return;
        }
        if(i == numbers.size())  return;
        if(sum > target)  return;
        sum += numbers.get(i);
        kSum(numbers, target, i + 1);
        sum -= numbers.get(i);
        kSum(numbers, target, i + 1);
    }

个人总结:写好跳出递归的判断语句,然后先加一次当前值,递归调用一次函数,再减掉当前值,递归调用一次函数(每次递归调用前两个参数不变,第三个参数步长都是1)。

3. 排序相关

3.1 堆排序

堆排序的时间复杂度为O(nlogn);空间复杂度为O(1);是不稳定的排序算法。
排序算法(最大堆):从最后一个有子节点(假设数组大小为i,这个节点的下标是i/2)的节点出发,遍历在这之前(包括这个节点)的所有节点,把遍历的节点(下面写作根节点)和他左右子节点(左子节点2i,右子节点2i+1)比较,如果根节点最大,继续遍历,如果子节点比根节点大,则替换(可能涉及到连锁替换反应)
个人总结:从最后一个树节点开始,往前,不停得更新(树节点-左子-右子)这个结构。。
插入节点:就是先在堆的最后添加一个节点,然后沿着堆树上升。跟最大堆的初始化过程大致相同。
顶节点删除:将堆树的最后的节点提到根结点,然后删除最大值,然后再把新的根节点放到合适的位置。

3.2 直接插入排序

时间复杂度O(n²);空间复杂度O(1);稳定
算法:遍历数组,每次都将遍历到的元素i与前面的元素比较,找到连续的两个元素k,k+1使得i元素大于k元素,i元素小于k元素(一遍找一遍移动数组元素,往后移一个位置便于i的插入)。找到以后把i的元素覆盖到k+1元素上。
个人总结:从左到右遍历把元素插进前面已经拍好序的部分里。

3.3 快速排序

时间复杂度O(nlog₂n);空间复杂度O(n)或者O(log₂n);不稳定
首先确定一个基数(理解为中位数)遍历数组(利用两侧的high!=low作为判断条件遍历),把大于基数的数放到high区,小于基数的数放到low区(high,low初始在数组的两侧,更换后两数的的具体操作以及遍历数组方向变化略)基数放在high和low重合的地方,以基数隔开两个区,再利用同样的方法处理。
个人总结:不断用中位数隔出两个区域,区域越来越小。

3.4 希尔排序(shell)

几个术语:
组:原来是一整个数组,现在按一定规则把数组平均分成几组,规则如下:如果要分三组就取原下标0,3,6一组1,4,7一组2,5,8一组。
增量:分的组数
算法:在各个分组里执行插入排序,逐渐减少增量,直到为1
增量序列的时间复杂度(最坏情形)是O(n^2)最好O(n)平均。。。不稳定

3.5 归并排序

算法:把原数组分为多块,每块元素个数相等(最后一块可能不等),在每一块中执行选择排序,两两合并,逐渐成为一大块。
时间复杂度都是O(nlog2n)空间复杂度是O(n)。

3.6 各排序方法对比

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值