【数据结构】精简知识总结

绪论

1.数据结构的研究内容:数据的逻辑结构,数据的存储结构,数据操作三方面

2.算法与程序的区别
a.程序不一定满足有穷性
b.程序中的指令必须是机器可执行的,算法中的指令则无此限制
c.算法代表了对问题的解,程序则是算法在计算机上的特定的实现
d.数据结构+算法=程序

3.算法概念及评价
五大特性:有穷性(有限步骤之内正常结束能形成无穷循环)
确定性(算法中的每一个步骤都有明确的含义,无二义性得以实现)
,可行性(一个算法是能行的,即算法中描述的操作都是以可以执行有限次来实现)
输入性(有多个或0个输入,这些输入取自于某个特定的对象的集合)
,输出性;(至少有一个或多个输出)
算法评价:正确性,可读性,健壮性,高效性,低存储性;
语句频度/时间频度:语句的执行次数
时间频度T(n)中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。
时间复杂度:用于评估执行程序所消耗的时间,可以估算出程序对处理器的使用程度,时间复杂度不是用来计算程序具体消耗时间的
空间复杂度:用于评估执行程序所占用的内存空间,是对一个算法在运行过程中临时占用存储空间大小的量度,反应的是一个趋势 用S(n)。该存储空间一般包括三个方面:指令常数变量所占用的存储空间,输入数据所占用的存储空间,辅助空间

4.数据结构的相关概念
数据结构是指相互之间存在一种或多种特定关系的数据元素集合。数据结构包括数据的逻辑结构和数据的存储结构。数据结构是一个二元组(D,S)D是数据元素的有限集,S是D上关系的有限集
数据对象是性质相同的数据元素的集合
数据元素是组成数据的基本单位
数据项是数据不可分割的最小单位
数据逻辑结构的俩大类型(线性结构,非线性结构),抽象数据类型(是指一个数字模型以及定义在该模型上的一组操作)
数据的四种逻辑结构(集合,线性结构,树状结构,图状结构)
数据的四种存储结构(顺序,链式,索引,散列)
不管是顺序存储结构还是链式存储结构,都要存储数据元素本身和数据元素之间的关系 顺序存储结构,链式存储结构都可存各种逻辑结构
逻辑结构是数据元素之间逻辑关系的描述
存储结构是物理结构,是逻辑结构在计算机中存储映像,是逻辑结构在计算机中的实现,它包括数据元素的表示和数据元素间关系的表示
数据操作:对数据要进行的运算
元素之间的关系在计算机中又俩种不同的表示方式:顺序表示和非顺序表示
逻辑结构:
线性结构:线性表,栈,队列,字符串,数组,广义表
非线性结构:树,图
顺序存储结构:用数据元素在存储器中的相对位置来表示数据元素之间的逻辑关系
链式存储结构:在每一个数据元素中增加一个存放另一个元素地址的指针,用该指针来表示数据元素之间的逻辑结构

5.数据逻辑结构,存储结构的区别和联系
区别:数据的逻辑结构是一个数学模型,数据的存储结构是数据的逻辑结构在计算机内部的存储方式。
联系:一种逻辑结构可以用多种存储结构来存储,一种存储结构可以用来存储多种逻辑结构。

6.抽象数据类型(ADT)是指一个数学模型以及定义在该模型上的一组操作
抽象数据类型的定义仅是一组逻辑特性描述,与其在计算机内的表示和实现无关。因此不论ADT的内部结构如何变化,只要其数学特性不变,都不影响其外部使用。

ADT的形式化定义是三元组:
ADT=(D,S,P) D是数据对象 S是D上的关系集,P是对D的基本操作集

7.算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。

线性表

1.线性表是由n个类型相同的数据元素的有限序列
数据元素之间的关系是一对一的关系,即每个数据元素最大有一个直接前驱和一个直接后继

2.线性表的特点
同一性:线性表由同类数据元素组成
有穷性:线性表由有限个数据元素组成,表长度就是表中数据元素的个数
有序性:线性表中相邻数据元素之间存在着序偶关系

3.线性表的俩种存储结构及其优缺点
线性表的存储结构是指在内存中用地址连续的一块存储空间对线性表中各元素按其逻辑顺序依次存放,用这种存储形式存储的线性表称为顺序表。
线性表的链式存储结构是用一组任意的存储单元存储线性表的各个数据元素。为了表示线性表中元素的前后关系,每个元素除了需要存储自身的信息外还保存直接前驱元素货直接后继元素的存储位置。所以链式存储结构的线性表示之间的逻辑关系是通过结点的指针域来表示的。

顺序存储有三个优点:方法简单,用数组容易实现;不用表示结点之间的逻辑关系而增加额外开销;具有按元素序号随机访问的特点

两个缺点:进行插入,删除时,平均移动表中一半的元素,效率低;需预先分配足够大的存储空间。空间过大,会导致空间闲置,预先分配过小,又会造成溢出。

链表的优缺点与顺序表相反。

4.与线性表两种存储结构有关的知识有:线性表中逻辑上相邻的两个元素其存放位置不一定相邻,线性表的链式存储表示不一定优于顺序存储表示;链式存储方式以指针表示结点间的逻辑关系;一个需经常作插入和删除运算的线性表应采用链式存储结构;线性表元素个数稳定很少进行插入和删除操作应采用顺序存储结构;若线性表最常用的操作的存取第i个元素及其前驱元素的值,则采用顺序存储方式最节省运算时间;

5.顺序存储结构时间复杂度分析
设线性表L中的第i个元素之前插入结点的概率为P,设各个位置插入是等概率,则Pi=1/(n+1),而插入时移动结点的次数为n-i+1。 平均移动次数E=n/2,插入的时间复杂度O(n);
Pi=1/n,删除时移动结点的次数为n-i。总的平均移动次数:E=(n-1)/2,时间复杂度O(n)
顺序表的定位删除:Pi=1/n 平均比较次数(n+1)/2 删除时平均移动次数(n-1)/2。总的平均时间复杂度o(n)

6.在单链表中设置头结点的作用是方便运算的实现.使空链表和非空链表处理一致。
LNode p
p=(LNode
)malloc(sizeof(LNode));
p->data = 1;
p->next =NULL;

