数据结构(不完整版,不断更新中)

一.何为数据结构

1.数据:数据就是代表事物的一串符号集合,当然,它要能被计算机识别和处理,计算机通过处理数据来实现各种程序
2.数据元素:如果说数据是一个庞大的集合的话,数据元素就是其中的一个小集合,可以看作是一个整体,再拆开这个数据元素,里面的最小项就是数据项,For example:steam是一个数据,你的游戏库就是一个数据元素,里面的游戏就是数据项,因为作为游戏,它无法再被分割了
3.数据对象:数据对象是一个特殊的子集,里面的数据元素都必须是一种类型
ps:以上这三都是套娃,按照集合大小和部分特殊集合来套,通常就是数据>数据元素>数据对象>数据项
4.数据类型:給数据加上些小小的操作,根据对集合的操作分为原子类型,结构类型,抽象数据类型
5.数据结构:在数据元素之间通过某些操作联系起来,这就是数据结构了,而操作我们称之为逻辑,逻辑又要运行在物理结构上,数据元素经由这些操作又形成了数据运算·,所以数据结构和逻辑结构,存储结构,数据的运算有关
逻辑结构:虚拟的关系,是数据元素间构成的逻辑关系,一般有线性和非线性,特殊点的就集合,集合里的数据元素的关系,除了属于一个集合外也没啥了,线性就是一个接一个,没有多余的映射关系,非线性就是发散,可以一个对多个(树形结构,二叉树),也可以多个对多个(图状结构,网状结构)
存储(物理)结构:存储结构就是逻辑关系映射在物理层面存储的映像,就是逻辑关系体现在物理结构上
一般有这几种存储结构:
顺序存储;通常操作是用malloc函数申请一片连续的存储空间用来存储数据元素,它们的逻辑关系就是存储关系,所以就可以直接一个for循环按值查找找到自己想要的数据元素,不过时间复杂度就高了(O(n)),或者是通过连续的地址,直接按位查找,从而实现随机存取,这样就可以时间复杂度(O(1))查找到元素了
eg:在用malloc申请连续的存储空间时会导致生成外部碎片,可以理解为切蛋糕,切了一块特别大的会产生许多碎屑,这些碎屑就没人吃了,同理,计算机对于这些碎片也无能为力,不能分配进程,不能存储数据,但是在这个存储结构中的元素是十分紧密的,每个元素占用最少的存储空间
链式存储:此时我们可以通过指针来沟通不同物理层面上的空间,所以尽管逻辑上它们是相邻的,但是它们的物理结构不是相邻的,但这样的话我们就失去了随机存取的优势了,此时地址不是连续,而是由指针连接,这样去找元素的话无论是按值还是按位时间复杂度都是O(n)了
eg:由于我们是小刀一块块切这个蛋糕,所以就不会出现碎片现象,但是此时存储的密度不高,而且存储空间会更大,因为还要再存储个指针嘛,通过指针把逻辑连在一块,而且由于位置不同,实现不了随机存取,只能顺序存取
索引存储:我理解为顺序加链式,因为它需要建立一个索引表,建立一个虚拟的顺序结构来实现链式存储的查找,通过把关键字和地址存储为一个个索引项,此时相对于一般链式存储,我们检索的时间会更快些,但是索引表会消耗一些存储空间,一些操作也会影响到索引表
散列(Hash)存储:通过元素关键字直接计算出元素的存储地址,神中神,干什么都很快,唯一的缺点是费脑子,散列函数不好的话,找的时候元素会产生冲突,此时会产生时间和空间开销
数据运算:针对存储结构实现运算结构

二.何为算法

1,算法:算法就是解决问题的方法,它的步骤是有限的,由很多的操作组成
2.算法的重要特性:
有穷性:算法的时间和步骤都有限,不能一直无限走下去
确定性:因果对应,输入啥对应的输出都一样,不会变化
可行性:操作都可以实现
输入:可以是某个集合,比如整数集,输入对应输出
输出:通过过程与输入对应
好算法:要有正确性,就是能正确解决问题(废话,没用写它干嘛),可读性,要让人可以理解(不要这个套着那个最后形成依托),健壮性,要对非法输入有反映和处理(这没啥问题,在操作前加个判断,不符合直接return false就行),高效率和低存储(就是时间和空间,优化算法)
3.咋衡量算法的效率
用时间复杂度T(n)和空间复杂度S(n)衡量
时间复杂度:根据每个语句的频度,取其最大的数量级
eg:由于是数量级,分析中并列的语句就一体分析,找到最大的数量级,嵌套的则需要根据嵌套的语句计算其数量级的乘积
一般来说,我们根据函数的增长速率可将时间复杂度依此分类
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(2 ^n)<O(n!)<O(n ^n)
空间复杂度:算法所需要的存储空间,与存储的规模有关

三.线性表

线性表:有相同数据类型的某些数据元素的有限序列,没有数据元素时就是空表(可将其类比为稀疏的闭区间,闭区间内数字都是同一类型吧?而且闭区间的端点元素前后都没有,而区间内的元素都是前后连着的,由于前后是连着的,那么当然有顺序,而在计算机语言中,我们需要多考虑一些因素,比如说数据类型,这样它们的存储空间都是一样的,还有就是数字代表的仅仅是位序,而不是具体的内容)
eg;这是一种逻辑结构
well,well,既然是逻辑结构,那么就有它的一个八股通式(没有具体意义)
即:
InitList(&L):初始化一个空表
Length(L):求表的长度
LocateElem(L,e):按值查找,返回e的值
GetElem(L,i):按位查找,返回第i处的值
ListInsert(&L,i,e):插入元素
Listdelete(&L,i,&e):删除元素,此处取地址是为了返回删除元素的值,我们往往需要确定删除元素的值,以免删除错误
PrintList(L):输出L中所有值
Empty(L):判断表是否为空
DestroyList(&L):销毁操作
补充:在此省略了调用函数时的函数类型,取地址的话,我觉得需要取地址的地方一是需要返回值,二是需要调用在某个函数中,此时需要把线性表部分遍历,且还有其他操作,或者说可以在一种方式下遍历多种类型,所以取不同的地址,初始化一般要用malloc申请连续的空间,销毁一般用free释放空间,具体函数的实现方式还是不太清楚,只是这两个函数有这样的作用
ps:写这·段补充是在初期的深夜,肯定会有所纰漏和不完美,以后再优化,学习不就是不断优化完善自己的过程么

3.1线性表之顺序表实现

顺序表:顺序实现线性表,直接申请一片连续的存储空间依次存放数据元素,所以逻辑位置与物理位置对应,又由于是相同的数据类型,此时我们要来查找或者删除某个元素就是个等差数列的问题了,所以就可以实现随机存取了,有个东西可以很好的描述这种情况,那就是数组,因为数组里面存的都是一样的数据类型,而且第一位第二位这样找也都是连续的,也有首项和末项,我们可以直接通过数组的下标找到想要的位序。
eg:但是用数组实现的话需要注意,数组的首项为0,而位序从1开始计算
而实现在顺序表,我们可以有两种方式:
1 .静态分配存储空间(编写更简单,但是要提前规划好,不然容易发生数据溢出)

#define Maxsize number //最大长度,number是长度,想要多长输入多长,注意不要浪费
typedef struct{
	Elemtype data[Maxsize];  //顺序表元素,elemtype是数据类型,在这里非法
	int length;  //顺序表当前长度
}SeqList;//该结构体的意义,顺序表咯

2 .动态分配存储空间(编写会较为困难,需要用到DP操作,下面以倍增空间为例)

#define InitSize number //Initial size 初始长度,因为咱后面要动态分配长度
typedef struct{
	Elemtype *data; //DP数组的指针
	int Maxsize,length; //数组的最大容量和当前长度,此时不用作define是因为在DP过程中
}SeqList;
//下面是具体的DP操作
SeqList* creatDParray(){
	SeqList *DParray=(SeqList*)malloc(sizeof(SeqList));//通过malloc函数为DP数组创造空间
	DParray->data=(int*)malloc(sizeof(int)*InitSize);//指针指向起始数组,并用malloc创建起始时最大长度空间
	DParray->length=0;//完成以上操作后将当前长度置为0
	DParray->Maxsize=InitSize;//将最大长度置为起始最大长度
	return DParray;//返回抽象的DP数组
}
void DPsize(SeqList *DParray){//这里是倍增的函数,内存不够时调用
	DParray->Maxsize *=2;//倍增数组长度
	DParray->data=(int*)realloc(DParray->data,sizeof(int)*DParray->Maxsize);//realloc函数重置存储空间
}

eg:DP分配并不是像链表一样物理位置不连续,而是当存储空间不够时重新规划新的更大的存储空间,再将数据转移
静态分配顺序表及其基本流程:

#include <stdio.h>//引入一个库函数
#include<stdbool.h>//引入bool函数,方便判断
#define Maxsize 100//提前定义好最大长度
typedef struct{
	int data[Maxsize];//给数组置为最大长度
	int length;//定义当前的长度,方便后续操作
}SqList;//定义一个结构体,方便后续引入
void initlist(SqList *list){//初始化线性表
	list->length=0;//创表时长度置为0,引入指针表示长度
}//此为静态条件下,动态则需考虑最大长度与起始长度之间的关系,再进行分配

//插入元素
bool insert(SqList *list,int i,int element){
	if(i<0||i>list->length||list->length==Maxsize){
		return false;//插入前先判误,此时我们按照的是数组的下标,所以判断从0开始,小于0无效,大于长度也无效,满了也无效
	}
	//通过一个for循环把插入位置以及之后的位置全部后移,用for循环一遍遍把后一个数组元素置为前一个数组元素,且置完之后再减少长度,直至长度为0
	for(int j=list->length-1;j>=i;j--){
		list->data[j+1]=list->data[j];
	}
		list->data[i]=element;//放入新元素
		list->length++;//线性表长度自增,此时已排除超出define的情况
	return true;
}

//删除元素
bool delete(SqList *list,int i){
	if(i<0||i>=list->length){
		return false;//与上面插入操作类似,但不需考虑超过最大长度了,不管length在哪个位置都没关系
	}
	for(int j=i;j<list->length-1;j++){
		list->data[j]=list->data[j+1];
	}//这里的逻辑与上面插入相反,因为插入会影响部分元素往后走,而删除会影响部分元素往前走,所以插入是从当前表长开始循环,一个个往表后面挪,删除则是从删除位置开始,一个个往前挪
	list->length--;//删完之后当前长度自然要减一
	return true;
}

//查找元素 ps:有数组在,没必要找第几位,只要找具体数值
int search(SqList *list,int element){
	for(int i=0;i<list->length;i++){
	if(list->data[i]==element){
	return i; //内置一个for循环,把传入的元素一一比对,找到就返回
		}
	}
	return -1;//没找到就传-1
}

int main(){
	Sqlist list;
	initList(&list);//在主函数中声明这个抽象数据类型,引入指针list
	//通过函数执行插入操作
	insert(&list,0,1);
	insert(&list,1,2);
	insert(&list,2,3);//引入函数,声明插入位置,以及插入内容
	//检验插入是否成功
	printf("Sequence list")
		for(int i=0;i<list.length;i++){
			printf("%d",list.data[i]);
		}//通过内置for循环依次输出元素
	printf("\n");//换行
	//删除
	delete(&list,1);//回忆下为什么比插入少一个,因为不需传入参数,free掉即可
	//检验
	printf("Sequence list")
		for(int i=0;i<list.length;i++){
			printf("%d",list.data[i]);
		}
	printf("\n");
	//查找
	int i=search(&list,3);//给查找的地址赋值并判断
	if(i!=-1){
		printf("3是第%d位\n",i);
	}
	else{
		printf("其不存在\n");
	}
	return 0; 
	}

附:顺序表折半查找,用于降低时间复杂度(有序情况下,建议加上一个compare和qsort)

typedef struct{
	int data[MAX_SIZE];//数据存储的数组
	int length;//当前长度
}Seqlist;
int binary(Seqlist list,int target){
	int low=0;//第一个元素
	int high=list.length-1;//数组范围大小从0开始,减一防止溢出,最后一个元素
	int mid;//作为中间判断的元素
	while(low<=high){
		mid=(low+high)/2;//从中间开始查找
		if(list.data[mid]==target){
			return mid;
		}else if(list.data[mid]<target){//说明前半部分没有
			low=mid+1;//加一从后半部分开始,更新最低位置的坐标
		}else{
		high=mid-1;//减一从前半部分开始,同理
		}
	}
	return -1;//不存在
}

3.2线性表之链表实现

单链表:对存储空间是否连续不再要求,逻辑上实现线性通过指针来连接,为此我们需要存储数据和指针,由于不再是连续的存储单元,所以我们无法直接找到某个特定的结点(因为舍弃了数组存储),查找的时候需要直接遍历
头指针与头节点:头指针永远指向第一个结点,头节点是为了方便而引入的,加入头节点后,链表判空更加容易.而且每个有实际效用的结点都不必再特殊处理(不需要处理第一个位置),头指针为null时为一个空表

#include <stdio.h>//引入标准输入输出函数
#include <stdlib.h>//引入一些实用的函数,这里主要指malloc
typedef struct node{//构建结构体,以node引入
	int data;//数据域
	struct node* next;//指针域
}node-;
//创建头结点
 node* initlist(){
	node*head=(node*)malloc(sizeof(node));//malloc给头指针分配空间,固定用法sizeof计算地址空间
	if(head==NULL){
		printf("失败咯\n");
		exit(1);
	}
	head->next=NULL;//指针下一位置空
	return head;
}
//插入结点(头插)
void insert(node* head,int data,int position){
	node* newnode=(node*)malloc(sizeof(node));//同样,先为指针申请空间并计算地址空间
	if(newnode==NULL){//判误
	printf("没有成功插入\n");
	exit(1);
	}
	newnode->data=data;//在指针域存入数据
	node* prev=head;//令前置结点为头节点
	for(int i=0;i<position&&prev->next!=NULL;i++){
		prev=prev->next;//遍历指针直到插入位置的前一个指针
	}
	newnode->next=prev->next;//新指针的指向置为其前一个指针的指向
	prev->next=newnode;//前一个指针的指向为新指针
}
//删除结点
void delete(node* head,int position){
	node* prev=head;//令前置指针为头指针
	for(int i=0;i<position&&prev->next!=NULL;i++){
		prev=prev->next;//遍历至插入位置的前一个位置
	}
	if(prev->next==NULL){//判误
		printf("插入位置不存在\n");
		return 0;
	}
	node* temp=prev->next;//令一个中间指针为当前指针的指向
	prev->next=temp->next;//令当前结点指向此结点的下一个结点,这样该结点就置空了
	free(temp);//释放这个临时结点
}
//求表长
int length(node* head){
	int length=0;//把表长置0开始计算
	node* l=head->next;
	while(l!=NULL){//当当前指向不为头指针
		length++;//表长自增
		l=l->next;//访问结点
	}
	return length;
}
//按序查找
node *getindex(node* head,int i){
	int i=0;//将遍历位置置为第一个
	node* l=head->next;//置一个临时结点为头节点
	while(l!=NULL&&i<i){//当链表为空或超出此位置就跳出
		l=l->next;//访问每一个在限制下的结点
		i++;//在范围内就自增
	}
	return l;//传出当前的位序
}
//按值查找
node* getvalue(node* head,int data){
	node* l=head->next;
	while(l!=NULL){
		if(l->data==data){
			return l;
		}
		l=l->next;
	}
	return NULL;
}
//打印链表
void printlist(node* head){
	node*l=head->next;
	printf("链表内容");
	while(l!=NULL){
		printf("%d",l->data);
		l=l->next;
	}
	printf("\n");
}
//free内存
void free(node* head){
	node* l=head->next;
	while(l!=NULL){
		node*temp=l;
		l=l->next;
		free(temp);
	}
	free(head);
}
int main(){
	node*head=initlist();
	//插入操作
	insert(head,1,0);
	insert(head,3,1);
	insert(head,5,2);
	insert(head,7,3);
	insert(head,9,4);
	printlist(head);
	//删除操作
	delete(head,1);
	printlist(head);
	//求表长
	int length=getindex(head);
	printf("表长是:%d\n",length);
	//按序查找
	node* i=getindex(head,1);
    if (i!=NULL) {
        printf("第一个元素是: %d\n", i->data);
    }
    else {
        printf("超出范围!\n");
    }
    // 按值查找
    Node* i= getNodeByValue(head, 7);
    if (i!=NULL) {
        printf("找到了.\n");
    }
    else {
        printf("没找到.\n");
    }
}	

eg:在用头插法建立单链表时,读出的顺序与链表元素是相反的,可以实现链表逆置,实际上就是保持头节点,不断 把头节点往后面推
当然,如果要实现按序输入按序输出,则我们可以设置一个队尾指针,每次从队尾插入,从头结点开始读
尾插法:

#include <stdio.h>
#include <stdlib.h>
struct Node {//定义结构体
    int data;//数据域
    struct Node* next;//指针域
};
// 在链表尾部插入新节点
void insertAtEnd(struct Node** head_ref, int new_data) {
    // 为新节点分配内存
    struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
    struct Node* last = *head_ref; // 临时指针指向头节点
    // 设置新节点的数据
    new_node->data = new_data;
    new_node->next = NULL;
    // 如果链表为空,则使新节点成为头节点
    if (*head_ref == NULL) {
        *head_ref = new_node;
        return;
    }
    // 找到链表的最后一个节点
    while (last->next != NULL) {
        last = last->next;
    }
    // 将新节点链接到最后一个节点
    last->next = new_node;
}
// 打印链表
void printList(struct Node* node) {
    while (node != NULL) {
        printf("%d ", node->data);
        node = node->next;
    }
}
// 主函数
int main() {
    // 初始化一个空链表
    struct Node* head = NULL;
    // 向链表中插入一些节点
    insertAtEnd(&head, 1);
    insertAtEnd(&head, 2);
    insertAtEnd(&head, 3);
    insertAtEnd(&head, 4);
    // 打印链表
    printf("The linked list is: ");
    printList(head);
    return 0;
}

3.3 线性表之双链表

双链表:有两个指针,prev和next,分别指向每个的直接前驱和直接后继,可以实现双向遍历

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

// 定义双链表节点结构
typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

// 初始化双链表
Node* initializeList() {
    return NULL;
}

// 创建新节点
Node* createNode(int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    new_node->data = data;
    new_node->prev = NULL;
    new_node->next = NULL;
    return new_node;
}

// 在双链表指定位置插入节点
Node* insertAtPosition(Node* head, int position, int new_data) {
    Node* new_node = createNode(new_data);
    if (head == NULL || position <= 1) {
        new_node->next = head;
        if (head != NULL)
            head->prev = new_node;
        return new_node;
    }
    Node* current = head;
    int count = 1;
    while (count < position - 1 && current->next != NULL) {
        current = current->next;
        count++;
    }
    if (current->next != NULL) {
        new_node->next = current->next;
        current->next->prev = new_node;
    }
    current->next = new_node;
    new_node->prev = current;
    return head;
}

// 删除指定值的节点
Node* deleteNode(Node* head, int key) {
    if (head == NULL)
        return head;
    if (head->data == key) {
        Node* temp = head;
        head = head->next;
        if (head != NULL)
            head->prev = NULL;
        free(temp);
        return head;
    }
    Node* current = head;
    while (current != NULL && current->data != key)
        current = current->next;
    if (current == NULL)
        return head;
    if (current->next != NULL)
        current->next->prev = current->prev;
    if (current->prev != NULL)
        current->prev->next = current->next;
    free(current);
    return head;
}

