面试总结2——数据结构

主要参考于这个博主的一些文章,https://blog.csdn.net/justloveyou_并根据自己的面试经验做了相关的补充。

二叉树有哪些类型,分别有什么特点

二叉树:每个结点至多有两个子树。

满二叉树:除叶结点外所有的节点都有左右节点,且叶结点都在最下一层(层数为k,结点总数为(2^k)-1,第i层上的结点数为2^(i-1)个)。节点数为n,则高度为:logn+1

完全二叉树:最下面一层的叶结点都依次排列在该层从左到右的位置上,且叶结点只出现在最下面的二层中。

二叉查找树:左子树值小于根节点,右子树大于根节点。

平衡二叉树:左右子树高度差不大于1. 搜索时间快。

红黑树:近似平衡,插入删除优于AVL,搜索低于AVL。

二叉树的遍历:前、中、后序遍历,层次遍历。

递归写法:

非递归写法:用栈实现前中后的遍历非递归写法。

红黑树(搜索效率为log n)

1,定义

  • 每个节点只能是红色或者黑色
  • 根节点为黑色
  • 叶结点(null)为黑色
  • 红节点的子节点为黑色
  • 任意节点到其每个叶子结点的所有路径包含数量相等的黑色节点。

2,特点

近似平衡,比AVL调整速度高

3,为什么结点要设置为红色和黑色

定义提到的5个条件避免二叉查找树退化成单链表的情况,定义4和定义5可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。

原因:当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。

4,应用

用来存储有序的数据,时间复杂度为logn,Java中TreeSet和TreeMap以及Linux虚拟内存的管理,都是红黑数实现的。

5,添加:当作二叉树,将节点插入;将插入节点着色为红色(因为插入之前所有根至外部节点的路径上黑色节点数目都相同,所以如果插入的节点是黑色肯定错误(黑色节点数目不相同),而相对的插入红节点可能会也可能不会违反“没有连续两个节点是红色”这一条件,所以插入的节点为红色,如果违反条件再调整);旋转着色。

6,与ALV比较

ALV树是一种严格按照定义来实现的平衡二叉查找树,所以它查找的效率非常稳定,为O(log n),由于其严格按照左右子树高度差不大于1的规则,插入和删除操作中需要大量且复杂的操作来保持ALV树的平衡(左旋和右旋),因此ALV树适用于大量查询,少量插入和删除的场景中。红黑树是近似平衡的,降低了ALV树对平衡性的要求从而达到快速的插入和删除。

平衡二叉树的构造:

https://blog.csdn.net/zhuyingqingfen/article/details/6530434

可以直接旋转:

LL型:插入位置为左子树的左结点

RR型:插入位置为右子树的右结点

直接旋转蹩脚,必须子树先旋转,在旋转整个子树

LR型:插入位置为左子树的右结点

RL型:插入位置为右子树的左结点。

 

!右左的第二幅图,95应该作为100的左子树。

堆的实现

1) 最小堆,每一个节点的值都小于等于父节点的值;

2) 底层是完全二叉树,可以用数组来实现(也可以用链表,但是比较麻烦)。当前节点为index,父节点为(index-1)/2;子节点为2*index+1,2*index+2

3) 插入时(上浮):在最后一个节点插入,从最后一个节点开始,通过不断与父节点比较交换调整顺序;

4) 删除时(下沉),将堆顶元素和堆得最后一个元素互换位置,然后删除最后一个节点,堆顶开始,通过与子节点中最大的节点互换位置,调整整个堆。

队列

只允许一端插入,另一端删除的线性表。

方法的区别

Offer,add:不允许添加的时候,一个返回false,一个抛出异常。

Poll,remove:空集合时,一个返回null,一个抛出异常。

Peek,element:队列为空时,一个返回null,一个抛出异常。

普通队列(线程不安全)

1,双端队列。

2,PriorityQueue(最大堆实现)

PriorityQueue 类实质上维护了一个有序列表。

