第一章 绪论
基本概念
数据:信息的载体,能输入到计算机中并被计算机程序识别和处理的符号的集合
数据元素:数据的基本单位,通常作为一个整体考虑和处理
数据项:一个数据元素由若干数据项组成,它是构成数据元素的不可分割的最小单位
假如一个班级就是一个数据,每个学生个体信息就是数据元素,而个体信息中,学号、姓名、年龄等信息就是一个个数据项
数据结构:相互之间存在一种或多种特定关系的数据元素的集合
数据对象:具有相同性质的数据元素的集合,是数据的一个子集
数据结构的三要素:
- 逻辑结构:数据元素之间的逻辑关系
-
集合:各个元素同属一个集合,元素间没关系
-
线性结构:数据元素之间一对一关系。除了首元素,所有元素都有唯一的前驱;除了尾元素,所有元素都有唯一的后继
-
树形结构:数据元素之间是一对多的关系
-
图结构:数据元素多对多的关系
-
- 物理结构(存储结构):如何用计算机表示数据元素的逻辑关系
- 顺序结构:逻辑上相邻的元素,物理位置也相邻
- 链式结构:逻辑相邻的元素,物理位置可以不相邻(当然,也可以相邻)
- 索引存储:在存储元素信息时,还建立附加的索引表,用于查找
- 散列存储(哈希存储):根据元素的关键字直接计算出该元素的存储地址。(下图是用关键字对3做取余运算,根据结果存到相应的位置)
- 顺序结构:逻辑上相邻的元素,物理位置也相邻
- 数据运算:针对逻辑结构指出运算的功能,运算的实现针对存储结构,指出运算的具体步骤
数据结构三要素的影响:
- 采用顺序存储,元素在物理上必须连续;若采用非顺序存储,则可以离散;
- 影响存储空间分配的方便程序;
- 影响数据运算的速度
数据类型:一个值的集合和定义在此集合上的一组操作的总称
- 原子类型:值不可再分的数据类型,比如int类型
- 结构类型:其值可以再分解为若干成分的数据类型,比如学生这个数据类型,由姓名(字符型)、年龄(int型)等组成
抽象数据类型(ADT):抽象数据组织及与之相关的操作。(比如把一篇文章里的所有句子当成一种抽象数据类型(句子类型),则对它赋予增删改查等操作。)
算法
程序=数据结构+算法
算法的特性:
- 有穷性:执行有穷步之后结束,每步在有穷时间内完成
- 确定性:相同的输入得出相同的输出
- 可行性:可以被执行
- 输入:零个或多个输入
- 输出:一个或多个输出
好算法的特质:
- 正确性
- 可读性:容易理解
- 健壮性:输入非法数据时,能适当做出反应或处理
- 高效率
- 低存储量
时间复杂度
事前预估算法时间开销T(n)与问题规模n的关系
时间复杂度的计算可只考虑代码中执行次数最多的或最深层的代码
时间复杂度考虑 n → ∞ n\rightarrow \infin n→∞,T(n)=O(f(n)),取的是表达式的高阶部分,如: T ( n ) = 100 n 4 + n 3 + 1 0 10 T(n)=100n^4+n^3+10^{10} T(n)=100n4+n3+1010,虽然表达式中 1 0 10 10^{10} 1010很大,但因为 n → ∞ n\rightarrow \infin n→∞,时间复杂度是 T ( n ) = O ( n 4 ) T(n)=O(n^4) T(n)=O(n4)(高阶项还要去掉系数100)
- 加法规划:只保留表达式中高阶项
- 乘法规则:多项相乘,都保留。如: T ( n ) = n 3 + n 2 log 2 n T(n)=n^3+n^2\log_2n T(n)=n3+n2log2n,则 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)
算法时间复杂度排序(最左边可以理解为用时最短):
O
(
1
)
<
O
(
log
2
n
)
<
O
(
n
)
<
O
(
n
log
2
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1)<O(\log_2n)<O(n)<O(n\log_2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
概括为常对幂指阶
其实就是拿表达式中每个项做高等数学 n → ∞ n\rightarrow\infin n→∞的极限运算,看哪个高阶,时间复杂度就是O(那个项)
三种复杂度:
- 最坏时间复杂度:考虑输入数据最坏的情况
- 平均时间复杂度:考虑所有情况的综合水平
- 最好时间复杂度:考虑输入数据最好的情况
空间复杂度
空间开销(内存开销)与问题规模n之间的关系
如果问题规模怎么变,运算时所需要的内存空间都是固定常量,则算法空间复杂度为S(n)=O(1),也称原地工作
空间复杂度的计算规则与时间复杂度一样
O ( 1 ) < O ( log 2 n ) < O ( n ) < O ( n log 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(\log_2n)<O(n)<O(n\log_2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
递归调用:空间复杂度就是递归的深度
第二章 线性表
线性表的概念
线性表:具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为: L = ( a 1 , a 2 , . . . , a n ) L=(a_1,a_2,...,a_n) L=(a1,a2,...,an)
除首元素 a 1 a_1 a1外,其他元素都有唯一的直接前驱( a 2 a_2 a2的前驱是 a 1 a_1 a1, a 3 a_3 a3的前驱是 a 2 a_2 a2);除尾元素 a 4 a_4 a4外,其他元素都有唯一的直接后继( a 1 a_1 a1的后继是 a 2 a_2 a2, a 2 a_2 a2的后继是 a 3 a_3 a3)
线性表的基本操作:
&符号是引用的意思,表示经过操作后会被修改
- 初始化表:构造一个空的线性表L,分配内存空间
InitList(&L)
- 销毁:销毁线性表,释放线性表L所占用的内存空间
DestroyList(&L)
- 插入:在L中的第i个位置上插入指定过犹不及e
ListInsert(&L,i,e)
- 删除:删除L中第i个位置的元素,并用e返回删除元素的值
ListDelete(&L,i,&e)
- 按值查找:按给定的值查找
LocateElem(L,e)
- 按位查找:获取L中第i个位置的元素值
GetElem(L,i)
- 求表长:求L中元素个数
Length(L)
- 输出操作:按前后顺序输出L的所有元素值
PrintList(L)
- 判空:若L为空表,返回true,否则返回false
Empty(L)
概括为:创销、增删改查
顺序表
顺序表:用顺序存储的方式实现的线性表(逻辑相邻,物理也相邻)
静态分配:
#define MaxSize 10//定义最大长度
typedef struct{
ElemType data[MaxSize];//用数组实现
int length;//顺序表当前长度
}SqList;
动态分配:
#define InitSize 10
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
顺序表特点:
- 随机访问:第n个元素的地址满足表达式:首元素位置+(n-1)*元素空间大小,所以可以在O(1)时间内找到第i个元素
- 存储密度高:每个节点只存储数据元素
- 拓展容量不方便
- 插入删除不方便
顺序表插入删除
插入:先把插入位置之后的所有元素往后移,再插入元素
void ListInsert(SqList &L, int i, int e){
for(int j=L.length; j>=i; j--)//元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e;
L.length++;
}
例如,将b插入到i=3的位置,j=7,算法过程如下图
时间复杂度:
- 最好情况:插入表尾,O(1)
- 最坏情况:插入表头,O(n)
- 平均情况: p = 1 n + 1 , n p + ( n − 1 ) p + . . . + 1 p = n ( n + 1 ) 2 1 n + 1 = n 2 p=\dfrac{1}{n+1},np+(n-1)p+...+1p=\dfrac{n(n+1)}{2}\dfrac{1}{n+1}=\dfrac{n}{2} p=n+11,np+(n−1)p+...+1p=2n(n+1)n+11=2n,时间复杂度为O(n)
删除:将后面的元素往前覆盖,再让长度-1
bool ListDelete(SqList &L, int i, int &e){
if(i<1 || i>L.length) //判断i是否超范围
return false
e=L.data[i-1];
for(int j=i; j<L.length; j++)
L.data[j-1]=L.data[j];
L.length--;
return true;
}
过程如下图
时间复杂度:
- 最好情况:删除表尾,O(1)
- 最坏情况:删除表头,O(n)
- 平均情况: p = 1 n , ( n − 1 ) p + ( n − 2 ) p + . . . + 1 p = n ( n − 1 ) 2 1 n = n − 1 2 p=\dfrac{1}{n},(n-1)p+(n-2)p+...+1p=\dfrac{n(n-1)}{2}\dfrac{1}{n}=\dfrac{n-1}{2} p=n1,(n−1)p+(n−2)p+...+1p=2n(n−1)n1=2n−1,时间复杂度O(n)
注意:删除第i个元素与删除数组下标为i的元素,完全不同,第i个元素的数组下标是i-1,因为数组从0开始
查找
按位查找:
静态
ElemType GetElem(SqList, int i){
return L.data[i-1];
}
动态:malloc函数申请一整片连续的空间,用若干内存存储一个元素
ElemType GetElem(SeqList L, int i){
return L.data[i-1];
}
时间复杂度O(1),可以实现随机存取
按值查找:
int LocateElem(SeqList L, ElemType e){
for (int i=0; i<L.length; i++)
if(L.data[i]==e)//两个结构体不能直接这样比较,要分别比较分量
return i+1;
return 0;
时间复杂度:
最好O(1)
最坏O(n)
平均O(n)
单链表
每个结点除了存放数据元素外,还要存储指向下一个结点的指针
优点:物理上离散,不需要大片连续空间
缺点:不能随机存取
typedef struct LNode{
ElemType data;//数据域
struct LNode *next;//指针域
}LNode, *LinkList;
//如果强调的是节点,用LNode *
//如果强度的是单链表,用LinkList
初始化单链表:
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L==NULL)//内存不足
return false;
L->next = NULL;
return true;
}
不带头结点的单链表判空:
bool Empty(LinkList L){
return (L==NULL);
}
带头结点的单链表判空:
bool Empty(LinkList L){
if(L->next == NULL)
return true;
else
return false;
}
头结点不存储数据
单链表插入和删除
按位序插入
带头结点:
头结点可以看成是第0个结点,插入时先连接后结点,再连接前结点
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i-1){
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next=p->next;
p->next=s;
return true
}
不带头结点:
i=1时,需要特殊处理,需要改变头指针L的指向
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
if(i==1){
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s;
return true;
}
LNode *p;
int j=1;
p=L;
while(p!=NULL && j<i-1){
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next=p->next;
p->next=s;
return true
}
指定结点的后插操作
时间复杂度O(1)
bool InsertNextNode(LNode*p,ElemType e){
if(p==NULL)
return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
if(s==NULL)
return false;
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
指定结点的前插操作
申请新结点s,把p结点的值复制到s,再把p结点的值改成要插入的值(结点不能跑,但结点数据可以跑),时间复杂度为O(1)
bool InsertPriorNode(LNode*p, ElemType e){
if(p==NULL)
return false;
LNode*s=(LNode*)malloc(sizeof(LNode));
if(s=NULL)
return false;
s->next=p->next;
p->next=s;
s->data=p->data;
p->data=e;
return true;
}
按位序删除
带头结点:
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i-1){
p=p->next;
j++
}
if(p==NULL)
return false;
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
最好O(1)
最坏、平均O(n)
指定结点的删除
将待删除结点p的下一个结点的数据复制到p,再删除p的下一个结点。
bool DeleteNode(LNode*p){
if(p==NULL)
return false;
LNode *q=p->next;
p->data=p->next->data;//如果q是NULL,会出错
p->next=q->next;
free(q);
}
如果考虑到q为NULL的情况,时间复杂度最终还是O(n)
单链表查找
按位查找
带头结点:
LNode *GetElem(LinkList L, int i){
if(i<0)
return NULL;
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i){
p=p->next;
j++;
}
return p;
}
平均时间复杂度O(n)
不带头结点(略)
按值查找
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL && p->data!=e)
p=p->next;
return p;
}
求表长
int Length(LinkList L){
int len=0;
LNode *p=L;
while(p->next != NULL){
p=p->next;
len++;
}
return len;
}
单链表的建立
尾插法
相当于每次都执行一次后插操作
设置变量length记录链表长度
while循环{
每次取一个数据元素e;
ListInsert(L,length+1,e)
length++;
}
时间复杂度O( n 2 n^2 n2)
优化:设置一个尾指针r,指向最后一个元素,时间复杂度为O(n)
头插法
while循环{
每次取一个数据元素e;
InsertNextNode(L,e);
}
头插法可用于链表的逆置
双链表
定义
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinklist;
初始化
bool InitDLinkList(DLinklist &L){
L=(DNode*)malloc(sizeof(DNode));
if(L==NULL)
return false;
L->prior=NULL;
L->next=NULL;
return true;
}
插入
bool InsertNextDNode(DNode *p, DNode *s){
if(p==NULL||s==NULL)
return false;
s->next=p->next;
if(p->next != NULL)
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
前插操作相当于找到前一个结点,执行后插操作
双链表删除
bool DeleteNextDNode(DNode *p){
if(p==NULL)
return false;
DNode *q=p->next;
if(q==NULL)
return false;
p->next=q->next;
if(q->next!=NULL)
q->next->prior=p;
free(q);
return true;
}
双链表的遍历
后向遍历
while(p!=NULL){
p=p->next;
}
前向遍历
while(p!=NULL){
p=p->prior;
}
双链表不支持随机存取,查找只能用遍历,时间复杂度为O(n)
循环链表
循环单链表
定义
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
初始化
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode));
if(L==NULL)
return false;
L->next=L;
return true;
}
判空
bool Empty(LinkList L){
if(L->next==L)
return true;
else
return false;
}
从任何一个结点出发,都能找到其他结点
从头结点找到尾部,时间复杂度为O(n);从尾找到头,O(1)
循环双链表
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinklist;
表头结点prior指向表尾结点
表尾结点的next指向表头结点
初始化
bool InitDLinkList(DLinklist &L){
L=(DNode*)malloc(sizeof(DNode));
if(L==NULL)
return false;
L->prior=L;
L->next=L;
return true;
}
判空
bool Empty(DLinklist L){
if(L->next==L)
return true;
else
return false;
}
插入
后插
bool InsertNextDNode(DNode *p, DNode *s){
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
}
删除
p->next=q->next;
q->next->prior=p;
free(q);
静态链表
数据元素+下个结点的数组下标(游标)
分配一整片连续的内存空间,各个结点集中安置
定义
#define MaxSize 10
struct Node{
ElemType data;
int next;
}SLinkList[MaxSize];
查找:从头结点逐个往后遍历,时间复杂度O(n)
插入位序为i的结点:
- 找到一个空的结点,存入数据元素;
- 从头结点出发,找到位序为i-1的结点;
- 修改新结点的next;
- 修改i-1号结点的next
注:在初始化时可以标记哪些是空结点,比如游标为-2表示空结点
优点:不用大量移动元素
缺点:不能随机存取,容量不可变
第三章 栈和队列
栈
栈(Stack)是只允许在一端插入或删除操作的线性表
栈顶:a5,允许插入删除的一端
栈底:a1,不允许插入删除的一端
出入顺序:后入先出(LIFO),入的顺序是a1~a5,出的顺序是a5~a1
InitStack(&S):初始化栈
DestroyStack(&L):销毁栈
Push(&S,x):进栈
Pop(&S,&x):出栈
GetTop(S,&x):读栈顶元素
StackEmpty(S):判空
卡特兰数:n个不同元素进栈,出栈元素不同排列的个数为 1 n + 1 C 2 n n \dfrac{1}{n+1}C^n_{2n} n+11C2nn
顺序栈
定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
初始化
void InitStack(SqStaxk &S){
S.top=-1;
}
判空
bool StackEmpty(SqStack S){
if(S.top==-1)
return true;
else
return false;
}
入栈
bool Push(SqStack &S,ElemType x){
if(S.top==MaxSize-1)
return false;
S.top=S.top+1;//……1
S.data[S.top]=x;//……2
//1、2两句等价于S.data[++S.top]=x;
return true;
}
S.data[S.top]=x;
S.top=S.top+1;
//以上两句等价于S.data[S.top++]=x;
出栈
bool Pop(SqStack &S, ElemType &x){
if(S.top==-q)
return false;
x=S.data[S.top];//……1
S.top=S.top-1;//……2
//1、2两句等价于x=S.data[S.top--];
return true;
}
S.top=S.top-1;
x=S.data[S.top]
//以上两句等价于x=S.data[--S.top];
读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top==-1)
return false;
x=S.data[S.top];
return true;
}
另一种方式:空栈时top指针指向0而不是-1,这种方式操作代码与之前的不同,相当于先让元素入栈,再移动指针
共享栈
定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top0;
int top1;
}ShStack;
链栈
用头插法建立单链表,过程跟栈入类似
typedef struct Linknode{
ElemType data;
struct Linknode *next;
}*LiStack;
带头结点与不带头结点的操作区别
队列
队列:只允许一端插入(入队),另一端删除(出队)的线性表
队列的模式跟平时的排队是一样的,先进先出(FIFO),入的顺序是a1~a3,出的顺序也是a1~a3
空队列:无任何元素的队列
队头:允许删除的一端,a1
队尾:允许插入的一端,a3
基本操作
InitQueue(&Q):初始化
DestroyQueue(&Q):销毁
EnQueue(&Q):入队
DeQueue(&Q,&x):出队
GetHead(Q,&x):读队头元素
顺序队列
定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
front指向队头元素,rear指向队尾的后一个位置
初始化
void InitQueue(SqQueue &Q){
Q.rear=Q.front=0;
}
判空
bool QueueEmpty(SqQueue Q){
if(Q.rear==Q.front)
return true;
else
return false;
}
入队操作
bool EnQueue(SqQueue &Q, ElemType x){
if(队列已满)
return false;
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize;
return true;
}
rear == MaxSize 时队列并不一定是满的,front前面有位置
可以用取余运算使rear跑到front前面
取余运算: 3 × 7 + 3 = 24 3\times7+3=24 3×7+3=24,则 23 % 7 = 3 23\%7=3 23%7=3
采用取余运算之后,队列在逻辑上呈环状,称为循环队列
这样会有一个位置不能使用,会浪费一个位置。如果全部填满,则rear == front,不能判断队列是空还是满
判满
关键语句:(Q.rear+1)%MaxSize == Q.front
入队
bool EnQueue(SqQueue &Q, ElemType x){
if((Q.rear+1)%MaxSize==Q.front)
return false;
Q.data[Q.rear]=x;
Q.rear=Q.rear+1;
return true;
}
出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear==Q.front)
return false;
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;
return true;
}
判满或判空
- 队列已满:(Q.rear+1)%MaxSize == Q.front
- 队列元素个数计算:(rear+MaxSize-front)%MaxSize
- 如果不能浪费一个空间,则可以定义为
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;
每次入队size++,每次出队size–,用size来判空判满
也可以定义为
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;
插入就将tag变为1,删除就将tag变为0;队列变满只可能是插入造成,队列变空只可能是删除造成
队满:if(front==rear && tag==1)
队空:if(front==rear && tag==0)
注意:有时候题目的队列rear指向的是当前的队尾元素,并非队尾元素的后一位
此时插入操作:
Q.rear=(Q.rear+1)%MaxSize;
Q.data[Q.rear]=x;
还有很多出题规则,自行分析
链式队列
定义
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
初始化(带头结点)
void InitQueue(LinkQueue &Q){
Q.fornt=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
Q.front->next=NULL;
}
判空(带头结点)
bool IsEmpty(LinkQueue Q){
if(Q.fornt==Q.rear)//或者是if(front->next==NULL)
return true;
else
return false;
}
初始化(不带头结点)
void InitQueue(LinkQueue &Q){
Q.fornt=NULL;
Q.rear==NULL;
}
判空(不带头结点)
bool IsEmpty(LinkQueue Q){
if(Q.fornt==NULL)
return true;
else
return false;
}
入队(带头结点)
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
s->data=x;
s->next=NULL;
Q.rear->next=s;
Q.rear=s;
}
入队(不带头结点)
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
s->data=x;
s->next=NULL;
if(Q.front==NULL){
Q.front=s;
Q.rear=s;
}
else{
Q.rear->next=s;
Q.rear=s;
}
}
出队(带头结点)
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front==Q.rear)
return false;
LinkNode *p=Q.front->next;
x=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front;
free(p);
return true;
}
出队(不带头结点)
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front==NULL)
return false;
LinkNode *p=Q.front;
x=p->data;
Q.fornt=p->next;
if(Q.rear==p){
Q.front=NULL;
Q.rear=NULL;
}
free(p);
return true;
}
链式队列不关心是否队满
双端队列
两端插入、两端删除的线性表
如果限制双端队列只能在一端插入删除,那么就变成了栈,所以栈能实现的功能,双端队列也能实现
输入受限的双端队列:只允许从一端插入,两端删除的线性表
输出受限的双端队列:只允许从两端插入、一端删除的线性表
考点:判断输出序列合法性
栈的应用——括号匹配
3是最后出进入,却最先被匹配,符合后进先出LIFO的规则,因此,是一个栈模型
出现左括号就入栈,出现右括号,就消耗一个左括号,相当于栈顶的左括号出栈
不匹配的情况:
- 扫描到右括号且栈空,说明右括号多出来了,没有相应的左括号与之匹配
- 处理完之后,栈非空,说明左括号多了,没有相应的右括号与之匹配
- 右左括号不匹配
算法实现
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S)
bool StackEmpty(SqStack S)
bool Push(SqStack &S, char x)
bool Pop(SqStack &S, char x)
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
for(int i=0; i<length; i++){
if(str[i]=='(' || str[i]=='[' || str[i]=='{'){
Push(S, str[i]);
}
else{
if(StackEmpty(S))
return false;
char topElem;
Pop(S, topElem);
if(str[i]==')' && topElem!='(')
return false;
if(str[i]==']' && topElem!='[')
return false;
if(str[i]=='}' && topElem!='{')
return false;
}
}
return StackEmpty(S);
}
栈的应用——表达式求值
表达式:操作数、运算符、界限符(就是括号)
中缀表达式:运算符在操作数之间,平时我们写的表达式,如
a
+
b
a+b
a+b
后缀表达式(逆波兰式):运算符在操作数之后,如
a
b
+
a b +
ab+
前缀表达式(波兰式):运算符顺操作数之前,如
+
a
b
+ab
+ab
中缀:
a
+
b
−
c
∗
d
a+b-c*d
a+b−c∗d
后缀:把
a
+
b
a+b
a+b看成一个整体,写成
a
b
+
ab+
ab+,把
c
∗
d
c*d
c∗d看成一个整体,写成
c
d
∗
cd*
cd∗,再把
a
b
+
ab+
ab+和
c
d
∗
cd*
cd∗看成两个操作数,最终就写成
a
b
+
c
d
∗
−
ab+cd*-
ab+cd∗−
前缀:与后缀规则类似,最终是
−
+
a
b
∗
c
d
-+ab*cd
−+ab∗cd
中缀转后缀的手算方法:
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照:左操作数 右操作数 运算符 的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续2
因为中缀表达式运算顺序可以不唯一,所以对应的后缀表达式顺序也不唯一,如果要计算机实现计算,必须做出规定,使结果唯一
规定:左优先原则,只要左边的运算符能先计算,就优先算左边的
A + B ∗ ( C − D ) − E / F A+B*(C-D)-E/F A+B∗(C−D)−E/F的后缀表达式: A B C D − ∗ + E F / − ABCD-*+EF/- ABCD−∗+EF/−
( ( 15 ÷ ( 7 − ( 1 + 1 ) ) ) × 3 ) − ( 2 + ( 1 + 1 ) ) ((15\div(7-(1+1)))\times3)-(2+(1+1)) ((15÷(7−(1+1)))×3)−(2+(1+1))的后缀表达式: 15 7 1 1 + − ÷ 3 × 2 1 1 + + − 15\space7\space1\space1+-\div3\times2\space1\space1++- 15 7 1 1+−÷3×2 1 1++−
后缀表达式手算:
从左往右扫描,每遇到一个运算符,就让运算符前面最好近的两个操作数执行对应运算,合体为一个操作数
- 扫描 15 7 1 1 + − ÷ 3 × 2 1 1 + + − 15\space7\space1\space1+-\div3\times2\space1\space1++- 15 7 1 1+−÷3×2 1 1++−
- 遇到第一个 + + +,执行 1 + 1 1+1 1+1,变成 15 7 2 − ÷ 3 × 2 1 1 + + − 15\space7\space2-\div3\times2\space1\space1++- 15 7 2−÷3×2 1 1++−
- 遇到 − - −,执行 7 − 2 7-2 7−2,变成 15 5 ÷ 3 × 2 1 1 + + − 15\space5\div3\times2\space1\space1++- 15 5÷3×2 1 1++−
- 遇到 ÷ \div ÷,执行 15 ÷ 5 15\div5 15÷5,变成 3 3 × 2 1 1 + + − 3\space3\times2\space1\space1++- 3 3×2 1 1++−
- 遇到 × \times ×,执行 3 × 3 3\times3 3×3,变成 9 2 1 1 + + − 9\space2\space1\space1++- 9 2 1 1++−
- 遇到 + + +,执行 1 + 1 1+1 1+1,变成 9 2 2 + − 9\space2\space2+- 9 2 2+−
- 遇到 + + +,执行 2 + 2 2+2 2+2,变成 9 4 − 9\space4- 9 4−
- 遇到 − - −,执行 9 − 4 9-4 9−4,变成 5 5 5
用栈实现后缀表达式的计算:
- 从左往右扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到1,否则执行3
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
A B + C D ∗ E / − F + AB+CD*E/-F+ AB+CD∗E/−F+
-
A入栈
-
B入栈
-
遇到+,弹出B、A,执行A+B(注意顺序,是A+B,而不是B+A),结果压回栈中
-
C入栈
-
D入栈
-
遇到 ∗ * ∗,弹出D、C,执行 C ∗ D C*D C∗D(而不是 D ∗ C D*C D∗C),结果压回栈
-
E入栈
-
遇到 / / /,弹出E、 C ∗ D C*D C∗D,执行 C ∗ D / E C*D/E C∗D/E,结果压回栈
-
遇到 − - −,弹出 C ∗ D / E C*D/E C∗D/E、 A + B A+B A+B,执行 ( A + B ) − ( C ∗ D / E ) (A+B)-(C*D/E) (A+B)−(C∗D/E),结果压回栈中
-
F入栈
-
遇到+,弹出F、 ( A + B ) − ( C ∗ D / E ) (A+B)-(C*D/E) (A+B)−(C∗D/E),执行 [ ( A + B ) − ( C ∗ D / E ) ] + F [(A+B)-(C*D/E)]+F [(A+B)−(C∗D/E)]+F,结果压回栈
中缀表达式转后缀表达式(机算):初始化一个栈,用于保存暂时不能确定运算顺序的运算符,从左到右处理各元素,直到末尾
- 遇到操作数,直接加入后缀表达式
- 遇到界限符,(直接入栈,)则依次弹出栈内运算符并加入后缀表达式,直到弹出(为止,但(不加入后缀表达式
- 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到(或栈空则停止,之后再把当前运算符入栈
A + B − C ∗ D / E + F A+B-C*D/E+F A+B−C∗D/E+F
- A入表达式,得A
- +入栈
- B入表达式,得AB
- 遇到 − - −,弹出+,得AB+,再将 − - −入栈
- C入表达式,得AB+C
- 遇到 ∗ * ∗,入栈
- D入表达式,得AB+CD
- 遇到 / / /,弹出 ∗ * ∗,得AB+CD*,再将 / / /入栈
- E入表达式,得AB+CD*E
- 遇到 + + +,弹出 / / /和 − - −,得AB+CD*E/-,再将 + + +入栈
- F入表达式,得AB+CD*E/-F,将 + + +弹出,得AB+CD*E/-F+
中缀表达式的计算:运算符栈+操作数栈
中缀转前缀,使用右优先规则,得唯一结果
转换方法与中缀转后缀类似
栈的应用——递归
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:调用返回地址、实参、局部变量
递归算法:把原始问题转换为属性相同,但规模较小的问题
如阶乘n!、斐波那契数列
F i b ( n ) = { F i b ( n − 1 ) + F i b ( n − 2 ) , n > 1 1 , n = 1 0 , n = 0 Fib(n)=\begin{cases} Fib(n-1)+Fib(n-2),n>1 \\ 1,n=1\\ 0,n=0 \end{cases} Fib(n)=⎩⎪⎨⎪⎧Fib(n−1)+Fib(n−2),n>11,n=10,n=0
队列应用
- 树的层次遍历
- 图的广度优先遍历
- 操作系统中多进程间的调用策略,FCFS
- 打印数据缓冲区
矩阵的压缩存储
本节的公式不建议记,可以画简图推算出需要多少存储空间、对应的数组下标是多少
一组数组:起始地址LOC,a[i]的存放地址=LOC+i*sizeof(ElemType)
二维数组:也具有随机存取的特性
行优先:一行一行地存储,共有M行N列,b[i][j]=LOC+(i*N+j)*sizeof(ElemType)
列优先:一列一列地存储,共有M行N列,b[i][j]=LOC+(j*M+i)*sizeof(ElemType)
矩阵下标从1开始,但计算机的数组下标是从0开始
对称矩阵:若n阶方阵中任意一个元素 a i , j a_{i,j} ai,j都有 a i , j = a j , i a_{i,j}=a_{j,i} ai,j=aj,i,则该矩阵为对称矩阵
i=j称为主对角线,主对角线之上的区域称为上三角区(i<j),之下称为下三角区(i>j)
存储策略:只存储主对角线+下三角区,按行优先原则存入一维数组中
n阶矩阵需要 1 + 2 + 3 + . . + n = n ( n + 1 ) 2 1+2+3+..+n=\dfrac{n(n+1)}{2} 1+2+3+..+n=2n(n+1)个存储单元,对应一维数组下标是 n ( n + 1 ) 2 − 1 \dfrac{n(n+1)}{2}-1 2n(n+1)−1
a i , j a_{i,j} ai,j对应数组下标可以画个简图推算出来,不建议记公式
三对角矩阵:带状矩阵,当
∣
i
−
j
∣
>
1
时
|i-j|>1时
∣i−j∣>1时有
a
i
,
j
=
0
(
1
≤
i
,
j
≤
n
)
a_{i,j}=0(1\leq i,j\leq n)
ai,j=0(1≤i,j≤n)
存储策略:按行优先原则,只存储带状部分
稀疏矩阵:非零元素远远少于矩阵元素的个数
存非0的元素
顺序存储:三元组(行,列,值)
链式存储:十字链表法
数据结构为:
(行,列,值,指向同列的下一个元素,指向同行的下一个元素)
向右域指向本行的第一个元素,向下域指向本列的第一个元素
第四章 串
串
定义:即字符串,是由零个或多个字符组成的有限序列,它是一种特殊的线性表,一般记为S=’
a
1
a
2
a
3
.
.
.
a
n
a_1a_2a_3...a_n
a1a2a3...an’
S为串名,
a
i
a_i
ai为值,当n=0时,称为空串
- 子串:串中任意个连续的字符组成的子序列
- 主串:包含子串的串
- 字符在主串中的位置:字符在串中的序号,从1开始
- 子中在主串中的位置:子串的第一个字符在主串中的位置
注意:空格也是字符,含有空格的串不会是空串
串的基本操作,如增删改查等通常以子串为操作对象
串的基本操作:
StrAssign(&T,chars)//赋值操作,把T赋给chars
StrCopy(&T,chars)//复制操作,由串S复制得到串T
StrEmpty(S)//判空操作。若S为空串,返回TRUE,否则返回FALSE
StrLength(S)//求串长,返回元素个数
ClearString(&S)//清空操作
DestroyString(&S)//销毁串
Concat(&T,S1,S2)//串联接。用T返回由S1和S2联接而成的新串
SubString(&Sub,S,pos,len)//求子串。用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T)//定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0
StrCompare(S,T)//比较大小,若S>T则返回值>0,S=T返回0,S<T返回值<0
串的存储结构
静态数组(定长顺序存储):
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
动态数组(堆分配存储):
typedef struct{
char *ch;
int length;
}HString;
顺序存储方案:
- 把length放到最后
- 把length放到最前面,这样字符位序与数组下标相同,但长度不能超过255,不然char变量无法表示
- 没有length变量,以字符"\0"表示结尾,它对应ASCII编码的0
- 第一个位置不用,把length放到最后面
串的链式存储:
typedef struct StringNode{
char ch[N];
struct StringNode *next;
}StringNode, *String;
如果存不满,可以用特定的字符填充
求子串:
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos+len-1>S.length)
return false;
for(int i=pos; i<pos+len; i++)
Sub.ch[i-pos+1]=S.ch[i];
Sub.length=len;
return true;
}
比较操作:
int StrCompare(SString S, SString T){
for(int i=1; i<=S.length && i<=T,length; i++){
if(S.ch[i]!=T.ch[i])
return S.ch[i]-T.ch[i];
}
return S.length-T.length;
}
定位操作:
int Inde(SString S, SString T){
int i=1, n=StrLength(S), m=StrLength(T);
SString sub;
while(i<=n-m+1){
SubString(sub,S,i,m);
if(StrCompare(sub,T)!=0)
++i;
else
return i;
}
return 0
}
朴素模式匹配算法
串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置
int Index(SString S,SString T){
int k=1;
int i=k,j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.ch[j]){
++i;
++j;//继续比较后继字符
}
else{
k++;//检查一下子串
i=k;
j=1;
}
}c
if(j>T.length)
return k;//返回位置
else
return 0;
}
若模式串长m,主串长n,则:
- 匹配成功最好的时间复杂度O(m)
- 匹配失败的最好时间复杂度O(n-m) ≈ \approx ≈O(n),因为通常情况下m远小于n
- 最坏的时间复杂度O(nm)
KMP算法
它是对朴素模式匹配算法的优化
朴素匹配算法缺点是主串的扫描指针i经常回溯,导致时间开销大
如果j=k(k小于等于模式串的长度)时才发现不匹配,说明1~k-1都匹配成功,此时j应该回溯到哪个位置?
它回溯的位置与模式串本身有关,建立一个数组next,用于记录j应该回到哪个位置
int Index_KMP(SString S,SString T,int next[]){
int i=1, j=1;
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
++i;
++j;
}
else
j=next[j];
}
if(j>T.length)
return i-T.length;
else
return 0;
}
KMP算法求next数组
如下图的匹配案例,当j=6不匹配时,说明1~5都匹配,此时j该回溯到哪里呢?
让模式串一步一步地往右移,发现移到下图位置时前面的ab正好匹配上,此时应该让j回溯到3的位置,与i比较
如下图案例,j=7时不匹配,让模式串一步一步往右移
移到如图位置时,前面部分abab可以匹配得上,此时j=7回溯到j=5的位置,与i比较
如果移到这种情况,看起来前面部分ab也匹配,但这样后面部分就不匹配了
所以,优先选择匹配长度大的(如前面的abab),而不是匹配长度小的(如ab)
next数组求法:只与模式串有关
next数组表示匹配失败时应该回溯到哪个位置
串的前缀:包含第一个字符,且不包含最后一个字符的子串,如S=ababc,则前缀为a、ab、aba、abab
串的后缀:包含最后一个字符,且不包含第一个字符的子串,如S=ababc,则后缀为c、bc、abc、babc
next[1]恒为0,next[2]恒为1
此外,当模式串第j个字符匹配失败,由前1~j-1个字符组成的串记为S,则next[j]=S的最长相等前后缀长度+1
模式串为abababac
- j=1时,next[1]=0;
- j=2时,前j-1个字符串S为a,前缀为空,后缀为空,最长相等前后缀长度为0,next[2]=1;
- j=3时,前j-1个字符串为ab,前缀为a,后缀为b,最长相等前后缀长度为0,next[3]=1;
- j=4时,aba,前缀a与后缀a相等,前缀ab与后缀ba不相等,最长相等前后缀长度为1,next[4]=2;
- j=5时,abab,前缀a不等于后缀b,前缀ab与后缀ab相等,前缀aba与后缀bab不相等,next[5]=3
- j=6时,ababa,最长相等前后缀为aba,next[6]=4;
- j=7时,ababab,最长相等前后缀为abab,next[7]=5;
- j=8时,abababc,最长相等前后缀为空,next[8]=1;
- j=9时,abababcd,最长相等前后缀为空,next[9]=1;
- j=10时,abababcde,最长相等前后缀为空,next[10]=1;
KMP算法优化——nextval数组
模式串S=‘google’,当j=4时,g与主串不匹配,按照next数组,j会回溯到j=1的位置,此时的字符还是g,匹配还是失败。为此产生了nextval数组优化算法。
nextval数组思路:
j=4的g匹配失败后,如果j=4的g与next[4]所指的位置j=1的字符g相同,则直接跳到next[1]所指位置0,遇到0,i++,j++,继续下一轮匹配。
所以,nextval数组的含义是当模式串匹配失败后,为了避免相同字符再次匹配而应该跳到的位置。
规则:
- nextval[1]恒为0
- 根据next数组跳到指定位置,如果字符相同,原来位置的nextval数组直接取跳到位置的nextval数组值
- 如果字符不相同,则原来位置的nextval数组的值就取自身的next数组的值
如上图,
- j=1时,nextval[1]=0
- j=2时,next[2]=1,但是j=2与j=1的字符相同,nextval[2]的值直接取nextval[1]的值,nextval[2]=0
- j=3时,next[3]=2,但j=3与j=2字符相同,所以nextval[3]直接取nextval[2]的值,nextval[3]=0
- j=4,next[4]=3,nextval[4]=nextval[3]=0
- j=5,next[5]=4,j=5与j=4字符不相同,nextval[5]=next[5]=4
含义:字符串中遇到a,因前面有大量相等值a,则直接i++,j++,进行下一轮匹配,没有必要让j回溯到1~j-1的位置;遇到b,因为b与a不相等,所以直接跳到该跳到的位置,也就是next[5]所指的位置
nextval数组的求法:
先算出next数组;
先令nextval[1]=0;
for(int j=2; j=T.length; j++){
if(T.ch[next[j]]==T.ch[j])
nextval[j]=nextval[next[j]];
else
nextval[j]=next[j];
}
第五章 树与二叉树
树的基本概念
树的模型:与大自然的树类似,只是倒了过来
根结点:没有前驱的结点。A结点叫根结点
边:连接两个结点的线
叶子结点:没有后继的结点。G、E、H结点叫叶子结点,它们是最尾端了,没有分支
分支结点:有后继的结点。B、C、D、F叫分支结点,它们有分支
穿树:结点数为0
非空树:有且仅有一个根结点;只有根结点没有前驱,只有叶子结点没有后继
树的定义:
树是
n
(
n
>
0
)
n(n>0)
n(n>0)个结点的有限集合,
n
=
0
n=0
n=0时,称为空树,在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根的结点
- 当
n
>
1
n>1
n>1时,其余结点可分为
m
(
m
>
0
)
m(m>0)
m(m>0)个互不相交的有限集合
T
1
,
T
2
,
.
.
.
,
T
m
T_1,T_2,...,T_m
T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根结点的子树
两个红框就是A的两棵子树
树是一种递归定义的结构,树本身包含更小的树
祖先结点:从一个结点出发,走到根结点,这条路径上的所有结点都是祖先结点。比如对于G来说,D、B、A都是祖先结点
子孙结点:一个结点分支下面的结点都是它的子孙结点。比如B结点的子孙结点是D、E、G
双亲结点(父结点):一个结点的直接前驱。比如G的父结点是D,D的父结点是B
孩子结点:一个结点的直接后继。比如B的孩子结点是D、E
兄弟结点:同一个结点的直接后继,它们之间称为兄弟结点。比如A生出B和C,所以B和C是兄弟结点,B生出了D和E,D和E是兄弟结点
堂兄弟结点:位于同一层,但不是相同的前驱结点之间称为堂兄弟结点。比如D和F是堂兄弟结点,E和F是堂兄弟结点,G和H是堂兄弟结点
结点间的路径:单向,从某结点(位于上面)一直沿边连接到某结点(位于下面),称为路径。比如AG的路径为ABDG,CH的路径为CFH
路径长度:路径上经历的边数,比如AG路径长度为3,CH路径长度为2
结点的层次(深度):从上往下数,有多少层。如上图,A是第一层,B和C是第二层,D、E、F是第三层,G、H是第四层
结点的高度:从下往上数,比如A的结点高度是4
树的高度(深度):总共有多少层。上图树高为4
结点的度:有几个分支,度就是几。比如B的度为2,A的度为2,D的度为1,G的度为0
树的度:各结点的度的最大值。比如上图中各结点的度最大值为2,树的度为2
有序树:逻辑上看,树中结点的各子树从左至右有次序,不能互换。
无序树:逻辑上看,树中结点的各子树从左至右无次序,可以互换。
森林:是
m
(
m
>
0
)
m(m>0)
m(m>0)棵互不相交的树的集合
树的性质
结点数=总度数+1
加1是因为根结点头上没有分支
树的度与m叉树的区别:
度为m的树 | m叉树 |
---|---|
任意结点的度 ≤ \leq ≤m(最多m个孩子) | 任意结点的度 ≤ \leq ≤m(最多m个孩子) |
至少有一个结点度=m(有m个孩子) | 允许所有结点的度都 < < <m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
度为m的树第i层至多有
m
i
−
1
(
i
≥
1
)
m^{i-1}(i\geq1)
mi−1(i≥1)个结点,m叉树的第i层至多有
m
i
−
1
(
i
≥
1
)
m^{i-1}(i\geq1)
mi−1(i≥1)个结点
每个结点都分出两条分支
高度为h的m叉树至多有 m h − 1 m − 1 \dfrac{m^h-1}{m-1} m−1mh−1个结点
其实是等比数列求和: a + a q + a q 2 + . . . + a q n − 1 = a ( 1 − q n ) 1 − q a+aq+aq^2+...+aq^{n-1}=\dfrac{a(1-qn)}{1-q} a+aq+aq2+...+aqn−1=1−qa(1−qn)
m 0 + m 1 + m 2 + . . . + m h − 1 = m h − 1 m − 1 m^0+m^1+m^2+...+m^{h-1}=\dfrac{m^h-1}{m-1} m0+m1+m2+...+mh−1=m−1mh−1
高度为h的m叉树至少有h个结点,高度为h、度为m的树至少有h+m-1个结点
具有n个结点的m叉树的最小高度为
⌈
log
m
(
n
(
m
−
1
)
+
1
)
⌉
\lceil\log_m(n(m-1)+1)\rceil
⌈logm(n(m−1)+1)⌉
每个结点都尽量有最多分支
前h-1层最多有
m
h
−
1
−
1
m
−
1
\dfrac{m^{h-1}-1}{m-1}
m−1mh−1−1个结点
前h层最多有 m h − 1 m − 1 \dfrac{m^{h}-1}{m-1} m−1mh−1个结点
列不等式得 m h − 1 − 1 m − 1 < n ≤ m h − 1 m − 1 \dfrac{m^{h-1}-1}{m-1}<n\leq \dfrac{m^{h}-1}{m-1} m−1mh−1−1<n≤m−1mh−1
化简得 m h − 1 < n ( m − 1 ) + 1 ≤ m h m^{h-1}<n(m-1)+1\leq m^h mh−1<n(m−1)+1≤mh
两边再取对数 h − 1 < log m ( n ( m − 1 ) + 1 ) ≤ h h-1<\log_m(n(m-1)+1)\leq h h−1<logm(n(m−1)+1)≤h
二叉树
二叉树是 n ( n ≥ 0 ) 个 结 点 的 有 限 集 合 n(n\geq0)个结点的有限集合 n(n≥0)个结点的有限集合:
- 或者为空二叉树, n = 0 n=0 n=0
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树(递归定义,自身包含更小的自身)
特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒(有序树)
注意:子树可以为空
满二叉树:一棵高度为h,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树。通俗地说,就是除了叶子结点,所有结点都有两个分支
特点:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父结点(如果存在)为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋
完全二叉树:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应,称为完全二叉树
满二叉树一定是完全二叉树,完全二叉树未必是满二叉树
特点:
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点
- 编号与满二叉树的编号一致
- i ≤ ⌊ n / 2 ⌋ i\leq \lfloor n/2 \rfloor i≤⌊n/2⌋为分支结点, i > ⌊ n / 2 ⌋ i>\lfloor n/2 \rfloor i>⌊n/2⌋为叶子结点
二叉排序树:
- 左子树上所有结点的关键字均小于根结点的关键字
- 右子树上所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一棵二叉排序树
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1
如图,左子树13、11、15高度为2,右子树50高度为1,相差为1
平衡二叉树的规定其实是为了方便搜索而设定的
二叉树性质
- 非空二叉树中度为0、1、2的结点个数分别为 n 0 n_0 n0、 n 1 n_1 n1和 n 2 n_2 n2,则有 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1,它表示叶子结点比双分支结点多一个
推导过程:
假设二叉树中结点总数为n,则
n
=
n
0
+
n
1
+
n
2
n=n_0+n_1+n_2
n=n0+n1+n2,再观察树的结构,发现除了根结点外,每个结点的头上都会连着一根线,于是总结点数就应该是总度数再加上根结点,
n
=
n
1
+
2
n
2
+
1
n=n_1+2n_2+1
n=n1+2n2+1,两式联立得结果
- 二叉树第i层至多有 2 i − 1 ( i > 1 ) 2^{i-1}(i>1) 2i−1(i>1)个结点
- 高度为h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点(满二叉树)
- 具有n个结点( n > 0 n>0 n>0)的完全二叉树高度h为 ⌈ log 2 ( n + 1 ) ⌉ \lceil\log_2(n+1)\rceil ⌈log2(n+1)⌉或 ⌊ log 2 n ⌋ + 1 \lfloor\log_2n\rfloor+1 ⌊log2n⌋+1
- 对于完全二叉树,可以由结点数n推出度为0、1、2的结点个数
完全二叉树度为1的结点数 n 1 = 0 o r 1 n_1=0\space or\space1 n1=0 or 1,因为 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1,因此 n 0 + n 2 n_0+n_2 n0+n2一定是奇数,若总数是偶数,则 n 1 = 1 n_1=1 n1=1,总数是奇数则为0
二叉树存储结构
顺序存储:
#define MaxSize 100
struct TreeNode{
ElemType value;//结点值
bool isEmpty;//结点是否为空
};
从左至右,从上至下存数据
基本操作:找到左孩子 2 i 2i 2i、右孩子 2 i + 1 2i+1 2i+1、父结点 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋、所在层次 ⌈ log 2 ( n + 1 ) ⌉ \lceil\log_2(n+1) \rceil ⌈log2(n+1)⌉或 ⌊ log 2 n ⌋ + 1 \lfloor\log_2n\rfloor+1 ⌊log2n⌋+1
如果不是完全二叉树,存储时编号顺序按完全二叉树来排序,没有的编号为空,则对应关系与完全二叉树一致
但是这样有缺点,如果二叉树只有一条左分支,那么存储单元将有很多空单元
因此,顺序存储结构只适用于完全二叉树
二叉树链式存储:
struct ElemType{
int value;
};
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNdode,*BiTree;
BiTree root = NULL;
root=(BiTree)malloc(sizeof(BiTNode));
root->data={1};
root->lchild=NULL;
root->rchild=NULL;
BiTNode *p=(BiTNode*)malloc(sizeof(BiTNode));
p->data={2};
p->lchild=NULL;
p->rchild=NULL;
root->lchild=p;
n个结点的二叉链表共有 n + 1 n+1 n+1个空链域
三叉链表:方便找父结点
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
struct BiTNode *parent;
}BiTNode,*BiTree;
二叉树遍历算法
- 层序遍历
- 先/中/后序遍历:利用递归特性访问
先序(先根)遍历:根左右
中序(中根)遍历:左根右
后序(中根)遍历:左右根
先序:
void PreOrder(BiTree){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序:
void InOrder(BiTree){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
后序:
void PostOrder(BiTree){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
- 先/中/后序可以与前缀、中缀和后缀表达式对应,但需要添加括号
- 递归求树的深度
int treeDepth(BiTree T){
if(T == NULL){
return 0;
}
else{
int l=treeDepth(T->lchild);
int r=treeDepth(T->rchild);
return l>r ? l+1:r+1;
}
}
二叉树的层序遍历
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左,右孩子插入队尾
- 重复3直至队列为空
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!IsEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
由遍历序列构造二叉树
中序遍历:左子树、根结点、右子树
BDCAE,给定一棵二叉树,中序序列唯一,但给定一个中序序列,二叉树却不唯一。比如BDCAE对应的树可以是
绪论:若只给出一棵二叉树的前、中、后序及层序遍历序列的一种,不能唯一确定一棵二叉树
由序列构造确定的二叉树:
- 前序+中序
- 后序+中序
- 层序+中序
前序:ADBCE
中序:BDCAE
由前序可确定A必是根结点,由中序可确定BDC为左子树,E为右子树
再看前序DBC,则D为根结点,由中序的BDC,则B为左子树,C为右子树
其他组合类似
如果缺少中序,只有前序、中序、层序两两组合,不能唯一确定一棵二叉树,例如:
前序:AB
后序:BA
层序:AB
可以对应
线索二叉树
中序序列DGBEAFC,可以视为一种线性表,D是表头,C是表尾,如果不知道根结点指针,只知道某一个结点,怎么像线性表一样,遍历全部结点?如果找结点的前驱和后继(比如F在中序序列中,前驱是A,后继是C)
思路:从根节点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点,当 q = = p q==p q==p时,pre为前驱,当 p r e = = p pre==p pre==p时,q为后继
对树模型进行改进:左孩子指针若空,则指向前驱(前驱线索),右孩子指针若空,则指向后继(后继线索)
- D为表头,左孩子指针指向NULL;
- G左孩子指针指针指向D,右孩子指针指向B;
- E的左孩子指针指向B,右孩子指针指向A;
- F左孩子指针指向A,右孩子指针指向C;
- C为表尾,右孩子指针指向NULL
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchid,*rchild;
int ltag,rtag;//左、右线索标志,为0表示指向孩子,为1表示指向线索
}ThreadNode,*ThreadTree;
其余先序、后序同理
二叉树的线索化
中序线索化:
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
ThreadNode *pre=NULL
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchild==NULL){
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
void CreateInThread(ThreadTree T){//中序线索化
pre=NULL;
if(T!=NULL){
InThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
先序线索化:
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag==0)//不加上会出现循环转圈
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchild==NULL){
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
void CreatePreThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
PreThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
后序线索化:
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
void visit(ThreadNode *q){
if(q->lchild==NULL){
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
void CreatePostThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
PostThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
线索二叉树找前驱、后继
找中序后继:
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
void InOrder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
找中序前驱:
ThreadNode *Lastnode(ThreadNode *p){
while(p->ltag==0)
p=p->rchild;
return p;
}
ThreadNode *Prenode(ThreadNode *p){
if(p->ltag==0)
return Lastnode(p->lchild);
else
return p->lchild;
}
void RevInOrder(ThreadNode *T){
for(ThreadNode *p=Lastnode(T);p!=NULL;p=Prenode(p))
visit(p);
}
先序可以找后继,无法找前驱;后序可以找前驱,不能找后继(除非采用三叉链表或者从根结点开始遍历寻找)
树存储结构
- 双亲表示法:每个结点中保存指向双亲的“指针”
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;//结点数
}PTree;
删除操作:修改结点数
-
将parent置为-1,修改相应的子孙结点
-
用最后的一个元素覆盖
-
孩子表示法(顺序+链式存储):
顺序存储各个结点,每个结点中保存孩子链表头指针
struct CTNode{
int child;
struct CTNode *next;
};
typedef struct{
ElemType data;
struct CTNode*firstChild;
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r;//结点数和根的位置
};
- 孩子兄弟表示法(链式存储)
左指针指向左孩子,右指针指向右兄弟
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
这种做法是把树转换成了二叉树
- 森林转二叉树:先把每棵树都转化为二叉树,再把树的根结点之间视为右兄弟,然后连接起来
树、森林的遍历
树的先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);
}
}
树的先根遍历序列和与之对应的二叉树先序序列相同
树的后根遍历:
void PostOrder(TreeNode *R){
if(R!=NULL){
visit(R);
while(R还有下一个子树T)
PostOrder(T);
}
}
树的后根遍历序列和与之对应的二叉树中序序列相同
树的层次遍历:
- 若树非空,则根节点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复2直到队列为空
深度优先遍历:树的先根遍历和后根遍历
广度优先遍历:树的层次遍历
森林的先序遍历:
- 若森林非空,则访问森林中第一棵树的根结点
- 先序遍历第一棵树中根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林
森林的先序遍历等同于依次对各个树进行先根遍历,也等同于对应二叉树的先序序列相同
森林的中序遍历:
- 若森林非空,则中序遍历森林中第一棵树的根结点的子树森林
- 访问第一棵树中根结点
- 中序遍历除去第一棵树之后剩余的树构成的森林
森林的中序遍历等同于依次对二叉树的中序遍历
三者对应关系如下:
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
二叉排序树
二叉排序树(二叉查找树BST):
空树,或者有以下特性:
- 左子树上所有结点的关键字均小于根结点的关键字
- 右子树上所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一棵二叉排序树
进行中序遍历,可得到一个递增的有序序列:
9、11、12、13、19、21、26、30、50、60、66、70
二叉排序树的查找:
若树非空,目标值与根结点比较,相等则查找成功;若小于根结点,则在左子树上查找,否则在右子树上查找,查找成功返回结点指针,查找失败返回NULL
非递归查找:
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
BSTNode *BST_Search(BSTree T,int key){
while(T!=NULL && key!=T->key){
if(key<T->key)
T=T->lchild;
else
T=T->rchild;
}
return T;
}
递归查找:
BSTNode *BSTSearch(BSTree T,int key){
if(T==NULL)
return NULL;
if(key==T->key)
return T;
else if(key<T->lchild,key);
return BSTSearch(T->lchild,key);
else
return BSTSearch(T->rchild,key);
}
二叉排序树的插入:
若为空,则直接插入;若关键字小于根结点值,则插入左子树,否则插入右子树
int BST_Insert(BSTree &T,int k){
if(T==NULL){
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1;
}
else if(k==T->key)
return 0;
else
return BST_Insert(T->rchild,k);
}
二叉排序树的构造:
void Creat_BST(BSTree &T,int str[],int n){
T=NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
二叉排序树的删除:
- 叶子结点直接删
- 若结点只有一棵子树,则让结点的子树替代它
- 若结点有左右子树,则令结点的直接后继(或直接前驱)替代结点,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况
查找效率分析:
查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
查找成功的平均查找长度ASL:次数乘以每层结点的个数之各,再除以总结点个数。
A
S
L
=
1
×
1
+
2
×
2
+
3
×
3
+
4
×
6
12
=
3.17
ASL=\dfrac{1\times1+2\times2+3\times3+4\times6}{12}=3.17
ASL=121×1+2×2+3×3+4×6=3.17
平均查找长度最坏情况下与最大树高相同, O ( h ) O(h) O(h),最好情况接近树的最小高度 ⌊ log 2 n ⌋ + 1 \lfloor \log_2n \rfloor+1 ⌊log2n⌋+1,为 O ( log 2 n ) O(\log_2n) O(log2n)
所以,要尽量使树更加饱满,更加矮,更加平衡
平衡二叉树
平衡二叉树(BT或B树),简称平衡树(AVL树),树上任一结点的左子树和右子树的高度差不超过1
结点的平衡因子=左子树高-右子树高
平衡二叉树的平衡因子为-1,0,1
typedef struct AVLNode{
int key;
int balance;
struct AVLNode *lchild,*rchild;
}AVLNode,*AVLTree;
平衡二叉树的插入:每次调整的对象都是最小不平衡子树,如图红框内的就是最小不平衡子树
- LL:在A的左孩子的左子树中插入导致不平衡
右旋
- RR:在A的右孩子的右子树中插入导致不平衡
左旋
- LR:在A的左孩子的右子树中插入导致不平衡
先左旋,后右旋
- RL:在A的右孩子的左子树中插入导致不平衡
先右旋,后左旋
与LR相似
平衡二叉树查找效率分析:
假设以
n
h
n_h
nh表示深度为
h
h
h的平衡树中含有的最少结点数,则有
n
0
=
0
,
n
1
=
1
,
n
2
=
2
n_0=0,n_1=1,n_2=2
n0=0,n1=1,n2=2,并且有
n
h
=
n
h
−
1
+
n
h
−
2
+
1
n_h=n_{h-1}+n_{h-2}+1
nh=nh−1+nh−2+1,则可以推出
n
3
=
4
,
n
4
=
7
,
n
5
=
12....
n_3=4,n_4=7,n_5=12....
n3=4,n4=7,n5=12....,所以如果
n
=
9
n=9
n=9,则最大高度只能为4
平衡二叉树的平均查找长度为 O ( log 2 n ) O(\log_2n) O(log2n)
哈夫曼树
结点的权:有某种现实含义的数值
结点的带树路径长度:从树的根到该结点的路径长度与该结点上权值的乘积
树的带树路径长度:树中所有叶结点的带权路径长度之和 W P L = ∑ i = 1 n w i l i WPL=\displaystyle\sum_{i=1}^{n}{w_il_i} WPL=i=1∑nwili
哈夫曼树:带权路径长度WPL最小的二叉树称为哈夫曼树
哈夫曼树的构造:每次都选择两个最小的结点
结点1、2、2、3、7,
- 1和2结合,得3、2、3、7
- 选择3和2结合,得5、3、7
- 选择5和3结合,得8、7
- 选择8和7结合,得15
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为 2 n − 1 2n-1 2n−1
- 哈夫曼树中不存在度为1的结点
- 哈夫曼树并不唯一,但WPL必然唯一且为最优
编码问题
A用00表示,B用01表示,C用10表示,D用11表示,现在有80个C,10个A,8个B,2个D
固定长度编码:每个字符用相等长度的二进制位表示,上述编码需要用 ( 80 + 10 + 8 + 2 ) × 2 = 200 (80+10+8+2)\times2=200 (80+10+8+2)×2=200长度字符
可变长度编码:允许对不同字符用不等长的二进位制表示
哈夫曼编码:用80、10、8、2构造哈夫曼树,可以规定,向左为0,向右为1(如果不规定,哈夫曼编码不唯一)
则C可以表示为1,A表示为01,B表示为001,D表示为000,共用
3
×
2
+
3
×
8
+
2
×
10
+
1
×
80
=
130
3\times2+3\times8+2\times10+1\times80=130
3×2+3×8+2×10+1×80=130个字符
前缀编码:没有一个编码是另一个编码的前缀。这样的编码没有歧义
有歧义的编码:A是1,B是111,则1111既可以是AAAA,也可以是AB,还可以是BA
第六章 图
图G由顶点集V和边集E组成,记G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系集合。若V={ V 1 , V 2 , . . . , V n V_1,V_2,...,V_n V1,V2,...,Vn},则用 ∣ V ∣ |V| ∣V∣表示图G中顶点的个数,也称图G的阶,E={ ( u , v ) ∣ u ∈ V , v ∈ V (u,v)|u\in V,v \in V (u,v)∣u∈V,v∈V},用 ∣ E ∣ |E| ∣E∣表示图G中边的条数
图不能是空图,因为图的顶点不能为空,但边可以为空
无向图:若E是无向边(简称边)的有限集合时,则图G为无向图,边的两个顶点为u,v,则无向边为(v,u)或(u,v)
有向图:若E是有向边(简称弧)的有限集合时,则图G为有向边。弧是顶点的有序对,记为<u,v>,u称为弧尾,v称为弧头,<u,v> ≠ \not= =<v,u>
简单图:不存在重得边,不存在顶点到自身的边。分为无向和有向图
多重图:图G中某两个结点之边的多于一条,又允许顶点通过同一条边和自己关联。分为无向和有向图
顶点的度:
- 对于无向图,顶点v的度是指依附于该顶点的边的条数,记为TD(v)
- 对于有向图,入度是以顶点v为终点的有向边和数目,记为ID(v),出度是以顶点v为起点的有向边的数目,记为OD(v);顶点v的度=入度+出度
如顶点B的入度为1,出度为2,度为3
在具有n个顶点、e条边的有向图中,入度=出度=e
路径:顶点之间的序列。有无向图中,路径没有方向,在有向图中,路径有方向
回路:第一个顶点和最后一个顶点相同的路径称为回路或环
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路
路径长度:路径中边的数目
点到点的距离:从顶点u出发,到顶点v的最短路径,若此路径不存在,则记该距离为无穷
连通:无向图中,两个顶点存在路径
强连通:有向图中,两个顶点相互有路径
连通图:若无向图G中任意两个顶点都是连通的,则称为连通图
强连通图:若有向图中任意一对顶点都是强连通的,则称为强连通图
对于n个顶点的无向图G,若G是连通图,则最少有n-1条边;若G是非连通图,则最多有 C n − 1 2 C_{n-1}^2 Cn−12条边
对于n个顶点的有向图G,若G是强连通图,则最少有n条边
子图:取出图的一些顶点和边
生成子图:子图中包含了原图的所有顶点(边无所谓)
连通分量:无向图中的极大连通子图(子图必须连通,且包含尽可能多的顶点和边)称为连通分量
如图中三个红圈为极大连通子图,也称为连通分量
强连通分量:有向图中的极大强连通子图(子图必须强连通,且包含尽可能多的顶点和边)称为强连通分量
如图中三个红圈为三个极在强连通子图,也称强连通分量。注意ABCD不是强连通,因为C没办法到达A、B、D。
生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图
如图中的红框为生成树。注意B和E不能连起来,因为连起来就不是极小连通子图了。生成树不是唯一的,还可以有其他的连线方式
边的权:每条边上的数字,称为权值
带权图/网:边上带有权值的图,也称为网
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带树路径长度
无向完全图:无向图中任意两个顶点之间都存在边。若有n个顶点,则边数在[0,n(n-1)/2]范围
有向完全图:有向图中任意两个顶点之间都有存在方向相反的两条弧。若有n个顶点,则边数在[0,n(n-1)]
树是一种特殊的图,是不存在回路,且连通的无向图
邻接矩阵法
无向图:有边为1,无边为0。
有向图:有指向为1,无指向为0
#define MaxVertexNum 100
typedef struct{
char Vex[MaxVertexNum];//顶点表
int Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
int vexnum,arcnun;//图的当前顶点数和边数
}MGraph;
无向图第i个结点的度:第i行或第i列的非零元素个数,时间复杂度O(n),空间复杂度O( ∣ v ∣ 2 |v|^2 ∣v∣2)
有向图第i个结点的度:
第i个结点的出度为第i行的非零元素的个数;
第i个结点的入度为第i列的非零元素的个数;
第i个结点的度为入度+出度
带权图的存储:点顶到自身、不可到达可以用无穷表示(自身到自身也可以表示为0),其余的为权值
邻接矩阵的n次方 A n [ i ] [ j ] A^n[i][j] An[i][j]等于由顶点i到顶点j的长度为n的路径的数目
邻接矩阵适合存储稠密图
邻接表
邻接矩阵容量太大,太浪费空间
邻接表:顺序+链式存储,并且表示方式不唯一,而邻接矩阵是唯一的。
typedef struct VNode{
VertexType data;
ArcNode *first;
}VNode, AdjList[MaxVertexNum];
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
typedef struct ArcNode{
int adjvex;
struct ArcNode *next;
}ArcNode;
边结点的数量是2
∣
E
∣
|E|
∣E∣,整体空间复杂度为O(
∣
V
∣
+
2
∣
E
∣
|V|+2|E|
∣V∣+2∣E∣)
度:顶点后面的边结点数
有向图:邻接表记录的是结点往外射的边,整体空间复杂度O(
∣
V
∣
+
∣
E
∣
|V|+|E|
∣V∣+∣E∣)
出度:结点后面的边结点数
入度:全部遍历寻找某结点的入度
邻接表适合存储稀疏图
十字链表存储有向图
弧结点:
顶号结点: