并查集和哈夫曼树

搜索树和平衡树的代码链接,今天的两种数据结构也在里面:
02_Tree · 柚求bing/C语言实现的数据结构 - 码云 - 开源中国 (gitee.com)icon-default.png?t=O83Ahttps://gitee.com/ice-ben/C_data-structure/tree/master/02_Tree

并查集

概述:

        并查集是一种数据结构,用于处理不交集的合并以及查询问题。例如:
以下的一张图中如何查询两个点是否相连,如何连接两个点,这都要用到今天的并查集。

        对于这两种操作,并查集合并和查询的其中两种,分别是quickFind和quickUnion。
        对与quickFind而言,它是将所有的元素进行ID分组管理,每一个元素对应一个ID号,若要是在查找时,只需要找ID号码是否相同即可,对于合并操作,直接将其中一组的ID号修改为另一组的ID号即可。因此对于quickFind而言,只需要本身数据,数据对应的id,以及初始化空间的大小(为了防止越界访问)即可。由于在查找时依据ID判断是否在同一个组内,因此查找效率高,时间复杂度为O(1),但是合并进行修改ID时,由于需要修改所有的ID,遍历这个并查集,因此修改效率低,时间复杂度为O(N),那么为了不是修改所有的ID,我们引入了quickUnion.
        对于quickUnion而言,它是将所有的元素进行树形管理,所有的元素都保存这它的父节点(quickFind是保存的ID),当它的父节点是自己的时候,那么这个节点就是根节点。对于寻找操作,只需要判断它的根节点是否相同,对于合并操作,将两个节点的根节点进行合并(其中一个根节点连接到另一个根节点)。因此对于quickUnion而言,我们可以有自己本身的数据,父节点,初始化空间的大小。可以根据具体的实现进行修改。
        quickUnion的一个优化,由于合并时如果将多的元素合并到少的元素(以1为例),在极端情况下可能会退化成链表,因此我们增加size,用来表示树的某元素的个数,将少的根节点插入多的根节点,可以减少数的高度。避免退化成链表。

代码实现:

 quickFind:

下标和data,dataID关系如下:

初始化quickFind时,给ID和数据进行内存分配,数量为n;释放quickFind时,如果数据存在进行数据释放,如果ID存在进行ID释放。这里比较简单,代码放到了文章开头的链接中。

对于数据的插入,需要知道插入那个并查集,插入什么数据,以及现有的并查集空间大小,插入时不仅要插入数据,还要进行ID插入,以表明新插入的数据属于哪一个组。代码如下:

void initQuickFindSet(QuickFindSet *setQF,const Element *data,int n) {
    for(int i = 0; i < n;i++) {
        setQF->data[i] = data[i];
        setQF->groupID[i] = i;
    }
}

        数据的寻找是需要知道从并查集找那个数据,如果找到返回数组下标,(注:这里的下标并不是ID)表示找到,找不到则返回-1。
        判断是否是在同一个组中要获取两个元素在并查集的位置,如果其中一个元素不在并查集中则表明不在同一个组中,找到两个下标的位置还需要判断ID是否相同,相同表示在同一组中,不同表示不在同一个组中。
        进行合并时获取两个数据的下标,根据下标获取两者的数据ID信息,对其中一组的ID信息进行修改,就完成了合并。三者的代码如下:

static int findIndex(QuickFindSet *setQF, Element e) {
    for(int i = 0;i < setQF->n; ++i) {
        if (setQF->data[i] == e) {
            return i;
        }
    }
    return -1;
}

int isSameQF(QuickFindSet *setQF,Element a,Element b) {
    int aIndex = findIndex(setQF,a);
    int bIndex = findIndex(setQF,b);
    if (aIndex == -1|| bIndex == -1) {
        return 0;
    }
    return setQF->groupID[aIndex]  == setQF->groupID[bIndex];
}

void unionQF(QuickFindSet *setQF,Element a,Element b) {
    int aIndex = findIndex(setQF,a);
    int bIndex = findIndex(setQF,b);
    for (int i = 0; i < setQF->n; ++i) {
        if (setQF->groupID[i] == aIndex) {
            setQF->groupID[i] = bIndex;
        }
    }
}

 quickUnion:

创造/释放/快速合并的并查集以及插入元素操作相较于quickFind,如果是size优化,增加了对于size的处理,类似于data和parent这里不在赘述。

查找元素的根节点如果找到了返回根节点的值,找不到继续递归寻找。
判断是否在同一个组内,依据查找的根节点进行比较,相同为1,不同为0.(这里只是一个标记,返回true和false也是一样的)。
对于合并,还是依据根节点,如果是根节点不同需要合并时,按照size优化,将少的元素根节点接入多的元素根节点,并对多的进行更新。代码如下:

static int findRootIndex(QuickUnionSet *setQU, int e) {  // 找父节点索引,即e
    if (setQU->parent[e] == e) {
        return e;
    } else {
        return findRootIndex(setQU, setQU->parent[e]);
    }
}

int isSameQU(QuickUnionSet *setQU,Element a,Element b) {
    int aRoot = findRootIndex(setQU,a);
    int bRoot = findRootIndex(setQU,b);
    if (aRoot == -1 || bRoot == -1) {
        return 0;
    }
    return aRoot == bRoot;
}

void unionQU(QuickUnionSet *setQU,Element a,Element b) {
    int aRoot = findRootIndex(setQU,a);
    int bRoot = findRootIndex(setQU,b);
    if (aRoot == -1 || bRoot == -1) {
        return;
    }
    if (aRoot != bRoot) {
        int aSize = setQU->size[aRoot];  // a 下的个数
        int bSize = setQU->size[bRoot];  // b 下的个数
        // if(aSize >= bSize) {
        //     setQU->parent[bRoot] = aRoot;
        //     setQU->size[aRoot] += bSize;  // 给a节点以及个数
        // } else{
        //     setQU->parent[aRoot] = bRoot;
        //     setQU->size[bRoot] = bSize;   // 给b节点以及个数
        // }
        // 另一种方式
        if(aSize > bSize) {
            setQU->parent[bRoot] = aRoot;
        } else if(aSize < bSize){
            setQU->parent[aRoot] = bRoot;
        } else {  // 相同
            setQU->parent[bRoot] = aRoot;
            ++setQU->size[bRoot];
        }
    }
}

哈夫曼树

概述:

        在开始哈夫曼树之前,先要了解哈夫曼树的一些相关名词,先是根据图形进行理解。
 

路径:在一棵树中,将一个节点到另一个节点的通路叫做路径。例如从节点a到节点c,就有一条路径。
路径长度:每经过一个节点,路径长度+1,例如,从根节点到c的路径长度为3.
节点的权:每个节点赋予的新值,例如a节点的5.
节点带权路径长度:路径长度*节点的权
树的带权路径长度:树中所有叶子节点的带权路径长度。

那么什么是哈夫曼树?
当n个带权节点构建一颗树时,如果构建的这颗树的带权路径长度最小,那么这棵树就是哈夫曼树。那么如何进行实现呢?

1.从n个权值选择最小的两个权值,对应的节点组成新的二叉树。
2.删除选择的权值,找剩下的最小的,重新构建新的根节点,直到用完所有带有权值的节点.
注意:新生成的权值也要参与在内

因此哈夫曼树需要定义节点的权值,左右孩子以及每个节点的父节点。
大家可以完成以下哈夫曼树的构建。
 

填写以下表格:
 

结果如下,自行对答案:
 

代码实现:

初始化:

构建哈夫曼树:申请2n个节点。为什么?根据以上的哈夫曼树的图形可以看出,实际上的叶子节点个数为n,但是构建哈夫曼树时节点之间要有一些节点相连接(看上面的哈夫曼图),总个数为2n-1,但是我们在实际使用这个数组(原数据存到数组中)时,是从下标为1开始使用的,因此是2n-1+1个节点。
初始化2n-1个节点时,先对父节点,两个子节点以及权值进行初始化,由于申请出的空间存入的值是未知的,注意:由于是从第一个位置进行的使用,因此for循环是从[1,m],其中m=2*n-1。然后进行权值的正确赋值,由于权值是从0下标开始的,因此在循环中注意正确的赋值位置,即:tree[i].w=w[i-1].代码如下:

    int m = 2 * n - 1;   // 储存Huffman树的总结点
    // 1.1申请2n个结点(第一个位置0不使用)
    tree = malloc(sizeof (HuffmanNode)*(m + 1));
    // 1.2 初始化1~2n - 1 个结点
    for(int i = 1;i <= m;++i) {
        tree[i].parent = tree[i].lChild = tree[i].rChild = 0;
        tree[i].weight = 0;
    }
    // 1.3 设置初始值的权值
    for(int i = 1;i <= n;++i) {
        tree[i].weight = w[i - 1];   // 由于w是从下标为0开始
    }

