第一章 数据结构绪论
基本概念
- 数据
- 数据元素:是组成数据的、有一定意义的基本单位
- 数据项:一个数据元素可以由若干个数据项组成
- 数据对象:性质相同的数据元素的集合,是数据的子集
- 数据结构:相互之间存在一种或多种特定关系的数据元素的集合
- 逻辑结构
- 集合结构:其中数据元素除了同属于一个集合外,它们之间没有其他关系
- 线性结构:线性结构中数据元素之间为一对一关系
- 树形结构:数据元素之间存在一种一对多的层级关系
- 图形结构:多对多关系
- 物理结构
- 顺序存储结构:把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的
- 链式存储结构:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的
- 逻辑结构
- 数据类型->抽象数据类型
第二章 算法
算法设计的要求
- 正确性
- 可读性:注释,命名规则
- 健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果
算法时间复杂度
- 常数阶:O(1)
- 线性阶:O(n)……
- 对数阶:O(log2n)……
- 平方阶:O(n2)……
常用时间复杂度
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
第三章 线性表
线性表分支
线性表的定义
线性表:零个或多个数据元素的有限序列
线性表元素的个数n (n ≥ 0) 定义为线性表的长度,当 n = 0 时,称为空表。
线性表的抽象数据类型
这些是线性表基本操作
复杂操作由此衍生
/*将所有的在线性表Lb中但不在La中的数据元素插入到La*/
void union( List *La,List Lb) {
int La_len, Lb_len,i;
ElemType e; /*声明与La和Lb相同的数据元素e*/
La_len = ListLength(La);/*求线性表的长度*/
Lb_len = ListLength(Lb);
for (i=1; i<=Lb_len; i++) {
GetElem (Lb, i,e);/*取Lb中第i个数据元素赋给e*/
if ( !LocateElem( La,e,equal))/*La中不存在和e相同数据元素*/
ListInsert ( La, ++La_len, e);/*插入*/
}
}
顺序存储结构
- 数据类型
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct {
ElemType data[MAXSIZE];/*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
-
基本操作
-
获取元素
- 如果获取位置不合理或者数组为空,抛出异常
- 直接获取即可
-
插入
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
- 从最后一个元素向前遍历到第i个位置,分别将它们都向后移动一个位置
- 将要插入元素填入位置i处
- 表长加1
-
删除
- 如果删除位置不合理或者数组为空,抛出异常
- 去除删除元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
- 表长减1
-
-
注意
-
优点
- 无须为表中元素之间的逻辑关系而增加额外的存储空间
- 快速存取随机元素
-
缺点
- 插入和删除要移动大量元素
- 线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的“碎片”
-
链式存储结构
- 数据类型
/*线性表的单链表存储结构*/
typedef struct Node { //结点
ElemType data; //结点数据域
struct Node *next; //结点指针域,指向下一个结点
}Node;
typedef struct Node *LinkList; //LinkList为头指针
-
基本操作
- 获取元素
-
声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始
-
当 j < i 时 (p && j < i) ,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1
-
若到链表末尾 p 为空 ( !p || j > i) , 则说明第 i 个元素不存在
-
否则查找成功,返回结点p的数据
-
插入
- 声明一结点 p 指向链表第一个结点,初始化 j 从 1 开始
- 当 j < i 时 (p && j < i) ,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1
- 若到链表末尾 p 为空 ( !p || j > i) , 则说明第 i 个元素不存在
- 否则查找成功,在系统中生成一个空结点 s
- 将数据元素 e 赋值给 s->data
- 单链表插入标准语句 s->next = p->next; p->next = s;
- 返回成功
-
删除
ListNode* removeElements(ListNode* head, int val) { // 删除头结点 while (head != NULL && head->val == val) { // 注意这里不是if ListNode* tmp = head; head = head->next; delete tmp; } // 删除非头结点 ListNode* cur = head; while (cur != NULL && cur->next!= NULL) { if (cur->next->val == val) { ListNode* tmp = cur->next; cur->next = cur->next->next; delete tmp; } else { cur = cur->next; } } return head; }
- 声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始
- 当 j < i 时 (p->next && j < i) ,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1
- 若到链表末尾 p 为空 ( !(p->next) || j > i) , 则说明第 i 个元素不存在
- 否则查找成功,将欲删除的结点 p->next 赋值给 q
- 单链表的删除标准语句 p->next = q->next
- 将 q 结点中的数据赋值给 e ,作为返回
- 释放 q 结点
- 返回成功
-
- 获取元素
-
注意
-
头结点
- 是为了操作的同一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了
- 头结点不一定是链表必须要素
-
单链表插入和删除 p 和 p->next 问题
- 一开始都是 p 指向头结点,而插入循环条件是 p && j < i,而删除循环条件是 p->next && j < i。因为插入是在p的后面插入,所以要找的是 p;而删除是删除p->next,所以要找的是 p->next
-
单链表的整表创建
- 头插法(CreateListHead)
- 声明一个结点p和计数器变量i
- 初始化一个空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环
- 生成一新结点赋值给p
- 随机生成一数字赋值给p的数据域p->data
- 将p插入到头结点与前一新结点之间
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L,int n) {
LinkList p;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /*先建立一个带头结点的单链表*/
for(i = 0; i < n; i++) {
p = (LinkList)malloc(sizeof(Node));
p->data = rand() % 100 + 1;
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
- 尾插法(CreateListTail)
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L,int n) {
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L; //r为指向尾部的结点
for(i = 0; i < n; i++) {
p = (LinkList)malloc(sizeof(Node));
p->data = rand() % 100 + 1;
r->next = p; //将表尾终端结点的指针指向新结点
r = p; //将当前的新结点定义为表尾终端结点
}
r->next = NULL; //表示当前链表结束
}
单链表的整表删除
- 操作
- 声明一结点p和q
- 将第一个结点赋值给p
- 循环
- 将下一结点赋值给q
- 释放p
- 将q赋值给p
单链表结构与顺序存储结构优缺点
-
时间性能
- 查找
- 顺序:O(1)
- 单链表:O(n)
- 插入和删除
- 顺序:O(n)
- 单链表:O(1)
- 查找
-
空间性能
单链表 > 顺序
静态链表(待补充)
循环链表(待补充)
双向链表
- 数据类型
//线性表的双向链表存储结构
typedef struct DulNode {
ElemType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后驱指针
}DulNode, *DuLinkList;
- 示意图
- 空链表
- 非空循环的带头结点的双向链表
- 注意
-
双向链表是单链表中扩展出来的结构,很多操作是和单链表相同的,比如求长度的ListLength,查找元素的GetElem,获得元素位置的LocateElem等。这些操作都只要涉及一个方向的指针即可,另一个指针并不能提供什么帮助
-
插入和删除需要更改两个指针变量(顺序不能乱)
- 插入
s->prior = p; //把p赋值给s前驱 s->next = p->next; //把2p->next赋值给s的后继 p->next->prior = s; //把s赋值给p->next的前驱 p->next = s; //把s赋值给p的后继
- 删除
p->prior->next = p->next; //把p->next赋值给p->prior的后继 p->next->prior = p->prior; //把p->prior赋值给p->next的前驱 free(p);
-
第四章 栈和队列
栈和队列分支
栈的定义
栈是限定仅在表尾进行插入和删除操作的线性表
栈的抽象数据类型
栈的顺序存储结构
- 数据类型
typedef int SElemType;
typedef struct {
SElemType data[MAXSIZE];
int top; //用于栈顶指针,指向最顶层的元素
}SqStack;
- 基本操作
-
进栈
- 如果栈满,则抛出异常
- 栈顶指针加1
- 将新插入元素赋值给栈顶空间
- 返回成功
-
出栈
- 如果栈空,则抛出异常
- 将要删除的栈顶元素赋值给e
- 栈顶指针减1
-
代码实现
#include<stdio.h> #include<malloc.h> #define DataType int #define MAXSIZE 1024 struct SeqStack { DataType data[MAXSIZE]; int top; }; //栈初始化,成功返回栈对象指针,失败返回空指针NULL SeqStack* initSeqStack() { SeqStack* s=(SeqStack*)malloc(sizeof(SeqStack)); if(!s) { printf("空间不足\n"); return NULL; } else { s->top = -1; return s; } } //判断栈是否为空 bool isEmptySeqStack(SeqStack* s) { if (s->top == -1) return true; else return false; } //入栈,返回-1失败,0成功 int pushSeqStack(SeqStack* s, DataType x) { if(s->top == MAXSIZE-1) { return -1;//栈满不能入栈 } else { s->top++; s->data[s->top] = x; return 0; } } //出栈,返回-1失败,0成功 int popSeqStack(SeqStack* s, DataType* x) { if(isEmptySeqStack(s)) { return -1;//栈空不能出栈 } else { *x = s->data[s->top]; s->top--; return 0; } } //取栈顶元素,返回-1失败,0成功 int topSeqStack(SeqStack* s,DataType* x) { if (isEmptySeqStack(s)) return -1; //栈空 else { *x=s->data[s->top]; return 0; } } //打印栈中元素 int printSeqStack(SeqStack* s) { int i; printf("当前栈中的元素:\n"); for (i = s->top; i >= 0; i--) printf("%4d",s->data[i]); printf("\n"); return 0; } //test int main() { SeqStack* seqStack=initSeqStack(); if(seqStack) { //将4、5、7分别入栈 pushSeqStack(seqStack,4); pushSeqStack(seqStack,5); pushSeqStack(seqStack,7); //打印栈内所有元素 printSeqStack(seqStack); //获取栈顶元素 DataType x=0; int ret=topSeqStack(seqStack,&x); if(0==ret) { printf("top element is %d\n",x); } //将栈顶元素出栈 ret=popSeqStack(seqStack,&x); if(0==ret) { printf("pop top element is %d\n",x); } } return 0; }
-
两栈共享空间结构
- 基本概念
- 一个栈的栈底为数组的始端,即下标为0处,另一个栈为栈的末端,即下标为数组长度n - 1处。两个栈如果增加元素,就是两端点向中间延伸
- 栈1空,即top1等于-1时;栈2空,即top2等于n时
- 栈满即top1 + 1 == top2
- 数据类型
typedef struct {
SElenType data[MAXSIZE];
int top1; //栈1栈顶指针
int top2; //栈2栈顶指针
}SqDoubleStack;
- 基本操作
- 进栈
- 如果栈满,抛出异常
- 若元素进栈1,则先top1 + 1后给数组元素赋值
- 若元素进栈2,则先top2 - 1后给数组赋值
- 返回成功
- 出栈
- 如果栈1空,抛出异常
- 否则栈1栈顶元素出栈
- 如果栈2空,抛出异常
- 否则栈2栈顶元素出栈
- 进栈
栈的链式存储结构
- 数据类型
typedef struct StackNode { //链栈结点
SElemType data;
struct StackNode *next;
}StackNode/*链栈结点*/, *LinkStackPtr/*链栈头指针*/;
typedef struct LinkStack { //链栈
LinkStackPtr top; //链栈头指针
int count; //结点元素个数
}LinkStack;
-
基本操作(和单链表差不多,插入和删除有不同)
- 进栈
/*插入元素e为新的栈顶元素*/ Status Push ( LinkStack *S,SElemType e) { LinkstackPtr s=(LinkStackPtr) malloc(sizeof (StackNode)); s->data=e; s->next=S->top; /*把当前的栈顶元素赋值给新结点的直接后继*/ S->top=s; /*将新的结点s赋值给栈顶指针*/ S->count++; return OK; }
- 出栈
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */ Status Pop ( LinkStack *S,SElemType *e) { LinkStackPtr p; if(stackEmpty(*S)) return ERROR; *e=S->top->data; p=S->top; /*将栈顶结点赋值给p*/ S->top=S->top->next; /*使得栈顶指针下移一位,指向后一结点*/ free (p); return OK; }
-
注意
- 链栈的头指针指向第一个有元素的结点,没有结点
四则运算表达式求值
- 后缀(逆波兰)表达法定义
- 中缀表达式转后缀表达式
- 中缀表达式
- 9 + (3 - 1) * 3 + 10 / 2
- 后缀表达式
- 9 3 1 - 3 * + 10 2 / +
- 规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
- 转化过程
- 初始化一空栈,用来对符号进出栈使用
- 第一个字符是9,输出9,后面是符号“+”,进栈
- 第三个字符是“(”,依然是符号,因其只是左括号,还未配对,故进栈
- 第四个字符是3,输出,总表达式为9 3,接着是“-”,进栈
- 接下来是数字1,输出,总表达式为9 3 1,后面是符号“)”,此时,我们需要去匹配此前的“(”,所以栈顶依次出栈,并输出,直到 “(” 出栈为止。此时左括号上方只有“-”,因此输出“-”。总的输出表达式为9 3 1 -
- 接着是数字3,输出,总的表达式为9 3 1 - 3。紧接着是符号” * “,因为此时的栈顶符号为“+”号,优先级低于“ * ”,因此不输出,“ * ”进栈
- 之后是符号“+”,此时当前栈顶元素“*”比这个“+”的优先级高,因此栈中元素出栈并输出(没有比“+”号更低的优先级,所以全部出栈),总输出表达式为9 3 1 - 3 * +。然后将当前这个符号“+”进栈。也就是说,前6张图的栈底的“+”是指中缀表达式中开头的9后面那个“+”,而图4-9-9左图中的栈底(也是栈顶)的“+”是指“9 + ( 3 - 1 ) × 3 +”中的最后一个“+”
- 紧接着数字10,输出,总表达式变为9 3 1 3 - * + 10。后是符号“÷”,所以“ / ”进栈。
- 最后一个数字2,输出,总的表达式为9 3 1 - 3 * + 10 2
- 因已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为9 3 1 - 3 * + 10 2 / +。
- 中缀表达式
- 后缀表达式转计算结果
- 规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
- 中缀表达式转后缀表达式
队列的定义
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列的抽象数据类型
队列的顺序存储结构
-
顺序存储结构在队头弹出一个元素,其余元素往前移动,调整时间复杂度为O(n),很低效
-
出现front和rear指针,动态指向队列(通过指针的移动指定队列起止下标),调整时间复杂度为O(1),更高效
-
front指针指向队头元素,rear指针指向队尾元素的下一个位置。当front等于rear时,队列为空队列
循环队列
- 基本概念
- 头尾 相接的顺序存储结构称为循环队列
- 队列满的条件是(rear + 1) % QueueSize == front(QueueSize代表队列的最大尺寸,而不是队列长度)
- 计算队列长度公式为(rear - front + QueueSize)% QueueSize
- 数据类型
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */
/*循环队列的顺序存储结构*/
typedef struct {
QElemType data [MAXSIZE];
int front; /*头指针*/
int rear; /*尾指针,若队列不空,指向队列尾元素的下一个位置*/
}SqQueue;
-
基本操作
- 初始化
Status InitQueue ( SqQueue *Q) { Q->front = 0; Q->rear = 0; return OK; }
- 入队
/*若队列未满,则插入元素e为Q新的队尾元素*/ Status EnQueue (SqQueue *Q, QElemType e) { if (Q->front = Q->rear) //队列空的判断 return ERROR; Q->data[Q->rear]=e; /*将元素e赋值给队尾*/ 0->rear= (Q->rear+1) 号MAXSIZE; /*rear指针向后移一位置,若到最后则转到数组头部*/ return OK; }
- 出队
/*若队列不空,则删除中队头元素,用e返回其值*/ Status DeQueue ( SqQueue *Q,QElemType *e) { if (Q->front = Q->rear) //队列空的判断 return ERROR; *e=Q->data[Q->front]; /*将队头元素赋值给e*/ Q->front=( Q->front+1)% MAXSIZE; /*front指针向后移一位置, 若到最后则转到数组头部*/ return OK; }
队列的链式存储结构
- 数据类型
typedef int QElemType;/* QElemType类型根据实际情况而定,这里假设为int */
typedef struct QNode { /*结点结构*/
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct { /*队列的链表结构*/
QueuePtr front, rear; /*队头、队尾指针*/
}LinkQueue;
-
基本操作
- 入队
- 与单链表相同,没有特别多需要注意的地方
- 出队
Status DeQueue(LinkQueue *Q,QElemType *e) { QueuePtr p; if(Q->front==Q->rear) return ERROR; p=Q->front->next;/*将欲删除的队头结点暂存给p*/ *e=p->data; Q->front->next=p->next;/*将原队头结点后继p->next赋值给头结点后继*/ if(Q->rear==p)/*若队头是队尾,则删除后将rear指向头结点*/ Q->rear=Q->front; free (p); return OK; }
- 入队
-
注意
- 队列链式存储结构带不存数据的头结点,和单链表相同
第五章 串
串的定义
串是由零个或多个字符组成的有限序列,又名字符串
串的抽象数据类型
-
解释 这里的StrLength 指的是字符串长度。原文中索引0的位置存放的应该是字符串长度的数值。如果以0为字符串起点的话,应该是0 ≤ pos ≤ StrLength - len
-
Index算法实现(模式匹配算法)
//T为非空串。若主串S中第pos个字符之后存在与T相等的子串,则返回第一个这样的子串在S中的位置,否则返回0
int Index(String s, String T, int pos) {
int n, m, i;
String sub;
if(pos > 0) { //如果pos不符合规范,抛出异常
n = StringLength(S); //得到主串S的长度
m = StringLength(T); //得到子串T的长度
i = pos;
while(i <= n - m + 1) {
SubString(sub, S, i, m); //取主串第i个位置,长度与T相等子串给sub
if(StringCompare(sub, T) != 0) //如果两串不相等
i++;
else
return i;
}
}
return 0; //若无子串与T相等,返回0
}
串的顺序存储结构
- 注意
- 可以在下标为0的地方存储字符串的长度,比放在字符串最后要好一些
- 字符串最后会以’\0’结尾
- 用malloc,free实现动态分配空间
串的链式存储结构
- 注意
- 一个结点对应一个字符,就会存在很大的空间浪费。因此一个结点也可以考虑存放多个字符
- 总的来说,性能不及顺序
朴素的模式匹配算法
- 定义:子串的定位操作通常称做串的模式匹配
- 代码实现:
//T非空,1 ≤ pos ≤ StringLength(s)
int Index(String S, String T, int pos) {
int i = pos; //i用于主串S中当前位置下标,若pos不为1,则从pos位置开始匹配
int j = 1; //j用于子串T中当前位置下标值
while(i <= S[0] && j <= T[0]) { //若i小于S长度且j小于T的长时循环
if(S[i] == T[j]) {
i++;
j++;
}
else {
i = i - j + 2; //i退回到上次匹配首位的下一位
j = 1;
}
}
if(j > T[0])
return i - T[0];
else
return 0;
}
KMP模式匹配算法
-
基本概念
-
前缀:包含首位字符但不包含末位字符的子串
-
后缀:包含末位字符但不包含首位字符的子串
-
next数组定义:当主串与模式串的某一字符不匹配的时候,模式串要回退的位置
-
next[j]:其值 = 第j位字符前面的j - 1位字符组成的子串的前后缀重合字符数 + 1
- 举例
j : 1 2 3 4 5 6 7 8
P: a b a a b c a c
Next[j]: 0 1 1 2 2 3 1 2 //Next[1] 默认为 0
-
-
关键推演
-
代码实现
//next数组
void GetNext(char T[], int next[]) { //length为串ch的长度
next[1] = 0;
int i = 1, j = 0;
while(i < T[0]) {
if(j == 0 || T[i] == T[j]) {
i++;
j++;
next[i] = j;
}
else j = next[j];
}
}
//简便计算:从第j - 1个字符向左看,若有k个连字符与开头的k个连字符相等,则该值为k + 1
//nextval数组
void GetNextVal(char T[], int nextval[]) { //length为串ch的长度
nextval[1] = 0;
int i = 1, j = 0;
while(i < T[0]) {
if(j == 0 || T[i] == T[j]) {
i++;
j++;
if(T[i] != T[j]) {
nextval[i] = j;
}
else {
nextval[i] = nextval[j];
}
}
else j = nextval[j];
}
}
//简便计算:第j个字符与第next[j]个字符想比较,若不等则等于next[j],若相等则等于nextval[next[j]];
//KMP
//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0
int Index_KMP(String S, String T, int pos) {
int i = pos; //从pos位置开始匹配
int j = 1; //用于子串T中当前位置下标值
int next[255];
GetNext(T, next);
while(i <= S[0] && j <= T[0]) { //若 i 小于 S 的长度且 j 小于 T 的长度时,循环继续
if(j == 0 || S[i] == T[j]) { //若两字母相等则继续,与朴素算法增加了 j = 0 的判断
i++;
j++;
}
else {
j = next[j]; //j退回合适的位置,i值不变
}
}
if(j > T[0]) {
return i - T[0];
}
else
return 0;
}
第六章 树
树的定义
- 基本概念
- 结点的度:结点拥有的子树数
- 叶结点/终端结点
- 孩子,兄弟,双亲
树的抽象数据类型
.5
树的存储结构
-
双亲表示法
-
代码实现
#define MAX_TREE_SIZE 100 typedef int TElemType; //树结点的数据类型 typedef struct PTNode { //结点结构 TElemType data; //结点数据 int parent; //双亲位置 }PTNode; typedef struct { //树结构 PTNode nodes[MAX_TREE_SIZE]; //结点数组 int r, n; //根的位置和结点数 }PTree;
-
拓展完善
可以根据是否需求,拓展长子域(firstchild)以及右兄弟域(rightsib),不存在都用-1来表示
-
-
孩子表示法
-
概念
-
根据孩子数的不同,每个结点可以由多个指针域(多重链表表示法)
-
把每个结点的孩子结点排列起来,以单链表作为存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
-
代码实现
#define MAX_TREE_SIZE 100 typedef struct CTNode { //孩子结点 int child; struct CTNode *next; }*ChildPtr; //可以加双亲结点,兄弟结点 typedef struct { //表头 TElemType data; ChildPtr firstchild; //在这可以加双亲指针,兄弟指针 }CTBox; typedef struct { //树结构 CTBox nodes[MAX_TREE_SIZE]; //结点数组 int r, n; //根的位置和结点数 }CTree;
-
-
孩子兄弟表示法
-
代码实现
typedef struct CSNode { ElemType data; struct CSNode *firstchild, *rightsib; }CSNode, *CSTree;
-
二叉树的定义
-
基本概念
-
定义:二叉树是n个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
-
相关概念
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。(从上往下数)
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。(从下往上数)
-
五中形态
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
-
特殊二叉树
- 斜树
-
左斜树(只有左子树)
-
右斜树(只有右子树)
-
满二叉树(所有分支结点都存在左子树和右子树,同时所有叶子结点都在同一层)
-
完全二叉树(不一定满,但是层序遍历时,每个结点的位置值和满二叉树层序遍历每个结点位置值相同)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NtEP3bET-1680705112798)(picture_source/14.jpg)]
- 叶子结点只能出现在最下两层。
- 最下层的叶子一定集中在左部连续位置。
- 倒数二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
- 同样结点数的二叉树,完全二叉树的深度最小。
-
平衡二叉树
- 定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
-
- 斜树
-
-
性质
-
在二叉树的第 i 层上至多有2i-1个结点(i ≥ 1)
-
深度为k的二叉树至多有2k-1个结点(k ≥ 1)
-
对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1
-
具有n个结点的完全二叉树的深度为[log2n] + 1([x]表示不大于x的最大整数)
-
如果对一棵有n个结点的完全二叉树(其深度为[log2n] + 1)的结点按层序编号(从第1层到第[log2n] + 1层,每层从左到右),对一结点i(1≤i≤n)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QdEDF69c-1680705112799)(picture_source/14.jpg)]
- 如果i = 1,则结点 i 是二叉树的根,无双亲; 如果 i > 1,则其双亲是结点[i / 2];
- 如果 2i > n,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子是结点 2i
- 如果 2i + 1 > n.则结点 i 无右孩子;否则其右孩子是结点 2i + 1
-
二叉树的存储结构
二叉树顺序存储结构
-
适用情况
-
完全二叉树
如果不是完全二叉树,就会浪费存储空间
-
二叉链表
-
代码实现
typedef struct BiTNode { TElemType data; //结点数据 struct BiTNode * lchild, rchild; //左右孩子指针 }BiTNode, *BiTree; //再加一个双亲指针域,称之为三叉链表
二叉树建立
void CreateBiTNode(BiTree *T) {
TElemType ch;
scanf("%c", &ch);
if(ch == '#') {
*T = NULL;
}
else {
*T = (BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data = ch; //生成根结点
CreateBiTNode(&(*T)->lchild); //构造左子树
CreateBiTNode(&(*T)->rchild); //构造右子树
}
}
遍历二叉树
-
定义
二叉树的遍历时指从根结点出发,按照某种次序一次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
-
遍历方法
-
前序
void PreOrderTraverse(BiTree T) { if(T == NULL) return; printf("%c", T->data); //显示结点数据,可以更改为其他对结点操作 PreOrderTraverse(T->lchild); //再先序遍历左子树 PreOrderTraverse(T->rchild); //再先序遍历右子树 } //输出结果为:ABCGHCEIF
-
中序
void InOrderTraverse(BiTree T) { if(T == NULL) return; InOrderTraverse(T->lchild); //中序遍历左子树 printf("%c", T->data); //显示结点数据,可以更改为其他对结点操作 InOrderTraverse(T->rchild); //最后中序遍历右子树 } //输出结果为:GDHBAEICF
-
后序
void PostOrderTraverse(BiTree T) { if(T == NULL) return; PostOrderTraverse(T->lchild); //先后序遍历左子树 PostOrderTraverse(T->rchild); //再后序遍历右子树 printf("%c", T->data); //显示结点数据,可以更改为其他对结点操作 } //输出结果为:GHDBIEFCA
-
层序
-
-
遍历性质
- 前序 + 中序 = 唯一确定
- 后序 + 中序 = 唯一确定
线索二叉树
-
定义
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树
-
线索化
-
将有空指针域的rchild指向其前驱,lchild指向其后继,没有前驱和后继统统指向NULL。
-
对二叉树以某种次序遍历使其变成线索二叉树的过程称作是线索化。
-
线索化的过程就是在遍历的过程中修改空指针的过程
-
-
增设ltag和rtag
ltag rtag 0 左孩子 右孩子 1 前驱 后继 -
代码实现
//二叉树的二叉线索存储结构的定义 typedef enum {Link, Thread/*线索*/} PointerTag; //Link == 0 表示指向左右孩子指针,Thread == 1 表示指向前驱或后继的线索 typedef struct BiTreeNode { //二叉线索存储结构 TElemType struct data; struct BiThrNode *lchild, *rchild; //左右孩子指针 PointerTag LTag; PointerTag RTag; //左右标志 }BiThrNode, *BiThrTree; //中序遍历进行中序线索化 BiThrTree pre; //全局变量,始终指向刚刚访问过的结点 void InThreading(BiThrTree p) { if(p) { InThreading(p->lchild); //递归左子树线索化 if(!p->lchild) { //没有左孩子 p->LTag = Thread; //前驱线索 p->lchild = pre; //左孩子指针指向前驱 } if(!pre->rchild) { //前驱没有右孩子 pre->RTag = Thread; //后继线索 pre->rchild = p; //前驱右孩子指针指向后继(当前结点p) } pre = p; //保持pre指向p的前驱 InThreading(p->rchild); //递归右子树线索化 } } //插入头结点 //T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。中序遍历二叉线索链表表示二叉树T bool InOrderTraverse_Thr(BiThrTree T/*头结点指针*/) { BiThrNode* p;//结点指针 p = T->lchild; //p 指向根结点 while(p != T) { //空树或遍历结束时,p == T while(p->LTag == Link) //当LTag == 0 时循环到中序序列第一个结点 p = p->lchild; printf("%c", p->data); //显示结点数据,可以更改为其他对结点操作 while(p->RTag == Thread && p->rchild != T) { p = p->rchild; printf("%c", p->data); } p = p->rchild; //p进至其右子树根 } return true; }
-
适用情况
二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继
树的转换
-
树->二叉树
-
森林->二叉树
-
二叉树->树
-
二叉树->森林
-
树的遍历
- 先根遍历,类似二叉树的前序遍历
- 后根遍历,类似于二叉树的后序遍历
-
森林遍历
- 前序遍历
- 后序遍历
堆
-
定义
特殊的完全二叉树,满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆
-
小根堆/大根堆
根结点最大,为大根堆;根结点最小,为小根堆
-
赫夫曼树(哈夫曼堆)
-
基本概念
-
路径
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径
-
路径长度
路径上的分支数目
-
树的路径长度
从树根到每一结点的路径长度之和
-
赫夫曼树(最优二叉树)
带权路径长度WPL最小的二叉树
-
-
-
代码实现
//最大堆实现 #define _CRT_SECURE_NO_WARNINGS #include <bits/stdc++.h> using namespace std; #define ll long long //堆模板 template<typename T> class CMyHeap { T* Heap; int size; int capacity; public: CMyHeap() { Heap = NULL; size = capacity = 0; }; ~CMyHeap() { clear(); } void addNode(int val); void deleteNode(); void initHeap(T*& arr, int n); void clear(); //特地写入这样一个函数是为了既可以自动结束类,也可以手动结束类 }; template<typename T> void CMyHeap<T>::clear() { if (Heap) delete[]Heap; Heap = NULL; //很重要,如果要多次调用这个,不NULL会导致上面if的判断成立,因空指针释放而报错 size = capacity = 0; } //插入 template<typename T> void CMyHeap<T>::addNode(int val) { //动态扩容 if (size == capacity) { capacity = capacity + ((capacity >> 1) > 1 ? (capacity >> 1) : 1); T* temp = new T[capacity]; if (!temp) { printf("开辟内存失败!\n"); return; } for (int i = 0; i < size; i++) { temp[i] = Heap[i]; } if (Heap) { delete[] Heap; Heap = NULL; } Heap = temp; } //上浮操作 Heap[size++] = val; //后++(size - 1)为数组尾部 int index = size - 1; T tempNum = Heap[index]; while (index > 0) { int parentIndex = (index - 1) >> 1;//公式 if (tempNum/*始终拿这个去判断*/ > Heap[parentIndex]) { Heap[index] = Heap[parentIndex]; index = parentIndex; //立flag } else break; } Heap[index] = tempNum; return; } //删除 template<typename T> void CMyHeap<T>::deleteNode() { if (size == 0) { printf("堆空!\n"); return; } if(size > 1) //可以不写,没必要 Heap[0] = Heap[size - 1]; size--; int index = 0; int tempNum = Heap[index]; while (1) { int left = 2 * index + 1; int right = 2 * index + 2; bool isLeft = true; //初始化左边上浮 if (left > size - 1) break; else { if (right <= size - 1) { if (Heap[left] < Heap[right]) {//左边小于右边,右边上浮 isLeft = false; } } if (isLeft) { if (tempNum < Heap[left]) { Heap[index] = Heap[left]; index = left; } else break; } else { if (tempNum < Heap[right]) { Heap[index] = Heap[right]; index = right; } else break; } } } Heap[index] = tempNum; } //初始化 template<typename T> void CMyHeap<T>::initHeap(T*& arr, int n) { Heap = new T[n]; if (!Heap) { printf("空间分配失败!\n"); return; } size = capacity = n; int i; for (i = 0; i < size; i++) { Heap[i] = arr[i]; } //(size - 2) >> 1为最后一个有子结点的结点 for (i = (size - 2) >> 1; i >= 0; i--) { int index = i; //不能将index替换成i来--,因为index在下面的操作中会变 T tempNum = Heap[index]; while (1) { int l = 2 * index + 1; int r = 2 * index + 2; bool isL = true; if (l > size - 1) break; else { if (r <= size - 1) { if (Heap[l] < Heap[r]) { isL = false; } } if (isL) { if (tempNum < Heap[l]) { Heap[index] = Heap[l]; index = l; } else break; } else { if (tempNum < Heap[r]) { Heap[index] = Heap[r]; index = r; } else break; } } } Heap[index] = tempNum; } } int main() { int arr[] = { 5,7,23,7,3,65,4,22,30,1,0,55,37,64,38 }; int* pArr = arr; CMyHeap<int> cmh; cmh.initHeap(pArr, 15); cmh.addNode(39); cmh.deleteNode(); cmh.clear(); return 0; }
第七章 图
图的定义
-
图:是由定点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合
-
有向图:弧有方向,顶点集和弧集构成的图
-
无向图:弧没方向
-
有向网,无向网:弧和边带权的图
-
完全网:n个定点,e条边。e=n(n - 1)/2条边的无向图
有向完全图……
-
稀疏图:e<nlogn;否则稠密图
-
邻接点:两个顶点之间存在一条边则两个点为邻接点
-
度:一个顶点有多少条关联边(出度 + 入度)
-
出度:从顶点发出的关联边的条数
-
入度:指向顶点的关联边的条数
-
简单路径:序列中顶点不重复出现的路径
-
回路:第一个顶点和最后一个顶点相同的路径
-
路径长度:路径上边的数目
-
连通图:任意两个顶点之间都有路径(不是都有边)
非连通图
-
连通分量:若无向图为非连通图,则各个极大联通子图称为图的连通分量
-
强连通图:任意两个顶点之间都存在一条有向路径,此图为强连通图
强连通分量
-
生成树
图的存储结构
邻接矩阵
-
无向图的邻接矩阵
设G是有n个顶点的无向图,则G的邻接矩阵是一个n*n矩阵
A[i, j] = A[j, i] = 1 (Vi与Vj有关系)
邻接矩阵第i行(或第i列)的元素之和则是顶点Vi的度
-
有向图的邻接矩阵
邻接矩阵第i行的元素之和为顶点Vi的出度
邻接矩阵第j列的元素之和为顶点Vj的入度
-
代码实现
typedef char VertexType; //顶点类型应由用户定义 typedef int EdgeType; //边上的权值类型应由用户定义 #define MAXVEX 100 //最大顶点数,应由用户定义 #define INFINTY 65535 //用65535来代表∞ typedef struct { VertexType vexs[MAXVEX]; //顶点表 EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,边表 int numVertexes, numEdges; //图中当前的顶点数和边数 }MGraph; //建立无向网图的邻接矩阵表示 void CreateMGraph(MGraph *G) { int i, j, k, w; scanf("%d%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数 for (i = 0; i < G->numVertexes; i++) { scanf("%d", &G->vexs[i]); //读入顶点信息,建立顶点表 } for (i = 0; i < G->numVertexes; i++) { for (j = 0; j < G->numVertexes; j++) { G->arc[i][j] = INFINTY; //将arc矩阵无关项开到最大 } } for (k = 0; k < G->numEdges; k++) { scanf("%d%d%d", &i, &j, &w); G->arc[i][j] = w; G->arc[j][i] = G->arc[i][j]; //无向图,矩阵对称 } } //时间复杂度:O(n + n ^ 2 + e)
-
缺陷
存储空间浪费得比较多
邻接表
-
特点:数组与链表相结合
类似于树存储结构里面的孩子表示法
-
无向图
-
有向图(邻接表便于分析出度,逆邻接表便于分析入度)
-
带权有向图
-
代码实现
typedef char vertexType;/*顶点类型应由用户定义*/ typedef int EdgeType;/*边上的权值类型应由用户定义*/ typedef struct EdgeNode { /*边表结点*/ int adjvex; /*邻接点域,存储该顶点对应的下标*/ EdgeType weight; /*用于存储权值,对于非网图可以不需要*/ struct EdgeNode *next;/*链域,指向下一个邻接点*/ }EdgeNode; typedef struct vertexNode {/*顶点表结点*/ vertexType data;/*顶点域,存储顶点信息*/ EdgeNode* firstedge;/*边表头指针*/ }VertexNode, AdjList[MAXVEX]; typedef struct { AdjList adjList; int numvertexes, numEdges; /*图中当前顶点数和边数*/ }GraphAdjList; //建立图的邻接表结构 void CreateALGraph(GraphAdjList* G) { int i,j,k; EdgeNode *e; printf("输入顶点鼓和边数:\n" ) ; scanf ( "%d%d" , &G->numvertexes,&G->numEdges );/*输入顶点数和边数*/ for(i = 0;i < G->numvertexes; i++) {/*读入顶点信息,建立顶点表*/ scanf("%d", &G->adjList[i].data);/*输入顶点信息*/ G->adjList[i].firstedge = NULL;/*将边表置为空表*/ for(k = 0;k < G->numEdges; k++) {/*建立边表*/ printf("输入边(vi, vj)上的顶点序号:n"); scanf("%d, %d", &i, &j);/*输入边(Vi, vj)上的顶点序号*/ e = (EdgeNode *)malloc(sizeof(EdgeNode));/*向内存申请空间, 生成边表结点*/ e->adjvex = j; /*邻接序号为j*/ e->next = G->adjList[i].firstedge;/*将e指针指向当前顶点指向的结点*/ G->adjList[i].firstedge = e;/*将当前顶点的指针指向e*/ e = (EdgeNode *)malloc(sizeof(EdgeNode));/*向内存申请空间, 生成边表结点*/ e->adjvex = i;/*邻接序号为i*/ e->next = G->adjList[j].firstedge;/*将e指针指向当前顶点指向的结点*/ G->adjList[j].firstedge = e;/*将当前顶点的指针指向e*/ } } }
十字链表
-
将邻接表和逆邻接表结合起来
//边表结构体 typedef struct edgeNode { int tailvex; //弧尾顶点(入度),即记录到弧尾顶点的结点的下标 int headvex; //弧头顶点(出度),即记录从弧头结点到某结点,某结点的下标 EdgeNode* headlink; //入边表指针域,指向终点相同的下一条边 EdgeNode* taillink; //出边表指针域,指向起点相同的下一条边 int weight; //权值 }EdgeNode; //顶点表结构体 typedef struct vertexNode { int data; EdgeNode* firstin; //入边表头指针,指向该顶点的入边表中第一个结点 EdgeNode* firstout; //出边表头指针,指向该顶点的出边表中第一个结点 }VertexNode;
邻接多重表(待补充)
边集数组(待补充)
图的遍历
BFS
-
原理
从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。
若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
-
数据结构
-
代码实现
DFS
-
原理
首先将图中每个顶点的访问标志设为 FALSE, 之后从图中某个顶点v0出发,访问此顶点,然后依次从v0的未被访问的邻接点出发深度优先遍历图,直至图中所有和v0有路径相通的顶点都被访问到为止;
若此时图中尚有顶点未被访问,则另选图中一个未曾访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
-
代码实现
最小生成树
-
prim算法
-
重点
按顶点构建生成树
-
代码实现
//邻接矩阵实现prim算法 #include <iostream> #define Max 103 using namespace std; typedef struct AMGraph { //图的结构体 int ves[Max]; //顶点表 int arcs[Max][Max]; //邻接矩阵 int vexnum, arcnum; //顶点数和边数 }; typedef struct Gedge { int adjvex; //最小边的前驱点 int lowcost; //最小边的权值 }; Gedge closedge[Max]; int sum = 0; //最小生成树权值和 void putin(AMGraph& G) //初始化并输入图 { int u, v, w; cin >> G.vexnum >> G.arcnum; for (int i = 1; i <= G.vexnum; i++) //初始化邻接矩阵值 { for (int j = 1; j <= G.vexnum; j++) G.arcs[i][j] = 0xcffffff; } for (int i = 1; i <= G.arcnum; i++) //输入边和其权值 { cin >> u >> v >> w; G.arcs[u][v] = G.arcs[v][u] = w; } } void Prim(AMGraph& G) //普利姆算法 { for (int i = 1; i <= G.vexnum; i++) //起始点默认为1 if (i != 1) closedge[i] = { 1, G.arcs[1][i] }; closedge[1].lowcost = 0; //初始化最小边集合 for (int i = 2; i <= G.vexnum; i++) //遍历n-1次 { int k, min = 0xcffffff; for (int j = 1; j <= G.vexnum; j++) //找到当前的最小边的顶点 { if (closedge[j].lowcost < min && closedge[j].lowcost != 0) { min = closedge[j].lowcost; k = j; } } cout << closedge[k].adjvex << " " << k << endl; sum += closedge[k].lowcost; //把最小边的权值求和 closedge[k].lowcost = 0; //把找到的最小边并入集合 for (int j = 1; j <= G.vexnum; j++) //刷新当前未并入集合的边 { if (G.arcs[k][j] < closedge[j].lowcost) { closedge[j].adjvex = k; closedge[j].lowcost = G.arcs[k][j]; } } } } int main() { AMGraph G; putin(G); Prim(G); cout << sum << endl; //输出要求的最小生成树的权值和 return 0; }
-
特点
适用于稠密图(边多),时间复杂度为O(V2)
-
-
kruskal算法
-
重点
按边构建生成树
-
代码实现
#include <iostream> #include <vector> #include <algorithm> #define Inf 0x7fffffff using namespace std; struct node { int u, v; int w; node(int a, int b, int x) :u(a), v(b), w(x) {} }; vector<node> edge; int vn, an; int pa[1000]; //比较两条边的权值 bool cmp(const node& a, const node& b) { return a.w < b.w; } //并查集分类 int find(int x) { return x == pa[x] ? x : pa[x] = find(pa[x]); } int kruskal() { //sumw 路径总权值 int sumw = 0, cnt = 0; for (int i = 1; i <= vn; ++i) pa[i] = i; for (int i = 0; i < an; ++i) { int x = find(edge[i].u), y = find(edge[i].v); if (x != y) { pa[x] = y; sumw += edge[i].w; ++cnt; } if (cnt == vn - 1) break; } return sumw; } int main() { cin >> vn >> an; for (int i = 0; i < an; ++i) { int a, b, x; cin >> a >> b >> x; edge.push_back(node(a, b, x)); } sort(edge.begin(), edge.end(), cmp); cout << kruskal(); return 0; }
-
特点
适用于稀疏图(很少),时间复杂度为O(ElogE)
-
两点之间的最短路径问题
dijkstra算法 (单源最短路径)
-
理论
- 每次从未标记的节点中选择距离出发点最近的节点,标记,收录到最优路径集合中
- 计算刚加入节点A的邻近节点B的距离(不包含标记的节点),若(节点A的距离+节点A到节点B的边长)<节点B的距离,就更新节点B的距离和前面点
-
要求
- 图为有权图,且权值全为正数。负数也要用到Bellman-Ford算法 若计算任意两点的最短路径则要用floyd算法
-
代码实现
-
这个代码的最终效果是求得源点到goal点的最短路径距离,如果要写出最短路径的话,需要为每一个结点加一个前一结点的值。比如:将做一个结构体
struct Node{ int dis; int prior;//在每一个for(int j)的时候可以赋值p }node[maxn];
-
/*数据区*/
const int inf=0x3f3f3f3f; //代表无穷大。
const int maxn=100;//最大顶点数
int n,m;//n个顶点,m条边。
int start,goal;//起点与目标点。
int graph[maxn][maxn];//带权图
struct {
bool visited;//判断是否确定到源点的最终最短距离。
int dis;//顶点到源点的最短距离。
int prior;//前驱点
}Point[maxn];
/*初始化*/
void init(){
for (int i = 0; i < n; i++){
vis[i] = false;
dis[i] = graph[start][i];//初始化dis数组。
if (dis[i] < inf) {
prior[i] = start;
}
else {
prior[i] = -1;
}
}
vis[start] = true;
dis[start] = 0;
}
/*dijkstra*/
void dijkstra(){
//源点为源点start。
int minn;//记录每趟最短路径中最小的路径值。
int pos;//记录得到的minn所对应的下标。
init();//调用初始化函数。
for (int i = 1; i < n; i++){ //剩下n - 1个结点判断
//将n个顶点依次加入判断。
minn = inf;
for (int j = 0; j < n; j++){
if (!visited[j] && dis[j] < minn) {
minn = dis[j];
pos = j;
}
}
//经过这趟for循环后我们找到的就是我们想要的点,可以确定这点到源点的最终最短距离了。
visited[pos] = true;//我们将此点并入已知集合。
//接下来就是更新dis数组了,也就是当前最短距离,这都是针对还没有并入已知集合的点。
for (int j = 0; j < n; j++) {
if (!visited[j] && dis[j] > dis[pos] + graph[pos][j]) {
dis[j] = dis[pos] + graph[pos][j];
prior[j] = pos;
}
}
}
//退出循环后,所有的点都已并入已知集合中,得到的dis数组也就是最终最短距离了。
cout << dis[goal] << endl;//输出目标点到源点的最短路径长度。
}
#include<bits/stdc++.h>
using namespace std;
int main()
{
while(cin>>n>>m){
memset(graph,inf,sizeof(graph));
int u,v,w;
for (int i = 0; i < m; i++){
cin >> u >> v >> w;
//graph[u][v]=w;//有向图
graph[u][v] = graph[v][u] = w;//无向图
}
cin >> start >> goal;//输入起点与终点。
dijkstra();
}
return 0;
}
floyd算法
#include<bits/stdc++.h> // 万能头
using namespace std;
#define INF 0x3f3f3f3f
const int graphSize = 3;
int floydGraph[graphSize][graphSize];
void init() {
int n;
memset(floydGraph, INF, sizeof(floydGraph));
cout << "请问有多少条边" << endl;
cin >> n;
int u, v, w;
for (int i = 0; i < graphSize; i++) {
floydGraph[i][i] = 0;
}
for (int i = 0; i < n; i++) {
cin >> u >> v >> w;
floydGraph[u][v] = w;
}
}
void Floyd() {
int i, j, k;
for (k = 0; k < graphSize; k++) {
for (i = 0; i < graphSize; i++) {
for (j = 0; j < graphSize; j++) {
if (floydGraph[i][k] + floydGraph[k][j] < floydGraph[i][j]) {
floydGraph[i][j] = floydGraph[i][k] + floydGraph[k][j];
}
}
}
}
for (i = 0; i < graphSize; i++) {
for (j = 0; j < graphSize - 1; j++) {
cout << floydGraph[i][j] << '\t';
}
cout << floydGraph[i][j] << endl;
}
}
int main()
{
init();
Floyd();
system("pause");
return 0;
}
拓扑排序
- 概念
- 有向无环图:一个无环的有向图称作有向无环图
- AOV-网:用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网,简称AOV-网
- 拓扑排序:将入度为0的结点放入排列队列中,并将其出度线段删去,这样依次寻找入度为0的结点,依次放入排列队列中去
关键路径
- 概念
- 关键路径和关键活动:要估算整项工程完成的最短时间, 就是要找一条从源点到 汇点的带权路径长度最长的路径,称为关键路径。关键路径上的活动叫做关键活动,这些活动是影响工程进度的关键, 它们的提前或拖延将使整个工程提前或拖延。
第八章 广义表
数组的定义
-
声明格式:数据类型 变量名称[行数] [列数]
int num[5][8];
-
n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组。
数组抽象数据类型
InitArray (&A,n, bound1, ...boundn) //构造数组A
DestroyArray (&A) //销毁数组A
Value (A, &e,index1,...,indexn) //取数组元素值
Assign (A, &e,index1,...,indexn) //给数组元素赋值
数组的顺序表示和实现
-
前提:
数组可以是多维的,但存储数据元素的内存单元地址是一维的, 因此,在存储数组结构之前,需要解决将多维关系映射到一维关系的问题。
-
顺序存储方式:
①以行序为主序(低下标优先)②以列序为主序(高下标优先)
以行序为主序:设数组开始存储位置LOC( 0, 0 ),存储每个元素需要L个存储单元则数组元素a[i] [j]的存储位置是: LOC( i, j)= LOC(0,0)+(n * i + j ) * L。(n为每一行所含有的元素个数,n * i + j表示在a[i] [j]前面所有元素个数)。
矩阵的压缩存储
-
特殊矩阵的压缩存储
-
压缩存储的定义:若多个数据元素的值都相同,则只分配一个元素值的存储空间,且零元素不占存储空间。
-
能够压缩的矩阵:一些特殊矩阵,如:对称矩阵,对角矩阵,三角矩阵,稀疏矩阵等。
-
三角矩阵压缩存储:
LOC(i, j) = LOC(0,0) + (i * (i + 1) / 2 + j) * L;
-
-
稀疏矩阵:矩阵中非零元素的个数较少(一般小于5%)。
- 存储方式
-
顺序存储
-
三元组
-
行逻辑链接的顺序表
-
带辅助行向量的二元组
-
伪地址表示法
-
-
链接存储
-
带行指针向量的单链表示法
-
行列表示法(十字链表)
-
多练表示法(正交表)
-
-
散列存储
-
- 存储方式
-
压缩存储的原则:存各非零元的值、行列位置和矩阵的行列数。
-
-
稀疏矩阵的顺序存储方法——三元组存储
-
代码实现
//矩阵M为原矩阵,矩阵N为转置矩阵 typedef struct { int i, j; //该非零元的行下标和列下标 elemtype v; //该非零元的值 }Triple; //三元组类型 typedef union { Triple data[MAXSIZE + 1]; //非零元三元组表,data[0]未用 int mu, nu, tu; //矩阵的行数、列数和非零元个数 }TSMatrix; //稀疏矩阵类型 num[col]; //(伪代码)表示矩阵M中第col列中非零元的个数 cpot[col]; //(伪代码)表示矩阵M中第col列中第一个非零元在N.data中的恰当位置 //快速转置 //第一行和第一列同一从1开始计数 Status FastTransmat(TSMatrix M, TSMatrix& N) { N.mu = M.nu; N.nu = M.mu; N.tu = M.tu; if (M.tu != 0) { for (col = 1; col <= M.nu; col++) { num[col] = 0; //初始化num向量 } for (t = 1; t <= M.tu; t++) { num[M.data[t].j]++; //求M中每一列含非零元个数 } cpot[1] = 1; for (col = 2; col <= M.nu; col++) { cpot[col] = cpot[col - 1] + num[col - 1]; } //求M中每一列的第一个非零元在N.data中的序号(位置) for (p = 1; p <= M.tu; p++) { col = M.data[p].j; q = cpot[col]; N.data[q].j = M.data[p].i; //p为M中的行号 N.data[q].i = M.data[p].j; //q为转置后在N中的行号 N.data[q].v = M.data[p].v; cpot[col]++; //该列下次开始遍历的位置++ } } }
-
三元组(i,j,Aij)可以唯一确定矩阵的一个非零元素。
-
三元组顺序表的优点:非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。
-
三元组顺序表的缺点:不能随机存取,若按行号存取某一行中的非零元,则需从头开始进行查找。
- 解决方法:可以使用行逻辑链接的顺序表
-
-
稀疏矩阵的链式存储方法——十字链表
-
优点:它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的各种运算。
-
在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row, col, value) 以外,还要有两个域:
①right:用于链接同一行中的下一个非零元素;
②down:用以链接同一列中的下一个非零元素。
-
广义表的定义
-
也称列表,是n个单元素或子表所组成的有限序列
-
LS为广义表的名称,n为其长度
大写字母表示广义表的名称,小写字母表示原子
LS非空时,第一个元素为LS的表头,其余元素组成的表为表尾
-
广义表的长度为最外层包含元素个数
广义表的深度定义为所含括弧的重数
原子深度为0,空表深度为1
-
广义表可以共享,可以递归
广义表的存储结构
-
表头表尾分析法
-
优点:给列表的运输算带来方便,如求列表的长度和深度,求表头、表尾等
-
缺点:表结点个数多,和列表中的括弧对数不匹配,也多占存储空间
-
李氏口诀!!!(谨记)(图推表的公式)
同层即并列,原子上表直接写,表上表加层括号,空表加空括号,最后加层打括号
-
-
子表分析法
- 优点:表结点个数少,并且和列表中的括弧对数一致
- 缺点:写递归算法不方便
第九章 查找
-
概念
- 静态查找表:只作查找操作的查找表
- 动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素
-
静态查找表
- 顺序查找
- 平均查找长度:(n + 1) / 2
- 折半查找
- 索引查找
- 稠密索引
- 分块索引
- 顺序查找
-
动态查找表
-
二叉排序树(二叉查找树、二叉搜索树)
typedef int dataType; typedef struct node { dataType data; struct node* lchild, * rchild; }BSTNode, *BSTree; void insertBST(BSTree& bst, dataType x) { if (bst == NULL) { bst = new BSTNode; bst->data = x; bst->lchild = NULL; bst->rchild = NULL; } else if (bst->data > x) { insertBST(bst->rchild, x); } else if (bst->data < x) { insertBST(bst->lchild, x); } } BSTNode* searchBST(BSTree& bst, dataType k) { if (bst == NULL) return NULL; else if (bst->data == k) return bst; else if (bst->data > k) searchBST(bst->lchild, k); else searchBST(bst->rchild, k); } bool delBSTNode(BSTree& bst, dataType k) { if (bst == NULL) return false; //如果找到了NULL,说明没有符合删除要求的结点 else if (bst->data == k) { if (bst->lchild == NULL) { //左子树为空,选取右子树最上层的元素(即大于k的最小值) BSTNode* p = bst; bst = p->rchild; delete p; } else { BSTNode* f = bst, * p = bst->lchild; while (p->rchild) { f = p; p = p->rchild; } bst->data = p->data; if (f == bst) bst->lchild = p->lchild; else f->rchild = p->lchild; delete p; } } else if (bst->data > k) { delBSTNode(bst->lchild, k); } else { delBSTNode(bst->rchild, k); } return true; } void inOrder(BSTree& T) { if (T == NULL) return; inOrder(T->lchild); cout << T->data << " "; inOrder(T->rchild); }
-