面试官Q1:可以手写一个LinkedList的简单实现吗?
当听见手写一个具体类的实现的时候,是不是有点懵逼,其实在大多数面试中,要手写几率还是很小的,对一些工作了好几年的老油条,一般面试只是让你简单介绍一下LinkedList数据结构,但是对于应届毕业生,不管是单向链表还是双向链表,考到要手写的几率还是蛮大的,所以扎实的基本功还是必须的。废话不多说了,我们先来总结一下LinkedList有哪些特点:
- LinkedList是基于链表实现;
- 其存储数据放在节点里面;
- 插入和删除操作效率高,无需遍历数据,打断节点连接,重新连接即可;
- 遍历数据效率低,无法通过索引定位;
数据结构
LinkedList底层的数据结构是基于双向循环链表的,每个节点分为头部、尾部以及业务数据,前一个节点尾部指向后一个节点的头部,后一节点的头部指向前一个节点的尾部,对应的就是下面图示:
但是对于头结点和尾节点比较特殊,头结点的头部没有上一个结点,从上图可知并没有指向,尾节点的尾部也没有指向下一个结点。
所以,如上我们可以知道链表是由一个一个节点构成,我们是不是可以定义一个结点类Node,如下:
1//用来表示一个节点
2public class Node {
3 Node previous;//头部,用来指向上一个节点
4 Object obj;
5 Node next;//尾部,用来指向下一个节点
6
7 public Node() {
8 }
9
10 public Node(Node previous, Object obj, Node next) {
11 super();
12 this.previous = previous;
13 this.obj = obj;
14 this.next = next;
15 }
16
17 public Node getPrevious() {
18 return previous;
19 }
20
21 public void setPrevious(Node previous) {
22 this.previous = previous;
23 }
24
25 public Object getObj() {
26 return obj;
27 }
28
29 public void setObj(Object obj) {
30 this.obj = obj;
31 }
32
33 public Node getNext() {
34 return next;
35 }
36
37 public void setNext(Node next) {
38 this.next = next;
39 }
40}
添加数据add
既然结点已经有了,我们是不是考虑将这些结点一个个串起来,形成我们想要的链表结构,定义add方法如下:
1public class MyLinkedList {
2 private Node first;//定义一个头结点
3 private Node last;//定义一个尾节点
4 private int size;
5
6 //添加业务数据
7 public void add(Object obj){
8 //new一个结点出来
9 Node n = new Node();
10 //如果是一个新的链表,没有任何数据
11 if(first==null){
12 //从图示可知,这个时候新增的结点既是头结点也是尾节点,头结点的头部没有任何指向所以设置为null,
13 //尾节点的尾部没有任何指向,所以也为null,真正的业务数据放在obj属性里面
14 n.setPrevious(null);
15 n.setObj(obj);
16 n.setNext(null);
17
18 first = n;
19 last = n;
20 }else{
21 //这个时候我们添加下一个结点,直接往last节点后增加新的节点
22 n.setPrevious(last);
23 n.setObj(obj);
24 n.setNext(null);
25 //当前结点尾部要指向新添加进来的结点
26 last.setNext(n);
27 //此时,新加进来的结点就变成了尾节点
28 last = n;
29 }
30 size++;
31 }
32
33 public int size(){
34 return size;
35 }
36}
用一张图,描述上述add的过程,结点就这样被串起来了
我们简单测试一下,add方法是否成功。
查询数据get
当我们add完数据后,如果想获取某个下标对应的结点,这个时候又该如何操作呢?LinkedList不像ArrayList可以直接通过索引下标定位到具体的数据,链表是一个个结点组成,当我们想要获取第三个结点时,需要一个个的去遍历,从头开始找直到找到为止
对应代码如下:
1public Object get(int index){ //2
2 // 0 1 2 3 4
3 Node temp = node(index);
4 if(temp!=null){
5 return temp.obj;
6 }
7 return null;
8 }
9
10 public Node node(int index){
11 Node temp = null;
12 if(first!=null){
13 temp = first;
14 for(int i=0;i<index;i++){
15 temp = temp.next;
16 }
17 }
18 return temp;
19 }
可想而知,如果这个链表数据量很大,这种方式去遍历效率该有多低,当然这个不是我们这节的重点,笔者只是想通过简单的方式,来阐述一下链表的实现原理。
删除数据remove
查询完数据后,现在我们来删数据,链表删数据效率是很高的,为什么效率高,看完下面这张图你就理解了:
只需要打断原有的指向关系,重新连接指向,就可以删除指定位置处的数据,非常的高效。如果是ArrayList删除数据,上面2节点,将会被后面的3节点取代,它要移位,后面的所有节点都要跟着移位,所以ArrayList效率比较低。
用一段代码,描述上述过程:
1public void remove(int index){
2 Node temp = node(index);
3
4 if(temp!=null){
5 Node up = temp.previous;
6 Node down = temp.next;
7 up.next = down;
8 down.previous = up;
9 size--;
10 }
11
12 }
指定位置添加数据
如果在指定位置添加一条数据,又该如何实现呢?
1public void add(int index,Object obj){
2 Node temp = node(index);
3
4 Node newNode = new Node();
5 newNode.obj = obj;
6
7 if(temp!=null){
8 Node up = temp.previous;
9 up.next = newNode;
10 newNode.previous = up;
11
12 newNode.next = temp;
13 temp.previous = newNode;
14
15 size++;
16 }
17 }
跟上面删除节点一个原理,打断原有指向,重新指向,数据就插入进去了。如下图:
本文通过简单的代码以及图示,讲解了LinkedList的实现原理,当然JDK源码里面的LinkedList实现方式可没有这个这么简单,大家如果有兴趣可以去看一下源码,源码提供了各种使用方式。如果面试遇到手写LinkedList实现,你会了吗?