// 遍历打印双链表
void printList(Node* head) {
    printf("双链表正向遍历: ");
    while (head != NULL) {
        printf("%d ", head->data);
        head = head->next;
    }
    printf("\n双链表反向遍历: ");
    while (head != NULL && head->prev != NULL) {
        printf("%d ", head->data);
        head = head->prev;
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();

    // 插入节点
    head = insertAtPosition(head, 1, 1); // 在头部插入节点
    head = insertAtPosition(head, 2, 3); // 在第2个位置插入节点
    head = insertAtPosition(head, 2, 2); // 在第2个位置插入节点
    head = insertAtPosition(head, 4, 4); // 在尾部插入节点

    // 打印双链表
    printf("初始双链表:\n");
    printList(head);

    // 删除节点
    head = deleteNode(head, 2); // 删除节点2
    printf("删除节点2后的双链表:\n");
    printList(head);

    return 0;
}

3.4循环链表

循环单链表:最后一个结点的指针不为NULL,而是指向头节点,从而构成一个完整的环

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

// 定义循环单链表节点结构
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 初始化循环单链表
Node* initializeList() {
    return NULL;
}

// 创建新节点
Node* createNode(int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    new_node->data = data;
    new_node->next = NULL;
    return new_node;
}

// 在循环单链表头部插入节点
Node* insertAtBeginning(Node* head, int new_data) {
    Node* new_node = createNode(new_data);
    if (head == NULL) {
        new_node->next = new_node; // 创建循环
        return new_node;
    }
    Node* last = head;
    while (last->next != head)
        last = last->next;
    new_node->next = head;
    last->next = new_node;
    return new_node;
}

// 在循环单链表尾部插入节点
Node* insertAtEnd(Node* head, int new_data) {
    Node* new_node = createNode(new_data);
    if (head == NULL) {
        new_node->next = new_node; // 创建循环
        return new_node;
    }
    Node* last = head;
    while (last->next != head)
        last = last->next;
    last->next = new_node;
    new_node->next = head;
    return head;
}

// 删除指定值的节点
Node* deleteNode(Node* head, int key) {
    if (head == NULL)
        return head;
    Node* temp = head, *prev;
    while (temp->data != key) {
        if (temp->next == head) // 如果节点是循环的最后一个节点
            return head;
        prev = temp;
        temp = temp->next;
    }
    if (temp == head) { // 如果是头节点
        prev = head;
        while (prev->next != head)
            prev = prev->next;
        prev->next = temp->next;
        head = temp->next;
    }
    prev->next = temp->next;
    free(temp);
    return head;
}

// 遍历打印循环单链表
void printList(Node* head) {
    Node* temp = head;
    if (head != NULL) {
        do {
            printf("%d ", temp->data);
            temp = temp->next;
        } while (temp != head);
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();

    // 插入节点
    head = insertAtEnd(head, 1);
    head = insertAtEnd(head, 2);
    head = insertAtEnd(head, 3);
    head = insertAtBeginning(head, 0);
    head = insertAtEnd(head, 4);

    // 打印循环单链表
    printf("初始循环单链表:\n");
    printList(head);

    // 删除节点
    head = deleteNode(head, 2); // 删除节点2
    printf("删除节点2后的循环单链表:\n");
    printList(head);

    return 0;
}

循环双链表:头节点的前指针指向表尾,当循环双链表为空表时,头结点的前后域都是它自己

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

// 定义循环双链表节点结构
typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

// 初始化循环双链表
Node* initializeList() {
    return NULL;
}

// 创建新节点
Node* createNode(int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    new_node->data = data;
    new_node->prev = NULL;
    new_node->next = NULL;
    return new_node;
}

// 在循环双链表头部插入节点
Node* insertAtBeginning(Node* head, int new_data) {
    Node* new_node = createNode(new_data);
    if (head == NULL) {
        new_node->next = new_node; // 创建循环
        new_node->prev = new_node;
        return new_node;
    }
    Node* last = head->prev;
    new_node->next = head;
    head->prev = new_node;
    new_node->prev = last;
    last->next = new_node;
    return new_node;
}

// 在循环双链表尾部插入节点
Node* insertAtEnd(Node* head, int new_data) {
    Node* new_node = createNode(new_data);
    if (head == NULL) {
        new_node->next = new_node; // 创建循环
        new_node->prev = new_node;
        return new_node;
    }
    Node* last = head->prev;
    new_node->next = head;
    head->prev = new_node;
    new_node->prev = last;
    last->next = new_node;
    return head;
}

// 删除指定值的节点
Node* deleteNode(Node* head, int key) {
    if (head == NULL)
        return head;
    Node* temp = head;
    do {
        if (temp->data == key) {
            if (temp->next == temp) { // 只有一个节点
                free(temp);
                return NULL;
            }
            if (temp == head) // 删除头节点
                head = temp->next;
            temp->prev->next = temp->next;
            temp->next->prev = temp->prev;
            if (temp == head) // 如果删除的是头节点,则更新头指针
                head = temp->next;
            free(temp);
            return head;
        }
        temp = temp->next;
    } while (temp != head);
    printf("未找到值为 %d 的节点\n", key);
    return head;
}

// 遍历打印循环双链表
void printList(Node* head) {
    Node* temp = head;
    if (head != NULL) {
        do {
            printf("%d ", temp->data);
            temp = temp->next;
        } while (temp != head);
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();

    // 插入节点
    head = insertAtEnd(head, 1);
    head = insertAtEnd(head, 2);
    head = insertAtEnd(head, 3);
    head = insertAtBeginning(head, 0);
    head = insertAtEnd(head, 4);

    // 打印循环双链表
    printf("初始循环双链表:\n");
    printList(head);

    // 删除节点
    head = deleteNode(head, 2); // 删除节点2
    printf("删除节点2后的循环双链表:\n");
    printList(head);

    return 0;
}

3.5静态链表

静态链表:用数组描述线性表,但此时的指针域代表的是数组下标(游标),也要提前分配一块连续的存储空间,相当于在顺序表中加入了指针,这样就不要移动元素,可以通过指针直接修改。

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

#define MAX_SIZE 100

// 静态链表结点定义
typedef struct Node {
    int data;
    int next; // 指向下一个结点的索引
} Node;

// 静态链表结构体
typedef struct {
    Node nodes[MAX_SIZE]; // 静态链表的存储空间
    int head; // 静态链表的头指针
    int length; // 静态链表的长度
} StaticLinkedList;

// 初始化静态链表
void initStaticLinkedList(StaticLinkedList *list) {
    list->head = -1; // 头指针初始化为-1
    list->length = 0; // 链表长度初始化为0
}

// 插入元素
void insertElement(StaticLinkedList *list, int data) {
    if (list->length >= MAX_SIZE) {
        printf("Static linked list is full\n");
        return;
    }

    int index = list->length; // 新结点的索引
    list->nodes[index].data = data;
    list->nodes[index].next = list->head; // 新结点指向原头结点
    list->head = index; // 更新头指针
    list->length++; // 链表长度加1
}

// 删除元素
void deleteElement(StaticLinkedList *list, int data) {
    int prev = -1; // 前驱结点的索引
    int current = list->head; // 当前结点的索引

    while (current != -1) {
        if (list->nodes[current].data == data) {
            // 找到了要删除的结点
            if (prev == -1) {
                // 要删除的是头结点
                list->head = list->nodes[current].next;
            } else {
                // 要删除的不是头结点
                list->nodes[prev].next = list->nodes[current].next;
            }
            list->length--; // 链表长度减1
            return;
        }
        prev = current;
        current = list->nodes[current].next;
    }

    printf("Element not found in static linked list\n");
}

// 打印链表
void printLinkedList(StaticLinkedList *list) {
    printf("Static Linked List: ");
    int current = list->head;

    while (current != -1) {
        printf("%d ", list->nodes[current].data);
        current = list->nodes[current].next;
    }
    printf("\n");
}

int main() {
    StaticLinkedList list;
    initStaticLinkedList(&list);

    insertElement(&list, 5);
    insertElement(&list, 10);
    insertElement(&list, 15);
    insertElement(&list, 20);
    insertElement(&list, 25);

    printLinkedList(&list);

    deleteElement(&list, 10);
    deleteElement(&list, 15);

    printLinkedList(&list);

    return 0;
}

3.6顺序表和链表的区别

1.读取:
顺序表可以顺序存取,也可以随机存取(因为它是通过数组存储的,可以直接访问数组的下标,找到相应的元素,时间复杂度为O(1)).
链表只能从头开始读取(时间复杂度为O(n)).
2.逻辑结构和物理结构:
顺序存储分配了连续的存储空间,逻辑与物理位置相对应.
链表通过指针连接,两者不一定对应.
3.增删改查:
查找:在乱序条件下两者的按值查找都一样,但是顺序表可以在顺序情况下对数组进行折半查找,而链表因为存储结构无法进行随机存取,所以不可折半查找,顺序表也可按序查找(数组)。
增,删,改:由于链表通过指针连接,操作较顺序表方便,仅需改变指针即可,而顺序表则需要移动大量元素。
4.空间分配;顺序表是分配一片连续的存储空间,而链表只需要调用函数分配即可。

四.栈

栈(stack):仅允许在一段进行插入,删除的线性表(就像手枪弹夹一样)
栈顶(top):允许插入删除的一端
栈底(bottom):不允许变动的一端
空栈:无元素的空表
特性:后进先出(last in first out)
数学特性:当n个不同元素进栈时,出栈的排列个数为1/(n+1)*Cn 2n
基本操作:
InitStack(&S):初始化一个空栈S
StackEmpty(S):判断一个栈是否为空,返回一个bool
Push(&S,x):进栈,若栈S未满,加入x使之成为新栈顶
Pop(&S,&x):出栈,若栈顶非空,则把x丢出,返回x元素
GetTop(S,&x):读取栈顶元素,返回x元素
DestroyStack(&S):销毁栈,释放其存储空间

4.1顺序栈实现

顺序栈:使用顺序存储栈,和顺序表不同的是,这里设置了一个指针来表示当前栈的位置,而不是设置一个长度变量,和顺序表一样申请了一片连续的存储空间

#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE 100

typedef struct{//定义一个结构体
	int data[MAX_SIZE];//最大长度
	int top;//栈顶指针
}stack;
//初始化栈
void initstack(stack *stack){
	stack->top=-1;//初始化栈顶指针为-1
}
//入栈
bool push(stack *stack,int value){//引入栈和压入的数字
	if(stack->top==MAX_SIZE-1){//栈满报错
		return false;
	}
	stack->data[++(stack->top)]=value;//往栈里压入一位
	return true;
}
//出栈
bool pop(stack *stack,int *value){
	if(stack->top==-1){
		return false;//栈空报错
	}
	*value=stack->data[(stack->top)--];//出栈自减
	return true;
}
//取栈顶元素
bool peek(stack *stack,int *value){
	if(stack->top==-1{
		return false;//栈空报错
	}
	*value=stack->data[(stack->top)];
	return true;
}
bool isempty(stack *stack){
	return stack->top==-1;
}
int main(){
	stack stack;
	initstack(&s);
	push(&stack,1);
	push(&stack,2);
	push(&stack,3);
	int pop1;
	pop(&s,&pop1);
	int pop2;
	peek(&stack,&pop2);
	return 0;
}

eg:注意初始设置的指针,不同情况下有不同的效果,栈的操作在数组中即可完成,入栈时考虑数组长度,防止上溢现象

4.2共享栈实现

共享栈:为了充分利用数组空间,可以设置两个指针,分别在数组的两边存入数据,即一个数组空间存入两个栈,一边栈顶为0,一边栈顶为数组最大值2

#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100;//定义栈的最大容量
typedef struct{
	int data[MAX_SIZE];//用数组存储栈
	int top1;//栈1栈顶指针
	int top2;//栈2栈顶指针
}sharestack;
//初始化共享栈
void initstack(sharestack *s){
	s->top1=-1;//初始化栈1的栈顶指针
	s->top2=MAX_SIZE;//初始化栈2栈顶指针
}
//入栈
void push(sharestack *s,int num,int data){//需要引入指针,共享栈的序号,插入的数字
	if(s->top1+1==s->top2){//先考虑栈满状态
		printf("栈满\n");
		return;
	}
	if(num==1){//栈1入栈
		s->data[++s->top1]=data;
	}
	else if(num==2){//栈2入栈
		s->data[--s->top2]=data;
	}
	else{
		printf("无效栈");
	}
}
//出栈
int pop(sharestack *s,int num){
//首先判断栈空情况
	if(num==1&&s->top1==-1){
		printf("栈1为空\n");
		return -1;
	}
	else if(num==2&&s->top2==MAX_SIZE){
		printf("栈2为空\n");
		return -1;
	}
	//栈1出栈
	if(num==1){
		return s->data[s->top1--];
	}
	else if(num==2){
		return s->data[s->top2++];	
	}
	else{
		printf("无效栈");
		return -1'
	}
}
int main(){
	sharestack s;
	initstack(&s);
	push(&s,1,10);
	push(&s,2,10);
	pop(&s,1);
	pop(&s,2);
	return 0;
}

eg:共享栈就相当于在两头进行栈的操作,节约了内存空间

4.3栈的链式存储实现

链栈:和线性表链式存储类似,不会出现溢出现象,但是所有操作都要在表头实现

#include <stdio.h>
#include <stdlib.h>
typedef struct node{//定义链栈结点类型
	int data;//数据域
	struct node *next;//指针域,指向下一个结点的指针
}node;
typedef struct{//定义链栈类型
	node* top;//定义栈顶指针
}stack;
//初始化链栈
stack* initstack(){
	stack* stack=(stack*)malloc(sizeof(stack));//为栈分配内存空间
	if(stack=NULL){//栈空则分配失败
		printf("内存分配失败\n");
		exit(1);
	}
	stack->top=NULL;//初始栈顶指针为空
	return stack;
}
//链栈判空
int isempty(stack *stack){
	return stack->top==NULL;//栈顶指针为空则返回
}
//入栈
void push(stack *stack,int data){
	node* newnode=(node*)malloc(sizeof(node));//给新结点分配内存
	if(newnode==NULL){
		printf("分配失败\n");
		exit(1);
	}
	newnode->data=data;//填入新结点数据
	newnode->next=stack->top;//新节点指向栈顶结点
	stack->top=newnode;//更新栈顶
}
//出栈
int pop(stack* stack){
	if(isempty(stack)){//判断栈空情况
		printf("栈空\n");
		exit(1);
	}
	node *temp=stack->top;//创建临时结点存储栈顶结点
	int head=temp->data;//保存当前栈顶数据
	stack->top=temp->next;//更新栈顶指针为栈顶的下一位
	free(temp);//释放原栈顶内存
	return head;//返回输出的栈
}
//查看栈顶
int peek(stack* stack){
	if(isempty(stack)){
		printf("栈为空\n");
		exit(1);
	}
	return stack->top->data;//返回栈顶元素
}
int main(){
	stack* stack=initstack();//初始化
	push(stack,10);//入栈
	push(stack,20);
	push(stack,30);
	pop(stack);//出栈
	free(stack);//释放内存
	return 0;
}

五.队列

队列:队列也是一种特殊的线性表,仅允许在表的一端进行插入,另一端进行删除,和排队类似,先进先出,First in first out.
队头(front):删除的一端.
队尾(rear):插入的一端.
空队列:什么都不包含的空表
InitQueue(&Q):初始化队列,构建一个空队列
QueueEmpty(Q):判断队列是否为空
EnQueue(&Q,x):入队,若队列未满,则加入使之称为新队尾
DeQueue(&Q,x):出队,队列非空,则退出队头元素并返回x
GetHead(Q,&x):读取队头,非空则赋值给x

5.1顺序队列实现/取模实现逻辑循环

顺序队列:和共享栈类似,具体操作有差别,顺序类的表往往都要申请一片连续的存储空间,队列也一样,然后再附上两个指针来执行队头队尾的操作,队头指针往往指向队头元素,而队尾指针可以指向队尾元素或者队尾元素的下一个位置
逻辑循环:在一些操作中很容易发生假溢出,而通过取模运算来找范围则可以避免这种溢出现象,让队尾指针溢满不了。
eg:
队首指针进一:q.front=(q,front+1)%MAXSIZE
队尾指针进一:q.rear=(q,rear+1)%MAXSIZE
队列长度:(q.rear+MAXSIZE-q.front)%MAXSIZE

#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100;
typedef struct{//定义队列结构体
	int data[MAX_SIZE];//定义最大数组
	int front;//队头指针
	int rear;//队尾指针2
}queue;
//初始化队列
void initqueue(queue *q){
	q->front=0;//初始头尾指针均置为0
	q->rear=0;
}
//队列判空
int isempty(queue *q){
	return q->front==q->rear;//头尾指针重合则为空
}
//队列判满
int isfull(queue *q){
	return (q->rear+1)%MAX_SIZE==q->front;
	//此时不可使用q.rear=MAX_SIZE,此时有可能为假溢出,即队列中出了数据,而队尾已满,通过取余来实现逻辑上的循环
}
//入队
void enqueue(queue *q,int value){
	if(isfull(q)){
		printf("队满,无法再入队\n");
		return;
	}
	q->data[q->rear]=value;//给尾指针赋值
	q->rear=(q->rear+1)%MAX_SIZE;//取余构建循环,使范围在数组范围内
}
//出队
int dequeue(queue *q){
	if(isempty(q)){
		printf("队列为空,无法再出队\n");
		exit(1);
	}
	int value=q->data[q->front];//出队同理
	q->front=(q->front+1)%MAX_SIZE;
	return value;
}
//读取队头元素
int front(queue *q){
	if(isempty){
		printf("队列为空,无队头\n");
		exit(1);
	}
	return q->data[q->front];
}
//读取队尾元素
int rear(queue *q){
	if(isempty(q)){
		printf("队列为空,无队尾\n");
		exit(1);
	}
	return q->data[(q->rear-1+MAX_SIZE)%MAX_SIZE];
}
int main(){
	queue q;
	initqueue(&q);
	enqueue(&q,10);
	dequeue(&q);
	return 0;
}

区分队空/队满的方法:
1)牺牲一个存储单元来区分队满和队空,入队少进一个队列单元,队满时队头指针在队尾指针的下一个位置,即逻辑循环
队满:(q.rear+1)%MAXSIZE==q.front
队空:q.front=q.rear
队列长度:(q.rear+MAXSIZE-q.front)%MAXSIZE
2)设置一个size表示元素个数,删除成功size减一,插入成功size加一,最后判断队满还是队空时用q.size=0和q.size=MAXSIZE来判断,两种的指针均为q.rear=q.front
3)设置一个tag,删除成功置tag为0,插入成功置tag为1,通过最后的tag数字来判断队空还是队满

5.2链式队列实现

链队列:即包含一个头指针和尾指针的单链表,当头尾指针均指向NULL时队列为空

#include <stdio.h>
#include <stdlib.h>
typedef struct node{//定义队列结点结构
	int data;
	struct node* next;
}node;
typedef struct{//定义链队列结构
	node *front,*rear;
}queue;
void initqueue(queue *q){
	q->front=q->rear=(node*)malloc(sizeof(node));//创建头节点
	if(q->front==NULL){
		printf("内存分配有误\n");
		exit(1);
	}
	q->front->next=NULL;//头结点下一个为空,表示队列为空
}
//队列判空
int isempty(queue *q){
	return q->front==q->rear;
}
//入队
void enqueue(queue *q,int value){
	node *newnode=(node*)malloc(sizeof(node));//创建新结点
	if(newnode==NULL){
		printf("分配失败\n");
		return;
	}
	newnode->data=value;//传入数值
	newnode->next=NULL;//尾插法,下一个为NULL
	q->rear->next=newnode;//新节点接到队列尾部
	q->rear=newnode;//更新队尾指针
}
//出队
int dequeue(queue *q){
	if(isempty(q)){
		printf("队空无法出队\n");
		return -1;
	}else{
		int value=q->front->next->data;//获取头部节点数据
		node *temp=q->front->next;//设置中间节点保存头节点
		q->front->next=q->front->next->next;//把头指针移到下一个节点
		free(temp);//释放内存
		if(q->front->next==NULL){
			q->rear=q->front;//更新尾指针为头节点
		}
		return value;//返回出队元素的数值
	}
}
// 打印队列中的元素
void display(Queue *q) {
    if (isEmpty(q)) { // 如果队列为空
        printf("队列为空\n");
    } else { // 如果队列不为空
        printf("队列中的元素为:");
        Node *current = q->front->next; // 从队列头部开始遍历
        while (current != NULL) { // 遍历到队列尾部
            printf("%d ", current->data); // 打印当前节点的数据
            current = current->next; // 移动到下一个节点
        }
        printf("\n");
    }
}

// 清空队列
void clearQueue(Queue *q) {
    Node *current = q->front->next;
    while (current != NULL) {
        Node *temp = current;
        current = current->next;
        free(temp);
    }
    q->rear = q->front;
}

int main() {
    Queue q;
    initQueue(&q); // 初始化队列

    enqueue(&q, 10); // 入队操作
    enqueue(&q, 20);
    display(&q); // 打印队列中的元素

    int dequeuedValue = dequeue(&q); // 出队操作
    if (dequeuedValue != -1) { // 如果出队成功
        printf("出队的元素为:%d\n", dequeuedValue); // 打印出队的元素
        display(&q); // 打印更新后的队列
    }

    clearQueue(&q); // 清空队列
    display(&q); // 打印队列中的元素

    return 0;
}

5.3双端队列

双端队列:两端均可进行插入删除的线性表,即可两端入队出队

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

// 定义双端队列节点结构
typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

// 定义双端队列结构
typedef struct {
    Node *front, *rear;
} Deque;

// 初始化双端队列
void initDeque(Deque *dq) {
    dq->front = dq->rear = NULL;
}

// 判断双端队列是否为空
int isEmpty(Deque *dq) {
    return dq->front == NULL;
}

// 在队头插入元素
void insertFront(Deque *dq, int value) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败,无法插入元素\n");
        return;
    }
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = dq->front;
    if (isEmpty(dq)) {
        dq->rear = newNode;
    } else {
        dq->front->prev = newNode;
    }
    dq->front = newNode;
}