基于优先级,无界,队列

不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。

内置不阻塞队列ConcurrentLinkedQueue

堵塞队列和非阻塞队列都是为了实现线程安全,阻塞队列是用了锁来实现,非阻塞队列是用循环CSA实现。

循环CAS方法实现,基于链接节点的、无界的、线程安全的队列。并发访问不需要同步。

使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。

head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。

由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue 使用三个不变式来维护非阻塞算法的正确性。

以批处理方式来更新 head/tail,从整体上减少入队 / 出队操作的开销。

为了有利于垃圾收集,队列使用特有的 head 更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。

阻塞队列

当队列满或者无元素的时候,线程执行阻塞操作,直到有空间或者元素可用。

  1. ArrayBlockingQueue :一个由数组支持的有界队列。内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。共用同一个锁对象,由此也意味着两者无法真正并行运行。为什么不是用分离锁:因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。
  2. LinkedBlockingQueue :一个由链接节点支持的可选有界队列。不指定时容量为Integer.MAX_VALUE。高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别
  3. PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
  4. DelayQueue :一个由优先级堆支持的、基于时间的调度队列。DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列。
  5. SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。

DelayQueue

定义:

  1. 队列使用PriorityQueue来实现,根据元素的delay对元素进行排列,最小堆。
  2. 支持延时获取元素的无界阻塞队列。
  3. 队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
  4. 继承了AbstractQueue抽象类并实现了BlockingQueue接口。

应用:

  1. 缓存系统的设计:使用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,就表示有缓存到期了。 
  2. 定时任务调度:使用DelayQueue保存当天要执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如Timer就是使用DelayQueue实现的。

Set(三个都是非同步的)

  1. LinkedHashset : 保证元素添加的自然顺序。
  2. TreeSet : 保证元素的自然顺序。
  3. HashSet:无序。(每添加每添加一个元素,都会按照其内部算法将元素添加到合适的位置,所以不能保证内部存储是按元素添加的顺序而存储的)

int hash = hash(key.hashCode());

int i = indexFor(hash, table.length);  // hashset的添加位置i是由要加入的元素的hash code值和table数组的长度决定的

hashset

  • 继承abstractSet,实现了cloneable和serializable接口;
  • 底层实现为初始化一个新的hashmap,初始容量16,负载因子0.75,元素存到Key中,value为一个统一的值:private static final Object PRESENT = new Object();
  • Vaule为什么不为null:remove的时候,无法分辨是否移除成功。

  • 插入删除,与hashmap一致;
  • 对hashset进行迭代时,返回元素的顺序不是特定的;
  • Contains方法调用了map.containsKey方法;
  • Hashset没有提供get方法,因为内部是无序的,只能通过迭代方法获得
  • 怎么实现的不可重复,无序呢:因为map的key就是不可重复的。

LinkedHashset

  • 不重合,可预测的迭代顺序。
  • 底层实现为hashmap+双向链表,初始化一个新的hashmap,初始容量16,负载因子0.75。
  • 插入的时候在末尾插入,所以迭代时获取顺序等于添加顺序;
  • 按照添加顺序。迭代是有序的。
  • 性能低于hashset,因为要维护双向链表。

TreeSet

  • 底层实现为初始化一个新的treemap作为容器,采用红黑书保存map中的每一个entry。
  • 添加的元素必须实现comparable接口。
  • 可根据comparator实现有序性。

Map

TreeMap

  • 有序的key_value集合,红黑树实现,按照键的自然顺序进行排序,或者可以根据Comparator进行排序,取决于使用的构造方法。
  • 继承于AbstractMap,实现了NavigableMap,可以返回有序的key集合;实现了Cloneable接口,可以被克隆;实现了Serializable接口,支持序列化。
  • 基本操作:containsKey,get,put,remove。时间复杂度(logn)
  • 非同步的,iterator方法返回的迭代器是fail-fast的。
  • Entry<key,value>是红黑树的节点,包含了六个基本组成成分,key,value,left,right,parent,color。Entry根据key进行排
  • 构造函数:public TreeMap():默认

        public TreeMap(Comparator c):带构造器

        public TreeMap(Map m):map成为treemap的子集,实际是逐个添加的。

         public TreeMap(SorteMap sm):sortedMap成为treemap的子集。

  • firstEntry(), lastEntry(), lowerEntry(), higherEntry(),floor Entry(), ceiling Entry(),,pollFirst Entry(),pollLast Entry(),

