数据结构——线性结构
引子
线性结构,即数据元素之间构成一个有序的序列。
数据结构的操作与数据结构的存储方式是密切相关的。
不同的数据存储方式,相应的操作实现方法是不一样的。
线性表的定义与实现
线性表的定义
线性表(Linear List)是由同一类型的数据元素构成的有序序列的线性结构。
线性表中元素的个数称为线性表的长度;当一个线性表中没有元素(长度为0)时,称为空表;表的起始位置称表头,表的结束位置称表尾。
线性表的抽象数据类型描述为:
类型名称: 线性表(List)
数据对象集:
线性表是 n(n>=0)个元素构成的有序序列(
a
1
,
a
2
,
…
,
a
n
a_1,a_2,…,a_n
a1,a2,…,an),其中
a
1
a_1
a1是表的第一个元素(表头),
a
n
a_n
an是表的最后一个元素(表尾);
a
i
+
1
a_{i+1}
ai+1称为
a
i
a_i
ai的之间后继,
a
i
−
1
a_{i-1}
ai−1为
a
i
a_i
ai的之间前驱;直接前驱和直接后继反映了元素之间一对一的邻接逻辑关系。
操作集:
对于一个具体的线性表L
∈
\in
∈List,一个表示位序的整数i,一个元素 X
∈
\in
∈ElementType,线性表的基本操作主要有:
(1)List MakeEmpty():初始化一个新的空线性表;
(2)ElementType FindKth(List L, int i):根据指定的位序i,返回L中相应的元素 a i a_i ai;
(3)Position Find(List L, ElementType X):已知X,返回线性表L中与X相同的第一个元素的位置;若不存在则返回错误信息;
(4)bool Insert(List L, ElementType X, int i):在L的指定位序i前插入一个新元素X;成功则返回true,否则返回false;
(5)bool Delete(List L, int i):从L中删除指定位序i的元素;成功则返回true,否则返回false;
(6)int Length(List L):返回线性表L的长度。
线性表的顺序存储实现
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素。
操作实现:
1.初始化
顺序表的初始化即构造一个空表。首先动态分配表结构所需要的存储空间,然后将表中Last指针置为-1,表示表中没有数据元素。
2.查找
顺序存储的线性表中,查找主要是指在线性表中查找与给定值X相等的数据元素。
查找过程实际上就是在数组里顺序查找:从第一个元素
a
1
a_1
a1起依次和X比较,直到找到一个与X相等的数据元素,返回它在顺序中的存储下标;或者查遍整个表都没有找到与X相等的元素,则返回错误信息ERROR。
3.插入
顺序表的插入是指在表的第 i(1
≤
\leq
≤i
≤
\leq
≤n+1)个位序上插入一个值为X的新元素(也可以理解为在第i个元素之前插入新元素),插入后使原长度为n的数字元素系列
(
a
1
,
a
2
…
,
a
i
−
1
,
a
i
,
a
i
+
1
,
…
,
a
n
)
(a_1,a_2…,a_{i-1},a_i,a_{i+1},…,a_n)
(a1,a2…,ai−1,ai,ai+1,…,an)成为长度为n+1的序列
(
a
1
,
a
2
…
,
a
i
−
1
,
X
,
a
i
,
a
i
+
1
,
…
,
a
n
)
(a_1,a_2…,a_{i-1},X,a_i,a_{i+1},…,a_n)
(a1,a2…,ai−1,X,ai,ai+1,…,an)。
当插入位序i为1时,代表插入到序列最前端;为n+1时,代表插入到序列最后。
4.删除
顺序表的删除运算是指将表中位序为 i(1
≤
\leq
≤i
≤
\leq
≤n+1)的元素从线性表中去掉,删除后使原长度为n的数组元素序列
(
a
1
,
a
2
…
,
a
i
−
1
,
a
i
,
a
i
+
1
,
…
,
a
n
)
(a_1,a_2…,a_{i-1},a_i,a_{i+1},…,a_n)
(a1,a2…,ai−1,ai,ai+1,…,an)成为长度为n-1的序列
(
a
1
,
a
2
…
,
a
i
−
1
,
a
i
+
1
,
…
,
a
n
)
(a_1,a_2…,a_{i-1},a_{i+1},…,a_n)
(a1,a2…,ai−1,ai+1,…,an)。
线性表的链式存储实现
线性表链式存储结构:
通过“链”建立起数据元素之间的逻辑关系,因此对线性表的插入、删除不需要移动数据元素,只需要修改“链”。
不需要用地址连续的存储单元来实现,因为它不要求逻辑上相邻的两个数据元素物理上也相邻。
用链表结构可以克服数组表示线性表的缺陷。
单向链表的表示形式,它有n个数据单元,每个数据单元由数据域和链接域两部分。
数据域用来存放数值。
链接域是线性表数据单元的结构指针,用一带箭头的线段表示,线性表的顺序是用个结点上指针构成的指针链实现的。
为了访问链表,必须先找到链表的第一个数据单元,因此实际应用中常用一个称为“表头(Header)”的指针指向链表的第一个单元,并用它表示一个具体的链表。
1. 求表长
在顺序存储表示的线性表中求表长是容易的事,直接返回Last+1值就可以了。
在链式存储表示中,需要将链表从头到尾遍历一遍:设一个移动指针p和计数器cnt,初始化后,p从表的第一个结点开始逐步往后移,同时计数器 cnt 加 1。当后面不再有结点时,cnt 的值就是结点个数,即表长。
代码实现:
int length(list L)
{
Position p;
int cnt = 0; /* 初始化计数器 */
P = L; /* p指向表的第一个结点 */
while(p){
p = p -> Next;
cnt++; /* 当前p指向的是第cnt个结点 */
}
return cnt;
}
2. 查找
在线性表抽象类型说明中,线性表的查找有两种,即按序号查找(FindKth)和按值查找(Find)两种。
(1)按序号查找(FindKth)
对于顺序存储,按序号查找是很直接的事情,要得到第K个元素的值,直接取L->Data[K-1]就可以了。
函数实现如:
#define ERROR -1 /* 一般定义为表中元素不可能取到的值 */
ElementType FindKth(List L, int K)
{ /* 根据指定的位序K,返回L中相应元素 */
Position p;
int cnt - 1; /* 位序从1开始 */
p = L; /* p指向L的第1个结点 */
while(p && cnt < K)
{
p = p->Next;
cnt++;
}
if((cnt == K) && p)
return p->Data; /* 找到第K个 */
else
return ERROR; /* 否则返回错误信息 */
}
(2)按值查找,即定位Find
按值查找的基本方法也是从头到尾遍历,直到找到为止:从链表的第一个元素结点起,判断当前结点其值是否等于X;若是,返回该结点的位置,否则继续后一个,直到表结束为止。找不到时返回错误信息。
函数实现代码:
#define ERROR NULL /* 用空地址表示错误 */
ElementType Find(List L, ElementType X)
{
Position p = L; /* p指向L的第1个结点 */
while(p && p->Data! = X)
p = p->Next;
/* 下列语句可以用return p; 替换 */
if(p)
return p;
else
return ERROR;
}
(3)插入
线性表的插入是在指定位序i(1
≤
\leq
≤i
≤
\leq
≤n+1)前插入一个新元素X。
当插入位序 i 为1时,代表插入到链表的头; i 为 n+1时,代表插入到链表最后。
基本思路:
如果 i 不为1,则找到位序为 i-1 的结点pre;若存在,则申请一个新结点并在数据域填上相应值X,然后将新结点插入到结点 pre 之后,返回结果链表; 如果不存在则返回错误信息。
(4)删除
在单向链表中删除指定位序 i 的元素,首先需要找到被删除结点的前一个元素,然后再删除结点并释放空间。
从链式线性表的插入、删除的程序实现中可以看出:
1)在单链表上插入、删除一个结点,必须知道前驱结点。
2)单链表不具有按序号随机访问的特点,只能从头指针开始一个个顺序进行。
广义表与多重链表
- 广义表
广义表是线性表的推广。
广义表与线性表一样,也是n个元素组成的有序序列。
不同点在于,对于线性表而言,n个元素都是基本的单元素;而在广义表中,这些元素不仅可以是单元素也可以是另一个广义表。
广义表不仅跟线性表一样可以表达简单的线性顺序关系,而且可以表达更复杂的非线性多元关系。
广义表中的结点可能有两种情况:
(1)单元素,需要有一个域来存储该单元素的值;
(2)广义表,需要有一个域来指向另一个链表。
-
多重链表
广义表采用链表存储的方式实现,其中代表子表的元素结点,不仅是这个广义链表中的一个结点,而且还是它所代表的子表的起点。
像这种存在结点属于多个链的链表叫“多重链表”。多重链表在数据结构实现中有广泛的用途,基本上如树、图这样相对复杂的数据结构都可以采用多重链表的方式实现存储。
堆栈
堆栈的定义
表达式求值是程序设计语言编译中的一个基本问题,即编译程序要将源程序中描述的表达式转换为正确的机器指令序列或直接求出常量表达式的值。
要实现表达式求值,首先需要正确理解一个表达式,主要是元素的先后顺序。
堆栈(Stack)可以认为是具有一定约束的线性表,插入和删除操作都作用在一个称为栈顶(Top)的端点位置。
通常把数据插入称为压入栈(Push),而数据删除可看作从堆栈中取出数据,叫做弹出栈(Pop)。也正是由于这一特性,最后入栈的数据将被最先弹出,所以堆栈也被称为后入先出(Last In First Out, LIFO)表。
堆栈的抽象数据类型定义为:
类型名称:堆栈(Stack)
数据对象集:对于一个有0个或对各元素的有穷线性表。
操作集:对于一个具体的长度为正整数MaxSize的堆栈 S
∈
\in
∈Stack,记堆栈中的任一元素X
∈
\in
∈ ElementType。
堆栈的基本操作主要有:
(1)Stack CreateStack ( int MaxSize ) :生成空堆栈,其最大长度为 MaxSize;
(2)bool IsFull ( Stack S ):判断堆栈S是否已满。若S中元素个数等于 MaxSize时返回true;否则返回false;
(3)bool Push ( Stack S, ElementType X ):将元素X压入堆栈。若堆栈已满,返回false;否则将数据元素X插入到堆栈S栈顶处并返回true;
(4)bool IsEmpty ( Stack S ): 判断堆栈S是否为空,若是返回true;否则返回false;
(5)ElementType Pop ( Stack S):删除并返回栈顶元素。若堆栈为空,返回错误信息;否则将栈顶数据元素从堆栈中删除并返回。
堆栈的实现
由于栈是线性表,因而栈的存储结构可采用顺序和链式两种形式。
顺序存储的栈称为顺序栈,链式存储的栈称为链栈。
栈的顺序存储实现
栈的顺序存储结构通常由一个一维数组个一个记录栈顶元素位置的变量组成,另外我们还可以用一个变量来存储堆栈的最大容量MaxSize,这样方便判断什么时候堆栈是满的。
用一维数组Data[MaxSize](下标0~MaxSize-1)存储一个栈的元素。
堆栈的两个主要操作:入栈 和 出栈。
1)入栈操作 Push
在执行堆栈Push操作是,首先判别栈是否满;若不满,Top加1,并将新元素放入Data数组中的Top位置。
具体实现:
bool IsFull(Stack S)
{
return(S->Top == S->MaxSize-1);
}
bool Push(Stack S, ElementType X )
{
if(IsFull(S)){
printf("堆栈满");
return false;
}
else{
S->Data[++(S->Top)] = X;
return true;
}
}
2)出栈操作Top
执行Pop操作时首先判别栈是否空;若不空,返回Data[Top],同时将Top减-1;否则要返回一个ElementType类型的特殊错误标准,即代码中的 ERROR ——这个值一般根据具体问题做定义,必须是正常的栈元素数据不可能取到的值。
具体实现:
bool IsEmpty(Stack S)
{
return(S->Top == -1);
}
ElementType Pop(Stack S)
{
if(IsEmpty(S)){
printf("堆栈空");
return ERROR; /* ERROR 是 ElementType的特殊值, 标志错误 */
}
else
return(S->Data[(S->Top)--]);
}
堆栈的链式存储实现
栈的链式存储结构(链式)与单链表类似,但其操作受限制,插入和删除操作只能在链栈的栈顶进行。
栈顶指针Top就是链表的头指针。
有时为了简便算法,链栈也可以带一空的表头结点,表头结点后面的第一个结点就是链栈的栈顶结点,栈中的其他结点通过它们的指针Next链接起来,栈底结点的Next为NULL。
用C语言描述链栈如下:
typedef struct SNode * PtrToSNode;
struct SNode{
ElementType Data;
PtrToSNode Next;
};
typedef PtrToSNode Stack;
堆栈应用:表达式求值
应用堆栈实现表达求值的基本过程:
从左到右读入后缀表达式的各项,并根据读入的对象判断执行操作。
操作分下列几种情况:
(1)当读入的是一个运算数时,把它被压入栈中;
(2)当读入的是一个运算符时,就从堆栈中弹出适当数量的运算数,对该运算进行计算,计算结果再压回到栈中;
(3)处理完整个后缀表达式之后,堆栈顶上的元素就是表达式的结果值。
应用堆栈将中缀表达式转换为后缀表达式的基本过程为:从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理。
对象分下列6种情况:
(1)如果遇到空格则认为是分隔符,不需处理;
(2)若遇到运算数,则直接输出;
(3)若是左括号,则将其压入至堆栈中;
(4)若遇到的是右括号,表明括号内的中缀表达式已经扫描完毕,将栈顶的运算符弹出并输出,直到遇到左括号(左括号也出栈,但不输出);
(5)若遇到的是运算符,若该运算符的优先级大于栈顶运算符的优先级时,则把它压栈;
若该运算符的优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出,再比较新的栈顶运算符,
按同样处理方法,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
(6)若中缀表达式中的各对象处理完毕,则把堆栈中存留的运算符一并输出。
队列
队列的定义
排队的基本规则是:新来者排在队伍末尾,排在队伍前面的人先得到服务,期间不允许插队。
多个数据构成一个有序序列,而对这个序列的操作(比如插入、删除)有一定要求:只能在一端插入,而在另一端删除。这样的数据组织方式就是“队列”。
队列(Queue)也是一个有序线性表,但队列的插入和删除操作是分别在线性表的两个不同端点进行的。
队列通常又称为“先进先出”表(Fist In First Out , FIFO)。
队列的抽象数据类型定义为:
类型名称:队列(Queue)
数据对象集:一个有0个或多个元素的有穷线性表。
操作集:对于一个长度为正整数MaxSize的队列 Q
∈
\in
∈ Queue,记队列中的任一元素 X
∈
\in
∈ ElementType。
队列的基本操作主要有:
(1)Queue CreateQueue(int MaxSize): 生成空队列,其最大长度为MaxSize;
(2)bool IsFull(Queue Q):判断队列Q是否已满。若是返回true;否则返回false;
(3)bool AddQ(Queue Q, ElementType X ):将元素X压入队列Q。若队列已满,返回false;否则将数据元素X插入到队列Q并返回true;
(4)bool IsEmpty(Queue Q): 判断队列Q是否为空,若是返回true;否则返回false;
(5)ElementType DeleteQ (Queue Q):删除并返回队列头元素。若队列为空,返回错误信息;否则将队列头数据元素从队列中删除并返回。
队列的实现
队列的顺序存储实现
队列最简单的表示方法是用数组。
用数组存储队列有许多中具体的方法。一般可以选择将队列头放数组下标小的位置,而将队列为放在数组下标大的位置,并用两个变量Front和Rear分别指示队列的头和尾。
队列的链式存储实现
队列与堆栈一样,也可以采用链式存储结构,但队列的头(Front)必须指向链表的头结点,队列的尾(Rear)指向链表的尾结点。
采用链式存储的入队和出队操作实际就是在一个链表的尾部插入结点或者在头部删除结点。
typedef struct Node * PtrToSNode;
struct Node{ /* 队列中的结点 */
ElementType Data;
PtrToNode Next;
};
typedef PtrToNode Position;
typedef struct QNode * PtrToQNode;
struct QNode{
Position Front,Rear; /* 队列的头、尾指针 */
int MaxSize; /* 队列最大容量 */
};
typedef PtrToQNode Queue;