// 在队尾插入元素
void insertRear(Deque *dq, int value) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败,无法插入元素\n");
        return;
    }
    newNode->data = value;
    newNode->prev = dq->rear;
    newNode->next = NULL;
    if (isEmpty(dq)) {
        dq->front = newNode;
    } else {
        dq->rear->next = newNode;
    }
    dq->rear = newNode;
}

// 从队头删除元素
int deleteFront(Deque *dq) {
    if (isEmpty(dq)) {
        printf("双端队列为空,无法删除元素\n");
        return -1;
    }
    int value = dq->front->data;
    Node *temp = dq->front;
    dq->front = dq->front->next;
    if (dq->front == NULL) {
        dq->rear = NULL;
    } else {
        dq->front->prev = NULL;
    }
    free(temp);
    return value;
}

// 从队尾删除元素
int deleteRear(Deque *dq) {
    if (isEmpty(dq)) {
        printf("双端队列为空,无法删除元素\n");
        return -1;
    }
    int value = dq->rear->data;
    Node *temp = dq->rear;
    dq->rear = dq->rear->prev;
    if (dq->rear == NULL) {
        dq->front = NULL;
    } else {
        dq->rear->next = NULL;
    }
    free(temp);
    return value;
}

// 获取队头元素
int getFront(Deque *dq) {
    if (isEmpty(dq)) {
        printf("双端队列为空\n");
        return -1;
    }
    return dq->front->data;
}

// 获取队尾元素
int getRear(Deque *dq) {
    if (isEmpty(dq)) {
        printf("双端队列为空\n");
        return -1;
    }
    return dq->rear->data;
}

int main() {
    Deque dq;
    initDeque(&dq); // 初始化双端队列

    insertFront(&dq, 10); // 在队头插入元素
    insertRear(&dq, 20); // 在队尾插入元素

    printf("队头元素:%d\n", getFront(&dq)); // 获取队头元素
    printf("队尾元素:%d\n", getRear(&dq)); // 获取队尾元素

    printf("删除队头元素:%d\n", deleteFront(&dq)); // 从队头删除元素
    printf("删除队尾元素:%d\n", deleteRear(&dq)); // 从队尾删除元素

    return 0;
}

六.串

串:串由字符组成,计算机中的非数值处理对象基本都是字符串数据,而串是由0个或多个字符组成的有限序列
子串:串中任意字符组成的子序列,字串在主串中的位置由第一个字符的位置决定
空串:没有字符的串
空格串:由空格字符组成的串
eg:与线性表类似,但操作对象不同

6.1串的存储结构

1.定长顺序存储表示:用一组地址连续的存储单元来储存串值的字符序列,即分配定长数组存储串,超出的串值则会被舍去(截断),在串结束时要加上\0,表示串结束

#define MAXLEN 255
typedef struct(
	char ch[MAXLEN];
	int length;
)string;

2.堆分配存储表示
堆分配存储依然是申请一段地址连续的存储空间,但其内存是动态分配的,(C语言中,堆是一个自由存储区,是一种特殊的树形结构,用于实现优先队列,最优先级的元素始终位于堆底)用free和malloc函数来实现动态分配,分配成功则用字符指针返回起始地址,分配失败则返回NULL,用free释放内存

typedef struct{
	char *ch;//按串长分配存储区,ch指向串的基地址
	int length;//串长
}string;

3.块链存储表示
块链存储:每个节点都可以存储一或多个字符,再将其通过链表连接起来,

6.2串的模式匹配(KMP)

1.暴力匹配模式串

void match(char *text,char *pattern){
	int l1=strlen(text);
	int l2=strlen(pattern);
	for(int i=0;i<=l1-l2;i++){//减l2是为了防止循环溢出
		for(int j=0;j<l2;j++){
			if(text[i+j]!=pattern[j]){//每一轮与字串匹配
			break;
			}
		}
	}
}

基本概念就是不断把连续的字符进行匹配比较,成功返回,不成功把主串匹配位置后移,最坏时间复杂度为O(nm),即主串和模式串都要完全遍历
2.KMP匹配
KMP:其核心匹配机制是生成一个模式串来记录前缀后缀,避免重复匹配的时间,减少回溯次数,匹配时记录移动的范围

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

// 构建部分匹配表
void PMT(char *pattern, int *pmt) {
    int m = strlen(pattern); // 获取模式串的长度
    pmt[0] = 0; // 部分匹配表的第一个元素总是0,因为一个字符的字符串没有真前缀和真后缀
    int i = 1; // 从模式串的第二个字符开始遍历
    int len = 0; // 记录当前已匹配的前缀的长度
    while (i < m) { // 循环直到遍历完整个模式串
        if (pattern[i] == pattern[len]) { // 如果当前字符和前缀的下一个字符相等
            len++; // 前缀长度加一
            pmt[i] = len; // 更新部分匹配表的值为当前前缀长度
            i++; // 继续比较下一个字符
        } else { // 如果当前字符和前缀的下一个字符不相等
            if (len != 0) { // 如果前缀长度不为0
                len = pmt[len - 1]; // 回溯到前一个前缀的长度,继续比较
            } else { // 如果前缀长度已经为0了
                pmt[i] = 0; // 更新部分匹配表的值为0
                i++; // 继续比较下一个字符
            }
        }
    }
}

// KMP 算法实现
int kmp_search(const char *text, const char *pattern) {
    int n = strlen(text); // 获取文本串的长度
    int m = strlen(pattern); // 获取模式串的长度
    int *pmt = (int *)malloc(sizeof(int) * m); // 动态分配存储部分匹配表的数组
    PMT(pattern, pmt); // 构建部分匹配表

    int i = 0; // 指向文本的指针
    int j = 0; // 指向模式串的指针

    while (i < n) { // 循环直到遍历完整个文本串
        if (pattern[j] == text[i]) { // 如果模式串和文本串对应位置的字符相等
            i++; // 文本串指针向后移动一位
            j++; // 模式串指针向后移动一位
        }

        if (j == m) { // 如果模式串指针已经到达末尾
            free(pmt); // 释放部分匹配表的内存
            return i - j; // 返回匹配的起始位置
        } else if (i < n && pattern[j] != text[i]) { // 如果模式串和文本串对应位置的字符不相等
            if (j != 0)
                j = pmt[j - 1]; // 如果不相等,则回溯到部分匹配表指定位置,继续匹配
            else
                i = i + 1; // 否则文本串指针向后移动一位,继续匹配
        }
    }
    free(pmt); // 释放部分匹配表的内存
    return -1; // 没有匹配
}

int main() {
    const char *text = "ABABABABCABAABABABABD"; // 文本串
    const char *pattern = "ABABCABAA"; // 模式串
    int match_index = kmp_search(text, pattern); // 调用 KMP 算法搜索模式串在文本串中的位置
    if (match_index == -1) {
        printf("Pattern not found in text.\n"); // 如果没有找到匹配,输出未找到
    } else {
        printf("Pattern found at index: %d\n", match_index); // 如果找到匹配,输出匹配的起始位置
    }
    return 0;
}

3.KMP优化版本
针对多个字符重复出现的情况,模式串的回溯长度还是较长,此时对模式串再用一个模式串优化,再次减少回溯时间

#include <stdio.h>
#include <string.h>

// 计算模式字符串的 next 数组
void computeNextArray(char *pat, int M, int *next) {
    int len = 0; // 上一个最长前缀后缀匹配的长度
    int i = 1;
    next[0] = 0; // next[0] 总是 0,因为单个字符没有前缀和后缀

    // 从索引为 1 的位置开始遍历模式字符串
    while (i < M) {
        // 如果当前字符和之前的最长前缀后缀匹配,则更新 next 数组并移动指针
        if (pat[i] == pat[len]) {
            len++;
            next[i] = len;
            i++;
        } else {
            // 如果当前字符和之前的最长前缀后缀不匹配,则回溯到上一个最长前缀后缀的位置
            if (len != 0) {
                len = next[len - 1];
            } else {
                // 如果之前没有匹配,则将 next[i] 设为 0,并移动指针
                next[i] = 0;
                i++;
            }
        }
    }
}

// 计算模式字符串的 nextval 数组
void computeNextvalArray(char *pat, int M, int *next, int *nextval) {
    int i = 0;
    nextval[0] = 0;

    // 从索引为 0 的位置开始遍历模式字符串
    while (i < M) {
        // 如果下一个字符和当前最长前缀后缀的下一个字符匹配,则更新 nextval 数组并移动指针
        if (pat[i + 1] == pat[nextval[i]]) {
            nextval[i + 1] = nextval[i] + 1;
            i++;
        } else if (nextval[i] != 0) {
            // 如果下一个字符和当前最长前缀后缀的下一个字符不匹配,并且当前最长前缀后缀长度不为 0,
            // 则将 nextval[i+1] 设为 next[nextval[i]-1],即回溯到之前的最长前缀后缀的位置
            nextval[i + 1] = next[nextval[i] - 1];
        } else {
            // 如果下一个字符和当前最长前缀后缀的下一个字符不匹配,并且当前最长前缀后缀长度为 0,
            // 则将 nextval[i+1] 设为 0,并移动指针
            nextval[i + 1] = 0;
            i++;
        }
    }
}

// KMP 搜索函数
void KMPSearch(char *pat, char *txt) {
    int M = strlen(pat); // 模式字符串的长度
    int N = strlen(txt); // 文本字符串的长度

    int next[M]; // 存储 next 数组的数组
    int nextval[M]; // 存储 nextval 数组的数组

    // 生成 next 数组
    computeNextArray(pat, M, next);
    // 生成 nextval 数组
    computeNextvalArray(pat, M, next, nextval);

    int i = 0, j = 0;
    while (i < N) {
        if (pat[j] == txt[i]) {
            j++;
            i++;
        }
        if (j == M) {
            printf("Pattern found at index %d\n", i - j); // 找到匹配的模式字符串
            j = next[j - 1]; // 更新 j,以便继续搜索下一个匹配
        } else if (i < N && pat[j] != txt[i]) {
            if (j != 0)
                j = nextval[j - 1]; // 使用 nextval 数组进行失配时的跳跃
            else
                i = i + 1;
        }
    }
}

int main() {
    char txt[] = "ABABDABACDABABCABAB"; // 文本字符串
    char pat[] = "ABABCABAB"; // 模式字符串

    // 使用 KMP 算法进行字符串匹配
    KMPSearch(pat, txt);

    return 0;
}

七.树

树:带n个结点的有限集,n为0则为空树,非空树有且仅有一个根结点,n>1,其余结点又可以分成若干有限集,称为子树,树是一种递归且分层的逻辑结构,除根结点外其余结点有且仅有一个前驱,所有结点都有0个或多个后继,树中的每个结点最多只和一个父节点有关系,与下层的每个孩子结点都有关
树的语言:一个结点的孩子个数称为度,最大度数称为树的度,度大于0的结点为分支结点,度为0则为叶节点,树的高度就是树的最高层数路径长度是节点间所经过边的个数
森林:把树的一些节点删了就变成森林,给森林加上节点就变成树
性质:
树的结点树=所有度+1(根结点)
度为m,具有n个结点的树的最大高度为n-m+1

7.1树的存储结构

1.双亲表示法:用一组连续的空间来存储每个结点,在每个结点中增设一个伪指针来表示双亲结点在数组中的位置,根结点置为-1,因为它没有双亲结点,其他结点指向其双亲

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

#define MAX_TREE_SIZE 100

// 定义节点结构
typedef struct PTNode {
    int data;    // 节点存储的值
    int parent;  // 父节点的索引
} PTNode;

// 定义树结构
typedef struct PTree {
    PTNode nodes[MAX_TREE_SIZE];  // 节点数组
    int n;                        // 节点数量
} PTree;

// 初始化树
void initTree(PTree *tree) {
    tree->n = 0;
}

// 添加节点
void addNode(PTree *tree, int data, int parent) {
    if (tree->n >= MAX_TREE_SIZE) {
        printf("树已满,无法添加新节点\n");
        return;
    }
    tree->nodes[tree->n].data = data;
    tree->nodes[tree->n].parent = parent;
    tree->n++;
}

