单链表
单链表的结构
我们上次研究了线性表的顺序存储,但我们知道存储结构分为顺序存储、链式存储、索引存储、散列存储。在这里,我们就继续研究线性表的链式存储,也就是单链表。
在我们先定义出一个链式存储的基本单位——结点:data_next,我们的链表就是由这些结点一个个组成的。其中,data我们叫做数据域,来存放数据元素,next我们叫做指针域,存放后继结点的地址。
#include <stdio.h>
// 定义链表节点结构体
typedef struct LNode {
int data; // 数据域
struct LNode *next; // 指针域,指向下一个节点
} LNode, *LinkList; // 类型别名,LNode表示节点类型,LinkList表示链表类型
现在,我们链表定义好了,但我们发现,目前并没有指向第一个结点的指针。这时候我们就需要一个放在最前面的结点了。我们在单链表的第一个结点之前加一个结点,就是头结点,且不设置data域的任何信息。
此处我们在说明一个概念:头指针,不管带不带头结点,头指针始终指向链表的第一个结点,也就是图中L后面那个“”;而头结点是带头结点链表的第一个结点,结点内通常不存储信息。
单链表的实现
按序号查找
这个算法和我们的顺序表操作类似,我们顺着next指针域逐个查找,直到查找到第i个结点为止,否则返回最后一个指针域NULL。我在代码的重要部分加入了注释,供大家参考。
LNode *GetElem(LinkList L, int i) {
int j = 1;
LNode *p = L->next; // p指向第一个有效节点
if (i == 0)
return L; // 返回头节点
if (i < 0)
return NULL; // i无效时返回NULL
while (p && j < i) {
p = p->next;
j++;
}
return p; // 返回第i个元素的指针,如果i大于链表长度,这里p为NULL
}
按值查找
按值查找的函数目标是在链表中查找第一个数据域等于给定值的结点,并返回该节点的指针。通过遍历链表,逐个检查每个节点的数据域是否等于指定的值来实现。我们这个函数从链表的第一个有效节点开始遍历(即跳过头节点)。
LNode *LocateElem(LinkList L, int e) {
LNode *p = L->next; // p指向第一个有效节点
// 遍历链表
while (p != NULL) {
if (p->data == e) {
return p; // 找到了,返回当前节点的指针
}
p = p->next; // 移动到下一个节点
}
return NULL; // 遍历结束,未找到,返回NULL
}
插入结点
好的,接下来,轮到插入结点的操作了。这需要我们使用前面提到的按序号查找函数,接下来我们按照以下步骤就可以完成操作。
查找位置:首先需要找到第 i-1
个节点,因为插入操作需要修改这个节点的 next
指针。
插入节点:创建一个新的节点,将其插入到链表的第 i
个位置。
移动指针:将新节点的next指向p的下一个节点,p的next现在指向新节点。
// 在单链表L的第i个位置插入值为e的新节点
// 成功插入返回1,插入失败(如位置不合法)返回0
int ListInsert(LinkList *L, int i, int e) {
LNode *p = *L; // p指向头节点
int j = 0;
// 寻找第i-1个节点
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
// 检查i的合法性(p为NULL表示未找到,或者i<1表示位置不合法)
if (p == NULL || i < 1) {
return 0; // 插入失败
}
// 创建新节点
LNode *newNode = (LNode *)malloc(sizeof(LNode));
if (newNode == NULL) { // 内存分配失败
return 0; // 插入失败
}
newNode->data = e; // 设置新节点的数据域
// 插入新节点
newNode->next = p->next; // 新节点的next指向p的下一个节点
p->next = newNode; // p的next现在指向新节点
return 1; // 插入成功
}
删除操作
删除结点的操作通常涉及到找到目标结点的前一个结点,然后调整该节点的 next
指针以跳过目标结点。最后值得注意的是,我们要使用free函数释放目标结点的内存。
// 在单链表L中删除第i个元素
// 成功删除返回1,失败(如位置不合法)返回0
int ListDelete(LinkList *L, int i) {
LNode *p = *L; // p指向头节点
int j = 0;
// 寻找第i-1个节点
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
// 检查i的合法性(p为NULL表示未找到,p的next为NULL表示i位置无有效节点)
if (p == NULL || p->next == NULL || i < 1) {
return 0; // 删除失败
}
// 删除操作
LNode *temp = p->next; // temp指向要删除的节点
p->next = temp->next; // 跳过要删除的节点
free(temp); // 释放节点内存
return 1; // 删除成功
}
使用Python语言的链表及其操作的复现
我们都知道,Python是一种可以面向对象的语言,语法相比之下很简明易懂。回顾我们的链表建立过程,称得上“对象”的有微观的“小结点”和整体的“大链表”。在Python编程中,链表通过定义一个节点类和一个链表类来实现。接下来让我们使用Python熟悉面向对象编程,复现我们在C中的链表操作吧!
首先,我们定义一个结点类 Node
,它有两个属性:
data
存储节点数据;
next
存储指向下一个节点的引用。
class Node:
def __init__(self, data):
self.data = data
self.next = None
接着,我们定义一个链表类 LinkedList:
class LinkedList:
def __init__(self):
self.head = None
def insert(self, index, data):
new_node = Node(data)
if index == 0:
new_node.next = self.head
self.head = new_node
return
prev_node = self.get_node(index - 1)
if prev_node is None:
print("Position out of bounds")
return
new_node.next = prev_node.next
prev_node.next = new_node
def delete(self, index):
if index == 0:
if self.head:
self.head = self.head.next
else:
print("List is empty")
return
prev_node = self.get_node(index - 1)
if prev_node is None or prev_node.next is None:
print("Position out of bounds")
return
prev_node.next = prev_node.next.next
def get_node(self, index):
current = self.head
for i in range(index):
if current is None:
return None
current = current.next
return current
def find(self, data):
current = self.head
index = 0
while current:
if current.data == data:
return index
current = current.next
index += 1
return -1
在类(class) “LinkedList”的内部,我们分别写出了LinkedList的构造函数(头结点设为None
)、insert方法、delete方法、get_node方法、find方法。由于篇幅限制,我们这里就不写具体的注释了,对于他们的理解和C语言代码类似。
双链表
双链表并不是结点有两个的链表,而是链表的每一个结点都有两个指针prior和next,分别指向前驱结点和后继结点,所以双链表也称双向链表。
双链表的插入操作、删除操作和单链表大同小异;而查找操作更是基本类似,所以我们在这里简单地用Python举个例子。
class Node:
def __init__(self, data):
self.data = data # 节点存储的数据
self.next = None # 指向下一个节点的指针
self.prior = None # 指向前一个节点的指针
class DoubleLinkedList:
def __init__(self):
self.head = None
# 插入操作
def insert(self, data):
new_node = Node(data)
if self.head is None: # 如果链表为空,新节点成为头节点
self.head = new_node
return
last = self.head
while last.next: # 遍历到链表的末尾
last = last.next
last.next = new_node # 将新节点插入到链表末尾
new_node.prior = last # 设置新节点的前指针
# 删除操作
def delete(self, key):
temp = self.head
if temp is not None:
if temp.data == key:
self.head = temp.next # 将头节点向后移动一位
if self.head: # 如果链表不为空,更新新头节点的前指针
self.head.prior = None
return
while temp is not None:
if temp.data == key:
break
prev = temp
temp = temp.next
循环链表
无论是在单链表还是在双链表中,我们发现它们的首尾是断裂的。我们可以尝试把它们的首尾相连起来,形成“循环链表”这类链式存储结构。我们在对它们操作的时候区别仅仅是判满条件(循环条件)的不同,不再是p或者p->next 是否为空,而是是否等于头指针。