1. 线性表数据结构系列内容的学习目录 → \rightarrow →浙大版数据结构学习系列内容汇总。
1.1 引入:多项式的表示
例:一元多项式及其运算
一元多项式:
f
(
x
)
=
a
0
+
a
1
x
+
.
.
.
+
a
n
−
1
x
n
−
1
+
a
n
x
n
f(x) = a_{0} + a_{1}x+...+a_{n-1}x^{n-1}+a_{n}x^{n}
f(x)=a0+a1x+...+an−1xn−1+anxn
主要运算: 多项式相加、相减、相乘等。
分析: 如何表示多项式?
⋄
\diamond
⋄ 多项式的关键数据:多项式项数n
⋄
\diamond
⋄ 各项系数
a
i
a_{i}
ai及指数
i
i
i
-
方法1: 顺序存储结构直接表示
数组各分量对应多项式各项,a[i]为项 x i x^{i} xi的系数 a i a_{i} ai。例如, f ( x ) = 4 x 5 − 3 x 2 + 1 f(x) = 4x^{5}-3x^{2}+1 f(x)=4x5−3x2+1表示成
两个多项式相加:两个数组对应分量相加。
问题: 如何表示多项式 x + 3 x 2000 x+3x^{2000} x+3x2000? -
方法2: 顺序存储结构表示非零项
每个非零项 a i x i a_{i}x^{i} aixi涉及两个信息:系数 a i a_{i} ai和指数 i i i,可以将一个多项式看成是一个 ( a i , i ) (a_{i}, i) (ai,i)二元组的集合。
用结构数组表示:数组分量是由系数 a i a_{i} ai、指数 i i i组成的结构,对应一个非零项。
例如, P 1 ( x ) = 9 x 12 + 15 x 8 + 3 x 2 P_{1}(x) = 9x^{12}+15x^{8}+3x^{2} P1(x)=9x12+15x8+3x2, P 2 ( x ) = 26 x 19 − 4 x 8 − 13 x 6 + 82 P_{2}(x) = 26x^{19}-4x^{8}-13x^{6}+82 P2(x)=26x19−4x8−13x6+82表示成
按指数大小有序存储!
相加过程:从头开始,比较两个多项式当前对应项的指数,若相同,将系数相加。即 P 3 ( x ) = P 1 ( x ) + P 2 ( x ) = 26 x 19 + 9 x 12 + 11 x 8 − 13 x 6 + 3 x 2 + 82 P_{3}(x) = P_{1}(x)+P_{2}(x)=26x^{19}+9x^{12}+11x^{8}-13x^{6}+3x^{2}+82 P3(x)=P1(x)+P2(x)=26x19+9x12+11x8−13x6+3x2+82。 -
方法3: 链表结构存储非零项
链表中每个结点存储多项式中的一个非零项,包括系数和指数两个数据域以及一个指针域。
例如 P 1 ( x ) = 9 x 12 + 15 x 8 + 3 x 2 P_{1}(x) = 9x^{12}+15x^{8}+3x^{2} P1(x)=9x12+15x8+3x2, P 2 ( x ) = 26 x 19 − 4 x 8 − 13 x 6 + 82 P_{2}(x) = 26x^{19}-4x^{8}-13x^{6}+82 P2(x)=26x19−4x8−13x6+82,链表存储形式如下所示。
链表结构定义如下所示。
typedef struct PolyNode *Polynomial;
struct PolyNode {
int coef;
int expon;
Polynomial link;
}
多项式表示问题的启示:1.同一个问题可以有不同的表示(存储)方法;
2.有一类共性问题:有序线性序列的组织和管理。
1.2 什么是线性表
线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表,存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点)。
“线性表(Linear List)”: 由同类型数据元素构成有序序列的线性结构。
⋄
\diamond
⋄ 表中元素个数称为线性表的长度;
⋄
\diamond
⋄ 线性表没有元素时,称为空表;
⋄
\diamond
⋄ 表起始位置称为表头,表结束位置称表尾。
1.3 线性表的抽象数据类型描述
类型名称: 线性表(List)
数据对象集: 线性表是 n (≥0) 个元素构成的有序序列
{
a
1
,
a
2
,
…
,
a
n
}
\{a_1,a_2,…,a_n\}
{a1,a2,…,an}
操作集: 线性表L
∈
∈
∈ List,整数 i 表示位置,元素 X
∈
∈
∈ElementType
线性表基本操作主要有:
-
List MakeEmpty()
: 初始化一个空线性表L; -
ElementType FindKth(int K, List L)
:根据位序K,返回相应元素; -
int Find(ElementType X, List L)
:在线性表L中查找X的第一次出现位置; -
void Insert(ElementType X, int i, List L)
:在位序i前插入一个新元素X; -
void Delete(int i, List L)
:删除指定位序i的元素; -
int Length(List L)
:返回线性表 的长度n。
1.4 线性表的顺序存储实现
利用数组的连续存储空间存放线性表的各元素。线性表的顺序存储中的下标从 0 开始。
线性表的顺序存储数据结构定义如下所示。
typedef struct LNode *List;
struct LNode{
ElementType Data[MAXSIZE];
int Last;
};
struct LNode L;
List PtrL;
访问下标为 i 的元素:L.Data[i]
或PtrL->Data[i]
。
线性表的长度:L.Last+1
或PtrL->Last+1
。
主要操作的实现:
1. 初始化(建立空的顺序表)
List MakeEmpty( )
{
List PtrL;
PtrL= (List )malloc( sizeof(struct LNode)) ;
PtrL->Last = -1;
return PtrL;
}
2. 查找
(1)按值查找:Find
int Find(ElementType X,List PtrL)
{
int i = 0;
while( i <= PtrL->Last && PtrL->Data[i]!= X )
{
i++;
}
if (i > PtrL->Last)
return -1; //如果没找到,返回-1
else
return i; //找到后返回的是存储位置·
}
(2)按序号查找:FindKth
ElementType FindKth(int K, List L)
{
if (K < 0 || L->Last < K) //位置越界
{
return NULL;
}
return L->Data[K];
}
查找成功的平均比较次数为 ( n + 1 ) / 2 (n+1)/2 (n+1)/2,平均时间性能为 O ( n ) O(n) O(n)。
3. 插入 (第 i ( 1 ⩽ i ⩽ n + 1 ) i (1 \leqslant i \leqslant n+1) i(1⩽i⩽n+1)个位置上插入一个值为X的新元素)
void Insert(ElementType x, int i, List PtrL)
{
if (PtrL->Last == MAXSIZE-1) //表空间已满,不能插入
{
cout << "表满" << endl;
return;
}
if ( i < 1 || i > PtrL->Last+2)
{
cout << "位置不合法" << endl;
return;
}
for (int j = PtrL->Last; j >= i-1; j--)
{
PtrL->Data[j+1]=PtrL->Data[j]; //将ai~an倒序向后移动
}
PtrL->Data[i-1] = X; //新元素插入
PtrL->Last++; //Last仍指向最后元素
return;
}
平均移动次数为 n / 2 n/2 n/2,平均时间性能为 O ( n ) O(n) O(n)。
4. 删除 (删除表的第 i i i ( i ( 1 ⩽ i ⩽ n ) i (1 \leqslant i \leqslant n) i(1⩽i⩽n))个位置上的元素)
void Delete(int i, List PtrL)
{
if(i < 1 || i > PtrL->Last+1 )
{
cout << "不存在第" << i << "个元素" << endl;
return ;
}
for (int j = i; j <=PtrL->Last; j++)
{
PtrL->Data[j-1]= PtrL->Data[j]; //将a[i+1]~a[n]顺序向前移动
}
PtrL->Last--; //Last仍指向最后元素
return;
}
平均移动次数为 ( n − 1 ) / 2 (n-1) / 2 (n−1)/2,平均时间性能为 O ( n ) O(n) O(n)。
线性表的顺序存储实现实例的代码如下所示。
#include<iostream>
using namespace std;
#define MAXSIZE 100 // MAXSIZE 定义为 Data 数组的大小
typedef int ElementType; // ElementType 可定义为任意类型;typedef为现有类型创建一个新的名字
typedef struct LNode *List; //typedef是预定义关键字,相当于类型重命名,起个简单的名字,这里就是用List表示struct LNode结构体指针
struct LNode
{
ElementType Data[MAXSIZE];
int Last; // Last 定义线性表的最后一个元素
};
List L;
//访问下标为 i 的元素:L->Data[i],下标从 0 开始
//线性表的长度:L->Last+1
List MakeEmpty(); //初始化顺序表
int Find(ElementType X, List L); //查找 X 第一次出现的下标
void Insert(ElementType X, int i, List L); //在下标为 i 的地方插入 X
void Delete(int i, List L); //删除下标为 i 的当前值
ElementType FindKth(int K, List L); //返回下标为 K 的当前值
int Length(List L); //返回顺序表的长度
//初始化
List MakeEmpty()
{
List L;
L = (List)malloc(sizeof(struct LNode)); //malloc的返回值是一个指针,指向一段可用内存的起始地址
L->Last = -1;
return L;
}
// 插入
void Insert(ElementType X, int i, List L)
{
if (L->Last == MAXSIZE - 1) //位置已满
{
cout << "表满" << endl;
return;
}
if (i < 0 || L->Last + 1 < i) //位置越界,如果将数插入 L->Data[L->Last+1],下面都不用腾位置了
{
cout << "位置不合法" << endl;
return;
}
for (int j = L->Last; j >= i; j--) // 从后往前依次向后挪一个,给 a[i]腾出位置
L->Data[j + 1] = L->Data[j];
L->Data[i] = X; //新元素插入
L->Last++; // Last仍然指向最后元素
return;
}
//删除
void Delete(int i, List L)
{
if (i < 0 || L->Last < i) //位置越界,而删除最多到 L->Data[L->Last]
{
cout << "L->Data[" << i << "]不存在元素" << endl;
return;
}
for (int j = i; j <= L->Last; j++) // 从前往后依次向前挪一个,将 a[i] 覆盖了
L->Data[j] = L->Data[j + 1];
L->Last--; // Last仍然指向最后元素
return;
}
// 按值查找
int Find(ElementType X, List L)
{
int i = 0;
while (i <= L->Last && L->Data[i] != X)
i++;
if (L->Last < i) //如果没找到,返回 -1
return -1;
else // 找到后返回下标
return i;
}
// 按序查找
ElementType FindKth(int K, List L)
{
if (K < 0 || L->Last < K) //位置越界
{
cout << "L->Data[" << K << "]不存在元素" << endl;
return NULL;
}
return L->Data[K];
}
//表长
int Length(List L)
{
return L->Last + 1;
}
int main()
{
L = MakeEmpty(); //MakeEmpty()清空定义的数据结构
//插入
Insert(11, 0, L);
cout << "在线性表L->Data[0]插入11" << endl;
Insert(25, 0, L);
cout << "在线性表L->Data[0]插入25" << endl;
Insert(33, 0, L);
cout << "在线性表L-Data[0]插入33" << endl;
Insert(77, 0, L);
cout << "在线性表L-Data[0]插入77" << endl;
cout << "此时的线性表为:";
for (int i = 0; i < Length(L); i++)
cout << L->Data[i] << " ";
cout << endl;
//查找
cout << "查找值为12的下标是:" << Find(12, L) << endl;
cout << "查找值为33的下标是:" << Find(33, L) << endl;
cout << "查找线性表中下标为3的值是:" << FindKth(3, L) << endl;
cout << "查找线性表中下标为5的值是:" << FindKth(5, L) << endl;
//删除
Delete(2, L);
cout << "删除线性表中下标为2的元素" << endl;
Delete(2, L);
cout << "删除线性表中下标为2的元素" << endl;
cout << "此时的线性表为:";
for (int i = 0; i < Length(L); i++)
cout << L->Data[i] << " ";
cout << endl;
system("pause");
return 0;
}
代码运行结果如下图所示。
1.5 线性表的链式存储实现
不要求逻辑上相邻的两个元素物理上也相邻,通过“链”建立起数据元素之间的逻辑关系。插入、删除不需要移动数据元素,只需要修改“链”。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。
线性表的链式存储数据结构定义如下所示。
typedef struct LNode *List;
struct LNode{
ElementType Data; //节点对应的数据
List Next; //下一个节点的位置
};
struct Lnode L;
List PtrL;
线性表的链式存储中的下标从 1 开始。
主要操作的实现:
1. 求表长
int Length(List PtrL)
{
List p = PtrL; //p指向表的第一个结点
int j = 0;
while (p)
{
p=p->Next;
j++; //当前p指向的是第j个结点
}
return j;
}
时间性能为 O ( n ) O(n) O(n)。
2. 查找
(1)按序号查找:FindKth
List FindKth(int K,List PtrL)
{
List p = PtrL;
int i =1;
while (p !=NULL && i < K )
{
p = p->Next;
i++;
}
if ( i == K )
return p; //产找到第K个,返回指针
else
return NULL; //否则返回空
}
(2)按值查找:Find
List Find(ElementType X, List PtrL)
{
List p = PtrL;
while ( p != NULL && p->Data != X )
p = p->Next;
return p;
}
平均时间性能为 O ( n ) O(n) O(n)。
3. 插入 (在第
i
−
1
(
1
⩽
i
⩽
n
+
1
)
i-1(1\leqslant i \leqslant n+1)
i−1(1⩽i⩽n+1)个结点后插入一个值为X的新结点)
(1) 先构造一个新结点,用
s
s
s指向;
(2) 再找到链表的第
i
−
1
i-1
i−1个结点,用
p
p
p指向;
(3) 然后修改指针,插入结点(
p
p
p之后插入新结点
s
s
s)。
思考: 修改指针的两个步骤如果交换一下,将会发生什么?
List Insert(ElementType X, int i,List PtrL)
{
List p, s;
if ( i == 1 ) //新结点插入在表头
{
s = (List)malloc(sizeof(struct LNode)); //申请、填装结点
s->Data = X;
s->Next = PtrL;
return s;
}
p = FindKth( i-1, PtrL ); //查找第i-1个结点
if ( p == NULL ) //第i-1个不存在,不能插入
{
cout << "结点错误" << endl;
return NULL;
}
else
{
s= (List)malloc(sizeof(struct LNode)); /申请、填装结点
s->Data = X;
s->Next = p->Next; //新结点插入在第i-1个结点的后面
p->Next = s;
return PtrL;
}
平均查找次数为 n / 2 n/2 n/2,平均时间性能为 O ( n ) O(n) O(n)。
4. 删除 (删除链表的第
i
(
1
⩽
i
⩽
n
)
i (1\leqslant i\leqslant n)
i(1⩽i⩽n)个位置上的结点)
(1) 先找到链表的第
i
−
1
i-1
i−1个结点,用
p
p
p指向;
(2) 再用指针
s
s
s指向要被删除的结点(
p
p
p的下一个结点);
(3) 然后修改指针,删除
s
s
s所指结点;
(4) 最后释放
s
s
s所指结点的空间。
思考: 操作指针的几个步骤如果随意改变,将会发生什么?
List Delete(int i, List PtrL)
{
List p, s;
if ( i == 1 ) //若要删除的是表的第一个结点
{
s = PtrL; //s指向第1个结点
if (PtrL != NULL)
PtrL = PtrL->Next; //从链表中删除
else
return NULL;
free(s);
return PtrL;
}
p = FindKth( i-1, PtrL ); //查找第i-1个结点
if ( p == NULL)
{
cout<< "第" << i-1 << "个结点不存在" << endl;
return NULL;
}
else if ( p->Next == NULL )
{
cout<< "第" << i << "个结点不存在" << endl;
return NULL;
}
else
{
s= p->Next; //s指向第i个结点*
p->Next = s->Next; //从链表中删除
free(s); //释放被删除结点
return PtrL;
}
}
平均查找次数为 n / 2 n /2 n/2,平均时间性能为 O ( n ) O(n) O(n)。
线性表的链式存储实现实例的代码如下所示。
#include<iostream>
using namespace std;
typedef int ElementType; // ElementType 可定义为任意类型
typedef struct LNode *List;
struct LNode
{
ElementType Data; //数据域
List Next; // 下一个链表的地址
};
List L;
List MakeEmpty(); //初始化链表
int Length(List L); // 以遍历链表的方法求链表长度
List FindKth(int K, List L); // 按序号查找
List Find(ElementType X, List L); // 按值查找
List Insert(ElementType X, int i, List L); //将 X 插入到第 i-1(i>0) 个结点之后
List Delete(int i, List L); // 删除第 i(i>0) 个结点
void Print(List L); // 输出链表元素
// 初始化链表
List MakeEmpty()
{
List L = (List)malloc(sizeof(struct LNode));
L = NULL;
return L;
}
//求表长
int Length(List L)
{
List p = L;
int len = 0;
while (p) // 当 p 不为空
{
p = p->Next;
len++;
}
return len;
}
// 按序查找
List FindKth(int K, List L)
{
List p = L;
int i = 1; //从 1 开始
while (p && i < K) {
p = p->Next;
i++;
}
if (i == K) // 找到了
return p;
else // 未找到
return NULL;
}
// 按值查找
List Find(ElementType X, List L) {
List p = L;
while (p && p->Data != X)
p = p->Next;
// 找到了,返回 p
// 未找到,返回 NULL,此时 p 等于 NULL
return p;
}
/* 插入
1. 用 s 指向一个新的结点
2. 用 p 指向链表的第 i-1 个结点
3. s->Next = p->Next,将 s 的下一个结点指向 p 的下一个结点
4. p->Next = s,将 p 的下一结点改为 s */
List Insert(ElementType X, int i, List L) {
List p, s;
if (i == 1) { // 新结点插入在表头
s = (List)malloc(sizeof(struct LNode));
s->Data = X;
s->Next = L;
return s; //插入的结点为头结点
}
p = FindKth(i - 1, L); // 找到第 i-1 个结点
if (!p) { // 第 i-1 个结点不存在
cout << "结点错误" << endl;
return NULL;
}
else {
s = (List)malloc(sizeof(struct LNode));
s->Data = X;
s->Next = p->Next; //将 s 的下一个结点指向 p 的下一个结点
p->Next = s; // 将 p 的下一结点改为 s
return L;
}
}
/* 删除
1. 用 p 指向链表的第 i-1 个结点
2. 用 s 指向要被删除的的第 i 个结点
3. p->Next = s->Next,p 指针指向 s 后面
4. free(s),释放空间
*/
List Delete(int i, List L) {
List p, s;
if (i == 1) { //如果要删除头结点
s = L;
if (L) // 如果不为空
L = L->Next;
else
return NULL;
free(s); // 释放被删除结点
return L;
}
p = FindKth(i - 1, L); // 查找第 i-1 个结点
if (!p || !(p->Next)) { // 第 i-1 个或第 i 个结点不存在
cout << "结点错误" << endl;
return NULL;
}
else {
s = p->Next; // s 指向第 i 个结点
p->Next = s->Next; //从链表删除
free(s); // 释放被删除结点
return L;
}
}
// 输出链表元素
void Print(List L)
{
List t;
int flag = 1;
cout << "当前链表为:";
for (t = L; t; t = t->Next)
{
cout << t->Data << " ";
flag = 0;
}
if (flag)
cout << "NULL";
cout << endl;
}
int main()
{
L = MakeEmpty();
Print(L);
L = Insert(11, 1, L);
L = Insert(25, 1, L);
L = Insert(33, 2, L);
L = Insert(77, 3, L);
Print(L);
cout << "当前链表长度为:" << Length(L) << endl;
cout << "此时链表中第二个结点的值是:" << FindKth(2, L)->Data << endl;
cout << "查找22是否在该链表中:";
if (Find(22, L))
cout << "是!" << endl;
else
cout << "否!" << endl;
cout << "查找33是否在该链表中:";
if (Find(33, L))
cout << "是!" << endl;
else
cout << "否!" << endl;
L = Delete(1, L);
cout << "删除链表中下标为1的元素" << endl;
L = Delete(3, L);
cout << "删除链表中下标为3的元素" << endl;
Print(L);
system("pause");
return 0;
}
代码运行结果如下图所示。
1.6 广义表
例: 之前已经表示了一元多项式,那么二元多项式又该如何表示?比如,给定二元多项式
P
(
x
,
y
)
=
9
x
12
y
2
+
4
x
12
+
15
x
8
y
3
−
x
8
y
+
3
x
2
P(x,y)= 9x^{12}y^{2}+4x^{12}+15x^{8}y^{3}-x^{8} y+3x^{2}
P(x,y)=9x12y2+4x12+15x8y3−x8y+3x2,进行表示。
分析: 可以将上述二元多项式看成关于
x
x
x的一元多项式。
比如将
P
(
x
,
y
)
=
(
9
y
2
+
4
)
x
12
+
(
15
y
3
−
y
)
x
8
+
3
x
2
P(x,y)=(9y^{2}+4)x^{12}+(15y^{3}-y)x^{8}+3x^{2}
P(x,y)=(9y2+4)x12+(15y3−y)x8+3x2看成
a
x
12
+
b
x
8
+
c
x
2
ax^{12} + bx^{8} + cx^{2}
ax12+bx8+cx2,所以上述二元多项式可以用 “复杂”链表表示为:
广义表(Generalized List)是线性表的推广。对于线性表而言,n个元素都是基本的单元素;广义表中,这些元素不仅可以是单元素也可以是另一个广义表。
广义表数据结构定义如下所示。
typedef struct GNode *GList;
struct GNode{
int Tag; //标志域:0表示结点是单元素,1表示结点是广义表
union //子表指针域Sublist与单元素数据域Data复用,即共用存储空间
{
ElementType Data;
GList SubList;
}
URegion;
GList Next; //指向后继结点
};
1.7 多重链表
多重链表: 链表中的节点可能同时隶属于多个链。
⋄
\diamond
⋄ 多重链表中结点的指针域会有多个,如前面例子包含了Next和SubList两个指针域;
⋄
\diamond
⋄ 但包含两个指针域的链表并不一定是多重链表,比如双向链表不是多重链表。
多重链表有广泛的用途:基本上如树、图这样相对复杂的数据结构都可以采用多重链表方式实现存储。
例: 矩阵可以用二维数组表示,但二维数组表示有两个缺陷:
⋄
\diamond
⋄ 一是数组的大小需要事先确定;
⋄
\diamond
⋄ 二是对于“稀疏矩阵”,将造成大量的存储空间浪费。
A = [ 18 0 0 2 0 0 27 0 0 0 0 0 0 − 4 0 23 − 1 0 0 12 ] , B = [ 0 2 11 0 0 0 9 − 4 − 1 0 0 0 0 0 0 9 13 0 0 − 2 0 0 10 7 6 0 0 5 0 0 ] A=\begin{bmatrix} 18 & 0&0 &2 & 0\\ 0& 27& 0& 0& 0\\ 0& 0& 0& -4& 0\\ 23&-1 & 0 & 0&12 \end{bmatrix}, B=\begin{bmatrix} 0 & 2&11 &0 & 0& 0\\ 9& -4& -1& 0& 0& 0\\ 0& 0& 0& 9& 13& 0\\ 0& -2& 0& 0&10& 7\\ 6&0 & 0 & 5&0 & 0 \end{bmatrix} A=⎣⎢⎢⎡1800230270−1000020−4000012⎦⎥⎥⎤,B=⎣⎢⎢⎢⎢⎡090062−40−2011−100000905001310000070⎦⎥⎥⎥⎥⎤
分析: 采用一种典型的多重链表——十字链表来存储稀疏矩阵。
⋄
\diamond
⋄ 只存储矩阵非0元素项
结点的数据域:行坐标Row、列坐标Col、数值value
⋄
\diamond
⋄ 每个结点通过两个指针域,把同行、同列串起来
⋆
\star
⋆ 行指针(或称为向右指针)Right
⋆
\star
⋆ 列指针(或称为向下指针)Down
用一个标识域Tag来区分头结点和非0元素结点:头节点的标识值为“Head”,矩阵非0元素结点的标识值为“Term”,如下图所示。