// 打印树
void printTree(PTree *tree) {
    for (int i = 0; i < tree->n; i++) {
        printf("节点 %d: 数据 = %d, 父节点索引 = %d\n", i, tree->nodes[i].data, tree->nodes[i].parent);
    }
}

int main() {
    PTree tree;
    initTree(&tree);

    // 添加根节点
    addNode(&tree, 1, -1);  // 根节点没有父节点,用-1表示

    // 添加其他节点
    addNode(&tree, 2, 0);  // 节点2的父节点是节点0
    addNode(&tree, 3, 0);  // 节点3的父节点是节点0
    addNode(&tree, 4, 1);  // 节点4的父节点是节点1
    addNode(&tree, 5, 1);  // 节点5的父节点是节点1

    // 打印树
    printTree(&tree);

    return 0;
}

2.孩子表示法:把每个结点的孩子视为一个线性表,以单链表作为存储结构,则n个结点就有n个孩子链表(叶结点孩子链表为空表),再把这些头结点再组成一个线性表

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

// 树节点结构体
struct TreeNode {
    int data; // 节点数据
    struct TreeNode *firstChild; // 指向第一个子节点的指针
    struct TreeNode *nextSibling; // 指向下一个兄弟节点的指针
};

// 创建树节点
struct TreeNode* createTreeNode(int data) {
    struct TreeNode* newNode = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    if (newNode != NULL) {
        newNode->data = data;
        newNode->firstChild = NULL;
        newNode->nextSibling = NULL;
    }
    return newNode;
}

// 在树中插入子节点
void insertChild(struct TreeNode* parent, struct TreeNode* child) {
    if (parent == NULL || child == NULL) {
        return;
    }
    // 如果父节点没有子节点,则将子节点直接作为父节点的第一个子节点
    if (parent->firstChild == NULL) {
        parent->firstChild = child;
    } else {
        // 否则将子节点插入到父节点的子节点链表的末尾
        struct TreeNode* sibling = parent->firstChild;
        while (sibling->nextSibling != NULL) {
            sibling = sibling->nextSibling;
        }
        sibling->nextSibling = child;
    }
}

// 测试
int main() {
    // 创建树节点
    struct TreeNode* root = createTreeNode(1);
    struct TreeNode* child1 = createTreeNode(2);
    struct TreeNode* child2 = createTreeNode(3);
    struct TreeNode* child3 = createTreeNode(4);
    
    // 插入子节点
    insertChild(root, child1);
    insertChild(root, child2);
    insertChild(child1, child3);
    
    // 打印树结构
    printf("Root: %d\n", root->data);
    printf("Children of root: ");
    struct TreeNode* child = root->firstChild;
    while (child != NULL) {
        printf("%d ", child->data);
        child = child->nextSibling;
    }
    printf("\n");

    // 释放内存
    free(root);
    free(child1);
    free(child2);
    free(child3);
    
    return 0;
}

3.孩子兄弟表示法:以二叉链表作为树的存储结构,每个结点包括三个内容,即结点值,指向结点第一个孩子结点的指针,指向结点下一个兄弟结点的指针

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

// 树节点结构体
struct TreeNode {
    int data; // 节点数据
    struct TreeNode *firstChild; // 指向第一个子节点的指针
    struct TreeNode *nextSibling; // 指向下一个兄弟节点的指针
};

// 创建树节点
struct TreeNode* createTreeNode(int data) {
    struct TreeNode* newNode = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    if (newNode != NULL) {
        newNode->data = data;
        newNode->firstChild = NULL;
        newNode->nextSibling = NULL;
    }
    return newNode;
}

// 在树中插入子节点
void insertChild(struct TreeNode* parent, struct TreeNode* child) {
    if (parent == NULL || child == NULL) {
        return;
    }
    // 如果父节点没有子节点,则将子节点直接作为父节点的第一个子节点
    if (parent->firstChild == NULL) {
        parent->firstChild = child;
    } else {
        // 否则将子节点插入到父节点的子节点链表的末尾
        struct TreeNode* sibling = parent->firstChild;
        while (sibling->nextSibling != NULL) {
            sibling = sibling->nextSibling;
        }
        sibling->nextSibling = child;
    }
}

// 测试
int main() {
    // 创建树节点
    struct TreeNode* root = createTreeNode(1);
    struct TreeNode* child1 = createTreeNode(2);
    struct TreeNode* child2 = createTreeNode(3);
    struct TreeNode* child3 = createTreeNode(4);
    
    // 插入子节点
    insertChild(root, child1);
    insertChild(root, child2);
    insertChild(child1, child3);
    
    // 打印树结构
    printf("Root: %d\n", root->data);
    printf("Children of root: ");
    struct TreeNode* child = root->firstChild;
    while (child != NULL) {
        printf("%d ", child->data);
        child = child->nextSibling;
    }
    printf("\n");

    // 释放内存
    free(root);
    free(child1);
    free(child2);
    free(child3);
    
    return 0;
}

7.2二叉树

二叉树:每个结点最多有两个子树,且为有序树
满二叉树:高度为h,有2^h-1个结点,即每层都被填满了
完全二叉树:二叉树的填充符合顺序
二叉排序树:左边关键字小于根结点,右边关键字大于根结点
平衡二叉树:任意结点的左右子树高度之差的绝对值不超过一
正则二叉树:每个分支结点都有两个孩子,即树中仅有度为0或2的结点
性质:
1.非空二叉树的叶结点树为度为二的结点树加一
2.非空二叉树第k层最多有2^(k-1)个结点
3.高度为h的二叉树最多有2^h-1个结点

7.3二叉树遍历

遍历:二叉树是一个非线性结构,为了实现每个结点都遍历且仅有一次,需要把非线性情况转化为线性队列来遍历,遍历主要通过递归来实现,每次都要按顺序对根结点,左右子树结点进行访问
1.先序遍历:不为空则以根左右的顺序对二叉树进行访问

#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点结构
typedef struct TreeNode {
    int data;//数据据域
    struct TreeNode *left;//左指针域
    struct TreeNode *right;//右指针域
} TreeNode;
// 创建新节点
TreeNode* createNode(int data) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    if (!newNode) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 先序遍历函数
void preOrderTraversal(TreeNode* root) {
    if (root == NULL) {
        return;
    }
    printf("%d ", root->data); // 访问根节点
    preOrderTraversal(root->left); // 访问左子树
    preOrderTraversal(root->right); // 访问右子树
}

// 释放二叉树内存
void freeTree(TreeNode* root) {
    if (root == NULL) {
        return;
    }
    freeTree(root->left);
    freeTree(root->right);
    free(root);
}

// 主函数
int main() {
    // 创建示例二叉树
    //       1
    //      / \
    //     2   3
    //    / \   \
    //   4   5   6

    TreeNode* root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->right = createNode(6);

    printf("先序遍历结果: ");
    preOrderTraversal(root);
    printf("\n");

    // 释放内存
    freeTree(root);

    return 0;
}

2.中序遍历:不为空则以左根右的顺序访问

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

// 定义二叉树节点结构
typedef struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

// 创建新的二叉树节点
TreeNode* createNode(int val) {
    TreeNode *newNode = (TreeNode *)malloc(sizeof(TreeNode));
    newNode->val = val;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 中序遍历递归实现
void inorderTraversal(TreeNode *root) {
    if (root == NULL) {
        return;
    }
    inorderTraversal(root->left);   // 访问左子树
    printf("%d ", root->val);       // 访问根节点
    inorderTraversal(root->right);  // 访问右子树
}

int main() {
    // 创建二叉树
    TreeNode *root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->left = createNode(6);
    root->right->right = createNode(7);

    printf("中序遍历结果: ");
    inorderTraversal(root);
    printf("\n");

    return 0;
}

3.后序遍历:不为空则以左右根的顺序访问二叉树

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

// 定义二叉树节点结构
typedef struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

// 创建新的二叉树节点
TreeNode* createNode(int val) {
    TreeNode *newNode = (TreeNode *)malloc(sizeof(TreeNode));
    newNode->val = val;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 后序遍历递归实现
void postorderTraversal(TreeNode *root) {
    if (root == NULL) {
        return;
    }

    postorderTraversal(root->left);   // 访问左子树
    postorderTraversal(root->right);  // 访问右子树
    printf("%d ", root->val);         // 访问根节点
}

int main() {
    // 创建二叉树
    TreeNode *root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->left = createNode(6);
    root->right->right = createNode(7);

    printf("后序遍历结果: ");
    postorderTraversal(root);
    printf("\n");

    return 0;
}

eg:以上三种非递归实现,需要用栈来实现
4.层次遍历(广度优先遍历):从上至下,从左至右依次访问各结点,进行层序遍历时需要借助队列实现,先把根结点入队,非空则把队头结点出队,访问此结点,按照左右,有孩子则入队,重复直至队列为空

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

// 定义二叉树节点结构
typedef struct TreeNode {
    int val;//数据域
    struct TreeNode* left;//左指针域
    struct TreeNode* right;//右指针域
} TreeNode;

// 定义队列节点结构
typedef struct QueueNode {
    TreeNode* treeNode;//数据域
    struct QueueNode* next;//指针域
} QueueNode;

// 定义队列结构
typedef struct Queue {
    QueueNode* front;//头指针
    QueueNode* rear;//尾指针
} Queue;

// 创建新的二叉树节点
TreeNode* createTreeNode(int val) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    newNode->val = val;//根结点数据
    newNode->left = NULL;//左右置空
    newNode->right = NULL;
    return newNode;
}

// 创建新的队列节点
QueueNode* createQueueNode(TreeNode* treeNode) {
    QueueNode* newQueueNode = (QueueNode*)malloc(sizeof(QueueNode));
    newQueueNode->treeNode = treeNode;
    newQueueNode->next = NULL;
    return newQueueNode;
}

// 初始化队列
Queue* createQueue() {
    Queue* queue = (Queue*)malloc(sizeof(Queue));
    queue->front = NULL;//前后置空
    queue->rear = NULL;
    return queue;
}

// 入队
void enqueue(Queue* queue, TreeNode* treeNode) {
    QueueNode* newQueueNode = createQueueNode(treeNode);
    // 如果队列为空,新的节点即为队列的头和尾
    if (queue->rear == NULL) {
        queue->front = queue->rear = newQueueNode;
        return;
    }
    // 否则将新的节点添加到队列尾部
    queue->rear->next = newQueueNode;
    queue->rear = newQueueNode;
}

// 出队
TreeNode* dequeue(Queue* queue) {
    // 如果队列为空,返回 NULL
    if (queue->front == NULL) {
        return NULL;
    }
    // 将队列头节点出队
    QueueNode* temp = queue->front;
    queue->front = queue->front->next;
    // 如果队列变为空,重置队列尾指针
    if (queue->front == NULL) {
        queue->rear = NULL;
    }
    TreeNode* treeNode = temp->treeNode;
    free(temp);
    return treeNode;
}

// 判断队列是否为空
int isQueueEmpty(Queue* queue) {
    return queue->front == NULL;
}

// 层次遍历二叉树
void levelOrderTraversal(TreeNode* root) {
    // 如果二叉树为空,直接返回
    if (root == NULL) {
        return;
    }

    // 创建并初始化队列
    Queue* queue = createQueue();
    enqueue(queue, root);

    // 循环直到队列为空
    while (!isQueueEmpty(queue)) {
        // 出队队列头节点并处理
        TreeNode* current = dequeue(queue);
        printf("%d ", current->val);

        // 将当前节点的左子节点入队
        if (current->left != NULL) {
            enqueue(queue, current->left);
        }
        // 将当前节点的右子节点入队
        if (current->right != NULL) {
            enqueue(queue, current->right);
        }
    }
}

// 测试层次遍历
int main() {
    // 创建二叉树
    TreeNode* root = createTreeNode(1);
    root->left = createTreeNode(2);
    root->right = createTreeNode(3);
    root->left->left = createTreeNode(4);
    root->left->right = createTreeNode(5);
    root->right->left = createTreeNode(6);
    root->right->right = createTreeNode(7);

    // 输出层次遍历结果
    printf("Level Order Traversal: ");
    levelOrderTraversal(root);
    printf("\n");

    return 0;
}

eg:在进行二叉树的遍历时,我们需要先把二叉树的结点排列成一个线性序列,从而得到遍历序列

7.4二叉树的存储结构

1.顺序存储:用一组连续的存储单元(数组)自上而下,从左到右存储二叉树的结点,一般来说存储完全二叉树和满二叉树比较合适,此时即可以节省存储空间又可以反应其逻辑关系,一般的二叉树存储时应将各相关结点补上空结点,与完全二叉树对照
2.链式存储:提高空间利用率,每个结点要存储三个域,左右指针域,数据域,在含有n个结点的二叉链表中包含n+1个空链域(指针),这些空链域可以组成线索链表

7.5线索二叉树

线索二叉树:含n个结点的二叉树中有n+1个空指针,为了加快查找的速度,则需要加入相关的指针,而为了区分指针的指向,则需要加入两个标志域,来判断指针指的是前/后驱,还是左/右孩子,设置一个tag来判断指针指的是域还是孩子,无左子树就指向前驱,无右子树则指向后继
线索化:遍一次二叉树,发现空指针,改为线索
eg:以中序线索二叉树为例

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

// 定义二叉树结点结构
typedef struct ThreadedNode {
    int data; // 结点数据
    struct ThreadedNode *left; // 左孩子指针
    struct ThreadedNode *right; // 右孩子指针
    int ltag; // 左线索标志位:0表示左孩子,1表示前驱线索
    int rtag; // 右线索标志位:0表示右孩子,1表示后继线索
} ThreadedNode;

// 初始化一个新的结点
ThreadedNode* createNode(int data) {
    ThreadedNode *node = (ThreadedNode*)malloc(sizeof(ThreadedNode));
    node->data = data;
    node->left = node->right = NULL;
    node->ltag = node->rtag = 0; // 初始化标志位为0
    return node;
}

// 中序遍历建立线索
void inOrderThreading(ThreadedNode *current, ThreadedNode **pre) {
    if (current != NULL) {
        inOrderThreading(current->left, pre); // 递归左子树
        
        // 如果当前结点没有左孩子,将ltag置为1,左指针指向前驱
        if (current->left == NULL) {
            current->left = *pre;
            current->ltag = 1;
        }
        
        // 如果前驱结点没有右孩子,将其rtag置为1,右指针指向当前结点
        if (*pre != NULL && (*pre)->right == NULL) {
            (*pre)->right = current;
            (*pre)->rtag = 1;
        }
        
        *pre = current; // 更新前驱为当前结点
        inOrderThreading(current->right, pre); // 递归右子树
    }
}

// 创建中序线索二叉树
void createInOrderThreadedTree(ThreadedNode *root) {
    ThreadedNode *pre = NULL; // 前驱结点初始化为空
    inOrderThreading(root, &pre); // 建立线索
    if (pre != NULL) {
        pre->right = NULL; // 最后一个结点的右指针置为空
        pre->rtag = 1; // 最后一个结点的rtag置为1
    }
}

// 中序遍历线索二叉树
void inOrderTraversal(ThreadedNode *root) {
    ThreadedNode *current = root;
    while (current != NULL) {
        // 找到最左下的结点
        while (current->ltag == 0) {
            current = current->left;
        }
        
        // 输出当前结点
        printf("%d ", current->data);
        
        // 使用线索找后继
        while (current->rtag == 1 && current->right != NULL) {
            current = current->right;
            printf("%d ", current->data);
        }
        
        // 移动到右孩子
        current = current->right;
    }
}

// 测试函数
int main() {
    // 手动创建一个示例二叉树
    ThreadedNode *root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->left = createNode(6);
    root->right->right = createNode(7);

    // 创建中序线索二叉树
    createInOrderThreadedTree(root);

    // 中序遍历线索二叉树
    printf("In-order Traversal of Threaded Binary Tree: ");
    inOrderTraversal(root);
    printf("\n");

    return 0;
}

7.6树,森林,二叉树之间的转化

1.树转化为二叉树:左孩子右兄弟,即每个结点的左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,二叉树右子树必空,因为根结点没有兄弟
2.森林转换为二叉树:先把所有树转化为相应的二叉树,再把所有二叉树根结点按兄弟关系转化为二叉树
3.二叉树转化为森林:按照2反推,不断把右链断开

7.7哈夫曼树

从一个树中一个结点到另一个结点之间的分支称为路径
路径上的分支数目称为路径长度
给结点赋值称为权
带权路径长度:根到该点路径长度乘该点权值
树的路径长度:树中所有叶结点带权路径长度之和
记为
W P L = ∑ i = 1 n WPL=\displaystyle\sum_{i=1}^n WPL=i=1n w i w\scriptstyle i wi l i l\scriptstyle i li
w i w\scriptstyle i wi是第i个叶节点所带权值, l i l\scriptstyle i li是该叶节点到根结点的路径长度
哈夫曼树(最优二叉树):在含有n个带权叶节点的二叉树中,WPL值最小的二叉树
哈夫曼树的构造:
1)把这n个结点分别作为单独结点的二叉树,构成森林
2)把权值最小的两个结点拉出来按左右构成二叉树,二者根结点权值为两者的和
3)删除森林中构成新二叉树的结点,把新的根结点加入森林中
4)重复直至没有空结点
其特点:
1)每个初始结点最后都会变成叶结点,且权值越小的结点到根结点的路径长度越大
2)构造过程中一共新建了n-1个结点,所有其结点总数为2n-1
3)不存在度为1的结点,要么为0,要么为2
固定长度编码:对每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
eg:可变长度编码可以通过区别分配使字符的平均编码长度减短,起到压缩数据的效果
前缀编码:没有一个编码是另一个编码的前缀
哈夫曼编码:把每个字符当作一个独立的结点,把每个字符出现的次数作为权值,构建哈夫曼树,此时就可以计算出总长度最短的二进制前缀编码

