第二讲 线性结构
2.1 线性表及其实现
2.1.1 引例:多项式的表示
- 顺序存储结构直接表示
a[i]
:项 \(x^i\) 的系数 \(a_i\)- \(f(x) = 4x^5-3x^2+1\)
a = [1,0,-3,0,0,4]
- \(x+3x^{2000}\)
太浪费了
- \(f(x) = 4x^5-3x^2+1\)
顺序存储结构表示非零项
每个非零项 \(a_i x^i\) 涉及两个信息:系数 \(a_i\) 和指数 \(i\)
故可将一个多项式看作一个 \((a_i,i)\) 二元组的集合
| \(P_1(x) = 9x^{12} +15x^8+3x^2\) | \([(9,12),(15,8),(3,2)]\) |
| --------------------------------- | ---------------------------------------- |
| \(P_2(x) = 26x^{19}-4x^8-13x^6+82\) | \([(26,19),(-4,8),(-13,6),(82,0)]\) |
| \(P_1+P_2\) | \([(26,19),(9,12),(11,8),(-13,6),(3,2),(82,0)]\) |链表结构存储非零项
链表中每个结点存储多项式中的一个非零项,包含系数和指数两个数据域以及一个指针域c typedef struct PolyNode *Polynomial; struct PolyNode{ int coef; int expon; Polynomial link; }
2.1.2 什么是线性表
- 线性表(Linear List):由同类型数据元素构成的有序序列的线性结构
- 线性表的长度:表中元素的个数
- 线性表中没有元素时,称为空表
- 表头:表的起始位置;表尾:表的结束位置
- 线性表的抽象类型描述
- 类型名称:线性表
- 数据对象集:\(n\,(\ge 0)\) 个元素构成的有序序列(\(a_1,a_2,...,a_n\))
- 操作集:线性表 \(L \in List\),整数 \(i\) 表示位置,元素 \(a_i \in \text{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前插入一个新元素Xvoid Delete(int i,List L)
:删除指定位序i的元素int Length(List L)
:返回线性表L的长度n
2.1.3 线性表的顺序存储实现
利用数组的连续存储空间顺序存放线性表的各个元素
typedef struct LNode L;
List PtrL;
// 访问呢下标为i的元素:L.Data[i]或PtrL->Data[i]
// 线性表的长度:L.Last+1或Ptr;->Last+1
初始化(建立空的顺序表)
c List MakeEmpty(){ List PtrL; PtrL = (List)malloc(sizeof(struct LNode)); PtrL->Last = -1; return PtrL; }
查找
c 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; /* 找到后返回的是存储位置 */ }
插入(在第 i (\(1\le i\le n+1\))个位置上插入一个值为X的新元素)
c void Insert(ElementType X,int i,List PtrL){ int j; if (PtrL->Last == MAXSIZE-1){ /* 表空间已满,不能插入*/ printf("表满"); return; } if (i<1 || i>PtrL->Last+2){ /*检查插入位置的合法性*/ printf("位置不合法"); return; } for (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; }
删除(删除表的第 i (\(1\le i\le n+1\))个位置上的元素)
c void Delete(int i,List PtrL){ int j; if( i<1 || i>PtrL->Last+1){ /*检查空表及删除位置的合法性*/ printf(“不存在第%d个元素”,i); return; } for (j=i;j<=PtrL->Last;j++) PtrL->Data[j-1] = PtrL->Data[j]; /*将 ai+1~ an顺序向前移动*/ PtrL->Last--; /*Last仍指向最后元素*/ return; }
2.1.4 线性表的链式存储实现
通过“链”建立起数据元素之间的逻辑关系,不要求逻辑上相邻的两个元素物理上也相邻
插入、删除不需要移动数据元素,只需要修改“链”
typedef struct LNode *List;
struct LNode{
ElementType Data;
List Next;
};
struct Lnode L;
List PtrL;
求表长
c int Length(List PtrL){ List p = PtrL; /* p指向表的第一个结点*/ int j = 0; while (p){ p = p->Next; j++; /* 当前p指向的是第 j 个结点*/ } return j; } // 时间性能为 O(n)
- 查找
- 按索引查找:
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; /* 否则返回空 */ } // 平均时间性能为 O(n)
- 按值查找:
Find
List Find(ElementType X, List PtrL){ List p = PtrL; while (p!=NULL && p->Data != X) p = p->Next; return p; } // 平均时间性能为 O(n)
- 按索引查找:
插入(在第 i-1 (\(1\le i\le n+1\))个结点后插入一个值为X的新结点)
先构造一个新结点,用s指向;
再找到链表的第 i-1 个结点,用p指向;
然后修改指针,插入结点(p之后插入新结点是s)c 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个不存在,不能插入 */ printf("参数i错"); 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,平均时间性能为 O(n)
删除(删除链表的第 i (\(1\le i\le n+1\))个位置上的结点)
先找到链表的第 i-1 个结点,用p指向;
再用指针s指向要被删除的结点(p的下一个结点);
然后修改指针,删除s所指结点;
最后释放s所指结点的空间。c 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){ printf(“第%d个结点不存在”,i-1); return NULL; } else if (p->Next == NULL){ printf(“第%d个结点不存在”, i); return NULL; } else { s = p->Next; /*s指向第i个结点*/ p->Next = s->Next; /*从链表中删除*/ free(s); /*释放被删除结点 */ return PtrL; } } // 平均查找次数为 n/2,平均时间性能为 O(n)
2.1.5 广义表
广义表是线性表的推广
对于线性表而言, n个元素都是基本的单元素;
广义表中,这些元素不仅可以是单元素也可以是另一个广义表
typedef struct GNode *GList;
struct GNode{
int Tag; /*标志域: 0表示结点是单元素, 1表示结点是广义表 */
union { /*子表指针域Sublist与单元素数据域Data复用,即共用存储空间*/
ElementType Data;
GList SubList;
} URegion;
GList Next; /* 指向后继结点 */
};
2.1.6 多重链表
多重链表: 链表中的节点可能同时隶属于多个链
- 多重链表中结点的指针域会有多个,如前面例子包含了Next和SubList两个指针域;
- 但包含两个指针域的链表并不一定是多重链表,比如双向链表不是多重链表
多重链表有广泛的用途
基本上如树、图这样相对复杂的数据结构都可以采用多重链表方式实现存储典型应用:采用十字链表来存储稀疏矩阵
- 矩阵可以用二维数组表示,但二维数组表示有两个缺陷:
- 数组的大小需要事先确定,
- 对于“稀疏矩阵”,将造成大量的存储空间浪费
- 十字链表是一种典型的多重链表
- 只存储矩阵非0元素项
- 结点的数据域:行坐标Row、列坐标Col、数值Value
- 每个结点通过两个指针域,把同行、同列串起来;
行指针(或称为向右指针)Right
列指针(或称为向下指针)Down - 用一个标识域Tag来区分头结点和非0元素结点:
头节点的标识值为“Head”,矩阵非0元素结点的标识值为“Term”
2.2 堆栈
2.2.1 引例:算术表达式的求值
算术表达式由运算数和运算符两类对象构成,不同运算符优先级不一样
- 中缀表达式:运算符位于两个运算数之间。如 \(a+b*c-d/e\)
- 后缀表达式:运算符位于两个运算数之后。如 \(abc*+de/-\)
2.2.2 什么是堆栈
- 堆栈:具有一定操作约束的线性表
- 约束:只在一端(栈顶,Top)作插入、删除处理
- 操作:
- 入栈(Push):插入数据
- 出栈(Pop):删除数据
- 原则:后入先出(Last In First Out)
- 堆栈的抽象数据类型描述
- 类型名称:堆栈(Stack)
- 数据对象集:一个有0个或多个元素的有穷线性表
- 操作集:长度为
MaxSize
的堆栈 \(S\in\text{Stack}\),堆栈元素 \(\text{itrm}\in\text{ElementType}\)Stack CreateStack(int MaxSize)
:生成空堆栈,其最大长度为MaxSize
int IsFull(Stack S,int MaxSize)
:判断堆栈S是否已满void Push(Stack S,ElementType item)
:将元素item压入堆栈int IsEmpty(Stack S)
:判断堆栈S是否为空ElementType Pop(Stack S)
:删除并返回栈顶元素
2.2.3 栈的顺序存储实现
栈的顺序存储结构通常由一个一维数组和一个记录栈顶元素位置的变量组成
定义
c #define MaxSize <储存数据元素的最大个数> typedef struct SNode *Stack; struct SNode{ ElementType Data[MaxSize]; int Top; };
入栈
c void Push(Stack PtrS,ElementType item){ if (PtrS->Top == MaxSize-1){ printf(“堆栈满”); return; } else { PtrS->Data[++(PtrS->Top)] = item; return; } }
出栈
c ElementType Pop(Stack PtrS){ if (PtrS->Top == -1) { printf(“堆栈空”); return ERROR; /* ERROR是ElementType的特殊值,标志错误*/ } else return (PtrS->Data[(PtrS->Top)--]); }
例:使用一个数组实现两个堆栈
要求:数组空间利用率最大化,使只要数组还有空间入栈操作就可以成功
【方法之一】:使这两个栈分别从数组的两头开始向中间生长。当两个栈的栈顶指针相遇,即表示两个栈都满了
#define MaxSize <存储数据元素的最大个数>
struct DStack{
ElementType Data[MaxSize];
int Top1; /* 堆栈1的栈顶指针 */
int Top2; /* 堆栈2的栈顶指针 */
} S;
S.Top1 = -1;
S.Top2 = MaxSize;
void Push(struct DStack *PtrS,ElementType item,int Tag){
/* Tag作为区分两个堆栈的标志,取值为1和2 */
if (PtrS->Top2 – PtrS->Top1 == 1){/*堆栈满*/
printf(“堆栈满”); return ;
}
if (Tag == 1) /* 对第一个堆栈操作 */
PtrS->Data[++(PtrS->Top1)] = item;
else /* 对第二个堆栈操作 */
PtrS->Data[--(PtrS->Top2)] = item;
}
ElementType Pop(struct DStack *PtrS,int Tag){
/* Tag作为区分两个堆栈的标志,取值为1和2 */
if (Tag == 1){ /* 对第一个堆栈操作 */
if (PtrS->Top1 == -1){ /*堆栈1空 */
printf(“堆栈1空”); return NULL;
}
else return PtrS->Data[(PtrS->Top1)--];
}
else { /* 对第二个堆栈操作 */
if (PtrS->Top2 == MaxSize){ /*堆栈2空 */
printf(“堆栈2空”);
return NULL;
}
else return PtrS->Data[(PtrS->Top2)++];
}
}
2.2.4 栈的链式存储实现
栈的链式存储结构实际上就是一个单链表,叫做链栈。插入和删除操作只能在链栈的栈顶进行
定义
c typedef struct SNode *Stack; struct SNode{ ElementType Data; struct SNode *Next; };
堆栈初始化(建立空栈)
c Stack CreateStack(){ /* 构建一个堆栈的头结点,返回指针 */ Stack S; S =(Stack)malloc(sizeof(struct SNode)); S->Next = NULL; return S; }
判断堆栈S是否为空
c int IsEmpty(Stack S){ /*判断堆栈S是否为空,若为空函数返回整数1,否则返回0 */ return ( S->Next == NULL ); }
入栈
c void Push(ElementType item,Stack S){ /* 将元素item压入堆栈S */ struct SNode *TmpCell; TmpCell=(struct SNode *)malloc(sizeof(struct SNode)); TmpCell->Element = item; TmpCell->Next = S->Next; S->Next = TmpCell; }
出栈
c ElementType Pop(Stack S){ /* 删除并返回堆栈S的栈顶元素 */ struct SNode *FirstCell; ElementType TopElem; if(IsEmpty(S)){ printf(“堆栈空”); return NULL; } else { FirstCell = S->Next; S->Next = FirstCell->Next; TopElem = FirstCell ->Element; free(FirstCell); return TopElem; } }
应用:表达式求值
- 堆栈实现后缀表达式求值
从左到右读入后缀表达式的各项(运算符或运算数)- 运算数:入栈;
- 运算符:从堆栈中弹出适当数量的运算数,计算并结果入栈;
- 最后,堆栈顶上的元素就是表达式的结果值
- 堆栈实现中缀表达式求值
基本策略:将中缀表达式转换为后缀表达式,然后求值- 无括号时
- 运算数相对顺序不变
- 运算符号顺序发生改变
- 需要存储“等待中”的运算符号
- 要将当前运算符号与“等待中”的最后一个运算符号比较
- 有括号时
将中缀表达式转换为后缀表达式- 从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理。
- 运算数:直接输出;
- 左括号:压入堆栈;
- 右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出);
- 运算符:
- 若优先级大于栈顶运算符时,则把它压栈;
- 若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
- 若各对象处理完毕,则把堆栈中存留的运算符一并输出
- 从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理。
- 无括号时
堆栈的其他应用:
- 函数调用及递归实现
- 深度优先搜索
- 回溯算法
2.3 队列
2.3.1 什么是队列
- 队列(Queue):只能在一端插入、另一端删除的线性表
- 操作:
- 数据插入:入队列(AddQ)
- 数据删除:出队列(DeleteQ)
- 原则:先进先出(FIFO)
- 操作:
- 队列的抽象数据类型描述
- 类型名称:队列(Queue)
- 数据对象集:一个有0个或多个元素的有穷线性表
- 操作集:长度为
MaxSize
的队列 \(Q\in\text{Queue}\),队列元素 \(item\in\text{ElementType}\)Queue CreatQueue(int MaxSize)
:生成长度为MaxSize的空队列int IsFullQ(Queue Q,int MaxSize)
:判断队列Q是否已满;void AddQ(Queue Q,ElementType item)
:将数据元素item插入队列Q中;int IsEmptyQ(Queue Q)
:判断队列Q是否为空;ElementType DeleteQ(Queue Q)
:将队头数据元素从队列中删除并返回
2.3.2 队列的顺序存储实现
队列的顺序存储结构通常由一个一维数组和一个记录队列头元素位置的变量front以及一个记录队列尾元素位置的变量rear组成
- 定义
c #define MaxSize <储存数据元素的最大个数> struct QNode{ ElementType Data[MaxSize]; int rear; int front; }; typedef struct QNode *Queue;
- 堆栈空和满的判别依据:
front == rear?
(无法区别)
解决方案:使用额外标记记录已存放个数或删除/插入标志位;或仅使用 n-1 个数组空间
- 堆栈空和满的判别依据:
入队列
c void AddQ(Queue PtrQ, ElementType item){ if ((PtrQ->rear+1)%MaxSize == PtrQ->front){ printf(“队列满”); return; } PtrQ->rear = (PtrQ->rear+1)% MaxSize; PtrQ->Data[PtrQ->rear] = item; } // Front和rear指针的移动采用“加1取余”法,体现了顺序存储的“循环使用”
出队列
c ElementType DeleteQ(Queue PtrQ){ if (PtrQ->front == PtrQ->rear){ printf(“队列空”); return ERROR; } else { PtrQ->front = (PtrQ->front+1)%MaxSize; return PtrQ->Data[PtrQ->front]; } }
2.3.3 队列的链式存储实现
队列的链式存储结构也可以用一个单链表实现。插入和删除操作分别在链表的两头进行(前删后入,不可逆)
定义
c struct Node{ElementType Data;struct Node *Next;}; struct QNode{ /* 链队列结构 */struct Node *rear; /* 指向队尾结点 */ struct Node *front; /* 指向队头结点 */}; typedef struct QNode *Queue; Queue PtrQ;
不带头结点的链式队列出队操作的一个示例
c ElementType DeleteQ(Queue PtrQ){ struct Node *FrontCell; ElementType FrontElem; if (PtrQ->front == NULL){ printf(“队列空”); return ERROR; } FrontCell = PtrQ->front; if (PtrQ->front == PtrQ->rear) /* 若队列只有一个元素 */ PtrQ->front = PtrQ->rear = NULL; /* 删除后队列置为空 */ else PtrQ->front = PtrQ->front->Next; FrontElem = FrontCell->Data; free(FrontCell); /* 释放被删除结点空间 */ return FrontElem; }