热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

树:哈夫曼树和哈夫曼编码的详细介绍以及代码实现

闲扯前言哈夫曼编码的代码实现对于初学数据结构的同学可能会有些困难,没有必要灰心,其实没啥,学习就犹如攀登一座又一座的山峰,

闲扯前言

哈夫曼编码的代码实现对于初学数据结构的同学可能会有些困难,没有必要灰心,其实没啥,学习就犹如攀登一座又一座的山峰,每当我们攻克一个难点后,回首来看,也不过如此嘛。我们要做的就是不断的去攀越学习上的山峰 不断的超越过去的自己。尤其是我们程序员,不进则退,中国最不缺的就是人,肯定不缺替代你的程序员,没有越老越吃香的程序员,中国的老程序员都去哪了?要么转管理,要么被淘汰转行了,当然还有一小部分成为技术专家 继续活跃在IT圈。时代在进步,技术在不断换代的更新,但是计算机一些固有的东西仍没有改变,编程语言只是我们手中的武器,我们唯有把内功修炼好,然后拿着利器 可以无往不胜!数据结构和算法就是我们程序员的内功修炼的一方面。不扯淡了。。。下面是博主如何攻克哈夫曼树和哈夫曼编码的一些思路。参考教材为 大话数据结构


哈夫曼树和哈夫曼编码的关系

哈夫曼树当然是一种树,不过这种树有些特殊之处。哈夫曼编码呢,是根据哈夫曼树规则生成的编码!提供一个字符,根据哈夫曼编码规则,你会得到一个哈夫曼编码,不过你提供的字符必须在哈夫曼编码表中有对应的编码才行


哈夫曼树

哈夫曼大叔说,从树中一个结点到另一个结点之间的分支构成2个结点之间的路径,路劲上的分支数目称作路径长度。树的路径长度就是从树根到每一结点的路径长度之和。如果考虑到带权的结点,结点的带权的路径长度就为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。

假设有n个权值{W1,W2.....,Wn},构造一棵n个叶子结点的二叉树,每个叶子结点带权Wk,每个叶子的路径长度为1k,我们通常记作,其中带权路径长度WPL最小的二叉树称为哈夫曼树(又称最优二叉树)。

定义我们只要有理解就ok,重点是下面如何构建哈夫曼树!


哈夫曼树构造步骤


  1. 根据给定的n个权值{W1,W2,...,Wn}构成n棵二叉的集合F={T1,T2,...Tn},其中每棵二叉树Ti只有一个带权为Wi的根结点,其左右子树均为空。
  2. 在F中选取2棵根结点最小的树 作为左右子树 构造一棵新的二叉树,且新的二叉树的根结点左右子树根结点权值之和。
  3. 在F中删除这2棵子树,同时将新得到的二叉树加入F中。
  4. 重复2和3步骤,直到F只含一棵树为止,这棵树便是哈夫曼树。

我们看这4个步骤,能否联想到队列的出队入队呢?往队列方向想,思路是对的。将权值最小的2棵树节点出队,然后创建新结点入队,这样一直循环,最后剩下那一个树结点时不就是哈夫曼树么。下面是构建哈夫曼树的HuffQueue.h的相关接口和数据结构。代码汇总在最后。

HuffQueue.h


#pragma once
#ifndef __HUFFQUEUE_H__
#define __HUFFQUEUE_H__
#include "HuffmanTree.h"//哈夫曼结点的队列 结点的数据结构
typedef struct HuffQueueNode
{HuffmanNode* treeNode;//哈夫曼树结点int weightNum;//权值struct HuffQueueNode* next;//指向下一个结点
}HuffQueueNode;//哈夫曼结点组成的队列数据结构,由于插入数据(根据权值)和删除数据(删除队头的数据)的特性,需要结点个数 和 头指针
typedef struct HuffQueue
{int size;HuffQueueNode* first;
}HuffQueue;//初始化队列
void InitHuffQueue(HuffQueue** queue);//获取结点(出队操作),从队头出队
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue);//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非结尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum);#endif // !__HUFFQUEUE_H__

BuildHuffmanTree函数



//根据用户提供字符集,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString)
{//统计每个字符出现的权值int charWeight[MAX_SZ] = { 0 };for (int i = 0; i data = i;treeNode->lchild = treeNode->rchild = NULL;AddHuffQueueNode(queue, treeNode, charWeight[i]);}}//根据哈夫曼树创建原理构建哈夫曼树//核心就是将权值最小的2个结点,取出作为新创建树结点的孩子结点,新创建树结点的权值为它们之和,然后放回树结点队列//一直这样循环进行操作,直到队列中最后剩一个结点,它就是树的根结点。while (queue->size != 1){HuffQueueNode* node1 = GetHuffQueueNode(queue);HuffQueueNode* node2 = GetHuffQueueNode(queue);HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));treeNode->data = '\0';treeNode->lchild = node1->treeNode;treeNode->rchild = node2->treeNode;int weightNum = node1->weightNum + node2->weightNum;AddHuffQueueNode(queue, treeNode, weightNum);}return queue->first->treeNode;
}