八.并查集

并查集:一种集合数据类型,主要实现对集合元素的归并和查找
1.基本操作
initial(S):将集合S中的每个元素都初始化为只有一个单元素的子集合
union(s,root1,root2):把集合S中的子集合root2并入root1,要求root1和root2互不相交,否则不执行合并
find(s,x):查找S中x所在的子集合,返回该子集合的根结点
2.并查集的存储结构
使用树的双亲表示,将各个子集合以树来表示,所有子集合的树构成森林,把各集合根结点都置为负数,内部满足双亲表示法
初始时所有根结点都置为-1,之后通过一定关系,把各根结点组成一棵棵树,归并只需要改根结点的值,使其指向新的双亲
3.并查集实现

#include <stdio.h>
#define SIZE 1000//定义最大长度
int parent[SIZE]//初始化父节点数组
//初始化并查集
void init(int n){
	for(int i=0;i<n;i++){
		parent[i]=-1;//每个元素父节点初始化为-1
	}
}
//查找集合中的元素
int find(int x){
	if(parent[x]<0){
		return x;//当前元素为根结点
	}else{
		int root=find(parent[x]);//递归查找根结点
		parent[x]=root;//将当前元素父结点直接设为根结点,节约时间
		return root;
	}
}
void union_sets(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);
    if (rootX != rootY) {
        parent[rootY] = rootX;
    }
}

// 示例使用
int main() {
    int n = 10;
    init(n);

    union_sets(1, 2);
    union_sets(2, 3);
    union_sets(4, 5);
    union_sets(6, 7);
    union_sets(2, 7); // 添加一条边,使得 7 在和 1 同一集合中

    // 查找包含给定元素的集合的根节点
    printf("Root of the set containing 1: %d\n", find(1)); // 输出 1

    return 0;
}

优化方法
1.优化并,比较秩,把小树接到大树上
2.优化查,找到根结点,压缩路径
核心方法为不断减小树的高度

#include <stdio.h>

#define MAXN 1000

int parent[MAXN]; // 存储每个节点的父节点
int rank[MAXN];   // 存储每个根节点的秩

void init(int n) {
    for (int i = 0; i < n; i++) {
        parent[i] = i; // 初始化每个节点的父节点为自己
        rank[i] = 0;   // 初始化每个根节点的秩为0
    }
}

// 查找操作,返回包含给定元素的集合的根节点
int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]); // 路径压缩:将当前节点直接连接到根节点
    }
    return parent[x];
}

// 合并操作,将两个集合合并
void union_sets(int x, int y) {
    int rootX = find(x); // 找到 x 所在集合的根节点
    int rootY = find(y); // 找到 y 所在集合的根节点
    if (rootX != rootY) { // 如果 x 和 y 不在同一个集合中
        // 按秩合并
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY; // 将 x 所在集合根节点连接到 y 所在集合根节点上
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX; // 将 y 所在集合根节点连接到 x 所在集合根节点上
        } else {
            parent[rootY] = rootX; // 将 y 所在集合根节点连接到 x 所在集合根节点上
            rank[rootX]++;         // 更新 x 所在集合根节点的秩
        }
    }
}

int main() {
    int n = 10; // 假设有10个元素
    init(n);    // 初始化并查集

    // 合并一些集合
    union_sets(1, 2);
    union_sets(2, 3);
    union_sets(4, 5);
    union_sets(6, 7);
    union_sets(2, 7); // 添加一条边,使得 7 在和 1 同一集合中

    // 查找包含给定元素的集合的根节点
    printf("Root of the set containing 1: %d\n", find(1)); // 输出 1

    return 0;
}

九.图

图:图G由顶点集V和边集E组成,记为G=(V,E)
V(G)表示图G中顶点的有限非空集
E(G)表示图G中顶点之间的关系/边集合
如果集内有多个点V(v1,v2,…vn),则用|V|表示图G中顶点个数
E = E= E={(u,v)|u ∈ \in V,v ∈ \in V},|E|表示图G中边的条数
eg:图不可为空,顶点必非空,边可为空(一个点)
各种类型的图:
有向图:若边集E为有向边(弧)的有限集合,则图G为有向图
弧:顶点的有序对,记为 < v , w > <v,w> <v,w>,称为从v到w的弧,v邻接到w
v,w为顶点,v为弧尾,w为弧头
有向图中,|E|的取值范围为0到n(n-1)

无向图:若边集E为无向边的有限集合,则G为无向图
边为顶点的无序对,记为(v,w),w和v互为邻接点,边(v,w)依附于w和v
无向图中,|E|的取值范围是0到n(n-1)/2

简单图:即不存在重复边,不存在顶点到自身的边
多重图:某两个顶点之间的边数大于一条,又允许顶点通过一条边和自身关联

完全图:有n(n-1)/2条边的无向图,完全图中任意两个顶点都存在边
有向完全图:有n(n-1)条弧,任意两个顶点之间都存在方向相反的两条弧