头插法和尾插法时间复杂度都为O(n)

单链表按序号查找第i个元素,时间复杂度O(n),按值查找O(n)
插入O(n) ,删除O(n) 链表合并的时间复杂度为O(m+n)

循环链表 判断是否是空链表 head ->next = head

1.栈是只允许在一端进行插入或删除的线性表,栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
2.栈的操作特性先进先出
3.栈的数学性质:n个不同元素进栈,出栈元素不同排列的个数为(1/(n+1))C2n~n。
4.栈的基本操作
InitStack(&S):初始化一个空栈。
StackEmpty(S):判断一个栈是否为空
Push(&S,x):进栈,若栈未满,则将X加入使之成为新栈顶。
Pop(&S,&x):出栈,若栈非空,则弹出栈顶元素,并用x返回
GetTop(S,&x):读栈顶元素,若栈非空,则用x返回栈顶元素
DestoryStack(&s):销毁栈,并释放栈S占用的存储空间

5.顺序栈的实现:

#define Maxsize 50//定义栈中元素的最大个数
typedef struct{
    ElemType data[Maxsize];
    int top;
}SqStack;

栈顶指针:S.top,初始时设置S.top=-1,栈顶元素:S.data[S.top]
进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素
出栈操作:栈不为空时,先取栈顶元素,再将栈顶指针减1
栈空条件:S.top==-1,栈满条件:S.top=Maxsize-1,栈长:S.top+1
受顺序栈的入栈操作受上界的约束,当对栈的最大使用空间估计不足时,有可能发生栈上溢
**初始化 **

void InitStack(SqStack *S){
    S.top=-1;
}

栈判空

bool StackEmpty(SqStack S)
{
    if(S.top==-1){
        return true;
    }
    return false;
}

进栈

bool Push(SqStack *S,EleType x)
{
    if(S.top==Maxsize-1){
        return false;
    }
    S.data[++S.top]=x;
    return true;
}

出栈

bool Pop(SqStack *S,ElemType *x)
{
    if(S.top==-1){
        return false;
    }
    else{
       x=S.data[S.top--];
       return true;
    }
}

读栈顶元素

bool GetTop(SqStack S,ElemType x){
    if(S.top==-1)
    {
        return false;
    }
    else{
        x=S.data[S.top];
        return true;
    }
}

6.共享栈
利用栈底位置相对不变的特性,可让俩个顺序栈共享一个一维数组空间,将俩个栈的栈底分别设置在共享空间的中间延伸。
俩个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,top1=Maxsize时1号栈为空,仅当俩个栈顶指针相邻(top1-top0=1)时,判断为栈满。当0号进栈时,top0先加1再赋值,1号栈进栈时top1先减一再赋值,出栈刚好相反
共享栈是为了更有效的利用存储空间,俩个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存储数据的时间复杂度均为O(1),所以对存储效率没什么影响。

7.栈的链式存储
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况,通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。

typedef struct Linknode{
    ElemType data;
    struct Linknode *next;
    
}*ListStack;

采用链式存储,便于结点的插入和删除,链栈的操作和链表类似,入栈和出栈的操作都在链表的表头进行。

8.栈和队列具有相同的逻辑结构。
9.栈是限制存储点的线性结构。
10.和顺序栈相比,链栈有一个比较明显的优势,即,通常不会出现栈满的情况。
11.链表不带头结点且所有操作均在表头进行,对于双向循环列表,不管是表头指针还是表尾指针,都可以很方便的找到表头结点,方便在表头做插入或删除操作。而单项循环链表通过尾指针可以很方便的找到表头结点,但通过头指针找为节点则需要遍历一次链表,此时找到尾结点需要花费O(n)。
12.向一个栈顶指针为top的链栈(不带头节点),中插入一个x结点,则执行(x->next=top;top=x)。
13. 链栈(不带头结点)执行pop操作,并将出栈的元素存在x当中,应该执行(x=top->data,top=top->next)。
14. 采取共享栈的好处是节省内存空间,降低发生上溢的可能性。
15. 函数调用时,系统要用栈保存必要的信息。

队列

1.队列,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除,向队
列中插入元素称为入队或进队;删除元素称为出队或离队。操作特性:先进先出(FIFO)。
2.队列的常见操作
InitQueue(*Q):初始化
QueueEmpty(Q):判断列空
EnQueue(*Q,x):入队
DeQueue(*Q,*x):出队
GetHead(Q,*X):读队头元素

3.队列的顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设俩个指针,对头front指向对头元素,队尾指针rear指向队尾元素的下一个位置。
顺序存储类型

#define MaxSize 50
typedef struct{
    ElemType data[MaxSize];
    int front,rear;
}SqQueue;

初始状态(队空条件):Q.frontQ.rear0
进队操作:队不满时,先送值到队尾指针,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。

不能用Q.rear=MaxSize作为队满的条件。会出现”假溢出“。

4.循环队链
将顺序队列臆造为一个环状的空间,把存储队列元素的表从逻辑上视为一个环,称为循环队列,当队首指针Q.front=MaxSize-1后,再前进一个位置就自动到0,这可用除法取余运算来实现。
初始时:Q.front=Q.rear=0
队首指针进1:Q.front=(Q.front+1)%MaxSize
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize
出队和入队时指针都按顺时针方向进1
队空的条件Q.front==Q.rear

为了区分队空还是队满的情况,有三种处理方式:
a.牺牲一个单元来区分队空和队满,入队时少用一个队列单元,约定以”队头指针在队尾指针的下一位置作为队满的标志“
队满的条件:(Q.rear+1)%MaxSize==Q.front

队为空的条件仍是Q.rear==Q.front

队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize

b.类型中增设白哦是元素个数的数据成员。这样队空的条件为Q.size== 0
队满的条件为Q.size== MaxSize。这两种情况都有Q.rear== Q.front。
c.类型中增设tag数据成员,以区分队满还是队空。tag等于0时,若因删导致Q.front== Q.rear,则为队空,tag等于1时,若因插入导致Q.front== Q.reard,则为队满。

初始化

void InitQueue(SqQueue *Q){
    Q.rear=Q.front=0;
}

判队空

