常见数据结构与简单应用

linear list

概览 : 以顺序表和链表为基础,从而实现栈,队列,串

一对一

基本操作

  • 对于个体: Insert Delete find(Locate/Get)
  • 对于整体: Initial Clear Destroy

    read(Traverse/Length/Empty)

  • 操作时注意: 下标越界,长度不够,内存的分配与失败

  • 每个操作的基本组成 : 检查,操作,(返回以及长度调整)

检查

变量名 : i:index ; L:list ; e:element ; p:positon

i

检查i是否合法

if ((i <= 0 ) || (i > length)) return ERROR;

L

L以指针形式传入,并检查是否为空,为空则返回错误(内存分配失败或者只有头节点)

if ((L == NULL) || (L->next = NULL)) return ERROR;

e

e在插入时表示插入的数,再删除时来接受删除的数(删除时以指针传入)

p

再分配内存后检查是否分配成功,在遍历,定位,尾插时更新,作为一个标尺

包含对象

长度 数据 指针

  • 预定义,提高代码可读性
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;//方便后期适配其他类型

顺序表(SequenceList)

用于对数表之类的存储,存储密度高

常用操作

  • 结构
#define MAXSIZE 20//动态储存较短的数据
typedef struct sqlist {
	ElemType data[MAXSIZE];
	int length;
}Sqlist;
//在传参时使用指针
  • 插入/删除时,将元素移动

下面代码只是关键部分,完整的还需要检查index是否合法,插入后L.length的改变,以及返回成功与否

//插入时,后面先移动
for (int j = L.length; j >= i; j--) 
    L.data[j] = L.data[j-1];
L.data[i-1] = e;

//删除时,前面覆盖 
e = L.data[i-1];
for (int j = i; j < L.length; j++) 
	L.data[j-1] = L.data[j];
  • 动态分配顺序表( 连接两个数组)

*先将原指针备份,再分配新大小的指针给原指针,将原数据拷贝到新指针,释放原指针

对于顺序表还需要调整长度

void IncreaseSize(SqList &L, int len)
{
    //备份 + 创建新的区域
	ElemType *p = L.data; 
	L.data = (ElemType *)malloc((L.MaxSize+len) * sizeof(ElemType));
    // 将数据复制到新区域
	for (int i = 0; i < L.length; i++)
		L.data[i] = p[i];  
	L.MaxSize = L.MaxSize + len; 
    // 释放原来的内存空间 
	free(p); 
}

链表 (LinkList)

空间利用率低,存储弹性强

操作 : 分配内存(检查+赋值),定位指针(在create中就不用了,每次循环就自动定位了),改变指针指向,(释放空间)

单链表

  • 声明

头节点的data可以用来存储长度

typedef struct Node{
    ElemType data;
    struct Node *next;
}Node, *LinkList;
typedef struct node Node;
struct node
{
    ElemType data;
    Node *next;
};
  1. 分配空间+检查
Node* alloc() {return (Node*)malloc(sizeof(Node*))}
p=alloc();
if(p==NULL)return ERROR;
  1. 定位到操作的位置
Node* p = L;
for(int j =0;j<i-1;j++)p=p->next;
//因为是对第i位操作所以定位到i-1位
  1. 改变指针指向

对于insert和delete都需要先定位p

  • ListInsert

创建新节点后,改变两个指针的指向

Node * in =alloc();
in->data = e;

in->next = p->next; 
p->next = in; 
  • ListDelete

先寄存被删节点,改变断点处指向,释放空间

Node *q = p->next;    
e = q->data;

p->next = q->next;      
free(q);
  • ListCreateHead

新指后,前指新

for (int i = 0; i < length; i++)
{
    Node* in = alloc();
    if (in == NULL) return ERROR;
    in->data =i;
    
    in->next = L->next;
    L->next = in;
}
  • ListCreateTail

尾指新,尾更新,最后末尾添NULL

Node* p = L;
for (int i = 0; i < length; i++)
{
    Node* in = alloc();
    if (in == NULL) return ERROR;
    in->data = i;
    
    p->next = in;
    p = in;
}
p->next = NULL;

数据也可以利用scanf进行输入

  • inverse

按顺序读取每一个节点再头插

Node *Inverse(Node *L)
{	
    Node *p, *q;	  
    p = L->next;     //p来存储原链表	  
    L->next = NULL;  //将链表清空       	
    while (p != NULL)
    {		   
        q = p;//q来定位每一个		  
        p = p->next;//p的迭代
        
        q->next = L->next;  
        L->next = q;
    }	   
    return L;
}