hashMap和hashtable的区别

相同点:

  1. 存储键值对,拉链法实现。通过table数组存储,数组的每个元素是一个entry,一个entry是一个单向链表。
  2. 添加:根据key求hash值,再计算数组索引,根据数组索引找到entry,遍历单向链表,将key与链表的每个节点的key进行对比,若已经存在,则取代旧的值,若不存在,则新建一个key-value节点,并插入entry链表的表头位置。
  3. 删除:根据key求hash值,再计算数组索引,根据数组索引找到entry,删除节点。

 

不同点:

  1. Hashmap继承于abstractMap,hashtable继承dictionary,都实现了map,cloneable,serializable接口。Dictionary是一个抽象类,直接继承于object,api比map少,而且一般通过枚举去遍历,map通过迭代去遍历。
  2. Hashtable几乎所有函数都是同步的,线程安全,hashmap函数非同步,不是线程安全的;若要在多线程中使用hashmap,要额外进行同步处理,使用collections类提供的synchronizedMap静态方法,或者直接用concurrentHashMap类。
  3. 对null值得处理:hashMap的key,value都可以为null(key只能有一个null,固定放入散列表的第一个位置,多个null会发生覆盖),hashtable都不可以为null。
  4. 支持的遍历不同:hashmap支持iterator遍历,hashtable支持迭代器和枚举器遍历。
  5. 通过迭代器遍历时,hashmap从前往后便利数组,hashtable从后往前便利数组。
  6. Hashmap初始容量16,扩容为2倍,hashtable初始容量为11,每次扩容2倍+1;
  7. 添加元素时,hashmap使用自定义的哈希算法,hashtable使用key的hashcode方法。
  8. Hasmaph不支持contains方法,没有重写toString方法,Hashtable支持contains方法,重写了toString方法,

 

应用

主要考虑线程安全与否,是否可以插入null值。

Hash算法的不同:

Hashtable:直接取key的hashcode

Int hash = key.hashCode();

Hashmap:key的hashcode低16位与高16位进行异或操作。

static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  // 减少hash扰动。

}

concurrenthashMap和hashtable的区别

  1. hashtable使用synchronized保证线程安全,每次锁住整个容器,竞争激烈的情况下效率非常低。
  2. concurrentHashMap,使用锁分段技术,数据分段存储,每一段数组配一把锁,当方法需要跨段时(比如size方法),按顺序锁定所有段,再按顺序释放所有段的锁。

Hashtable:

  1. 底层数组+链表,
  2. key,value不能为null,
  3. 线程安全(函数都是同步的),锁住整个hahstable,效率低
  4. 初始容量11,每次扩容2倍+1;

 

为什么扩容2倍+1

Int hash = key.hashCode();

Int index = (hash&0x7fffffff)%len;

  1. 2倍是为了利用位运算提高运算速度,加一是为了得到的数是奇数,这样是素数的概率比较大。用素数作为哈希表的大小,可以减少哈希冲突。
  2. Hash&0x7fffffff是为了消除负的hash值。

Hashmap:

  1. 初始容量16,负载因子0.75,每次扩容2倍(计算index,减少冲突);
  2. key,value可以为null
  3. 插入元素后才判断该不该扩容,有可能无效扩容。
  4. Index计算方法:hash&(len-1)。
  5. Jdk1.7:数组+链表,头插法(查找速度快,但是并发下扩容会发生死循环)。