子图:对 G = ( V , E ) G=(V,E) G=(V,E) G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E),如果顶点集和边集满足子集的关系,则 G ′ G' G G G G的子图
生成子图:对于 V ( G ′ ) = V ( G ) V(G')=V(G) V(G)=V(G)的子图 G ′ G' G,称其为G的生成子图,包含原图的所有结点
eg:V,E的任何子集不一定能构成G的子图,因为不一定满足图的定义,E子集中某些边关联的顶点不一定在此V的子集中

连通:无向图中,顶点之间有路径存在,则称这两个顶点是连通的
连通图:若图G中任意两个顶点都是连通的,则其为连通图,反之为非连通图
连通分量:无向图中的极大连通子图
eg:若一个图有n个顶点,边数小于n-1,则此图必为非连通图

强连通:在有向图中,有一对顶点来回都有路径,则称这两个顶点强连通
强连通图:图中任意一对顶点都是强连通的
强连通分量:有向图中的极大强连通子图
eg:若一个有向图有n个顶点,若它是强连通图,则最少要n条边构成一个环路

生成树:给定无向图的一个子图,包含原图的所有顶点,且是一个无环连通图,生成树连接了图中所有结点但没有环路,且边的数量为节点数-1
连通图的生成树是包含图中所有结点的一个极小连通子图,若图中顶点数为n,则它的生成树有n-1条边
包含图中全部顶点的极小连通子图,只有生成树满足这个极小条件,砍去生成树的一条边则变成非连通图,加上一条边则会形成一个回路
eg;边尽可能少,同时保证连通

顶点的度,入度,出度
无向图:顶点v的度是指依附于顶点v的边的条数,记为TD(v)
eg:无向图的全部顶点的度之和为边数的两倍,因为每条边和两个顶点相关联
有向图:顶点v的度分为出度和入度,入度是以顶点v为终点的有向边的数目,记为ID(v),出度是以顶点v为起点的有向边的数目,记为OD(v),TD(v)=ID(v)+OD(v),有向图的全部顶点的入度之和等于出度之和,且等于边数,因为每条有向边都有一个起点和终点

边的权和网
权值:图中的边标上的数值
网(带权图):边上带有权值的图

稠密图与稀疏图
稀疏图是边数很少的图,反之为稠密图,当图G满足|E|<|V|log|V|时,G可视为稀疏图

路径,路径长度,回路
路径:两个顶点之间经过的顶点序列,组成要素也包括相关联的边
路径长度:路径上边的数目
回路:第一个顶点和最后一个顶点相同的路径
eg:图中有n个顶点,且有大于n-1的边,则此图一定有环

简单路径,简单回路
简单路径:顶点不重复出现的路径
简单回路:除了第一个顶点和最后一个顶点外,其他顶点不重复出现的回路

距离
距离:两个顶点之间的最短路径长度
如果两个顶点内不存在路径,则改距离记为无穷

有向树
一个顶点入度为0,其他顶点入度都是1的有向图

图的基本操作
Adjacent(G,x,y):判断图是否存在边<x,y>,(x,y)
Neighbors(G,x):列出图中与结点x邻接的边
InsertVertex(G,x):在图中插入顶点x
DeleteVertex(G,x):在图中删除顶点x
AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则在图中添加该边
RemoveEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则从图中删除
FirstNeighbor(G,x):求图G中顶点的第一个邻接点,有则返回顶点号,没有相关信息则返回-1
NextNeighbor(G,x,y):假设图G中顶点y是x的邻接点,则返回除y以外顶点x的下一个邻接点的顶点号,如果y为x的最后一个邻接点,则返回-1
Get_edge_value(G,x,y):获取图中边对应的权值
Set_edge_value(G,x,y,v):设置图中对应权值

9.1图的邻接矩阵存储法

邻接矩阵法:对应线性结构中顺序存储,用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(各顶点间的邻接关系),此二维数组称为邻接矩阵
顶点数为n的图G=(V,E)的邻接矩阵是n*n类型的
对于G中的点,有:
A [ i ] ] j ] = { 1 , if  ( v i , v j ) 是 E ( G ) 中的边 0 , if  ( v i , v j ) 不是 E ( G ) 中的边 A[i]]j] = \begin{cases} 1, &\text{if } (vi,vj) 是E(G)中的边\\ 0, &\text{if } (vi,vj) 不是E(G)中的边 \end{cases} A[i]]j]={1,0,if (vi,vj)E(G)中的边if (vi,vj)不是E(G)中的边 (无论是否为有无向图)
如果是带权图的情况,则邻接矩阵中有边则存入边的权值,无边则存入0或 ∞ \infty
eg:无向图的邻接矩阵是对称矩阵,对规模大的邻接矩阵可以使用压缩存储.此法的空间复杂度为O(n^2),n为图的顶点数
特点:
1.无向图邻接矩阵必为唯一的对称矩阵,所以在实际存储邻接矩阵时只需要存储上/下三角矩阵的元素
2.无向图中,邻接矩阵的第i行/列的非0元素的个数正好是顶点i的度TD(vi),因为只有相关边才有数据
3.对于有向图,邻接矩阵的第i行非0元素的个数正好是顶点i的出度OD(vi),第i列非0元素的个数正好是顶点i的出度ID(vi)
4,邻接矩阵存储图,易于确定顶点间是否有边相连接,但是要确定有多少条边,则必须按行/列对每个元素进行检测,时间复杂度高
5.该方法适合存储稠密图
eg:

#include <stdio.h>
#include <stdlib.h>
//定义图的最大顶点数
#define MAX 100
//定义图的邻接矩阵结构体
typedef struct{
	int vertices;//图中顶点数量
	int edges[MAX][MAX];//邻接矩阵
}Graph;
//创造图的函数
Graph* creat(int vertices){
	Graph* graph=(Graph*)malloc(sizeof(Graph));//malloc函数分配内存空间
	graph->vertices=vertices;//存入顶点数量
	for(int i=0;i<vertices;i++){//初始化邻接矩阵,所有边的权值初始化为0
		for(int j=0;j<vertices;j++){
			graph->edges[i][i]=0;
		}
	}
	return graph
}
//添加边的函数
void add(Graph* graph,int src,int dest,int weight){
	//在邻接矩阵中给边赋权值
	graph->edges[dest][src]=weight;
	graph->edges[src][dest]=weight;
	//如果是无向图,需要将对称位置也设置
}
// 打印邻接矩阵的函数
void printGraph(Graph* graph) {
    printf("Adjacency Matrix:\n");
    for (int i = 0; i < graph->vertices; i++) {
        for (int j = 0; j < graph->vertices; j++) {
            printf("%d ", graph->edges[i][j]);
        }
        printf("\n");
    }
}
int main() {
    int vertices = 4; // 图中顶点的数量
    Graph* graph = createGraph(vertices);

    // 添加边
    addEdge(graph, 0, 1, 10);
    addEdge(graph, 0, 2, 6);
    addEdge(graph, 0, 3, 5);
    addEdge(graph, 1, 3, 15);
    addEdge(graph, 2, 3, 4);

    // 打印邻接矩阵
    printGraph(graph);

    return 0;
}

9.2图的邻接表法

邻接表法:结合了顺序存储和链式存储,对图G中每个结点v创建一个单链表,每个单链表中存入与单链表相关联的边(有向图则是以顶点v为尾的弧),此单链表称为顶点v的边表,而所有边表的头指针和顶点的数据信息用顺序存储,称为顶点表
顶点表结点:由两个域组成,数据域(data)存储顶点v的信息,边表头指针域(firstarc)存入指向第一条边的边表结点
边表结点:至少由两个域组成,邻接点域(adjvex)存放与头节点顶点v邻接的顶点编号,指针域(nextarc)存放指向下一条边的边表结点
特点:
1.若G为无向图,则其存储空间为O(|V|+2|E|),因为此时邻接表中每条边都出现了2次,若为有向图,则其存储空间为O(|V|+|E|)
2.该方法适合存储稀疏图
3.在邻接表中,通过读取邻接表,可以很快读取一个结点相关的邻边,而确定顶点之间是否存在顶点则相对较慢,因为邻接表要通过结点对应的边表确定另一结点
4.对于无向图求其顶点的度仅需计算其相关边表结点的个数,对于有向图求出度可以计算相关边表的结点个数,但是求某个结点的入度则需要遍历整个邻接表
5.邻接表表示并不唯一,因为连接次序可以任意,随算法而定
eg:

#include <stdio.h>
#include <stdlib.h>
#define MAX 100//定义图中最大顶点数
//定义邻接表中的链表结点结构
typedef struct node{
	int vertex;//顶点编号
	struct node* next;//指向下一个结点的指针
}node;
//定义图结构
typedef struct{
	int numvertices;//图中顶点数量
	node* adjlist[MAX];//邻接表数组,每个元素指向一个链表的头节点
}Graph;
// 创建新的节点
Node* createNode(int v) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

// 创建一个空图
Graph* createGraph(int vertices) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->numVertices = vertices;

    // 初始化邻接表中的每个链表头节点为NULL
    for (int i = 0; i < vertices; i++) {
        graph->adjLists[i] = NULL;
    }

    return graph;
}

// 添加边到图中
void addEdge(Graph* graph, int src, int dest) {
    // 添加从src到dest的边
    Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;

    // 无向图,所以也要添加从dest到src的边
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

// 打印邻接表表示的图
void printGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        Node* temp = graph->adjLists[i];
        printf("顶点 %d 的邻居: ", i);
        while (temp) {
            printf("%d -> ", temp->vertex);
            temp = temp->next;
        }
        printf("NULL\n");
    }
}

// 主函数
int main() {
    int vertices = 5; // 假设图有5个顶点

    Graph* graph = createGraph(vertices); // 创建一个包含5个顶点的图

    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    printGraph(graph); // 打印图的邻接表表示

    return 0;
}

9.3十字链表

十字链表:有向图的一种链式存储方式,包括两个结点,弧结点,顶点结点
弧结点:包括五个域,tailvex和headvex两个域分别表示弧尾和弧头两个顶点的编号,头链域hlink指向弧头相同的下一个弧结点,尾链域tlink指向弧尾相同的下一个弧结点,info存放相关信息
eg;此时可实现弧头和弧尾相同的弧都在一个链表中
顶点结点:包括三个域,data域存放相关信息,firstin域指向以该顶点为弧头的第一个弧结点,firstout域指向以该顶点为弧尾的第一个弧结点
eg:顶点之间是顺序存储的
十字链表易于找到以vi为头/尾的弧,所有容易找到顶点的出度和入度
十字链表表示不唯一,但对应的图唯一

9.4邻接多重表

邻接多重表:无向图的一种存储方式也是使用两个结点存储表的相关信息
边结点:ivex和jvex两个域指向依附于该边的两个顶点的编号,ilink指向下一条依附于顶点ivex的边,jlink指向下一条依附于顶点jvex的边,info存放信息
即两个域存顶点,两个域存上/下一位的指向,一个域存信息
顶点结点:由两个域组成,data存放数据,firstedge存放第一个依附于该结点的边
eg:在此表中,所有依附于同一顶点的边串联在同一链表中,每条边依附于两个顶点,每个边结点同时链接在两个链表中

9.5广度优先搜索(BFS)

BFS:先访问起始顶点v,再从v出发访问v的各个邻接结点,然后再从这些访问的邻接结点出发访问它们的邻接顶点,直到访问完所有的顶点,如果此时还有没访问的顶点,则另选图中一未被访问过的顶点,重复该步骤,直到所有顶点被访问(类似于二叉树的层序遍历)
eg:此过程是由一顶点出发,由近至远访问和顶点相同的顶点,这是一种分层的查找,非递归,而实现逐层访问则需要一个辅助队列

邻接矩阵实现:

#include <stdio.h>
#include <stdlib.h>
#define MAX 100
//邻接矩阵表示图
int graph[MAX][MAX];
int num;
//辅助队列
typedef struct{
	int data[MAX];
	int front,rear;
}queue;
//初始化队列
void initqueue(queue *q){
	q->front=q->rear=0;
}
//入队
void enqueue(queue *q,int x){
	q->data[q->rear]=x;
	q->rear=(q->rear+1)%MAX;
}
//出队
int dequeue(queue *q){
	int x=q->data[q->front];
	q->front=(q->front+1)%	MAX;
	return x;
}
//判断队列是否为空
int isempty(queue *q){
	return q->front==q->rear;
} 
//广度优先搜索(BFS)
void bfs(int start){
	queue q;//调用队列
	int visit[MAX]={0};//所有visit数组元素置为0,作为标记
	int i;
	initqueue(&q);
	visit[start]=1;//对访问顶点做已访问标记
	enqueue(&q,start);//对顶点i入队
	while(!isempty(&q)){
		int u=dequeue(&q);//队头出队
		printf("%d",u);
		for(i=0;i<num;i++){//访问i的所有邻接点
			if(graph[u][i]&&!visit[i]){//i为u的未访问顶点
				visit[i]=1;//对i进行已访问标记
				enqueue(&q,i);//顶点i入队
			}
		}
	} 
}
int main() {
    // 初始化图
    num_vertices = 6;
    graph[0][1] = graph[0][5] = 1;
    graph[1][0] = graph[1][2] = graph[1][5] = 1;
    graph[2][1] = graph[2][3] = graph[2][4] = graph[2][5] = 1;
    graph[3][2] = graph[3][4] = 1;
    graph[4][2] = graph[4][3] = graph[4][5] = 1;
    graph[5][0] = graph[5][1] = graph[5][2] = graph[5][4] = 1;

    // 从顶点0开始进行BFS
    bfs(0);
    return 0;
}

邻接表实现:


eg:辅助数组用于标记顶点是否访问,初始置为同一值表示未访问,访问后立刻修改数值表示已访问,防止多次访问
BFS性能分析
BFS实现过程中都需要一个数组来辅助输出,每个顶点都要入队一次,最坏情况下空间复杂度为O(|V|)
遍历图其实质为对每个顶点查找其邻接点,耗时取决于存储的数据结构
邻接表:每个顶点都要入队一次,时间复杂度为O(|V|),查找每个点的邻接点时,每条边至少访问一次, 时间复杂度为O(|E|),总时间复杂度为O(|V|+|E|)
邻接矩阵:查找每个顶点所需时间O(|V|),总时间复杂度为O(|V|^2)
BFS算法求解单源最短路径问题
设图为G=(V,E)
最短路径d(u,v):从顶点u到顶点v为从u到v的任何路径中最少的边数,如果没有通路,则置其为无穷
广度优先生成树:根据存储方式的不同,相应生成树也不一定唯一,邻接矩阵的存储方式唯一,只有唯一对应的生成树,邻接表的存储形式不唯一,生成树不唯一

9.6深度优先搜索(DFS)

DFS:先访问图中的某一顶点v,然后从此顶点出发,访问与v邻接且未被访问的任意一个顶点w,再从w开始继续此过程,直到向下无法再进行访问,此时回退到上一顶点再进行此操作,然后根据情况再退回,再执行,直到所有顶点都访问过
eg:类似于图的先序遍历,由于其具备递归回退的特性,所有实现该算法需要借助一个递归工作栈,每次访问时如果没有邻接顶点则回退,再从回退的顶点寻找其邻接顶点
邻接矩阵实现:


邻接表实现:


DFS性能分析
DFS实现是递归的,需要借助栈来实现,空间复杂度为O(|V|)
两种遍历方式的时间复杂度相同,都是寻找邻接点的过程,此时邻接表存储的时间复杂度为O(|V|+|E|),邻接矩阵存储的时间复杂度为O(|V|^2)
DFS的生成树和生成森林
对连通图才能生成树,否则只能生成森林,其余的与BFS类似

9.7通过遍历来判断图的连通

无向图:若无向图是连通的,则从任意一个顶点出发,遍历一次就可以访问到所有的结点,否则无法一次访问到
有向图:如果从初始结点到其他各结点都有路径,则可以访问到所有的顶点,否则无法直接访问到

9.8最小生成树MST

生成树:连通图的生成树包含树的所有结点,且只含尽量少的边
eg:砍掉它的一条边,图就变成非连通图,加上一条边就会形成回路
最小生成树MST:对带权连通无向图G其权值最小的生成树
MST性质:
1.若图中存在权值相同的树,则其最小生成树不唯一,因为其树形不同,权值不相等时最小生成树唯一,如果无向图的边数比顶点树少1,即它本生为一棵树,则最小生成树·1就是它本身
2.即使最小生成树不唯一,但其对应的权值之和唯一且最小
3.MST边数为顶点数-1
eg:MST中所有边权值之和最小,但不能确保其任意两个顶点之间的路径
4.假设G=(V,E)是一个带权连通无向图,U为顶点集的一个非空子集,若(u,v)是一条具有最小权值的边,其中u ∈ \isin U,v ∈ \isin V-U,则此时必存在一颗包含边(u,v)的最小生成树

9.9Prim算法

prim:初始时从图中任取一顶点加入树T,然后再选一个离该顶点集合最近的顶点加入树T(包括顶点和相应的边),每次操作后顶点数和边数都加一,直到所有的顶点都加入T中,此时T就是最小生成树,T中必然有n-1条边
步骤:
假设G={V,E}是连通图,MST=(U,ET),其中ET是MST中树的边集合
初始化:向空树T=(U,ET)中添加图G=(V,E)的任意一个顶点u0,使U={u0},ET= ⊘ \oslash
循环(重复直到U=V):从图中选择满足{(u,v)|u ∈ \isin U,v ∈ \isin V-U}且具有最小权值的边(u,v),加入树,置U=U ∪ \cup {v},ET=ET ∪ \cup {(u,v)}