双链表

既有前驱,又有后继

  • 声明
typedef struct DNode
{
    ElemType data;
    struct DNode *prior, *next;  //前驱和后继指针
}DNode;
  • 尾插

可以通过循环获取输入

DNode* Last, *p;
p = (DNode*)malloc(sizeof(DNode*));
if(p==NULL)return ERROR;
p->data = e;

Last->next = p;//双向
p->prior = Last;

p->next = NULL;
Last = p;//Last更新
  • 删除
//先将p定位到i处
p->prior->next = p->next;
if(p->next)//如果是最后一个节点,规则就不一样了
p->next->prior = p->prior;
free(p);

栈(Stack)

只有一端允许插入和删除,叫做栈顶(top),另一端叫做栈底(bottom),大小固定为S(size)

后进先出(Last in First Out) LIFO结构

栈的操作: push/pop 进(入)栈/出栈 (压栈/弹栈) 只是叫法的不同 GetTop

本质是前面线性表的退化(只能对一个接口进行操作)

用途

  1. 函数的执行
  2. 表达式的求解(后缀表达式)
  3. 递归

顺序表实现

typedef struct Stack{
    ElemType data[MAXSIZE];
    int top;
}SqStack;

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

Status Push(SqStack *S,Elem e)
{
    if(S->top>=MAXSIZE-1) return ERROR;//判断栈是否已满
    S->data[++(S->top)]=e;
    return OK;
}
Status Pop(SqStack *S, ElemType *e){  
    if(S->top == -1)  return false; //判断栈是否为空     
    *e = S->data[(S->top)--];    
    return OK;
}

链表实现

typedef struct Linknode{        
    ElemType data;
    Linknode *next;      
}LinkNode,*LStack;
//push/pop就跟前面的尾插/删除一样

队列(Queue)

顺序表实现

队头删除队尾插入

FIFO(First In First Out)

为了区别队空与队满,将队列最后一位不放数据

rear指向队尾的下一位空格

I n i t / D e s t r o y E n Q u e u e / D e Q u e u e G e t H e a d Init/Destroy \quad EnQueue/DeQueue \quad GetHead Init/DestroyEnQueue/DeQueueGetHead

以下直接用c++描述了,C不能传引用参数

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

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

length = (Q.rear + MAXSIZE - Q.front)%MAXSIZE;
//考虑到循环队列,以及rear处没数据

//检查:判断长度 顺序表中的length判断
if((Q.rear+1)%MaxSize == Q.front;//在入队前判断队列是否满
if(Q.rear == Q.front);   //出队时判断是否为空 
//定位:顺序表先天优势
//操作:入队与出队
Q.data[Q.rear] = e;   
e = Q.data[Q.front]; 
//修改length
Q.rear = (Q.rear+1)%MaxSize; 
Q.front = (Q.front+1)%MaxSize;
//返回

Q.rear的妙处

  1. 指向的说末尾的下一个,在入队时直接使用
  2. 因为指向的多一个,在处理长度是相当于默认加了1,让表达式更简洁

链表实现

typedef struct LinkNode{  //定义节点
    ElemType data;    
    struct LinkNode *next;
};

typedef struct{ //定义队列       
    LinkNode *front, *rear;
}LinkQueue;

void InitQueue(LinkQueue &Q){   // 初始化时,front、rear都指向头结点 
    Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));  
    Q.front -> next = NULL;
}

//入队即尾插,出队即删除结点,都比较简单,就不写了

小结

栈和列表只是规定了操作的顺序表和链表,不需要定位操作了

cpp中STL(Standard Template Library):就是链表就是顺序表

而这两种基本的也叫存储结构 顺序/链式存储

一般,我们习惯顺序表下标从0开始,链表从1开始

串(String)

以字串为使用单位的顺序表

如 : 名字储存用顺序表,聊天记录用链表

空串:任意个空格组成的串

主串 S 子串 T

Concat SubStr Replace

朴素模式匹配

作用上类似低配版find

int Index(hstring S, hstring T){   
    int k=1; //开始比较的位置   
    int i=k, j=1;  
    while(i<=S.length && j<=T.length){    
        if(S.ch[i] == T.ch[j]){     
            ++i; ++j; 
        }else{        
            k++; i=k; j=1; 
        }   
    }   
    if(j>T.length) 
        return k;   
    else       
        return 0;
}