Jdk1.8:数组+链表+红黑树,尾插法,当数组长度达到64并且单链表的长度达到8之后,链表会转化成红黑树的结构,当长度降到6就转成普通链表。

为什么扩容2

  1. hash取余可以等价于hash&(len-1),位运算提高运算速度;
  2. 扩容后原来元素的位置要么位于原地址(原来的hash值新增的bit市0)要么位于原地址+原容量大小;
  3. 当数组长度为2的n次幂的时候,不同的key计算index相同的几率较小,数据在数组上分布就比较均匀,减小hash冲突。

求hash的时候使用了扰动函数

Hash取余之后容易发生碰撞,扰动函数使得hash值包含了高16位和第16位的特性,降低碰撞的概率。

为什么节点个数为8时转换为红黑树,个数为6转换为链表(树化)

  1. 空间和时间的权衡:虽然查询速度的话红黑树优于链表,但是TreeNodes占用空间是普通Nodes的两倍,所以只有当链表包含足够多的节点时才会转成TreeNodes。
  2. hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。

Hashmap多线程下不安全的具体体现

  1. 如果几个线程同时在一个位置table[i]进行添加或者删除操作,会出现被覆盖或者其它情况;
  2. 读不加锁得到不一致数据(一个读,一个改,没有实现立即可见性);
  3. 多线程同时操作一个HashMap,进行扩容重排的过程中,有可能会出现环形链表。

hashmap中头插法为什么会导致死锁

两个线程同时对hashmap进行扩容。调整大小时,存储在链表中的元素的次序会反过来。两个线程同时修改一个链表结构会产生一个循环链表。

concurrentHashMap

  1. 线程安全,分段加锁,多个修改并发执行,效率提高n倍;
  2. key,value不能为null
  3. hashEntry的value变量是volatile的,保证读取到最新的值。
  4. 默认16个桶,段内扩容,插入前检测需不需要扩容,避免无效扩容;
  5. 读不加锁(因为hashEntry中value用volatile修饰,立即可见,其他(key,hash,next)都是final,hashmap读不加锁得到不一致数据;
  6. 对于节点的修改只能从头部开始(final修饰了next),put添加到hash链的头部,remove中间节点时,需要将前面节点整个复制一遍,最后一个结点指向删除节点的下一个节点,后面的节点可以重用;
  7. Jdk1.7:segment+hashEntry(数组+链表),segment是一种可重入锁ReentrantLock,每次锁住一个segment,包含多个hashEntry;

Jdk1.8:node数组+链表+红黑树,链表长度大于8时,链表转为红黑树,并发控制使用synchronized和CAS操作实现。降低锁粒度(每次锁住的是hashEntry的头结点)。如果没有hash冲突直接CAS插入(key对应的数组元素为null的时候),否则加锁插入。

  1. 弱一致性:get,clear,iterator是弱一致性的,比如get获取数据时,刚好有一个线程在put,若put的key是存在的,那么get获取数据的时候可以获取到put的新值(volatile),但是如果put一个新的hashEntry,则get线程不能马上看到。弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。

一致性哈希

为什么采用一致性哈希:

分布式系统中对象与节点的映射关系,传统方案是使用对象的哈希值,对节点个数取模,再映射到相应编号的节点,这种方案在节点个数变动时,绝大多数对象的映射关系会失效而需要迁移;而一致性哈希算法中,当节点个数变动时,映射关系失效的对象非常少,迁移成本也非常小。

定义:

         虚拟环结构

         位置数量不再固定

应用:

         分布式数据库

         分布式缓存

特性:

         单调性:单调性是指如果已经有一些请求通过哈希分派到了相应的服务器进行处理,又有新的服务器加入到系统中时候,应保证原有的请求可以被映射到原有的或者新的服务器中去,而不会被映射到原来的其它服务器上去。

         分散性(Spread):避免相同的内容被不同的终端映射到不同的缓冲区中。

         平衡性(Balance):平衡性也就是说负载均衡,是指客户端hash后的请求应该能够分散到不同的服务器上去。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值