一、面试的血泪史
1. 一个字符串,如何统计这个字符串里的每个字符出现的次数?
String str = "2hin2hbyb2bhbhbvas";
String key;
Map<String, Integer> map = new HashMap<>();
for(int i = 0; i < str.length(); i++) {
key = String.valueOf(str.charAt(i));
map.put(key, map.get(key) == null ? 1 : map.get(key)+1);
}
for(Map.Entry entry : map.entrySet()) {
System.out.println(entry.getKey() +":" +entry.getValue());
}
输出结果:
a:1
2:3
b:5
s:1
v:1
h:4
i:1
y:1
n:1
2. 如何用java来实现链表、栈、队列这些数据结构?
答:使用LinkedList
然而当时年轻不懂事,都答不上来,于是我就决定好好学学容器,然后一拖就拖到现在。。。
二、什么是容器
如果有一个类专门用来存放其它类的对象,这个类就叫做容器,或者就叫做集合,集合就是将若干性质相同或相近的类对象组合在一起而形成的一个整体
三、为什么要使用容器
使程序更简洁、更强大、更高效。
四、容器的分类——Set集、List列表、Map映射
1. List列表——允许重复,按顺序存储
- ArrayList(数组):擅长随机访问
- LinkedList(双链表):擅长随机插入
结论:如果你发现在对ArrayList进行插入删除操作的时候程序明显变慢,不妨试试改为LinkedList进行优化
在这里再补充一点LinkedList的扩展
LinkedList还添加了可以使其作为栈(Stack)、队列(Queue)或者双向队列的方法。
例如:
removeFirst()——移除并返回列表的头,如果列表为空则返回NoSuchElementException;同理还有remove Last()
addFirst()——将某个元素插入开头
add()与addLast()——相同,他们都是将某个元素插入到列表的尾端
2. Set集——不允许重复
- HashSet(哈系数/散列):查询速度快
- TreeSet(红—黑树):保持元素处于排序状态
- LinkedHashSet(链表+哈系数):保存元素插入顺序
下面有请截图,你就知道我为什么不讲set了:
照着Java这尿性,你可能已经猜到其他的set是怎么实现的了(手动滑稽)
推荐一篇文章,看了这篇文章你会发现set不用学了:https://blog.csdn.net/sugar_rainbow/article/details/68257208
看过java编程思想这本书的少年可能有个疑问,为什么我们用HashSet敲出来的代码居然排序了,怎么add()进去怎么遍历出来,书上却是没有排序?其实就是java7与java8的区别了,感兴趣可以看一篇文章:https://www.douban.com/note/596873407/
最后,我们使用set来实现一下交差并集:
3. Map映射——以键值对形式进行存储
- HashMap(哈系数):数组方式存储key/value,线程非安全,允许null作为key和value,key不可以重复,查询速度快
- HashTable(哈系数):允许null,跟HashMap实现是一样的,只是在方法中加入了synchronized(同步),所以是线程安全的,同时也影响了它的查询速度,没有HashMap查询快
- TreeMap(树):基于红黑二叉树的NavigableMap的实现,线程非安全,不允许null,key不可以重复,value允许重复,当用Iterator 遍历TreeMap时,得到的记录是排过序的
- LinkedHashMap(链表+哈系数):保存元素插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的
4. HashMap中的哈希表
Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”。
hashMap的put()流程:当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。
hashMap的get(key)流程:通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
(上面文字来源:https://www.cnblogs.com/chenssy/p/3521565.html)
5. TreeMap中的红黑树
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具有二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。
我们知道一颗基本的二叉树他们都需要满足一个基本性质–即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVL,SBT,伸展树,TREAP,红黑树等等。
平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等等子节点,其左右子树的高度都相近。
红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则:
1、每个节点都只能是红色或者黑色。
2、根节点是黑色。
3、每个叶节点(NIL/空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
对于红黑二叉树而言它主要包括三大基本操作:左旋、右旋、着色。
总结:TreeMap新增(put)的时候先构建普通的排序二叉树,再平衡二叉树;删除(delect)也是一样的
(上面文字来源:http://www.cnblogs.com/chenssy/p/3746600.html)
五、迭代器
1. 什么是迭代器,引用java编程思想里面的话来说
2. 下面用一段代码来介绍迭代器
class Phone {
private BigDecimal money;
Phone(BigDecimal money) {
this.money = money;
}
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
}
private void display(Iterator<Phone> it) {
while(it.hasNext()) {
System.out.println(it.next().getMoney());
}
System.out.println("----------------------");
}
@org.junit.Test
public void test3() {
ArrayList<Phone> arrayList = new ArrayList<>();
LinkedList<Phone> linkedList = new LinkedList<>();
HashSet<Phone> hashSet = new HashSet<>();
for(int i = 0; i < 3; i++) {
arrayList.add(new Phone(new BigDecimal(i)));
linkedList.add(new Phone(new BigDecimal(i)));
hashSet.add(new Phone(new BigDecimal(i)));
}
display(arrayList.iterator());
display(linkedList.iterator());
display(hashSet.iterator());
}
3. 然而,看了上面一段代码我却更加疑惑了,明明上面那段代码我可以用循环来办到,甚至用foreach代码更简单,为什么还要用迭代器呢?那我们在看一段代码
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add("111");
arrayList.add(222);
arrayList.add(1.12);
// 迭代器实现
Iterator<Object> it = arrayList.iterator();
while(it.hasNext()) {
if("111".equals(it.next().toString())) {
// 这里执行remove就能删除arrayList里面的数据了,结论就是使用迭代器操作数据比较简单
it.remove();
}
}
//arrayList.removeIf(object -> "111".equals(object.toString()));
// foreach实现
for(Object object : arrayList) {
System.out.println(object);
}
4. 看到这里,你可能会怀疑,为什么Iterator能够去操作容器,在这里我举例ArrayList的继承关系,大家看了就懂了,其他的容器继承关系也是类似的
六、最后附上一张集合类库的完整继承图
PS:
1. 本文引用了java编程思想和很多博客中的话,这些博客里面写的比较深入,感兴趣的请点击上面的文章内容的连接学习
2. 以上只是我对java的底层和算法的粗浅了解,如有错误,欢迎指出