时间复杂度:设子串长度为m,主串长度为n

  • 匹配成功的最好时间复杂度: O ( m ) O(m) O(m) 在第一个位置就匹配成功
  • 匹配失败的最好时间复杂度: O ( n ) O(n) O(n) 子串的第一个和主串的每一位都不同
  • 最坏时间复杂度: O ( n m ) O(nm) O(nm)

KMP

名字只是三个人名抽的第一个 可记成(看猫片)

  • 原理

根据已经匹配的来得到匹配失败时该匹配的位置

  • 实现
  1. next[j]数组
void GetNext(string t,int next[])
{
	int i = 0, j = -1;
	next[0] = -1;
	while (i < t.length() - 1)
	{
		if (j == -1 || t[i] == t[j])
		{
			i++; j++;
			next[i] = j;
		}
		else j = next[j];
	}
}
  1. 利用next[j]数组进行跳跃
int Index_KMP(string S, string T)
{
	int  i = 0, j = 0;
	int l1 = S.size(), l2 = T.size();
	int* next = new int[l2];
	GetNext(T, next);
	while (i < l1 && j < l2)
	{
		if (j == -1 || S[i] == T[j])
		{
			i++;
			j++;
		}
		else j = next[j];
	}
	return j == l2 ? i - l2 : -1;
}

KMP的平均时间复杂度为 O ( m + n ) O(m+n) O(m+n)

一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。 —KMP

树(tree)

一对多

树(tree)=根节点(root)+子树(subtree)

度(degree):一个节点的子树个数,叶结点(leaf)(终端节点):度为0,分支节点(非终端节点):root和leaf之外的

子节点(child) 父节点(parent) 兄弟节点(sibling)同一父节点的子节点

深度(depth):节点的层数 ,从0开始

森林(forest):不相交的树的集合,实际上是图

  • 抽象数据类型

AbstractDataType,ADT

基本操作

T : Tree 根节点来记录长度

对于树

  1. Init/Destroy/Create/Clear BiTree //create时注意要把空指针域设为NULL
  2. Tree Empty/Depth

对于创建二叉树,可以将没有数据的位置填成^

对于节点

  1. Root / Parent / LeftChild / RightSibling获取相应的地址,不存在则返回NULL
  2. Assign/Value/InsertChild/DeleteChild赋值/读值/增删

不是完全二叉树时,与链表的实现大差不差,只是不方便精确获取某个位置

实现方式

  • 顺序表 父节点表示法

节点 = 数据 + 关系

node = data + parent 可以迅速找到父节点(将根节点下标默认为-1)(树的基本形式)

node = data + parent + 长子下标(firstchild) 可以迅速找到子节点(将长子默认为-1,用来判断叶节点)

node = data + parent + 右兄弟下标(rightsib) 可以迅速找到同级

  • 顺序表+链表 孩子表示法

顺序表为主体,关系由链表表达

childtree.png

  • 孩子兄弟表示法

完全用链表,二叉树最常用

二叉树

定义

每个节点的子节点数不超过2(区分左右节点)

斜树: 只有一种结点(左/右),像链表一样

满二叉树: 除了叶结点层,其他每层的其他结点都有左右子结点

二叉树一般用链表存储,如下结构体

