链表是一种用于存储元素集合的线性数据结构。与数组不同,链表使用节点来存储元素,这些元素并非存储在连续的内存位置中。在本文中,你将了解什么是链表、它们如何工作以及如何构建一个链表。
什么是链表
链表是节点的集合,其中每个节点都包含数据以及列表中下一个节点的内存地址。
链表是一种常见的数据结构,它由一系列节点组成。每个节点包含两部分:数据部分和指针部分。数据部分用于存储节点的数据,指针部分用于存储下一个节点的地址(在单链表中),通过这些指针将各个节点连接起来。
你可以看到节点的地址不一定是连续的。第一个节点的地址是 200,第二个节点的地址是 801,而不是 201。
链表中的节点
节点是链表的最小组成单位。链表中的一个节点由两部分组成:表示节点值的数据;指向下一个节点的引用(next)。
链表中的头节点和尾节点
链表的第一个节点称为头节点。它是链表的起点。最后一个节点称为尾节点。由于最后一个节点之后没有节点,所以最后一个节点总是指向 null。next 指针不指向任何内存位置。
如何创建链表
此时,你应该对链表的工作原理及其结构有了基本的了解。让我们按照以下步骤创建一个链表:
-
创建节点。
-
连接节点。
-
添加节点。
-
插入节点。
-
删除节点。
如何创建节点
如你所知,一个节点由两部分组成:数据和指向下一个节点的地址。
创建一个名为 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;
你已成功创建了一个链表并连接了节点。
如何向链表添加节点
添加节点意味着在链表的末尾添加一个节点。添加节点时有两种情况需要考虑:
-
向空链表添加节点。
-
向非空链表添加节点。
如何向空链表添加节点
如果链表中没有节点,则它是空链表。你可以通过检查头节点是否为 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;
如何在链表中插入节点
插入节点意味着在给定索引处添加一个节点。插入节点时有两种情况需要考虑:
-
在第一个索引处插入节点。
-
在给定索引处插入节点。
如何在第一个索引处插入节点
将节点的 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;
}
如何在链表中删除节点
在链表中删除节点有两种方法:
-
删除头节点。
-
删除给定位置的节点。
如何删除头节点
删除链表的头节点很简单。如果以后需要访问头节点的数据,可以将其存储在一个临时变量中。然后将头指针设置为指向头节点之后的下一个节点。
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();
}
}
总结
链表数据结构可用于各种应用程序,如网页浏览器和音乐播放器。例如,在网页浏览器中,浏览器历史记录可以存储为链表。每个访问的页面可以由一个节点表示,每个节点指向访问的下一个页面。通过简单地遍历链表,就完成了历史页面的跳转。
同样,在音乐播放器中,播放列表可以表示为链表。每首歌曲可以由一个节点表示,每个节点指向播放列表中的下一首歌曲。通过简单地遍历链表,就可以实现播放列表的功能了。
在实际编程中,手动构建链表的情况较为少见,毕竟多数编程语言都已内置链表。不过,亲自创建链表并理解其实现逻辑,会使我们对数据结构的认知得到显著提升。在实际应用场景里,我们便能更加准确地判别何时适宜采用链表而非其他数据结构。