第二讲 线性结构
2.1 线性结构及其实现
线性结构是数据结构里面最基础,最简单的一种数据结构类型,其中最典型的是线性表。
引子:一元多项式表示
在程序设计语言里,要表示一个问题。首先要分析一下,这个问题的关键数据,关键信息在哪里。
对多项式来讲:
关键数据:
多项式项数n;各项的洗漱ai及指数i。
最简单的一种表示方法是顺序存储的一种直接表示。
方法一:顺序存储结构直接表示:数组
数组各个分量对应多项式各项。
a [ i ] : a[i]: a[i]:项 x i x^i xi的系数 a i a_i ai。
例如:
很容易表示多项式相加:对应分量相加。
但是存在问题:用顺序存储结构直接表示多项式,数组大小需要多少?2001个。而且浪费了1999个空间。
方法二:顺序结构表示非零项
结构数组表示:数组分量是由系数
a
i
a_i
ai,指数
i
i
i组成的结构,对应一个非零项。
要实现运算方便:每一项按照指数大小有序存储。
相加过程:**二路归并。**从头开始比较,看谁指数高,指数高的先出来。指数相等的对应系数相加。
方法三:链表结构存储非零项
每个结点存储多项式中的一个非零项,包括系数和指数两个数据域以及一个指针域。
coef | expon | link |
---|
typedef struct PolyNode *Polynomial;
struct PolyNode
{
int coef;
int expon;
Polynomial link;
};
加法运算同于数组:指向头,判断大小。
线性表及顺序存储
启示:同一个问题可以有不同的表示方法,不同的存储方法。
线性表是n 个类型相同数据元素的有限序列,通常记作(a1, a2, a3, …, an )。
操作集合:
List InitList() //构造一个空的线性表L
void DestroyList(&L) //销毁线性表 L
bool ListEmpty(L) //判断栈L是否空
int ListLength(L)//求L的长度
int PriorElem(L,cur_e,&pre_e)//求前驱的值
int NextElem(L,cur_e,&next_e)//求后继的值
int GetElem(L,i,&e)//取i位置的值
int Find(e,L)//在线性表中查找e,并返回其第一次出现的位置
List ListTraverse(List L,visit())//遍历线性表
void ClearList(&L)//清空线性表
void ListInsert(List L,int i,e)//在i位置插入e
void Delete(List L,int i)//删除i位置的元素
线性表的顺序存储实现
typedef struct LNode *List;
struct LNode
{
int Data[MAXSIZE];//本应该使用伪代码ElementType,但这里使用int便于表示
int Last;
};
struct LNode L;//一个L就可以实现线性表,Last存储最后一个元素
List Ptrl;//指针式定义
访问下标为i的元素:L.Data[i]
或Ptrl->Data[i]
线性表的长度;L.Last+1
或Ptrl->Last+1
主要操作实现
一:初始化
List InitList() //构造一个空的线性表L
List MakeEmpty()//构造一个空的线性表L
{
List Ptrl;
Ptrl=(List)malloc(sizeof(struct LNode));//申请一个结构
Ptrl->Last=-1;//Last为0代表表里有一个元素放在第一个位置。没有元素设为-1
return Ptrl;
}
二:查找
int Find(int x,List Ptrl)
{
int i = 0;
while(i<=Ptrl->Last&&Ptrl->Data[i]!=x)
{
++i;
}
if(i>Ptrl->Last)
return -1;//没有找到
else
return i;//找到返回其位置
}
三:插入 第i个位置插入一个值为X的新元素
插入到 i-1的位置,把i-1位置及其之后的全部往后挪动一位。
先移动,再插入
void Insert(int x,int i,List Ptrl)
{
int j;
if(Ptrl->Last==MAXSIZE-1)//表空间元素已满,不能插入
{
cout << "表满";
return;
}
if(i<1||i>Ptrl->Last+2)
{
cout << "位置不合法";
return;
}
for (j = Ptrl->Last; j >= i - 1;j--)
{
Ptrl->Data[j + 1] = Ptrl->Data[j];//a[i]~a[n]后移动
}
Ptrl->Data[i-1]=x;//新元素插入
Ptrl->Last++;//Last仍指向最后元素
return;
}
平均移动次数是
O
(
n
/
2
)
O(n/2)
O(n/2)
四:删除 删除表的第i个位置(也即下标为i-1的元素)上的元素
void Delete(int i, List Ptrl)
{
int j;
if (i < 1 || i > Ptrl->Last+1)//检查空表及删除位置的合法性
{
cout << "不存在第" << i << "个元素";
return;
}
for (j = i; j <= Ptrl->Last;++j)
{
Ptrl->Data[j - 1] = Ptrl->Data[j];
}
Ptrl->Last--;
return;
}
线性表的链式存储实现
不要求逻辑上相邻的两个元素物理上也相邻。
插入删除不需要移动元素,只需要修改链。
不方便的两个操作:找第i个元素,获得长度
typedef struct LNode *List;
struct LNode
{
int Data;
List Next;
};
struct LNode L;
List Ptrl;
主要操作实现
一:求表长
int Length(List Ptrl)
{
List p=Ptrl;//p指向表的第一个结点
int j = 0;
while(p)
{
p = p->Next;
++j;//当前p指向的是第j个结点
}
return j;
}
二:查找
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;
else
return NULL;
}
List Find(int x,List Ptrl)//按值查找
{
List p=Ptrl;
while(p!=NULL&&p->Data!=x)
{
p = p->Next;
}
return p;//
}
三:插入(插在第i-1个结点的后面)
(1)先构造一个新结点,用s指向
(2)再找到链表的第i-1个结点,用p指向
(3)修改指针,插入结点
List Insert(int x,int i,List Ptrl)
{
List p,s;
if(i==1)//i-1=0,0这个位置在链表中不存在。新结点插在表头
{
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 << "参数错";
return NULL;
}
else
{
s = (List)malloc(sizeof(struct LNode));
s->Data = x;
s->Next = p->Next;//新结点插入
p->Next=s;
return Ptrl;
}
四:删除(删除链表第i个位置上的结点)
(1)先找到链表的第i-1个结点,用p指向
(2)再用s指向要删除的结点(p的下一个结点)
(3)然后修改指针,删除s所指的结点
(4)最后释放s所指结点的空间
List Delete(List Ptrl, int i) //注意删除操作返回的是删除后的链表
{
//特殊处理删除的是头节点
List p,s;
if(i==1)
{
s = Ptrl;
if(Ptrl==NULL)//删除头节点也分情况,表空和表不空
Ptrl = Ptrl->Next;
else
return NULL;
free(s);
return Ptrl;
}
p = FindKth(i - 1, Ptrl);
if(p==NULL)
{
cout << "删除位置不存在";
return NULL;
}
else if(p->Next==NULL)
{
cout<<"删除位置不存在";
return NULL;
}
else
{
p->Next = s->Next;
free(s);
}
}
广义表
知道了一元多项式如何表示,那么二元多项式表示方法?
- 看作一元多项式,y就是x的系数的一部分。
广义表是线性表的推广,对于线性表而言,n个元素都是基本的元素,广义表中这些元素不仅可以是单元素,也可以是另一个广义表。
typedef struct GNode *GList;
struct GNode
{
int Tag;//标志域,0表示节点是氮元素,1表示结点时广义表
union//子表指针域sublist与单元素域data复用,即共用存储空间
{
int Data;
GList SubList;
} URegion;
GList Next;//指向后继结点
};
多重链表:
多重链表:链表中的结点可能同时隶属于多个链。
多重链表的结点的指针域会有多个,如前面例子包含了Next和SubList两个指针域。
但包含了两个指针域的链表不一定是多重链表,比如双向链表不是多重链表。
多重链表有广泛的用途,树,图。
多重链表之矩阵存储
矩阵可以用二维数组表示,但二维数组有两个缺陷:
- 一是数组大小需要事先确定,
- 对于"稀疏矩阵",将造成大量空间浪费。
采用一种典型的多重链表—十字链表来存储稀疏矩阵 - 只存储非0元素项
-
- 结点的数据域:行坐标Row,列坐标Col,数值Value
- 每个结点通过两个指针域,把行,同列串起来
-
- 行指针(或称为向右指针)Right
-
- 列指针(或称为向下指针) Down
Term(代表稀疏矩阵的非0项)有两个指针,一个指向同一行的,一个指向同一列的。
Head既作为行链表的头节点,也作为列链表的头节点。
最左上角的Term是整个程序的入口,4行,5列,7项。
在矩阵的多重链表表示中,第i行的head和第i列的head实际上是同一个结点。
- 列指针(或称为向下指针) Down
- 用一个标识域Tag来区分头节点和非0元素结点。
- 头结点的标识为"Head",矩阵非0元素结点的标识值为"Term"
- 通过UNION将head和term联系在一起。
-
2.2堆栈
什么是堆栈?
计算机如何进行表达式求值?
例如:
算数表达式5+6/2-3*4
。
正确理解:
5+6/2-3*4=5+3-3*4=8-3*4=8-12=-4
由两类对象构成:
- 运算数,如
2,3,4
- 运算符号,如
+,-,*,/
不同运算符号优先级不一样。
后缀表达式
前缀表达式,即运算符号位于运算数之前,比如a+b*c
的前缀表达式是+a*bc
。
你能写出a+b*c-d/e
的前缀表达式吗?
-+a*bc/de
**中缀表达式:**运算符号位于两个运算数之间。如,a+b*c-d/e
。
后缀表达式:运算符号位于两个运算数之后。如,abc*+de/-
。
求值策略:
从左向右扫描,逐个处理运算数和运算符号,
1.遇到运算数怎么办?如何记住目前还未参加运算的数,
2.遇到运算符号怎么办?对应的运算数是什么?
启示:存储运算数,并在需要时倒序输出。堆栈!
线性的。
堆栈:具有一定操作约束的线性表。
只在一端(栈顶,TOP)做插入,删除。
插入数据:入栈(Push)
删除数据:出栈(POP)
后入先出:Last In First Out(LIFO)
基本操作集:
Stack Create(int MaxSize)//生成空堆栈,其最大长度为MaxSize
int IsFull(Stack S,int MaxSize)//判断堆栈是否已满
void Push(Stack S,int item)//将元素item压入堆栈
int IsEmpty(Stack S)//判断堆栈S是否为空
int Pop(Stack S)//删除并返回栈 顶元素
栈的顺序存储实现
栈的顺序存储结构通常由一个一维数组和一个记录栈顶元素位置的变量组成。
#define MaxSize 100
typedef struct SNode *Stack;
struct SNode
{
int Data[MaxSize];
int Top;//栈顶位置的数组下标
};
一:入栈
void Push(Stack PtrS,int item)
{
if(PtrS->Top==MaxSize-1)
{
cout << "堆栈满";
return;
}
else
{
PtrS->Data[++(PtrS->Top)] = item;
return;
}
}
二:出栈
int Pop(Stack PtrS)
{
if(PtrS->Top==-1)
{
cout << "堆栈空";
return NULL;
}
else
return (PtrS->Data[(PtrS->Top)--]);
}
例子:用一个数组实现两个堆栈
对半分,但简单对分如果一个没有利用满,会浪费空间。
只要有空余空间,就允许有入栈操作。
则让堆栈都往中间增长。
分析:
一种比较聪明的方法是让这两个栈分别从两头开始向中间生长,当两个栈的栈顶指针相遇时,标识两个栈都满了。
#define MaxSize 1000
struct DStack
{
int Data[MaxSize];
int Top1;//堆栈1的栈顶指针
int Top2;//堆栈2的栈顶指针
}S;
S.Top1 = -1;//左栈空位置
S.Top2 = MaxSize;//右栈空位置
一:入栈
void Push(struct DStack *PtrS,int item,int Tag)//Tag指示对第一个堆栈操作还是对第二个堆栈操作
{
if(PtrS->Top2-PtrS->Top1==1)
{
cout << "堆栈满";
return;
}
if(Tag==1)//对第一个堆栈操作
PtrS->Data[++(PtrS->Top1)] = item;
else//对第二个堆栈操作
PtrS->Data[--(PtrS->Top2)]=item;
}
讨论2.2 堆栈顺序存储的另一种实现?
有人给出了堆栈用数组实现的另一种方式,即直接在函数参数中传递数组和top变量(而不是两者组成的结构指针),其中Push操作函数设计如下。这个Push函数正确吗?为什么?
答:不正确,因为top是形参,即便函数内部通过
++top
修改了,实际没有改变。
#define MaxSize 100
ElementType S[MaxSize];
int top;
void Push(ElementType *S, int top, ElementType item)
{ if (top==MaxSize-1) {
printf(“堆栈满”); return;
}else {
S[++top] = item;
return;
}
}
堆栈的链式存储实现
栈的链式存储结构实际上就是一个单链表,叫做链栈。插入和删除操作只能在链栈的栈顶执行。栈顶指针Top应该在链表的哪一头?
只能在表头,在表尾删除操作无法实现。
typedef struct SNode *Stack;
struct SNode
{
int Data;
struct SNode *Next;
};
一:堆栈初始化
Stack CreateStack()
{//构建一个堆栈的头节点,返回指针
Stack S;
S = (Stack)malloc(sizeof(struct SNode));
S->Next = NULL;
return S;
}
二:判断堆栈S是否为空
int IsEmpty(Stack S)
{//判断堆栈S是否为空,若为空函数返回整数1,否则返回0
return (S->Next==NULL);
}
三:入栈操作
int IsEmpty(Stack S)
{//判断堆栈S是否为空,若为空函数返回整数1,否则返回0
return (S->Next==NULL);
}
//1.堆栈初始化
//2.判断堆栈是否为空
void Push(int item,Stack S)
{//将元素item压入堆栈
struct SNode *TmpCell;
TmpCell = (struct SNode *)malloc(sizeof(struct SNode));
TmpCell->int = item;
TmpCell->Next = S->Next;
S->Next = TmpCell;
}
四:出栈操作
int Pop(Stack S)
{//删除并返回堆栈S的栈顶元素
struct SNode *FirstCell;
int TopElem;
if(IsEmpty(S))
{
cout << "堆栈空";
return NULL;
}
else
{
FirstCell = S->Next;//先初始化一个结点指向要删除的结点
S->Next = FirstCell->Next;
TopElem = FirstCell->Element;
free(FirstCell);
return TopElem;
}
}
堆栈应用:表达式求值
回忆:应用堆栈实现后缀表达式求值的基本过程。
从左到右读入后缀表达式的各项(运算符或运算数)
1.运算数:入栈
2.运算符:从堆栈中弹出适当数量的运算数,计算并结果入栈
3.最后栈顶上的元素就是表达式的结果值
中缀表达式求值
基本策略:中缀表达式转为后缀表达式,然后求值
观察一个简单的例子:2+9/3-5→293/+5-
1.运算数相对顺序不变
2.运算符号顺序发生改变
- 需要存储"等待中"的运算符号
- 要将当前运算符号中与等待中的最后一个运算符号比较
在这里插入代码片
中缀表达式转为后缀表达式
从头到尾读取中缀表达式中的每个对象,对不同对象按不同的情况处理。
(1)运算数:直接输出
(2)左括号:压入堆栈
(3)右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出)
(4)运算符:
- 若优先级大于栈顶运算符时,则把它压入栈
- 若优先级小于栈顶运算符时,将栈顶元素运算符弹出并输出;
堆栈的其他应用
- 函数调用及递归实现
- 深度优先算法
- 回溯算法