哈夫曼编码


定义还是先过一遍!一般地,设需要编码的字符集为{d1,d2,...dn},各个字符在电文中出现的次数或者频率集合{W1,W2,...Wn},以d1,d2,...dn作为叶子结点,以W1,W2,...Wn作为相应叶子结点的权值来构造一棵哈夫曼树规定哈夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路劲分支组成的0和1的序列便为该结点对应字符的编码,这就是哈夫曼编码

后面一句话是重点!走到叶子结点经过的0101...就是对应字符的哈夫曼编码!

有了哈夫曼树,写哈夫曼编码表 就很好,编写了!看下哈夫曼编码表的数据结构采用链式队列的方式,这样是否更方便!数据结构是我们人为去定义,是为我们操作数据提供方便的。下面是 HuffmanTree.h提供的相关接口和相应的数据结构。代码汇总在最后。


HuffmanTree.h

#pragma once
#ifndef __HUFFMANTREE_H__
#define __HUFFMANTREE_H__//创建哈夫曼树提供的字符的最大长度
#define MAX_SZ 256
//哈夫曼编码字符串最大长度
#define MAX_ENCODE 1024
//哈夫曼树结点数据结构
typedef struct HuffmanNode
{char data;struct HuffmanNode *lchild, *rchild;
}HuffmanNode,*HuffmanTree;//哈夫曼编码表结点数据结构
typedef struct HuffmanTableNode
{char data; //字符char *encode; //字符对应的哈夫曼编码struct HuffmanTableNode *next;
}HuffmanTableNode;
//哈夫曼编码表数据结构
typedef struct HuffmanTable
{HuffmanTableNode* front;//指向队头元素HuffmanTableNode* tail; //指向队尾元素
}HuffmanTable;//根据用户提供原始数据,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString);//根据哈夫曼树 生成对应的哈夫曼编码表
HuffmanTable* BuildHuffmanTable(HuffmanTree tree);//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table, char* src);//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root, char* encode);//遍历哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table);
#endif // !__HUFFMANTREE_H__


BuildHuffmanTable函数

/*
递归遍历哈夫曼树
depth 树的深度
tree 哈夫曼树
hTable 哈夫曼编码表
encode 字符对应的哈夫曼编码
*/
void traverseHuffTree(HuffmanTable* hTable,HuffmanTree tree,char* encode,int depth)
{if (NULL == tree->lchild && NULL == tree->rchild){HuffmanTableNode* tableNode = (HuffmanTableNode*)malloc(sizeof(HuffmanTableNode));tableNode->data = tree->data;tableNode->next = NULL;encode[depth] = '\0';tableNode->encode = (char*)malloc(depth+1);strcpy(tableNode->encode, encode);if (hTable->front == NULL){hTable->front = hTable->tail = tableNode;}else{hTable->tail->next = tableNode;hTable->tail = tableNode;}}if (NULL != tree->lchild){encode[depth] = '0';//左分支代表0traverseHuffTree(hTable, tree->lchild, encode, depth+1);}if (NULL != tree->rchild){encode[depth] = '1';//右分支代表1traverseHuffTree(hTable, tree->rchild, encode, depth+1);}return;
}//根据哈夫曼树生成哈夫曼编码表
HuffmanTable * BuildHuffmanTable(HuffmanTree tree)
{HuffmanTable* hTable = (HuffmanTable*)malloc(sizeof(HuffmanTable));hTable->front = hTable->tail = NULL;char encode[MAX_SZ] = { 0 };traverseHuffTree(hTable, tree, encode, 0);return hTable;
}


哈夫曼树和哈夫曼编码的使用

既然有了哈夫曼编码,那么就可以进行对用户提供的字符集进行哈夫曼编码了,当然也可用利用用户提供的哈夫曼编码进行解码咯。这就是小case了,encode 编码:根据用户提供的字符到哈夫曼编码表中找到对应的哈夫曼编码然后返回给用户就可以了decode解码:用户提供01100等哈夫曼编码,利用哈夫曼树进行解码就ok,0就往左走,1就往右走,直到叶子结点 对应的字符不就出来了么,然后又从哈夫曼树根结点开始走,直到将哈夫曼编码走完。下面是encode和decode代码。


encode函数


char* encode(HuffmanTable* table,char* src)
{char* encode = (char*)calloc(sizeof(char)*MAX_ENCODE,1);for (int i = 0; i front;while (iterator != NULL){if (iterator->data == ch){strcat(encode, iterator->encode);break;}iterator = iterator->next;}if (iterator == NULL){printf("哈夫曼编码表中没有字符%c对应的哈夫曼编码!\n",ch);return NULL;}}return encode;
}

decode函数



//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root,char* encode)
{char* decode = (char*)calloc(MAX_SZ, 1);HuffmanTree tree = root;for (int i = 0; i lchild;}else//1 往右走{tree = tree->rchild;}//走到头,也就是找到相应的 源字符了if (tree->lchild == NULL && tree->rchild == NULL){strncat(decode, &tree->data, 1);//找到字符后,树节点回到根结点,继续解码tree = root;}}return decode;
}


代码汇总一览


HuffQueue.h


#pragma once
#ifndef __HUFFQUEUE_H__
#define __HUFFQUEUE_H__
#include "HuffmanTree.h"//哈夫曼结点的队列 结点的数据结构
typedef struct HuffQueueNode
{HuffmanNode* treeNode;//哈夫曼树结点int weightNum;//权值struct HuffQueueNode* next;//指向下一个结点
}HuffQueueNode;//哈夫曼结点组成的队列数据结构,由于插入数据(根据权值)和删除数据(删除队头的数据)的特性,需要结点个数 和 头指针
typedef struct HuffQueue
{int size;HuffQueueNode* first;
}HuffQueue;//初始化队列
void InitHuffQueue(HuffQueue** queue);//获取结点(出队操作),从队头出队
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue);//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非结尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum);#endif // !__HUFFQUEUE_H__

