LinkedList容器
LinkedList底层用双向链表实现的存储。
特点:查询效率低,增删效率高,线程不安全。 在频繁使用增删的情况下,一把使用LinkedList。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
- 容器中所有的比较操作都用equals方法,一般不用==实现。
- 上面说的指针实质上就是对象,这个对象存储的是节点的地址。
下面是LinkedList的双链表存储结构图:
往往链表的第一个节点是不放数据的,只存储两个指向数据的指针,且头指针必须存在。
下面根据程序分析LinkedList:
package mylinkedlist;
/**
* 测试 LinkedList 双向链表的使用
*
* @author 发达的范
* @date 2020/11/09 21:45
*/
public class TestLinkedList01 {
private Node First;//头指针
private Node Last;//尾指针
private int size;
public void add(Object object) {
Node node = new Node(object);//创建一个新的链表节点
if (First == null) {
//如果是第一个节点
node.Previous = null;
node.Next = null;
First = node;//头指针指向新添加这个节点
Last = node;//尾指针指向新添加这个的节点
} else {
//如果不是第一个节点
node.Previous = Last;//新添加的Previous的头指向上一个节点
node.Next = null;
Last.Next = node;//此处有疑问!!!
Last = node;//尾指针指向这个新添加的节点
First.Previous = Last;//头尾相连
//Last.Next = First;
}
size++;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node temp = First;
while (temp != null) {
stringBuilder.append(temp.element + ",");
temp = temp.Next;
}
stringBuilder.setCharAt(stringBuilder.length() - 1, ']');
return stringBuilder.toString();
}
public static void main(String[] args) {
TestLinkedList01 testLinkedList01 = new TestLinkedList01();
testLinkedList01.add("fa");
testLinkedList01.add("da");
testLinkedList01.add("de");
testLinkedList01.add("fan");
System.out.println(testLinkedList01);//此处有疑问
}
}
package mylinkedlist;
/**
* @author 发达的范
* @date 2020/11/09 21:48
* @description
*/
public class Node {
Node Previous;
Node Next;
Object element;
public Node(Node Previous, Node nodeNest, Object object) {
this.Previous = Previous;
this.Next = nodeNest;
this.element = object;
}
public Node(Object object) {
this.element = object;
}
}
运行结果:
虽然运行成功,但是我还有一些问题
-
问题1:上面TestLinkedList01类中的第24行代码,Last.Next = node;这句的功能是把新节点和原来链表的尾部节点连接起来,但是问题就出在,当添加第一个节点的时候,执行了Last = node;语句,此时Last指向第一个节点,而并不是等于第一个节点本身,在添加第二个节点的时候,没有让第一个节点的Next指向下一个节点,却让这个指针Last的Next指向下一个节点,这样能把这个链表连接起来吗?
问题1 我的猜想是指针Last指向的地址是第一个节点,指针Last的Next在计算机的内存中就是第一个节点的的Next指针,所以才能连接起来。
扫描二维码关注公众号,回复: 13120449 查看本文章 -
问题2:输出链表全部内容时,没有调用toString()方法也能输出,这是为什么?
我猜想的是这可能是个线程的方法,但是没有证据(手动狗头)。
带着这两个问题,我开始Debug
- 我在这两处设置两个断点,如下:
- 点击小虫子,得到下面的对象的值:
从上面的对象的值可见,再添加第一个节点时,第一个节点First指针指向的地址是Node@488,最后一个节点Last指针指向的地址也是Node@488,由此可见第一个节点的内存地址是Node@488。
- 继续执行:
重点来了,更新尾指针Last前,此时Last指针指向的地址仍是第一个节点的地址Node@488,而Last.Next指针指向的地址是Node@497,正是第二个节点的地址,竟然连起来了,也就是说我的猜想是正确的。
- 继续运行:
更新尾指针Last后,看见First.Previous指针指向的地址已经变成了Node@497,正是尾部节点(第二个节点)的地址,Last.Next指针指向的地址已经变成了Node@488,正是第一个节点的地址,但是现在并不能完全说明已经首尾相连了,因为此时只有两个节点,下面添加第三个节点。
继续运行:
可见,更新尾指针Last前的Last.Next指针指向的地址是Node@510,说明第三个节点连接上了。
更新尾指针Last后,看见,此时First.Previous指针指向的地址已经变成了Node@510,正是尾部节点(第三个节点)的地址,Last.Next指针指向的地址已经变成了Node@488,正是第一个节点的地址,证明这个链表已经首尾相连,且连接正常。
这部分使用设置断点的方法来查看内存中链表是否正确创建的过程,阅读起来可能会有点繁琐,但是有助于初学者从内存中理解链表的存储过程,对双向链表有更深刻的理解。
2020/11/11 15:00更新 :
关于add()方法中添加节点时最后有一个首尾相连的操作的补充:
注:可以把头指针指向尾部节点,但是不能把尾指针指向头部节点。因为一旦形成一个环,使用toString()方法时就会报错:
这是堆栈溢出的错误,这正是把链表连接成了一个环导致的,因为toString()方法里面的while循环没有终止条件,所以就一直向堆栈中放置数据,最终导致堆栈溢出。
解决方法:删除Last.Next = First;即可。
2020/11/11 21:58更新 :
关于LinkedList实现的全部代码我已经push到个人github中了,您如果有需要可以自取,地址是:
https://github.com/fanfada/Testing-the-LinkedList-of-Java.git
当自己做出来的时候,回头看看,觉得链表也不是很难。下面说几个注意点:
- 重复使用的部分封装成一个方法,减少代码的冗余度。
- 使用泛型规范化容器的使用。
- 删除/添加节点之后节点数量要进行相应的改变。
- 在链表中插入节点不同于直接添加节点,可以在头部和中间进行插入,在结尾插入就显得没必要了,直接添加add()即可。
- 善用Debug!