目录
一 实验目的
二 实验内容及要求
实验内容:
实验要求:
三 实验过程及运行结果
实验一:先序输入建立二叉树,先序中序后序遍历输出,观察输出序列
一 算法设计思路
二 源程序代码
实验二:根据输入的字符串中各个字符出现的频率,建立哈夫曼树,实现哈夫曼编码
一 算法设计思路
二 源程序代码
四 调试情况、设计技巧及体会
1.理解二叉树的类型定义与性质。
2.掌握二叉树的二叉链表存储结构的表示和实现方法。
3.掌握二叉树遍历操作的算法实现。
4.熟悉二叉树遍历操作的应用。
采用二叉链表存储,实现二叉树的创建、遍历(递归)、赫夫曼编码和译码等典型操作。
1. 编程实现如下功能:
(1)假设二叉树的结点值是字符型,根据输入的一棵二叉树的完整先序遍历序列(子树空用’#’表示),建立一棵以二叉链表存储表示的二叉树。
(2)对二叉树进行先序、中序和后序遍历操作,并输出遍历序列,观察输出的序列是否与逻辑上的序列一致。
(3)主程序中要求设计一个菜单,允许用户通过菜单来多次选择执行哪一种遍历操作。
2. 编程实现如下功能:
按字符出现的次数对其建立哈夫曼树,并求出各个字符的哈夫曼编码。
1.键盘输入数据;
2.屏幕输出运行结果。
3.要求记录实验源代码及运行结果。
4.运行环境:CodeBlocks/Dev c++/VC6.0等C编译环境
包含七个功能函数和一个主函数:InitialTree(TreeNode &tree)、EmptyTree(TreeNode& tree) 、InOrderOutputTree(TreeNode* cur)、PreOutputTree(TreeNode* cur)、PostOutputTree(TreeNode *cur)、InPutTree(TreeNode* cur)、creatTree(TreeNode*p)、main()
InitialTree(TreeNode &tree): 初始化树,将根节点置为空 ,左右指针域置为空
EmptyTree(TreeNode& tree) : 判断树是否空,如果空返回是,否则返回否
InOrderOutputTree(TreeNode* cur): 函数InOrderOutputTree用于中序输出二叉树的元素。检查二叉树是否为空,可以使用EmptyTree函数判断。如果为空,则不进行输出操作。
如果二叉树不为空,执行以下操作:
获取当前节点cur的数据字符,将其赋值给变量ch。
如果ch不等于#,执行以下操作:
递归调用InOrderOutputTree函数输出当前节点的左子树。
输出当前节点的数据字符ch。
递归调用InOrderOutputTree函数输出当前节点的右子树。
PreOutputTree(TreeNode* cur): 函数PreOutputTree用于前序输出二叉树的元素。检查二叉树是否为空,可以使用EmptyTree函数判断。如果为空,则不进行输出操作。
如果二叉树不为空,执行以下操作:
获取当前节点cur的数据字符,将其赋值给变量ch。
如果ch不等于#,执行以下操作:
输出当前节点的数据字符ch。
递归调用PreOutputTree函数输出当前节点的左子树。
递归调用PreOutputTree函数输出当前节点的右子树。
PostOutputTree(TreeNode *cur): 函数PostOutputTree用于后序输出二叉树的元素。检查二叉树是否为空,可以使用EmptyTree函数判断。如果为空,则不进行输出操作。
如果二叉树不为空,执行以下操作:
获取当前节点cur的数据字符,将其赋值给变量ch。
如果ch不等于#,执行以下操作:
递归调用PostOutputTree函数输出当前节点的左子树。
递归调用PostOutputTree函数输出当前节点的右子树。
输出当前节点的数据字符ch。
InPutTree(TreeNode* cur): 函数InPutTree用于向二叉树中输入元素。以下是该函数的思路:
输出提示信息:"请输入要加入二叉树的元素:"。声明一个字符变量ch并将其初始化为0。声明一个指针变量p,并将其初始化为cur,用于标记cur的初始位置。从用户输入中读取一个字符并将其赋值给ch。如果ch不等于#,执行以下操作:
将ch赋值给cur节点的data成员,即将该字符作为当前节点的数据。
调用creatTree函数创建左子树,并将返回的指针赋值给cur节点的TreeLNext成员。
调用creatTree函数创建右子树,并将返回的指针赋值给cur节点的TreeRNext成员。
creatTree(TreeNode*p):代码实现了一个递归函数creatTree,用于创建一棵二叉树。函数接受一个指向 TreeNode 结构的指针p 作为参数,并返回根节点的指针。在函数内部,首先创建一个新节点p,然后分别创建左子树和右子树。从用户输入中读取一个字符ch。如果 ch不等于#,则将其作为当前节点的数据,并递归调用 creatTree 函数创建左子树和右子树,分别将返回的节点指针赋值给p->TreeLNext和p->TreeRNext。如果ch等于#,则说明当前节点是一个空节点,将#作为其数据,并直接返回当前节点指针p。整个递归过程会根据用户输入的字符不断创建新节点,并建立节点之间的父子关系,直到遇到 #字符表示结束。
main():调用相关函数,输出菜单以供用户选择
/*
1. 编程实现如下功能:
(1)假设二叉树的结点值是字符型,根据输入的一棵二叉树的完整先序遍历序列(子树空用’#’表示),建立一棵以二叉链表存储表示的二叉树。
(2)对二叉树进行先序、中序和后序遍历操作,并输出遍历序列,观察输出的序列是否与逻辑上的序列一致。
(3)主程序中要求设计一个菜单,允许用户通过菜单来多次选择执行哪一种遍历操作。*/
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
#include
#include
using namespace std;
typedef struct TreeNode
{
char data='#';
struct TreeNode* TreeLNext =NULL;
struct TreeNode* TreeRNext =NULL;
};
void InitialTree(TreeNode &tree)
{
tree.data = '#';
tree.TreeLNext = NULL;
tree.TreeRNext = NULL;
}//初始化树,将根节点置为空 ,左右指针域置为空
bool EmptyTree(TreeNode& tree)
{
if (tree.data == '#')
return true;
else
return false;
}//如果空返回是 否则返回否
//返回目前该输入的叶子所在节点指针?
//第一个是从根 开始输入
//第二个是从当前叶子节点开始输入? 【cy】
/*
a
/ \
/ \
/ \
b c
/ \ / \
/ \ / \
d e f g
/ \ / \ / \ / \
h i j k l m n o
先序 abdhiejkcflmgno
中序 hdibjekalfmcngo
后序 hidjkeblmfnogca
输入:abdh##i##ej##k##cfl##m##gn##o##
*/
TreeNode *creatTree(TreeNode*p)
{
char ch;
p = new TreeNode;
p->TreeLNext = new TreeNode;
p->TreeRNext = new TreeNode;
cin >> ch;
if ( ch != '#')
{
p->data = ch;
p->TreeLNext=creatTree(p->TreeLNext);
p->TreeRNext=creatTree(p->TreeRNext);
return p;
}
else
{
p->data = ch;
return p;
}
}
void InPutTree(TreeNode* cur)
{
cout << "请输入要加入二叉树的元素:" << endl;
char ch='0';
TreeNode* p = cur;//标记cur初始位置?
cin >> ch;
if ( ch != '#')
{
cur->data = ch;
cur->TreeLNext=creatTree(cur->TreeLNext);
cur->TreeRNext=creatTree(cur->TreeRNext);
}
}//先序遍历创建二叉树
void PostOutputTree(TreeNode *cur)
{
if (EmptyTree(*cur))
{
//cout << "二叉树为空!无法输出" << endl;
}
else
{
char ch = cur->data;
if ( ch != '#')
{
PostOutputTree(cur->TreeLNext);
PostOutputTree(cur->TreeRNext);
cout << ch;
}
}
}//输出二叉树
void PreOutputTree(TreeNode* cur)
{
if (EmptyTree(*cur))
{
//cout << "二叉树为空!无法输出" << endl;
}
else
{
char ch = cur->data;
if (ch != '#')
{
cout << ch;
PreOutputTree(cur->TreeLNext);
PreOutputTree(cur->TreeRNext);
}
}
}//先序输出二叉树
void InOrderOutputTree(TreeNode* cur)
{
if (EmptyTree(*cur))
{
//cout << "二叉树为空!无法输出" << endl;
}
else
{
char ch = cur->data;
if (ch != '#')
{
InOrderOutputTree(cur->TreeLNext);
cout << ch;
InOrderOutputTree(cur->TreeRNext);
}
}
}//中序输出二叉树
int main()
{
TreeNode root;
TreeNode* cur = &root;
InitialTree(root);
cout << "\n\n-------------菜单----------------" << endl;
cout << "----------1.输入二叉树-----------" << endl;
cout << "----------2.先序遍历二叉树-------" << endl;
cout << "----------3.中序遍历二叉树-------" << endl;
cout << "----------4.后序遍历二叉树-------" << endl;
cout << "----------5.退出-----------------" << endl;
cout << "---------------------------------" << endl;
int choice;
cout << "请输入菜单序号!" << endl;
cin >> choice;
while (1)
{
switch (choice)
{
case 1:
InPutTree(cur);
cout << "已按先序遍历顺序建立好树了" << endl;
break;
case 2:
cout << "以下是先序遍历所得序列:" << endl;
PreOutputTree(cur);
puts("");
break;
case 3:
cout << "以下是中序遍历所得序列:" << endl;
InOrderOutputTree(cur);
puts("");
break;
case 4:
cout << "以下是后序遍历所得序列:" << endl;
PostOutputTree(cur);
puts("");
break;
case 5 :
exit(1);
default:
cout << "请输入正确的菜单选项" << endl;
break;
}
cout << "请输入需要操作的菜单选项" << endl;
cin >> choice;
}
return 0;
}
包含四个功能函数、三个全局变量和一个主函数:BuildHuffmanTree()、GenerateHuffmanCodes(HuffmanNode* node,string code)、InputHuffmanTree(string input)、OutputHuffmanTree(string input)、main、
三个全局变量:
int frequency[256] :用于存储每个字符出现的频率。
HuffmanNode* node_list[256] :存储哈夫曼树节点的指针,数组的下标对应字符的 ASCII 码值,数组的值是指向 HuffmanNode 结构体的指针。
string codes[256] :数组的下标对应字符的 ASCII 码值,数组的值是该字符对应的哈夫曼编码。
四个功能函数:
BuildHuffmanTree():函数BuildHuffmanTree用于构建Huffman树。 首先初始化变量count为0,用于记录节点数目。然后使用循环遍历frequency数组,范围是0到255。对于每个非零频率的字符,执行以下操作:
创建一个新的HuffmanNode结构体,将字符的ASCII码作为节点的数据,将频率作为节点的权值。
将新节点存储在node_list数组中,并将count增加1。进入循环,条件是count大于1。在每次循环中,执行以下操作:
初始化变量min1为0,变量min2为1,用于记录最小权值的节点的索引。
如果node_list[min1]的权值大于node_list[min2]的权值,则交换min1和min2的值,确保min1对应的节点权值小于等于min2对应的节点权值。
使用循环从索引2开始遍历node_list数组,范围是2到count-1。对于每个节点,执行以下操作:
如果当前节点的权值小于node_list[min1]的权值,更新min2为min1,并将min1更新为当前节点的索引,确保min1对应的节点权值最小。
否则,如果当前节点的权值小于node_list[min2]的权值,更新min2为当前节点的索引,更新最小权值的节点索引。
创建一个新的HuffmanNode节点,数据为0,权值为node_list[min1]和node_list[min2]节点的权值之和。
将新节点的左子树指向node_list[min1],将新节点的右子树指向node_list[min2]。
将node_list[min1]替换为新节点。
将node_list[min2]替换为node_list[count-1],并将count减少1。
最后返回node_list[0],即Huffman树的根节点。
GenerateHuffmanCodes(HuffmanNode* node,string code):函数GenerateHuffmanCodes用于生成Huffman编码。首先检查当前节点node是否为叶子节点,即判断node的左子节点和右子节点是否都为空。然后如果node是叶子节点,执行以下操作:
将当前节点的数据(字符)作为索引,将参数code作为对应的Huffman编码存储在codes数组中。
如果node不是叶子节点,执行以下操作:
递归调用GenerateHuffmanCodes函数,传入当前节点的左子节点和code加上字符"0"。 递归调用GenerateHuffmanCodes函数,传入当前节点的右子节点和code加上字符"1"。
InputHuffmanTree(string input):函数InputHuffmanTree用于输入Huffman树的频率信息。首先遍历输入字符串input的每个字符。然后对于每个字符,执行以下操作:
使用字符作为索引,将frequency数组中对应位置的值加一。frequency数组用于记录每个字符的频率信息。循环结束后,frequency数组中存储了输入字符串中每个字符的频率信息。
OutputHuffmanTree(string input):函数OutputHuffmanTree用于输出Huffman树的编码结果。首先创建一个名为printed_chars的整型数组,用于记录已经打印过的字符。数组的索引对应字符的ASCII码,初始值都为0。如果某个字符已经打印过,对应索引的值将被设置为1。遍历输入字符串input的每个字符。对于每个字符,执行以下操作:
检查printed_chars[input[i]]的值,如果为0,表示该字符还未被打印过。否则输出当前字符input[i]和其对应的编码codes[input[i]] 。将printed_chars[input[i]]的值设置为1,表示该字符已经被打印过。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
#include
#include
using namespace std;
struct HuffmanNode
{
char data;
int weight;
HuffmanNode* Lnext, * Rnext;
HuffmanNode(char data = 0, int weight = 0) : data(data), weight(weight), Lnext(nullptr), Rnext(nullptr) {}
};
int frequency[256] = { 0 };//用于存储每个字符出现的频率。
HuffmanNode* node_list[256] = { nullptr };//存储哈夫曼树节点的指针,数组的下标对应字符的 ASCII 码值,数组的值是指向 HuffmanNode 结构体的指针。
string codes[256] = { "" };//数组的下标对应字符的 ASCII 码值,数组的值是该字符对应的哈夫曼编码。
// 建立哈夫曼树
HuffmanNode* BuildHuffmanTree()
{
int count = 0;
for (int i = 0; i < 256; i++)
{
if (frequency[i] > 0) {
node_list[count++] = new HuffmanNode(i, frequency[i]);//count确定节点数目
}
//遍历frequency数组,将字符转化成HuffmanNode 结构体并存储在node_list数组中。
}
while (count > 1)
{
int min1 = 0, min2 = 1;
if (node_list[min1]->weight > node_list[min2]->weight)
{
swap(min1, min2);//找最小节点
}//确保min1对应的节点频率小于等于min2对应的节点频率。
for (int i = 2; i < count; i++) {
if (node_list[i]->weight < node_list[min1]->weight)//比较权值
{
min2 = min1;
min1 = i;
}//确保min1当前权值最小
else if (node_list[i]->weight < node_list[min2]->weight)
{
min2 = i;
}//更新最小
}
//选择两个频率最小的节点,构建一个新的节
HuffmanNode* new_node = new HuffmanNode(0, node_list[min1]->weight + node_list[min2]->weight);
new_node->Lnext = node_list[min1];
new_node->Rnext = node_list[min2];//新节点连接,左右子树
node_list[min1] = new_node;//新节点替换最小节点
node_list[min2] = node_list[--count];//节点数进行减少
}
return node_list[0];
}
// 生成哈夫曼编码
void GenerateHuffmanCodes(HuffmanNode* node, string code)
{
if (node->Lnext == nullptr && node->Rnext == nullptr)//无子节点
{
codes[node->data] = code;
return;
}
GenerateHuffmanCodes(node->Lnext, code + "0");
GenerateHuffmanCodes(node->Rnext, code + "1");
}
void InputHuffmanTree(string input)
{
for (int i = 0; i < input.size(); i++)
{
frequency[input[i]]++;
}//遍历添加
}
void OutputHuffmanTree(string input)
{
cout << "哈夫曼编码:" << endl;
int printed_chars[256] = { 0 }; // 记录已经打印过的字母
for (int i = 0; i < input.size(); i++)
{
if (printed_chars[input[i]] == 0)
{
cout << input[i] << ": " << codes[input[i]] << " ";
printed_chars[input[i]] = 1;
}
}
cout << endl;
}
// 主函数
int main()
{
string input;
cout << "请输入要进行哈夫曼编码的字符串:";
cin >> input;
InputHuffmanTree(input);
HuffmanNode* root = BuildHuffmanTree();
GenerateHuffmanCodes(root, "");
OutputHuffmanTree(input);
return 0;
}
实验一:二叉树的遍历操作
在实现二叉树的遍历操作的过程中,我遇到了一些典型错误,并通过相应的修改方法解决了这些问题。同时,我也学到了一些技巧和积累了经验。
典型错误及修改方法:
1. 错误:处理空节点的情况不正确。
在先序遍历序列中,使用特殊字符"#"表示空节点,但在中序和后序遍历中并没有类似的标记。如果在处理空节点时没有正确处理,可能导致遍历出错。
修改方法:我在建立二叉树时,使用了一个特殊的值来表示空节点,例如使用-1或其他不会出现在节点值中的数值作为标记。这样,在遍历过程中,当遇到这个特殊值时,就知道该节点是空节点,可以正确处理。
2. 错误:没有正确处理递归终止条件。
在递归遍历过程中,没有正确设置递归终止条件可能导致无限递归或遍历不完整。
修改方法:在先序遍历中,递归的终止条件是遇到空节点或已经遍历完所有节点。在中序和后序遍历中,终止条件是遇到空节点。确保正确设置递归终止条件是实现遍历操作的关键。
学到的技巧和经验:
1. 递归算法的应用。
通过使用递归算法,可以简洁地实现二叉树的遍历操作。递归的思想是将问题分解为更小的子问题,然后通过递归调用解决子问题。在二叉树的遍历中,递归函数的调用顺序决定了遍历的顺序。
2. 递归终止条件的设置。
在递归遍历过程中,正确设置递归终止条件是非常重要的。终止条件决定了递归何时结束,避免了无限递归的问题。对于二叉树的遍历操作,终止条件通常是遇到空节点或者遍历完所有节点。
3. 空节点的处理。
在处理二叉树的遍历时,需要正确处理空节点的情况。可以使用特殊的值或标记来表示空节点,在遍历过程中进行判断和处理。确保对空节点的正确处理可以避免遍历出错。
4. 三种遍历方式的特点。
先序、中序和后序遍历是三种常见的二叉树遍历方式。先序遍历先访问根节点,然后递归遍历左子树和右子树;中序遍历先递归遍历左子树,然后访问根节点,最后递归遍历右子树;后序遍历先递归遍历左子树和右子树,最后访问根节点。了解每种遍历方式的特点和顺序可以帮助理解和实现遍历操作。
实验二:哈夫曼树和哈夫曼编码
在实现哈夫曼树和哈夫曼编码的过程中,我遇到了一些典型错误,并通过相应的修改方法解决了这些问题。同时,我也学到了一些技巧和积累了经验。
典型错误及修改方法:
1. 错误:处理权重相同的节点时没有正确排序。
在构建哈夫曼树时,当遇到权重相同的节点时,我们需要按照一定的规则来确定节点的位置。如果两个节点的权重相同,我们可以比较它们的字母顺序,将字母较小的节点放在左子树,字母较大的节点放在右子树。如果没有正确排序节点,可能会导致生成的哈夫曼树不唯一。
修改方法:在构建哈夫曼树时,对于权重相同的节点,按照字母顺序进行排序。可以使用比较函数或者排序算法来实现节点的排序,确保生成的哈夫曼树是唯一的。
2. 错误:生成哈夫曼编码时没有正确处理叶子节点和非叶子节点之间的区别。
在生成哈夫曼编码时,叶子节点代表原始字符,非叶子节点代表字符的组合。如果没有正确处理叶子节点和非叶子节点的区别,可能会导致生成的哈夫曼编码错误。
修改方法:在生成哈夫曼编码时,需要遍历哈夫曼树,并记录从根节点到每个叶子节点的路径。对于每个叶子节点,路径上的0表示左子树,1表示右子树。通过正确处理叶子节点和非叶子节点的区别,可以生成正确的哈夫曼编码。
学到的技巧和经验:
1. 使用最小堆来构建哈夫曼树:构建哈夫曼树的过程中,可以使用最小堆来提高效率。最小堆是一种数据结构,可以在常数时间内找到最小值。通过将节点的权重作为关键字,可以使用最小堆来选择权重最小的节点进行合并,从而构建哈夫曼树。
2. 递归算法遍历哈夫曼树:生成哈夫曼编码时,可以使用递归算法来遍历哈夫曼树,并记录路径信息。递归的思想可以简化编码生成的过程。对于每个节点,可以通过递归调用来遍历左子树和右子树,并记录路径上的0和1,从而生成哈夫曼编码。
3. 注意权重相同节点的处理:在处理权重相同的节点时,需要按照一定的规则来排序节点。一种常见的规则是按照节点的字母顺序进行排序。通过正确处理权重相同的节点,可以确保生成的哈夫曼树是唯一的。
通过这次实验,我不仅加深了对二叉树和遍历操作的理解,还学到了如何使用递归算法解决相关问题。同时,通过解决实验中的错误和问题,我提高了问题排查和调试的能力。这些经验和技巧对于进一步学习和应用数据结构和算法都具有重要的指导意义。实验过程中的错误和解决方法以及学到的技巧和经验,对于日后的学习和实践都有很大的帮助。
在哈夫曼树和哈夫曼编码的实验中我不仅加深了对哈夫曼树和哈夫曼编码的理解,还学到了如何使用最小堆和递归算法来解决相关问题。同时,我也通过解决实验中的错误和问题,提高了问题排查和调试的能力。这些经验和技巧对于进一步学习和应用数据结构和算法都具有重要的指导意义。