HuffQueue.c



#include "HuffQueue.h"
#include //初始化队列
void InitHuffQueue(HuffQueue** queue)
{*queue = (HuffQueue*)malloc(sizeof(HuffQueue));(*queue)->size = 0;(*queue)->first = NULL;
}//获取结点(出队操作),只需要树节点的数据,故只返回树节点数据
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue)
{if (NULL == queue || 0 == queue->size || NULL == queue->first){return NULL;}HuffQueueNode* queueNode = queue->first;queue->first = queue->first->next;queue->size--;return queueNode;}//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非队尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum)
{HuffQueueNode *queueNode = (HuffQueueNode*)malloc(sizeof(HuffQueueNode));queueNode->weightNum = weightNum;queueNode->treeNode = treeNode;queueNode->next = NULL;//队列为空if (0 == queue->size){queue->first = queueNode;queue->size++;return;}else{//比第一个结点权值都小if (weightNum first->weightNum){queueNode->next = queue->first;queue->first = queueNode;queue->size++;return;}HuffQueueNode* iterator = queue->first;HuffQueueNode* pre = NULL;while (iterator != NULL && weightNum > iterator->weightNum){pre = iterator;iterator = iterator->next;}//在队列中找到自己位置,将其插入其中if (NULL != iterator){queueNode->next = iterator->next;iterator->next = queueNode;}//没找到,说明自己权值最大,插入到末尾else{pre->next = queueNode;}queue->size++;return;}return;
}

HuffmanTree.h



#pragma once
#ifndef __HUFFMANTREE_H__
#define __HUFFMANTREE_H__//创建哈夫曼树提供的字符的最大长度
#define MAX_SZ 256
//哈夫曼编码字符串最大长度
#define MAX_ENCODE 1024
//哈夫曼树结点数据结构
typedef struct HuffmanNode
{char data;struct HuffmanNode *lchild, *rchild;
}HuffmanNode,*HuffmanTree;//哈夫曼编码表结点数据结构
typedef struct HuffmanTableNode
{char data; //字符char *encode; //字符对应的哈夫曼编码struct HuffmanTableNode *next;
}HuffmanTableNode;
//哈夫曼编码表数据结构
typedef struct HuffmanTable
{HuffmanTableNode* front;//指向队头元素HuffmanTableNode* tail; //指向队尾元素
}HuffmanTable;//根据用户提供原始数据,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString);//根据哈夫曼树 生成对应的哈夫曼编码表
HuffmanTable* BuildHuffmanTable(HuffmanTree tree);//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table, char* src);//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root, char* encode);//遍历哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table);
#endif // !__HUFFMANTREE_H__


HuffmanTree.c


