正在看文章的你不妨试着回答一下java岗位必问面试题:
1.HashMap的底层数据结构是什么?
2.他的set的过程是怎么样的?
3.hashmap产生哈希冲突了怎么做的?(和问题2有关)
如果你暂时说不清楚的话就试着看下我:
上菜!!!:
本文章脑图:
前言:不知大家有没有在面试中碰到过这样的问题,反正我是碰到过?而且当时面试的还是一家普通的外包公司,所以hashmap的数据结构相关面试题可以说是非常普遍了,所以关于这一块的数据结构非常有必要花时间去钻研一下,有实力和精力的甚至建议去读源码;
一 为什么面试官那么喜欢问HashMap这个板块?
很简单!!!
1.从数据结构的角度:HashMap就涉及到了:数组+链表+红黑树,这对面试者对数据结构的掌握有一定的要求;
2.从面试检验是否技术底层牢固的角度:死记硬背面试题是可以回答出一些内容的,但是如果没有理解透彻的话,面试官稍微换个角度问,马上就露馅了;
二 学习HashMap底层数据结构小tips
首先每个人有每个人的学习方式,对于像我这种半路出家恶补基础的人来说,我的建议是这样的:
1.学习数组+链表的数据结构
2.学习二叉树+红黑树的数据结构
3.尝试手动依次实现这些数据结构(什么语言随你)
4.借助HashMap的实现过程,将以上学习到的内容串在一起;
三 什么是链表?
数组很容易理解,先从链表开始)
3.1 理解链表,建立画面感
官方定义:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
大白话:
1.玩过老鹰抓小鸡吗?被抓的小鸡们就是一个单向链表,除了第一个人(头节点),余下每个人只专注抓稳一个人,后面不管;
2.看过解放军战士的抗洪一线新闻吗?解放军手拉手站在洪水里,和洪水斗争那英勇的样子就是双向链表;
PS:致敬!!!
3.2 单向链表内部:
内部的各个节点,每一个节点包含:
1.存储的元素(data)
2.指向下一个节点的指针next;
public static class Node<T> {
//存储文件,元素
private T data;
//存储下一个节点
private Node next;
}
整个链表的连接是通过每一个节点内部的next指针指向自己的下一个节点的方式环环相扣,就像老鹰抓小鸡游戏里面的一群小鸡,每一个人只管抓前面一个人,就这样一直保持一个长长的的队伍进行着游戏;
3.3 双向链表内部:
双向链表稍微复杂一些,每一个节点包含:
1.存储的元素(data)
2.指向下一个节点的指针next
3.指向上一个节点的指针previous
PS:
public static class Node<T> {
//存储文件,元素
private T data;
//存储下一个节点
private Node next;
//存储上一个节点
private Node previous;
}
整个链表的连接是通过每一个节点内部的next指针指向自己的下一个节点,previous指向自己的上一个节点,环环相扣,就像解放军战士手拉着手,铸成最坚固的人墙!!!
四 链表的基本API(查-增-改-删)
先瞅瞅原本方法名是啥样:
import java.util.LinkedList;
//可以直接复制过去自己idea瞅瞅
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
//API 新增
linkedList.add("one");
linkedList.add("two");
System.out.println("linkedList.size() = " + linkedList.size());
//在头结点增加和在尾结点增加
linkedList.addFirst("zero");
linkedList.addLast("ten");
//修改指定位置的元素
linkedList.set(1, "changeOne");
//得到指定位置的元素
linkedList.get(1);
//for循环遍历,迭代器使用
for (String message : linkedList) {
System.out.println("循环得到" + message);
}
System.out.println("------------------------");
//删除指定位置的元素
linkedList.remove(1);
for (String message : linkedList) {
System.out.println("循环得到" + message);
}
System.out.println("------------------------");
//删除头结点和尾结点:remove() 和removeFirst()作用一样
//linkedList.remove();
linkedList.removeFirst();
//for循环遍历,迭代器使用
for (String message : linkedList) {
System.out.println("循环得到" + message);
}
linkedList.removeFirst();
linkedList.removeLast();
}
}
也就是要实现下面的这样的方法:
/**
* 手动模拟一个单向链表list
* 单向链表API:(这里选择一部分进行实现)
* 无参构造方法
* list.add(data)
* list.add(index,data)
* list.addFirst()
* list.addLast()
* list.remove(index)
* list.removeAll()
* list.removeFirst()
* list.removeLast()
* list.set(index,data)
* list.setFirst()
* list.setLast()
* list.get(index)
* list.getFirst()
* list.getLast()
*/
4.1 查: 链表的查找
链表的查找操作是比较繁琐的,一句话总结:从头开始一个一个找,直到找到为止,分解如下
第一步:拿到header节点;
//先定一个node作为标记(当前节点);
Node node = header;
第二步: 通过header的next指针找到下一个节点;
第三步:通过第二步拿到的节点,对比当前的index以及目标节点的index,对上了找到然后返回,反之继续获取下一个节点;
//假如index大于等于1,遍历依次将node标记向后移动,一直到index处;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
第四步:返回node中的data
//获取到对应索引的data
public Object get(int index) {
return getNode(index).data;
}
4.2 增: 链表的指定位置节点的插入(想一下,你想插队老鹰抓小鸡队伍里,你会怎么做?)
有了第一个get的API,这个就方便一些,步骤如下:
第一步:获取到指定index-1 处的node(nodeIndexFront)
第二步:获取到指定index 处的node(indexNode)
第三步:将第一步的nodeIndexFront的下一个节点设置成新插入的节点
第四步:将新插入的节点的下一个节点设置成第二步的indexNode
第五步:链表长度+1
代码上!!!
//在指定位置index处,添加data
public void add(int index, T data) {
//首先判断是否index是否超纲
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("IndexOutOfBoundsException;index超出数组长度");
}
//拿到在index处的node
Node nodeIndex = getNode(index);
//拿到在index - 1 处的node
Node nodeIndexFront = getNode(index - 1);
//将在index - 1 处的node 下一个节点设置为新增的node(nodeInsert)
Node nodeInsert = new Node(data);
nodeIndexFront.next = nodeInsert;
//将在index处的node(nodeInsert)下一个节点设置为原本index处的node
nodeInsert.next = nodeIndex;
//长度+1
length++;
}
4.3 改: 链表的指定位置节点的更改(可以直接看代码)
到这里,如果你已经理解了前面两个 增+查两个的实现的话,这里就很顺利成章了,步骤如下:
第一步:获取到指定index-1 处的node(nodeIndexFront)
第二步:获取到指定index+1 处的node(nodeIndexBack)
第三步:将第一步的nodeIndexFront下一个节点设置成新插入的节点;
第四步:将新插入的节点的下一个节点设置成第二步的nodeIndexBack
第五步:链表长度+1
撸代码:
//修改在index的data
//思路,将index-1处的next设置为新的node,然后
public void set(int index, T data) {
//首先判断是否超纲(这里可以提取出一个方法,判断index是否越界,越界返回true,否返回false)
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
//拿到在index+1处的node
Node nodeIndexBack = getNode(index + 1);
//拿到在index - 1 处的node
Node nodeIndexFront = getNode(index - 1);
//将在index - 1 处的node 下一个节点设置为新增的node(nodeInsert)
Node nodeInsert = new Node(data);
nodeIndexFront.next = nodeInsert;
//将在index 处的node 下一个节点设置为index+1处的node(nodeIndexBack)
nodeInsert.next = nodeIndexBack;
}
4.4 删:这里留个homework吧,自己手动试着实现一下,因为也不难,嘿嘿,皮这一下很开心
//大家可以试着动手实现一下,注意判断index是否越界
public void remove(int index) {
}
五 手撸单向链表代码(完整有点长):
public class MyList<T> {
private Node header;
private Node tail;
private int length = 0;
//无参构造,构造出头节点和尾节点,并且将头节点next指针指向尾节点(头节点不储存元素)
public MyList() {
header = new Node<T>();
tail = new Node<T>();
header.next = tail;
}
/**
* 新增
* @param data
*/
public void add(T data) {
//构造函数新增node节点
Node node = new Node(data);
//如果长度为0,直接在头结点加,并设为尾结点
if (length == 0) {
header.next = node;
//将node设为尾结点
tail = node;
length++;
return;
}
//如果长度不为0,将tail的下一个节点设置为node,
tail.next = node;
//将node带上tail的标签,变成尾结点
tail = node;
//别忘记增加长度
length++;
}
//在指定位置index处,添加data
public void add(int index, T data) {
//首先判断是否index是否超纲
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("IndexOutOfBoundsException;index超出数组长度");
}
//拿到在index处的node
Node nodeIndex = getNode(index);
//拿到在index - 1 处的node
Node nodeIndexFront = getNode(index - 1);
//将在index - 1 处的node 下一个节点设置为新增的node(nodeInsert)
Node nodeInsert = new Node(data);
nodeIndexFront.next = nodeInsert;
//将在index处的node(nodeInsert)下一个节点设置为原本index处的node
nodeInsert.next = nodeIndex;
//长度+1
length++;
}
//获取到对应索引的data
public Object get(int index) {
/*//return getNode(index).data;代码的分步骤
Node node = getNode(index);
Object data = node.data;
return data;*/
return getNode(index).data;
}
//获取对应位置的节点
private Node getNode(int index) {
//首先判断是否超纲
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
//先定一个node作为标记(当前节点);
Node node = header;
//假如index大于等于1,遍历依次将node标记向后移动,一直到index处;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
//修改在index的data
//思路,将index-1处的next设置为新的node,然后
public void set(int index, T data) {
//首先判断是否超纲(这里可以提取出一个方法,判断index是否越界,越界返回true,否返回false)
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
//拿到在index+1处的node
Node nodeIndexBack = getNode(index + 1);
//拿到在index - 1 处的node
Node nodeIndexFront = getNode(index - 1);
//将在index - 1 处的node 下一个节点设置为新增的node(nodeInsert)
Node nodeInsert = new Node(data);
nodeIndexFront.next = nodeInsert;
//将在index 处的node 下一个节点设置为index+1处的node(nodeIndexBack)
nodeInsert.next = nodeIndexBack;
}
//拿到length长度
public int size() {
return length;
}
//移除index处的data
//思路就是拿到index - 1和index+1处的node,然后让index+1成为 index-1处的next节点即可
//大家可以试着动手实现一下
public void remove(int index) {
}
/**
* 节点
*/
public static class Node<T> {
//存储文件,元素
private T data;
//存储下一个节点
private Node next;
public Node() {
this.data = null;
}
public Node(T data) {
this.data = data;
}
}
}
六 测试方法测试:
public class MyListTest {
//测试MyList
public static void main(String[] args) {
System.out.println("测试新建单向链表");
MyList<Integer> myList = new MyList<>();
System.out.println("myList.size() = " + myList.size());
//增加元素
System.out.println("测试单向链表增加元素");
myList.add(0);
myList.add(1);
myList.add(2);
System.out.println("测试单向链表size方法是否准确");
System.out.println("myList.size() = " + myList.size());
System.out.println("测试单向链表set方法和get方法是否准确");
myList.set(2, 9);
System.out.println("myList.get(2): " + myList.get(2));
System.out.println("测试单向链表测试指定位置add元素");
myList.add(2,8);
System.out.println("myList.get(2): " + myList.get(2));
System.out.println("myList.get(3): " + myList.get(3));
System.out.println("myList.size() = " + myList.size());
//最后留个位置给大家写remove的测试
//myList.remove();
}
}
END总结:多问为什么?多思考!多敲代码!不图快,只图啃下来!
完活!这次先到这,以后再慢慢手动实现一下二叉树红黑树啥的~
特此感谢<小灰的算法之旅>这本书,给了很多的启发和系统性的讲解!强烈推荐大家作为算法的入门进行学习;
周末快乐,下周再见,不说了,笨鸟要继续肝算法了~