链式表示相比于顺序表示是牺牲空间效率换取时间效率。
在顺序表中,逻辑上相邻的元素对应的存储位置也相邻,所以插入删除等操作是非常费时的操作。而在链表中,通过指针相连接,结点的存储位置可以任意安排,所以在执行上述操作时只需要修改相关节点的指针域就可以了。
目录
1.1 线性链表
线性链表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素。
存储数据元素信息的域叫做数据域,存储直接后继存储位置的域叫做指针域,这两部分信息组成数据的存储映像,称为结点。指针域中存储的信息叫做指针或链。
每一个结点只包含一个指针域的称为线性链表或单链表,用C语言对单链表进行表示:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
我们在单链表的第一个结点之前附设一个结点,称之为头结点。头结点的数据域可以不存储任何信息,头指针的指针域指向第一个节点的指针。头节点并不存储实际的数据,它只是用来管理链表的结构。在某些特殊的实现中,有时候也可以让头节点存储一些附加信息,例如链表的长度等。
头节点在单链表中具有以下好处:
-
方便操作:头节点作为链表的起始节点,可以方便地操作整个链表。通过头节点,我们可以轻松地找到链表的首元素,并进行插入、删除等操作,而无需特殊处理。
-
简化插入和删除操作:对于单链表的插入和删除操作,需要考虑在链表的开头、中间或末尾进行操作。然而,如果我们有一个明确的头节点,无论是插入还是删除,都可以通过修改头节点的指针来处理,而不需要单独处理特殊情况。
-
方便遍历:遍历链表是常见的操作,头节点可以作为起点,通过遍历头节点的指针域,我们可以遍历整个链表,访问每个节点的数据。
-
空链表判断:通过检查头节点是否为空,我们可以快速判断链表是否为空。如果头节点为空,那么整个链表中没有节点。
-
保护链表结构:头节点作为链表的管理节点,可以帮助保护链表的结构完整性。通过头节点,我们可以确保链表始终保持正确的连接状态,避免链表中出现无效或丢失的节点。
1.2 单链表的操作,建立和运算
插入结点
在a,b结点之间插入一个x结点,只需将x的指针域指向b结点,然后将a结点的指针域指向x结点。两者顺序不能调换,否则指向b结点的指针会丢失。
x->next=a->next;
a->next=x
删除结点
只需要将a的指针域指向下下个结点即可
a->next=a->next->next;
建立单链表
头插法:从一个空表开始依次读取数据元素,生成一个新节点,然后数据域存储该数据元素,然后让头指针的指针域指向这个新节点,直到所有元素插入。
void CreatListF(LinkNode *&L, ElemType a[], int n) {
LinkNode *s;
L = nullptr; // 初始化链表的头节点为 nullptr
for (int i = 0; i < n; i++) {
s = new LinkNode; // 使用 new 运算符动态分配内存
s->data = a[i];
s->next = L; // 将新节点的下一个节点指向当前头节点
L = s; // 将新节点设为链表的新头节点
}
}
首先将链表的头节点 L
初始化为 nullptr
。然后通过循环创建新节点 s
,并将数组 a
中的元素存储在新节点的数据域 data
中。接着,将新节点的指针域 next
指向当前头节点 L
,将新节点 s
设为链表的新头节点 L
。
nullptr
是 C++11 引入的空指针常量。它用于表示一个空指针,表示指针不指向任何有效的对象或函数。在早期的 C++ 版本中,通常使用
NULL
来表示空指针。然而,NULL
实际上是一个宏定义,通常被定义为整数 0。这样的定义会导致在某些上下文中出现类型不匹配的问题,因为整数类型和指针类型不同。为了解决这个问题,C++11 引入了
nullptr
。nullptr
是一个特殊的关键字,用于表示空指针,具有指针类型。它没有具体的值,只是一个表示空指针的常量。使用
nullptr
可以提高代码的可读性和类型安全性,避免了在不同上下文中出现类型不匹配的问题。
老版的代码:nullptr
是 C++ 中用于表示空指针的常量,相较于 NULL
,它提供了更好的类型安全性和代码可读性。
void CreatListF(LinkNode *&L,ElemType a[] int n){
LinkNode *s;
L->next=NULL;
for(int i=0;i<n;i++){
s=(LinkNode *)malloc(sizeof(LinkNode));
s->date =a[i];
s->next=L->next;
L->next=s;
}
}
尾插法:
-
初始化链表的头节点
L
和尾节点tail
为nullptr
。 -
遍历待插入的元素数组,对于每个元素:
- 创建一个新节点,并为其分配内存。
- 将当前元素存储在新节点的数据域中。
- 将新节点的指针域
next
初始化为nullptr
。
-
如果链表为空(即头节点
L
为nullptr
),则将新节点设为头节点L
和尾节点tail
。 -
否则,将新节点连接到尾节点
tail
的后面,并将尾节点tail
更新为新节点。 -
遍历完成后,整个数组的元素都被插入到链表的尾部,构建完成。
void CreatListT(LinkNode *&L, ElemType a[], int n) {
LinkNode *s, *tail;
L = nullptr; // 初始化链表的头节点为 nullptr
tail = nullptr; // 初始化尾节点为 nullptr
for (int i = 0; i < n; i++) {
s = new LinkNode; // 使用 new 运算符动态分配内存
s->data = a[i];
s->next = nullptr;
if (L == nullptr) { // 链表为空,插入的节点作为头节点
L = s;
tail = s;
} else {
tail->next = s;
tail = s;
}
}
}
函数 CreatListT
接受一个链表头节点指针 L
、一个元素数组 a[]
和数组的长度 n
。首先将头节点 L
和尾节点 tail
初始化为 nullptr
。
然后通过循环创建新节点 s
,并将数组 a
中的元素存储在新节点的数据域 data
中。将新节点的指针域 next
初始化为 nullptr
。
如果链表为空,将新节点 s
设为头节点 L
和尾节点 tail
。否则,将新节点连接到尾节点的后面,更新尾节点为新节点。
初始化线性表:
void InitList(LinkNode *&L){
L=(LinkNode *)malloc(sizeof(LinkNode));
L->next=NULL;
}
函数 InitList
接受一个单链表的头节点指针 L
的引用作为参数。它使用 malloc
函数动态分配了一个 LinkNode
结构的内存空间,并将分配得到的地址赋值给头节点指针 L
。然后,将头节点的指针域 next
初始化为 NULL
,表示链表为空。
销毁线性表:
void DestoryList(LinkNode *&L) {
LinkNode* pre = L; // 用于记录当前节点的前一个节点
LinkNode* p = L->next; // 用于遍历链表的指针
while (p != NULL) {
free(pre); // 释放当前节点的内存
pre = p; // pre 指向下一个节点
p = pre->next; // p 移动到下一个节点
}
free(pre); // 释放最后一个节点的内存
}
函数 DestoryList
接受一个单链表的头节点指针 L
的引用作为参数。使用两个指针变量 pre
和 p
来遍历链表,初始时 pre
指向头节点,p
指向头节点的下一个节点。
在循环中,先释放当前节点 pre
的内存,然后将 pre
移动到下一个节点,p
移动到下一个节点的下一个节点。
重复以上步骤,直到 p
指向 NULL
,即遍历完整个链表。最后,释放最后一个节点 pre
的内存。
判断是否为空表
bool ListEmpty(const LinkNode* L) {
return L == nullptr; // 当头节点指针为空时,链表为空
}
求线性表的长度
int ListLength(const LinkNode* L) {
int length = 0;
const LinkNode* current = L;
while (current != nullptr) {
length++;
current = current->next;
}
return length;
}
函数 ListLength
接受一个链表头节点指针 L
的常量指针作为参数,并返回一个整数表示链表的长度。
在函数中,我们使用一个变量 length
来记录链表的长度,并初始化为0。然后,使用一个指针变量 current
来遍历链表,从头节点开始。
在循环中,每遍历到一个节点,就将长度 length
加1,并将 current
移动到下一个节点。重复这个过程,直到遍历完整个链表,即 current
为 nullptr
。
最后,返回记录的链表长度 length
。
输出线性表
void PrintList(const LinkNode* L) {
const LinkNode* current = L;
while (current != nullptr) {
std::cout << current->data << " ";
current = current->next;
}
std::cout << std::endl;
}
函数 PrintList
接受一个链表头节点指针 L
的常量指针作为参数。它用于遍历链表并输出每个节点的数据。
在函数中,使用一个指针变量 current
来遍历链表,初始时指向头节点。
在循环中,每遍历到一个节点,就输出节点的数据,并将 current
移动到下一个节点。重复这个过程,直到遍历完整个链表,即 current
为 nullptr
。
最后,输出换行符,以便在控制台上打印结果后换行。
求线性表中某个数据元素的值
bool GetElement(const LinkNode* L, int index, ElemType& value) {
const LinkNode* current = L;
int count = 0;
while (current != nullptr) {
if (count == index) {
value = current->data;
return true; // 找到指定索引的元素
}
current = current->next;
count++;
}
return false; // 未找到指定索引的元素
}
函数 GetElement
接受一个链表头节点指针 L
的常量指针作为参数,以及要获取的数据元素的索引 index
和一个引用参数 value
用于存储获取到的值。
在函数中,我们使用一个指针变量 current
来遍历链表,初始时指向头节点。还有一个变量 count
用于记录当前遍历到的节点的索引。
在循环中,每遍历到一个节点,就检查当前节点的索引是否与目标索引 index
相等。如果相等,则将当前节点的数据值赋给 value
,并返回 true
表示成功获取到指定索引的元素。
如果遍历完整个链表仍未找到指定索引的元素,则返回 false
。
按元素查找
int FindElement(const LinkNode* L, ElemType value) {
const LinkNode* current = L;
int index = 0;
while (current != nullptr) {
if (current->data == value) {
return index; // 返回匹配元素的序号
}
current = current->next;
index++;
}
return -1; // 表示未找到匹配元素
}
函数 FindElement
接受一个链表头节点指针 L
的常量指针作为参数,以及要查找的元素值 value
。
在函数中,我们使用一个指针变量 current
来遍历链表,初始时指向头节点。还有一个变量 index
用于记录当前遍历到的节点的序号。
在循环中,每遍历到一个节点,就检查当前节点的数据值是否与目标值 value
相等。如果相等,则返回当前节点的序号。
如果遍历完整个链表仍未找到匹配的元素,则返回 -1,表示未找到匹配元素。
插入数据元素
bool InsertElement(LinkNode*& L, int index, ElemType value) {
if (index < 0) {
return false; // 插入位置索引无效
}
LinkNode* current = L;
int count = 0;
while (current != nullptr && count < index) {
current = current->next;
count++;
}
if (count < index) {
return false; // 插入位置超出链表长度
}
LinkNode* newNode = new LinkNode;
newNode->data = value;
if (index == 0) {
newNode->next = L;
L = newNode;
} else {
newNode->next = current->next;
current->next = newNode;
}
return true; // 插入成功
}
在函数中,我们首先检查插入位置索引是否小于0,如果是,则返回 false
表示插入位置无效。
然后,使用指针变量 current
遍历链表,定位到插入位置的前一个节点,同时使用计数器 count
记录当前遍历的节点索引。
如果遍历完整个链表,即 current
为 nullptr
且 count
小于插入位置索引,则表示插入位置超出链表长度,返回 false
。
如果插入位置合法,则创建一个新的节点 newNode
,设置其数据值为要插入的元素值。
如果插入位置为链表的头部(即索引为0),则将新节点的 next
指针指向原来的头节点 L
,并将新节点设为新的头节点。
如果插入位置不是头部,则将新节点的 next
指针指向当前节点的下一个节点,然后将当前节点的 next
指针指向新节点,完成插入操作。
最后,返回 true
表示插入成功。
删除数据元素
bool DeleteElement(LinkNode*& L, int index) {
if (index < 0) {
return false; // 删除位置索引无效
}
LinkNode* current = L;
LinkNode* prev = nullptr;
int count = 0;
while (current != nullptr && count < index) {
prev = current;
current = current->next;
count++;
}
if (count < index || current == nullptr) {
return false; // 删除位置超出链表长度
}
if (prev == nullptr) {
L = current->next;
} else {
prev->next = current->next;
}
delete current;
return true; // 删除成功
}
首先,我们检查删除位置索引是否小于0,如果是,则返回 false
表示删除位置无效。
然后,使用指针变量 current
和 prev
遍历链表,定位到要删除的节点和其前一个节点,同时使用计数器 count
记录当前遍历的节点索引。
如果遍历完整个链表,即 current
为 nullptr
且 count
小于删除位置索引,则表示删除位置超出链表长度,返回 false
。
如果删除位置合法,则根据情况进行删除操作。如果删除的是头节点(即索引为0),则将链表头指针 L
指向头节点的下一个节点。
如果删除的不是头节点,则将前一个节点 prev
的 next
指针指向当前节点的下一个节点,实现删除操作。
最后,释放被删除节点的内存空间,防止内存泄漏。
最后,返回 true
表示删除成功。
总结
学习单链表是数据结构和算法中的重要内容之一。下面是单链表学习的一些要点和总结:
- 单链表是一种常见的线性数据结构,由一系列节点组成,每个节点包含数据元素和指向下一个节点的指针。
- 单链表的优点是插入和删除操作的时间复杂度为O(1),可以在常数时间内完成,而不受表的长度影响。
- 单链表的缺点是访问某个位置的节点需要遍历链表,时间复杂度为O(n),其中n是链表的长度。
- 单链表需要一个头节点作为起始节点,方便对链表进行操作。
- 单链表的创建可以使用头插法或尾插法,头插法将新节点插入到头节点之后,尾插法将新节点插入到链表的末尾。
- 单链表的插入操作可以在指定位置插入节点,需要找到插入位置的前一个节点,并调整指针指向。
- 单链表的删除操作可以删除指定位置的节点,同样需要找到删除位置的前一个节点,并调整指针指向,释放被删除节点的内存空间。
- 单链表的遍历可以通过循环遍历链表的节点,并对每个节点进行相应的操作。
- 单链表的查找可以根据元素值或位置进行查找,对于元素值的查找需要遍历链表逐个比较,对于位置的查找需要遍历指定次数找到目标节点。
- 在使用单链表时,需要注意处理边界情况,如空链表或插入/删除的位置超出链表长度的情况。
- 在操作单链表时,需要注意内存管理,及时释放不再使用的节点的内存空间,避免内存泄漏。
- C++中可以使用结构体或类来定义单链表的节点,并使用指针来表示节点之间的关系。
通过学习单链表,我们可以掌握链表的基本操作和应用,为解决实际问题提供了一种有效的数据结构。同时,了解链表的特点和使用场景,可以帮助我们选择合适的数据结构来解决具体的问题。