【学习笔记】数据结构(六 ②)

news2024/11/13 23:31:19

树和二叉树(二)

文章目录

  • 树和二叉树(二)
      • 6.3.2 线索二叉树
    • 6.4 树和森林
      • 6.4.1 树的存储结构
      • 6.4.2 森林与二叉树的转换
      • 6.4.3 树和森林的遍历
    • 6.5 树与等价问题
      • 6.5.1 等价定义
      • 6.5.2 划分等价类的方法
      • 6.5.3 划分等价类的具体操作 - 并查集MFSet
      • 6.5.4 用树型结构表示集合
      • 6.5.5 并查集及其优化
    • 6.6 赫夫曼树及其应用
      • 6.6.1 最优二叉树(赫夫曼树)
      • 6.6.2 赫夫曼编码
    • 6.7 回溯法与树的遍历
    • 6.8 树的计数

6.3.2 线索二叉树

lchildLTagdataRTagrchild
  • LTag = 0 lchild 域指示结点的左孩子
  • LTag = 1 lchild 域指示结点的前驱
  • RTag = 0 rchild域指示结点的右孩子
  • RTag= 1 rchild域指示结点的后继

以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树 (Threaded Binary Tree) 。

对二叉树 以某种次序遍历使其变为线索二叉树的过程叫做线索化

