彻底搞懂链表是如何工作的

链表是一种用于存储元素集合的线性数据结构。与数组不同,链表使用节点来存储元素,这些元素并非存储在连续的内存位置中。在本文中,你将了解什么是链表、它们如何工作以及如何构建一个链表。

什么是链表

链表是节点的集合,其中每个节点都包含数据以及列表中下一个节点的内存地址。

链表是一种常见的数据结构,它由一系列节点组成。每个节点包含两部分:数据部分和指针部分。数据部分用于存储节点的数据,指针部分用于存储下一个节点的地址(在单链表中),通过这些指针将各个节点连接起来。

你可以看到节点的地址不一定是连续的。第一个节点的地址是 200,第二个节点的地址是 801,而不是 201。

链表中的节点

节点是链表的最小组成单位。链表中的一个节点由两部分组成:表示节点值的数据;指向下一个节点的引用(next)。

链表中的头节点和尾节点

链表的第一个节点称为头节点。它是链表的起点。最后一个节点称为尾节点。由于最后一个节点之后没有节点,所以最后一个节点总是指向 null。next 指针不指向任何内存位置。

如何创建链表

此时,你应该对链表的工作原理及其结构有了基本的了解。让我们按照以下步骤创建一个链表:

  1. 创建节点。

  2. 连接节点。

  3. 添加节点。

  4. 插入节点。

  5. 删除节点。

如何创建节点

如你所知,一个节点由两部分组成:数据和指向下一个节点的地址。

创建一个名为 Node 的类来表示链表中的一个节点:

kotlin 复制代码
class Node {
    int data;
    Node next;

    Node(int data) {
        this.data = data;
        this.next = null;
    }
}

它有两个实例变量:data(保存节点中存储的数据)和 next(保存对列表中下一个节点的引用)。构造函数接受一个 int 参数 data 来初始化 data 变量,并默认将 next 变量设置为 null。

现在可以通过创建 Node 类的实例来简单地创建节点并向其中添加数据:

ini 复制代码
// 创建节点
Node node1 = new Node(11);
Node node2 = new Node(18);
Node node3 = new Node(24);

在上面的代码中,我们创建了三个节点。

如何连接节点

创建节点后,你必须连接它们以形成一个链表。为此,你首先需要创建一个带有头节点的链表。

kotlin 复制代码
class LinkedList {
    Node head;

    LinkedList() {
        this.head = null;
    }
}

最初,头节点设置为 null,因为链表中还没有节点。现在要在链表中添加节点,你可以先将头节点设置为列表中的第一个节点,在这种情况下是 node1。

ini 复制代码
head = node1;

然后使 node1 的 next 指向 node2,node2 的 next 指向 node3。即:

ini 复制代码
node1.next = node2;
node2.next = node3;

你已成功创建了一个链表并连接了节点。

如何向链表添加节点

添加节点意味着在链表的末尾添加一个节点。添加节点时有两种情况需要考虑:

  1. 向空链表添加节点。

  2. 向非空链表添加节点。

如何向空链表添加节点

如果链表中没有节点,则它是空链表。你可以通过检查头节点是否为 null 来做到这一点。如果头节点为 null,则可以简单地将 head 设置为新节点:

ini 复制代码
if (head == null) {
    head = newNode;
}

如何向非空链表添加节点

如果链表中有一个或多个节点,则它是非空链表。

要向非空链表添加节点,使最后一个节点指向新节点。与数组不同,我们不能直接访问链表中的任意元素。我们必须从头节点遍历到最后一个节点。可以一个临时指针(你可以将指针称为 current),使其指向头节点。

然后使 current 指向其下一个节点,直到 current 节点的 next 指向 null。

当 current 的下一个节点为 null 时,你可以使 current 节点的 next 指向新节点。

ini 复制代码
while (current.next!= null) {
    current = current.next;
}
current.next = newNode;

如何在链表中插入节点

插入节点意味着在给定索引处添加一个节点。插入节点时有两种情况需要考虑:

  1. 在第一个索引处插入节点。

  2. 在给定索引处插入节点。

如何在第一个索引处插入节点

将节点的 next 指向头节点。

![](/Users/gaozhengqi/Library/Application Support/typora-user-images/image-20241201144956130.png)

将头节点设置为新节点。

ini 复制代码
newNode.next = head;
head = newNode;

如何在任意位置插入节点

假设你要在上面的链表中的索引 2 处添加一个节点。要在索引 2 处插入节点,你必须遍历索引 2 之前的节点。

接下来,创建一个新节点并使新节点的 next 指向 current 节点的 next。然后使 current 的 next 指向新节点。

ini 复制代码
for (int i = 0; i < index - 1 && current!= null; i++) {
    current = current.next;
}
if (current!= null) {
    newNode.next = current.next;
    current.next = newNode;
}

如何在链表中删除节点

在链表中删除节点有两种方法:

  1. 删除头节点。

  2. 删除给定位置的节点。

如何删除头节点

删除链表的头节点很简单。如果以后需要访问头节点的数据,可以将其存储在一个临时变量中。然后将头指针设置为指向头节点之后的下一个节点。

ini 复制代码
deletedValue = head.data;
head = head.next;