构建树:

        构建树是[n+1,m]的范围,第一步就是在原有的基础上生成新的带有权值的点,因此我们先写一个selectNode函数用来选择正确的值;

        先假定一个最小值为0(约定节点的权值大于0),然后从未连接到树的节点中寻找最小值,即if(tree[i].parent == 0),接下来是继续寻找,如果比当前获取的最小值小,更新最小值,写完这两个循环后,就找到第一个最小值。接下来还是按照此方法并且加上找的值不等于原来找到的最小值,经过此轮寻找后,就成功找到了两个最小值。由于不能返回两个值,这里函数传入两个函数指针。代码如下:

static void selectNode(HuffmanTree tree,int n,int *s1,int *s2) {
    int min = 0;     // 表示最小值的索引号
    for(int i = 1;i <= n;++i) {
        if(tree[i].parent == 0) {
            min = i;
            break;
        }
    }
    for(int i = 1; i <= n;++i) {
        if(tree[i].parent == 0) {
            if (tree[i].weight < tree[min].weight) {
                min = i;
            }
        }
    }
    *s1 = min;
    for (int i = 1; i <= n; ++i) {
        if (tree[i].parent == 0 && i != *s1) {
            min = i;
            break;
        }
    }
    for (int i = 1; i <= n; ++i) {
        if (tree[i].parent == 0 && i != *s1) {
            if (tree[i].weight < tree[min].weight) {
                min = i;
            }
        }
    }
    *s2 = min;
}

接下来更新父节点,孩子节点以及节点的权值。

哈夫曼编码:

        上面我们构建了哈夫曼树,我们可以从根节点出发,遍历到每个叶子节点,规定左子树为0,右子树为1,从根节点到也是节点的0和1的序列为该字符的哈夫曼编码。以上面的哈夫曼树为例:A节点的哈夫曼编码是0001。那么我们怎么寻找哈夫曼编码呢?
        可以利用我们建立的表格,还是以A为例,A的父节点为9,9的右孩子是A,因此得到“1”,9的父节点是11,11的左孩子是9,因此得到“0”,11的父节点是13,13的左孩子是11,因此得到“0”,13的父节点是15,15的左孩子是13,因此得到“0”,15是根节点结束。得到的倒叙排序得到“0001”

        由于不同位置的节点哈夫曼编码长度不同,如果每个位置的节点申请最大空间(这里指节点个数n),那么对于一些占位小的字符造成空间浪费,为了合理利用空间,因此我们需要申请临时空间temp来存储倒序序列,实际空间code为存到temp的字符个数,由于申请的空间内容杂乱,因此我们可以用memset进行清理,需要注意的是memset用于将一块内存区域中的所有字节设置为一个特定的值,但是我们这里不是字节,需要的是Huffmancode这个结构体中内存的清理,因此需要传入sizeof(HuffmanCode)*n来指定初始化的内存区域大小。

        由以上的寻找过程可知,需要临时空间的索引start  用于 表示存放的位置,父节点的信息p(即找到了父节点是谁), 以及pos来判断父节点是通过左孩子还要右孩子进入的(用于 判断是1还是0,),开始位置是n-1(因为从下往上找是倒序),为了结束还需要在temp数组中放结束标志"/0",当此节点的父节点存在时,一直向上走,直到父节点不存在。最后将temp复制到codes中,最后释放temp代码如下:

HuffmanCode *createHuffmanCode(HuffmanTree tree, int n) {
    // 求当前字符的倒序构造的临时空间,n个节点,树的高度最高为n,编码个数最多为n个
    char *temp = malloc(sizeof(char ) * n);
    // 生成n个字符的编码表[结果]
    HuffmanCode *codes = (HuffmanCode *) malloc(sizeof(HuffmanCode) * n);
    memset(codes, 0, sizeof(HuffmanCode) * n);
    int start;			// 在临时空间里标记待写入的索引位置,往前走--
    int p;				// 父节点的信息
    int pos;			// 判断父节点到底是通过左孩子还是右孩子进入的
    for (int i = 1; i <= n; ++i) {
        start = n - 1;
        temp[start] = '\0';
        pos = i;
        p = tree[i].parent;
        while (p) {
            --start;
            temp[start] = ((tree[p].lChild == pos) ? '0' : '1');
            pos = p;
            p = tree[p].parent;
        }
        // 把临时空间拷贝到结果指向的位置,先分配指向的空间大小
        codes[i - 1] = (HuffmanCode)malloc(sizeof(char ) * (n - start));
        strcpy(codes[i - 1], &temp[start]);
    }
    free(temp);
    return codes;
}

ok,文章至此完毕,下一篇更新图或者红黑树,如果有错误请在评论区指正。(作者才疏学浅,还请包容)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值