bool isEmpty(SqQueue Q){
    if(Q.rear==Q.front)
    {
        return true;
    }
     return true;
}

入队

bool EnQueue(SqQueue *Q,ElemType x)
{
    if((Q.rear+1)%Maxsize==Q.front){
        return false;
    }
    else{
        Q.data[Q.rear]=x;
        Q.rear=(Q.rear+1)%MaxSize;
        return true;
    }
}

出队

bool DeQueue(SqQueue *Q,ElemType *x)
{
    if(Q.rear==Q.front){
        return false;
    }
    else{
        x=Q.data[Q.front];
        Q.front=(Q.front+1)%MaxSize;
        return true;
    }
}

6.队列的链式存储
队列的链式表示称为链队列,它实际上时一个同时带有队头指针和队尾指针的单链表,头指针指向头结点,尾指针指向队尾结点,即单链表的最后一个结点
队列的链式存储类型

typedef struct LinkNode{
    ElemType data;
    struct LinkNode *next;
}LinkNode;
typedef struct{
    LinkNode *front,*rear;
}LinkQueue;

当Q.front== NULL且Q.rear==NULL时,链表队列为空。
出队时,首先判断队是否为空,若不为空则取出对头元素,将其从链表中摘除,并让Q.front指向下一个结点,若该节点是最后一个结点则置Q.front和Q,rear都为NULL。入队时,建立一个新结点,将新结点插入链表的尾部,并改让Q.rear指向这个新插入的结点(若原队列为空队,则令Q.front也指向该节点)
设置为带头结点 这样删除和插入操作就统一了。
用单链表表示的链式队链特别适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的情况。另外,假如程序要使用多个队列,与多个栈的情况最好使用链式队列,这样就不会出现存储分配不合理和溢出的问题。

链式队列的基本操作
初始化

void InitQueue(LinkQueue *Q){
    Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));//建立头结点
    Q.front->next=NULL;//初始为空
}

判断空

bool IsEmpty(LinkQueue Q){
    if(Q.front==Q.rear)
    {
        return true;
    }
    return true;
}

入队

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;
}

出队

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;
}

7.双端队列
双端队列指的是允许俩端都可以进行入队和出队操作的队列,其逻辑结构仍是线性结构,将队列的俩端分别称为前端和后端,两端都可以入队和出队。
在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列。
输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输出受限的双端队列。
若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变成俩个栈底相邻接的栈。
8.最适合用作链队的链表是带队首指针和队尾指针的非循环单链表或者双向循环链表。
9.最不适合用作链式队列的链表是只带队首指针的非循环双链表
10.在用单链表实现队列时,对头设在链表的链头位置
11.用链式存储方式的队列进行删除操作时需要头尾指针可能都需要修改。

12.栈和队列的应用
栈在括号匹配中的应用
算法的主要思想:初始设置一个空栈,顺序读入序号;若不是右括号,则或者使置于栈顶的最急迫的期待得以消解,或者是不合法的情况(括号序列不匹配,退出程序);若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性降了一级,算法结束时,栈为空,否则括号序列不匹配。(遇到左括号就入栈,遇到右括号就消耗一个左括号)。
栈在求值表达式中的应用

逆波兰表达式 = 后缀表达式
波兰表达式 = 前缀表达式

中缀表达式:运算符在两个操作数中间
后缀表达式 :运算符在两个操作数后面
前缀表达式:运算符在两个操作数前面

中缀表达式转后缀表达式的手算方法:

  1. 确定中缀表达式中各个运算符的运算顺序
  2. 选择下一个运算符,按照【左操作数 右操作数 运算符】的方式组合成一个新的操作数
  3. 如果还有运算符没被处理,就继续2操作
    注:运算顺序不唯一,因此对应的后缀表达式也不唯一
    左优先原则:只要左边的运算符能先计算,就优先计算左边的。
    引入左优先原则可保证

后缀表达式的手算方法:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。
注意:两个操作数的左右顺序
特点:最后出现的操作数先被运算

用栈实现后缀表达式的计算:

  1. 从左往右扫描下一个元素,直到处理完所有元素
  2. 若扫描到操作数则压入栈,并回到1,否则执行3
  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1

栈在递归中的应用
递归是一种重要对的程序设计方法。若在一个函数,过程或数据结构的定义中又应用了它自身,则这个函数,过程或数据结构称为是递归定义的,简称递归。

