背景
在看HashMap 的源码时,发现扩容操作中对于链表的复制操作,不是很能理解,代码如下所示:
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
之后再网上查了一下,发现这是一个典型的单链表的尾插法操作,后续对单链表的头插法也进行了相关了解,整理了这片文章,与大家一起分享一下。
头插法
顾名思义,头插法就是在单链表的节点插入操作中,新的节点总是在前面,结果有点类似栈的先进后出。下面是一段头插法的代码实现:
public class Node {
/**
* 节点存的内容
*/
private Integer value;
/**
* 链表的下一个节点
*/
private Node next;
/**
* 用于记录所有生成的值
* 比较头插法 和 尾插法对于数据的顺序影响
*/
public static Integer[] valueArray;
/**
* 生成指定数量的节点链表
*
* @param count 链表节点数
* @return 链表节点
*/
public static Node generateLinkedNode(int count) {
Node head = null;
// 初始化记录数组
valueArray = new Integer[count];
for (int i = 0; i < count; i++) {
// 新节点
Node newNode = new Node();
// 随机一个值
Integer value = new Random().nextInt(100) + 1;
// 新节点赋值
newNode.value = value;
// 记录节点存的值
valueArray[i] = value;
if (head == null) {
// 头结点不存在时,新节点赋值成头结点
head = newNode;
} else {
// 存在头结点,将新节点的下一节点设置成头结点
// 也就是说在头结点前插入新节点
newNode.next = head;
// 将新节点变成头结点
// 与上一步相呼应,移动链表
head = newNode;
}
}
return head;
}
public Integer getValue() {
return value;
}
public Node getNext() {
return next;
}
}
public static void main(String[] args) {
Node linkedNode = Node.generateLinkedNode(5);
System.out.println(Arrays.toString(Node.valueArray));
do {
if (linkedNode.getNext() == null) {
System.out.print(linkedNode.getValue());
} else {
System.out.print(linkedNode.getValue() + ", ");
}
} while ((linkedNode = linkedNode.getNext()) != null);
}
结果如下所示,我们可以看到,数据的生成的顺序是63,78,85,62,60,但是我们采用头插法插入后,遍历节点取出数据,发现数据恰恰是相反的顺序,这是什么原因呢?
现在就让我们通过画图来更明确地展示整个头插法的流程,来回答上面这个问题。
if (head == null) {
// 头结点不存在时,新节点赋值成头结点
head = newNode;
}
这段代码,我们可以看出,当头结点不存在时,我们会把插入的这个节点作为头结点。如下图所示:
// 存在头结点,将新节点的下一节点设置成头结点
// 也就是说在头结点前插入新节点
newNode.next = head;
// 将新节点变成头结点
// 与上一步相呼应,移动链表
head = newNode;
头结点存在时,我们就需要将新节点的下一节点设置成头结点。如下图所示:
后面新插入的节点,不断地重复这一步,直到没有新节点为止,最终的单链表结构就如下所示:
这就是单链表的头插法,新节点总是在链表的头部。
尾插法
尾插法,也比较好理解,就是每一个新节点都是插入到链表的尾部,有点类似于队列的先进先出。下面来看看尾插法的实现:
/**
* 生成指定数量的节点链表-尾插法
*
* @param count 链表节点数
* @return 链表节点
*/
public static Node generateTailLinkedNode(int count) {
// 头结点不能移动,所以需要一个临时节点来进行操作
Node head = null, temp = null;
// 初始化记录数组
valueArray = new Integer[count];
for (int i = 0; i < count; i++) {
// 新节点
Node newNode = new Node();
// 随机一个值
Integer value = new Random().nextInt(100) + 1;
// 新节点赋值
newNode.value = value;
// 记录节点存的值
valueArray[i] = value;
if (temp == null) {
// 头结点不存在时,新节点赋值成头结点
// 这一步结合下面的temp = newNode,两者此时都指向同一节点,
// 所以后续对temp 节点的操作其实就是对与head 的这一节点操作。
head = newNode;
} else {
// 存在头结点,将新节点设置为下一节点
// 也就是说在头结点尾部插入新节点
temp.next = newNode;
}
// 临时节点重新赋值,移动到下一节点
temp = newNode;
}
return head;
}
我们运行同样的main 方法,发现新节点的插入顺序是和输出顺序一样的,也就是说每一个新节点都是插入上一节点的尾部。
下面,我们来根据代码,一步步地画图来看看这是如何实现的,
第一步和头插法是一样的,我们就不细看了,主要是看下面的操作。
if (temp == null) {
// 头结点不存在时,新节点赋值成头结点
// 这一步结合下面的temp = newNode,两者此时都指向同一节点,
// 所以后续对temp 节点的操作其实就是对与head 的这一节点操作。
head = newNode;
} else {
// 存在头结点,将新节点设置为下一节点
// 也就是说在头结点尾部插入新节点
temp.next = newNode;
}
// 临时节点重新赋值,移动到下一节点
temp = newNode;
在头结点存在的情况下,新插入一个节点,我们需要将新节点设置为下一节点,并且最终要移动临时节点指向下一节点。
对于后续的节点插入,我们只需要不断地重复这一操作即可,temp 临时节点在没插入一个新节点后,都需要重新指向这个新节点,为下一节点的插入做准备。最终我们的链表就是如下所示的情况:
这就是单链表的尾插法,其头结点就是我们的第一个节点,后续的节点都是插入上一节点的尾部,最终的输出顺序就是我们输入的节点顺序。
以上就是单链表插入的两种方法,希望对大家能有所帮助,能够更好地理解头插法和尾插法。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。