#include <stdio.h>
#include <stdbool.h>
#define INF 999999//定义无穷,表示两个顶点之间没有边存在时的权值
#define V 5//图中顶点的数量

//寻找在MST集合中的最小权值的顶点
int minkey(int key[],bool mst[]){
	int min=INF,minindex;//min只是定义了一个数,后续会代替为最小值
	for(int v=0;v<V;v++){
		if(mst[v]==false&&key[v]<min){//mst是判断顶点是否在MST中的数组,即如果外部顶点不在MST中且其与内部顶点的权值最小
			min=key[v];//将当前最小值更新为更小的权值
			minindex=v;//跟新最小值的顶点索引
		}
	}
	return minindex;//返回最小值的顶点索引
}

//打印MST
void printMST(int parent[],int graph[V][V]){
	printf("edge\tweight\n")  //打印表头
	  for (int i = 1; i < V; i++) {
        printf("%d - %d \t%d \n", parent[i], i, graph[i][parent[i]]);
    }
}

//prim构造MST
void primMST(int graph[V][V]){
	int parent[V];//储存MST中的父节点
	int key[V];//储存MST中的权值
	bool mst[V];//判断顶点是否在MST中
	for(int i=0;i<V;i++){//for循环把所有权值置为无穷大,所有顶点都不在MST中
		key[i]=INF;//设权值为无穷
		mst[i]=false;//设所有顶点均不在MST中
	}
	//第一个顶点始终时MST的根结点
	key[0]=0;//根结点权值为0
	parent[0]=-1;//根结点没有父节点
	//构建MST
	for(int count=0;count<V-1;count+=){
		int u=minkey(key,mst);//选择权值最小且不在MST中的顶点
		mst[u]=true;//把加入的顶点重新标记
		for(int v=0;v<V;v++){
			//更新与所选顶点的相邻的顶点的权值与父节点
			if(graph[u][v]&&mst[v]==false&&graph[u][v]<key[v]){//graph[u][v]检查u和v之间是否存在一条边,如果值不为0,则两只之间有一条边,第二部分判断顶点是否在MST中,第三部分判断从当前顶点u到v之间的权值是否小于v当前存储的权值
			parent[v]=u;//更新父节点
			key[v]=graph[u][v];//更新权值
			}
		}
	}
	 // 打印构建的MST
    printMST(parent, graph);
}
int main() {
    // 用邻接矩阵表示图
    int graph[V][V] = {
        {0, 2, 0, 6, 0},
        {2, 0, 3, 8, 5},
        {0, 3, 0, 0, 7},
        {6, 8, 0, 0, 9},
        {0, 5, 7, 9, 0}
    };

    // 执行Prim算法并输出结果
    primMST(graph);

    return 0;
}

eg:其时间复杂度为O(|V|^2),与E无关,其基本思想为每次加入不相干的顶点,且顶点与MST内部的顶点的边的权值最小,所以其适合求解边稠密的图的MST,

9.10Kruskal算法

Kruskal:初始时置为有n个顶点而没有边的非连通图T={V,{}},每个顶点自成一个连通分量,根据边的权值从小到大排序,不断选取没有选过的边长权值最小的边,若该边依附的顶点落在T中不同的连通分量上(并查集判断其是否属于同一棵集合树),再把此边加入T中,否则舍弃该边再选择下一条权值最小的边,最后所有的顶点都在一个连通分量上
步骤:
设G(V,E)是连通图,MST=(U,ET)
初始化:U=V,ET= ⊘ \oslash ,每个顶点构成一棵独立的树,此时MST是一个仅有|V|个顶点的森林
循环(重复直至完成MST的生成):按G的边的权值递增顺序依次从E-ET中选一个边加入T中且不构成回路,否则舍弃,直到ET中有n-1条边

#include <stdio.h>
#include <stdlib.h>
//边的结构体
struct Edge{
	int src,dest,weight;//起点,终点,权值
};
//图的结构体
struct Graph{
	int V,E;//图的顶点和边数
	struct Edge* edge;//存储图的所有边
};
//创建图的函数
struct Graph^creat(int V,int E){
	//分配存储空间
	struct Graph* graph=(struct Graph*)malloc(sizeof(struct Graph));
	graph->V=V;
	graph->E=E;
	//为图的边组分配存储空间
	graph->edge=(struct Edge*)malloc(E* sizeof(struct Edge));
	return graph;//返回创建的图
}
//辅助函数:找到包含结点i的子集的根
int find(int parent[],int i){
	if(parent[i]==-1)//如果结点i没有父节点,则它是根结点
		return i;
	//递归循环查找i结点的父节点,直到找到根结点
	return find(parent,parent[i]);
}
//辅助函数:将两个子集合合并为一个子集
void union(int parent[],int x,int y){
	//找到x和y所在子集的根
	int xset=find(parent,x);
	int yset=find(parent,y);
	//将x所在的子集的根设置为y所在子集的根
	parent[xset]=yset;
}


	


9.11最短路径

带权路径长度:当图为带权图时,把从一个顶点v0到图中其余任意一个顶点vi的一条路径的所经过边上的权值之和
最短路径:带权路径长度最短的路径
核心性质:两点之间的最短路径也包括了路径上其他顶点之间的最短路径,即最短路径集合之间各顶点之间路径也最短

9.12Djikstra算法(单源最短路径)

Djikstra:设置一个集合S记录已求得最短路径的顶点,初始时把源点放进S,集合S每次并入一个新顶点vi,都要修改源点v0到集合V-S中顶点当前的最短路径长度,即每回加入新顶点时,都要保证v0的路径与其他顶点都是最短
实现时需要的三个辅助数组
final[ ]:标记各顶点是否已经找到最短路径,即是否需要并入集合S
dist[ ]:记录从源点v0到其他各顶点当前的最短路径长度,初始值为:若源点到其他顶点之间有弧,则用dist[i]存储权值,没有则置为无穷
path[ ]:path[i]表示从源点到i之间之间的最短路径的前驱顶点,在算法结束时,可根据其值追朔到两点之间的最短路径
用邻接矩阵arcs[i][j]来表示带权有向图及其对应边
Djikstra步骤
1)初始化:集合S初始化为{0},dist[]的初始值dist[i]=arcs[0][i]
2)从顶点集合V-S中选出vj,使其满足dist[j]=Min{dist[i]|vi ∈ \isin V-S},vj为当前求得的一条从v0出发的最短路径的终点,使得S=S ∪ \cup {j},即找出点,加入此点有关的最短边
3)修改从v0出发到集合V-S上任意一个顶点vk可到达的最短路径长度,若dist[j]+arcs[j][k]<dist[k],则更新dist[k]=dist[j]+arcs[j][k],即加入一个新结点时,再度判断上一个顶点与新顶点边之和要最小,此时再更新最短的值
4)重复2,3操作直到n-1次,即把所有边都加入,且此时所有顶点都在集合S中
eg:更新时要把原路径长度更新为加入该结点之后的最短路径长度

#include <stdio.h>
#include <limits.h>
#include <stdbool.h>

#define V 9 // 定义顶点数量

// 找到距离数组dist[]中最小值的索引
int minDistance(int dist[], bool sptSet[]) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < V; v++)
        // 如果顶点v未包含在最短路径树中并且距离小于等于当前最小值
        if (sptSet[v] == false && dist[v] <= min)
            // 更新最小值和对应的索引
            min = dist[v], min_index = v;

    return min_index;
}

// 打印最短路径结果
void printSolution(int dist[], int final[], int path[]) {
    printf("Vertex \t Distance from Source \t Path\n");
    for (int i = 0; i < V; i++) {
        printf("%d \t %d\t\t", i, dist[i]); // 打印顶点i和到源顶点的距离
        if (final[i] != -1) {
            printf("%d ", i); // 打印最短路径上的顶点
            int j = i;
            while (j != 0) {
                printf("<- %d ", path[j]); // 打印前驱节点
                j = path[j];
            }
        }
        printf("\n");
    }
}

// Dijkstra算法函数
void dijkstra(int graph[V][V], int src) {
    int dist[V]; // 存储从源到每个顶点的最短距离
    int final[V]; // final[i]标记顶点i是否已经找到最短路径
    int path[V]; // path[i]记录顶点i的前驱节点

    bool sptSet[V]; // sptSet[i]为true表示顶点i包含在最短路径树中

    // 初始化所有距离为无穷大,sptSet[]为false,final[]为-1,path[]为-1
    for (int i = 0; i < V; i++)
        dist[i] = INT_MAX, sptSet[i] = false, final[i] = -1, path[i] = -1;

    // 源顶点到自身的距离为0
    dist[src] = 0;

    // 循环遍历V-1次,找到最短路径树中的每个顶点
    for (int count = 0; count < V - 1; count++) {
        // 选出不在最短路径树中的距离最小的顶点
        int u = minDistance(dist, sptSet);
        // 将选中的顶点标记为已处理
        sptSet[u] = true;

        // 更新与选中顶点相邻的顶点的距离值
        for (int v = 0; v < V; v++)
            // 如果顶点v未包含在最短路径树中、存在从u到v的边、
            // 以及通过u可以缩短到v的距离,则更新dist[v]和final[v],以及path[v]
            if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX 
                && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
                final[v] = 1; // 标记顶点v已找到最短路径
                path[v] = u; // 记录顶点v的前驱节点为u
            }
    }

    // 打印结果
    printSolution(dist, final, path);
}

int main() {
    // 图的邻接矩阵表示
    int graph[V][V] = { {0, 4, 0, 0, 0, 0, 0, 8, 0},
                        {4, 0, 8, 0, 0, 0, 0, 11, 0},
                        {0, 8, 0, 7, 0, 4, 0, 0, 2},
                        {0, 0, 7, 0, 9, 14, 0, 0, 0},
                        {0, 0, 0, 9, 0, 10, 0, 0, 0},
                        {0, 0, 4, 14, 10, 0, 2, 0, 0},
                        {0, 0, 0, 0, 0, 2, 0, 1, 6},
                        {8, 11, 0, 0, 0, 0, 1, 0, 7},
                        {0, 0, 2, 0, 0, 0, 6, 7, 0}
                    };

    // 从顶点0开始计算最短路径
    dijkstra(graph, 0);

    return 0;
}

ps:总而言之,就是先选定一个顶点,再根据它的邻接顶点定义边长,再选出最小边长的顶点加入顶点集合,再根据新顶点找剩下邻接顶点的最短距离,不断更新,直到所有顶点都在集合内
eg;此方法时间复杂度均为O(|V|^2),且不可对带负值的边图使用

9.13Floyd算法(各顶点之间最短路径)

Floyd:初始时,对任意两个顶点vi和vj,如果它们之间存在边,则将此边的权值作为它们之间的最短路径长度,如果不存在边,则路径长度置为无穷,之后再不断在原路径中加入顶点作为中间顶点,如果加入中间顶点后,得到了比原路径更短的路径,则代替之
步骤:
定义一个n阶方阵序列:A(-1),A(0),……A(n-1),
A(-1)[i][j]=arcs[i][j] A(K)[i][j]=Min{A(k-1)[i][j],A(k-1)[i][k]+A(k-1)[k][j]},k=0……n-1
A(0)[i][j]的意义是从顶点vi到顶点vj,中间顶点为v0的最短路径
A(k)[i][j]的意义为从顶点vi到顶点vj,中间顶点序号不大于k的最短路径
eg:Floyd是一个迭代的过程,是对于之前最短路径性质的逆运用


9.14拓扑排序

有向无环图(DAG):一个有向图中不存在环
eg:用于描述公共子式的表达式
AOV网:用有向无环图来表示一个实例,顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj,意义为顶点表示活动的网络
拓扑排序:由一个有向无环图的顶点组成的序列满足:1)每个顶点都出现且只出现一次,2)若顶点A排在顶点B的前面,则没有B到A的路径

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
大禹数据治理方法论完全版本是指一套完整、系统的数据治理方法体系,它是基于大禹数据治理方法论基础版本进一步发展而来的。完全版本对数据治理的各个方面进行了更加深入和全面的研究,以帮助组织更好地管理和利用数据。 大禹数据治理方法论完全版本包括以下主要内容: 1. 数据治理目标:确定组织数据治理的目标和愿景,明确为什么需要数据治理以及希望达到什么样的效果。 2. 数据治理策略:制定组织的数据治理策略,包括明确数据治理的原则、规范和方法,并将其与组织的战略目标相结合。 3. 数据治理架构:设计数据治理的架构,包括组织结构、流程和技术工具的规划和部署,确保有效的数据治理运作。 4. 数据管理和质量:建立数据管理和质量管理的体系,包括数据采集、清洗、整合、存储和使用等方面的规范和流程,提高数据的准确性、一致性和可靠性。 5. 数据安全和隐私:确保数据的安全和隐私,制定数据安全策略、权限控制和数据保护措施,防止数据泄露和滥用。 6. 数据治理文化和培训:培养数据治理的文化和意识,加强员工的数据管理能力和意识,推动数据治理的内外部传播和持续推进。 7. 监控和评估:建立数据治理的监控和评估机制,跟踪数据治理的进展和效果,并根据评估结果进行调整和改进。 8. 持续改进:持续改进数据治理的方法和实践,根据实际情况和反馈意见,不断优化和完善数据治理的流程和方式。 通过大禹数据治理方法论完全版本的应用,组织可以更好地管理和利用数据资产,提高业务决策的准确性和效率,提升数据的价值和竞争力。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值