<think>我们首先需要实现单链表的基本运算和整体建表算法。根据要求,我们需要编写两个文件:linklist.cpp(包含单链表的基本操作函数)和exp2-2.cpp(主程序,调用这些函数完成指定功能)。
单链表的基本操作包括:
初始化单链表
尾插法插入元素
输出单链表
计算单链表长度
判断单链表是否为空
获取指定位置的元素
查找元素的位置(返回第一个匹配的位置)
在指定位置插入元素
删除指定位置的元素
释放单链表
我们将设计一个结构体来表示单链表的结点,每个结点包含一个字符类型的数据和一个指向下一个结点的指针。
在linklist.cpp中,我们将实现以下函数:
- void InitList(LinkNode *&L):初始化单链表,创建一个头结点。
- void DestroyList(LinkNode *&L):释放单链表。
- bool ListEmpty(LinkNode *L):判断单链表是否为空(只有头结点为空)。
- int ListLength(LinkNode *L):返回单链表的长度(头结点不算)。
- void DispList(LinkNode *L):输出单链表的所有元素。
- bool GetElem(LinkNode *L, int i, ElemType &e):获取单链表中第i个位置的元素(i从1开始计数)。
- int LocateElem(LinkNode *L, ElemType e):查找元素e在单链表中首次出现的位置(位置从1开始),若不存在返回0。
- bool ListInsert(LinkNode *&L, int i, ElemType e):在单链表的第i个位置插入元素e(注意:头结点是第0个位置,然后第1个位置是第一个元素结点)。
- bool ListDelete(LinkNode *&L, int i, ElemType &e):删除单链表的第i个位置的元素,并用e返回其值(这里我们不需要返回删除的值,但为了通用性保留,在调用时如果不关心删除的值,可以传入一个临时变量)。
另外,我们还需要一个尾插法建立单链表的函数,但是题目要求的是整体建表算法,我们可以通过多次调用尾插法插入函数来实现,也可以单独写一个尾插法建立单链表的函数。为了简单,我们直接使用ListInsert函数在尾部插入(但这样每次插入都要遍历到尾部效率低)。另一种方法是维护一个尾指针,这样尾插时可以直接插入。这里我们为了效率,在插入时维护一个尾指针。
然而,考虑到我们需要频繁在尾部插入多个元素,我们可以设计一个尾插法插入元素的函数,这样效率更高。但是根据题目要求,我们只需要在exp2-2.cpp中完成指定的功能,因此我们可以选择在linklist.cpp中实现一个尾插法插入的函数:
void InsertRear(LinkNode *&L, ElemType e); // 使用尾指针快速插入
但是,为了保持接口的一致性,我们也可以不使用额外的尾指针,而是每次插入时遍历到尾部。由于我们一次插入的元素数量不多,遍历到尾部也是可以接受的。不过,为了效率,我们可以在链表结构中增加一个尾指针,但这样就需要定义一个新的链表结构体(包含头指针和尾指针)。由于题目没有明确要求,我们这里采用最简单的单链表(只包含头指针),然后在插入到尾部时遍历链表。
但是,注意题目要求“整体建表算法”,我们可以用尾插法建立整个链表(一次建立多个元素)。我们可以写一个函数:void CreateListR(LinkNode *&L, ElemType a[], int n); 用数组a中的n个元素建立单链表(尾插法)。不过,题目要求的功能是依次插入a、b、c、d、e,我们可以调用多次尾插函数来实现。
权衡之后,我们决定在linklist.cpp中实现以下函数(使用头指针和遍历方式):
初始化、销毁、判断空、求长度、输出、按位置查找元素、按元素查找位置、按位置插入(包括在任意位置插入,包括头部和尾部)、按位置删除。
另外,我们单独实现一个尾插法的插入函数(但不维护尾指针,而是每次遍历到尾部),这样在插入多个元素时效率会低,但为了简单,我们先这样实现。
另一种方案是:我们在主程序exp2-2.cpp中,通过循环调用按位置插入函数,每次插入到当前链表的末尾(位置为长度+1)。这样我们就可以利用按位置插入函数实现尾插。这样就不需要单独写尾插函数了。
但是,按位置插入函数在插入到末尾时需要遍历整个链表,时间复杂度为O(n),而如果我们在链表中维护一个尾指针,则可以在O(1)时间内完成插入。考虑到这是一个练习题,我们选择简单实现,即不维护尾指针。
因此,我们将在linklist.cpp中实现一个通用的插入函数ListInsert,它可以在任意位置插入(包括在第一个元素之前插入,即在头结点后插入,以及插入到末尾)。这样,尾插就可以通过调用ListInsert(L, n+1, e)来实现(n为当前长度)。
然而,注意题目要求(2)依次采用尾插法插入a、b、c、d、e元素。尾插法通常是指每次将新结点插入到链表的尾部,所以我们需要能够找到链表的尾部。如果我们每次插入都从头遍历,那么插入n个元素的时间复杂度就是O(n^2)。为了避免这种情况,我们可以用一个指针记住当前的尾结点,这样每次插入时就可以直接插入。
因此,我们修改设计:在单链表的结构中,我们添加一个尾指针。但是题目没有要求使用带尾指针的链表,而且引用[2]中也没有提到尾指针。为了简单,我们仍然使用普通的单链表(只有头指针),然后在插入时遍历。因为最多5个元素,效率问题不大。
但是,为了练习,我们可以采用两种方法:
方法1:在链表中增加尾指针(需要重新定义链表结构体)
方法2:在插入多个元素时,用一个临时指针记录当前尾结点,这样在插入一组元素时,我们可以做到O(n)的时间。
由于题目要求的功能是依次插入多个元素,我们可以在exp2-2.cpp中用一个循环来插入,并且在循环内部保持一个指向尾结点的指针,这样我们就不需要在linklist.cpp中改变结构。但是,linklist.cpp中的函数并不知道我们在循环中保持了尾指针,所以这种方法只能用在exp2-2.cpp中,而不能封装到linklist.cpp的函数中。
综合考虑,我们决定在linklist.cpp中不维护尾指针,而是提供一个尾插的函数,该函数每次遍历到链表尾部,然后插入。这样虽然每次插入是O(n),但总时间是O(n^2)。由于n=5,可以接受。
步骤:
1. 定义单链表结点类型和元素类型。
2. 在linklist.cpp中实现各个基本运算函数。
3. 在exp2-2.cpp中调用这些函数完成题目要求的功能。
具体实现如下:
首先,我们在linklist.h中定义结构和函数声明(为了模块化,我们最好创建一个头文件,但题目要求是.cpp文件,所以我们可以将声明和实现都放在.cpp文件中,但这样exp2-2.cpp就不能包含linklist.h了。因此,我们将linklist.cpp设计为包含函数定义,然后在exp2-2.cpp中通过extern声明函数,或者在linklist.cpp中声明一个头文件linklist.h,然后在两个.cpp中都包含它。
为了清晰,我们创建linklist.h和linklist.cpp,但是题目要求的是linklist.cpp和exp2-2.cpp,所以我们可以将linklist.h的内容放在linklist.cpp中,然后在exp2-2.cpp中包含linklist.h?但是题目没有要求头文件,因此我们可以将结构定义和函数声明放在linklist.cpp的开头,然后在exp2-2.cpp中再写一遍?这样不好。
另一种方法是,将结构定义和函数声明放在一个头文件linklist.h中,然后在两个.cpp中都包含它。这样更符合实际编程。但题目要求是编写linklist.cpp和exp2-2.cpp,所以我们可以这样安排:
文件:linklist.h
文件:linklist.cpp
文件:exp2-2.cpp
但是题目没有要求linklist.h,所以如果我们不想多一个文件,可以将linklist.h的内容直接放到exp2-2.cpp的开头(这样重复代码,不好)或者在linklist.cpp中定义结构,然后在exp2-2.cpp中再定义一遍(更不好)。
因此,我们按照实际工程做法,创建linklist.h,这样两个cpp文件都可以包含它。虽然题目要求中没有提到,但我们可以理解为linklist.cpp需要包含这些定义,而exp2-2.cpp也需要包含这些定义。
所以,我们创建三个文件:
linklist.h:包含单链表的结构定义和函数声明
linklist.cpp:包含函数实现
exp2-2.cpp:主程序
但是,如果题目严格要求只能有两个文件:linklist.cpp和exp2-2.cpp,那么我们可以将linklist.h的内容放在linklist.cpp中,然后在exp2-2.cpp中通过包含linklist.cpp来获取声明?这通常不是好做法,因为包含.cpp文件会导致重复定义(如果在多个地方包含的话)。
因此,我们采用另一种方式:将linklist.h的内容放在linklist.cpp中,然后在exp2-2.cpp中手动重复这些声明(因为只有exp2-2.cpp需要,而且声明不多,可以接受)。
考虑到代码清晰和可维护性,我们还是创建三个文件,并在回答中说明。如果用户要求必须两个文件,我们再调整。
根据题目要求,用户要求编写两个文件:linklist.cpp和exp2-2.cpp。因此,我们这样安排:
在linklist.cpp中,我们定义:
#include <iostream>
using namespace std;
typedef char ElemType;
typedef struct LNode {
ElemType data;
struct LNode *next;
} LinkNode;
// 函数声明
void InitList(LinkNode *&L);
void DestroyList(LinkNode *&L);
bool ListEmpty(LinkNode *L);
int ListLength(LinkNode *L);
void DispList(LinkNode *L);
bool GetElem(LinkNode *L, int i, ElemType &e);
int LocateElem(LinkNode *L, ElemType e);
bool ListInsert(LinkNode *&L, int i, ElemType e);
bool ListDelete(LinkNode *&L, int i, ElemType &e);
// 另外,我们实现一个尾插法的插入函数(可选),但这里我们用ListInsert来实现尾插,所以可以不提供。
// 但是,为了效率,我们提供一个专门尾插的函数,这样在exp2-2.cpp中插入多个元素时,可以避免每次遍历整个链表。
// 这个函数叫做InsertR,它直接在链表尾部插入一个元素,而不需要指定位置。
bool InsertR(LinkNode *&L, ElemType e); // 在尾部插入元素
然后,在exp2-2.cpp中,我们需要包含头文件,但为了简化,我们直接在exp2-2.cpp中再次定义这些结构体和函数声明(因为函数实现在linklist.cpp中,所以我们需要在exp2-2.cpp中声明这些函数,否则无法链接)。
具体编写:
linklist.cpp:
包含所有函数的实现。
exp2-2.cpp:
首先包含<iostream>,然后定义相同的结构体和函数声明(或者通过extern声明函数),然后写main函数。
但是,重复定义结构体和函数声明会显得冗余,而且容易出错。因此,我们建议将linklist.cpp拆分为linklist.h和linklist.cpp,然后在exp2-2.cpp中包含linklist.h。不过,题目要求是linklist.cpp,所以我们这里采用将linklist.h的内容放在linklist.cpp中,然后在exp2-2.cpp中复制一份结构定义和函数声明(因为只有这一个程序使用,而且函数不多)。
为了减少重复,我们在linklist.cpp中定义结构体和函数,在exp2-2.cpp中手动写一遍这些声明。这样虽然重复,但可以满足要求。
现在,我们开始编写代码。
注意:单链表通常有一个头结点,这样插入删除操作会更统一。
我们设计的链表带有头结点。
步骤:
初始化:创建头结点,指针域为空。
尾插法:找到最后一个结点,在其后插入新结点。
其他操作都按常规。
具体实现:
1. 初始化单链表
void InitList(LinkNode *&L) {
L = new LinkNode; // 创建头结点
L->next = NULL;
}
2. 销毁单链表
void DestroyList(LinkNode *&L) {
LinkNode *p = L, *q;
while (p != NULL) {
q = p->next;
delete p;
p = q;
}
L = NULL; // 将L置为NULL
}
3. 判断单链表是否为空
bool ListEmpty(LinkNode *L) {
return (L->next == NULL);
}
4. 求单链表的长度
int ListLength(LinkNode *L) {
int n = 0;
LinkNode *p = L->next;
while (p != NULL) {
n++;
p = p->next;
}
return n;
}
5. 输出单链表
void DispList(LinkNode *L) {
LinkNode *p = L->next;
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
6. 按位置查找元素
bool GetElem(LinkNode *L, int i, ElemType &e) {
// i从1开始计数,即头结点后的第一个结点为第1个结点
int j = 0;
LinkNode *p = L;
while (j < i && p != NULL) {
j++;
p = p->next;
}
if (p == NULL) // 位置i超出范围
return false;
else {
e = p->data;
return true;
}
}
7. 按元素值查找位置(返回第一个匹配的位置)
int LocateElem(LinkNode *L, ElemType e) {
int i = 1;
LinkNode *p = L->next;
while (p != NULL) {
if (p->data == e)
return i;
p = p->next;
i++;
}
return 0; // 未找到返回0
}
8. 按位置插入(在第i个位置插入,即在第i-1个结点后插入,头结点是第0个结点)
bool ListInsert(LinkNode *&L, int i, ElemType e) {
// 找到第i-1个结点
int j = 0;
LinkNode *p = L, *s;
while (j < i-1 && p != NULL) {
j++;
p = p->next;
}
if (p == NULL) // 位置i不合法(i太大)
return false;
else {
s = new LinkNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
}
9. 按位置删除(删除第i个结点)
bool ListDelete(LinkNode *&L, int i, ElemType &e) {
// 找到第i-1个结点
int j = 0;
LinkNode *p = L, *q;
while (j < i-1 && p != NULL) {
j++;
p = p->next;
}
if (p == NULL || p->next == NULL) // 第i-1个结点不存在或第i个结点不存在
return false;
else {
q = p->next;
e = q->data; // 返回被删除元素的值
p->next = q->next;
delete q;
return true;
}
}
10. 尾插法插入一个元素(可选)
我们实现一个函数InsertR,它直接在尾部插入元素。
bool InsertR(LinkNode *&L, ElemType e) {
LinkNode *p = L;
// 找到最后一个结点
while (p->next != NULL) {
p = p->next;
}
LinkNode *s = new LinkNode;
s->data = e;
s->next = NULL;
p->next = s;
return true;
}
但是,为了减少代码量,我们也可以不写这个函数,而在exp2-2.cpp中调用ListInsert(L, ListLength(L)+1, e)来实现。但是这样效率低,因为每次插入都要遍历整个链表。因此,我们选择实现InsertR函数。
现在,我们在linklist.cpp中实现以上所有函数(包括InsertR)。
然后,在exp2-2.cpp中,我们需要重复这些声明(因为linklist.cpp中已经定义,我们在exp2-2.cpp中需要声明这些函数才能使用)。
exp2-2.cpp的内容:
#include <iostream>
using namespace std;
typedef char ElemType;
typedef struct LNode {
ElemType data;
struct LNode *next;
} LinkNode;
// 函数声明
void InitList(LinkNode *&L);
void DestroyList(LinkNode *&L);
bool ListEmpty(LinkNode *L);
int ListLength(LinkNode *L);
void DispList(LinkNode *L);
bool GetElem(LinkNode *L, int i, ElemType &e);
int LocateElem(LinkNode *L, ElemType e);
bool ListInsert(LinkNode *&L, int i, ElemType e);
bool ListDelete(LinkNode *&L, int i, ElemType &e);
bool InsertR(LinkNode *&L, ElemType e); // 尾插函数
int main() {
// 按照题目要求完成功能
LinkNode *h; // 单链表头指针
// (1) 初始化单链表h
InitList(h);
// (2) 依次采用尾插法插入a、b、c、d、e元素
InsertR(h, 'a');
InsertR(h, 'b');
InsertR(h, 'c');
InsertR(h, 'd');
InsertR(h, 'e');
// (3) 输出单链表h
cout << "单链表h: ";
DispList(h);
// (4) 输出单链表h的长度
cout << "单链表h的长度: " << ListLength(h) << endl;
// (5) 判断单链表h是否为空
if (ListEmpty(h))
cout << "单链表h为空" << endl;
else
cout << "单链表h不为空" << endl;
// (6) 输出单链表h的第3个元素
ElemType e;
if (GetElem(h, 3, e))
cout << "第3个元素为: " << e << endl;
else
cout << "第3个元素不存在" << endl;
// (7) 输出元素a的位置
int pos = LocateElem(h, 'a');
if (pos)
cout << "元素a的位置为: " << pos << endl;
else
cout << "元素a不存在" << endl;
// (8) 在第4个元素位置上插入f元素
// 注意:这里在第4个位置插入,即在第4个元素之前插入,插入后成为第4个元素
if (ListInsert(h, 4, 'f'))
cout << "在第4个位置插入f成功" << endl;
else
cout << "插入失败" << endl;
// (9) 输出单链表h
cout << "插入f后的单链表h: ";
DispList(h);
// (10) 删除单链表h的第3个元素
ElemType deletedElem;
if (ListDelete(h, 3, deletedElem))
cout << "删除第3个元素成功,删除的元素为: " << deletedElem << endl;
else
cout << "删除失败" << endl;
// (11) 输出单链表h
cout << "删除第3个元素后的单链表h: ";
DispList(h);
// (12) 释放单链表h
DestroyList(h);
return 0;
}
最后,我们需要编译这两个文件。在编译时,需要将linklist.cpp和exp2-2.cpp一起编译。
注意:在linklist.cpp中,我们包含了所有函数的实现。在exp2-2.cpp中,我们包含了相同的结构定义和函数声明。
由于时间关系,我们这里只给出代码框架。实际编写时,请确保所有函数的正确性。
下面,我们给出完整的文件内容:
文件:linklist.cpp
</think>### C++实现单链表基本运算
以下是完整的`linklist.cpp`和`exp2-2.cpp`实现,满足单链表的所有基本运算要求(初始化、尾插法插入、输出链表、计算长度、判空、获取元素、查找位置、插入元素、删除元素、释放链表等)。
---
#### 文件:linklist.cpp
```cpp
#include <iostream>
using namespace std;
typedef char ElemType;
// 单链表节点结构
typedef struct LNode {
ElemType data;
struct LNode* next;
} LinkNode;
// 1. 初始化单链表
void InitList(LinkNode*& L) {
L = new LinkNode; // 创建头结点
L->next = NULL;
}
// 2. 尾插法插入元素
void InsertRear(LinkNode*& L, ElemType e) {
LinkNode* newNode = new LinkNode;
newNode->data = e;
newNode->next = NULL;
LinkNode* p = L;
while (p->next != NULL) // 定位到尾节点
p = p->next;
p->next = newNode; // 尾插新节点
}
// 3. 输出单链表
void DispList(LinkNode* L) {
LinkNode* p = L->next; // 跳过头结点
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
// 4. 计算单链表长度
int ListLength(LinkNode* L) {
int len = 0;
LinkNode* p = L->next;
while (p != NULL) {
len++;
p = p->next;
}
return len;
}
// 5. 判断链表是否为空
bool ListEmpty(LinkNode* L) {
return (L->next == NULL);
}
// 6. 获取第i个元素
bool GetElem(LinkNode* L, int i, ElemType& e) {
if (i < 1) return false;
LinkNode* p = L->next;
int count = 1;
while (p != NULL && count < i) {
p = p->next;
count++;
}
if (p == NULL) return false;
e = p->data;
return true;
}
// 7. 查找元素位置
int LocateElem(LinkNode* L, ElemType e) {
int pos = 1;
LinkNode* p = L->next;
while (p != NULL) {
if (p->data == e)
return pos;
p = p->next;
pos++;
}
return 0; // 未找到返回0
}
// 8. 在第i个位置插入元素
bool ListInsert(LinkNode*& L, int i, ElemType e) {
if (i < 1) return false;
LinkNode* p = L;
int count = 0;
// 定位到第i-1个节点
while (p != NULL && count < i - 1) {
p = p->next;
count++;
}
if (p == NULL) return false;
LinkNode* newNode = new LinkNode;
newNode->data = e;
newNode->next = p->next;
p->next = newNode;
return true;
}
// 9. 删除第i个元素
bool ListDelete(LinkNode*& L, int i, ElemType& e) {
if (i < 1) return false;
LinkNode* p = L;
int count = 0;
// 定位到第i-1个节点
while (p->next != NULL && count < i - 1) {
p = p->next;
count++;
}
if (p->next == NULL) return false;
LinkNode* delNode = p->next;
e = delNode->data;
p->next = delNode->next;
delete delNode;
return true;
}
// 10. 释放单链表
void DestroyList(LinkNode*& L) {
LinkNode* p = L;
while (p != NULL) {
LinkNode* temp = p;
p = p->next;
delete temp;
}
L = NULL; // 防止野指针
}
```
---
#### 文件:exp2-2.cpp
```cpp
#include <iostream>
#include "linklist.cpp" // 包含链表操作实现
int main() {
LinkNode* h; // 单链表头指针
// (1) 初始化单链表h
InitList(h);
cout << "初始化完成" << endl;
// (2) 尾插法插入a,b,c,d,e
InsertRear(h, 'a');
InsertRear(h, 'b');
InsertRear(h, 'c');
InsertRear(h, 'd');
InsertRear(h, 'e');
cout << "插入a,b,c,d,e完成" << endl;
// (3) 输出单链表h
cout << "单链表h: ";
DispList(h); // 输出: a b c d e
// (4) 输出长度
cout << "长度: " << ListLength(h) << endl; // 5
// (5) 判断是否为空
cout << "是否为空: " << (ListEmpty(h) ? "是" : "否") << endl; // 否
// (6) 输出第3个元素
ElemType e;
if (GetElem(h, 3, e))
cout << "第3个元素: " << e << endl; // c
else
cout << "位置无效" << endl;
// (7) 查找元素a的位置
int pos = LocateElem(h, 'a');
if (pos > 0)
cout << "a的位置: " << pos << endl; // 1
else
cout << "未找到" << endl;
// (8) 在第4个位置插入f
if (ListInsert(h, 4, 'f')) {
cout << "插入f后的链表: ";
DispList(h); // a b c f d e
}
// (9) 输出单链表h
cout << "当前链表: ";
DispList(h);
// (10) 删除第3个元素
ElemType deletedElem;
if (ListDelete(h, 3, deletedElem)) {
cout << "删除元素: " << deletedElem << endl; // c
cout << "删除后链表: ";
DispList(h); // a b f d e
}
// (11) 输出单链表h
cout << "最终链表: ";
DispList(h);
// (12) 释放单链表
DestroyList(h);
cout << "链表已释放" << endl;
return 0;
}
```
---
### 功能验证说明
1. **初始化**:创建头结点
2. **尾插法**:依次插入a,b,c,d,e → `a b c d e`
3. **输出链表**:遍历输出所有元素
4. **计算长度**:返回节点数量(头结点不计入)
5. **判空**:检查头结点是否为空
6. **获取第3个元素**:返回`c`(索引从1开始)
7. **查找元素位置**:`a`位于位置1
8. **位置插入**:在第4位插入`f` → `a b c f d e`
9. **删除元素**:删除第3个元素`c` → `a b f d e`
10. **释放链表**:递归删除所有节点
> 编译命令:`g++ exp2-2.cpp -o exp2-2 && ./exp2-2`
---
### 相关问题
1. 单链表头结点有什么作用?能否省略头结点?
2. 尾插法相比头插法有什么优缺点?
3. 如何检测单链表是否存在环?
4. 在单链表中实现元素排序有哪些方法?
> 参考资料:数据结构单链表基本操作实现[^1][^2]