目录
线性结构的特点
在数组元素的非空有限集中,
- 存在惟一的一个被称做“第一个”的数据元素
- 存在惟一的一个被称做“最后一个”的数据元素
- 除第一个之外,集合中的每个数据元素均只有一个前驱
- 除最后一个之外,集合中每个数据元素均只有一个后继
定义
线性表:n 个数据元素的有限序列。
注意:同一线性表中的数据元素具有相同特征,即属同一数据对象,相邻数据元素之间存在着序偶关系。
线性表记作:
:线性起点,起始结点
:线性终点,终端结点
1,2,...,n:下标,元素的序号,表示元素在表中的位置
: 的直接前驱
: 的直接后继
当 i=1,2,...,n-1 时, 有且仅有一个直接后继;当 i=2,3,...,n 时, 有且仅有一个直接前驱
n:n ≥0,线性表的长度,n=0 时称为空表
抽象数据类型定义
ADT List{
数据对象:D={ | ∈ ElemSet,i=1,2,...,n,n≥0 }
数据关系:R1={ <> | , ∈ D,i=2,3,...,n }
基本操作:
InitList(&L)
操作结果:构造一个空的线性表 L
DestroyList(&L)
初始条件:线性表 L 已存在
操作结果:销毁线性表 L
ClearList(&L)
初始条件:线性表 L 已存在
操作结果:将 L 重置为空表
ListEmpty(L)
初始条件:线性表 L 已存在
操作结果:若 L 为空表,则返回 true,否则返回 false
ListLength(L)
初始条件:线性表 L 已存在
操作结果:返回 L 中数据元素个数
GetElem(L,i,&e)
初始条件:线性表 L 已存在,1 ≤ i ≤ ListLength(L)
操作结果:用 e 返回 L 中第 i 个数据元素的值
LocateElem(L,e,compare())
初始条件:线性表 L 已存在,compare() 是数据元素判定函数
操作结果:返回 L 中第 1 个与 e 满足关系 compare() 的数据元素的位序。若这样的数据元素不存在,则返回值为 0
PriorElem(L,cur_e,&pre_e)
初始条件:线性表 L 已存在
操作结果:若 cur_e 是 L 的数据元素,且不是第一个,则用 cur_e 返回它的前驱,否则操作失败,pre_e 无定义
NextElem(L,cur_e,&next_e)
初始条件:线性表 L 已存在
操作结果:若 cur_e 是 L 的数据元素,且不是最后一个,则用 next_e 返回它的后继,否则操作失败,next_e 无定义
ListInsert(&L,i,e)
初始条件:线性表 L 已存在,i ≤ i ≤ ListLength(L)+1
操作结果:在 L 中第 i 个位置之前插入新的数据元素 e,L 的长度加 1
ListDelete(&L,i,&e)
初始条件:线性表 L 已存在且非空,1 ≤ i ≤ ListLength(L)
操作结果:删除 L 的第 i 个数据元素,并用 e 返回其值,L 的长度减 1
ListTraverse(L,visit())
初始条件:线性表 L 已存在
操作结果:依次对 L 的每个数据元素调用函数 visit() 。一旦 visit() 失败,则操作失败
}ADT List
顺序表示
定义
线性表的顺序表示:用一组地址连续的存储单元依次存储线性表的数据元素
基地址/起始位置:线性表的第 1 个数据元素 的存储位置
计算存储位置
知道某个元素的存储位置就可以计算其他元素的存储位置
假设线性表的每个元素需占 l 个存储单元,则第 i+1 个数据元素的存储位置和第 i 个数据元素的存储位置之间满足关系:LOC( )=LOC( )+l
所有数据元素的存储位置均可由第一个数据元素的存储位置得到:LOC( )=LOC( )+(i-1)*l
特点
- 以物理位置相邻表示逻辑关系
- 任一元素均可随机存储(优点)【随机存取】
实现
可用一维数组表示顺序表。由于线性表长可变,但是数组长度不可动态定义,所以需要用一变量表示顺序表的长度属性。
顺序表类型定义
逻辑位序和物理位序相差1,逻辑结构下标从1开始计算,存储结构下标从0开始计算【使用数组存储】。
//数组静态分配
typedef struct {
ElemType elem[LIST_INIT_SIZE];
int length;//长度
}SqList;
//数组动态分配
typedef struct {
ElemType* elem;
int length;
}SqList;
初始化
bool InitList_Sq(SqList& L) {//构造一个空的顺序表L
L.elem = new ElemType[LIST_INIT_SIZE];//为顺序表分配空间
if (!L.elem)
return false;//存储分配失败
L.length = 0;//空表长度为0
return true;
}
创建
void CreateList_Sq(SqList& L, int n) {
for (int i = 0;i < n;i++)
cin >> L.elem[i];
L.length = n;
}
销毁
void DestroyList(SqList& L) {
if (L.elem)
delete L.elem;
}
清空
void ClearList(SqList& L) {
L.length = 0;
}
判断线性表是否为空
bool ListEmpty(SqList L) {
if (L.length == 0)
return true;
else
return false;
}
求线性表长度
int ListLength(SqList L) {
return (L.length);
}
获取元素
bool GetElem(SqList L, int i, ElemType& e) {
if (i<1 || i>L.length)
return false;
e = L.elem[i - 1];
return true;
}
查找
要求
在线性表 L 中查找与指定值 e 相同的数据元素的位置
思路
从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到返回 0
算法分析
平均查找长度 ASL:为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度
对含有 n 个记录的表,查找成功时:
【:第 i 个记录被查找的概率;:找到第 i 个记录需比较的次数】
假设每个记录的查找概率相等:,则:
//按值查找
int LocateElem(SqList L, ElemType e) {
for (int i = 0;i < L.length;i++)
if (L.elem[i] == e)
return i + 1;
return 0;
}
插入
插入位置
- 插入位置在最后:直接插入即可
- 插入位置在中间、插入位置在最前面:将第 i 个位置及其后面的元素依次向后移动一位,将第 i 个位置空出来,然后再插入要插入的元素
算法思想
- 判断插入位置 i 是否合法
- 判断顺序表的存储空间是否已满,若已满返回 ERROR
- 将第 n 至第 i 位的元素依次向后移动一个位置,空出第 i 个位置
- 将要插入的新元素 e 放入第 i 个位置
算法分析
- 算法时间主要耗费在移动元素的操作上
-
- 插入在尾结点之后,则根本无需移动(特别快)
- 插入在首结点之前,则表中元素全部后移(特别慢)
- 插入在各种位置的平均移动次数:
可插入元素的位置有 n+1 个,插入每个元素需移动 n-i+1 次
顺序表插入算法的平均时间复杂度:O(n)
//在第i个元素之前插入
bool ListInsert_Sq(SqList& L, int i, ElemType e) {
if (i<1 || i>L.length + 1)
return false;
if (L.length == LIST_INIT_SIZE)
return false;
for (int j = L.length - 1;j >= i - 1;j--)
L.elem[j + 1] = L.elem[j];
L.elem[i - 1] = e;
L.length++;
return true;
}
删除
删除位置
- 删除位置在最后:直接删除
- 删除位置在中间、删除位置在最前面:将第 i 个位置的元素删除,然后将第 i 个位置后面的元素依次向前移动
算法思想
- 判断删除位置 i 是否合法(合法值为1<=i<=n)
- 将预删除的元素保留在 e 中
- 将第 i+1 至第 n 位的元素依次向前移动一个位置
- 表长 -1,删除成功返回 OK
算法分析
- 算法时间主要耗费在移动元素的操作上
-
- 删除尾结点,则根本无需移动(特别快)
- 删除首结点,则表中 n-1个元素全部前移(特别慢)
- 删除在各种位置的平均移动次数:
删除每个元素移动 n-i 次,一共有 n 个元素
顺序表删除算法的平均时间复杂度:O(n)
//删除第i个元素
bool ListDelete_Sq(SqList& L, int i) {
if (i < 1 || i>L.length)
return false;
for (int j = i;j <= L.length - 1;j++)
L.elem[j - 1] = L.elem[j];
L.length--;
return true;
}
输出
void DispList(SqList L) {
for (int i = 0;i < L.length;i++)
printf("%c", L.elem[i]);
printf("\n");
}
完整代码
#include<iostream>
using namespace std;
#define LIST_INIT_SIZE 100
typedef int ElemType;
//顺序表类型定义
//数组静态分配
//typedef struct {
// ElemType elem[LIST_INIT_SIZE];
// int length;//长度
//}SqList;
//数组动态分配
typedef struct {
ElemType* elem;
int length;
}SqList;
//初始化
bool InitList_Sq(SqList& L) {//构造一个空的顺序表L
L.elem = new ElemType[LIST_INIT_SIZE];//为顺序表分配空间
if (!L.elem)
return false;//存储分配失败
L.length = 0;//空表长度为0
return true;
}
//创建
void CreateList_Sq(SqList& L, int n) {
for (int i = 0;i < n;i++)
cin >> L.elem[i];
L.length = n;
}
//销毁
void DestroyList(SqList& L) {
if (L.elem)
delete L.elem;
}
//清空
void ClearList(SqList& L) {
L.length = 0;
}
//判断线性表是否为空
bool ListEmpty(SqList L) {
if (L.length == 0)
return true;
else
return false;
}
//求线性表长度
int ListLength(SqList L) {
return (L.length);
}
//获取元素
bool GetElem(SqList L, int i, ElemType& e) {
if (i<1 || i>L.length)
return false;
e = L.elem[i - 1];
return true;
}
//按值查找
int LocateElem(SqList L, ElemType e) {
for (int i = 0;i < L.length;i++)
if (L.elem[i] == e)
return i + 1;
return 0;
}
//插入
//在第i个元素之前插入
bool ListInsert_Sq(SqList& L, int i, ElemType e) {
if (i<1 || i>L.length + 1)
return false;
if (L.length == LIST_INIT_SIZE)
return false;
for (int j = L.length - 1;j >= i - 1;j--)
L.elem[j + 1] = L.elem[j];
L.elem[i - 1] = e;
L.length++;
return true;
}
//删除
//删除第i个元素
bool ListDelete_Sq(SqList& L, int i) {
if (i < 1 || i>L.length)
return false;
for (int j = i;j <= L.length - 1;j++)
L.elem[j - 1] = L.elem[j];
L.length--;
return true;
}
//输出
void DispList(SqList L) {
for (int i = 0;i < L.length;i++)
printf("%d ", L.elem[i]);
printf("\n");
}
int main() {
SqList L;
InitList_Sq(L);
int n;
printf("线性表中开始有几个数据?\n");
scanf("%d", &n);
printf("请依次输入:\n");
CreateList_Sq(L, n);
DispList(L);
printf("线性表是否为空?\n");
if (ListEmpty(L) == 1)
printf("是\n");
else
printf("不是\n");
printf("线性表中有%d个数据\n", ListLength(L));
printf("在第2个元素之前插入8\n");
ListInsert_Sq(L, 2, 8);
DispList(L);
printf("删除第4个元素\n");
ListDelete_Sq(L, 4);
DispList(L);
printf("输出8所在的位置:%d\n", LocateElem(L, 8));
printf("输出10所在的位置:%d\n", LocateElem(L, 10));
return 0;
}
链式表示
定义
链式存储结构:结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
线性表的链式表示又称为非顺序映像或链式映像
结点:数据元素的存储映像,由数据域和指针域两部分组成
链表:n 个结点由指针链组成一个链表,是线性表的链式存储映像,称为线性表的链式存储结构
单链表/线性链表:结点只有一个指针域的链表
双链表:结点有两个指针域的链表
循环链表:首尾相接的链表
头指针:指向链表中的第一个结点的指针
首元结点:链表中存储第一个数据元素的结点
头结点:在链表的首元结点之前附设的一个结点,头结点的数据域可不存储任何信息,也可存储类似于线性表的长度等类的附加信息,头结点的指针域存储指向第一个结点的指针(即第一个元素结点的存储位置)
讨论
如何表示空表?
- 无头结点时,头指针指向为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
在链表中设置头结点有什么好处?
- 便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
- 便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理就统一
头结点的数据域内装的是什么?
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值
特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点【顺序存取法】,所以寻找第一个结点和最后一个结点所花费的时间不等
优点
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来表示,插入和删除时不需要移动数据元素
缺点
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,增加了算法的复杂度
存储密度
定义:
指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即 存储密度=结点数据本身占用的空间/结点占用的空间总量
存储密度越大,存储空间的利用率越高。顺序表的存储密度为 1(100%),链表的存储密度小于 1 。
单链表
带头结点的单链表
单链表是由表头唯一确定,因此单链表可以用头指针的名字命名,若头指针名是 L,则把链表称为表 L
//当结点的数据域有多项内容时,为了同一链表的操作,进行如下定义
typedef struct {
char data1;
int data1;
...
}ElemType;
typedef struct Lnode {//声明结点类型和指向结点的指针类型
ElemType data;//结点的数据域
struct Lnode* next;//结点的指针域
}Lnode,*LinkList;
初始化
算法步骤
- 生成新结点作头结点,用头指针 L 指向头结点
- 将头结点的指针域置空
bool InitList_L(LinkList& L) {
L = new Lnode;
if (!L)
return false;
L->next = NULL;
return true;
}
建立单链表
头插法/前插法 O(n)
元素插入在链表头部
倒位序输入 n 个元素的值,建立带头结点的单链表
为什么倒序输入 n 个元素的值?
使用头插法创建链表时,每次将新生成的结点插入到头结点与首元结点之间,使得新产生的结点位于链表表头,与其逻辑位置相反,本该处于链表最前面的元素反而位于链表的表尾,使得其他操作无法进行,为避免该情况出现,需倒序输入元素,使得其逻辑位置与物理位置相一致。
算法步骤
- 从一个空表 L 开始,重复读入数据
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将各结点插入到链表的前端
void CreateList_H(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;//建立一个带头结点的单链表
for (int i = n;i > 0;i--) {
LinkList p = new Lnode;//生成新结点
cin >> p->data;//输入数据域
p->next = L->next;
L->next = p;
}
}
尾插法/后插法 O(n)
元素插入在链表尾部
正位序输入 n 个元素的值,建立带头结点的单链表
算法步骤
- 从一个空表 L 开始,将新结点逐个插入到链表的尾部,尾指针 r 指向链表的尾结点
- 初始时,r 和 L 均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r 指向新结点
void CreateList_R(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;
LinkList r = L;
for (int i = 0;i < n;i++) {
LinkList p = new Lnode;//生成新的结点
cin >> p->data;//输入数据域
p->next = NULL;
r->next = p;//插入到表尾
r = p;//r指向新的尾结点
}
}
判断链表是否为空
算法思路
判断头结点指针域是否为空
bool ListEmpty(LinkList L) {
if (L->next)
return false;
else
return true;
}
销毁
链表销毁后不存在
算法思路
从头指针开始,依次释放所有结点
void DestroyList_L(LinkList& L) {
LinkList p;
while (L) {
p = L;
L = L->next;
delete p;
}
}
清空
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍存在)
算法思路
依次释放所有结点,并将头结点指针域设置为空
void ClearList(LinkList& L) {
LinkList p, q;
p = L->next;
while (p) {
q = p->next;
delete p;
p = q;
}
L->next = NULL;
}
求单链表的表长
算法思路
从首元结点开始,依次计数所有结点
int ListLength_L(LinkList L) {
LinkList p = L;//p指向为头结点,i设置为0,即头结点的序号为0
int i = 0;
while (p->next) {//遍历单链表,统计结点数
i++;
p = p->next;
}
return i;
}
取值——取单链表中第i个元素的内容
算法思路
从链表的头指针出发,顺着链域 next 逐个结点往下搜索,直至搜索到第i个结点为止【链表不是随机存取结构】
算法步骤
- 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p 初值 p=L->next
- j 做计数器,累计当前扫描过的结点数,j 的初值为1
- 当 p 指向扫描到的下一个结点时,计数器 j 加1
- 当 j==i 时,p 所指的结点就是要找到的第 i 个结点
bool GetElem_L(LinkList L, int i, ElemType& e) {//获取线性表L中的某个数据元素的内容,通过变量e返回
LinkList p;
p = L->next;//p指向首元结点
int j = 1;
while (p && j < i) {//向后扫描,直到p指向第i个元素或p为空
p = p->next;
j++;
}
if (!p || j > i)
return false;
e = p->data;
return true;
}
查找 O(n)
按值查找——根据指定数据获取该数据所在的位置(地址)
算法步骤
- 从第一个结点起,依次和e相比较
- 如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或地址
- 如果查遍整个链表都没有找到其值和e相等的元素,则返回 0 或 NULL
Lnode* LocateElem_L(LinkList L, ElemType e) {
//在线性表L中查找值为e的数据元素,找到返回L中值为e的数据元素的地址,查找失败返回NULL
LinkList p = L->next;
while (p && p->data != e)
p = p->next;//循环结束后会出现两种情况,一种是找到直接返回p,另一种是没有找,p指向为空,直接返回p即可
return p;
}
按值查找——根据指定数据获取该数据所在的位置序号(是第几个数据元素)
时间复杂度
因为链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)
int LocateElem_LL(LinkList L, ElemType e) {//查找失败返回0
LinkList p = L->next;
int j = 1;
while (p && p->data != e) {
p = p->next;
j++;
}
if (p)
return j;
else
return 0;
}
插入
在第 i 个结点前插入值为 e 的新结点 O(n)
算法步骤
- 首先找到 的存储位置 p
- 生成一个数据域为 e 的新结点 s
- 插入新结点【新结点的指针域指向结点 — >结点 的指针域指向新结点】
bool ListInsert_L(LinkList& L, int i, ElemType e) {
LinkList p = L;
int j = 0;
while (p && j < i - 1) {//找到第i-1个位置,或者给出的位置超过表长
p = p->next;
j++;
}
if (!p || j > i - 1)//插入非法位置,j>i-1,如果i<1,则i-1<0
return false;
LinkList s = new Lnode;//生成新的结点s
s->data = e;//将结点s的数据域设置为e
s->next = p->next;//将结点s插入L中
p->next = s;
return true;
}
时间复杂度
因线性表不移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。
删除 O(n)
删除第 i 个结点
算法步骤
- 首先找到 的存储位置 p,保存要删除的 的值
- 令 p->next 指向
- 释放结点 的空间
时间复杂度
因线性表不移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。
bool ListDelete_L(LinkList& L, int i, ElemType& e) {
LinkList p = L;
int j = 0;
while (p->next && j < i - 1) {//寻找第i个结点,并令p指向其前驱
p = p->next;
j++;
}
if (!(p->next) || j > i - 1)//只能删除第1~n个位置的结点,大于n的!(p->next),小于1的j>i-1,删除位置不合理
return false;
LinkList q = p->next;//q指向要删除的结点,临时保存被删结点的地址以备释放
p->next = q->next;//改变删除结点前驱结点的指针域
e = q->data;//保存删除结点的数据域
delete q;//释放删除结点的空间
return true;
}
输出
void DispList(LinkList L) {
LinkList i = L;
while (i->next) {
i = i->next;
cout << i->data << " ";
}
cout << endl;
}
完整代码
#include<iostream>
using namespace std;
typedef int ElemType;
typedef struct Lnode {//声明结点类型和指向结点的指针类型
ElemType data;//结点的数据域
struct Lnode* next;//结点的指针域
}Lnode,*LinkList;
//初始化
bool InitList_L(LinkList& L) {
L = new Lnode;
if (!L)
return false;
L->next = NULL;
return true;
}
//建立单链表
//头插法建立单链表
void CreateList_H(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;//建立一个带头结点的单链表
for (int i = n;i > 0;i--) {
LinkList p = new Lnode;//生成新结点
cin >> p->data;//输入数据域
p->next = L->next;
L->next = p;
}
}
//尾插法建立单链表
void CreateList_R(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;
LinkList r = L;
for (int i = 0;i < n;i++) {
LinkList p = new Lnode;//生成新的结点
cin >> p->data;//输入数据域
p->next = NULL;
r->next = p;//插入到表尾
r = p;//r指向新的尾结点
}
}
//判断链表是否为空
bool ListEmpty(LinkList L) {
if (L->next)
return false;
else
return true;
}
//销毁
void DestroyList_L(LinkList& L) {
LinkList p;
while (L) {
p = L;
L = L->next;
delete p;
}
}
//清空
void ClearList(LinkList& L) {
LinkList p, q;
p = L->next;
while (p) {
q = p->next;
delete p;
p = q;
}
L->next = NULL;
}
//求单链表长度
int ListLength_L(LinkList L) {
LinkList p = L;//p指向为头结点,i设置为0,即头结点的序号为0
int i = 0;
while (p->next) {//遍历单链表,统计结点数
i++;
p = p->next;
}
return i;
}
//取值——取单链表中第i个元素的内容
bool GetElem_L(LinkList L, int i, ElemType& e) {//获取线性表L中的某个数据元素的内容,通过变量e返回
LinkList p;
p = L->next;//p指向首元结点
int j = 1;
while (p && j < i) {//向后扫描,直到p指向第i个元素或p为空
p = p->next;
j++;
}
if (!p || j > i)
return false;
e = p->data;
return true;
}
//按值查找——根据指定数据获取该数据所在的位置(地址)
Lnode* LocateElem_L(LinkList L, ElemType e) {
//在线性表L中查找值为e的数据元素,找到返回L中值为e的数据元素的地址,查找失败返回NULL
LinkList p = L->next;
while (p && p->data != e)
p = p->next;//循环结束后会出现两种情况,一种是找到直接返回p,另一种是没有找,p指向为空,直接返回p即可
return p;
}
//按值查找——根据指定数据获取该数据所在的位置序号(是第几个数据元素)
int LocateElem_LL(LinkList L, ElemType e) {//查找失败返回0
LinkList p = L->next;
int j = 1;
while (p && p->data != e) {
p = p->next;
j++;
}
if (p)
return j;
else
return 0;
}
//插入——在第i个结点前插入值为e的新结点
bool ListInsert_L(LinkList& L, int i, ElemType e) {
LinkList p = L;
int j = 0;
while (p && j < i - 1) {//找到第i-1个位置,或者给出的位置超过表长
p = p->next;
j++;
}
if (!p || j > i - 1)//插入非法位置,j>i-1,如果i<1,则i-1<0
return false;
LinkList s = new Lnode;//生成新的结点s
s->data = e;//将结点s的数据域设置为e
s->next = p->next;//将结点s插入L中
p->next = s;
return true;
}
//删除——删除第i个结点
bool ListDelete_L(LinkList& L, int i, ElemType& e) {
LinkList p = L;
int j = 0;
while (p->next && j < i - 1) {//寻找第i个结点,并令p指向其前驱
p = p->next;
j++;
}
if (!(p->next) || j > i - 1)//只能删除第1~n个位置的结点,大于n的!(p->next),小于1的j>i-1,删除位置不合理
return false;
LinkList q = p->next;//q指向要删除的结点,临时保存被删结点的地址以备释放
p->next = q->next;//改变删除结点前驱结点的指针域
e = q->data;//保存删除结点的数据域
delete q;//释放删除结点的空间
return true;
}
//输出
void DispList(LinkList L) {
LinkList i = L;
while (i->next) {
i = i->next;
cout << i->data << " ";
}
cout << endl;
}
int main() {
LinkList L1, L2;
int n1, n2;
cout << "头插法建立链表L1:1 2 3 4 5" << endl;
cout << "输入L1的数据数" << endl;
cin >> n1;
CreateList_H(L1, n1);
DispList(L1);
cout << "头插法建立链表L2:6 7 8 9" << endl;
cout << "输入L2的数据数" << endl;
cin >> n2;
CreateList_R(L2, n2);
DispList(L2);
cout << "判断链表L2是否为空" << endl;
if (ListEmpty(L2))
cout << "YES" << endl;
else
cout << "NO" << endl;
cout << "单链表L2有" << ListLength_L(L2) << "个元素" << endl;
cout << "单链表L2中8的位置为:" << LocateElem_L(L2, 8) << endl;
cout << "单链表L2中8是:" << LocateElem_LL(L2, 8) << endl;
cout << "在单链表的第2个结点之前插入10" << endl;
ListInsert_L(L2, 2, 10);
DispList(L2);
cout << "删除L2的第3个结点" << endl;
ElemType temp;
ListDelete_L(L2, 3, temp);
cout << "删除的是:" << temp << endl;
DispList(L2);
return 0;
}
单循环链表
定义
是一种头尾相接的链表,即表中的最后一个结点的指针域指向头结点,整个链表形成一个环。
优点
从表中任一结点出发均可找到表中其他结点。
注意
由于循环链表中没有 NULL 指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断 p 或 p->next 是否为空,而是判断它们是否等于头指针。
单链表 | 单循环链表 |
p != NULL | p != L |
p->next != NULL | p->next != L |
时间复杂度
表的操作常常是在表的首尾位置上进行
带尾指针循环链表的合并
- p 存表头结点【因为在进行下一步操作的时候,会将 Ta 的表头结点丢掉】
p=Ta->next;
- Tb 表头连接到 Ta 表尾
Ta->next=Tb->next->next;
- 释放 Tb 表头结点
delete Tb->next;
- 修改指针
Tb->next=p;
//带尾指针循环链表的合并
LinkList Connect(LinkList Ta,LinkList Tb){//假设Ta、Tb都是非空的单循环链表
p=Ta->next;
Ta->next=Tb->next->next;
delete Tb->next;//free(Tb->next);
Tb->next=p;
return Tb;
}
双向链表
定义
在单链表的每个结点里再增加一个指向其直接前驱的指针域 prior,这样链表中就形成了有两个方向不同的链。
注意
在双向链表中有些操作,因只涉及一个方向的指针,故其算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为 O(n)。
类型定义
typedef struct DuLnode{
Elemtype data;
struct DuLnode *prior,*next;
}DuLnode,*DuLinkList;
插入
bool ListInsert_DuL(DuLinkList &L,int i,ElemType e){//在带头结点的双向循环链表L中的第i个位置之前插入元素e
DuLinkList p;
if(!(p=GetElemP_DuL(L,i)))
return false;
DuLinkList s=new DuLnode;
s->data=e;
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return true;
}
删除
true ListDelete_DuL(DuLinkList &L,int i,ElemType &e){//删除带头结点的双向循环链表L的第i个元素,并用e返回
DuLinkList p;
if(!(p=GetElemP_DuL(L,i)))
return false;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return true;
}
双循环链表
和单链的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
对称性
设指针 p 指向某一结点,p->prior->next=p=p->next->prior;
时间效率比较
查找表头结点(首元结点) | 查找表尾结点 | 查找结点 *p 的前驱结点 | |
带头结点的单链表 L | L->next O(1) | 从 L->next 依次向后遍历 O(n) | 通过 p->next 无法找到其前驱 |
带头结点仅设头指针 L 的循环单链表 | L->next O(1) | 从 L->next 依次向后遍历 O(n) | 通过 p->next 可以找到其前驱 O(n) |
带头结点仅设尾指针 R 的循环单链表 | R->next->next O(1) | R P(1) | 通过 p->next 可以找到其前驱 O(n) |
带头结点的双向循环链表 L | L->next O(1) | L->prior O(1) | p->prioi O(1) |
顺序表和链表的比较
顺序表 | 链表 | ||
空间 | 存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配,不会出现存储空间闲置或溢出现象 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于 1 | |
时间 | 存取元素 | 随机存取,按位置访问元素 O(1) | 顺序存取,按位置访问元素 O(n) |
插入、删除 | 平均移动约表中一半元素 O(n) | 不需移动元素,确定插入、删除位置后,时间复杂度 O(1) | |
适用空间 |
|
|
应用
线性表的合并
问题描述
假设利用两个线性表 La 和 Lb 分别表示两个集合 A 和 B ,现要求一个新的集合 A=A∪B 。
算法步骤
依次取出 Lb 中的元素,执行以下操作:
- 在 La 中查找该元素
- 如果找不到,则将其插入 La 的最后
实现
void union(SqList &La,SqList Lb){
La_len=ListLength(La);
Lb_len=ListLength(Lb);
for(int i=1;i<=Lb_len;i++){
GetElem(Lb,i,e);
if(!LocateElem(La,e))
ListInsert(&La,++La_len,e);
}
}
有序表的合并
问题描述
已知线性表 La 和 Lb 中的数据元素按值非递减有序排列,现要求将 La 和 Lb 归并为一个新的线性表 Lc,且 Lc 中的数据元素仍按值非递减有序排列。
算法步骤
- 创建一个空表 Lc
- 依次从 La 或 Lb 中“摘取”元素值较小的结点插入到 Lc 表的最后,直至其中一个表变空为止
- 继续将 La 或 Lb 其中一个表的剩余结点插入在 Lc 表的最后
算法效率
顺序表
时间复杂度 O(ListLength(La)+ListLength(Lb))
空间复杂度 O(ListLength(La)+ListLength(Lb))
链表
时间复杂度 O(ListLength(La)+ListLength(Lb))
//顺序表实现
void MergeList_Sq(SqList La,SqList Lb,SqList &Lc){
SqList pa=La.elem;
SqList pb=Lb.elem;//指针pa和pb的初值分别指向两个表的第一个元素
Lc.length=La.length+Lb.length;//新表长度为待合并两表的长度之和
Lc.elem=new ElemType[Lc.length];//为合并后的新表分配一个数组空间
SqList pc=Lc.elem;//指针pc指向新表的第一个元素
SqList pa_last=La.elem+La.length-1;//指针pa_last指向La表的最后一个元素
SqList pb_last=Lb.elem+Lb.length-1;//指针pb_last指向Lb表的最后一个元素
while(pa<=pa_last&&pb<=pb_last){//两个表都非空
if(*pa<=*pb)
*pc++=*pa++;//依次“摘取”两表中值较小的结点
else
*pc++=*pb++;
}
while(pa<=pa_last)
*pc++=*pa++;//Lb表已到达表尾,将La中剩余元素加入Lc
while(pb<=pb_last)
*pc++=*pb++;//La表已到达表尾,将Lb中剩余元素加入Lc
}
//链表实现
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
LinkList pa=La->next;
LinkList pb=Lb->next;
LinkList pc=Lc=La;//用La的头结点作为Lc的头结点
while(pa&&pb){
if(pa->data<=pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}
else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb;//插入剩余段【判断条件是pa是否为空,非空pc->next=pa,空pc->next=pb】
delete Lb;//释放Lb的头结点
}
稀疏多项式
顺序表
下标 i | 0 | 1 | ... | m-1 |
系数 a[i] | p1 | p2 | ... | pm |
指数 | e1 | e2 | ... | em |
线性表 P=((p1,e1),(p2,e2),...,(pm,em))
多项式相加
创建一个新数组 c
分别从头遍历比较 a 和 b 的每一项
- 指数相同,对应系数相加,若其和不为零,则在 c 中增加一个新项
- 指数不相同,则将指数较小的项复制到 c 中
一个多项式已遍历完毕时,将另一个剩余项依次复制到 c 中即可
问题
数组 c 开多大合适?
存在的问题
- 存储空间分配不灵活
- 运算的空间复杂度高
链表
多项式创建
算法步骤
- 创建一个只有头结点的空链表
- 根据多项式的项的个数 n ,循环 n 次执行以下操作:
- 生成一个新结点 *s
- 输入多项式当前项的系数和指数赋给新结点 *s 的数据域
- 设置一前驱指针 pre,用于指向待找到的第一个大于输入项指数的结点的前驱,pre 初值指向头结点
- 指针 q 初始化,指向首元结点
- 循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点 *q
- 将输入项结点 *s 插入到结点 *q 之前
多项式相加
算法步骤
指针 p1 和 p2 初始化,分别指向 Pa 和 Pb 的首元结点
p3 指向和多项式的当前结点,初值为 Pa 的头结点
当指针 p1 和 p2 均未到达相应表尾时,则循环比较 p1 和 p2 所指结点对应的指数值
p1->expn 与 p2->expn ,有下列三种情况:
p1->expn==p2->expn
时,将两个结点中的系数相加p1->expn<p2->expn
时,摘取 p1 所指结点插入到和多项式链表中p1->expn>p2->expn
时,摘取 p2 所指结点插入到和多项式链表中将非空多项式的剩余段插入到 p3 所指结点之和
释放 Pb 的头结点
typedef struct PNode{
float coef;//系数
int expn;//指数
struct PNode *next;//指针域
}PNode,*Polynomial;
//多项式创建
void CreatPolyn(Polynomial &P,int n){//输入m项的系数和指数,建立表示多项式的有序链表P
P=new PNode;
P->next=NULL;//先建立一个带头结点的单链表
for(int i=1;i<=n;i++){//依次输入n个非零项
Polynomial s=new PNode;//生成新结点
cin>>s->coef>>s->expn;//输入系数和指数
Polynomial pre=P;//pre用于保存q的前驱,初值为头结点
Polynomial q=P->next;//q初始化,指向首元结点
while(q&&q->expn<s->nexpn){//找到第一个大于输入项指数的项*q
pre=q;
q=q->next;
}
s->next=q;
pre->next=s;//将输入项s插入到q和其前驱结点pre之间
}
}
void Add(Polynomial &Pa,Polynomial &Pb){
Polynomial p1=Pa->next;
Polynomial p2=Pb->next;
Polynomial p3=Pa;
while(p1&&p2){
if(p1->expn==p2->expn){
p1->coef+=p2->coef;
p3=p1;
p1=p1->next;
p2=p2->next;
}
else if(p1->expn<p2->expn){
p3=p1;
p1=p1->next;
}
else{
Polynomial temp=p2->next;
p2->next=p1;
p3->next=p2;
p3=p2;
p2=temp;
}
}
if(!p1)
p3->next=p2;
}
void DispList(Polynomial L){
Polynomial i = L;
while (i->next) {
i = i->next;
printf("(指数:%d,系数:%f) ", i->expn,i->coef);
}
printf("\n");
}
int main(){
int n1, n2;
cin >> n1 >> n2;
Polynomial L1;
CreatPolyn(L1, n1);
Polynomial L2;
CreatPolyn(L2, n2);
DispList(L1);
DispList(L2);
Add(L1, L2);
DispList(L1);
return 0;
}
单链表的逆置
就地逆置
遍历原链表中的每一个结点,改变其next指针的指向,为了防止改变next指针指向过程中结点的丢失,借助tmp_next指针(指向tmp的后继)来完成。
void ReverseList(LinkList L) {
LinkList tmp_pre = NULL;
LinkList tmp = L->next;
LinkList tmp_next = NULL;
while (tmp) {
tmp_next = tmp->next;
tmp->next = tmp_pre;
tmp_pre = tmp;
tmp = tmp_next;
}
L->next = tmp_pre;
}
头插法
创建一个新表,作为逆置链表,依次遍历原链表的每一个结点,将其插入到逆置链表的头结点之后。
void ReverseList(LinkList& L) {
LinkList p, revlist;
p = L->next;
revlist = NULL;
while (p) {
LinkList q = p;
p = p->next;
q->next = revlist;
revlist = q;
}
L->next = revlist;
}
递归
//递归逆置
#include<iostream>
using namespace std;
typedef int ElemType;
typedef struct Lnode {
ElemType data;
struct Lnode* next;
}Lnode,*LinkList;
//创建单链表,无头结点
void CreateList(LinkList &L,int n) {
L = NULL;
for (int i = 0;i < n;i++) {
LinkList p = new Lnode;
cin >> p->data;
p->next = L;
L = p;
}
}
//输出
void DispList(LinkList L) {
while (L!=NULL) {
printf("%d ", L->data);
L = L->next;
}
printf("\n");
}
//递归逆置
LinkList ReverseList(LinkList L) {
if (L == NULL || L->next == NULL)//当链表L为空或者链表L中只有一个结点时,直接返回该链表即可
return L;
LinkList revlist = ReverseList(L->next);
L->next->next = L;//改变当前结点的前一个结点的指针域,使其指向当前结点
L->next = NULL;//将当前结点的指针域置为空,为了统一操作,最后逆置完成之后,原来的第一个结点的指针域指向为空
return revlist;
}
int main() {
int n;
cin >> n;
LinkList L;
CreateList(L, n);
DispList(L);
DispList(ReverseList(L));
return 0;
}
练习题
一、 选择题
1.下述哪一条是顺序存储结构的优点?( )
A.存储密度大 B.插入运算方便 C.删除运算方便 D.可方便地用于各种逻辑结构的存储表示
2.下面关于线性表的叙述中,错误的是哪一个?( )
A.线性表采用顺序存储,必须占用一片连续的存储单元。
B.线性表采用顺序存储,便于进行插入和删除操作。
C.线性表采用链接存储,不必占用一片连续的存储单元。
D.线性表采用链接存储,便于插入和删除操作。
3.线性表是具有n个( )的有限序列(n≥0)。
A.表元素 B.字符 C.数据元素 D.数据项 E.信息项
4.若某线性表最常用的操作是存取任一指定序号的元素和在最后进行插入和删除运算,则利用( )存储方式最节省时间。
A.顺序表 B.双链表 C.带头结点的双循环链表 D.单循环链表
5.下面的叙述不正确的是( )
A.线性表在链式存储时,查找第i个元素的时间同i的值成正比
B. 线性表在链式存储时,查找第i个元素的时间同i的值无关
C. 线性表在顺序存储时,查找第i个元素的时间同i 的值成正比
D. 线性表在顺序存储时,查找第i个元素的时间同i的值无关
6.设一个链表最常用的操作是在末尾插入结点和删除尾结点,则选用( )最节省时间。
A. 单链表 B.单循环链表 C. 带尾指针的单循环链表 D.带头结点的双循环链表
7.链表不具有的特点是( )
A.插入、删除不需要移动元素 B.可随机访问任一元素
C.不必事先估计存储空间 D.所需空间与线性长度成正比
8.在单链表指针为p的结点之后插入指针为s的结点,正确的操作是:( )。
A.p->next=s;s->next=p->next; B. s->next=p->next;p->next=s;
C.p->next=s;p->next=s->next; D. p->next=s->next;p->next=s;
9.对于一个头指针为head的带头结点的单链表,判定该表为空表的条件是( )
A.head==NULL B.head→next==NULL
C.head→next==head D.head!=NULL
10.对于顺序存储的线性表,访问结点和增加、删除结点的时间复杂度为( )。
A.O(n) O(n) B. O(n) O(1) C. O(1) O(n) D. O(1) O(1)
11.循环链表h的尾结点p的特点是( )
A.p→next==h B.p→next==h->next C.p==h D.p==h->next
12.完成在双循环链表结点p之后插入s的操作是( )
A. p->next=s ; s->prior=p; p->next->prior:=s ; s->next=p->next;
B. p->next->prior=s; p->next=s; s->prior=p; s->next:=p->next;
C.s->prior=p; s->next:=p->next; p->next=s; p->next->prior=s ;
D.s->prior=p; s->next:=p->next; p->next->prior=s ; p->next=s;
13.双向链表中有两个指针域,llink和rlink分别指向前趋及后继,设p指向链表中的一个结点,现要求删去p所指结点,则正确的删除是( )(链中结点数大于2,p不是第一个结点)
A.p->llink->rlink=p->llink; p->llink->rlink=p->rlink; free(p);
B.free(p); p->llink->rlink=p->llink; p->llink->rlink=p->rlink;
C.p->llink->rlink=p->llink; free(p); p->llink->rlink=p->rlink;
D.以上A,B,C都不对。
二、填空
1.当线性表的元素总数基本稳定,且很少进行插入和删除操作,但要求以最快的速度存取线性表中的元素时,应采用_______存储结构。
2.线性表L=(a1,a2,…,an)用数组表示,假定删除表中任一元素的概率相同,则删除一个元素平均需要移动元素的个数是________。
4.在一个长度为n的顺序表中第i个元素(1<=i<=n)之前插入一个元素时,需向后移动________个元素。
5.在单链表中设置头结点的作用是________。
6.对于一个具有n个结点的单链表,在已知的结点*p后插入一个新结点的时间复杂度为________。
在给定值为x的结点后插入一个新结点的时间复杂度为________。
7. 在双向链表结构中,若要求在p 指针所指的结点之前插入指针为s 所指的结点,则需执行下列语句:
s->next=p; s->prior= ________;p->prior=s;________=s;
8.顺序存储结构是通过________表示元素之间的关系的;链式存储结构是通过________表示元素之间的关系的。
9. 对于双向链表,在两个结点之间插入一个新结点需修改的指针共 ______个,单链表为_______个。
10. 循环单链表的最大优点是:________。
11. 带头结点的双循环链表L中只有一个元素结点的条件是:________
12. 在单链表L中,指针p所指结点有后继结点的条件是:________
13. 带头结点的双循环链表L为空表的条件是:________。
三. 算法设计题
1. 假设有两个按元素值递增次序排列的线性表,均以单链表形式存储。请编写算法将这两个单链表归并为一个按元素值递减次序排列的单链表,并要求利用原来两个单链表的结点存放归并后的单链表。
3. 已知L1、L2分别为两循环单链表的头结点指针,m,n分别为L1、L2表中数据结点个数。要求设计一算法,用最快速度将两表合并成一个带头结点的循环单链表。
4. 顺序结构线性表LA与LB的结点关键字为整数。LA与LB的元素按非递减有序,线性表空间足够大。试用类C语言给出一种高效算法,将LB中元素合到LA中,使新的LA的元素仍保持非递减有序。高效指最大限度的避免移动元素。
5. 设 Listhead为一单链表的头指针,单链表的每个结点由一个整数域DATA和指针域NEXT组成,整数在单链表中是无序的。编一类C过程,将 Listhead链中结点分成一个奇数链和一个偶数链,分别由P,Q指向,每个链中的数据按由小到大排列。程序中不得使用 NEW过程申请空间。
6. 试编写在带头结点的单链表中删除(一个)最小值结点的(高效)算法。
7. 已知非空线性链表由list指出,链结点的构造为(data,link).请写一算法,将链表中数据域值最小的那个链结点移到链表的最前面。要求:不得额外申请新的链结点。
8. 已知p指向双向循环链表中的一个结点,其结点结构为data、llink、rlink三个域,写出算法change(p),交换p所指向的结点和它的前缀结点的顺序。
9 已知两个单链表A和B,其头指针分别为heada和headb,编写一个过程从单链表A中删除自第i个元素起的共len个元素,然后将单链表A插入到单链表B的第j个元素之前。
10.在一个递增有序的线性表中,有数值相同的元素存在。若存储方式为单链表,设计算法去掉数值相同的元素,使表中不再有重复的元素。例如:(7,10,10,21,30,42,42,42,51,70)将变作(7,10,21,30,42,51,70),分析算法的时间复杂度。