写在前面
课程采用教材《数据结构(C语言版)》严蔚敏,吴伟民,清华大学出版社。
本系列博文用于自我学习总结和期末复习使用,同时也希望能够帮助到有需要的同学。如果有知识性的错误,麻烦评论指出。
单链表
线性链表
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素ai的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。链表的中每个结点中只包含一个指针域时,又称线性链表或单链表。
单链表的类型定义
本次实验中抽象数据单链表的定义如下:
ADT LinkList{
数据对象:D={ai| ai∈ElemSet,i=1,2,…,n,n≥0}
数据关系:R1={<ai-1,ai>| ai-1,ai ∈D,i=1,2,…,n }
基本操作:
CreateListHead(& L, n)
操作结果:构造一个逆位序输入n个元素值的单链线性表L,头插入建表。
CreateListTail(& L, n)
操作结果:构造一个顺位序输入n个元素值的单链线性表L,尾插入建表。
PrintList(L)
初始条件:线性表L已存在。
操作结果:输出线性表信息。
DestroyList(&L)
初始条件:线性表L已存在。
操作结果:销毁线性表L。
}ADT LinkList
单链表建表的方法
主要实现单链表的头插入创建和尾插入创建。
头插入建表
具体过程:(图中曲线箭头表示操作过程中的赋值,直线箭头表示指针指向)
①建立头结点L和待插入的in结点,并向in结点数据域赋值data1
②将头结点指针域赋值给in结点的指针域,也即in结点的指针域指向头结点指针域
③将in结点的结构指针赋值给头结点的指针域,也即头结点的指针域指向in结点
④完成第一次表链增长
⑤开始下一次头插入in结点,并循环到结束条件
尾插入建表
具体过程:(图中曲线箭头表示操作过程中的赋值,直线箭头表示指针指向)
①创建头结点和end结点,并给它们相同的存储空间,创建in结点,并向其数据域赋值data1
②将end结点的指针域指向in结点,同时头结点的指针域也指向了in结点
③将end结点移动到in结点的位置,即表尾,并将in结点的指针域指空,同时end结点的指针域也指向了空,完成第一次表链增长
④开始下一次尾插入in结点,并循环到结束条件
(3)动态内存分配
动态内存分配采用C++中的new和delete来实现,相较于C中的malloc函数,减少了语句量。
单链表实现(头插入建表和尾插入建表)
//1、用到的头文件、命名空间和函数执行结果作态代码
#include <iostream>
using namespace std;
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status是函数的类型,其值是函数结果状况代码
typedef int Status;
typedef int ElemType;//将ElemType定义为int类型
//2、采用的存储结构
//-----线性表的单链表存储结构-----
typedef struct LNode {
ElemType data;// 数据域,保存结点值
struct LNode* next;// 指针域
}*LinkList;
//3、基本操作侧函数原型说明及实现
//-----基本操作的函数原型说明-----
Status CreateListHead(LinkList& L, int n);
// 操作结果:构造一个逆位序输入n个元素值的单链线性表L,头插入建表。
Status CreateListTail(LinkList& L, int n);
// 操作结果:构造一个顺位序输入n个元素值的单链线性表L,尾插入建表。
void PrintList(LinkList L);
// 初始条件:线性表L已存在。
// 操作结果:输出线性表信息。
Status DestoryList(LinkList& L);
// 初始条件:线性表L已存在。
// 操作结果:销毁单链线性表L。
//-----基本操作函数的实现-----
Status CreateListHead(LinkList& L, int n) {
// 逆位序输入n个元素的值,建立带表头结点的单链线性表L。
L = new LNode;
if (!L)exit(OVERFLOW);// 空间分配失败
L->next = NULL;// 先建立一个带头结点的单链表
LinkList in;
for (int i = 0; i < n; i++)
{
in = new LNode;// 生成新的结点
if (!in)exit(OVERFLOW);// 空间分配失败
cin >> in->data;// 输入元素值
in->next = L->next;// 不能是NULL
L->next = in;// 插入到表头
}
return OK;
}//CreateListHead
Status CreateListTail(LinkList& L, int n) {
// 顺位序输入n个元素的值,建立带表头结点的单链线性表L。
LinkList end, in;
L = end = new LNode;// 为L头和尾申请同一空间
if (!L)exit(OVERFLOW);// 空间分配失败
end->next = NULL;// 创建单链表的表头结点
for (int i = 0; i < n; i++)
{
in = new LNode;// 为插入结点分配空间
if (!in)exit(OVERFLOW);// 空间分配失败
cin >> in->data;// 向插入结点数据域赋值
end->next = in;// 将插入结点接在尾结点之后
end = in;// 用插入结点代替尾节点
in->next = NULL;// 尾节点指针域指空
}
return OK;
}//CreateListTail
void PrintList(LinkList L) {
// 按照结点的顺序输出
if (!L)exit(INFEASIBLE);
cout << "L中有:";
while (L->next != NULL) {// 循环条件,循环到最后一个结点
L = L->next;
cout << L->data << " ";
}cout << endl;
}// PrintList
Status DestoryList(LinkList& L) {
// 销毁单链表L
if (!L)exit(INFEASIBLE);
delete L;
L = NULL;
return OK;
}// DestoryList
//4、主函数
int main()
{
LinkList L;
cout << "请输入十个元素:\n";
if (CreateListHead(L, 10))// 测试头插入建表
PrintList(L);
cout << "请输入十个元素:\n";
if (CreateListTail(L, 10))// 测试尾插入建表
PrintList(L);
if (DestoryList(L))// 测试销毁单链表
PrintList(L);
return 0;
}
运行结果
实验总结
本次实验实现了线性表的链式存储结构,以及单链表的头插入创建和尾插入创建,输出和销毁。
实验中,需要加深理解的是,结点的概念。定义结点时,使用了结构体的嵌套,在结点的指针域指向下一个结点的结构指针,结点的数据域存储数据元素,这样就达到了链接各个结点的功能。需要注意的是,头结点的数据域一般不存储数据元素,尾结点的指针域指空。在程序执行中,可以清楚地查看到结点内部状况。其中,L结点为头结点,采用头插入方法建表。
通过本次实验,对结点的理解更加深入,对结点的一些操作方法更加熟悉,对比顺序结构,可以感觉到链式结构的操作更加灵活,没有占用多余的空间,但创建起来有些麻烦,因为逻辑结构与物理结构不能很好地对应,对于指针的理解也有待加深。对比头插入和尾插入建表,两种方法各有优劣。头插入建表,算法相对简单,比较好理解,但获得的链表存储顺序与输入顺序相反,输出链表时先进入的后出来。尾插入建表,算法相对复杂,特别是结构指针的相互赋值容易混乱,但获得的链表存储顺序与输入顺序相同,输出链表时先入先出。