它通常把一个大型的复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。但在通常情况下效率都不是很高。(精髓在于能否将原始问题转换为属性相同但规模较小的问题
必须注意递归模型不能是循环定义的,其必须满足俩个条件:递归表达式(递归体);边界条件(递归出口)
在递归调用的过程中,系统为每一层的返回点,局部变量,传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出。而其效率不高的原因是递归调用过程中包含很多重复的计算

队的应用
在层序遍历中可用队。
队列在计算机中的应用:第一方面是解决主机与外部设备之间速度不匹配的问题(例:缓冲区),第二个方面是解决由多用户引起的资源竞争问题。(例:CPU资源竞争问题)

1.字符串简称串,计算机上非数值处理的对象基本都是字符串数据(例:信息检索系统,文本编辑程序,问答系统,自然语言翻译系统)。

2.串是由零个或多个字符组成的有限序列。

3.串中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串。某个字符在串中的序号称为该字符串中的位置。子串在主串中的位置以子串中的第一个字符在主串中的位置来表示,当俩个串的长度相等且每个对应位置的字符都相等时,称这俩个子串是相等的。

4.由一个或多个空格组成的串称为空格串,空格串不是空串,其长度为串中空格字符的个数。

5.串的存储结构
a.定长顺序存储表示
类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值得字符序列。在串得定长存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组。

#define MAXLEN 255
typedf struct{
    char ch[MAXLEN];
    int length;
}SString;

串的实际长度只能小于等于MAXLEN,超过预定义长度的串值会被舍去,称为截断。串长有俩种表示方法:一是如上所定义的描述那样,用一个额外的变量len来存放串的长度;二是在串值后面加入一个不计入串长的结束标记字符"\0",此时的串长为隐含值。
在一些串的操作中,若串值序列的长度超过上界,约定用截断法处理,要克服这种弊端,只能不限定串长的最大长度,即动态分配的方式。
b.堆分配存储
表示仍然一组地址连续的存储单元存放船只的字符序列,但它们的存储空间是在程序执行过程中动态分配到的。

typedef struct{
    char *ch;
    int length;
}HString;

在C语言中,存在一个称之为堆的自由存储区,并用malloc()和free()函数来完成动态存储管理。利用malloc()为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基地址,这个串由ch指针来表示;若分配失败则返回NULL。已分配的空间可用free()释放掉。
c.块链存储表示
类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个节点称为块,整个链表称为块链结构。

6.串的基本操作
StrAssign(*T,chars):赋值操作。把串T赋值给chars。
StrCopy(*T,S):复制操作。又串S复制得到T
StrEmpty(S):判空操作
StrCompare(S,T):比较操作,若S>T,则返回值>0;若S==T,返回false;若S<T,则返回值<0
StrLength(S):求串长,返回串S的元素个数
StrString(*Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符起始长度为len的子串。
Concat(*T,S1,S2):串连接。用T返回由S1和S2联接而成的新串。
Index(S,T):定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0
ClearString(*S):清空操作。将S清为空串,
DeStroyString(*S):销毁串。将串S销毁。

7.串的模式匹配

此块知识点可直接参考:
https://blog.csdn.net/qq_45813532/article/details/125930852

a.简单的模式匹配
子串常称模式串,对字串的定位通常称作串的模式匹配(BF算法),它求的是子串在主串中的位置。

#include <stdio.h>
#include <stdlib.h>

#define maxlen  255
typedef struct
{
    char str[maxlen];
    int length;
}bf;

int BF(bf T,bf P)
{
    int j=0,i=0,ret=-1;
    while((j<strlen(P.str))&&(i<strlen(T.str)))
    {
        if(P.str[j]==T.str[i])//字符串相等则继续
        {
            i++;
            j++;//目标串和字符串进行下一个字符的匹配
        }
        else{
            i=i-j+1;
            j=0;
        }
    }
    if(j=strlen(P.str))
    {
        ret=i-j+1;
    }

    return ret;
}

int main()
{
    bf P,T;
    printf("-------BF算法----");
    printf("输入主串:\n");
    scanf("%s",T.str);
    printf("输入子串:\n");
    scanf("%s",P.str);
    printf("%d\n",BF(T,P));
    printf("Hello world!\n");
    return 0;
}

BF算法效率分析:
最坏时间复杂度O(mn)
空间复杂度O(1)

b.KMP(快速模式匹配)
是在BF算法的基础上改进的,BF算法由于每次匹配失败后都是模式后移一位再从头开始比较,而某趟一匹配相等的字符序列是模式的某个前缀,这种频繁的重复相当于比较模式串在不断的进行自我比较,这就是其效率的根源。因此可从模式本身的结构入手,如果一匹配相等的字符对齐的位置,主串i指针无需回溯,并从该位置开始比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。

前缀:指除最后一个元素以外,字符串的所有头部子串;
后缀:指除第一个元素以外,字符串的所有尾部子串;
部分匹配值:字符串中的前缀和后缀的最长相等的前后缀长度。

例:子串abcab的部分匹配值
a 前缀 后缀都为空集 0
ab 前缀:a 后缀:b 0
abc 前缀:a,ab 后缀:c,bc 0
abca 前缀:a,ab,abc 后缀:a,ca,bca 1
abcab 前缀:a,ab,abc,abca 后缀:b,ab,cab,bcab 0

移动位数 =已匹配的字符数-对应的部分匹配值
Move = (j-1)-PM[j-1]
使用部分匹配值时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将PM表右移一位,这样哪个元素匹配失败,直接看他自己的部分匹配值即可。

我们注意到:
第一个元素右移以后空缺的用-1来填充,因为若是第一个元素匹配失败,则需将子串向右移动一位,而不需要计算字串移动的位数。
最后一个元素在右移动一位过程中移除,因为原来的子串中最后一个元素的部分匹配值时其下一个元素使用的,但显然没有下一个元素,故可以舍去。
上式可改写为:Move = (j-1)-next[j]
相当于将子串的比较指针j回退到:j=next[j]+1
有时为了公式更加简洁,计算简单,将next数组整体+1
最终得到子串指针的变化公式为j=next[j]

next[j]的含义是:在子串的第j个字符与主串发生失配是,则跳到子串的next[j]位置重新与主串当前位置进行比较。

1.树是n(n>=0)个结点的有限集,当n=0时,称为空树。在任意一棵非空树中应满足:

  • 有且仅有一个特定的称为根的结点
  • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,T3…Tm,其中每个集合本身又是一棵子树,并且称为根的子树,因此可以看出树的定义是递归的,即在树的定义中又用到了树自身。

2.树是一种递归的数据结构,树作为一种逻辑结构同时也是一种分层结构,具有以下两个特点:

  • 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
  • 树中所有结点可以有零个或多个后继

3.树适合于具有层次结构的数据。树中的某个结点(除根结点以外)最多只上一层的一个结点有直接关系(父结点),根结点没有直接上层结点,因此在n个结点的树中有n-1条边,而树中每个结点与其下一层的零个或多个结点(即其子女结点)有直接关系。

5.树结构通常用来存储逻辑结构为”一对多“的数据。
4.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TvBzB1Hf-1667396496474)(vx_images/116402808221062.png)]

  • 结点K。从根结点A到K的唯一路径上的任意结点,称为结点K的祖先。结点B是结点K的祖先,结点K是结点B的子孙,结点E称为K的双亲结点,结点K为结点E的孩子,根A是整个树中唯一没有双亲的结点。有相同双亲的结点称为兄弟结点
  • 树中一个结点的孩子树称为该结点的度,树中结点的最大度数称为树的
  • 度大于0的结点称为分支结点/非终端结点,度为0的结点称为叶子结点/终端结点。在分支结点中,每个结点的分支数就是该结点的
  • 结点的深度是从根结点开始自顶向下逐层累加的。
  • 结点的高度是从叶节点开始自底向上逐层累加的。
  • 结点的层次从树根开始定义,根结点是一层,根结点的子结点为第二层,依此类推。
  • 树中结点的各子树从左到右是有次序的,不能互换的,称该树为有序树,否则,称为无序树
  • 树中俩个结点之间的路径是由这俩个结点之间经过的结点序列构成的。路径长度是路径上所经过的边的个数。(树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下 的,同一双亲的两孩子结点之间不存在路径)
  • 森林是m棵互不相交的树的集合。把树的根结点删除就成了树,反之给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。

