背景

在看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,但是我们采用头插法插入后,遍历节点取出数据,发现数据恰恰是相反的顺序,这是什么原因呢?
image.png


现在就让我们通过画图来更明确地展示整个头插法的流程,来回答上面这个问题。

if (head == null) {
    // 头结点不存在时,新节点赋值成头结点
    head = newNode;
} 

这段代码,我们可以看出,当头结点不存在时,我们会把插入的这个节点作为头结点。如下图所示:
image.png

// 存在头结点,将新节点的下一节点设置成头结点
// 也就是说在头结点前插入新节点
newNode.next = head;
// 将新节点变成头结点
// 与上一步相呼应,移动链表
head = newNode;

头结点存在时,我们就需要将新节点的下一节点设置成头结点。如下图所示:
image.png
后面新插入的节点,不断地重复这一步,直到没有新节点为止,最终的单链表结构就如下所示:
image.png
这就是单链表的头插法,新节点总是在链表的头部。


尾插法

尾插法,也比较好理解,就是每一个新节点都是插入到链表的尾部,有点类似于队列的先进先出。下面来看看尾插法的实现:

/**
 * 生成指定数量的节点链表-尾插法
 *
 * @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 方法,发现新节点的插入顺序是和输出顺序一样的,也就是说每一个新节点都是插入上一节点的尾部。
image.png
下面,我们来根据代码,一步步地画图来看看这是如何实现的,
第一步和头插法是一样的,我们就不细看了,主要是看下面的操作。

if (temp == null) {
    // 头结点不存在时,新节点赋值成头结点
    // 这一步结合下面的temp = newNode,两者此时都指向同一节点,
    // 所以后续对temp 节点的操作其实就是对与head 的这一节点操作。
    head = newNode;
} else {
    // 存在头结点,将新节点设置为下一节点
    // 也就是说在头结点尾部插入新节点
    temp.next = newNode;
}
// 临时节点重新赋值,移动到下一节点
temp = newNode;

在头结点存在的情况下,新插入一个节点,我们需要将新节点设置为下一节点,并且最终要移动临时节点指向下一节点。
image.png
对于后续的节点插入,我们只需要不断地重复这一操作即可,temp 临时节点在没插入一个新节点后,都需要重新指向这个新节点,为下一节点的插入做准备。最终我们的链表就是如下所示的情况:
image.png
这就是单链表的尾插法,其头结点就是我们的第一个节点,后续的节点都是插入上一节点的尾部,最终的输出顺序就是我们输入的节点顺序。


以上就是单链表插入的两种方法,希望对大家能有所帮助,能够更好地理解头插法和尾插法。


lxKLj
0 声望0 粉丝

学无止境,笨鸟先飞