#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link(孩子)为0,Thread(线索)为1
typedef enum {
    Link,
    Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {
    TElemType data;//数据域
    struct BiThrNode* lchild, * rchild;//左孩子,右孩子指针域
    PointerTag LTag, RTag;//标志域,枚举类型
}BiThrNode, * BiThrTree;

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 先序线索化

    在这里插入图片描述

/*  先序线索化  */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum {
    Link,
    Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {
    TElemType data;//数据域
    struct BiThrNode* lchild, * rchild;//左孩子,右孩子指针域
    PointerTag LTag, RTag;//标志域,枚举类型
}BiThrNode, * BiThrTree;


//创建二叉树
BiThrTree CreateBiTree();
//创建线索二叉树
void CreatePreThread(BiThrTree T);
//先序遍历线索化
void PreThread(BiThrTree T);
void Visit(BiThrNode* q);

//后继
BiThrNode* NextNode(BiThrNode* p);
//对先序线索二叉树进行先序遍历
void PrintElement(TElemType e);
void PreOrder(BiThrTree T);


BiThrNode* pre = NULL;

int main() {
    BiThrTree T = NULL;
    printf("输入前序二叉树:\n");  // abd#g##e##cf###
    T = CreateBiTree();
    CreatePreThread(T);
    PreOrder(T);
    return 0;
}

//先序创建二叉树
BiThrTree CreateBiTree() {
    char ch;
    if (scanf(" %c", &ch) != 1) { // 检查输入是否成功
        // 输入失败,可能是文件结束符或错误输入
        exit(EXIT_FAILURE);
    }
    if (ch != '#') {
        BiThrNode* node = (BiThrNode*)malloc(sizeof(BiThrNode)); // 分配内存
        if (node == NULL) {
            // 内存分配失败
            perror("内存分配失败");
            exit(EXIT_FAILURE);
        }
        node->data = ch;
        node->lchild = CreateBiTree(); // 递归创建左子树
        node->rchild = CreateBiTree(); // 递归创建右子树
        node->LTag = Link;
        node->RTag = Link;
        return node;
    }
    else {
        return NULL;
    }
}

/*
对二叉树进行线索化
*/
void CreatePreThread(BiThrTree T)
{
    pre = NULL;
    if (T)
    {
        PreThread(T); // 先序线索化
        if (pre->rchild == NULL)
        {
            pre->RTag = Thread; // 处理最后一个结点
        }
    }
}

// 先序遍历 - 边遍历边线索化
void PreThread(BiThrTree T)
{
    if (T)
    {
        Visit(T);
        if (T->LTag == 0)
        {
            PreThread(T->lchild);
        }
        if (T->RTag == 0)
        {
            PreThread(T->rchild);
        }
    }
}

void Visit(BiThrNode* q)
//线索化
{
    if (q->lchild == NULL)
    {
        q->lchild = pre;
        q->LTag = Thread;
    }
    if (pre != NULL && pre->rchild == NULL)
    {
        pre->rchild = q;
        pre->RTag = Thread;
    }
    pre = q;
}

//找先序后继:根左右
/*
① 若 p->RTag == 1,则 next = p->rchild
② 若 p->RTag == 0,则 如果p有左孩子,next=p的左孩子;如果p没有左孩子,next=p的右孩子
*/
BiThrNode* NextNode(BiThrNode* p)
//寻找p的后继结点
{
    if (p->RTag == 1)
    {
        return p->rchild;
    }
    if (p->RTag == 0)
    {
        if (p->lchild && p->LTag == 0)
            return p->lchild;
        else
            return p->rchild;
    }
    
}
/*
对先序线索二叉树进行先序遍历
*/
void PrintElement(TElemType e)
{
    printf("%c ", e);
}

void PreOrder(BiThrTree T)
{
    BiThrNode* p = T;
    for (p; p != NULL; p = NextNode(p))
    {
        PrintElement(p->data);
    }
}
  • 中序线索化

在这里插入图片描述

/*  中序线索化  */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum {
    Link,
    Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {
    TElemType data;//数据域
    struct BiThrNode* lchild, * rchild;//左孩子,右孩子指针域
    PointerTag LTag, RTag;//标志域,枚举类型
}BiThrNode, * BiThrTree;


//创建二叉树
BiThrTree CreateBiTree();
//创建线索二叉树
void CreateInThread(BiThrTree T);
//中序遍历线索化
void InThread(BiThrTree T);
void Visit(BiThrNode* q);

//寻找p最左下结点
BiThrNode* FirstNode(BiThrNode* p);
//获取后继结点
BiThrNode* NextNode(BiThrNode* p);

//对中序线索二叉树进行中序遍历
void PrintElement(TElemType e);
void InOrder(BiThrTree T);

//寻找p最右下结点
BiThrNode* LastNode(BiThrNode* p);
//获取前驱结点
BiThrNode* PreNode(BiThrNode* p);
//对中序线索二叉树进行逆向中序遍历
void RevInOrder(BiThrTree T);

BiThrNode* pre = NULL;

int main() {
    BiThrTree T = NULL;
    printf("输入前序二叉树:\n");  // abd#g##e##cf###
    T = CreateBiTree();
    CreateInThread(T);
    printf("对中序线索二叉树进行中序遍历\n");
    InOrder(T); // d g b e a f c
    printf("\n对中序线索二叉树进行逆向中序遍历\n");
    RevInOrder(T); // c f a e b g d
    return 0;
}

//先序创建二叉树
BiThrTree CreateBiTree() {
    char ch;
    if (scanf(" %c", &ch) != 1) { // 检查输入是否成功
        // 输入失败,可能是文件结束符或错误输入
        exit(EXIT_FAILURE);
    }
    if (ch != '#') {
        BiThrNode* node = (BiThrNode*)malloc(sizeof(BiThrNode)); // 分配内存
        if (node == NULL) {
            // 内存分配失败
            perror("内存分配失败");
            exit(EXIT_FAILURE);
        }
        node->data = ch;
        node->lchild = CreateBiTree(); // 递归创建左子树
        node->rchild = CreateBiTree(); // 递归创建右子树
        node->LTag = Link;
        node->RTag = Link;
        return node;
    }
    else {
        return NULL;
    }
}

/*
对二叉树进行线索化
*/
void CreateInThread(BiThrTree T)
{
    pre = NULL;
    if (T)
    {
        InThread(T); // 中序线索化
        if (pre->rchild == NULL)
        {
            pre->RTag = Thread; // 处理最后一个结点
        }
    }
}

// 中序遍历 - 边遍历边线索化
void InThread(BiThrTree T)
{
    if (T)
    {
        InThread(T->lchild);
        Visit(T);
        InThread(T->rchild);
    }
}

void Visit(BiThrNode* q)
//线索化
{
    if (q->lchild == NULL)
    {
        q->lchild = pre;
        q->LTag = Thread;
    }
    if (pre != NULL && pre->rchild == NULL)
    {
        pre->rchild = q;
        pre->RTag = Thread;
    }
    pre = q;
}

//找中序后继:左根右
/*
① 若 p->RTag == 1,则 next = p->rchild
② 若 p->RTag == 0,则 next = p的右子树最左下结点
*/
BiThrNode* FirstNode(BiThrNode* p)
//寻找p最左下结点
{
    while (p->LTag == 0)
    {
        p = p->lchild;
    }
    return p;
}
BiThrNode* NextNode(BiThrNode* p)
//寻找p的后继结点
{
    if (p->RTag == 0)
    {
        return FirstNode(p->rchild);//寻找p的右子树最左下结点
    }
    else
    {
        return p->rchild;
    }
}

/*
对中序线索二叉树进行中序遍历
*/
void PrintElement(TElemType e)
{
    printf("%c ", e);
}

void InOrder(BiThrTree T)
{
    BiThrNode* p = FirstNode(T);
    for (p; p != NULL; p = NextNode(p))
    {
        PrintElement(p->data);
    }
}

//找中序前驱:左根右
/*
① 若 p->LTag == 1,则 pre = p->lchild
② 若 p->LTag == 0,则 pre = p的左子树最右下结点
*/
BiThrNode* LastNode(BiThrNode* p)
//寻找p最右下结点
{
    while (p->RTag == 0)
    {
        p = p->rchild;
    }
    return p;
}
BiThrNode* PreNode(BiThrNode* p)
//寻找p的前驱结点
{
    if (p->LTag == 0)
    {
        return LastNode(p->lchild);//寻找p的左子树最右下结点
    }
    else
    {
        return p->lchild;
    }
}
/*
对中序线索二叉树进行逆向中序遍历
*/
void RevInOrder(BiThrTree T)
{
    BiThrNode* p = LastNode(T);
    for (p; p != NULL; p = PreNode(p))
    {
        PrintElement(p->data);
    }
}
  • 后序线索化

    在这里插入图片描述

/*  后序线索化  */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum {
    Link,
    Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {
    TElemType data;//数据域
    struct BiThrNode* lchild, * rchild;//左孩子,右孩子指针域
    PointerTag LTag, RTag;//标志域,枚举类型
}BiThrNode, * BiThrTree;


//创建二叉树
BiThrTree CreateBiTree();
//创建线索二叉树
void CreatePostThread(BiThrTree T);
//后序遍历线索化
void PostThread(BiThrTree T);
void Visit(BiThrNode* q);

//寻找前驱
BiThrNode* PreNode(BiThrNode* p);
//逆向遍历
void PrintElement(TElemType e);
void RevPostOrder(BiThrTree T);


BiThrNode* pre = NULL;

int main() {
    BiThrTree T = NULL;
    printf("输入前序二叉树:\n");  // abd#g##e##cf###
    T = CreateBiTree();
    CreatePostThread(T);
    //正序:g d e b f c a
    //逆序:a c f b e d g
    RevPostOrder(T);
    return 0;
}

//先序创建二叉树
BiThrTree CreateBiTree() {
    char ch;
    if (scanf(" %c", &ch) != 1) { // 检查输入是否成功
        // 输入失败,可能是文件结束符或错误输入
        exit(EXIT_FAILURE);
    }
    if (ch != '#') {
        BiThrNode* node = (BiThrNode*)malloc(sizeof(BiThrNode)); // 分配内存
        if (node == NULL) {
            // 内存分配失败
            perror("内存分配失败");
            exit(EXIT_FAILURE);
        }
        node->data = ch;
        node->lchild = CreateBiTree(); // 递归创建左子树
        node->rchild = CreateBiTree(); // 递归创建右子树
        node->LTag = Link;
        node->RTag = Link;
        return node;
    }
    else {
        return NULL;
    }
}

/*
对二叉树进行线索化
*/
void CreatePostThread(BiThrTree T)
{
    pre = NULL;
    if (T)
    {
        PostThread(T); // 后序线索化
        if (pre->rchild == NULL)
        {
            pre->RTag = Thread; // 处理最后一个结点
        }
    }
}

// 后序遍历 - 边遍历边线索化
void PostThread(BiThrTree T)
{
    if (T)
    {
        PostThread(T->lchild);
        PostThread(T->rchild);
        Visit(T);
    }
}

void Visit(BiThrNode* q)
//线索化
{
    if (q->lchild == NULL)
    {
        q->lchild = pre;
        q->LTag = Thread;
    }
    if (pre != NULL && pre->rchild == NULL)
    {
        pre->rchild = q;
        pre->RTag = Thread;
    }
    pre = q;
}

//找后序前驱:左右根
/*
① 若 p->LTag == 1,则 pre = p->lchild
② 若 p->LTag == 0,则 如果p有右孩子,pre=p的右孩子;如果p没有右孩子,pre=p的左孩子
*/

BiThrNode* PreNode(BiThrNode* p)
//寻找p的前驱结点
{
    if (p->LTag == 1)
    {
        return p->lchild;//寻找p的左子树最右下结点
    }
    else
    {
        if (p->rchild && p->RTag == 0)
        {
            return p->rchild;
        }
        else
        {
            return p->lchild;
        }
    }
}
/*
对后序线索二叉树进行逆向的后序遍历
*/
void PrintElement(TElemType e)
{
    printf("%c ", e);
}

void RevPostOrder(BiThrTree T)
{
    BiThrNode* p = T;
    for (p; p != NULL; p = PreNode(p))
    {
        PrintElement(p->data);
    }
}

6.4 树和森林

6.4.1 树的存储结构

  • 双亲表示法(顺序存储)

    在这里插入图片描述

    #define MAX_TREE_SIZE 100   //树中最多结点数
    typedef char ElemType;
    typedef struct {            //结点结构
        ElemType data;          //数据元素
        int parent;             //双亲位置域
    }PTNode;
    
    typedef struct {                   //树结构
        PTNode nodes[MAX_TREE_SIZE];  
        int r, n;                      //根的位置和结点数
    }PTree;
    

    可以根据结点的parent 指针很容易找到它的双亲结点,所用的时间复杂度为0(1),直到parent为-1时,表示找到了树结点的根。
    要找到结点的孩子只能遍历整个结构。

  • 孩子表示法(顺序+链式存储)

    在这里插入图片描述

    #define MAX_TREE_SIZE 100   //树中最多结点数
    typedef char ElemType;
    
    typedef struct CTNode{   //孩子结点
        int child;
        struct CTNode* next;
    }* ChildPtr;
    
    typedef struct {
        ElemType data;
        ChildPtr firstchild;  //孩子链表头指针
    }CTBox;
    
    typedef struct {
        CTBox nodes[MAX_TREE_SIZE];
        int n, r; //结点数和根的位置
    }CTree;
    

    查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树就对头结点的数组循环即可。但是查找某个结点的双亲, 需要整棵树遍历才行。

  • 孩子兄弟表示法(链式存储)

    在这里插入图片描述

    typedef struct CSNode{  
        ElemType data;
        struct CSNode * firstchild, *nextsibling;
    }CSNode, * CSTree;
    

6.4.2 森林与二叉树的转换

  • 树转换为二叉树

    在这里插入图片描述

  • 森林转化为二叉树

    在这里插入图片描述

  • 二叉树转化为森林

    在这里插入图片描述

6.4.3 树和森林的遍历

  • 树的遍历

    1️⃣先根遍历 / 深度优先遍历

    ​ 先访问树的根结点,然后依次先根遍历根的每棵子树

    2️⃣后根遍历 / 深度优先遍历

    ​ 先依次后根遍历每棵子树,然后访问根结点

    3️⃣层次遍历(用队列实现) / 广度优先遍历

    ​ ①若树非空,则根节点入队

    ​ ②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队

    ​ ③重复②直到队列为空

    在这里插入图片描述

  • 森林的遍历

    1️⃣先序遍历

    ​ 若森林为非空,则按如下规则进行遍历:

    ​ 访问森林中第一棵树的根结点。

    ​ 先序遍历第一棵树中根结点的子树森林。

    ​ 先序遍历除去第一棵树之后剩余的树构成的森林。

    2️⃣中序遍历

    ​ 若森林为非空,则按如下规则进行遍历:

    ​ 中序遍历森林中第一棵树的根结点的子树森林。

    ​ 访问第一棵树的根结点。

    ​ 中序遍历除去第一棵树之后剩余的树构成的森林。

    在这里插入图片描述

6.5 树与等价问题

6.5.1 等价定义

  • 等价关系

    A是一个非空集,R是A上的一个二元关系,若R有自反性对称性、传递性,则说R是A上的等价关系。

    设R是集合A上的一个二元关系,即R ⊆ A x A。
    定义1:对于任意的x∈A,均有(x,x)∈R, 则称关系R有自反性或称R是A上的自反关系。
    定义2: 对于任意的x,y∈A,若(x,y)∈R,就有(y,x)∈ R,则称关系R有对称性,或称R是A上的对称关系。
    定义3: 对于任意的x,y,z∈A,若(x,y)∈ R且(y,z)∈R,就有(x,z)∈R,则称关系R有传递性,或称R是A上的传递关系。

  • 等价类

    设R是集合S的等价关系。对任何x∈S,由[x]R = {y|y∈S ∧ xRy} 给出的集合[ x ]R ⊆ S 称 为由于x ∈ S生成的一个R等价类。

    (设R是定义在集合A上的等价关系。与A中的一个元素a有关系的所有元素的集合叫作a的等价类。A的关于R的等价类记作[a]R。当只考虑一个关系时,我们将省去下标R并把这个等价类写作[a]。)

    若R是集合S上的一个等价关系,则由这个等价关系可产生这个集合的唯一划分,即可按R将S划分为若干不相交的子集S1, S2,…,他们的并为S,则这些子集Si便为S的等价类。

    在这里插入图片描述

6.5.2 划分等价类的方法

​ (1) 令S中每个元素各自形成一个只含单个成员的子集,记作 S1, S2 , …, Sn

​ (2) 重复读入m个偶对,对每个读入的偶对 (x,y) 判定 所属子集。

​ 假设 x∈Si,y∈Sj,若 Si ≠ Sj,则将Si并入Sj并置Si为空(或将Sj并入Si并置 Sj空)。

​ (3) 当m个偶对都被处理过后, S1, S2 , …, Sn中所有非空子集即为S的R等价类。

6.5.3 划分等价类的具体操作 - 并查集MFSet

  • 其一是构造只含单个成员的集 合;
  • 其二是判定某个单元素所在子集;(查)
  • 其三是归并两个互不相交的集合为一个集合。(并)

MFSet的ADT:

ADT MFSet{
    数据对象: 若设S是MFSet型的集合,则它由 n(n>O)个子集Si, (i = 12... ,n) 构成,每个子集的成员都是子界[-maxnumber .. maxnumber 内的整数;
	数据关系: S1∪S2∪ … ∪Sn=S Si⊂S(i=l,2,•••,n)
	基本操作:
		Initial(&S, n, x1 , x2 ,……,xn);
        操作结果:初始化操作。构造一个由 个子集(每个子集只含单个成员 xi) 构成的集合S
		Find(S, x); 
		初始条件:S是已存在的集合, x是S中某个子集的成员。
		操作结果:查找函数。确定S中x所属子集Si
		Merge(&S, i, j);
		初始条件:Si,Sj是S中的两个互不相交的非空集合。
		操作结果:归并操作。将Si和Sj中的一个并入另一个中
}ADT MFSet;

6.5.4 用树型结构表示集合

  • 以森林F =( T1 , T2 , . . . , Tn )表示MFSet型的集合S

​ 森林中的每一棵树Ti ( T1 , T2 , . . . , Tn )表示S中的一个元素——子集 Si(Si ⊂ S, i= 1,2, …,n)

​ 树中的每个结点表示对应子集Si中的一个成员x

​ 令每个结点含有一个指向其双亲的指针,并约定根结点的成员兼作子集的名字

以采用双亲表示法来作树的存储结构

  • 由于各子集成员均不相同,"并操作"只需将一棵子集树的根指向另一子集树的根即可;
  • 完成"查找"某个成员所在集合的操作,只需从该成员结点出发,顺链而进,直至找到树的根结点为止.。
// ---- 树的双亲表存储表示 ----
#define MAX_TREE_SIZE 100   //树中最多结点数
typedef char ElemType;
typedef struct {            //结点结构
    ElemType data;          //数据元素
    int parent;             //双亲位置域
}PTNode;

typedef struct {                   //树结构
    PTNode nodes[MAX_TREE_SIZE];  
    int r, n;                      //根的位置和结点数
}PTree;

//---- MFSet的树的双亲存储表示 ----
typedef PTree MFSet;

6.5.5 并查集及其优化

优化前最坏的时间复杂度

​ 查:=最坏树高=O(n)

​ 将n个独立元素通过多次Union合并为一个集合:O(n2)

并优化后树高不超过 ⌊log2n⌋+1 ,最坏的时间复杂度

​ 查:O(log2n)

​ 将n个独立元素通过多次Union合并为一个集合:O(nlog2n)

find优化后最坏的时间复杂度

​ 查:O(α(n))

​ 将n个独立元素通过多次Union合并为一个集合:O(nα(n))

​ α(n) 是一个增长极其缓慢的函数,对于通常所见到的正整数n而言, α(n) ≤ 4

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

// ---- 树的双亲表存储表示 ----
#define MAX_TREE_SIZE 100   //树中最多结点数
typedef char ElemType;
typedef struct {            //结点结构
    ElemType data;          //数据元素
    int parent;             //双亲位置域
}PTNode;

typedef struct {                   //树结构
    PTNode nodes[MAX_TREE_SIZE];
    int r, n;                      //根的位置和结点数
}PTree;

//---- MFSet的树的双亲存储表示 ----
typedef PTree MFSet;

void Initial(MFSet* mfset, ElemType elements[], int parent[], int size)
{
    mfset->n = size; // 设置结点数
    mfset->r = -1;   // 假设根节点的双亲位置为-1

    for (int i = 0; i < size; i++) {
        mfset->nodes[i].data = elements[i]; // 设置数据元素
        mfset->nodes[i].parent = parent[i]; // 设置双亲位置
    }
}


// 打印MFSet
void printMFSet(MFSet mfset) {
    printf("MFSet:\n");
    for (int i = 0; i < mfset.n; i++) {
        printf("Node %d: Data = %c, Parent = %d\n", i, mfset.nodes[i].data, mfset.nodes[i].parent);
    }
}

//查 - 找i所属集合的根节点  最坏的时间复杂度O(d), 其中d是树的深度
int Find_MFSet(MFSet s, int i)
{
    if (i<0 || i>s.n - 1)
    {
        return -2;
    }
    int j;
    for (j = i; s.nodes[j].parent >= 0; j = s.nodes[j].parent);
    return j;
}

//并 -i,j两个集合合并为一个  时间复杂度O(1)
void Merge_MFSet(MFSet* s, int i, int j)
{
    if (i<0 || i>(*s).n - 1 || j<0 || j>(*s).n - 1)
    {
        return -2;
    }
    if (i == j)
    {
        return -2;
    }
    (*s).nodes[j].parent = i; //j并到i
}



int main() {
    ElemType elements[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M' };
    int parent[] = { -1, 0, -1, -1, 1, 1, 2, 3, 3, 3, 4, 4, 7 };
    int size = sizeof(elements) / sizeof(elements[0]);
    MFSet myMFSet;

    Initial(&myMFSet, elements, parent, size);
    printMFSet(myMFSet);

    int gen = Find_MFSet(myMFSet, parent[6]);
    printf("G所在集合的根节点:%d, 根节点为:%c\n", gen, myMFSet.nodes[gen].data);
    Merge_MFSet(&myMFSet, 0, 2); // C集合合并到A集合
    printMFSet(myMFSet);
    int gen1 = Find_MFSet(myMFSet, parent[6]);
    printf("G所在集合的根节点:%d, 根节点为:%c\n", gen1, myMFSet.nodes[gen1].data);
    return 0;
}

并优化

  • 在作“并”操作之前先判别子集中所含成员的数目,然后令含成员少的子集树根结点指向含成员多的子集的根

    在这里插入图片描述

    #define _CRT_SECURE_NO_WARNINGS 1
    #include <stdio.h>
    #include <stdlib.h>
    
    // ---- 树的双亲表存储表示 ----
    #define MAX_TREE_SIZE 100   //树中最多结点数
    typedef char ElemType;
    typedef struct {            //结点结构
        ElemType data;          //数据元素
        int parent;             //双亲位置域
    }PTNode;
    
    typedef struct {                   //树结构
        PTNode nodes[MAX_TREE_SIZE];
        int r, n;                      //根的位置和结点数
    }PTree;
    
    //---- MFSet的树的双亲存储表示 ----
    typedef PTree MFSet;
    
    void Initial(MFSet* mfset, ElemType elements[], int parent[], int size)
    {
        mfset->n = size; // 设置结点数
        mfset->r = -1;   // 假设根节点的双亲位置为-1
    
        for (int i = 0; i < size; i++) {
            mfset->nodes[i].data = elements[i]; // 设置数据元素
            mfset->nodes[i].parent = parent[i]; // 设置双亲位置
        }
    }
    
    
    // 打印MFSet
    void printMFSet(MFSet mfset) {
        printf("MFSet:\n");
        for (int i = 0; i < mfset.n; i++) {
            printf("Node %d: Data = %c, Parent = %d\n", i, mfset.nodes[i].data, mfset.nodes[i].parent);
        }
    }
    
    //并的优化 
    void Mix_MFSet(MFSet* s, int i, int j)
    {
        if (i<0 || i>(*s).n - 1 || j<0 || j>(*s).n - 1)
        {
            return -2;
        }
        if (s->nodes[i].parent > s->nodes[j].parent)
        {
            //Si 所含成员数比 Sj少
            s->nodes[j].parent += s->nodes[i].parent;
            s->nodes[i].parent = j;
    
        }
        else
        {
            s->nodes[i].parent += s->nodes[j].parent;
            s->nodes[j].parent = i;
        }
    }
    
    
    int main() {
        printf("------------------------优化-----------------------");
        ElemType elements[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M' };
        int parent[] = { -6, 0, -2, -5, 1, 1, 2, 3, 3, 3, 4, 4, 7 };
        int size = sizeof(elements) / sizeof(elements[0]);
        MFSet myMFSet;
        Initial(&myMFSet, elements, parent, size);
        printMFSet(myMFSet);
        printf("----------------------合并A和C----------------------");
        Mix_MFSet(&myMFSet, 0, 2);
        printMFSet(myMFSet);
       /* printf("----------------------合并C和D----------------------");
        Mix_MFSet(&myMFSet, 2, 3);
        printMFSet(myMFSet);*/
        return 0;
    }
    

查找优化

  • 压缩路径–Find 操作,先找到根节点,再将查找路径上所有结点都挂到根结点下

    在这里插入图片描述

    #define _CRT_SECURE_NO_WARNINGS 1
    #include <stdio.h>
    #include <stdlib.h>
    
    // ---- 树的双亲表存储表示 ----
    #define MAX_TREE_SIZE 100   //树中最多结点数
    typedef char ElemType;
    typedef struct {            //结点结构
        ElemType data;          //数据元素
        int parent;             //双亲位置域
    }PTNode;
    
    typedef struct {                   //树结构
        PTNode nodes[MAX_TREE_SIZE];
        int r, n;                      //根的位置和结点数
    }PTree;
    
    //---- MFSet的树的双亲存储表示 ----
    typedef PTree MFSet;
    
    void Initial(MFSet* mfset, ElemType elements[], int parent[], int size)
    {
        mfset->n = size; // 设置结点数
        mfset->r = -1;   // 假设根节点的双亲位置为-1
    
        for (int i = 0; i < size; i++) {
            mfset->nodes[i].data = elements[i]; // 设置数据元素
            mfset->nodes[i].parent = parent[i]; // 设置双亲位置
        }
    }
    
    
    // 打印MFSet
    void printMFSet(MFSet mfset) {
        printf("MFSet:\n");
        for (int i = 0; i < mfset.n; i++) {
            printf("Node %d: Data = %c, Parent = %d\n", i, mfset.nodes[i].data, mfset.nodes[i].parent);
        }
    }
    
    //查 - 优化(压缩路径)
    int Find_MFSet(MFSet *s, int i)
    {
        if (i<0 || i>s->n-1)
        {
            return -2;
        }
        int root = i;
        while (s->nodes[root].parent >= 0)
        {
            root = s->nodes[root].parent;
        }
        while (i != root)
        {
            int t = s->nodes[i].parent;
            s->nodes[i].parent = root;
            i = t;
        }
        return root;
    }
    
    int main() {
        ElemType elements[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M' };
        int parent[] = { -1, 0, -1, -1, 1, 1, 2, 3, 3, 3, 4, 4, 7 };
        int size = sizeof(elements) / sizeof(elements[0]);
        MFSet myMFSet;
    
        Initial(&myMFSet, elements, parent, size);
        printMFSet(myMFSet);
    
        printf("----------------------查找优化----------------------\n");
        int gen = Find_MFSet(&myMFSet, 11);
        printf("G所在集合的根节点:%d, 根节点为:%c\n", gen, myMFSet.nodes[gen].data);
        printMFSet(myMFSet);
        return 0;
    }
    

6.6 赫夫曼树及其应用

6.6.1 最优二叉树(赫夫曼树)

  • 从树中一个结点到另一个结点之间的分支构成这 两个结点之间的路径,路径上的分支数目称做路径长度

  • 树的路径长度是从树根到每一 结点的路径长度之和。

  • 树的带权路径长度(Weighted Path Length of Tree,简记为WPL):定义为树中所有叶子结点的带权路径长度之和.

    • 结点的权值:在一些应用中,赋予树中结点的一个有某种意义的实数。

    • 结点的带权路径长度:从该结点到树根之间的路径长度与该结点上权值的乘积。

  • 给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树(Full Binary Tree)或者哈夫曼树(Huffman Tree)

    特点:

    • 叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。

    • 最优二叉树中,权越大的叶子离根越近。

    • 最优二叉树的形态不唯一,WPL最小。

      在这里插入图片描述

6.6.2 赫夫曼编码

任一字符的编码都不是另一个字符的编码的前缀,这种编码称为前缀码

在这里插入图片描述

在这里插入图片描述

//方法一 - 从叶子到根
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) {
    *HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
    char* cd = (char*)malloc(n * sizeof(char)); //存放结点哈夫曼编码的字符串数组
    cd[n - 1] = '\0';//字符串结束符

    for (int i = 1; i <= n; i++) {
        //cd数组的下标,用于逆序存储到cd数组
        int start = n - 1;
        //当前结点在数组中的位置
        int c = i;
        //当前结点的父结点在数组中的位置
        int j = HT[i].parent;
        // 一直寻找到根结点
        while (j != 0) {
            // 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
            if (HT[j].lchild == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
            //以父结点为孩子结点,继续朝树根的方向遍历
            c = j;
            j = HT[j].parent;
        }
        //跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
        (*HC)[i] = (char*)malloc((n - start) * sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    //使用malloc申请的cd动态数组需要手动释放
    free(cd);
}

在这里插入图片描述

// 方法二: 从根到叶子结点
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) {
    *HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
    int m = 2 * n - 1;
    int p = m;
    int cdlen = 0;
    char* cd = (char*)malloc(n * sizeof(char));
    

    for (int i = 1; i <= m; i++) {
        HT[i].weight = 0;
    }
    //一开始 p 初始化为 m,也就是从树根开始。一直到p为0
    while (p) {
        //如果当前结点一次没有访问,进入这个if语句
        if (HT[p].weight == 0) {
            HT[p].weight = 1;//重置访问次数为1
            //如果有左孩子,则访问左孩子,并且存储走过的标记为0
            if (HT[p].lchild != 0) {
                p = HT[p].lchild;
                cd[cdlen++] = '0';
            }
            //当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
            else if (HT[p].rchild == 0) {
                (*HC)[p] = (char*)malloc((cdlen + 1) * sizeof(char));
                cd[cdlen] = '\0';
                strcpy((*HC)[p], cd);
            }
        }
        //如果weight为1,说明访问过一次,即是从其左孩子返回的
        else if (HT[p].weight == 1) {
            HT[p].weight = 2;//设置访问次数为2
            //如果有右孩子,遍历右孩子,记录标记值 1
            if (HT[p].rchild != 0) {
                p = HT[p].rchild;
                cd[cdlen++] = '1';
            }
        }
        //如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
        else {
            HT[p].weight = 0;
            p = HT[p].parent;
            --cdlen;
        }
    }
}

存储结构

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include<string.h>

typedef struct {
	unsigned int weight;
	unsigned int parent, lchild, rchild;
}HTNode, * HuffmanTree;  //动态分配数组存储赫夫曼树

typedef char** HuffmanCode;   //动态分配数组存储赫夫曼编码表

void Select(HuffmanTree HT, int end, int* s1, int* s2)
{
    int min1, min2;
    //遍历数组初始下标为 1
    int i = 1;

    /*找到还没构建树的结点*/
    //i取值[1,9) -> [1,n+1) -> [1,n]
    while (HT[i].parent != 0 && i < end) {
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;

    i++;
    while (HT[i].parent != 0 && i < end) {
        i++;
    }
    //对找到的两个结点比较大小,min2为大的,min1为小的
    if (HT[i].weight < min1) {
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }
    else {
        min2 = HT[i].weight;
        *s2 = i;
    }
    //两个结点和后续的所有未构建成树的结点做比较
    for (int j = i + 1; j < end; j++)
    {
        //如果有父结点,直接跳过,进行下一个
        if (HT[j].parent != 0) {
            continue;
        }
        //如果比最小的还小,将min2=min1,min1赋值新的结点的下标
        if (HT[j].weight < min1) {
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介于两者之间,min2赋值为新的结点的位置下标
        else if (HT[j].weight >= min1 && HT[j].weight < min2) {
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}

void CreateHuffmanTree(HuffmanTree* HT, int* w, int n)
{
    if (n <= 1) return; // 如果只有一个编码就相当于0
    int m = 2 * n - 1; // 赫夫曼树总结点数,n + (n - 1)【 4个结点-创建的赫夫曼树总共需要7个结点】
    *HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); // 0号位置不用
    // 初始化赫夫曼树中的所有结点
    if (*HT)
    {
        for (int i = 1; i <= n; i++)
        {
            ((*HT) + i)->weight = *(w + i - 1);
            ((*HT) + i)->parent = 0;
            ((*HT) + i)->lchild = 0;
            ((*HT) + i)->rchild = 0;
        }
        //从树组的下标 n+1 开始初始化赫夫曼树中除叶子结点外的结点
        for (int i = n + 1; i <= m; i++)
        {
            ((*HT) + i)->weight = 0;
            ((*HT) + i)->parent = 0;
            ((*HT) + i)->lchild = 0;
            ((*HT) + i)->rchild = 0;
        }
        //构建赫夫曼树
        for (int i = n + 1; i <= m; i++)
        {
            int s1, s2;
            Select(*HT, i, &s1, &s2);
            (*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;
        }
    }
    
}
//方法一实现:
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n)
{ // w存放n个字符的权值(均 >O), 构造赫夫曼树HT, 并求出n个字符的赫夫曼编码HC
    *HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
    char* cd = (char*)malloc(n * sizeof(char)); //存放结点哈夫曼编码的字符串数组
    cd[n - 1] = '\0';//字符串结束符

    for (int i = 1; i <= n; i++) {
        //从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
        int start = n - 1;
        //当前结点在数组中的位置
        int c = i;
        //当前结点的父结点在数组中的位置
        int j = HT[i].parent;
        // 一直寻找到根结点
        while (j != 0) {
            // 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
            if (HT[j].lchild == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
            //以父结点为孩子结点,继续朝树根的方向遍历
            c = j;
            j = HT[j].parent;
        }
        //跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
        (*HC)[i] = (char*)malloc((n - start) * sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    //使用malloc申请的cd动态数组需要手动释放
    free(cd);
}

//打印哈夫曼编码的函数
void PrintHuffmanCode(HuffmanCode htable, int* w, int n)
{
    printf("Huffman code : \n");
    for (int i = 1; i <= n; i++)
        printf("%d code = %s\n", w[i - 1], htable[i]);
}
int main()
{
    int w[8] = { 5,29,7,8,14,23,3,11 };
    int n = 8;
    HuffmanTree htree;
    HuffmanCode htable;
    CreateHuffmanTree(&htree, w, n);
    HuffmanCoding(htree, &htable, n);
    PrintHuffmanCode(htable, w, n);
	return 0;
}

在这里插入图片描述

6.7 回溯法与树的遍历

回溯法也可以叫做回溯搜索法,它是⼀种搜索的⽅式

回溯法(backtracking)是设计递归过程的一种重要方法,它的求解过程实质上是一个先序遍历一棵“状态树”的过程,只是这棵树不是遍历前预先建立的,而是隐含在遍历过程中。回溯是递归的副产品,只要有递归就会有回溯。

可以解决的问题:

  • 组合:N个数⾥⾯按⼀定规则找出k个数的集合(无序)
  • 切割:⼀个字符串按⼀定规则有⼏种切割⽅式
  • 子集:⼀个N个数的集合⾥有多少符合条件的⼦集
  • 排列:N个数按⼀定规则全排列,有⼏种排列⽅式(有序)
  • 棋盘:N皇后,解数独等等

模板框架:

在这里插入图片描述

void backtracking(参数) {
	if (终⽌条件) {
		存放结果;
		return;
	}
	for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
		处理节点;
		backtracking(路径,选择列表); // 递归
		回溯,撤销处理结果
	}
}
  • 组合问题

    • 给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

      示例: 输⼊: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

      在这里插入图片描述

      #include <stdio.h>
      #include <stdlib.h>
      
      typedef struct {
          int** result;
          int* path;
          int resultSize;
          int pathSize;
      } Solution;
      
      void backtracking(Solution* sol, int n, int k, int startIndex) {
          if (sol->pathSize == k) {
              sol->result[sol->resultSize] = (int*)malloc(k * sizeof(int));
              if (sol->result[sol->resultSize])
              {
                  for (int i = 0; i < k; i++) {
                      sol->result[sol->resultSize][i] = sol->path[i];
                  }
                  sol->resultSize++;
                  return;
              }
              return;
          }
          for (int i = startIndex; i <= n; i++) {
              sol->path[sol->pathSize] = i;
              sol->pathSize++;
              backtracking(sol, n, k, i + 1);
              sol->pathSize--; // 回溯,撤销处理的节点
          }
      }
      
      void combine(Solution* sol, int n, int k) {
          sol->resultSize = 0;
          sol->pathSize = 0;
          sol->result = (int**)malloc(10000 * sizeof(int*)); // 假设结果集不会超过10000个
          sol->path = (int*)malloc(k * sizeof(int));
          backtracking(sol, n, k, 1);
      }
      
      void printSolution(Solution* sol, int k) {
          for (int i = 0; i < sol->resultSize; i++) {
              for (int j = 0; j < k; j++) {
                  printf("%d ", sol->result[i][j]);
              }
              printf("\n");
              free(sol->result[i]); // 释放每一行的内存
          }
          free(sol->result); // 释放结果集的内存
          free(sol->path); // 释放路径的内存
      }
      
      int main() {
          Solution sol;
          int n = 4;
          int k = 2;
          combine(&sol, n, k);
          printSolution(&sol, k);
          return 0;
      }
      
    • 剪枝操作

      在这里插入图片描述

      void backtracking(Solution* sol, int n, int k, int startIndex) {
          if (sol->pathSize == k) {
              sol->result[sol->resultSize] = (int*)malloc(k * sizeof(int));
              if (sol->result[sol->resultSize])
              {
                  for (int i = 0; i < k; i++) {
                      sol->result[sol->resultSize][i] = sol->path[i];
                  }
                  sol->resultSize++;
                  return;
              }
              return;
          }
          for (int i = startIndex; i <= n - (k - sol->pathSize) + 1; i++) {
              sol->path[sol->pathSize] = i;
              sol->pathSize++;
              backtracking(sol, n, k, i + 1);
              sol->pathSize--; // 回溯,撤销处理的节点
          }
      }
      
    • 组合总和

      找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 说明: 所有数字都是正整数。 解集不能包含重复的组合。输⼊: k = 2, n = 4

      在这里插入图片描述

      #include <stdio.h>
      #include <stdlib.h>
      
      typedef struct {
          int** result;
          int* path;
          int resultSize;
          int pathSize;
      } Solution;
      
      
      // targetSum:⽬标和,也就是题⽬中的n。
      // k:题⽬中要求k个数的集合。
      // sum:已经收集的元素的总和,也就是path⾥元素的总和。
      // startIndex:下⼀层for循环搜索的起始位置。
      void backtracking(Solution* sol, int targetSum, int n, int k, int sum, int startIndex) {
          if (sol->pathSize == k) {
              if (sum > targetSum) return; // 剪枝操作
              if (sum == targetSum)
              {
                  sol->result[sol->resultSize] = (int*)malloc(k * sizeof(int));
                  if (sol->result[sol->resultSize])
                  {
                      for (int i = 0; i < k; i++) {
                          sol->result[sol->resultSize][i] = sol->path[i];
                      }
                      sol->resultSize++;
                      return;
                  }
              }
              return;
          }
          // 剪枝操作
          for (int i = startIndex; i <= n - (k - sol->pathSize) + 1; i++) {
              sum += i;
              sol->path[sol->pathSize] = i;
              sol->pathSize++;
              backtracking(sol, targetSum, n, k, sum, i + 1);
              sum -= i;
              sol->pathSize--; // 回溯,撤销处理的节点
          }
      
      }
      
      void combine(Solution* sol, int n, int k, int targetSum) {
          sol->resultSize = 0;
          sol->pathSize = 0;
          sol->result = (int**)malloc(10000 * sizeof(int*)); // 假设结果集不会超过10000个
          sol->path = (int*)malloc(k * sizeof(int));
          backtracking(sol, targetSum, n, k, 0, 1);
      }
      
      void printSolution(Solution* sol, int k) {
          for (int i = 0; i < sol->resultSize; i++) {
              for (int j = 0; j < k; j++) {
                  printf("%d ", sol->result[i][j]);
              }
              printf("\n");
              free(sol->result[i]); // 释放每一行的内存
          }
          free(sol->result); // 释放结果集的内存
          free(sol->path); // 释放路径的内存
      }
      
      int main() {
          Solution sol;
          int n = 9;
          int target_sum = 11;
          int k = 3;
          combine(&sol, n, k, target_sum);
          printSolution(&sol, k);
          return 0;
      }
      
  • N皇后

    在n×n格的棋盘上放置彼此不受攻击的n个皇后

    皇后的约束条件: 1. 不能同⾏ 2. 不能同列 3. 不能同斜线

    在这里插入图片描述

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    
    bool isSafe(int* queens, int row, int col) {
        for (int i = 0; i < row; i++) {
            if (queens[i] == col || abs(row - i) == abs(col - queens[i])) {
                return false;
            }
        }
        return true;
    }
    
    void backtrack(int* queens, int row, int N, char*** result, int* resultSize) {
        if (row == N) {
            // 当前解已经找到,将其存入结果数组
            result[*resultSize] = (char**)malloc(N * sizeof(char*));
            for (int i = 0; i < N; i++) {
                result[*resultSize][i] = (char*)malloc((N + 1) * sizeof(char));
                for (int j = 0; j < N; j++) {
                    result[*resultSize][i][j] = (queens[i] == j) ? 'Q' : '.';
                }
                result[*resultSize][i][N] = '\0';
            }
            (*resultSize)++;
            return;
        }
    
        for (int col = 0; col < N; col++) {
            if (isSafe(queens, row, col)) {
                queens[row] = col;
                backtrack(queens, row + 1, N, result, resultSize);
                queens[row] = -1;
            }
        }
    }
    
    
    char*** solveNQueens(int N, int* returnSize) {
        char*** result = (char***)malloc(1000 * sizeof(char**));
        *returnSize = 0;
    
        int* queens = (int*)malloc(N * sizeof(int));
        for (int i = 0; i < N; i++) {
            queens[i] = -1;
        }
    
        backtrack(queens, 0, N, result, returnSize);
        free(queens); // 释放queens数组
        return result;
    }
    
    // 打印所有合法的棋盘配置
    void printSolutions(char*** result, int N, int returnSize) {
        for (int i = 0; i < returnSize; i++) {
            for (int row = 0; row < N; row++) {
                printf("%s\n", result[i][row]);
            }
            printf("\n");
            for (int row = 0; row < N; row++) {
                free(result[i][row]); // 释放每一行的内存
            }
            free(result[i]); // 释放二维数组的内存
        }
        free(result); // 释放结果集的内存
    }
    
    int main() {
        int N = 4; // 例如,解决4皇后问题
        int returnSize;
        char*** result = solveNQueens(N, &returnSize);
    
        printf("Total %d solutions for %d Queens problem:\n", returnSize, N);
        printSolutions(result, N, returnSize);
        return 0;
    }
    

6.8 树的计数

含有n个结点的不相似的二叉树有 1 n + 1 C 2 n n \frac{1}{n+1} C^n_{2n} n+11C2nn

在这里插入图片描述

参考:

教材:

严蔚敏《数据结构》(C语言版).pdf

离散数学及其应用(原书第8版) (Kenneth H.Rosen) .pdf

《代码随想录》回溯算法(V3.0).pdf

视频:

https://www.bilibili.com/video/BV1Zf4y1a77g/?spm_id_from=333.788&vd_source=a89593e8d33b31a56b894ca9cad33d33

https://www.bilibili.com/video/BV1b7411N798/?p=51&spm_id_from=333.880.my_history.page.click&vd_source=a89593e8d33b31a56b894ca9cad33d33

https://www.bilibili.com/video/BV1pu411c7pf/?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=a89593e8d33b31a56b894ca9cad33d33

https://www.bilibili.com/video/BV17F411T7Ao?p=195&vd_source=a89593e8d33b31a56b894ca9cad33d33

博客:

https://blog.csdn.net/Real_Fool_/article/details/113930623

https://blog.csdn.net/m0_73633088/article/details/133443742

https://blog.csdn.net/wangrunmin/article/details/7831318

https://blog.csdn.net/Colorful___/article/details/133603913

https://blog.51cto.com/u_13360/6732847

https://zhuanlan.zhihu.com/p/600665635

https://blog.csdn.net/crr411422/article/details/129932889

https://blog.51cto.com/u_15773967/6148952

https://blog.csdn.net/2401_82584055/article/details/138349195

https://mp.weixin.qq.com/s?__biz=Mzg5MjkxNTA5MA==&mid=2247484467&idx=2&sn=3ea1749b1c7c301289a40dac4497e87e&chksm=c03798cef74011d8ead16e27ebea8e3a1df2a9a0242385d328f0ec05973e9b12ef1fa7254e14&scene=27

工具:

https://www.cs.usfca.edu/~galles/visualization/DisjointSets.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2155495.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode热题100】位运算

这篇博客先介绍了常见位运算操作&#xff0c;然后记录了关于位运算的几道题&#xff0c;包括判定字符是否唯一、丢失的数字、两整数之和、只出现一次的数字2、消失的两个数字。 在这一部分&#xff0c;我们不妨先来总结一下常见位运算操作&#xff1a; 1.基础位运算 >>…

vite 使用飞行器仪表示例

这里写自定义目录标题 环境vue代码效果图 环境 jquery npm install -S jqueryjQuery-Flight-Indicators 将img、css、js拷贝到vite工程目录中 打开 jquery.flightindicators.js&#xff0c;在文件开头加上import jQuery from "jquery"; vue代码 <template>&…

C#(.NET FrameWork库)逆向基础流程(纯小白教程)

一&#xff0c;例题链接 限时题目&#xff0c;只能用网盘来分享了&#xff0c;侵权联系删->百度网盘 请输入提取码 二&#xff0c;文件特征 使用工具查看文件信息&#xff0c; 能看到分析出文件编写语言为C#&#xff0c;使用了.NET库 三&#xff0c;做题流程 &#xff08…

浙版传媒思迈特软件大数据分析管理平台建设项目正式启动

近日&#xff0c;思迈特软件与出版发行及电商书城领域的领军企业——浙江出版传媒股份有限公司&#xff0c;正式启动大近日&#xff0c;思迈特软件与出版发行及电商书城领域的领军企业——浙江出版传媒股份有限公司&#xff0c;正式启动大数据分析管理平台建设项目。浙版传媒相…

Java之继承1

1. 继承 1.1 为什么要继承 在Java中我们定义猫类和狗类&#xff0c;如下 public class Cat {public String name;public int age;public String color;public void eat(){System.out.println(name "正在吃饭");}public void sleep(){System.out.println(name &qu…

基于pytorch本地部署微调bert模型(yelp文本分类数据集)

项目介绍 本项目使用hugging face上提供的Bert模型API&#xff0c;基于yelp数据集&#xff0c;在本地部署微调Bert模型&#xff0c;官方的文档链接为https://huggingface.co/docs/transformers/quicktour&#xff0c;但是在官方介绍中出现了太多的API调用接口&#xff0c;无法…

React 中的延迟加载

延迟加载是 Web 开发中的一种有效的性能优化技术&#xff0c;尤其是对于 React 等库和框架。它涉及仅在需要时加载组件或资源&#xff0c;无论是响应用户操作还是当元素即将在屏幕上显示时。这可以减少应用程序的初始加载时间&#xff0c;减少资源消耗&#xff0c;并改善用户体…

ETLCloud:新一代ETL数据抽取工具的定义与革新

数据集成、数据治理已经成为推动企业数字化转型的核心动力&#xff0c;现在的企业比任何时候都需要一个更为强大的新一代数据集成工具来处理、整合并转化多种数据源。 而ETL&#xff08;数据提取、转换、加载&#xff09;作为数据管理的关键步骤&#xff0c;已在企业数据架构中…

串口助手的qt实现思路

要求实现如下功能&#xff1a; 获取串口号&#xff1a; foreach (const QSerialPortInfo &serialPortInfo, QSerialPortInfo::availablePorts()) {qDebug() << "Port: " << serialPortInfo.portName(); // e.g. "COM1"qDebug() <<…

【JavaEE】——线程的安全问题和解决方式

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能够帮助到你&#xff01; 目录 一&#xff1a;问题引入 二&#xff1a;问题深入 1&#xff1a;举例说明 2&#xff1a;图解双线程计算…

SwiftUI 实现关键帧动画

实现一个扫描二维码的动画效果&#xff0c;然而SwiftUI中没有提供CABasicAnimation 动画方法&#xff0c;该如何实现这种效果&#xff1f;先弄清楚什么关键帧动画&#xff0c;简单的说就是指视图从起点至终点的状态变化&#xff0c;可以是形状、位置、透明度等等 本文提供了一…

(done) 声音信号处理基础知识(3) (一个TODO: modulation 和 timbre 的关联)(强度、响度、音色)

来源&#xff1a;https://www.youtube.com/watch?vJkoysm1fHUw sound power 通常可以被认为是能量传输的速率 声源往所有方向传输的每时间单位能量 用 瓦特(W) 作为单位测量 Sound intensity 声音强度&#xff0c;每单位面积的 sound power W/m^2 人类实际上能听到非常小强…

八. 实战:CUDA-BEVFusion部署分析-coordTrans Precomputation

目录 前言0. 简述1. 案例运行2. coordTrans3. Precomputation总结下载链接参考 前言 自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考 本次课程我们来学习下课程第八章—实战&#xff1a;CUDA-BEVFusion部署分…

Python Selenium 自动化爬虫 + Charles Proxy 抓包

一、场景介绍 我们平常会遇到一些需要根据省、市、区查询信息的网站。 1、省市查询 比如这种&#xff0c;因为全国的省市比较多&#xff0c;手动查询工作量还是不小。 2、接口签名 有时候我们用python直接查询后台接口的话&#xff0c;会发现接口是加签名的。 而签名算法我…

keil5 MDK 最新版本官网下载(v5.40为例) ARM单片机环境搭建安装教程(STM32系列为例)

正所谓授之以鱼不如授之以渔。本文将细讲从官网下载keil5MDK来保证keil5为最新版本的实时性 &#xff08;注意新老版本可能出现版本兼容问题&#xff0c;若不放心&#xff0c;跟着老弟我一起下载5.40版本即可&#xff09; 目录 一、下载keil5 MDK 方法①:CSDN下载&#xff0…

计算机毕业设计 基于 Hadoop平台的岗位推荐系统 SpringBoot+Vue 前后端分离 附源码 讲解 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

【深入学习Redis丨第六篇】Redis哨兵模式与操作详解

〇、前言 哨兵是一个分布式系统&#xff0c;你可以在一个架构中运行多个哨兵进程&#xff0c;这些进程使用流言协议来接收关于Master主服务器是否下线的信息&#xff0c;并使用投票协议来决定是否执行自动故障迁移&#xff0c;以及选择哪个Slave作为新的Master。 文章目录 〇、…

Django 5 学习笔记 2024版

1. 官方中文文档 Django 文档 | Django 文档 | Django (djangoproject.com) 2. 第一个应用 博客 总目录 <1>依赖安装: pip install django <2> 创建 工程 myapp django-admin startproject myapp cd myapp <3>创建 应用 app > python manage.py s…

算法-排序算法(冒泡选择插入希尔快速归并堆计算)

1.算法概述 1.1什么是算法 算法是特定问题的求解步骤的描述&#xff0c;是独立存在的一种解决问题的思想和方法。对于算法而言计算机编程语言并不重要&#xff0c;可以用任何计算机编程语言来编写算法。 程序数据结构算法 1.2数据结构和算法的区别和联系 数据结构只是静态…

CentOS 7 YUM源不可用

CentOS 7 操作系统在2024年6月30日后将停止官方维护&#xff0c;并且官方提供的YUM源将不再可用。 修改&#xff1a;nano /etc/yum.repos.d/CentOS-Base.repo # CentOS-Base.repo [base] nameCentOS-$releasever - Base baseurlhttp://mirrors.aliyun.com/centos/$rel…