引言(未完成版)
由于学习了数据结构这门课,所以我决定对这些天学习的数据结构内容进行一个总结,可能总结的不是很到位,但是主要目的是为了自己的理解,以及加深对数据结构的知识。内容将按照书本大章分类来划分,在这里先说明一下我使用的是数据结构书为余云,李登辉,刘三满主编的书,即下图的小蓝书。
当然,该文章也不可能全部按照课本来写,我会在里面加上我个人在其他地方学习到的一些数据结构,来增加一下知识面。如果有错误,欢迎大佬指出。
数据结构
第一章:绪论
什么是数据结构?
首先来看一下数据结构的官方定义:数据结构是计算机存储、组织数据的方式。是指相互之间存在一种或多种特定关系的数据元素的集合。
数据结构:数据元素和数据关系的集合。
数据结构分为数据的逻辑结构,数据的存储结构,数据结构的运算三个方面。
几个基本概念
数据:所有能够输入到计算机中去描述事物的符号。
数据元素:数据的基本单位也叫节点、记录。
数据项:有独立含义的数据最小单位,也叫域。
数据对象:具有相同特征的数据元素的集合,是数据的一个子集。
结点:一个交叉点、一个标记。算法中的点一般都称为结点,数据集合中的每一个数据元素都用中间标有元素值的方框来表示,称为结点。
节点:一个实体,具有处理的能力。
数据的逻辑结构
定义:数据元素间的逻辑关系。
分为以下四种基本结构:
1.集合:数据元素属于同个集合,无其他关系
2.线性结构:数据元素之间为一对一关系
3.树形结构:数据元素之间为一对多关系
4.图状结构:数据元素之间为多对多关系
数据的存储结构
数据的存储结构是数据的逻辑结构在计算机实际的存储方式,又称为物理结构。分为顺序存储结构与链式存储结构。
顺序存储结构
利用数据元素在存储器中的相对位置来表示数据元素间的逻辑顺序。数据元素在逻辑上相邻,实际存储位置一定相邻。通常用数组实现。
优点:随机访问,访问效率极高。
缺点:空间利用率低,对内存要求比较高,插入、删除不方便。
链式存储结构
利用结点中的指针来表示数据元素之间的关系。数据元素在逻辑上相邻,实际存储位置不一定相邻。通常用指针实现。
优点:插入、删除方便,空间利用率高。
缺点:不能随机访问,只能由前到后逐个访问。
数据结构和运算
1、建立数据结构 create
2、销毁数据结构 destory
3、清空数据结构 clean
4、数据结构排序 sort
5、删除元素 delete
6、插入元素 insert
7、访问元素 access
8、修改元素 modify
9、查询元素 query
10、遍历数据结构 ergodic show print
ATD
ATD是抽象数据类型的英文缩写,全称为Abstract Data Type,是指一个数学模型以及定义在该模型上的一组操作。它通常是指对数据的某种抽象,定义了数据的取值范围及其结构形式,以及对数据的操作的集合。
说白了,抽象数据类型 = 逻辑结构+数据运算。所以抽象数据类型包含三部分:数据对象,数据对象上关系的集合,数据对象的基本操作的集合,用字母表示分别为(D,R,P)。
抽象数据类型可以不需要具体的某一种语言,主要是逻辑上的实现即可。这也是抽象数据类型存在的意义,因为每个人所学习的语言不同,如果使用具体语言来实现,将会有所区别,而所以ATD将减少这个麻烦,让每个人都可以看懂。本文章的数据类型将使用C/C++来实现具体操作。
抽象数据类型的定义
ATD 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
数据操作:<数据操作的定义>
}ATD抽象数据类型名;
数据对象和数据关系使用伪代码描述即可
基本操作名(参数表)
初始条件:<初始条件描述>
操作结果:<操作结果描述>
算法和算法分析
算法的定义
算法是对特定问题求解步骤的一种描述,是指令的有限序列,其中每一条指令表示一个或多个操作。
算法:数据结构所具备的功能,解决特定的问题的方法。
算法的重要特性
1.输入:有零个或者多个输入
2.输出:有一个或者多个输出
3.确定性:每步定义都是确切,无歧义的
4.有穷性:算法应在执行有穷步后结束
5.可行性:每一条运算应足够基本
一个好的算法具备的条件
1.正确性:对任意一个合法的输入经过有限步执行之后算法应给出正确的结果。
2.可读性:可供人们阅读的容易程度。
3.健壮性:算法对于非法输入的抵抗能力
4.高效性:算法运行效率高,即算法运行所消耗的时间短
算法分析
算法分析指分析算法的效率。算法效率主要从时间与空间上来衡量,即算法的运行时间与所需要的存储空间。在绝大部分的算法中,时间与空间不能同时获得,要么速度快的需要的存储空间多,要么需要的存储空间少时间慢,但是在当下的社会,我们往往选择时间快的,因为存储空间可以增加,而时间是无价的。
时间复杂度与空间复杂度
对于时间复杂度与空间复杂度,我认为可以单独开一篇文章详细说明一下。
第二,三章:线性表
线性表,全名为线性存储结构,是最基本、最简单、也是最常用的一种数据结构,线性表中数据元素之间的关系是一对一的关系,一个线性表是n个具有相同特性的数据元素的有限序列,字母记为L = (a1,a2,…,an)。
当 n = 0 时,称线性表为空表。
当 n != 0 时, a1有唯一后继,an有唯一前驱,其他数据元素都有一个前驱和后继。
下图为一个简单的线性表:
顺序表
顺序表是线性表顺序存储方式的实现,即用数组实现线性存储结构。
顺序表的结构体
const int N = 1e5 + 10; //确定顺序表的最大容量
typedef elemtype int;
typedef struct listnode{
elemtype data[N];
int last;
}listnode;
顺序表的初始化
//无返回值定义
void Init_list(listnode head){
head->last = 0;
}
//有返回值定义
listnode *Init_list(){
listnode *L = (listnode *)malloc(sizeof(listnode));
if(L != NULL) L->last = -1;
return L;
}
int main(){
listnode *L;
L = Init_list();
listnode head;
Init_list(head);
return 0;
}
顺序表的插入
int Insert_list(listnode *L, int x, int i){//x为数据,i为插入位置
int j;
if(L->last >= N - 1) return 0; //链表已为最大存储容量,无法存入
if(i < 1 || i > L->last + 2) return -1 // 插入位置错误
for(j = L->last; j >= i - 1; j--)
L->data[j + 1] = L->data[j];
L->data[i - 1] = x;
L->last++;
return 1;
}
顺序表的遍历
void fun_list(listnode *L){
if(L->last == -1) printf("顺序表为空");
for(int i = 0; i < L->last; i++){
printf("%d ", L->data[i]);
}
}
顺序表的查找
//在顺序表中找到 x 的第一个位置并返回
int find_list(listnode *L, int x){
if(L->last == -1) return 0;
int k;
for(int i = 0; i < L->last; i++){
if(L->data[i] == x) k = i;
}
return k + 1; // +1是因为位置从 1开始算,而顺序表存储是从 0开始
}
顺序表的删除
//删除顺序表指定元素(具体为指定元素全部删除)
int delete_list(listnode *L, int x){
if(L->last == -1) return 0;
int k;
for(int i = 0; i < L->last; i++){
if(L->data[i] == x) {
for(int j = i; j < L->last - 1; j++){
L->data[j] = L->data[j + 1];
}
L->last--;
}
}
return 1;
}
顺序表的一些其他操作
//求顺序表长度
int length_list(listnode *L){
return (L->last + 1);
}
//清空顺序表
void clear_list(listnode &L){
L->last = -1;
}
//判断顺序表是否为空
int Isempty_list(listnode L){
if(L->last == -1) return 1;
return 0;
}
链表
链表是线性表链式存储方式的实现,即用指针来实现线性存储结构
头指针:指向链表中第一个结点的指针。
头结点:在链表的首元结点之前的附设的一个结点,数据域内只放空表标志和表长。
首元结点:链表中存储第一个数据元素的结点。
单链表
链表的结构体
typedef elemtype int;
typedef struct listnode{
elemtype data;
struct listnode *next;
}listnode, *list;
链表的初始化
//带头结点
LinkList *Create_List(){
LinkList *head;
head = (LinkList *)malloc(len_of_linklist);
head -> next = NULL;
return head;
}
链表的三种插入
//头插法
void headinsert(listnode *L, int x){
list p = new p;
p->next = head->next;
head->next = p;
p->data = x;
}
//指定位置之后插入
void insertion_list(LinkList *head,int key,int i){
LinkList *p, *last;
p = (LinkList *)malloc(len_of_linklist);
p->data = key;
last = head;
for(int k = 1; last = last->next; k++)
if(last->next == NULL || k == i) break;
p->next = last->next;
last->next = p;
}
//尾插法
void endinsert(listnode *L, int x){
list p = new p, last;
last = head;
while(last->next != NULL){
last = last->next;
}
last->next = p;
p->data = x;
p->next = NULL;
}
链表的遍历
void fun_list(listnode *L){
if(L->next == NULL) printf("链表为空");
while(L->next != NULL){
printf("%d ", L->data);
L = L->next;
}
}
链表的查找
int fun_list(listnode *L, int x){
if(L->next == NULL) return 0;
int k = 0;
while(L->next != NULL){
if(L->data == x) return k;
L = L->next;
k++;
}
}
链表的删除
void fun_list(listnode *L, int x){
if(L->next == NULL) printf("链表为空");
while(L->next != NULL){
if(L->next->data == x) {
L = L->next->next;
}
L = L->next;
}
}
双链表
定义
typedef elemtype int;
typedef struct listnode{
elemtype data[N];
struct listnode *next, *prior;
}listnode, *list;
顺序表对比链表
顺序表和链表都具有增、删、查、改的相同功能,但算法复杂度却不相同。
增:
顺序表往指定位置,不覆盖的添加一个值,后面的值日要往后移动,算法复杂度为O(n);
链表往指定位置添加一个节点,需要从表头遍历到指定位置,算法复杂度为O(n),如果带有索引的节点,算法复杂度为O(1)。
删:
顺序表指定位置,删除一个值时,需要将后面的值向前移动,算法复杂度为O(n);
链表指定一个位置,删除一个时,如果没有对指定节点进行索引,需要从表头遍历到指定位置,然后将指定节点删除,算法复杂度为O(n), 如果对指定节点做索引,删除节点的算法复杂度为O(1)。
查:
顺序表直接查询指定位置值算法复杂度为O(1);
链表需要遍历节点到指定位置,算法复杂度为O(n);如果节点指定位置节点有索引,算法复杂度为O(1).
改:修改其实就是查找修改值的位置,再对值进行修改。
顺序表的增和删表数量规模比较大时,平均移动一半的元素,效率不高。
对于有索引的链表,添加和删除只需要O(1)算法复杂度,效率高。因此,链表用于频繁的添加和删除数据时,有优势。
内存
顺序表是由数组组成的线性表,数组是一组地址连续的单元存储块,分配于栈区,可以自动释放。
链表是由不连续的地址节点组成的线性表,每个节点可以是一个单元的地址块或连续地址块,分配于堆区,节点必须手动释放。内存管理比较不方便。
顺序表
优点:可以直接存取数据元素,方便灵活、效率高。
缺点:插入、删除操作时将会引起元素的大量移动,因而降低效率。
链表
优点:内存采用动态分配,利用率高。结点的插入、删除操作较简单。
缺点:需增设指示结点之间关系的指针域,存取数据元素不如顺序存储方便。
会神奇的发现顺序表和链表的优缺点刚好相反,那么在实际的应用当中,我们就需要考虑是使用顺序表效率高还是链表效率高。例如当需要大量插入和删除的线性表时,我们就可以选择链表。
栈
栈是一个只能在一端插入和删除的线性表,属于一种后进先出模式,可以用顺序与链式俩种方式来存储。
栈的结构体
const int N = 1e5 + 10; //确定顺序表的最大容量
typedef elemtype int;
typedef struct stack{
elemtype data[N];
int top;
}stack;
栈的初始化
stack *Init(){
stack *s;
s = (stack *)malloc(sizeof(stack));
if(s == NULL) return s;
s->top = -1;
return s;
}
入栈
void push(stack * s, elemtype x){
if(s->top >= N - 1) {
printf("栈满,插入失败");
return;
}
s->top++;
s->data[s->top] = x;
}
出栈
void pop(stack *s, elemtype *x){//删除栈顶,并返回栈顶元素
if(s->top == -1) {
printf("栈空,删除失败");
return;
}
s->top--;
*x = s->data[s->top + 1];
}
队列
队列是一个只能在一端插入,在另一端删除的线性表,属于一种后进后出模式,可以用顺序与链式俩种方式来存储。
队列的结构体
const int N = 1e5 + 10; //确定顺序表的最大容量
typedef elemtype int;
typedef struct queue{//与栈不同的是增加了一个变量来分别表示头尾
elemtype data[N];
int front; // 队头
int rear; // 队尾
}queue;
队列的初始化
queue*Init(){
queue *q;
q = (queue*)malloc(sizeof(queue));
if(q == NULL) return q;
q->front = q->rear = -1;
return q;
}
入队
void push(queue* q, elemtype x){
if((q->rear + 1) % N == q->front) {
printf("队满,插入失败");
return;
}
q->rear++;
q->data[q->rear] = x;
}
出队
void pop(queue *q, elemetype *x){
if(q->rear == q->front){
printf("队空,删除失败");
return;
}
*x = q->data[q->front];
q-front++;
}
循环队列
第四章:不一样的线性表
串
串是一种用字符来组成的线性表,可以用顺序与链式俩种方式来存储。所以初始化等操作基本上与线性表一致
串的结构体
const int N = 1e5 + 10; //确定串的最大容量
typedef struct string{
char data[N];
int curlen;
}string;
对于串我们的学习基本上是一些有关字符串的操作,虽然这些操作可以直接调用一些函数实现,但是我们还是学习一下这些函数的原理。
串连接
void StrConcat(char *s1, char *s2, char *s){
int i = 0, j = 0, k = 0;
while(s1[i] != '\0') s[k++] = s1[i++];
while(s2[j] != '\0') s[k++] = s2[j++];
s[k] = '\0';
}
求子串
int Substring(char *sub, char *s, int pos, int len){
int slen = StrLengrh(s); // 求字符串长度
if(pos < 1 || pos > slen || len < 0 || len > slen - pos + 1) return 0;
for(int i = 0; i < len; j++) sub[i] = s[pos + j - 1];
sub[len] = '\0';
return 1;
}
串比较
int StrComp(char *s1, char *s2){
int i = 0;
while(s1[i] == s2[i] && s1[i] != '\0') i++;
return (s1[i] - s2[i]);
}
数组
对于数组,大家应该都特别熟悉了,这里也将不进行代码的说明,用语言说明一下几个概念与公式。
数组分为列存储与行存储,如下图所示,
由于存储方式不同,地址将不同。
矩阵的压缩
为什么需要压缩矩阵,由于在一些程序或者工作中,需要开的空间很大,并且很多元素都是没有用到的,都是又不得不开多维矩阵,为了节省空间,我们就需要压缩矩阵
对称矩阵
在一个n阶方阵中,对于每个元素都有aij= aji,这样的矩阵就叫对称矩阵,如下图所示
对于对称矩阵,我们只需要存储上三角或者下三角即可,这样将可以节省n * (n + 1) / 2 的空间,我们用一个一维数组来保存这个二维数组,具体保存如下:
我们可以看到规律,利用等差数列求和公式可以得到行和列以及一维下标k的关系公式
k = i(i-1)/2+j-1 (i>=j)
k = j(j-1)/2+i-1 (i<j)
如果想了解具体推导过程可以看这个作者写的 对称矩阵下标推导
三角矩阵
矩阵的上(下)三角中的元素均为常量称为三角矩阵,如下图所示:
三角矩阵的压缩方式与对称矩阵基本一致,只是在数组的最后加一个常量数,存储为一维数组如下图所示:
以上俩矩阵皆为下三角存储,如果想了解上三角存储推导过程可以看这个作者写的 三角矩阵下标推导
带状矩阵
在n阶方阵中的非零元素都集中在以主对角线为中心的带状区域中,称为带状矩阵也称对角矩阵,如下图所示:
对于对角矩阵的压缩,我们通常是将元素向左对齐,然后将右边多余的零删除,将上图对角矩阵压缩可以得到下图:
稀疏矩阵
在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵,如下图所示:
对于稀疏矩阵通常采用三元组顺序表存储,即用一个结构体里面定义三个变量,分别记录每一个元素的行(i),列(j),元素(w)。上图稀疏矩阵压缩后可以得到下图:
当然也可以用链表来实现,但是将过于复杂化,感兴趣的小伙伴可以自行去查询这方面的知识
广义表
广义表(Lists,又称列表)是一种非连续性的数据结构,是线性表的一种推广。即广义表中放松对表元素的原子限制,容许它们具有其自身结构。
通常用字母LS表示广义表缩写,用大写字母表示单个广义表,用小写表示单个数据元素,广义表用括号括起来,括号内的元素用逗号分隔开。
广义表的性质
- 广义表是一种多层次的数据结构。
- 广义表是一种线性结构。
- 广义表可以是递归的表。
- 广义表可以为其他表所共享。
第五章:树
树的定义
树是一种层次结构,在生活中只要有层次的方式就可以使用树如:组织关系,家族关系等,因此树在计算机领域就有着广泛的应用。
树是n(n>=0)个结点的有限集。当n = 0时,称为空树。在任意一棵非空树中应满足:
1.有且仅有一个特定的称为根的结点。
2.当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,
并且称为根的子树。
3.树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
4.树中所有结点可以有零个或多个后继。
显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,因此n个结点的树中有n-1条边。
树的基本术语
下面术语中均以图5.1 (b) 为例来说明
- 结点:树中的一个独立单元。包含一个数据元素及若于指向其子树的分支,如图中的A、B、C 等。
- 结点的度:结点拥有的子树数称为结点的度。例如,A的度为3, C的度为0。
- 树的度: 树的度是树内各结点度的最大值。所示的树的度为3。
- 叶子:度为0的结点称为叶子或终端结点。结点C、F、G、I、J、L都是树的叶子。
- 非终端结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A,A的孩子有B、C、D。
- 兄弟:同一个双亲的孩子之间互称兄弟。例如,H 、J、K互为兄弟。
- 祖先:从根到该结点所经分支上的所有结点。例如, F 的祖先为A 、B、E。
- 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如B 的子孙为E 、G、F。
- 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等千其双亲结点的层次加l。
- 堂兄弟:双亲在同一层的结点互为堂兄弟。例如,结点H与E 、G互为堂兄弟。
- 树的深度:树中结点的最大层次称为树的深度或高度。如图所示的树的深度为4。
- 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
二叉树
二叉树定义
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点,二叉树特点是每个节点最多只能有两棵子树,且有左右之分。
简单地理解,满足以下两个条件的树就是二叉树:
- 本身是有序树。
- 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2。
二叉树的性质
二叉树具有以下几个性质(以下性质都没写证明,有兴趣的小伙伴可以自己查找):
- 二叉树中,第 i 层最多有 2i-1 个结点。(i >= 1)
- 如果二叉树的深度为 k,那么此二叉树最多有 2k-1 个结点。(k >= 1)
- 二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1。
满二叉树与完全二叉树
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。满二叉树一定是完全二叉树。
完全二叉树还具有以下性质:
- 具有n个结点的完全二叉树的深度为[log2n] + 1。(lon2n向下取整,注意是完全二叉树)
- 对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 a),对于任意一个结点 i ,当 i > 1 时,父亲结点为结点 [i / 2] 。( i = 1 时,表示的是根结点,无父亲结点 )如果 2 * i > n( 总结点的个数 ),则结点 i 肯定没有左孩子( 为叶子结点 );否则其左孩子是结点 2 * i 。如果 2 * i + 1 > n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2 * i + 1 。
二叉树的顺序存储
二叉树的顺序存储指的是使用顺序表(数组)存储二叉树。虽然树是非线性存储结构,但也可以用顺序表存储。需要注意的是,顺序存储只适用于完全二叉树。对于普通的二叉树,必须先将其转化为完全二叉树,才能存储到顺序表中。将一棵普通二叉树转化为完全二叉树需要给二叉树额外添加一些结点,"拼凑"成完全二叉树,空间大量浪费。这也导致二叉树大部分是用链式存储。
如果需要找到对应的结点,我们可以使用完全二叉树的性质,求出该结点的下标,甚至是左右孩子的下标。
二叉树的结构体
const int N = 1e5;
typedef int ElemType;
typedef struct FullBiTree{
ElemType data[N];
int n;
}FullBiTree;
二叉树的链式存储
用链表存储二叉树,具体的存储方案是:从树的根节点开始,将各个节点及其左右孩子使用链表存储。每个节点都有下图结构:
采用链式存储二叉树时,树中的节点由 3 部分构成:
- 指向左孩子节点的指针(Lchild)
- 节点存储的数据(data)
- 指向右孩子节点的指针(Rchild)
具体代码如下
typedef int ElemType;
//二叉链表
typedef struct BiTreeNode{
ElemType data;
struct BiTreeNode *lchild, *rchild;
}BiTreeNode, *BiTree;
//三叉链表
typedef struct BiTreeNode{
ElemType data;
struct BiTreeNode *lchild, *rchild, *parent;
}BiTreeNode, *BiTree;
课本还提供了一个三叉链表,即在原有的二叉链表多加一个parent指针,这个指针的作用为让孩子可以快速找到双亲。如下图所示:
创建二叉树
对于二叉树的创建,目前大部分使用的是扩展先(中后)序遍历序列来建立二叉树,这种创建需要在输入的自己把NULL的节点改成某种符号来代替输入,否则将输入失败。本人第一次使用这种建立的时候,输入之后一直没有输出,一直以为是代码哪里错误,后来才发现是自己的输入问题。所以各位在使用扩展先(中后)序遍历序列建立的时候需要注意好输入是否为满二叉树。
链式存储
//输入需要将二叉树补成满二叉树 ABD##E##CH### 或者 AB##C##
//扩展先序遍历序列
void createBiTree(BiTree &T) {
char c;
cin >> c;
if ('#' == c) T = NULL; //当遇到#时,令树的根节点为NULL
else{
T = new BiTreeNode;
T->data = c;
createBiTree(T->left);
createBiTree(T->right);
}
}
//扩展中序遍历序列
void createBiTree(BiTree &T) {
char c;
cin >> c;
if ('#' == c) T = NULL; //当遇到#时,令树的根节点为NULL
else{
T = new BiTreeNode;
createBiTree(T->left);
T->data = c;
createBiTree(T->right);
}
}
//扩展后序遍历序列
void createBiTree(BiTree &T) {
char c;
cin >> c;
if ('#' == c) T = NULL; //当遇到#时,令树的根节点为NULL
else{
T = new BiTreeNode;
createBiTree(T->left);
createBiTree(T->right);
T->data = c;
}
}
链式存储在课本上还有另外一种方法创建二叉树,是利用先序和中序来创建,但是由于创建与扩展先序遍历序列相比太复杂,这里也就不写出来了。
顺序存储
void InitBiTree(BiTree T) {
ElemType node;
int i = 0;
//按照层次从左往右输入树中结点的值,0表示空结点;
while (scanf("%d", &node)) T[i++] = node;
}
遍历二叉树
课本介绍了三种遍历方式,可以看到这三种遍历方式好像基本上差不多。
所谓先序遍历二叉树,指的是从根结点出发,按照以下步骤访问二叉树的每个结点:
- 访问当前结点;
- 进入当前结点的左子树,以同样的步骤遍历左子树中的结点;
- 遍历完当前结点的左子树后,再进入它的右子树,以同样的步骤遍历右子树中的结点;
中序遍历二叉树,指的是从左结点出发,按照以下步骤访问二叉树的每个结点:
- 进入当前结点的左子树,以同样的步骤遍历左子树中的结点;
- 访问当前结点;
- 最后进入当前结点的右子树,以同样的步骤遍历右子树中的结点。
后序遍历二叉树,指的是从左结点出发,按照以下步骤访问二叉树的每个结点:
- 进入当前结点的左子树,以同样的步骤遍历左子树中的结点;
- 如果当前结点没有左子树,则进入它的右子树,以同样的步骤遍历右子树中的结点;
- 直到当前结点的左子树和右子树都遍历完后,才访问该结点。
以该图为例
先序遍历:1,2,4,5 ,3 ,6 ,7
中序遍历:4,2,5,1 ,6 ,3 ,7
后序遍历:4,5,2,6 ,7 ,3 ,1
如果题目给出先序和中序是可以推出后序,或者给后序和中序是开头推出先序,需要注意的是,给先序和后序是无法推出中序的。
顺序存储二叉树
const int MAXs = 100;
//先序遍历
void PreOrderTraverse(BiTree T, int i) {
//根节点的值不为 0,证明二叉树存在
if (T[i]) {
printf("%d ", T[i]);
//先序遍历左子树
if ((2 * i + 1 < MAXs) && (T[2 * i+ 1] != 0))
PreOrderTraverse(T, 2 * i + 1);
//最后先序遍历右子树
if ((2 * i + 2 < MAXs) && (T[2 * i + 2] != 0))
PreOrderTraverse(T, 2 * i+ 2);
}
}
//中序遍历
void INOrderTraverse(BiTree T, int i) {
if (((2 * i + 1) < MAXs) && (T[2 * i + 1] != 0))
INOrderTraverse(T, 2 * i + 1);
printf("%d ", T[i]);
if (((2 * i + 2) < MAXs) && (T[2 * i + 2] != 0))
INOrderTraverse(T, 2 * i + 2);
}
//后序遍历
void INOrderTraverse(BiTree T, int i) {
if (((2 * i + 1) < MAXs) && (T[2 * i + 1] != 0))
INOrderTraverse(T, 2 * i + 1);
if (((2 * i + 2) < MAXs) && (T[2 * i + 2] != 0))
INOrderTraverse(T, 2 * i + 2);
printf("%d ", T[i]);
}
链式存储二叉树
//先序遍历
void PreOrderTraverse(BiTree T) {
//如果二叉树存在,则遍历二叉树
if (T) {
printf("%d",T->data);
PreOrderTraverse(T->lchild);//访问该结点的左孩子
PreOrderTraverse(T->rchild);//访问该结点的右孩子
}
}
//中序遍历
void PreOrderTraverse(BiTree T) {
if (T) {
PreOrderTraverse(T->lchild);
printf("%d",T->data);
PreOrderTraverse(T->rchild);
}
}
//后序遍历
void PreOrderTraverse(BiTree T) {
if (T) {
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
printf("%d",T->data);
}
}
接下来我们再介绍一个遍历,层次遍历,所谓层次遍历二叉树,就是从树的根结点开始,一层一层按照从左往右的次序依次访问树中的结点。
以上图为例,层次遍历:1,2,3,4 ,5 ,6 ,7
顺序存储
void LevelOrderTraverse(BiTree T) {
for (int j = 0; j < MAXs; j++) {//从根结点起,层次遍历二叉树
//只访问非空结点
if (T[j] != 0) printf("%d ", T[j]);
}
}
因为顺序存储本来就是以层次来存储的,所以在层次遍历的时候会非常方便。
链式存储
void LevelOrderTraverse(BiTree T) {
//判断二叉树是否存在
if (T) {
BiTree queue[MAXs];
int front = -1, rear = -1;
queue[++rear] = T;
while (front != rear){
front++; //从队列取出一个结点
printf("%d ", queue[front]->data);//访问当前结点
//将当前结点的左右孩子依次入队
if (p->lchild) queue[++rear] = queue[front]->lchild;
if (p->rchild) queue[++rear] = queue[front]->rchild;
}
}
}
森林
森林是m (m >= 0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。森林与树是可以相互变化的,对于任何一棵树,删除其根就变成森林,如果森林里面的树作为一个根的子树,就变成了树。
哈夫曼树
哈夫曼树,别名“赫夫曼树”、“最优树”以及“最优二叉树”。在给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则称该二叉树为哈夫曼树。
解释几个与哈夫曼数相关的名词:
-
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。下图从根结点到结点 a 之间的通路就是一条路径。
-
路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。下图从根结点到结点 c 的路径长度为 3。
-
结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。例如,下图结点 a 的权为 7,结点 b 的权为 5。
-
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。下图结点 b 的带权路径长度为 2 * 5 = 10 。
-
树的带权路径长度:所有叶子结点的带权路径长度之和,记为WPL。下图的WPL = (1 * 7)+(2 * 5)+(2 * 3)+(4 * 3) = 35。
创建哈夫曼数
下面给出一个非常简洁易操作的算法,来构造一棵哈夫曼树:
- 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和;。
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
- 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。
哈夫曼编码的生成
对于任意一棵二叉树来说,把二叉树上的所有分支都进行编号,将所有左分支都标记为0,所有右分支都标记为1,对于树上的任何一个结点,都可以根据从根结点到该结点的路径唯一确定一个编号。这个编号即为哈夫曼编码。以上图D图为例,可以得到下图:
在上图中:
叶子结点a的哈夫曼编码为:0
叶子结点b的哈夫曼编码为:10
叶子结点c的哈夫曼编码为:110
叶子结点d的哈夫曼编码为:111
线段树(待补)
并查集(待补)
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
并查集的各种操作
#define MAXN 100
int f[MAXN];
//初始化
void init(int n){
for(int i = 0; i < n; i++){
f[i] = i; //存放每个结点的结点(或双亲结点)
}
}
//查询根结点
int find(int x){
if(f[x] == x) return x;
else return find(f[x]);
}
//合并,把 j 合并到 i 中去,就是把j的双亲结点设为i
void Union(int i,int j){
f[find(j)] = find(i);
}
并查集的代码很简单,但是如果不进行优化的话将导致效率很低,接下来我们将进行俩方面的优化
int findx(int x) {
if(f[x] != x)
f[x] = findx(f[x]); // 路径压缩优化
return f[x];
}
void Union(int i, int j){//按秩合并优化
int x = find(i),y = find(j); //分别找到结点i和结点j的根节点
//如果以x作为根结点的子树深度小于以y作为根结点的子树的深度,则把x合并到y中
if(Rank[x] < Rank[y]) f[x]=y;
else f[y]=x;
if(Rank[x] == Rank[y] && x != y)//如果深度相同且根结点不同,以x为根结点的子树深度+1
Rank[x]++;
}
第六章:图论
图论(Graph Theory)是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些实体之间的某种特定关系,用点代表实体,用连接两点的线表示两个实体间具有的某种关系。
图
图可以被表示为 G={V, E},其中 V={v1, … , vN},E= {e1, … , eM}。(也就是说,V是节点的集合,即所谓的点集,E是连接两点的线即两点间关系的集合,即所谓的边集)图结构常用来存储逻辑关系为“多对多”的数据,也是数据结构中最复杂、难掌握的存储结构。
图的基本术语
-
顶点(vertex):上图中圆圈的点就是顶点,表示某个事物或对象。由于图的术语没有标准化,因此,称顶点为点、节点、结点、端点等都是可以的。
-
边(edge):上图中顶点之间的线条就是边,表示事物与事物之间的关系。需要注意的是边表示的是顶点之间的逻辑关系。
-
弧(arc):在有向图中,边也称做为弧,无箭头一端的顶点通常被称为"初始点"或"弧尾",箭头一端的顶点被称为"终端点"或"弧头"。
-
度(degree):图中与结点v关联的边的条数称为结点v的度,在有向图中以结点v为起点的有向边的条数称为结点v的出度。有向图中以结点v为终点的有向边的条数称为结点v的入度。可以看到在有向图中出度等于入度。
-
权重(weight):在图的边或弧中给出相关的数。而带权的图通常称为网。
-
子图(subgraph):由图中一部分顶点和边构成的图,称为原图的子图。
-
路径和回路(loop):从一个顶点到另一顶点途经的所有顶点组成的序列(包含这两个顶点),称为一条路径。如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。若路径中各顶点都不重复,此路径被称为"简单路径";若回路中的顶点互不重复,此回路被称为"简单回路"(或简单环)。
稀疏图:图中具有很少的边(或弧)
稠密图:图中具有很多的边(或弧)
有向图:有用箭头标明了边是有方向性的,上图中,G1为有向图。
有向图中描述从 V1 到 V2 的"单向"关系可以用 <V1,V2> 来表示,意为从V1到V2的一条弧。
无向图:没有用箭头标明了边是有方向性的,上图中,G2,G3为无向图。
无向图中描述两顶点 V1 和 V2 之间的关系可以用 (V1, V2) 来表示;意为从V1与V2之间有一条边。
连通图:图中任意两点之间均至少有一条通路。
强连通图:在有向图中, 若对于每一对顶点v1和v2, 都存在一条从v1到v2和从v2到v1的路径。
基图:将有向图的所有的有向边替换为无向边,所得到的图称为原图的基图。
弱连通图:一个有向图的基图是连通图,则有向图是弱连通图。
简单图:一个无环,无多重边的图称为简单图。
多重图:一个无环,有多重边的图称为多重图。
完全无向图:具有n个顶点,并具有n(n - 1)/2 条边的无向图,即每个顶点到其他顶点都有边。
完全有向图:具有n个顶点,并且具有n(n - 1) 条边的有向图,即每个顶点到其他顶点都有相互两条边。
完全图:完全无向图和完全有向图都称为完全图。
图的存储
邻接矩阵–顺序存储
邻接矩阵存储方式,本质上就是开一个二维数组,若存在该边则将该俩顶点为下标的数组+1即可,使用这种方法可以存储所有的图,无论是环还是多重边,并且方便好理解。缺点就是如果是稀疏图将大量浪费空间。以及当顶点过多时将无法使用,例如有105个顶点,这个时候是无法开出105 * 105这么大的数组。这个就需要后面的方法来存储这个图。
#define MAX_VERtEX_NUM 100 //顶点的最大个数
typedef int EdgeType //存储弧或者边信息的变量类型
typedef char VertexType //图中顶点的数据类型
typedef struct {
VertexType vexs[MAX_VERtEX_NUM]; //存储图中顶点数据
EdgeType edges[MAX_VERtEX_NUM][MAX_VERtEX_NUM];//邻接矩阵
int vexnum,arcnum; //记录图的顶点数和弧(边)数
}MGraph;
邻接表–顺序与链式存储
邻接表存储方式,是一种顺序存储与链式存储的结合,解决了邻接矩阵浪费空间的问题,直接用链表的形式在每一个有边的顶点连接起来。实际上,邻接表就是由一个顺序表和多个单链表组成的,顺序表用来存储图中的所有顶点,各个单链表存储和当前顶点有直接关联的边或弧。如下图:
可以看到V1有俩条边分别向V2,V3,用邻接表的形式先将V1的指针指向V2的数组下标即1,然后再指向V3的数组下标即2。这样就将V1指向的边存储完了。这里的链表存储可以是各种插入,但是大部分是一头插入为主来进行保存指向的顶点。
typedef struct node{//表结点
int adjvex;//邻接点域,存储弧,即另一端顶点在数组中的下标
struct node * next;//指向下一个邻接点的指针域
}EdgeNode;
typedef struct vnode{//表头结点
VertexType vertex;//顶点域
EdgeNode * firstedge;//边表头指针
}VertexNode;
typedef struct ALGraph{
VertexNode adjlist[MAX_VERtEX_NUM];//邻接表
int n, e; //记录图的顶点数和弧(边)数
}ALGraph;
十字链表–链式存储
十字链表是有向图的一种链式存储结构,可以看做将有向图的邻接表和逆邻接表结合的一种链表。因为用邻接表存储有向图(网),可以快速计算出某个顶点的出度,但计算入度的效率不高。反之,用逆邻接表存储有向图(网),可以快速计算出某个顶点的入度,但计算出度的效率不高。所以用十字链表就可以实现可以快速计算出有向图(网)中某个顶点的入度和出度。将图中的所有顶点存储到顺序表(也可以是链表)中,同时为每个顶点配备两个链表,一个链表记录以当前顶点为弧头的弧,另一个链表记录以当前顶点为弧尾的弧。
顺序表中的空间用来存储图中的顶点,结构如下图所示:
链表的结点用来存储图中的弧,结构如下图所示:
各部分的含义分别是:
data 数据域:用来存储顶点的信息;
firstin 指针域:指向一个链表,链表中记录的都是以当前顶点为弧头的弧的信息;
firstout 指针域:指向另一个链表,链表中记录的是以当前顶点为弧尾的弧的信息。
tailvex数据域:存储弧尾一端顶点在顺序表中的位置下标;
headvex 数据域:存储弧头一端顶点在顺序表中的位置下标;
hlink 指针域:指向下一个以当前顶点作为弧头的弧;
tlink 指针域:指向下一个以当前顶点作为弧尾的弧;
info 指针:存储弧的其它信息,例如有向网中弧的权值。如果不需要存储其它信息,可以省略。
typedef struct ArcNode{
int tailvex, headvex;
struct ArcNode *hlink, *tlink;
}ArcNode;
typedef struct VexNode{
VertexType data;
ArcNode *firstin, *firstout;
}VexNode;
邻接多重表
十字链表是无向图的一种链式存储结构。可以看作是邻接表和十字链表的结合体,具体来讲就是:将图中的所有顶点存储到顺序表(也可以用链表)中,同时为每个顶点配备一个链表,链表的各个结点中存储的都是和当前顶点有直接关联的边。当需要在无向图中做大量的插入或删除边的操作时,选用邻接多重表存储无向图,可以提高程序的执行效率。
顺序表用来存储图中的各个顶点,各个存储空间的结构如下图所示:
邻接多重表中的链表用来存储和当前顶点有直接关联的边,结点的结构如下图所示:
各个部分的含义分别是:
data 数据域用来存储顶点的数据;
firstedge 指针域用来指向为当前顶点配备的链表。
mark 标志域:实际场景中,可以为每个结点设置一个标志域,记录当前结点是否已经被操作过。例如遍历无向图中的所有边,借助 mark 标志域可以避免重复访问同一条边;
ivex 和 jvec:都是数据域,分别存储边两端顶点所在顺序表中的位置下标;
ilink 指针域:指向下一个与 ivex 顶点有直接关联的边结点;
jlink 指针域:指向下一个与 jvex 顶点有直接关联的边节点;
info 指针域:存储当前边的其它信息,比如存储无向网时,可以用 info 指针域存储边的权值。
typedef struct ENode{ //边结点类型
int mark; //访问标记
int ivex, jvex; //该边的俩个顶点位置信息
struct ENode *ilink, *jlink; //分别指向依附这俩个顶点的下一条边
}ENode;
typedef struct Vnode{ //顶点结点类型
VertexType data; //顶点数据域
ENode * firstedge; //指向第一条依附该顶点的边
}Vnode;
DFS
DFS名为深度优先搜索,即当我们对某个树或者图进行搜索时,一条路走到底。dfs总是沿着某个最深的方向来进行搜索,直到无路可走,才会返回选择另外一条路继续一条路走到底,与前面介绍的先序遍历一致。
实现方式
- 就是从图中的某个顶点出发,不停的寻找相邻的、尚未访问的顶点:
- 如果找到多个,则任选一个顶点,然后继续从该顶点出发;
- 如果一个都没有找到,则回退到之前访问过的顶点,看看是否有漏掉的;
#define MAX_VERtEX_NUM 20 //顶点的最大个数
#define VRType int //表示顶点之间关系的类型, 0 表示不相邻,1 表示相邻
#define VertexType int //图中顶点的数据类型
#define States int
typedef enum { false, true }bool; //定义bool型常量
bool visited[MAX_VERtEX_NUM]; //设置全局数组,标记图中的各个顶点是否被访问过
typedef struct {
VRType adj; //用 1 或 0 表示是否相邻;
}ArcCell, AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];
typedef struct {
VertexType vexs[MAX_VERtEX_NUM]; //存储图中的顶点
AdjMatrix arcs; //二维数组,记录顶点之间的关系
int vexnum, arcnum; //记录图的顶点数和弧(边)数
}MGraph;
//根据顶点数据,返回顶点在二维数组中的位置下标
int LocateVex(MGraph* G, VertexType v) {
int i = 0;
//遍历一维数组,找到变量v
for (; i < G->vexnum; i++) if (G->vexs[i] == v) break;
//如果找不到,输出提示语句,返回-1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//构造无向图
States CreateDN(MGraph* G) {
int i, j, n, m;
int v1, v2;
scanf("%d,%d", &(G->vexnum), &(G->arcnum));
for (i = 0; i < G->vexnum; i++) scanf("%d", &(G->vexs[i]));
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++)
G->arcs[i][j].adj = 0;
}
for (i = 0; i < G->arcnum; i++) {
scanf("%d,%d", &v1, &v2);
n = LocateVex(G, v1);
m = LocateVex(G, v2);
if (m == -1 || n == -1) {
printf("no this vertex\n");
return -1;
}
G->arcs[n][m].adj = 1;
G->arcs[m][n].adj = 1;//无向图的二阶矩阵沿主对角线对称
}
return 1;
}
int FirstAdjVex(MGraph G, int v){
//对于数组下标 v 处的顶点,找到第一个和它相邻的顶点,并返回该顶点的数组下标
for (int i = 0; i < G.vexnum; i++)
if (G.arcs[v][i].adj)return i;
return -1;
}
int NextAdjVex(MGraph G, int v, int w){
//对于数组下标 v 处的顶点,从 w 位置开始继续查找和它相邻的顶点,并返回该顶点的数组下标
for (int i = w + 1; i < G.vexnum; i++)
if (G.arcs[v][i].adj)return i;
return -1;
}
void DFS(MGraph G, int v) {
printf("%d ", G.vexs[v]); //访问第 v 个顶点
visited[v] = true; //将第 v 个顶点的标记设置为true
//对于与第 v 个顶点相邻的其它顶点,逐个调用深度优先搜索算法
for (int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
//如果该顶点的标记为false,证明尚未被访问,就调用深度优先搜索算法
if (!visited[w]) DFS(G, w);
}
//深度优先搜索
void DFSTraverse(MGraph G) {
//visit数组记录各个顶点是否已经访问过,全部初始化为 false
for (int v = 0; v < G.vexnum; ++v) visited[v] = false;
//对于每个标记为false的顶点,都调用一次深度优先搜索算法
for (int v = 0; v < G.vexnum; v++)
//如果该顶点的标记位为false,就调用深度优先搜索算法
if (!visited[v]) DFS(G, v);
}
BFS
BFS名为广度优先搜索,即当我们对某个树或者图进行搜索时,bfs是从最近地方开始,范围慢慢扩大,与前面介绍的层次遍历一致。
实现方式
- 就是从图中的某个顶点出发,不停的寻找相邻的、尚未访问的顶点:
- 所有和它连通的顶点都访问完之后,重新选择一个尚未访问的顶点(非连通图中就存在这样的顶点),继续以同样的思路寻找未访问的其它顶点。
//队列链表中的结点类型
typedef struct Queue {
VertexType data;
struct Queue* next;
}Queue;
typedef struct {
VRType adj; //用 0 表示不相邻,用 1 表示相邻
}ArcCell, AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];
typedef struct {
VertexType vexs[MAX_VERtEX_NUM]; //存储图中的顶点
AdjMatrix arcs; //二维数组,记录顶点之间的关系
int vexnum, arcnum; //记录图的顶点数和弧(边)数
}MGraph;
int LocateVex(MGraph* G, VertexType v)
void CreateDN(MGraph* G)
int FirstAdjVex(MGraph G, int v)
int NextAdjVex(MGraph G, int v, int w){
//以上函数与DFS一致
//初始化队列,这是一个有头结点的队列链表
void InitQueue(Queue** Q) {
(*Q) = (Queue*)malloc(sizeof(Queue));
(*Q)->next = NULL;
}
//顶点元素v进队列
void EnQueue(Queue** Q, VertexType v) {
Queue* temp = (*Q);
Queue* element = (Queue*)malloc(sizeof(Queue)); //创建一个存储 v 的结点
element->data = v;
element->next = NULL;
while (temp->next != NULL) { //将 v 添加到队列链表的尾部
temp = temp->next;
}
temp->next = element;
}
//队头元素出队列
void DeQueue(Queue** Q, int* u) {
Queue* del = (*Q)->next;
(*u) = (*Q)->next->data;
(*Q)->next = (*Q)->next->next;
free(del);
}
//判断队列是否为空
bool QueueEmpty(Queue* Q) {
if (Q->next == NULL) return true;
return false;
}
//释放队列占用的堆空间
void DelQueue(Queue* Q) {
Queue* del = NULL;
while (Q->next) {
del = Q->next;
Q->next = Q->next->next;
free(del);
}
free(Q);
}
//广度优先搜索
void BFSTraverse(MGraph G) {
int v, u, w;
Queue* Q = NULL;
InitQueue(&Q);
//将用做标记的visit数组初始化为false
for (v = 0; v < G.vexnum; ++v) visited[v] = false;
for (v = 0; v < G.vexnum; v++) { //遍历图中的各个顶点
if (!visited[v]) {//若当前顶点尚未访问,从此顶点出发,找到并访问和它连通的所有顶点
printf("%d ", G.vexs[v]); //访问顶点,并更新它的访问状态
visited[v] = true;
EnQueue(&Q, G.vexs[v]); //将顶点入队
while (!QueueEmpty(Q)) { //遍历队列中的所有顶点
DeQueue(&Q, &u); //从队列中的一个顶点出发
u = LocateVex(&G, u); //找到顶点对应的数组下标
for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)) {//遍历紧邻 u 的所有顶点
if (!visited[w]) {//将紧邻 u 且尚未访问的顶点,访问后入队
printf("%d ", G.vexs[w]);
visited[w] = true;
EnQueue(&Q, G.vexs[w]);
}
}
}
}
}
DelQueue(Q);
}
最小生成树
生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n - 1条边
最小生成树:一个连通图的生成树可能有多个。边的权值之和最小的生成树是最小生成树。最小生成树在生活中有许多用处,例如俩城市线路的造价,选择花费价格最小的一条路线,其实就是在寻找最小生成树。
kruskal算法
kruskal算法称克鲁斯卡尔算法,是一种按权值递增的次序选择合适的边来构造最小生成树。
const int MASx = 100;
typedef struct KEdge{
int vex1; //边的起始顶点
int vex2; //边的终止顶点
int weight; //边的权值
}
void Kruskal(KEdge E[], int n, int e){
int i, j, m1, m2, sn1, sn2, k;
int vset[MASx]; //用于记录顶点是否属于同一集合的辅助数组
for(i = 0; i < n; i++) vset[i] = i; // 初始化辅助数组
k = 0; //表示当前构造最小生成树的第k条边,初始化为0;
for(j = 0; j < e; j++){
m1 = E[j].vex1;
m2 = E[j].vex2; //取一条边的俩个顶点
sn1 = vset[m1];
sn2 = vset[m2]; //分别得到俩个顶点所属的集合编号
if(sn1 != sn2){ //俩顶点分属于不同的集合,该边是最小生成树的一条边
printf("(%d,%d):%d\n", m1, m2, E[j].weight);
k++;
if(k == n - 1) break;
for(i = 0; i < n; i++) if(vset[i] == sn1) vset[i] = sn2;
}
}
}
prim算法
prim算法称普利姆算法,是一种将顶点连通的方式来构造最小生成树。
struct{
int adjvex;
int lowcost;
}closedge[MAXs];
void prim(MGraph G[], int v){
int i, j, minCost k;
closedge[v].lowcost = 0; //用于记录顶点是否属于同一集合的辅助数组
for(i = 0; i < n; i++)
if(j != v){
closedge[j].adjvex = v;
closedge[j].lowcost = G.edges[v][j];
}
for(i = 1; i < G; i++){
for(j = 0; j < e; j++){
if(closedge[j].lowcost != 0){
k = j;
break;
}
}
minCost = closedge[j]..lowcost;
for(j = 0; j < G.n; j++)
if(closedge[j].lowcost < minCost && closedge[j].lowcost){
minCost =closedge[j].lowcost;
k = j;
}
printf("(%c,%c)\n",closedge[j].adjvex, G.vexs[k]);
closedge[j].lowcost = 0;
for(j = 0; j < G.n; j++)
if(G.edges[k][j] < closedge[j].lowcost){
closedge[j].adjvex = G.vexs[k];
closedge[j].lowcost = G.edges[k][j];
}
}
最短路径
在图结构中,一个顶点到另一个顶点的路径可能有多条,最短路径指的就是顶点之间“最短”的路径。在不同的场景中,路径“最短”的含义也有所差异,比如途径顶点数量最少、总权值最小等。提到最短路径,往往指的是总权值最小的路径,所以常常在网结构(带权的图)中讨论最短路径问题,包括有向网和无向网。
单源最短路径
单源最短路径,是指从一点出发到图中其他所有点的最短路径。在求单源最短路径中,基本上用迪杰斯特拉(dijkstra)算法
基本思想:对图G(V, E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已包含所有顶点。
用邻接矩阵实现dijkstra算法
int n, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示已访问。初值均为false
void Dijkstra(int s){ //s为起点
fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
d[s] = 0; //起点s到达自身的距离为0
for(int i = 0; i < n; i++) {
int u = -1, MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j = 0; j < n; j++) //找到未访问的顶点中d[]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return;
vis[u] = true; //标记u为已访问
for(int v = 0; v < n; v++)
//如果v未访问 && u能到达v && 以u为中介点可以使d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v])
d[v] = d[u] + G[u][v]; //优化d[v]
}
}
用邻接表实现dijkstra算法
struct Node{
int v, dis; //v为边的目标顶点,dis为边权
}
vector<Node> Adj[MAXV]; //图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n; //n为顶点数,图G使用邻接表实现,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示已访问。初值均为false
void Dijkstra(int s) //s为起点{
fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
d[s] = 0; //起点s到达自身的距离为0
for(int i = 0; i < n; i++){//循环n次
int u = -1, MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j = 0; j < n; j++) //找到未访问的顶点中d[]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return;
vis[u] = true; //标记u为已访问
//只有下面这个for与邻接矩阵的写法不同
for(int j = 0; j < Adj[u].size(); j++){
int v = Adj[u][j].v; //通过邻接表直接获得u能到达的顶点v
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v])
//如果v未访问 && 以u为中介点可以使d[v]更优
d[v] = d[u] + Adj[u][j].dis; //优化d[v]
}
}
}
任意两点间最短路径
任意两点间最短路径,这时源点就从单个确定的点变成了所有的点,要求出所有点到所有点的最短路径。。在求任意两点间最短路径中,基本上用弗洛伊德(floyd)算法,当然也可以使用dijkstra算法,因为这样就是需要dijkstra算法把每个点都跑一遍,这样的时间复杂度为O(n3).而floyd算法的时间复杂度也是O(n3),但是在代码上大大减少。
const int INF = 1000000000;
const int MAXV = 200; //MAXV为最大顶点数
int n, m; //n为顶点数,m为边数
int dis[MAXV][MAXV]; //dis[i][j]表示顶点i和顶点j的最短距离
void Floyd(){
for(int k = 0; k < n; k++)
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
if(dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j])
dis[i][j] = dis[i][k] + dis[k][j]; //找到更短的路径
}
最小生成树与最短路径区别
拓扑排序
对一个有向无环图 G 进行拓扑排序,是将 G中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v ,若边 < u , v > ∈ E ( G ),则 u 在线性序列中出现在 v之前。通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
关键路径
AOE网(Activity On Edge Network)指一个带权值有向图中,结点表示活动结点,边表示活动,权值表示活动持续时间。AOE网具有特性。
关键路径实际上就是路径长度最长的路径。
第七章:十大排序
排序考虑俩个方面,第一个为速度,第二个为稳定性,我个人认为还应该考虑一个代码的难易程度。对此本章将从这三个方面介绍十大排序。
稳定性
排序的稳定性指的是存在多个具有相同的数据元素,若经过排序,这些数据元素的相对次序保持不变。例如一个序列,a[1] = a[5], 经过排序后如果元素a[1]的位置比元素a[5] 的位置后,则说明该排序是不稳定的,若元素a[1]的位置比元素a[5] 的位置前面,则说明该排序是稳定的。
稳定排序:
冒泡排序、插入排序、归并排序
非稳定排序:
选择排序、希尔排序、堆排序、快速排序
O(n2)排序
插入排序
const int N = 1e10;
int mian(){
int a[N];
int temp, n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", a[i]);
for(int i = 1; i < n; i++){
temp = a[i];
for(int j = i - 1; j >= 0 && temp < a[j]; j--)
a[j + 1] = a[j];
a[j + 1 ] = temp;
}
}
冒泡排序
const int N = 1e5;
int mian(){
int a[N];
int temp, n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", a[i]);
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
if(a[j - 1] > a[j])
swap(a[j - 1], a[j]);
}
选择排序
const int N = 1e5;
int mian(){
int a[N];
int temp, n, min;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a[i]);
for(int i = 0; i < n - 1; i++){
min = i;
for(int j = i + 1; j < n; j++)
if(a[j] < a[min]) min = j;
swap(a[i], a[min]);
}
}
O(nlgn)排序
归并排序
const int N = 1e5;
int tmp[N];
void merger_sort(int a[], int l, int r){
if(l == r) return ;
int mid = (l + r) >> 1;
merger_sort(a, l, mid);
merger_sort(a, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while(i <= mid && j <= r){
if(a[i] <= a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while(i <= mid) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
for(int i = l, j = 0; i <= r; i++, j++) a[i] = tmp[j];
}
快速排序
const int N = 1e5;
void quick_sort(int a[], int l, int r){
if(l == r) return ;
int num = a[(l + r) >> 1];
int i = l - 1, j = r + 1;
while(i < j){
do i++; while(a[i] < num);
do j--; while(a[j] > num);
if(i < j) swap(a[i],a[j]);
}
quick_sort(a,l,j);
quick_sort(a,j + 1,r);
}
堆排序
int nLen = 0;
void swap(std::vector<int> &vecArry,int nSrc, int nDes)
{
int nTemp = vecArry[nSrc];
vecArry[nSrc] = vecArry[nDes];
vecArry[nDes] = nTemp;
}
void funcBuild(std::vector<int> &vecArry, int n)
{
int left = 2 * n + 1;
int right = 2 * n + 2;
int largest = n;
if (left < nLen && vecArry[left] < vecArry[largest]) {
largest = left;
}
if (right < nLen && vecArry[right] < vecArry[largest]) {
largest = right;
}
if (largest != n) {
swap(vecArry, n, largest);
funcBuild(vecArry, largest);
}
}
void heapify_sort(std::vector<int> &vecArry)
{
nLen = vecArry.size();
for (int i = nLen/2-1; i >= 0; i--)
{
funcBuild(vecArry,i);
}
for (int j = nLen-1;j >= 0; j--)
{
swap(vecArry,0,j);
nLen--;
funcBuild(vecArry,0);
}
}
特殊排序
基数排序
void radixSort(std::vector<int> &vecArry) //基数排序
{
int nLen = vecArry.size();
int nMax = vecArry.at(0);
for (auto &value : vecArry)
{
if (nMax < value)
{
nMax = value;
}
}
int nCount = 0;
while(nMax)
{
nMax/=10;
nCount++;
}
int radix = 1;
std::vector<std::vector<int>> vecTemp(10);
for (int i = 0; i < nCount; i++)
{
vecTemp.clear();
vecTemp.resize(10);
for(auto &value : vecArry)
{
int nIndex = (value/radix)%10;
vecTemp.at(nIndex).push_back(value);
}
int nIndex = 0;
for (auto &value : vecTemp)
{
for (auto &data : value)
{
vecArry[nIndex++] = data;
}
}
radix *= 10;
}
}
桶排序
void bucketSort2(std::vector<int> &vecArry)
{
int nMax = vecArry[0];
for (auto &value : vecArry)
{
if (value > nMax)
{
nMax = value;
}
}
std::vector<int> vecTemp(nMax+1,0);
for (auto &value : vecArry)
{
vecTemp[value]++;
}
int nIndex = 0;
for (int i = 0; i < vecTemp.size(); ++i)
{
while (vecTemp[i] > 0)
{
vecArry[nIndex++] = i;
vecTemp[i]--;
}
}
}
void bucketSort(std::vector<int> &vecArry) {
int nMax = vecArry.at(0);
int nMin = vecArry.at(0);
for (auto &data : vecArry)
{
if (data > nMax)
{
nMax = data;
}
if (data < nMin)
{
nMin = data;
}
}
int nValue = nMax - nMin;//差值
std::vector<std::vector<int>> vecTemp;
int nLen = vecArry.size();//序列长度
vecTemp.resize(nLen);
for (auto &value : vecArry)
{
//nLen-1 个区间
//将差值为nValue平分到nLen-1个区间上
//value-min是当前元素和最小值差值
//nIndex就为(value - nMin)/nValue/(nLen-1)
int nIndex = (value - nMin)/nValue/(nLen-1);
vecTemp[nIndex].push_back(value);
}
for (auto &data : vecTemp)
{
insertSort(data);
}
int nIndex= 0;
for (auto &data : vecTemp)
{
for(auto &value : data)
{
vecArry[nIndex++] = value;
}
}
}
计数排序
void countSort(std::vector<int> &vecArry)
{
int nMax = 0;
for (auto &item : vecArry) {
if (nMax < item)
{
nMax = item;
}
}
std::vector<int> vecCount(nMax+1,0);
for (int i = 0; i < vecArry.size();i++)
{
vecCount[vecArry[i]]++;
}
std::vector<int> vecResult(vecArry.size(),0);
int nIndex = 0;
for (int j = 0; j< vecCount.size();j++)
{
while(vecCount.at(j) > 0)
{
vecResult[nIndex++] = j;
vecCount[j]--;
}
}
}