5.树的性质

  • 树中的结点数等于所有结点的度数之和加1(结点数=总度数+1)
  • 度为m的树中第i层上至多有m^i-1次方个结点(i>=1)。
  • 高度为h的m叉树至多有(m^h-1)(m-1)个结点
  • 具有n个结点的m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil logm(n(m1)+1)⌉
  • m叉树:任意结点的度<=m
    证明:具有n个结点的m叉树的最小高度为
    ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil logm(n(m1)+1)⌉
    高度最小的情况下所有节点都有m个孩子
    [m(h-1)-1/(m-1)]<n<[mh-1/(m-1)]
    m^h-1 < n <m^h
    h-1< log ⁡ m ( n ( m − 1 ) + 1 ) \log_m(n(m-1)+1) logm(n(m1)+1)<h
    hmin= ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil logm(n(m1)+1)⌉

总结点数=n0+…nm
总分支数=1n1+2n2+…+n*nm(度为m的结点引出m条分支)
总结点数=总分支数+1

6.二叉树
二叉树是另外一种树形结构,其特点是每个结点至多有两棵子树(即二叉树不存在度大于2的结点),并且有左右之分,其次序不能任意颠倒。与树相似,二叉树也以递归的形式定义。二叉树是n个结点的有限集合。或者为空二叉树,即n=0。或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树有分别是一棵二叉树。
二叉树是有序树,若将其左子树,右子树颠倒,则成为另外一棵二叉树。即使树中结点只有一棵子树,也要区分他是左子树还是右子树。
7.二叉树与度为2的有序树的区别:度为2的树至少有三个结点,而二叉树可以为空;度为2的有序树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子,则这个孩子就无需区分左右次序,而二叉树无论其孩子树是否为2,均需确认其左右次序,即二叉树的结点次序不是相对于另一个结点而言的,而是确认的

8.满二叉树:一颗高度为h,且含有2^h-1个结点的二叉树称为满二叉树,即树中的每一层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点外的每个结点度数均为2。

9.完全二叉树:高度为h,有n个结点的二叉树,当且仅当其每一个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。

  • 叶子结点只可能在层次最大的两层上出现,对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
  • 若有度为1的结点,则只可能有一个,且该节点只有左孩子而无右孩子。
  • 层序编号之后,一旦出现某节点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
  • 若n为奇数,则每个分支结构都有左孩子和右孩子;若n为偶数,则编号最大的分支结点只有左孩子,没有右孩子,其余分支结点左孩子右孩子都有。

10.二叉排序树:左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于跟系欸但的关键字,左右子树又各是一颗二叉排序树。

11.平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。

12.二叉树的性质

  • 非空二叉树上的叶子结点树等于度为2的结点数加1,即n0=n2+1。
  • 非空二叉树上第K层上至多有2^(k-1)个结点。(等比数列)
  • 高度为h的二叉树至多有2^h - 1个结点(等比数列求和)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNOjWxET-1667396496479)(vx_images/24511211221068.png)]
    13.二叉树的顺序存储
    二叉树的顺序存储是指用一组地址连续的存储单元一次自上而下,自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下摆那位i-1的分量中。
    依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,书中系欸但的序号可以唯一地反映结点之间的逻辑关系,这样既能最大地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
    但对于一般二叉树,为了让数组能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,在存储到一维数组的相应分量中。然而在最坏的情况下,一个高度为h且只有h个结点的单支树却需要占据近2^h-1个存储单元。

14.由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树的每一个结点。在二叉树中某节点结构通常包含若干数据域和若干指针域。二叉链表至少包含三个域,数据域,左指针域,右指针域。

15.在含有n个结点的二叉链表中,含有n+1个空链域。

16.二叉树的遍历:指在按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。

  • 先序遍历:根左右
  • 中序遍历:左根右
  • 后序遍历:左右根

三种算法中,递归遍历左右子树的顺序都是固定的,知识访问根结点的顺序不同,不管采用哪种方式,每个结点都访问一次且仅访问一次,故时间复杂度都是O(n)。在递归遍历中,递归工作栈的深度恰好为树的深度,所以在最坏的情况下,二叉树是由n个结点且深度为n的单支树,遍历算法的空间复杂度O(n)。

17.层序遍历的思想:借助一个辅助队列。先将二叉树根结点入队,然后出队,访问出对节点,若它有左子树,则将左子树根结点入队,若它有右子树,则将右子树根结点入队。然后出队,访问出队结点……如此反复,直至队列为空。

  • 由二叉树的先序遍历和中序遍历可以唯一确定一棵二叉树。
  • 由二叉树的后序遍历和中序遍历可以唯一确定一棵二叉树
  • 由二叉树的层序遍历和中序遍历可以唯一确定一棵二叉树。
  • 先序遍历和后序遍历不可以唯一确定一棵二叉树。

19.引入二叉树正是为了加快查找结点前驱和后继的速度。
规定:若无左子树,令lchlid指向其前驱结点;若无右子树,令rchlid指向其后继结点。还需增加来个标志域表示指针域是左孩子(右孩子)还是指向前驱(后继)。
以这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针为线索。加上线索的二叉树称为线索二叉树。
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树。

  • 若非空二叉树的先序序列和后序序列正好相反,二叉树的先序序列是NLR,后序序列是LRN。要使NLR=NRL成立,L或R为空,这样的二叉树每层只有一个结点,二叉树的形态是其高度等于结点个数。
  • 若某非空二叉树的先序序列和后序序列正好相同,则L和R应均为空,所以满足条件的二叉树只有一个根结点。

21.后序遍历二叉树的非递归算法思想:
后序非递归遍历二叉树是先访问左子树,再访问右子树,最后访问根结点。

  • 沿着根的左孩子,依次入栈,知道左孩子为空,此时栈内abc;
  • 读栈顶元素;若其右孩子不为空且未被访问过,将右子树转入执行第一步;否则,栈顶元素出栈并访问