如何删除给定位置的节点

假设你要删除下面图表中索引 2 处的节点。

你可以通过使索引 1 处的节点指向索引 3 处的节点来删除索引 2 处的节点。要删除一个节点,你必须访问要删除的节点及其之前的节点。使用两个临时指针(你可以将指针称为 previous 和 current)。让 previous 指向 null,current 指向头节点。现在,将 current 向前移动一步,并将 previous 移动到 current,直到到达索引 2。

使 previous 的 next 指向 current 节点的 next。然后将 current 的数据存储在一个变量中以供将来使用。在删除指向索引 2 处节点的引用后,通过链表中的任何引用都无法再访问该节点。

需要注意的是,从链表中删除节点时,不需要显式删除给定索引处的节点本身。这是因为当节点不再通过任何引用可达时,垃圾回收器将自动处理被删除的节点。然而,在像 C 或 C++ 这样没有自动垃圾回收的语言中,当节点不再需要时,你需要手动删除它以避免内存泄漏。

ini 复制代码
for (int i = 0; i < index && current!= null; i++) {
    previous = current;
    current = current.next;
}
if (current!= null) {
    deletedValue = current.data;
    previous.next = current.next;
}

完整的链表代码

下面的代码展示了一个完整的链表。你可以创建、添加、插入、删除节点以及打印所有节点:

ini 复制代码
class Node {
    int data;
    Node next;

    Node(int data) {
        this.data = data;
        this.next = null;
    }
}

class LinkedList {
    Node head;

    LinkedList() {
        this.head = null;
    }

    public void createLinkedList() {
        Node node1 = new Node(11);
        this.head = node1;
        Node node2 = new Node(18);
        node1.next = node2;
        Node node3 = new Node(24);
        node2.next = node3;
    }

    public void append(Node newNode) {
        Node current = this.head;
        if (current == null) {
            this.head = newNode;
        } else {
            while (current.next!= null) {
                current = current.next;
            }
            current.next = newNode;
        }
    }

    public void insert(Node newNode, int index) {
        Node current = this.head;
        if (index == 0) {
            newNode.next = current;
            this.head = newNode;
        } else {
            for (int i = 0; i < index - 1 && current!= null; i++) {
                current = current.next;
            }
            if (current!= null) {
                newNode.next = current.next;
                current.next = newNode;
            }
        }
    }

    public int delete(int index) {
        Node current = this.head;
        Node previous = null;
        int deletedValue = -1;
        if (index == 0) {
            deletedValue = this.head.data;
            this.head = this.head.next;
            return deletedValue;
        } else {
            for (int i = 0; i < index && current!= null; i++) {
                previous = current;
                current = current.next;
            }
            if (current!= null) {
                deletedValue = current.data;
                previous.next = current.next;
            }
            return deletedValue;
        }
    }

    public void displayLinkedList() {
        Node current = this.head;
        while (current!= null) {
            System.out.println(current.data);
            current = current.next;
        }
    }
}

class Main {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();
        Node newNode1 = new Node(22);
        Node newNode2 = new Node(43);
        Node newNode3 = new Node(5);

        list.createLinkedList();
        list.append(newNode1);
        list.insert(newNode2, 0);
        list.insert(newNode3, 2);
        list.delete(2);
        list.displayLinkedList();
    }
}

总结

链表数据结构可用于各种应用程序,如网页浏览器和音乐播放器。例如,在网页浏览器中,浏览器历史记录可以存储为链表。每个访问的页面可以由一个节点表示,每个节点指向访问的下一个页面。通过简单地遍历链表,就完成了历史页面的跳转。

同样,在音乐播放器中,播放列表可以表示为链表。每首歌曲可以由一个节点表示,每个节点指向播放列表中的下一首歌曲。通过简单地遍历链表,就可以实现播放列表的功能了。

在实际编程中,手动构建链表的情况较为少见,毕竟多数编程语言都已内置链表。不过,亲自创建链表并理解其实现逻辑,会使我们对数据结构的认知得到显著提升。在实际应用场景里,我们便能更加准确地判别何时适宜采用链表而非其他数据结构。

相关推荐
漂流瓶66666622 分钟前
Scala的模式匹配变量类型
开发语言·后端·scala
夏天吃哈密瓜27 分钟前
Scala中的正则表达式01
大数据·开发语言·后端·正则表达式·scala
2401_8337880529 分钟前
Scala的模式匹配(2)
java·开发语言
叁散1 小时前
PTA--数据结构预习报告: 考试排名汇总
数据结构
悠悠龙龙2 小时前
框架模块说明 #05 权限管理_03
java·开发语言·spring
开心羊咩咩3 小时前
Idea 2024.3 突然出现点击run 运行没有反应,且没有任何提示。
java·ide·intellij-idea
waterme1onY3 小时前
IDEA中MAVEN的一些设置问题
java·maven·intellij-idea
阿华的代码王国3 小时前
【算法】——前缀和(矩阵区域和详解,文末附)
java·开发语言·算法·前缀和
是老余3 小时前
算法基础之链表:移除链表元素leetcode203
数据结构·算法·链表
梦.清..3 小时前
面向对象(二)——类和对象(上)
java