#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include "HuffmanTree.h"
#include "HuffQueue.h"//根据用户提供字符集,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString)
{//统计每个字符出现的权值int charWeight[MAX_SZ] = { 0 };for (int i = 0; i data = i;treeNode->lchild = treeNode->rchild = NULL;AddHuffQueueNode(queue, treeNode, charWeight[i]);}}//根据哈夫曼树创建原理构建哈夫曼树//核心就是将权值最小的2个结点,取出作为新创建树结点的孩子结点,新创建树结点的权值为它们之和,然后放回树结点队列//一直这样循环进行操作,直到队列中最后剩一个结点,它就是树的根结点。while (queue->size != 1){HuffQueueNode* node1 = GetHuffQueueNode(queue);HuffQueueNode* node2 = GetHuffQueueNode(queue);HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));treeNode->data = '\0';treeNode->lchild = node1->treeNode;treeNode->rchild = node2->treeNode;int weightNum = node1->weightNum + node2->weightNum;AddHuffQueueNode(queue, treeNode, weightNum);}return queue->first->treeNode;
}
/*
递归遍历哈夫曼树
depth 树的深度
tree 哈夫曼树
hTable 哈夫曼编码表
encode 字符对应的哈夫曼编码
*/
void traverseHuffTree(HuffmanTable* hTable,HuffmanTree tree,char* encode,int depth)
{if (NULL == tree->lchild && NULL == tree->rchild){HuffmanTableNode* tableNode = (HuffmanTableNode*)malloc(sizeof(HuffmanTableNode));tableNode->data = tree->data;tableNode->next = NULL;encode[depth] = '\0';tableNode->encode = (char*)malloc(depth+1);strcpy(tableNode->encode, encode);if (hTable->front == NULL){hTable->front = hTable->tail = tableNode;}else{hTable->tail->next = tableNode;hTable->tail = tableNode;}}if (NULL != tree->lchild){encode[depth] = '0';//左分支代表0traverseHuffTree(hTable, tree->lchild, encode, depth+1);}if (NULL != tree->rchild){encode[depth] = '1';//右分支代表1traverseHuffTree(hTable, tree->rchild, encode, depth+1);}return;
}//根据哈夫曼树生成哈夫曼编码表
HuffmanTable * BuildHuffmanTable(HuffmanTree tree)
{HuffmanTable* hTable = (HuffmanTable*)malloc(sizeof(HuffmanTable));hTable->front = hTable->tail = NULL;char encode[MAX_SZ] = { 0 };traverseHuffTree(hTable, tree, encode, 0);return hTable;
}//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table,char* src)
{char* encode = (char*)calloc(sizeof(char)*MAX_ENCODE,1);for (int i = 0; i front;while (iterator != NULL){if (iterator->data == ch){strcat(encode, iterator->encode);break;}iterator = iterator->next;}if (iterator == NULL){printf("哈夫曼编码表中没有字符%c对应的哈夫曼编码!\n",ch);return NULL;}}return encode;
}//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root,char* encode)
{char* decode = (char*)calloc(MAX_SZ, 1);HuffmanTree tree = root;for (int i = 0; i lchild;}else//1 往右走{tree = tree->rchild;}//走到头,也就是找到相应的 源字符了if (tree->lchild == NULL && tree->rchild == NULL){strncat(decode, &tree->data, 1);//找到字符后,树节点回到根结点,继续解码tree = root;}}return decode;
}//打印哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table)
{HuffmanTableNode* node = table->front;while (node != NULL){printf("源字符:%c ->哈夫曼编码:%s\n", node->data, node->encode);node = node->next;}
}int main(int argc, char *argv[])
{HuffmanTree tree = BuildHuffmanTree("aabbccc");HuffmanTable* table = BuildHuffmanTable(tree);TraverseHuffmanTable(table);char *src = "abc";char* dest = "10110101101";char* encode_str = encode(table, src);char* decode_str = decode(tree, dest);printf("原始字符串:%s ->哈夫曼编码:%s\n", src, encode_str);printf("哈夫曼编码:%s ->解码为:%s\n", dest, decode_str);return 0;
}

图解和运行测试



图解

我们那字符串aabbccc来生成哈夫曼树和哈夫曼编码表。对应的哈夫曼树如下图:




很明显,c字符->哈夫曼编码为0,a字符->哈夫曼编码10,b字符->哈夫曼编码11。

测试代码


int main(int argc, char *argv[])
{HuffmanTree tree = BuildHuffmanTree("aabbccc");HuffmanTable* table = BuildHuffmanTable(tree);TraverseHuffmanTable(table);char *src = "abc";char* dest = "10110101101";char* encode_str = encode(table, src);char* decode_str = decode(tree, dest);printf("原始字符串:%s ->哈夫曼编码:%s\n", src, encode_str);printf("哈夫曼编码:%s ->解码为:%s\n", dest, decode_str);return 0;
}


运行结果











推荐阅读
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
  • 学习Java异常处理之throws之抛出并捕获异常(9)
    任务描述本关任务:在main方法之外创建任意一个方法接收给定的两个字符串,把第二个字符串的长度减1生成一个整数值,输出第一个字符串长度是 ... [详细]
author-avatar
小嫣师妹
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有