21.树的存储方式有多种,即可采用顺序存储结构,又可采用链式存储结构,但无论采用何种存储方式,都要求能唯一地反映树中各节点之间的逻辑关系。

22.双亲表示法:这种存储方式采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。根结点下标为0,其伪指针可设为-1。该存储结构利用了每个结点(根结点除外),只有唯一双亲的性质,可以很快的找到每个结点的双亲结点,但求孩子结点时需要遍历整个结构。
区别树的顺序存储结构和二叉树的顺序存储结构,在树的顺序存储结构中,数组下标代表结点的编号,下标中所存的内容指示了结点之间的关系。而在二叉树的顺序存储结构中,数组下标既代表了结点的编号,又指示了各结点之间的关系。当然,二叉树属于树,因此二叉树都可以用树的存储结构来存储,但树却不都能用二叉树的存储结构来存储。

23.孩子表示法:孩子表示法是将每个结点的孩子节点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表,叶子结点的孩子链表为空。这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历n个结点中孩子链表指针所指向n个孩子链表。

24.孩子兄弟表示法:孩子兄弟表示法又称为二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法使每个结点包括三个部分内容:结点值,指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。这种存储表示方法比较灵活,其最大的优点使可以方便地实现树转换为二叉树的操作,易查找孩子结点等,但缺点使从当前结点查找其双亲结点比较麻烦。若为每个结点增设一个parent域指向其父节点,则查找父节点也很方便。

25.树,森林,二叉树的相互转换

  • 树转换为二叉树的规则: 每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,“左孩子右兄弟”。由于根结点没有兄弟,所以对应的二叉树没有右子树。

  • 森林转化为二叉树的规则:先将森林中每一棵树转化为二叉树,由于任何一棵和树对应的二叉树的右子树必为空,若把森林中第二棵树根视为第一棵树根的右兄弟,即将第二棵树对应的二叉树当作第一棵二叉树根的右子树,将第三棵树对应的二叉树当作第二棵二叉树根的右子树……依此类推就可以将森林转化为二叉树。

26.树和森林的遍历
a. 树的遍历是指用某种方式访问树中的每个结点,且仅访问一次。
先根遍历。若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历字数是仍遵循先根后子树的规则。其遍历序列域这棵树相对应的二叉树的先序序列相同。

  • 后根遍历。若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则。其遍历序列与这棵树相应的中序序列相同。
  • 树的层序遍历与二叉树的层序遍历思想基本相同,即按层序一次访问各结点。

b.森林的遍历方法

  • 先序遍历森林。若森林为非空,则按照如下规则进行遍历:访问森林中的第一棵树的根结点;先序遍历第一棵树中根结点的子树森林;先序遍历除去第一棵树之后剩余的树构成的森林
  • 中序遍历森林。若森林为非空,则按照如下规则进行遍历:中序遍历森林中第一棵树的根结点的子树森林;访问第一棵树的根节点;中序遍历除去第一棵树之后剩余的树构成的森林。
    森林的先序遍历和中序遍历和二叉树的先序遍历和中序遍历相对应。

27.哈夫曼树
树中的结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树根到任意结点的路径长度与该结点的权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为WPL。
在含有n个带权叶结点的二叉树中,其中带权路径长度小的二叉树称为哈夫曼树,也称最优二叉树。
28.哈夫曼树的构造

  • 将n个结点分别作为n棵仅含一个结点的二叉树,构成森林。
  • 构造一个新结点,从森林中选择俩棵根结点权值最小的树作为新结点的左,右子树,并且将新结点的权值置为左右子树上根权值之和。
  • 从森林中删除刚才选出的俩棵树,同时将得到的树加入森林中。
  • 重复上述步骤二三,直至森林中只剩下一棵树为止。

构造出的哈夫曼树有以下特点:

  • 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
  • 构造过程中共新建了n-1个结点,因此哈夫曼树的结点总数为2n-1。
  • 每次构造都选择了2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。

29.哈夫曼编码
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。可变长度编码比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率底的字符则赋予长编码,从而使得字符的平均编码长度变短,起到压缩数据的效果。
哈夫曼编码是一种被广泛应用且非常有效的数据压缩编码。
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。

左右孩子结点的顺序是任意的,所以构造出的哈夫曼树更可能不同,但WPL必然是相同且最优的。

1.线性表可以是空表,树可以是空树,但是图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。

2.有向图:若E(边集)是有向边的有限集合时,则图G称为有向图。

3.无向图:若E是无向边的有限结合时,则图G称为无向图。

