线性表
顺序存储
只列出创建和插入与删除的操作
在操作完成,对线性表的元素进行移动的时候,用地址来实现的话,更加便捷,明了
动态分配的顺序存储结构
通过分析代码,我们发现,要注意什么:
- 要分清你的下标
- Insert 函数是可以用来没有元素的时候,增加元素的
- Init(或者Create )函数一般只用来分配空间等的初始化
//动态分配空间的顺序存储结构的线性表
#include<stdio.h>
#include<stdlib.h>
#define Linitesize 100
#define Laddsize 10
#define OK 1
#define error 0
typedef int Status;
typedef int Elemtype;
typedef struct{
Elemtype * elem;
int length;
int listsize;
}SqList;
void Show(SqList L)
{
int i;
for(i=0;i<L.length ;i++)
printf("%d ",L.elem[i]);
printf("\n");
return ;
}
Status Create(SqList &L)
{
L.elem = (Elemtype *)malloc(Linitesize*sizeof(Elemtype));
if(!(L.elem ))
return error;
L.length = 0;
L.listsize = Linitesize;
return OK;
}
//在第i个元素之前插入 ,从1开始计数,就是下标为i
Status Insert(SqList &L,int i,Elemtype e)
{
int j;
if(i<1||i>L.length+1 )
return error;
if(L.length>=L.listsize)
{
L.elem =(Elemtype *)realloc(L.elem ,(L.listsize + Laddsize)*sizeof(Elemtype));
if(!(L.elem ))
return error;
L.listsize = L.listsize + Laddsize;
}
for(j=L.length-1 ;j>=i-1;j--)
L.elem[j+1] = L.elem[j];
L.elem[i-1] = e;
L.length ++;
return OK;
}
//i为你想要删除的第几个元素
Status Delete(SqList &L,int i,Elemtype &e)
{
int j;
if(i<1||i>L.length )
return error;
e = L.elem[i-1];
for(j=i-1;j<L.length-1;j++)
L.elem[j] = L.elem[j+1];
L.length --;
return OK;
}
int main()
{
int i,j;
Elemtype e;
SqList L;
Create(L);
for(i=1;i<=5;i++)
Insert(L,i,i*i);
printf("输出具体数据:\n");
Show(L);
printf("请输入你想要删除第几个元素:\n");
scanf("%d",&j);
Delete(L,j,e);
printf("删除的数据是:%d \n",e);
Show(L);
return 0;
}
线性表的合并(非递减)
考点
- 两个有序递增的顺序表的合并
关键点,可以学到什么,就是分别用pa,pb,pc,来记录首地址,一句话,就是用辅助变量来方便操作
void Merge(Sqlist la,Sqlist lb,Sqlist &lc)
//目标,将原本有序递增的la,pb顺序表整合到lc ,lc认为有序递增的
{
pa = la.elem;
pb = la.elem;
lc.listsize = lc.length = la.length + lb.length;
//长度
pc =lc.elem = (ElemType *)malloc(lc.listsize*(sizeof(ElemType)));
if(!lc.elem)
exit OVERFLOW;
pa_last = pa + la.length-1;
pb_last = pb + lb.length-1;
while(pa<=pa_last&&pb<=pb_last)
{
if(*pa<*pb) *pc++ = *pa++;
else *pc++ = *pb++;
}
while(pa<=pa_last) *pc++ = *pa++;
while(pb<=pb_last) *pc++ = *pb++;
}
顺序表优点与缺点
:
- 优点:可以随便进行数据的插入与删除
- 优点:占据较少的空间
- 缺点:需要连续的一串地址
- 缺点:在插入与删除时,要移动大量的元素
链式存储
创建,插入,删除
插入结点:寻找第i-1个结点的时候,循环要加上对于p 不为空的一个判断,循环完,要判断,循环是否正常终止,插入的时候先插后面,再接前面
对于链表的结点的删除,也是同理,对于寻找第i - 1 的结点的时候要注意
//动态分配空间的顺序存储结构的线性表
#include<stdio.h>
#include<stdlib.h>
#define Linitesize 100
#define Laddsize 10
#define OK 1
#define error 0
typedef int Status;
typedef int Elemtype;
typedef struct LNode{
struct LNode * next;
Elemtype data;
}LNode,*LinkList;
int Length(LinkList L)
{
int sum=0;
while(L->next !=NULL)
{
sum++;
L=L->next ;
}
return sum;
}
//尾插法
Status Create(LinkList &L,Elemtype e)
{
LinkList p = L;//开始p 指向头结点
while(p->next !=NULL )//找到最后一个结点
p=p->next ;
LinkList temp = (LNode *)malloc(sizeof(Elemtype));
if(!temp) return error;
temp->data = e;
//由于p 指向最后一个结点,那么p->next 进行赋值,实际上会改变原来的数据
temp->next = p->next ;
p->next = temp;
return OK;
}
Status Show(LinkList L)
{
LinkList p = L->next ;
while(p !=NULL)
{
printf("%d ",p->data );
p = p->next ;
}
printf("\n");
return OK;
}
//在第i 个元素之前插入 ,确保不超过范围
Status Insert(LinkList &L,int i,Elemtype e)
{
if(i<1||i>Length(L)+1)
return error;
LinkList p = L;//指向头结点
//找到第i-1个结点
int j ;
for(j=1;j<i;j++)
p = p->next ;
LinkList temp = (LNode * )malloc(sizeof(LNode));
if(!temp) return error;
temp->data = e;
temp->next = p->next ;
p->next = temp;
return OK;
}
Status Delete(LinkList &L,int i,Elemtype &e)
{
//删除第i 个元素,并返回其值
if(i<1||i>Length(L))
return error;
//找到第i-1个结点
int j;
LinkList temp = L;
for(j=1;j<i;j++)
temp = temp->next ;
e = temp->next->data;
temp->next = temp->next->next;
return OK;
}
int main()
{
Elemtype e;
LinkList L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;
for(int i = 1;i<= 5;i++)
Create(L,i);
Show(L);
Insert(L,2,10);
Show(L);
Delete(L,3,e);
Show(L);
printf("%d \n",e);
return 0;
}
优点以及缺点
- 优点:插入与删除不用移动大量的元素
- 优点:不需要连续的地址
- 优点:采用动态链表不用固定最大长度
- 缺点:占用较大的内存
- 缺点:不可以随机访问某个元素
求并集,交集。差集
一元多项式
栈与队列
栈
定义:只能在表尾进行插入与删除的线性表(先进后出)
- 表尾是栈顶,表头是栈底
顺序栈
顺序栈的精髓就是通过S.top == S.base 来判断栈是否为空
用S.top-S.base >=stacksize 来判断栈是否满
对于进栈操作,是*S.top = e;赋值完之后,S.top ++ ,而对于出栈操作,是 S.top -- ,再e = *S.top
#include<stdio.h>
#include<stdlib.h>
#define initsize 100
#define addsize 50
#define OK 1
#define error 0
typedef int Status;
typedef int Elemtype;
typedef struct{
Elemtype *base,*top;//base 指向第一个元素,top 指向新的一个元素
int stacksize;
}SqStack;
Status Init(SqStack &S)
{
S.base =(Elemtype *)malloc(initsize*sizeof(Elemtype ));
if(!S.base )
return error;
S.top = S.base ;//说明此时栈为空
S.stacksize = initsize;
return OK;
}
Status Push(SqStack &S,Elemtype e)
{
if(S.top -S.base >=S.stacksize )//栈满
{
S.base =(Elemtype *)realloc(S.base ,(initsize + addsize)*sizeof(Elemtype));
if(!S.base)
return error;
}
*(S.top ) = e;
S.top ++;
return OK;
}
Status Pop(SqStack &S,Elemtype &e)
{
if(S.base == S.top )
return error;//栈为空
S.top --;
e = *(S.top );
return OK;
}
int main()
{
int n,i;
Elemtype e;
SqStack S;
Init(S);
printf("请输入你想要入栈的元素的个数:\n");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
printf("请输入你要入栈的元素:");
scanf("%d",&e);
Push(S,e);
}
printf("出栈 \n");
for(i=1;i<=n;i++)
{
Pop(S,e);
printf("%d ",e);
}
return 0;
}
链栈
简而言之,就是链表的插入与删除均在头结点那里操作
- 相比之下,链栈的操作更为简单,
- 在初始化的时候给传递过来的头指针分配一个头结点
- 当S->next ==NULL 的时候,就说明栈为空
- 进行插入操作的时候,先分配空间LinkStack temp ,来存储数据,temp->next =S->next; S->next=S
- 删除操作的时候,temp = S-> next,e = temp ->data;S->next = temp ->next;
不过应该注意的是,链栈是在队头进行操作,区别于顺序栈的队尾,不过,可以这么记忆,你在哪边进行插入的就在哪边进行删除就可以
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define Error 0
typedef int Status;
typedef int Elemtype;
typedef struct Node {
Elemtype data;
struct Node * next;
}Node,*ListStack;
Status InitStack(ListStack &S)
{
S =(ListStack )malloc(sizeof(Node));
if(!S) return Error;
S->next = NULL ;
return OK;
}
Status Pop(ListStack &S,Elemtype &e)
{
if(S->next ==NULL)//栈为空
return Error;
ListStack p = S->next ;
e = p->data ;
S->next = p->next ;
free(p);
return OK;
}
Status Push(ListStack &S,Elemtype e)
{
ListStack p =(ListStack )malloc(sizeof(Node));
if(!p) return Error;
p->data = e;
p->next = S->next ;
S->next = p;
return OK;
}
int main()
{
ListStack S;
Elemtype e;
int i;
InitStack(S);
printf("请输入想要进栈的6个数据:\n");
for(i=1;i<=6;i++)
{
scanf("%d",&e);
Push(S,e);
}
printf("依次出栈:\n");
for(i=1;i<=6;i++)
{
Pop(S,e);
printf("%d ",e);
}
return 0;
}
栈的应用(数制的转化)
栈的应用(行编辑)
栈的应用(表达式求值)
队列
出队列是DeQueue,进队列是EnQueue
链队列
- 链队列:判断为空的条件:Q.rear==Q.front 指向头结点
- 这也要求了,在删除操作的时候,要判断删除之后是否为空,要是为空就让两个指针相等
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define Error 0
typedef int Status ;
typedef int Elemtype;
typedef struct QNode{
Elemtype data ;
struct QNode * next;
}QNode,*QueuePtr;
typedef struct{
QueuePtr front;
QueuePtr rear;
}LinkQueue;
Status InitQueue(LinkQueue &Q)
{
QNode * p=(QNode *)malloc(sizeof(QNode));//头结点
if(!p) return Error;
p->next =NULL;
Q.front =Q.rear =p;
return OK;
}
Status Push(LinkQueue &Q,Elemtype e)
{
QNode * p=(QNode *)malloc(sizeof(QNode));
if(!p) return Error;
p->data =e;
p->next = Q.rear ->next;
Q.rear ->next = p;
Q.rear = p ;
return OK;
}
Status Pop(LinkQueue &Q,Elemtype &e)
{
if(Q.front ==Q.rear )//队列为空
return Error;
QNode * p ;
p = Q.front ->next;
e = p->data;
Q.front ->next = p->next ;
if(Q.rear == p)//此处十分重要,不然的话,后面释放空间的话,会出错
Q.rear = Q.front ;
free(p);
return OK;
}
int main()
{
LinkQueue Q;
Elemtype e;
printf("创建带有头结点的链队列\n");
InitQueue(Q);
printf("请输入5个你想要依次入队列的数字\n");
for(int i=1;i<=5;i++)
{
scanf("%d",&e);
Push(Q,e);
}
printf("依次出队列\n");
for(int i=1;i<=5;i++)
{
Pop(Q,e);
printf("%d ",e);
}
return 0;
}
顺序队列
注意循环队列的判断队列为满的方法是 队尾+1 再取模,看是不是等于 队头
循环队列的插入与删除,分别是 rear + 1 取模,以及 front +1 取模
小结
串
串的基本操作
StrAssign 是串的赋值,StrCompare 是串的比较 ,StrLength是串的求长度,Concat是串的联接,SubString 是求子串,这5个操作是不能由其他的操作结合,属于基本的操作
树
- 树的结点的度:拥有的子树的个数
- 非终端结点是除了叶子结点的结点
- 树的度为树内的各结点的度的最大值
二叉树
满二叉树
完全二叉树
二叉树的存储结构
顺序存储
- 二叉树的利用下标进行数组中的对应,某个结点i 的父节点为[ i/2] ,如果存在左孩子和右孩子,则分别为 2i 与 2i + 1
- 但是对于非二叉树,利用数组来顺序存储,没有统一的下标的标准,而且可能会浪费许多空间
链式存储
二叉链表(两个指针域)与三叉链表(三个指针域)
二叉树的遍历
先序遍历
虽然是非递归,但是真正蕴含的思想与递归的相同,借助一个栈来对已经访问的元素进行记录,判断当前的指针p 是否为空,如果不为空,则输出结点的值,然后一路向左遍历,当到达最左边时,就出栈,得到父节点,然后对父节点进行相同的操作,当左边遍历完成,就进入右结点,进行相同的操作,当p 和 栈同时为空时遍历完成
中序遍历
后序遍历
和先序遍历的非递归算法类似,都是借助一个栈,不过,不同的是,中序是从最左边的结点开始访问,当遇到指针p 为空,说明已经到达当前最左的的结点的左孩子结点,那么就可以将栈的元素出栈,输出该结点的值,然后p 就等于该结点的右孩子结点,然后又重新找最左边的元素
后序遍历
和前序和中序的遍历相比,后序遍历的难度更大,要增加一个tag 来标记该结点是否被遍历过:首先还是先找到最左边的结点,然后判断该结点的右孩子是否存在,如果不存在,则输出该结点的数据内容,并标记tag 为1,然后让p=null(这样可以让遍历回溯到栈的元素),否则要进入又孩子结点进行新的一轮的重新的遍历:
层序遍历
Status leverTraverse(BiTree T){
BiTree L[100] ;
int front = 0;
int rear = 0;
if(T&&front==rear) //如果根节点非空,那么输出结点后存入队列
{
printf("%c ",T->data);
L[rear++] = T ;
}
while(front!=rear){
BiTree node = L[front++]; //每次出队列
BiTree lc = node->lchild ; //取左孩子
BiTree rc = node->rchild ; //取右孩子
if(lc ) //如果左孩子非空,输出数据后存入队列
{
printf("%c ",lc->data);
L[rear++] = lc;
}
if(rc) //如果右孩子非空,输出数据后存入队列
{
printf("%c ",rc->data );
L[rear++] = rc;
}
}
}
所谓的层序遍历,就是借助一个队列进行相对应的遍历的操作:开始的时候,当根节点非空,输出数据之后存入队列,然后就用一个while 循环,每次从队列中取出一个结点,遍历该结点的非空的左右孩子,并存入队列中,重复该循环,直至队列被遍历完
二叉树的创建
- 二叉链树的代码的注意事项:
- 在进行遍历的时候,应该判断的是传入的指针是否为空,指针不为空才继续往下面进行遍历,否则的话就要返回
- 在进行创建的时候,如果要进行字符的输入,那么就直接输入一整个序列(包含终止字符)
- 创建的过程中,如果字符不为终止字符,则进行分配空间,存储数据,接着遍历,,若为终止字符,则应该让该指针为NULL
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define OK 1
#define error 0
typedef char Elemtype;
typedef int Status;
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
Status Create(BiTree &T)
{
char temp;
scanf("%c",&temp);
if(temp !='!')
{
T = (BiTNode *)malloc(sizeof(BiTNode));
if(!T)
return error;
T->data = temp ;
Create(T->lchild );
Create(T->rchild );
}
else
T = NULL; //很重要
return OK;
}
Status preOrder(BiTree T)
{
if(T)//不为空才进行输出
{
printf("%c ",T->data );
preOrder(T->lchild );
preOrder(T->rchild );
}
return OK;
}
int main()
{
BiTree T;
printf("请按照先序排列进行建树(! 表示结束)\n");
Create(T);
preOrder(T);
return 0;
}
注意
相关题型
给出二叉树的树状图,求遍历的顺序
给出中序+ 先序 或者中序+后序,求树状图
无论是给出先序还是后序,由于分别是根左右,左右根,根结点都是在一边的,先序就是从左到右依次获取每一个根,后序就是从右到左,依次获取一个根,然后一一对照中序遍历的序列,得出哪些没有安排位置的结点是该根节点的左孩子还是右孩子即可
当然,最好的方法就是,写完之后,检验一遍
线索二叉树
如果一个结点有左孩子,那么LTag = 0,lchild 就指向该结点的左孩子,否则LTag = 1 ,lchild 指向该结点的前驱,RTag 同理
树,森林与二叉树的转换
普通的树变成二叉树,就是用孩子兄弟表示法,孩子往左边走,兄弟右边走,然后森林变成二叉树,就是把森林里面的全部的树先变成二叉树,然后让另一棵二叉树为一棵二叉树的又子树,依次来构成
树与森林的遍历
赫夫曼树
学会这个过程,考试可能会考
统计二叉树中叶子结点的个数
开始误以为要将计数的步骤放在if(T) 的外部,但是这犯了一个问题,导致不能合理的进行递归的停止,当你放在里面的时候,就算到达的叶子结点,然后将叶子结点的左右孩子(虽然为空)进行继续递归,但是有着if(T) 进行判断,程序也能很好的完成停止的步骤
求二叉树的深度
类似于递推,有点汉诺塔的味道
图
注意,无向图的连通分量在物理结构上可以容易看出,但是有向图的强连通分量不能一下子就看出,要判断是否符合一个强连通图的一个定义
- (1)以顶点v 为头的弧的数目称为v 的入度,以v 为尾的弧的数目称为v 的出度
- (2)连通图就是任意的两个顶点,都存在路径相通,连通分量就算一个无向图中极大连通子图的数目
- (3)在有向图中,如果对于任意一对顶点都有路径,那么称为强连通图,在有向图中,极大的强连通子图的个数被称为强连通分量
图的存储结构
数组表示
要记住:数组表示的结构的专有的名称:ArcCell 来定义邻接矩阵结点类型,对于邻接矩阵的结点存储的值用VRType来表示,AdjMatrix 来定义整个邻接矩阵 ,arcs 用来表示 邻接矩阵的名字,vertexType 表示 顶点向量,然后vexs 就是顶点向量的名字,然后又有vexnum 和arcnum 分别记录顶点的个数以及边的个数
- 对于图的注意事项:
- 要记录好图的顶点的个数,边的个数,即vexnum 以及arcnum
- 定义一个一维数组来记录数据的数据向量,一个二维的数组来记录数据的邻接矩阵
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define error 0
#define infinity 1000000
#define Max 100
typedef int Status;
typedef int vertexType;//这里设置的是结点的数据的类型
typedef int adjMatrix[Max][Max];//这里设置邻接矩阵
typedef struct {
int vexnum;//顶点的个数
int arcnum;//边的个数
vertexType vexs[Max];//顶点向量
adjMatrix arcs ;//邻接矩阵
}MGraph;
Status Create(MGraph &G)
{
int i,j,start,end,weight;
printf("请输入你的有向图的顶点的个数以及边的个数:\n");
scanf("%d%d",&G.vexnum ,&G.arcnum );
for(i=0;i<G.vexnum ;i++)
for(j=0;j<G.vexnum ;j++)
G.arcs[i][j] = infinity;
printf("请输入你的个个顶点的数据\n");
for(i=0;i<G.vexnum ;i++)
scanf("%d",&G.vexs[i]);
printf("请输入每条边之间的权重(起点->终点)+权重\n");
for(i=1;i<=G.arcnum ;i++)
{
scanf("%d%d%d",&start,&end,&weight);
G.arcs[start][end] = weight;
}
return OK;
}
Status Show(MGraph G)
{
int i,j;
printf("个个结点的数据分别为\n");
for(i=0;i<G.vexnum ;i++)
printf("%d ",G.vexs[i]);
printf("\n");
printf("邻接矩阵:\n");
for(i=0;i<G.vexnum ;i++)
{
for(j=0;j<G.vexnum ;j++)
printf("%d ",G.arcs[i][j]);
printf("\n");
}
return OK;
}
int main()
{
MGraph G;
Create(G);
Show(G);
return 0;
}
缺点
1.对顶点的位置的修改麻烦,涉及顶点向量与邻接矩阵的同时的修改,2.浪费空间,开辟的空间按照最大的需求来开3.对邻接矩阵的检测麻烦,每一个都要检查
邻接表
有向图
无向图
记住表结点,ArcNode存储边,以及一个ArcNode 指针,然后头结点这里,就VexType 类型 来定义data,还有 一个ArcNode 的一个指针,最后在结构体外部定义一个AdjList 的顶点向量,然后有vexnum 和arcnum 来记录个数,最后是ALGraph
对于邻接表,找到某一个顶点的邻接点,就直接遍历邻接表即可,但是判断某两个顶点是否有临界边就要遍历,不及邻接矩阵直接判断
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define error 0
#define max 100
typedef int VertexType;
typedef int Status;
//表结点
typedef struct ArcNode{
int adjvex;//记录边的另一个顶点
int weight;
struct ArcNode *nextarc;
}ArcNode;
//头结点
typedef struct VNode{
VertexType data;
ArcNode * firstarc;
}VNode,AdjList[max];
typedef struct{
int arcnum;//边的个数
int vexnum;//顶点的个数
AdjList vertices;//头结点向量
}ALGraph;
//创造有向图
Status Create(ALGraph &G)
{
int i,j;
int start,end,weight;
printf("请输入有向图的顶点的个数以及边的个数:\n");
scanf("%d%d",&G.vexnum ,&G.arcnum );
printf("请输入每个数据结点的数据:\n");
for(i=0;i<G.vexnum ;i++)
{
scanf("%d",&G.vertices[i].data );
G.vertices[i].firstarc = NULL;
}
printf("请输入每条边的数据(起点 -> 终点 + 权重)\n");
for(i=0;i<G.arcnum ;i++)
{
scanf("%d%d%d",&start,&end,&weight);
ArcNode *temp = (ArcNode *)malloc(sizeof(ArcNode));//分配表结点的空间
temp->adjvex = end;
temp->weight = weight;
temp->nextarc = G.vertices[start].firstarc ;//衔接在头结点旁边
G.vertices[start].firstarc = temp;
}
return OK;
}
Status Show(ALGraph G)
{
int i;
for(i=0;i<G.vexnum ;i++)
{
printf("%d :",G.vertices[i].data );
ArcNode *p = G.vertices[i].firstarc ;
while(p)
{
printf("%d 权重(%d)",p->adjvex ,p->weight );
p=p->nextarc ;
}
printf("\n");
}
return OK;
}
int main()
{
ALGraph G;
Create(G);
Show(G);
return 0;
}
逆邻接表
逆邻接表就是由尾指向头(接受),而邻接表是由头指向尾(付出)
十字链表
tailvex 和 headvex 分别为一条边的尾和头,hlink 指向与弧头相同的下一条弧,tlink 指向弧尾相同的下一条弧
顶点结点的firstin 与 firstout 分别指向以该顶点为弧头和弧尾的弧
一定要分清楚头和尾,最好按照上面定义的顺序 tailvex ,headvex,hlink,tlink 这样的话,容易看
容易求入度和出度
邻接多重表(用于无向图与无向网)
邻接多重表与十字链表的区别:顶点结点不一样,邻接多重表只有一个data 和一个firstedge ,而十字链表有data ,firstin,firstou ,对于表结点,形式上,十字链表有tailvex,headvex,hlink,tlink和一个info ,但是邻接多重表多了一个mark 来记录有没有被访问过,但是实际上hlink ,tlink 分别指向以headvex 为头的结点,以tailvex 为尾的结点,但是ilink 和jlink 是分别指向下一个以 i,j相邻的边,并不考虑头和尾(十字链表用来表示有向图,多重表来表示无向图)
邻接多重表与邻接表(无向图和有向图都可以)的区别:邻接表的表头只有一个data 和 firstarc,并且表结点只有记录弧头的一个数据和一个nextarc 指针
图的遍历
深度优先搜索
深度优先的算法,采用数组的存储结构,就是n^2 的时间复杂度,邻接表的话就是n+e 的复杂度,在总的函数中没有区别,就是要遍历整个顶点,对于DFS 函数,区别就在于,由于要判断一个结点的邻接顶点,对于数组表示法,要遍历全部结点才可以找到,但是对于邻接表表示法的话,就不用,直接遍历该结点的邻接表即可,总的遍历才为边数e
对于DFS 的具体的实现,使用数组法来判断两个顶点之间是否有路径比较块,也就是G.arc[i][j] !=0
广度优先搜索
与树的层序遍历相似,都是借助一个队列:外部是一个大循环,用来遍历全部的结点,当该结点没有被访问时,就对该结点进行访问,然后加入队列中,然后对队列里面的元素进行访问,如果队列不为空,则队头出队列,对队头的邻接的且没有被访问的顶点进行访问,访问完成就加入队列
时间复杂度与深度优先相同
可以用来计算连通分支的个数
图的连通分支的问题
对于图的连通分支问题,只需记录在对每个结点的大循环中,if 语句进行几次即可
连通图的最小代价生成树
普里姆算法
对于普里姆算法,为什么时间的复杂度是n^2? 以顶点为基础,每次在剩余的顶点中找与已经找到的顶点的最小的代价,并加入该顶点
克鲁斯卡尔算法
克鲁斯卡尔算法,就是先将所有的顶点都加进去,然后从边的权值从小到大开始筛选,当新增减的边的两端的顶点位于不同的连通分量就加入该条边,整体的时间复杂度就是 eloge
最短路径
迪杰斯特拉算法
弗洛伊德算法
有向无环图的应用
拓扑排序
关键路径
就是说先从前往后,事件Vj 的最早的发生的时间,其实是到达该点的最晚的时间,从源点开始算起,算到最后一个汇点,然后以汇点为开始,往前面算,每件事件最晚的发生时间,这时取得是最小值,当Ve(i)=Vl(i) 时,说明该事件是关键事件
有向图得可达问题
算法得精髓:判断i到j 是否可达,就是判断i 等不等于j ,若等于,直接成立,否则,判断是否存在 i到达 k,然后递归判断k 到j 是否可达,将问题缩小化
图的中心顶点问题
查找
顺序表的查找
有序表的查找
折半查找法
注意,最多的查找次数不多于[ logn]+1,可以看作二叉树的深度
索引顺序表的查找
动态查找表
二叉排序树
类似于折半查找
二叉树的生成
二叉排序树的删除
就是在左子树找一个最大的,或者在右子树找一个最小的,顶替即可
二叉排序树的查找
平衡二叉树
个人觉得是为了增加查找的速度,最好的打算是logn
平衡二叉树的调整
LR 型,就是c 先和b进行左旋转,然后c 再和a 进行右旋转
RL 型,就是c 先和b进行右旋转,然后c 再和a 进行左旋转
平衡二叉树的插入
B-树
B- 树的插入与删除
记得m 阶B- 树的每一个结点都至少[m/2]棵子树,对于插入与删除的话,没有什么特别的算法,就是要保证是B- 二叉树的结构,也就是对于Ki-1<Ki 的,且Ai-1指向的结点的最大值小于Ki,Ai指向的结点的最小值大于Ki
叶子结点都是空
B+ 树
注意,B+ 树的值都存储在叶子结点,而B- 树的叶子结点不存储信息;B- 树的数据的特点,但是B+ 树的非终端结点的数据只是子树的最大值或者最小值
散列(哈希表)
哈希函数的构造方法
解决冲突
如何理解:查找成功的时候的计算?先按照哈希函数来计算,若找不到,就按照开放地址法来查找,将每一个数字的查找次数加起来即可
如何理解:查找不成功?按照给定的已经排好的哈希表,进行m次的不成功的查找,当按照解决冲突的方法继续查找到一个为空的值后就可以暂停,然后继续找下一个,最后全部加起来,除以总数m
对于链地址法的查找成功的平均查找长度更好算:第一列的个数乘1+第二列的个数乘2++++即可
哈希表的查找
排序
插入排序算法
直接插入排序
//直接插入算法的升序排列
void InsertSort(SqList &L)
{
int i,j;
for(j=2;j<=L.length;j++)
{
//降序排序的话,就将<改成>
if(L.elem[j].key<L.elem[j-1].key)
{
L.elem[0] = L.elem[j];
//及时更新,将j-1的数据存到 j 处
L.elem[j] = L.elem[j-1];
for(i=j-2;L.elem[0].key < L.elem[i];i--)
L.elem[i+1] = L.elem[i];
//当跳出循环的时候,说明L.elem[0].key 比L.elem[i].key 大
//那么 L.elem[0].key 应该插在L.elem[i].key 后面
L.elem[i+1] = L.elem[0];
}
}
}
其他插入排序
折半插入排序
void BinsertSort(SqList &L)
{
// 外循环,从第二个元素开始,逐个将元素插入到已排序序列中
int i,j;
for(i=2;i<=L.length;i++)
{
// 将当前元素保存到临时变量中
L.elem[0] = L.elem[i];
// 初始化已排序序列的左、右边界
int low = 1,high = i-1;
// 使用二分查找算法,找到当前元素的插入位置
while(low<=high)
{
// 计算中间元素的下标
int m = (low + high)/2;
// 如果当前元素小于中间元素,则将右边界向左移动
if(L.elem[0].key < L.elem[m].key) high = m-1;
// 否则,将左边界向右移动
else low = m+1;
}
// 将已排序序列中大于当前元素的元素向后移动一位
for(j=i-1;j>= high+1;j--)
L.elem[j+1] = L.elem[j];
// 将当前元素插入到已排序序列中
L.elem[high + 1] = L.elem[0];
}
}
如何理解?在找到i 的插入的位置的时候,结束的条件一定是high = low -1 ,而由于这样的计算肯定使得 L.r[high].key <=L.r[0].key 的,所以要插入high +1 之中,先将原本的high + 1到 i- 1 的元素向后移动一位,最后将元素插入high + 1
2-路插入排序
表插入排序
希尔排序
注意:当序列基本有序的时候,希尔排序十分高效
交换排序
冒泡排序
快速排序
就是利用了一个类似于树的遍历,得到枢纽的位置之后,再将左边和右边部分调用相应的函数(递归)
枢纽的选择最好是中间的位置
选择排序
简单选择排序
树型选择排序
每次从底部开始每两个进行选择,由于是正则的二叉树,所以可以比较,依次向上传递,每次比较完成的元素用无穷来代替
堆排序
如何进行建堆操作:先按照顺序建立完全二叉树,然后从第一个非终端结点开始逐步调整为大根堆还是小根堆,就是调整每一个都符合就可以
堆排序要时刻更新列表(按照完全二叉树的顺序),每一次建堆成功,都要将队列的最前面与最后面进行交换,然后再进行重新的建堆操作
归并排序
基数排序
数组与广义表
对于稀疏矩阵的存储结构,一般有三元组顺序存储以及链表存储
三元组顺序存储
转置算法
算法一:
- 总体的思想就是:从列进行遍历,内循环用三元组的非0的个数tu 来控制即可,每次遇到匹配列的元素就进行转置,存入新的三元组T 中
算法二:
矩阵加法
主要的理念就是线性表的并集运算
链式存储
广义表
注意:表头不一定是广义表,但是表尾一定是广义表
如何判定自己画的广义表是否错误?
原子都用原子结点来存储
,就是说你的元素应该都是从头指针引出- 其中表姐点中,除了第一个tag = 1,其余的hp 、 tp 分别为头指针与尾指针
- 对于存储结构广义表的转换是一个难点
广义表的存储结构的画法的转换
常用存储结构