typedef struct BiTNode
{
    TElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

二叉树同构:可以交换左右子树转换的树

完全二叉树

高密度,用顺序表存储

从右往左,从下往上依次去除结点的满二叉树(可以在顺序表中快速读取)

其他接近完全二叉树的可以将缺少部分填为 “^” (用来表示空的符号)

数量关系全用等比数列理解

遍历二叉树

访问 次序

树对我们来说虽然很直观,

前/中/后序是根节点的访问次序,层序遍历就顾名思义

Pre/In/PostOrderTraverse

而在代码的实现上,都是用的递归,区别不同的只有打印出现的位置

而层序遍历是通过将结点值放入一个队列之中,在父节点出队后,子节点再入队.

//前/中/后序遍历
void __OrderTraverse(BiTree T)
{
    if(T==NULL) return ;//若为空,则啥也不干
    //printf("%c",T->data); //前序遍历  
    __OrderTraverse(T->lchild);
    //printf("%c",T->data); //中序遍历
    __OrderTraverse(T->rchild);
    //printf("%c",T->data); //后序遍历    
}

//层序遍历
void LevelOrderTraverse(BiTree T){
    LinkQueue Q; //准备好队列
    BiTree p;
    EnQueue(Q, T);
    while(!IsEmpty(Q)){
        DeQueue(Q, p);   
        visit(p);      
        if(p->lchild!=NULL)  
            EnQueue(Q, p->lchild); 
        if(p->rchild!=NULL)    
            EnQueue(Q, p->rchild);
    }
}
  • 中序遍历,非递归实现,利用栈来实现

其他两种遍历同理可行

void __OrderTraverse(BiTree BT)
{
	BiTree T = BT;
	stack S[Maxsize];
    while(T||!isEmpty(S)){
        while(T){
            Push(S,T); 
            //printf("%c",T->Data);先序遍历
            T=T->left;
        }
        if(!isEmpty(S)){
            T=Pop(S);
            //printf("%c",T->Data);中序遍历
            T=T->right;
        }
    }
}

线索二叉树

在二叉树中,每个叶节点都会有两个空指针,拿满二叉树来举例,空指针的数量约为已用指针的两倍,大大浪费了空间,为了利用这个空间,出现了线索二叉树(Threaded Binary Tree)

为了区分是指针是孩子还是前驱/后继,追加tag来标记

中序遍历线索化:

对于每次递归,p指向的自己,pre指向的中序访问中访问的前一个

而所做的,将叶结点的左指针指向前一个,右指针指向后一个

如果为左斜二叉树,这样就变成双向链表了

线索二叉树提高了遍历效率,特别是中序遍历,

typedef int ElemType;
typedef enum { Link, Thread }PointerTag;
typedef struct BiThrNode
{
	ElemType data;
	struct BiThrNode* lchild, * rchild;
	PointerTag ltag, rtag;//左右标签,上面枚举的
}BiThrNode,*BiThrTree;
BiThrNode* pre;

void Threading(BiThrTree p)
{
	if (p!=NULL)
	{
		Threading(p->lchild);
		if (!p->lchild)//将p指向pre
		{
			p->ltag = Thread;
			p->lchild = pre;
		}
		if (!pre->rchild)//将pre指向p
		{
			pre->rtag = Thread;
			pre->rchild = p;
		}
		pre = p;//pre更新
		Threading(p->rchild);
	}
}

二叉搜索树

(BST,Binary Search Tree)又称二叉排序树,因为元素按顺序存储,左子树结点值 < 根结点值 < 右子树结点值,二分查找,最大查找次数为树的深度

BST.jpg
  • 查找
typedef struct BSTNode{
    int key;
    struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;

BSTNode *BST_Search(BSTree T, int key){
    while(T!=NULL && key!=T->key){
        if(key < T->key)	 T=T->lchild;
        else if(key > T->key) T=T->rchild;
        else return T;
    }
    return NULL;
}

插入同理,先找到位置,直接挂为叶结点

对于同一集合以不同顺序给出,会生成不同的二叉搜索树,导致数据一边倒(左),查找效率就并没有提升,此时出现了平衡二叉树(右)

pingheng.jpg

平衡二叉树

Balanced Binary Tree,又称AVL,发明者的名字

平衡因子 Balance Factor B L = h l − h r BL=h_l-h_r BL=hlhr 左右子树的高度差不超过1

解决二叉搜索树的缺点,实现效率最大化,搜索数据的时间复杂度为 l o g 2 n log_2n log2n,远小于 n n n

插入时的平衡调整

平衡改变后的三个数,将中间值作为新的父结点,将它的子结点分给原来的父节点

如下两种调整

balanceadjust.jpg

红黑树

RBT(red-black tree),解决了AVL调整时太浪费时间,牺牲一点平衡换来性能的优越

即优先队列(Priority Queue),新加入的元素按照优先级分到相应优先级的队列,

用完全二叉树存储,最大堆:任何结点值是子树结点的最大值,最小堆反之,下面只说最大堆

  • 插入新元素

加到完全二叉树末尾,然后与父节点比较,当新元素比父节点大时,交换值,以次类推

typedef struct heap{
    ElemType Arr[MAX_SZIE+1];//顺序表存储完全二叉树
    //第一个位置来存放哨兵(一个上界),用于后面插入时循环
    int size,capacity;//当前使用量,总容量
}Heap;
void Insert(Heap H;Elemtype e){
    if(IsFull(H))return;//先判断是否已满
    int i=size+1;
    Elem[i]=e;
    for(;H->arr[i/2]<e;i/=2)
    	swap(H->arr[i],H->arr[i/2]);
}

  • 删除最大元素

删除最大值(根节点)后,将最后一个元素替补根节点位置,然后与子结点比较,如果子结点较大,则交换,以此类推

插入和删除的复杂度都为 O ( l o g n ) O(logn) O(logn),

  • 建立堆
  1. 每次将新元素插入原本的堆中,共n次,时间复杂度 n l o g n nlogn nlogn

  2. 先建立起完全二叉树,从最小规模的堆依次调整堆,时间复杂度 O ( n ) O(n) O(n)

哈夫曼树

最优二叉树

结点的权 : 自己设的,一般用整数表达(cpu计算整数效率更高)

结点的带权路径长度 : 路径长度*该节点的权

树的带权路径长度(Weighted Path Length of Tree,简称WPL) : 所有叶结点的带权路径长度之和

哈夫曼树 : 当有n个结点(都作叶结点,且每个节点都有各自的权值)试图构建一棵树时,当WPL最小时,则称这棵树为哈夫曼树

原则 : 权重越大越靠近根节点

构建步骤 : 每次取权最小的叶结点两个作为中间结点的子节点,并将权的和赋值给中间结点,使其与未取出的最小结点一起参与下一次选取

A-E的权为1-5

huffman.jpg
  • 利用最小堆进行构建

每次取权重最小的两个进行构建,并将权重和插入最小堆中,循环此过程

 HuffmanTree Huffman(MinHeap H)
 {/*假设H->Size个权值已经存在H->data[]->Weight里*/
 	int i,num;
	HuffmanTree T;//构建的哈夫曼树
	
	BuildMinHeap( H );  //将H->data[]按权值调整为最小堆
	/*此处必须将H->Size的值交给num,因为后面做DeleteMin()和 Insert()函数会改变H->Size的值*/
	num = H->Size;     
	for(i=1; i<num; i++){  //做 H->Size-1次合并   //此处教科书有问题!原书直接为H->Size
		T = NewHuffmanNode();  //建立一个新的根结点 
		T->Left = DeleteMin(H);  //从最小堆中删除一个节点,作为新T的左子结点
		T->Right = DeleteMin(H);  //从最小堆中删除一个节点,作为新T的右子结点 
		T->Weight = T->Left->Weight+T->Right->Weight;  //计算新权值 
		//printf("%3d 0x%x 0x%x\n",T->Weight,T->Left,T->Right);
		Insert(H,T);  //将新T插入到最小堆 
	} 
	T = DeleteMin(H);
	return T; 
  } 
  • 利用权重构造元素组+中间数组进行构建
//huffmanCoding.c
#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <stdlib.h>
#define N 6

typedef struct huffNode
{
    int weight;//权重
    int lchild, rchild, parent;  //与一个结点相连接的其他结点的下标值
}HTNode, * HuffTree;
typedef char** HuffCode;//用来储存每个叶结点的编码

//找出数组中无父节点且权值最小的两个节点下标,分别用s1和s2保存
void select(const HuffTree& HT, int n, int& s1, int& s2);
//HT:哈夫曼树,HC:哈夫曼编码,w:构造哈夫曼树节点的权值,n:构造哈夫曼树节点的个数
void HuffmanCode(HuffTree& HT, HuffCode& HC, int* w, int n);

int main()
{
    char key[N] = { '0','A','B','C','D','E' };//第0个元素保留不用
    int w[N] = { 0,1,2,4,5,6 }; //第0个元素保留不用
    HuffTree HT;
    HuffCode HC;
    HuffmanCode(HT, HC, w, N - 1);
    for (int i = 1; i < N; i++)
        printf("%c:%s\n", key[i], HC[i]);

    printf("\n");
    return 0;
}


void select(const HuffTree& HT, int n, int& s1, int& s2)
{
    s1 = s2 = 0;
    int min1 = INT_MAX;//最小值,INT_MAX在<limits.h>中定义的
    int min2 = INT_MAX;//次小值

    for (int i = 1; i <= n; ++i)
    {
        
        if (HT[i].parent == 0)
        {//筛选没有父节点的最小和次小权值下标
            if (HT[i].weight < min1)
            {//如果比最小值小
                min2 = min1;
                s2 = s1;
                min1 = HT[i].weight;
                s1 = i;
            }
            else if ((HT[i].weight >= min1) && (HT[i].weight < min2))
            {//如果大于等于最小值,且小于次小值
                min2 = HT[i].weight;
                s2 = i;
            }
            else;//如果大于次小值,则什么都不做
        }
    }
}

void HuffmanCode(HuffTree& HT, HuffCode& HC, int* w, int n)
{//1.初始化 2.构建哈夫曼树 3.获取哈夫曼编码表

    //1.初始化
    int s1,s2;
    int m = 2 * n - 1;//n个叶结点,n-1个中间结点
    HT = (HuffTree)malloc((m + 1) * sizeof(HTNode)); //0单元未使用
    for (int i = 1; i <= n; i++)
        HT[i] = { w[i],0,0,0 };//初始化前n个叶节点
    for (int i = n + 1; i <= m; i++)
        HT[i] = { 0,0,0,0 };  //初始化后n-1个中间结点



    //2.构建哈夫曼树
    for (int i = n + 1; i <= m; i++)
    {
        select(HT, i - 1, s1, s2);//w数组和新的结点构成的数组中寻找最小的两个
        //将最小的两个连接到后面的中间节点上
        HT[s1].parent = i;
        HT[s2].parent = i;
        HT[i].lchild = s1;
        HT[i].rchild = s2;
        HT[i].weight = HT[s1].weight + HT[s2].weight;//将子树的权的和赋给中间结点,参与下一次select
    }

  
    //3.获取哈夫曼编码表
    HC = (char**)malloc((n) * sizeof(char*));//哈夫曼编码表
    char* code = (char*)malloc(n * sizeof(char)); //暂存结点编码
    int c, f, j;//c表示当前结点,f表示当前节点的父节点,j用于内层循环
    for (int i = 1; i <= n; i++)
    {

        //从叶子扫描到根,判断c是f的左孩子(0)还是右孩子(1),并给上标记
        for (c = i,  f = HT[c].parent,j = 0; f != 0; c = HT[c].parent, f = HT[c].parent, j++)
        {
            if (HT[f].lchild == c)  code[j] = '0';
            else if (HT[f].rchild == c) code[j] = '1';
            else;
        }
        code[j] = '\0';
        HC[i] = (char*)malloc(strlen(code) * sizeof(char)+1);//需要拷贝'\0'
        memcpy(HC[i], code,strlen(code)+1);
    }
}

输出编码

A:000
B:100
C:10
D:01
E:11

用途 : 无损压缩

先处理文件,得出每个单位的出现频率(权重),由此构建一个字符-编码表

然后将源文件根据这张表转换为01代码

解压时也是根据这张表来读取文件

哈夫曼编码

按照频率进行编码后101010110101110110…,如何断开

设计的时候已经设计好了,因为每个码都是叶结点,则按照哈夫曼寻找到叶结点后就断开,开始解码下一个

集合

正数集合,利用父亲表示法,将所有根节点设为-1

parentnotation.png
typedef struct{
    ElemType Data;
    int Parent;//父节点下标
}SetType;
//查集,找到根结点
int Find(SetType S[],ElemType x){
    for(int i=0;i<MaxSize && S[i].Data!=X;i++);//在集合中寻找需要的数据x
    if(i>=MaxSize)return -1;//没找到
    for(;S[i].Parent>=0;i=S[i].Parent);
    return i;
}
//并集,将多个树的根节点指向其中一个,最好把小的树指向大树,尽可能减小树的高度
void Union(SetType S[],ElemType x1,ElemType x2){
    int root1,root2;
	root1=Find(S,x1);
    root2=Find(S,x2);
    if(root1!=root2)s[root2].Parent = root1;
}
FT
  • file transfer,判断每个电脑是否已经连通,意思是把把集合连接成一个树
  • 集合简化,因为是有限个集合,可以将构造两个数组(键值对),这里就分成data和parent两个数组
  • 在本题中,下标即代表电脑编号,值即代表父节点

TSSN too simple sometimes naive很傻很天真

int Find(SetType S[],int x){
    for(; S[x]>0; x=S[x]);
    return x;
}
void Union(SetType S[],int x1,int x2){//默认不在一起
    S[Find(x1)]=Find(x2);
}

缺陷: 1️⃣ 可以形成斜树,高度不断增加,就会失去树的优势2️⃣查找根节点时比较朴实

  • 按秩归并

改进: Union时将矮的树依附到高的树上,将根节点原本的-1用来记录数的大小(高度或规模)

最坏情况树高为 l o g n logn logn

//高度
if(root2高度>root1高度)
    s[root1]=root2;
else {
    if(二者等高) s[root1]--;//因为负数表示高度
    s[root2]=root1;
}
//规模
if(s[root1]>s[root2]){
    s[root1]+=s[root2];
    s[root2]=root1;
}else{
    s[root2]+=s[root1];
    s[root1]=root2;
}
  • 路径压缩

左边转化为右边,因为

pathcompress.png

int Find(SetType S[],int x){
    if(s[x]<0)return x;
    else return s[x]=Find(s,s[x]);
}

图(graph)

多对多

  1. G 由顶点集 V (vertex)和集 E (edge)组成,记为$ G = ( V , E )$
    $V = { v_1 , v_2 , … , v_n } $,用 ∣ V ∣ ∣ V ∣ V表示图 G 中顶点的个数,也称图 G 的
    E = ( u , v ) ∣ u ∈ V , v ∈ V E = { ( u , v ) ∣ u ∈ V , v ∈ V } E=(u,v)uV,vV,用 ∣ E ∣ ∣ E ∣ E表示图 G 中边的条数。
    注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集。
  2. 无向图:边全是无向边 ,无向边记为$ ( v , w )或 (w, v)$ 点的是依赖它的边的个数
    有向图:边全是有向边(),弧记为 $< v,w > $,顶点v称为弧尾(tail),顶点w称为弧头(head) 以点为弧尾的弧的数目称为出度,同理,为头的叫入度
  3. 网(network): 带图,即每个结点都有权
  4. 简单图:① 不存在重复边; ② 不存在指向自己的边
  5. 完全图: 无向图 :任意两个点之间都存在边,共有 C n 2 C_n^2 Cn2条边, 有向图为 2 C n 2 2C_n^2 2Cn2(弧为来回两个)
  6. 对于图的边$ ( v , w )$ v,w互为邻接点,而边依附于邻接点,或者说边与点相关联
  7. 连通图: 无向图所有点之都具有路径,无向图中的具有极大顶点数的连通图子图称为连通分量(可以不包含某些边)
  8. 强连通图: 有向图所有点之间都存在路径,有向图中具有极大强连通图叫做强连通分量
  9. 连通图的生成树: 含有全部顶点(n个),去掉闭环条边(n-1条),成为树(可以有多种去法)

存储

  • 邻接矩阵表示法int Graph[maxn][maxn];

  • 邻接链表表示法 对每个结点建立链表来存储它的边

​ vector来存点,pair来存边 vector <pair<int,int> > Graph[maxn];

链表只适合很稀疏的情况,因为一个结点需要的空间多

遍历

按照不同的顺序访问,并利用visited数组来记录结点的访问状态

时间复杂度 N为顶点,E为边

邻接表 O ( N + E ) O(N+E) O(N+E) 邻接矩阵 O ( N 2 ) O(N^2) O(N2)

深度优先遍历

DFS Depth_First_Search,类似树的先序遍历

从一个结点开始,每次都向一个方向访问,递归访问

伪代码如下

void DFS(Vertex V)
{
    visited[V]=TRUE;//访问后标记
    for(V的每个邻接点W)
        if(!visit[W])
            DFS[W];
}

广度优先遍历

BFS Breadth_First_Search,类似树的层序遍历

从一个结点开始,访问完与它相连的所有点,然后对访问了的结点递归访问,类似层序遍历

//Q为队列
void BFS(Vertex V)
{	
    visited[V]=TRUE;
    Enqueue(V,Q);
    while(!IsEmpty(Q)) {
		V=Dequeue(Q);
        for(V的每个邻接点W)
        	if(!visited[W]){
                visited[W]=TRUE;
                Enquene(W,Q);
        }
    }
}

最小生成树(贪心)

贪心算法:每一步都是当前最优解

MinimumSpanningTree.png

拓扑排序

Activity on Vertex AOV网络 活动在顶点上

排课的先后顺序,点天赋的前置天赋,

拓扑序 v到w存在一条路径,路径的顶点序列

拓扑排序,获得拓扑序的过程

合理拓扑序是一个有向无环图 (Directed Acyclic Graph DAG)

先构建树,再广度优先搜索,选择没有前驱顶点(入度为0)

Activity on Edge AOE 每一个边代表一个工序 网络计划图

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值