2021 字节跳动面试参考手册(第一期)
一、Java 基础面试题
1.String 能被继承吗?为什么?
不可以,因为 String 类有 final 修饰符,而 final 修饰的类是不能被继承的,实现细节不允许改变。平常我们定义的 String str=”abc”(直接赋一个字面量);其实和 String str=new
String(“abc”)(通过构造器构造)还是有差异的。
2.String, Stringbuffer, StringBuilder 的区别。
String 字符串常量(final 修饰,不可被继承),String 是常量,当创建之后即不能更改。(可以通过 StringBuffer 和 StringBuilder 创建 String 对象(常用的两个字符串操作类)。)
==StringBuffer 字符串变量(线程安全),==其也是 final 类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的 Append 方法也做了同步处理(synchronized 修饰)。其自 jdk1.0 起就已经出现。其 toString 方法会进行对象缓存,以减少元素复制开销。
==StringBuilder 字符串变量(非线程安全)==其自 jdk1.5 起开始出现。与 StringBuffer 一样都继承和实现了同样的接口和类,方法除了没使用 synch 修饰以外基本一致,不同之处在于最后 toString 的时候,会直接返回一个新对象。
3.说一说常见的输入输出流
计算机的存储器按用途可以分为主存储器和辅助存储器。
a.主存储器又称内存,是 CPU 能直接寻址的存储空间,它的特点是存取速率快。内存一般采用半导体存储单元,包括随机存储器(RAM)、只读存储器(ROM)和高级缓存(Cache)。
b.辅助存储器又称外存储器(简称外存),就是那些磁盘、硬盘、光盘,也就是你在电脑上看到的C、D、E、F盘。
根据处理数据类型的不同分为:字符流和字节流
字节流和字符流的区别:
读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符, 一次可能读多个字节。
处理对象不同:字节流能处理所有类型的数据(如图片、avi 等),而字符流只能处理字符类型的数据。
字节流:一次读入或读出是 8 位二进制。
字符流:一次读入或读出是 16 位二进制。
设备上的数据无论是图片或者视频,文字,它们都以二进制存储的。二进制的最终都是以一 个 8 位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。
结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
输入流只能进行读操作,输出流只能进行写操作,程序中需要根据待传输数据的不同特性而 使用不同的流。
4.说一说 java 中的文件类 File
在 Java 语言的 java.io 包中,由 File 类提供了描述文件和目录的操作与管理方法。File 类不同于输入输出流,它不负责数据的输入输出,而专门用来管理磁盘文件与目录。
File 类共提供了三个不同的构造函数,以不同的参数形式灵活地接收文件和目录名信息。构造函数:
1)File (String pathname)
例 :File f1=new File(“FileTest1.txt”); //创建文件对象 f1,f1 所指的文件是在当前目录下创建的 FileTest1.txt
2)File (String parent , String child)
例:File f2 = new File(“D:\dir1",“FileTest2.txt”) ; // 注意:D:\dir1 目录事先必须存在,否则异常
3)File (File parent , String child)
例 :File f4=new File("\dir3");
File f5=new File(f4,“FileTest5.txt”); // 在如果 \dir3 目录不存在使用
f4.mkdir()先创建
一个对应于某磁盘文件或目录的 File 对象一经创建, 就可以通过调用它的方法来获得文件或目录的属性。
File 类中还定义了一些对文件或目录进行管理、操作的方法,常用的方法有:
5.如何选择 IO 流:
1)确定是 输入还是输出
2)明确操作的数据对象是否是纯文本
3)明确具体的设备
4)是否需要缓冲提高效率
,
6.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
不对,两个对象的 hashCode()相同,equals()不一定 true。代码示例:
执行的结果:
代码解读:很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false, 因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
7.String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
8.BIO、NIO、AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
二、Java 集合框架
- 请先介绍一下 java 结合框架
1、List(有序、可重复)
List 里存放的对象是有序的,同时也是可以重复的,List 关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往 list 集合里插入或删除数据时,会伴随着后面数据的移动, 所有插入删除数据速度慢。
2、Set(无序、不能重复)
Set 里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
3、Map(键值对、键唯一、值不唯一)
Map 集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对 map 集合遍历时先得到键的 set 集合,对 set 集合进行遍历,得到相应的值。
1.Interface Iterable
迭代器接口,这是 Collection 类的父接口。实现这个 Iterable 接口的对象允许使用 foreach 进行遍历,也就是说,所有的 Collection 集合对象都具有"foreach 可遍历性"。这个 Iterable 接口只有一个方法: iterator()。它返回一个代表当前集合对象的泛型迭代器,用于之后的遍历操作
1.1Collection
Collection 是最基本的集合接口,一个 Collection 代表一组 Object 的集合,这些 Object 被称作 Collection 的元素。Collection 是一个接口,用以提供规范定义,不能被实例化使
用
1)Set
Set 集合类似于一个罐子,“丢进"Set 集合里的多个对象之间没有明显的顺序。Set 继承自 Collection 接口,不能包含有重复元素(记住,这是整个 Set 类层次的共有属性)。
Set 判断两个对象相同不是使用”=="运算符,而是根据 equals 方法。也就是说,我们在加入一个新元素的时候,如果这个新元素对象和 Set 中已有对象进行注意 equals 比较都返回 false, 则 Set 就会接受这个新元素对象,否则拒绝。
因为 Set 的这个制约,在使用 Set 集合的时候,应该注意两点:1) 为 Set 集合里的元素的实现类实现一个有效的equals(Object)方法、2) 对Set 的构造函数,传入的 Collection 参数不能包含重复的元素
1.1)HashSet
HashSet 是 Set 接口的典型实现,HashSet 使用 HASH 算法来存储集合中的元素, 因此具有良好的存取和查找性能。当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode()方法来得到该对象的 hashCode 值,然后根据该 HashCode 值决定该对象在 HashSet 中的存储位置。
值得主要的是,HashSet 集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法的返回值相等
1.1.1) LinkedHashSet
LinkedHashSet 集合也是根据元素的 hashCode 值来决定元素的存储位置,但和 HashSet 不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。当遍历 LinkedHashSet 集合里的元素时,LinkedHashSet 将会按元素的添加顺序来访问集合里的元素。
LinkedHashSet 需要维护元素的插入顺序,因此性能略低于 HashSet 的性能, 但在迭代访问 Set 里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)
1.2)SortedSet
此接口主要用于排序操作,即实现此接口的子类都属于排序的子类
1.2.1) TreeSet
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态
1.3)EnumSet
EnumSet 是一个专门为枚举类设计的集合类,EnumSet 中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建 EnumSet 时显式、或隐式地指定。EnumSet 的集合元素也是有序的,它们以枚举值在 Enum 类内的定义顺序来决定集合元素的顺序
2)List
List 集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。
List 集合允许加入重复元素,因为它可以通过索引来访问指定位置的集合元素。List 集合默认按元素 的添加顺序设置元素的索引
2.1)ArrayList
ArrayList 是基于数组实现的 List 类,它封装了一个动态的增长的、允许再分配的
Object[]数组。
2.2)Vector
Vector 和 ArrayList 在用法上几乎完全相同,但由于 Vector 是一个古老的集合,所以 Vector 提供了一些方法名很长的方法,但随着 JDK1.2 以后,java 提供了系统的集合框架,就将 Vector 改为实现 List 接口,统一归入集合框架体系中
2.2.1) Stack
Stack 是 Vector 提供的一个子类,用于模拟"栈"这种数据结构(LIFO 后进先出)
2.3)LinkedList
implements List, Deque。实现 List 接口,能对它进行队列操作,即可以根据索引来随机访问集合中的元素。同时它还实现 Deque 接口,即能将 LinkedList 当作双端队列 使用。自然也可以被当作"栈来使用"
3)Queue
Queue 用于模拟"队列"这种数据结构(先进先出 FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入(offer)到队列的尾部, 访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。结合生活中常见的排队就会很好理解这个概念
3.1)PriorityQueue
PriorityQueue 并不是一个比较标准的队列实现,PriorityQueue 保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序,这点从它的类名也可以 看出来
3.2)Deque
Deque 接口代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此 Deque 的实现类既可以当成队列使用、也可以当成栈使用
3.2.1)ArrayDeque
是一个基于数组的双端队列,和 ArrayList 类似,它们的底层都采用一个动态的、可重分配的 Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层重新分配一个 Object[]数组来存储集合元素
3.2.2)LinkedList
1.2Map
Map 用于保存具有"映射关系"的数据,因此 Map 集合里保存着两组值,一组值用于保存
Map 里的 key,另外一组值用于保存 Map 里的 value。key 和 value 都可以是任何引用类型的数据。Map 的 key 不允许重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较结果总是返回 false。
关于 Map,我们要从代码复用的角度去理解,java 是先实现了 Map,然后通过包装了一个所有 value 都为 null 的 Map 就实现了 Set 集合
Map 的这些实现类和子接口中 key 集的存储形式和 Set 集合完全相同(即 key 不能重复)
Map 的这些实现类和子接口中 value 集的存储形式和 List 非常类似(即 value 可以重复、根据索引来查找)
1)HashMap
和HashSet 集合不能保证元素的顺序一样,HashMap 也不能保证key-value 对的顺序。并且类似于 HashSet 判断两个 key 是否相等的标准也是: 两个 key 通过 equals()方法比较返回 true、 同时两个 key 的 hashCode 值也必须相等
1.1) LinkedHashMap
LinkedHashMap 也使用双向链表来维护 key-value 对的次序,该链表负责维护
Map 的迭代顺序,与 key-value 对的插入顺序一致(注意和 TreeMap 对所有的 key-value 进行排序进行区分)
2)Hashtable
是一个古老的 Map 实现类
2.1) Properties
Properties 对象在处理属性文件时特别方便(windows 平台上的.ini 文件),
Properties 类可以把Map 对象和属性文件关联起来,从而可以把 Map 对象中的key-value 对写入到属性文件中,也可以把属性文件中的"属性名-属性值"加载到 Map 对象中
3)SortedMap
正如 Set 接口派生出 SortedSet 子接口,SortedSet 接口有一个 TreeSet 实现类一样,
Map 接口也派生出一个 SortedMap 子接口,SortedMap 接口也有一个 TreeMap 实现类
3.1) TreeMap
TreeMap 就是一个红黑树数据结构,每个 key-value 对即作为红黑树的一个节点。
TreeMap 存储 key-value 对(节点)时,需要根据 key 对节点进行排序。TreeMap 可以保证所有的 key-value 对处于有序状态。同样,TreeMap 也有两种排序方式: 自然排序、定制排序
4)WeakHashMap
WeakHashMap 与 HashMap 的用法基本相似。区别在于,HashMap 的 key 保留了对实际对象的"强引用",这意味着只要该 HashMap 对象不被销毁,该 HashMap 所引用的对象就不会被垃圾回收。但 WeakHashMap 的 key 只保留了对实际对象的弱引用,这意味着如果 WeakHashMap 对象的 key 所引用的对象没有被其他强引用变量所引用,则这些
key 所引用的对象可能被垃圾回收, 当垃圾回收了该 key 所对应的实际对象之后,
WeakHashMap 也可能自动删除这些 key 所对应的 key-value 对
5)IdentityHashMap
IdentityHashMap 的实现机制与 HashMap 基本相似,在 IdentityHashMap 中,当且仅当两个 key 严格相等(key1 == key2)时,IdentityHashMap 才认为两个 key 相等
6)EnumMap
EnumMap 是一个与枚举类一起使用的 Map 实现,EnumMap 中的所有 key 都必须是
单个枚举类的枚举值。创建 EnumMap 时必须显式或隐式指定它对应的枚举类。EnumMap 根据 key 的自然顺序 (即枚举值在枚举类中的定义顺序)
2Vector 和 ArrayList 的区别
ArrayList 和 Vector 是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作, 所以索引数据快,插入数据慢,Vector 由于使用了 synchronized 方法(线程安全)所以性能上比 ArrayList 要差,LinkedList 使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快。
3arraylist 和 linkedlist 的区别
4HashMap 与 TreeMap 的区别
同样做测试:
在 HashMap 中,同样的值的 map,顺序不同,equals 时,false;
而在 treeMap 中,同样的值的 map,顺序不同,equals 时,true,说明,treeMap 在 equals() 时是整理了顺序了的。
5HashTable 与 HashMap 的区别
6.常用的集合类有哪些?
Map 接口和 Collection 接口是所有集合框架的父接口:
Collection 接口的子接口包括:Set 接口和 List 接口
Map 接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等
Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet 等
List 接口的实现类主要有:ArrayList、LinkedList、Stack 以及 Vector 等
7.List,Set,Map 三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?
Java 容器分为 Collection 和 Map 两大类,Collection 集合的子接口有 Set、List、Queue三种子接口。我们比较常用的是 Set、List,Map 接口不是 collection 的子接口。
Collection 集合主要有 List 和 Set 两大接口
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入 多个 null 元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个 null 元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map 是一个键值对集合,存储键、值和之间的映射。 Key 无序,唯一;value 不要求有序, 允许重复。Map 没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的 常 用 实 现 类 : HashMap 、 TreeMap 、 HashTable 、 LinkedHashMap 、
ConcurrentHashMap
8.集合框架底层数据结构
Collection List
Arraylist: Object 数 组Vector: Object 数 组
LinkedList: 双向循环链表Set
HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素LinkedHashSet : LinkedHashSet 继 承 与 HashSet , 并 且 其 内 部 是 通 过
LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于
Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) Map
HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上, 增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树)
9.哪些集合类是线程安全的?
vector:就比 arraylist 多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在 web 应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck:堆栈类,先进后出。
hashtable:就比 hashmap 多了个线程安全。
enumeration:枚举,相当于迭代器。
10.怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:
11.迭代器 Iterator 是什么?
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
Iterator 使用代码如下:
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
12 说一下 ArrayList 的优缺点
ArrayList 的优点如下:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性 能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
13.如何实现数组和 List 之间的转换?
数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。代码示例:
14.插入数据时,ArrayList、LinkedList、Vector 谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?
ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较 ArrayList 差。
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数
据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
15.多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的, 如果遇到多线程场景, 可以通过 Collections 的
synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
16.HashSet 如何检查重复?HashSet 是如何保证数据不可重复的?
向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较 hash 值,同时还要结合 equles 方法比较。
HashSet 中的 add ()方法会使用 HashMap 的 put()方法。
HashMap 的 key 是唯一的, 由源码可以看出 HashSet 添加进去的值就是作为
HashMap 的 key,并且在 HashMap 中如果 K/V 相同时,会用新的 V 覆盖掉旧的 V,然后返回旧的 V。所以不会重复( HashMap 比较 key 是否相等是先比较 hashcode 再比较equals ) 。
以下是 HashSet 部分源码:
hashCode()与 equals()的相关规定:
如果两个对象相等,则 hashcode 一定也是相同的两个对象相等,对两个 equals 方法返回 true
两个对象有相同的 hashcode 值,它们也不一定是相等的
综上,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与 equals 的区别
==是判断两个变量或实例是不是指向同一个内存空间 equals 是判断两个变量或实例所指向的内存空间的值是不是相同
==是指对内存地址进行比较 equals()是对字符串的内容进行比较 3.==指引用是否相同
equals()指的是值是否相同
17.BlockingQueue 是什么?
Java.util.concurrent.BlockingQueue 是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue 接口是 Java 集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在 BlockingQueue 的实现类中被处理 了 。 Java 提 供 了 集 中 BlockingQueue 的 实 现 , 比 如 ArrayBlockingQueue 、
LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue 等。
18.说一下 HashMap 的实现原理?
HashMap 概述: HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 的数据结构: 在 Java 编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap 也不例外。HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
当我们往 Hashmap 中 put 元素时,利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标
存储时,如果出现 hash 值相同的 key,此时有两种情况。(1)如果 key 相同,则覆盖原始值;
(2)如果 key 不同(出现冲突),则将当前的 key-value 放入链表中
获取时,直接找到 hash 值对应的下标,在进一步判断 key 是否相同,从而找到对应值。理解了以上过程就不难明白 HashMap 是如何解决 hash 冲突的问题,核心就是使用了数组的存储方式,然后将冲突的 key 的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意 Jdk 1.8 中对 HashMap 的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的 O(n)到 O(logn)
19.HashMap 在 JDK1.7 和 JDK1.8 中有哪些不同?
HashMap 的底层实现
在 Java 中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易, 插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
JDK1.8 之前
JDK1.8 之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组, 数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相比于之前的版本,jdk1.8 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.7 VS JDK1.8 比 较
JDK1.8 主要解决或优化了一下问题:
resize 扩容优化
引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
20.HashMap 是怎么解决哈希冲突的?
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们 还要知道什么是哈希才行;
什么是哈希?
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出, 所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同
**。
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞
(哈希碰撞)。
HashMap 的数据结构
在 Java 中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易, 插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:
这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash 值所对应的bucket 下, 但 相 比 于 hashCode 返 回 的 int 类 型 , 我 们 HashMap 初 始 的 容 量 大 小DEFAULT_INITIAL_CAPACITY = 1 << 4(即 2 的四次方 16)要远小于 int 类型的范围, 所以我们如果只是单纯的用 hashCode 取余来获取对应的 bucket 这将会大大增加哈希碰撞的概率, 并且最坏情况下还会将 HashMap 变成一个单链表, 所以我们还需要对
hashCode 作一定的优化
hash()函数
上面提到的问题,主要是因为如果使用 hashCode 取余,那么相当于参与运算的只有
hashCode 的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode 取值出的高位也参与运算,进一步降低 hash 碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在 JDK 1.8 中的 hash()函数如下:
这比在 JDK 1.7 中,更为简洁,相比在 1.7 中的 4 次位运算,5 次异或运算(9 次扰动), 在 1.8 中,只进行了 1 次位运算和 1 次异或运算(2 次扰动);
JDK1.8 新增红
通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的 HashMap 中存在大量数据时,加入我们某个 bucket 下对应的链表有 n 个元素,那么遍历时间复杂度就为 O(n),为了针对这个问题,JDK1.8 在 HashMap 中新增了红黑树的数据结构,进一步使得遍历复杂度降低至 O(logn);
总结
简单总结一下 HashMap 是使用了哪些方法来有效解决哈希冲突的:
21.如果使用 Object 作为 HashMap 的 Key,应该怎么
办呢?
答:重写 hashCode()和 equals()方法
重写 hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的 Hash 碰撞; 重写 equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非 null 的引用值 x,x.equals(null)必须返回 false 的这几个特性,目的是为了保证 key 在哈希表中的唯一性;
22.HashMap 与 HashTable 有什么区别?
线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方 法 基 本 都 经 过 synchronized 修 饰 。 ( 如 果 你 要 保 证 线 程 安 全 的 话 就 使 用
ConcurrentHashMap 吧 !);
效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
对 Null key 和 Null value 的支持: HashMap 中,null 可以作为键,这样的键只有一个, 可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个
null,直接抛 NullPointerException。
**初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,
Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。②创建时如果给定了容量初
始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是
2 的幂次方。
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
23.ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,
JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是
HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配 16 个 Segment, 比 Hashtable 效率提高 16 倍。) 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和
CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图: HashTable:
JDK1.7 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
答:
24.TreeMap 和 TreeSet 在排序时如何比较元素?
Collections 工具类中的 sort()方法如何比较元素?
TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的
compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。
Collections 工具类的 sort 方法有两种重载的形式,
三、Linux 常用指令
1.常见命令
2.常见的操作文件,文件夹的命令
cd — Change Directory : 切换文件路径,cd 将给定的文件夹(或目录)设置成当
前工作目录
rm— Remove : 删除指定的文件。 在 linux 没有回收站,删除之后无法恢复
rm -rf 文件(r 递归删除,f 直接强行删除)
sudo rm -r hadoop # 删除文件夹下的所有文件
vi : 创建一个文件 vi test.txt
一种比较方便打开文件的方式
sudo gedit + 文 件
是覆盖,>> 是追加
echo >: 向已有的文件中增加内容, echo “added contents” >>test.txt
echo >> : 覆 盖 文 件 内 容 , 若 不 存 在 则 创 建 一 个 新 的 文 件 echo "new
file" >newFile.txt
cat— concatenate and print files : 显示一个文件 cat test.txt
less, more : 如果文件太大,分页显示一个文件,按 Q 结束浏 览
3.软件下载安装
4.系统重启和关机指令
5.文件模式和访问权限
第一列的字符可以分为三组,每一组有三个,每个字符都代表不同的权限,分别为读取®、
写入(w)和执行(x):
u 第一组字符(2-4)表示文件所有者的权限,-rwxr-xr-- 表示所有者拥有读取®、写入(w)
和执行(x)的权限。
g 第二组字符(5-7)表示文件所属用户组的权限,-rwxr-xr-- 表示该组拥有读取®和执行(x)
的权限,但没有写入权限。
o 第三组字符(8-10)表示所有其他用户的权限,rwxr-xr-- 表示其他用户只能读取®文件。
目录的访问模式为:
读取:用户可以查看目录中的文件
写入:用户可以在当前目录中删除文件或创建文件
执行:执行权限赋予用户遍历目录的权利,例如执行 cd 和 ls 命令。(只是对于目录而言)
chmod o=rwx anoterDir : 设置其他用户的权限,o,
- 表示删除权限, + 表示增加权限
6.环境变量
7.ubuntu 登陆到 mysql
四、MySQL 基础面试
1.三个范式是什么
上面的文字我们肯定是看不懂的,也不愿意看下去的。接下来我就总结一下:
首先要明确的是:满足着第三范式,那么就一定满足第二范式、满足着第二范式就一定满足 第一范式
第一范式:字段是最小的的单元不可再分
学生信息组成学生信息表,有年龄、性别、学号等信息组成。这些字段都不可再分,所以它 是满足第一范式的
第二范式:满足第一范式,表中的字段必须完全依赖于全部主键而非部分主键。
其他字段组成的这行记录和主键表示的是同一个东西,而主键是唯一的,它们只需要依赖于 主键,也就成了唯一的
学号为 1024 的同学,姓名为 Java3y,年龄是 22 岁。姓名和年龄字段都依赖着学号主键。第三范式:满足第二范式,非主键外的所有字段必须互不依赖
就是数据只在一个地方存储,不重复出现在多张表中,可以认为就是消除传递依赖
比如,我们大学分了很多系(中文系、英语系、计算机系……),这个系别管理表信息有以下字段组成:系编号,系主任,系简介,系架构。那我们能不能在学生信息表添加系编号, 系主任,系简介,系架构字段呢?不行的,因为这样就冗余了,非主键外的字段形成了依赖关系(依赖到学生信息表了)!正确的做法是:学生表就只能增加一个系编号字段。
2.什么是事务?
事务简单来说:一个 Session 中所进行所有的操作,要么同时成功,要么同时失败ACID — 数据库事务正确执行的四个基本要素
包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程
(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。
举个例子:A 向 B 转账,转账这个流程中如果出现问题,事务可以让数据恢复成原来一样【A 账户的钱没变,B 账户的钱也没变】。
事例说明:
从上面看,我们的确可以发现 A 向 B 转账,成功了。可是如果 A 向 B 转账的过程中出现了问题呢?下面模拟一下
显然,上面代码是会抛出异常的,我们再来查询一下数据。A 账户少了 500 块钱,B 账户的钱没有增加。这明显是不合理的。
我们可以通过事务来解决上面出现的问题
//A 账户减去 500 块
String sql = "UPDATE a SET money=money-500 ";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.executeUpdate();
//在转账过程中出现问题
int a = 3 / 0;
//B 账户多 500 块
String sql2 = “UPDATE b SET money=money+500”;
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.executeUpdate();
//如果程序能执行到这里,没有抛出异常,我们就提交数据
connection.commit();
//关闭事务【自动提交】
connection.setAutoCommit(true);
} catch (SQLException e) {
try {
上面的程序也一样抛出了异常,A 账户钱没有减少,B 账户的钱也没有增加。
注意:当 Connection 遇到一个未处理的 SQLException 时,系统会非正常退出,事务也会自动回滚,但如果程序捕获到了异常,是需要在 catch 中显式回滚事务的。
3.事务隔离级别
数据库定义了 4 个隔离级别:
Serializable【可避免脏读,不可重复读,虚读】Repeatable read【可避免脏读,不可重复读】Read committed【可避免脏读】
Read uncommitted【级别最低,什么都避免不了】
分别对应 Connection 类中的 4 个常量TRANSACTION_READ_UNCOMMITTED TRANSACTION_READ_COMMITTED TRANSACTION_REPEATABLE_READ TRANSACTION_SERIALIZABLE
脏读:一个事务读取到另外一个事务未提交的数据
例子:A 向 B 转账,A 执行了转账语句,但 A 还没有提交事务,B 读取数据,发现自己账户钱变多了!B 跟 A 说,我已经收到钱了。A 回滚事务【rollback】,等 B 再查看账户的钱时,发现钱并没有多。
不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其 他事务所做的修改
注:A 查询数据库得到数据,B 去修改数据库的数据,导致 A 多次查询数据库的结果都不一样【危害:A 每次查询的结果都是受 B 的影响的,那么 A 查询出来的信息就没有意思了】
虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
注:和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致
简单总结:脏读是不可容忍的,不可重复读和虚读在一定的情况下是可以的【做统计的肯定 就不行】。
4.数据库的乐观锁和悲观锁是什么?
确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的 统一性,乐观锁和悲观锁是并发控制主要采用的技术手段。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
在查询完数据的时候就把事务锁起来,直到提交事务
实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定
实现方式:使用 version 版本或者时间戳悲观锁:
乐观锁:
可以参考:
http://www.open-open.com/lib/view/open1452046967245.html
5.超键、候选键、主键、外键分别是什么?
超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超 键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
候选键(候选码):是最小超键,即没有冗余元素的超键。
主键(主码):数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个 数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
外键:在一个表中存在的另一个表的主键称此表的外键。 候选码和主码:
例子:邮寄地址(城市名,街道名,邮政编码,单位名,收件人)
它有两个候选键:{城市名,街道名} 和 {街道名,邮政编码}
如果我选取{城市名,街道名}作为唯一标识实体的属性,那么{城市名,街道名} 就是主码(主键)
6.SQL 约束有哪几种?
7.drop、delete 与truncate 分别在什么场景之下使用?
我们来对比一下他们的区别:
drop table
1)属于 DDL
2)不可回滚
3)不可带 where
4)表内容和结构删除
5)删除速度快truncate table
1)属于 DDL
2)不可回滚
3)不可带 where
4)表内容删除
5)删除速度快delete from
1)属于 DML
2)可回滚
3)可带 where
4)表结构在,表内容要看 where 执行的情况
5)删除速度慢,需要逐行删除
不再需要一张表的时候,用 drop
想删除部分数据行时候,用 delete,并且带上 where 子句保留表而删除所有数据的时候用 truncate
8.索引特点
索引的特点
(1)索引一旦建立, Oracle 管理系统会对其进行自动维护, 而且由 Oracle 管理系统决定何时使用索引
(2)用户不用在查询语句中指定使用哪个索引
(3)在定义 primary key 或 unique 约束后系统自动在相应的列上创建索引
(4)用户也能按自己的需求,对指定单个字段或多个字段,添加索引
需要注意的是:Oracle 是自动帮我们管理索引的,并且如果我们指定了 primary key 或者
unique 约束,系统会自动在对应的列上创建索引…
什么时候【要】创建索引
(1)表经常进行 SELECT 操作
(2)表很大(记录超多),记录内容分布范围很广
(3)列名经常在 WHERE 子句或连接条件中出现
什么时候【不要】创建索引
(1)表经常进行 INSERT/UPDATE/DELETE 操作
(2)表很小(记录超少)
(3)列名不经常作为连接条件或出现在 WHERE 子句中
索引优缺点:
索引加快数据库的检索速度
索引降低了插入、删除、修改等维护任务的速度(虽然索引可以提高查询速度,但是它们也会导致数据库系统更新数据的性能下降,因为大部分数据更新需要同时更新索引)
唯一索引可以确保每一行数据的唯一性,通过使用索引,可以在查询的过程中使用优化隐藏 器,提高系统的性能
索引需要占物理和数据空间索引分类:
唯一索引:唯一索引不允许两行具有相同的索引值
主键索引:为表定义一个主键将自动创建主键索引,主键索引是唯一索引的特殊类型。主键 索引要求主键中的每个值是唯一的,并且不能为空
聚集索引(Clustered):表中各行的物理顺序与键值的逻辑(索引)顺序相同,每个表只能 有一个
非聚集索引(Non-clustered):非聚集索引指定表的逻辑顺序。数据存储在一个位置,索引 存储在另一个位置,索引中包含指向数据存储位置的指针。可以有多个,小于 249 个
9.非关系型数据库和关系型数据库区别,优势比较?
非关系型数据库的优势:
性能:NOSQL 是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过
SQL 层的解析,所以性能非常高。
可扩展性:同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。 关系型数据库的优势:
复杂查询:可以用 SQL 语句方便的在一个表以及多个表之间做非常复杂的数据查询。
事务支持:使得对于安全性能很高的数据访问要求得以实现。 其他:
1.对于这两类数据库,对方的优势就是自己的弱势,反之亦然。
2.NOSQL 数据库慢慢开始具备 SQL 数据库的一些复杂查询功能,比如 MongoDB。
3.对于事务的支持也可以用一些系统级的原子操作来实现例如乐观锁之类的方法来曲线救 国,比如 Redis set nx。
10.MYSQL 的两种存储引擎区别(事务、锁级别等等),各自的适用场景
11.索引有 B+索引和 hash 索引
12为什么设计红黑树
红黑树通过它规则的设定,确保了插入和删除的最坏的时间复杂度是 O(log N) 。
红黑树解决了 AVL 平衡二叉树的维护起来比较麻烦的问题,红黑树,读取略逊于 AVL,维护强于 AVL,每次插入和删除的平均旋转次数应该是远小于平衡树。
因此:
相对于要求严格的 AVL 树来说,红黑树的旋转次数少,所以对于插入、删除操作较多的情况下,我们就用红黑树。但是,只是对查找要求较高,那么 AVL 还是较优于红黑树.
13B 树的作用
B 树大多用在磁盘上用于查找磁盘的地址。因为磁盘会有大量的数据,有可能没有办法一次将需要的所有数据加入到内存中,所以只能逐一加载磁盘页,每个磁盘页就对应一个节点, 而对于 B 树来说,B 树很好的将树的高度降低了,这样就会减少 IO 查询次数,虽然一次加载到内存的数据变多了,但速度绝对快于 AVL 或是红黑树的。
14B 树和 B+树的区别
B/B+树用在磁盘文件组织、数据索引和数据库索引中。其中 B+树比 B 树更适合实际应用中操作系统的文件索引和数据库索引,因为:
1、B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。 一次性读入内存中的需要查找的关键字也就越多。相对来说 IO 读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳 16bytes,而一个关键字 2bytes,一个关键字具体信息指针 2bytes。一棵 9 阶 B-tree(一个结点最多 8 个关键字)的内部结点需要 2 个盘快。而 B+ 树内部结点只需要 1 个盘快。当需要把内部结点读入内存中的时候,B 树就比 B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
2、B±tree 的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何 关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致 每一个数据的查询效率相当。
3、B 树在元素遍历的时候效率较低
由于 B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是 B 树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以 B+树更加适合在区间查询的情况,所以通常 B+树用于数据库索引。在数据库中基于范围的查询相对频繁,所以此时 B+树优于 B 树。
15B 树和红黑树的区别
最大的区别就是树的深度较高,在磁盘 I/O 方面的表现不如 B 树。
要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接
着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘 IO 代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘 IO 频繁读写。根据磁盘查找存取的次数往往由树的高度所决定。
所以,在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘 IO 读写过于频繁,进而导致效率低下。在这方面,B 树表现相对优异,B 树可以有多个子女,从几十到上千,可以降低树的高度。
16AVL 树和红黑树的区别
红黑树的算法时间复杂度和 AVL 相同,但统计性能比 AVL 树更高。
在查找方面:
。
所以,综上:
AVL 比 RBtree 更加平衡,但是 AVL 的插入和删除会带来大量的旋转。 所以如果插入
和删除比较多的情况,应该使用 RBtree, 如果查询操作比较多,应该使用 AVL。
AVL 是一种高度平衡的二叉树,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当 然,如果场景中对插入删除不频繁,只是对查找特别有要求,AVL 还是优于红黑的。
17数据库为什么使用 B 树,而不使用AVL 或者红黑树
我们假设 B+树一个节点可以有 100 个关键字,那么 3 层的 B 树可以容纳大概 1000000 多个关键字(100+101100+101101*100)。而红黑树要存储这么多至少要 20 层。所以使用 B 树相对于红黑树和 AVL 可以减少 IO 操作
18mysql 的 Innodb 引擎为什么采用的是 B+树的索引方式
B+树只有叶子节点存放数据,而其他节点只存放索引,而 B 树每个节点都有 Data 域。所以相同大小的节点 B+树包含的索引比 B 树的索引更多(因为 B 树每个节点还有 Data 域) 还有就是 B+树的叶子节点是通过链表连接的,所以找到下限后能很快进行区间查询,比 B 树中序遍历快
19红黑树 和 b+树的用途有什么区别?
20为什么 B+树比B 树更为友好
- 数据库优化
在我们书写 SQL 语句的时候,其实书写的顺序、策略会影响到 SQL 的性能,虽然实现的功
能是一样的,但是它们的性能会有些许差别。
因此,下面就讲解在书写 SQL 的时候,怎么写比较好。
①选择最有效率的表名顺序
数据库的解析器按照从右到左的顺序处理 FROM 子句中的表名,FROM 子句中写在最后的表将被最先处理
在 FROM 子句中包含多个表的情况下:
如果三个表是完全无关系的话,将记录和列名最少的表,写在最后,然后依次类推 也就是说:选择记录条数最少的表放在最后
如果有 3 个以上的表连接查询:
如果三个表是有关系的话,将引用最多的表,放在最后,然后依次类推。 也就是说:被其他表所引用的表放在最后
例如:查询员工的编号,姓名,工资,工资等级,部门名
emp 表被引用得最多,记录数也是最多,因此放在 form 字句的最后面
select emp.empno,emp.ename,emp.sal,salgrade.grade,dept.dname from salgrade,dept,emp
where (emp.deptno = dept.deptno) and (emp.sal between salgrade.losal and salgrade.hisal)
②WHERE 子句中的连接顺序
数据库采用自右而左的顺序解析 WHERE 子句,根据这个原理,表之间的连接必须写在其他
WHERE 条件之左,那些可以过滤掉最大数量记录的条件必须写在 WHERE 子句的之右。
emp.sal 可以过滤多条记录,写在 WHERE 字句的最右边
③SELECT 子句中避免使用*号
我们当时学习的时候,“*”号是可以获取表中全部的字段数据的。
但是它要通过查询数据字典完成的,这意味着将耗费更多的时间
使用*号写出来的 SQL 语句也不够直观。
④用 TRUNCATE 替代 DELETE
这里仅仅是:删除表的全部记录,除了表结构才这样做。
DELETE 是一条一条记录的删除,而 Truncate 是将整个表删除,保留表结构,这样比
DELETE 快
⑤多使用内部函数提高 SQL 效率
例如使用 mysql 的 concat()函数会比使用||来进行拼接快,因为 concat()函数已经被
mysql 优化过了。
⑥使用表或列的别名
如果表或列的名称太长了,使用一些简短的别名也能稍微提高一些 SQL 的性能。毕竟要扫描的字符长度就变少了。。。
⑦多使用 commit
comiit 会释放回滚点…
⑧善用索引
索引就是为了提高我们的查询数据的,当表的记录量非常大的时候,我们就可以使用索引了。
⑨SQL 写大写
我们在编写 SQL 的时候,官方推荐的是使用大写来写关键字,因为 Oracle 服务器总是先将小写字母转成大写后,才执行
⑩避免在索引列上使用 NOT
因为 Oracle 服务器遇到 NOT 后,他就会停止目前的工作,转而执行全表扫描
①①避免在索引列上使用计算
WHERE 子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描,这样会变得变慢
①②用 >= 替代 >
①③用 IN 替代 OR
①④总是使用索引的第一个列
如果索引是建立在多个列上,只有在它的第一个列被 WHERE 子句引用时,优化器才会选择使用该索引。 当只引用索引的第二个列时,不引用索引的第一个列时,优化器使用了全表扫描而忽略了索引
上边就不使用索引了。
数据库结构优化
五、数组
- 在 java 中,声明一个数组过程中,是如何分配内存的?
1.当声明数组类型变量时,为其分配了(32 位)引用空间,由于未赋值,因此并不指向任何对象;
2.当创建了一个数组对象(也就是 new 出来的)并将其地址赋值给了变量,其中创建出来的那几个数组元素相当于引用类型变量,因此各自占用(32 位的)引用空间并按其默 认初始化规则被赋值为 null
3.程序继续运行,当创建新的对象并(将其地址)赋值给各数组元素,此时堆内存就会有值了
2.找数组当中重复的元素
第一种方法:两层 for 循环 0(n^2) public class TestArray {
public static void main(String[] args) {
int[] arr = new int[] {1, 2, 5, 5, 6, 6, 7, 2, 9, 2}; findDupicateInArray(arr);
}
//第一种,两层 for 循环,指定一个循环一周 0(n^2) public static void findDupicateInArray(int[] arr) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i+1; j < arr.length; j++) { if (arr[i] == arr [ j]) {
System.out.println(“arr[”+j+"]="+ arr[i] + " 和 " +
“arr[”+i+"]="+arr[i]+“冲突” + ", ");
}
}
}
}
}
第二种方法:扫描的时候用哈希表来存储数组的每一个元素,当碰到数组元素与哈希表中一致时,可以确认数字重复。这样的方法时间复杂度是 O(N),但是需要临时空间 O(N)。
public static boolean duplicate(int numbers[],int length,int [] duplication) { Map<Integer,Integer> map=new HashMap<>();
if(length==0) return false; for(int i:numbers){
if(map.containsKey(i)){
System.out.println(“产生冲突的就是”+i);
}
else map.put(i,0);
}
duplication[0]=-1;
return false;
}
3.翻转一个数组
第一种方法:新开辟一个空间public class TestArray {
public static void main(String[] args) { int[] arr = new int[] {1,2,3,4,5};
//定义一个新数组,把老数组中的元素反向添加到新数组中newArrReverse(arr);
//在本数组上进行翻转ArrReverse(arr);
}
//定义一个新数组,把老数组中的元素反向添加到新数组中public static void newArrReverse(int[] arr) {
int[] brr = new int[arr.length];
int length = arr.length - 1; //定义一个从后向前的指针for (int i = 0; i < arr.length; i++) {
brr[i] = arr[length]; length --;
}
System.out.println(Arrays.toString(brr));
}
}
第二种方法:交换法。i 一直递增,length 一直递减,交换
//在本数组上进行翻转
public static void ArrReverse(int[] arr) { int temp = 0;
int length = arr.length - 1;
for (int i = 0; i < arr.length; i++) { if (length == i) {
break;
}
temp = arr[i];
arr[i] = arr[length]; arr[length] = temp; length --;
}
System.out.println(Arrays.toString(arr));
}
4.二维数组中的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,
每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个 整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下: [
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。给定 target = 20,返回 false。
从左下角开始搜索,如果该数大于目标数,则 行数 减去 1,向上搜索小的数值; 如果小于目标数,则 列数 + 1 ,向左边搜索,搜索更大的数值
public class Solution {
public boolean Find(int target, int [][] array) { int row = array.length-1;
int col = 0; // 从左下角开始搜索,array.length 表示行的大小,array[0].length 表示列的大小
while (row >= 0 && col <= array[0].length-1){ if (array[row][col] == target){
return true;
}else if(array[row][col] > target){ row–;
}else{
col++;
}
}
return false;
}
}
5.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1。
NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。
二分查找的变形,注意到旋转数组的首元素肯定不小于旋转数组的尾元素,设置中间点。
1.如果中间点大于首元素,说明最小数字在后面一半,如果中间点小于尾元素,说明最小数字在前一半。依次循环。
2.当一次循环中首元素小于尾元素,说明最小值就是首元素。
3.但是当首元素等于尾元素等于中间值,只能在这个区域顺序查找。如:【1,2,2,3,4】 -->
【2,3,4,1,2】
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
int len = array.length;
if (len == 0){
return 0;
}
// 如果第一个数字 都比最后一个数字小,则代表没有旋转
int left = 0;
int right = len - 1;
if (array[left] < array[right]){
return array[left];
}
int mid;
int minVal = array[left];
while ((right - left) > 1){
mid = (right + left) / 2;
if (array[left] <= array[mid]){
left = mid;
}else if (array[right] >= array[mid]){
right = mid;
}else if ((array[left] == array[mid]) && array[mid] == array[right]){
6.矩形覆盖
我们可以用 21 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 21 的小矩形无重叠地覆盖一个 2n 的大矩形,总共有多少种方法?
比 如 n=3 时 , 23 的 矩 形 块 有 3 种 覆 盖 方 法 :
还是找规律的一道题,归根结底还是 斐波那契数列
7.调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的
前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位 置不变。
8.数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如输入一个长度为 9 的数组{1,2,3,2,2,2,5,4,2}。由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2。如果不存在则输出 0。
#使用哈希表 思路: 使用 hash,key 是数字,value 是出现的次数
9 把数组排成最小数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中 最小的一个。
例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为 321323。
这里自定义一个比较大小的函数,比较两个字符串 s1, s2 大小的时候,先将它们拼接起来, 比较 s1+s2,和 s2+s1 那个大,如果 s1+s2 大,那说明 s2 应该放前面,所以按这个规则,
s2 就应该排在 s1 前面。(类似于排序算法)
比如: s1 = 32, s2 = 321, 先将两者字符串转换成数字,比较 s1 + s2 = 32321, 和s2 + s1 = 32132 的数字的大小,
如果前者比较大的话,将 s1 和 s2 交换。
代码这里使用了一个小技巧:
int pre = Integer.valueOf(numbers[i] +"" + numbers[j]); // 将数字转换为字符串,拼接,再转换成数字
10.丑数
丑数的定义:把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。
例如 6、8 都是丑数,但 14 不是,因为它包含质因子 7。 习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。
首先从丑数的定义我们知道,一个丑数的因子只有 2,3,5,那么丑数 p = 2 ^ x * 3 ^ y * 5 ^
z,换句话说一个丑数一定由另一个丑数乘以 2 或者乘以 3 或者乘以 5 得到,那么我们从 1 开始乘以 2,3,5,就得到 2,3,5 三个丑数,在从这三个丑数出发乘以 2,3,5 就得到 4,6,10,6,
9,15,10,15,25 九个丑数,我们发现这种方法得到重复的丑数,而且我们题目要求第 N 个丑数。
(1)丑数数组: 1
乘以 2 的队列:2
乘以 3 的队列:3
乘以 5 的队列:5
选择三个队列头最小的数 2 加入丑数数组,同时将该最小的数乘以 2,3,5 放入三个队列;
(2)丑数数组:1,2 乘以 2 的队列:4
乘以 3 的队列:3,6
乘以 5 的队列:5,10
此时可能出现,乘以 5 的数值大于 乘以 3 的数值,所以要取 乘以 3 和乘以 5 的最小值
选择三个队列头最小的数 3 加入丑数数组,同时将该最小的数乘以 2,3,5 放入三个队列;
(3)丑数数组:1,2,3 乘以 2 的队列:4,6
乘以 3 的队列:6,9
乘以 5 的队列:5,10,15
选择三个队列头里最小的数 4 加入丑数数组,同时将该最小的数乘以 2,3,5 放入三个队列; import java.util.ArrayList;
public class Solution {
public int GetUglyNumber_Solution(int index) { if (index <= 0) return 0; ArrayList res = new ArrayList(); res.add(1);
int i2=0, i3=0, i5=0; while (res.size() < index){
int i2_value = res.get(i2) * 2; int i3_value = res.get(i3) * 3; int i5_value = res.get(i5) * 5;
// 找到最小的值
int minValue = Math.min(i2_value, Math.min(i3_value, i5_value)); res.add(minValue);
if (i2_value == minValue) i2++; // 将 index 往后移动if (i3_value == minValue) i3++;
if (i5_value == minValue) i5++;
}
return res.get(res.size()-1);
}
}
11.数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现 一次的数字。
使用哈希表,这是比较容易想到的方法
map.put(array[i], map.get(array[i]) + 1);
}
}
// 获 取 value 值 为 1 的 key 值
boolean sign = false;
for (int i = 0; i < size; i++){
if (map.get(array[i]) == 1){
if (sign == false){
num1[0] = array[i];
sign = true;
}else{
num2[0] = array[i];
}
}
}
}
使用异或
使用的原则是:
位运算中异或的性质:两个相同数字异或=0,一个数和 0 异或还是它本身。
当只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的 数,因为成对儿出现的都抵消了。
只不过这里使用两次异或,内在还是要将不同的两个数字分开,再异或。
任何一个数字异或他自己都等于 0,0 异或任何一个数都等于那个数。数组中出了两个数字之外,其他数字都出现两次,那么我们从头到尾依次异或数组中的每个数,那么出现两次的 数字都在整个过程中被抵消掉,那两个不同的数字异或的值不为 0,也就是说这两个数的异或值中至少某一位为 1。
我们找到结果数字中最右边为 1 的那一位 i,然后一次遍历数组中的数字,如果数字的第 i 位为 1,则数字分到第一组,数字的第 i 位不为 1,则数字分到第二组。这样任何两个相同的数字就分到了一组,而两个不同的数字在第 i 位必然一个为 1 一个不为 1 而分到不同的组, 然后再对两个组依次进行异或操作,最后每一组得到的结果对应的就是两个只出现一次的数字。
int num = 0;
for (int i = 0; i < size; i++){
num ^= array[i];
}
int index = getFirstBitOne(num);
for (int i = 0; i< size; i++){
if(isBitOne(array[i], index)){
num1[0] ^= array[i];
}else{
num2[0] ^= array[i];
}
}
}
private int getFirstBitOne(int num){
int index = 0;
while ((num & 1) == 0 && index < 32){
num >>= 1;
index += 1;
}
return index;
}
private boolean isBitOne(int num, int index){
12.数组中重复的数字
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。 数组中某些数字是重复的, 但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为 7 的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字 2。使用类似哈希表的方法: