1、多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
2、TCP三次握手,四次挥手
三次握手:
三次握手主要的作用是为了确认双方的接收能力和发送能力是否正常。
1):客户端给服务端发送一个SYN报文,并指明客户端的初始化序列seq=x,此时客户端处于SYN_SEND状态。(SYN=1,seq=x)
2):服务端收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也指明了自己的初始化序列号,同时会把客户端的序列号+1作为ack的值,表示自己已经收到了客户端的SYN,此时服务端处于SYN_REVD状态。(SYN=1, ACK=1,ack=x+1,seq=y)
- 回传SYN是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
- 传了SYN为啥还要传ACK:传了SYN只能证明发送到接收方的通道没有问题,但是接收方到发送方的通道还需要ACK信号来进行验证。
3):客户端收到SYN报文之后,会发送一个ACK报文,也是把服务端的序列号+1作为ack的 值,表示已经收到服务端的SYN报文,此时客户端处于Establish状态,服务端收到ACK报文之后,确认ack无误之后,也处于establish状态。连接建立成功(ACK=1,ack=y+1,seq=x+1)
四次挥手:
四次挥手是由TCP的半关闭造成的,半关闭就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
1):客户端发送一个FIN,用来关闭客户端到服务器的数据传送,此时客户端处于FIN_WAIT状态,并停止发送数据,主动关闭TCP连接(FIN=1,seq=u)
2):服务器收到这个FIN,它回传一个ACK,确认序号为收到的序号+1。此时服务端处于CLOSE_WAIT状态。此时TCP处于半关闭状态。客户端收到服务端的确认之后,进入FIN_WAIT2状态,等到服务端发出的连接释放报文。(ACK=1,ack=u+1,seq=v)
3):服务器关闭与客户端的连接,发送一个FIN给客户端,且指定一个序列号,此时服务端处于LAST_ACK状态,(FIN=1,ACK=1,ack=u+1,seq=w)
4):客户端发回ACK报文确认,并将确认序号设位收到的序号加1.此时客户端处于TIME_WAIT状态。此时TCP未closed,需要等待计时器设置2MSL之后,客户端才进入Closed状态。(ACK=1,ack=w+1,seq=u+1)
为什么需要三次握手,四次挥手?
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭。
(三次握手)
1)双方确认自己于对方的发送和接收都是正常的。
2)防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误.
客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,成功建立。就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文(如果是两次握手,就直接建立了连接,但是并没有使用,造成不必要的资源浪费),但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
(四次挥手)关闭连接的时候,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,以防ACK和FIN一般都会分开发送,从而导致多了一次。
3、实现快排
快速排序(Quick Sort)的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法分别对这两部分数据继续进行排序。快速排序的平均时间复杂度为。
算法步骤:
- 从数列中挑出一个元素,称为 "枢纽元素";
- 重新排序数列,所有元素比枢纽值小的摆放在枢纽前面,所有元素比枢纽值大的摆在枢纽的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)分别对小于枢纽值元素的子数列和大于枢纽值元素的子数列按步骤1和2进行排序;
void sort(int[] nums) {
int low = 0, high = nums.length - 1;
quickSort(nums, low, high);
}
void quickSort(int[] nums, int low, int high) {
if (low < high) {
int partitionIndex = partition(nums, low, high);
//对左分区进行排序
quickSort(nums, low, partitionIndex - 1);
//对右分区进行排序
quickSort(nums, partitionIndex + 1, high);
}
}
int partition(int[] nums, int low, int high) {
//这里选择当前子数组中的第一个元素作为枢纽元素,注意枢纽元素位置的不同,其代码实现也不同
int key = nums[low];
while (low < high) {
//将比枢纽元素小的元素交换到左端
while (low < high && nums[high] >= key)
high--;
swap(nums, low, high);
//将比枢纽元素大的元素交换到左端
while (low < high && nums[low] <= key)
low++;
swap(nums, low, high);
}
//返回枢纽所在位置
return low;
}
4、比较快排、冒泡、堆排序、归并排序、选择排序
5、读取文本数据,文本中每一行由字符和数字构成,提取数字并进行排序
6、HashMap、HashTable比较
- 线程是否安全:
HashMap
是非线程安全的,HashTable
是线程安全的,因为HashTable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); - 效率: 因为线程安全的问题,
HashMap
要比HashTable
效率高⼀点。另外,HashTable
基本被淘汰,不要在代码中使用它; - 对
Null key
和Null value
的支持:HashMap
可以存储null
的key
和value
,但null
作为键只能有⼀个,null
作为值可以有多个;HashTable
不允许有null
键和null
值,否则会抛出NullPointerException 。 - 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 。HashMap
默认的初始化大小为 。之后每次扩充,容量变为原来的 倍。② 创建时如果给定了容量初始值,那么Hashtable
会直接使⽤你给定的大小,而HashMap
会将其扩充为的幂次方大小(HashMap
中的tableSizeFor()
⽅法保证,下⾯给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表⻓度
⼤于阈值(默认为 )(将链表转换成红黑树前会判断,如果当前数组的⻓度小于,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红⿊树,以减少搜索时
间。Hashtable
没有这样的机制。
7、HashMap实现原理,扩容机制
实现原理
–》JDK 1.7 : Table数组+ Entry链表;
–》JDK1.8 : Table数组+ Entry链表/红黑树;(为什么要使用红黑树?)
- HashMap 通过key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
扩容机制
创建时如果不指定容量初始值, HashMap
默认的初始化大小为 。之后每次扩充,容量变为原来的 倍。创建时如果给定了容量初始值, HashMap
会将其扩充为的幂次方大小( HashMap
中的 tableSizeFor()
⽅法保证,下⾯给出了源代码)。也就是说HashMap
总是使⽤ 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
** HashMap的长度为什么是2 的幂次方?**
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到
了过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得比较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) &hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。
这个算法应该如何设计呢?
我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2
的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提
是 length 是2的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解
释了 HashMap 的⻓度为什么是2的幂次⽅。
8、描述链表数据结构
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。链表常用的有 3 类: 单链表、双向链表、循环链表。