4.简单图满足(不存在重复边&&不存在顶点到自身的边,那么称图G为简单图。与其相对应的为多重图(某俩个顶点之间的边数大于1条,又允许顶点通过边和自身关联)。

5.完全图:任意俩个顶点之间都存在边的无向图。边数为n(n-1)/2。有向完全图:任意俩个顶点之间都存在方向相反的俩条弧。边数为n(n-1)。

6.子图
设有俩个图G=(V,E),G’=(V’,E’),若V’是V的子集,且E’是E的子集,则称图G’是G的子图。
并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图,即E的子集中某些边关联的顶点可能不在这个V的子集中。

7.在无向图中,若从顶点A到B有路径存在,则称A和B是连通的。一个图中若任意的俩个顶点之间都有路径则称图G为连通图。否则为非连通图。无向图中的极大连通子图称为连通分量。假设有n个顶点,如果边数小于n-1则为非连通图。
非连通图边数最多的情况下:由n-1个顶点构成一个完全图,此时再任意加入一条边则变成连通图。

8.在有向图中,如果有一对顶点AB,从A到B之间有路径,从B到A之间有路径,则称这俩个顶点是强连通的。若图中任意俩顶点之间都有这样的性质,则这个图是强连通图。有向图中的极大强连通子图,称为有向图的强连图分量。在有向图中有n个顶点,有向强连通的情况下边最少的情况,至少需要n条边,构成一个环路。

9.连通图的生成树是包含图中全部顶点的极小连通子图。若图中顶点数为n,则他的生成树含有n-1条边。包含图中全部顶点的极小连通子图,只有生成树满足这个条件,对生成树而言,若砍去它的一条边,则会变成非连通图,加上一条边则会形成一个回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。

10.若无向图不是连通图,但图中存储某个子图符合连通图的性质,则称该子图为连通分量。

11.由图中部分顶点和边构成的图为该图的一个子图,但这里的子图指的是图中"最大"的连通子图(也称"极大连通子图")。

12.对连通图进行遍历,过程中所经过的边和顶点的组合可看做是一棵普通树,通常称为生成树

13.连通图中,由于任意两顶点之间可能含有多条通路,遍历连通图的方式有多种,往往一张连通图可能有多种不同的生成树与之对应。

14.连通图中的生成树必须满足以下 2 个条件:

  • 包含连通图中所有的顶点;
  • 任意两顶点之间有且仅有一条通路;

15.连通图的生成树具有这样的特征,即生成树中边的数量 = 顶点数 - 1。

16.非连通图可分解为多个连通分量,而每个连通分量又各自对应多个生成树(至少是 1 棵),因此与整个非连通图相对应的,是由多棵生成树组成的生成森林。

17.区分极大联通子图和极小连通子图:极大连通子图是无向图的连通分量,极大即要求该连通子图包含其所有的边;极小连通子图是既要保持图连通又要使得边数最少的子图。

18.在无向图中,顶点的度是指依附于顶点V的边的条数,记为TD(v)。对于具有n个顶点,e条边的无向图,无向图的全部顶点的度的和是边数的二倍也就是2e,因为每条边和两个顶点相关联。

19.在有向图中,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,记为ID(v),而出度是以顶点v为起点的有向边的数目,记为OD(v)。顶点V的度为入度和出度之和,即TD(v)=ID(v)+OD(v)。对于具有n个顶点,e条边的有向图,有向图的全部顶点的入度之和与出度之和相等,并且等于边数,这是因为每条有向边都有一个起点和终点。

20.边的权和网
在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。

21.稠密图:边数很少的图称为稀疏图,反之称为稠密图。
稀疏和稠密的判断条件是:e<nlogn,其中 e 表示图中边(或弧)的数量,n 表示图中顶点的数量。如果式子成立,则为稀疏图;反之为稠密图。

22.路径:顶点Vp到Vq之间的一条路径是指顶点序列Vp…Vq。
路径上的边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环若一个图有n个顶点,并且有大于n-1条边,则此图一定有环。

23.在路径序列中,顶点不重复出现的路径称为简单路径。出第一个给顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。

24.距离:从顶点u出发到顶点v的最短路径若存在,此路径的长度记为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷。

25.有向树:一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树。

26.在一个具有n个顶点的无向完全图中,所含的边数为n*(n-1)/2,有n个顶点的强连通图最多有n(n-1)条边,最少有n条边。

30.图的邻接矩阵,邻接链表存储结构

  • 如n个顶点e条边的无向图的邻接表的存储中,表头结点的个数是n,边结点的个数是2e;
  • 无向图的邻接矩阵具有对称性;
  • 带权有向图G用邻接矩阵A存储,则顶点i的入度等于A中第i列非∞且非0的元素个数,顶点的出度等于A中第i行非∞且非0的元素个数;
  • 从有向图中删除一条边<i,j>,其邻接矩阵第i行第j列都要改为0;
  • 从无向图中删除一条边(i,j),其邻接矩阵的第i行第j列和第j行第i列都要改为0;
  • 已知一个有向图的邻接矩阵,要想删除所有以第i个顶点为起始点的弧,应该将邻接矩阵的第i行置0;
  • 设有向图G中有n个顶点和e条边,则其对应的邻接表中有e个边结点。

31.邻接矩阵
所谓邻接矩阵,是指用一个一维数组存储图中的顶点信息,用一个二位数组存储图中的边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。

  • 在见到那应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等可以忽略)
  • 在邻接矩阵中元素仅表示相应的边是否存在时,EdgeType可采用值为0或1的枚举类型
  • 无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储
  • 邻接矩阵表示法的空间复杂度为O(n^2),其中n为图的顶点数|V|。

邻接矩阵的特点

  • 无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需存储上或下三角矩阵的元素。
  • 对于无向图,邻接矩阵的第i行非零元素的个数正好的顶点i的度
  • 对于有向图,邻接矩阵的第i行非零元素的个数正好时顶点i的出度,第i列非零元素的个数正好时顶点i的入度。
  • 用邻接矩阵存储图,很容易确定图中任意来顶点之间是否有边相连。但是要确定图中有多少条边,则必须按行,按列对每个元素进行检测,所花费的时间代价很大。
  • 稠密图适合使用邻接矩阵的存储表示
  • 该图G的邻接矩阵为A,An的元素An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
  • 删除边很方便,删除顶点需要移动大量元素。

32.邻接表
当一个图为稀疏图时,使用邻接矩阵显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储的方法,大大减少了这种不必要的浪费。
所谓邻接表,就是指对图G中的每个顶点vi建立一个单链表,第i个单链表中的结点表时依附于顶点vi的边(有向图则时以顶点vi为尾的弧),这个单链表就称为顶点vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在俩种结点:顶点表结点和边表结点。

图的邻接表存储方法有以下特点

  • 若G为无向图,则所需的存储空间为O(|V|+2|E|),(V是顶点数,E是边数);若G为有向图,则所需的存储时间为O(|V|+|E|)。前者的倍数是2是由于无向图中,每条边在邻接表中出现了俩次。
  • 对于稀疏图中,采用邻接表表示将极大地节省存储空间。
  • 在邻接表中,给定一顶点,能很容易找出它的所有邻边,因为只需读取它的邻接表。在邻接矩阵中相同的操作则需扫描一行,花费的时间为O(n)。但是,若要确定给顶俩个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应的结点对应的边表中查找另一结点,效率较低。
  • 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中结点的个数;但求其顶点的入度则需要遍历全部的邻接表。
  • 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各级欸点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

33.十字链表
十字链表是有向图的一种链式存储结构。在十字链表中,对应于有向图的中的每条弧有一个结点,对应的每个顶点也有一个结点。
弧结点中有5个域,尾域(tailvex)和头域(headvex)分别指弧尾和弧头这俩个顶点在图中的位置;链域hlink指弧头相同的下一条弧;链域tlink指向弧尾相同的下一条弧;info域指向该弧的相关信息,这样,弧头相同的弧就在同一个链表上,弧尾相同的弧也在同一个链表上。

顶点结点中有3个域:data 域存放顶点相关的数据信息,如顶点名称;firstin和fristout俩个域分别指向以该顶点尾弧头或弧尾的第一个弧结点。
顶点结点之间是顺序存储的。
在十字链表中,既容易找到Vi为尾的弧,又容易找到Vi为头的弧,因而容易求出结点的出度和入度。图的十字链表表示不唯一的,但一个十字链表表示确定一个图。

时间复杂度O(|v|+|E|)。

34.邻接多重表
邻接多重表是无向图的另一种链式存储结构。
在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求俩个顶点之间是否存在边而对边执行操作时,需要分别在两个顶点的边表中遍历,效率较低。

35.广度优先遍历(BFS)
广度优先遍历类似于二叉树的层序遍历,基本思想是:首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的顶点,然后依次访问所有未被访问过的邻接顶点;在从这些访问过的顶点出发,访问他们所有未被访问过的邻接顶点,直至图中所有顶点都被访问为止。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问过的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。

广度优先搜索遍历图的过程是以v为起始点,由近至远依次访问和v有路径相通且长度为1,2,……的顶点。广度优先搜索是一种分层的查找过程,每向前走一步可能访问的一批顶点,不想深度优先搜索那样有往回退的情况,因此他不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。

BFS算法的性能分析:无论时邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,n个顶点均需入队一次,在最坏的情况下,空间复杂度为O(|v|)。
采用邻接表存储方式时,每个顶点均需搜索一次,故时间复杂度为O(|V|),在搜索任一顶点的邻接点时,每条边至少访问依次,故时间复杂度为O(|E|),算法的总时间复杂度为O(|V|+|E|)。采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为O(|V|),故算法总的时间复杂度为O(|V|^2)。

36.深度遍历
与广度遍历不同,深度优先遍历类似于树的先序遍历。尽可能深的搜索一个图。
基本思想:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w,再访问与w相邻接且未被访问的任一顶点e,重复上述操作。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问为止。

图的邻接矩阵表示时唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同。因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于邻接表的遍历所得到的DFS和BFS是不唯一的。

DFS算法的性能分析
DFS是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为O(|V|)。
遍历图的过程的实质上时对每个顶点查找其邻接点的过程,其消耗的时间取决于所用的存储结构。

以邻接矩阵表示时,查找每个顶点的邻接点所需的时间为O(|V|),故总的时间复杂度为O(|V^2|)。

以邻接表表示时,查找所有顶点的邻接点所需的时间O(|E|),访问顶点所需的时间为O(|V|),此时,总的时间复杂度为O(|V|+|E|)。

37.广度优先生成树和生成森林,深度优先生成树和生成森林
在广度遍历的过程中,我们可以得到一棵遍历树,称为广度优先生成树,一给定的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的,但由于邻接表存储表示不唯一,则广度优先生成树也是不唯一的。
与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的,即对连通图调用DFS操作才会产生深度优先生成树,否则产生的僵尸深度优先生成森林。与BFS相似,基于邻接表的深度优先生成树是不唯一的。

38.图的遍历算法可以用来判断图的连通性。
对于无向图来说,若无向图是连通的,则从任一结点出发,仅需依次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对图中其他连通分量的顶i但,则无法通过这次遍历访问。对于有向图来说,若从初始顶点到图中的每个顶点都有路径,则能够访问到图中所有顶点,否则不能访问到所有顶点。

39.最小生成树
连通图的生成树是包含所有顶点的极小连通子图,对于生成树来说,砍去它的一条边,则会变成非连通图,加上一条边则会形成一条回路。
对于带权无向图,权值之和最小的那个生成树成为最小生成树

最小生成树的性质:

  • 最小生成树不是唯一的,即最小生成树的树形不唯一,当图中,各边权值不相等时,最小生成树是唯一的。若无向连通图的边数比顶点数少1,即G本身是一棵树时,则G的最小生成树就是它本身。
  • 最小生成树的边的权值之和总是唯一的,虽然最小生成树不唯一;但其对应边的权值之和是唯一的,而且是最小的。
  • 最小生成树的边数为定点数-1。

40.Prim算法(普里姆)
基本思想:就是从某个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树中,直到所有顶点都纳入为止。

Prim伪代码实现:

void prim(G,T)
{
    T=空集;//初始化空树;
    U={w}; //添加任一顶点w
    while((V-U)!=空集)
    {
        设(u,v)是使u∈U,v∈(V-U),且权值最小的边
        T=T∪(u,v);//边归树
        U=U∪{v};//顶点归入树
    }
}

Prim算法的时间复杂度为O(|V^2|),不依赖于E,因此它适用于求解边稠密的图的最小生成树。虽然采用其他方法能改进prim算法的时间复杂度,但增加了实现的复杂度。

41.Kruskal算法(克鲁斯卡尔)
基本思想:每次都选择一条权值最小的边,使这两头连通(原本连通的不算),直到所有结点都连通。

克鲁斯卡尔算法的伪代码实现:

void Kruskal(G,T)  
{
    T=V;//初始化树T,仅含顶点
    numS=n;//连通分量数
    while(numS>1)
    {
        从E中取出权值最小的一条边(v,u);
        if(v和u属于T中不同的连通分量)
        {
            T=T∪{(v,u)};//将此边加入生成树中
            numS--;//连通分量数-1
        }
    }
}

通常在kruskal算法中,采用堆来存放边的集合,因此么此选择最小权值的边只需O(ElogE)的时间。此外,由于生成树T中的所有边可视为一个等价类,因此每次添加的新边的过程类似于求解等价类的过程,由此可以采用并查集的数据结构来描述T,从而构造T的时间复杂度为O(eloge),因此,Kruskal算法适合于边稀疏而顶点较多的图。

42.最短路径

BFS算法求单源最短路径是只适用于无权图,或所有边权值都相同的图。

当图是带权图时,把从一个顶点v0到图中任意一顶点vi的一条路径所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的路径称为最短路径。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值