【数据结构复习之路】树和二叉树(严蔚敏版)万字详解&主打基础

专栏数据结构复习之路

复习完上面四章【线性表】【栈和队列】【】【数组和广义表】,我们接着复习 树和二叉树,这篇文章我写的非常详细且通俗易懂,看完保证会带给你不一样的收获。如果对你有帮助,看在我这么辛苦整理的份上,三连一下啦 

目录

一、树的定义

1.1 基本术语

1.2 扩展:树的四种表示形式

二、二叉树

2.1 二叉树的定义

2.2 二叉树的性质

2.3 满二叉树

2.4 完全二叉树

2.5 二叉排序树

2.6 平衡二叉树

2.7 二叉树的顺序存储结构

2.8 二叉树的链式存储结构

 2.9 二叉树的先中后序遍历

① 先序遍历

② 中序遍历

③ 后序遍历

 相关题目练习:

2.10 先中后序遍历的扩展

2.11 递归实现(顺序表存储)

2.12 二叉树的层次遍历

2.13 通过二叉树的先、中、后序遍历重构二叉树

三、线索二叉树

3.1 基本概念 

 3.2 中序线索化

构建:

3.3 先序线索化

构建: 

3.4 后序线索化

3.5 线索二叉树找前驱和后继

【1】中序线索二叉树

【2】先序线索二叉树

 【3】后序线索二叉树

四、树的存储结构

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

4.2 孩子表示法(顺序+链式)

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

 4.4 森林和二叉树的转换

五、树和森林的遍历

5.1 树的先根遍历

5.2 树的后根遍历

​ 5.3 树的层次遍历

5.4 森林的先序遍历

5.5 森林的中序遍历

六、哈夫曼树

基本概念:

定义:

哈夫曼树的构造

 哈夫曼编码

译码

​ 结尾


一、树的定义

树(Tree) 是 n(n≥0)个结点的有限集。若 n=0, 称为空树

若 n > 0,则它满足如下两个条件:

  1. 有且仅有一个特定的称为根 (Root) 的结点
  2.  其余结点可分为 m (m≥0) 个互不相交的有限集 T1 , T2 , T3 , …, Tm, 其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。

1.1 基本术语

1、结点拥有的子树数称为 :结点的度(degree),如上图结点A的度为3,B的度为2。

  • 度为0的结点叫:叶子(leaf)(终端结点)
  • 度不为0的结点叫:分支结点(非终端结点)
  • 内部结点(B、C、D、E、H)
  • 树的度是树中各结点的最大的度(上图树的度为3)

2、结点的子树的根称为该结点的孩子(child)

  • 双亲(parent)(D为H、I、J的双亲)
  • 兄弟(sibling)(H、I、J互为兄弟)
  • 祖先,子孙(B的子孙为E、K、L、F;M的祖先为A、D、H)

3、结点的层次

  • 根结点为第一层
  • 某结点在第 i 层,其孩子在第 i+1 层
  • 树中结点的最大层次称为树的深度
  • 其双亲在同一层的结点互为堂兄弟

4、森林(forest) 是 m (m≥0) 棵互不相交的树的集合。比如上图中除去 A 结点,那么分别以 B、C、D 为根结点的三棵子树就可以称为森林。

树可以理解为是由根结点若干子树构成的,而这若干子树本身就是一个森林,因此树还可以理解为是由根结点森林组成的。

1.2 扩展:树的四种表示形式

 一、树形表示法(正如上述所介绍)

二、嵌套集合(文氏)表示法

:它是以嵌套集合的形式表示的(集合之间绝不能相交,即任意两个圆圈不能有交集) 

三、凹入表示法

:最长条为根结点,相同长度的表示在同一层次。例如 B、C、D 长度相同,都为 A 的子结点,E 和 F 长度相同,为 B 的子结点,K 和 L 长度相同,为 E 的子结点,依此类推。

​四、广义表表示法(正如在上一章广义表所述)

二、二叉树

二叉树在树结构的应用中起着非常重要的作用,因为对二叉树的许多操作算法简单,而任何树均可与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性。

2.1 二叉树的定义

二叉树是 n (n≥0) 个结点的有限集,它或者是 空集 (n = 0),或者由一个根结点及两棵互不相交的 分别称作这个根的左子树和右子树的二叉树组成。

特点:

  • 每个结点最多有俩孩子 (二叉树中不存在度大于 2 的结点) 。
  • 子树有左右之分,其次序不能颠倒。
  • 二叉树可以是空集合,根可以有空的左子树或空的右子树。

⚠️注意:二叉树不是树的特殊情况,它们是两个概念。

理由:二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。树当结点只有一个孩子时,就无须区分它是左还是右。(也就是 二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了),因此二者是不同的。这是二叉树与树的最主要的差别。 

⚠️注意:虽然二叉树与树概念不同, 但有关树的基本术语对二叉树都适用。

2.2 二叉树的性质

性质 1:在二叉树的第 i 层上至多有 2^{i-1} 个结点 (i ≥1)。

性质 2:深度为 k 的二叉树至多有 2^{k}-1 个结点(k ≥1)。


扩展:深度为 k 的 m 叉树至多有 \frac{m^{k}-1}{m-1} 个结点。

性质 3:高度为 h 的 m 二叉树至少有 h 个结点。


扩展:高度为h、度为 m 的树至少有 h + m - 1 个结点。

性质 4:具有 n 个结点的 m叉树 的最小高度 h 为:log_{m}(n(m-1)+1) 

通过:\frac{m^{h-1}-1}{m-1}< n<= \frac{m^{h}-1}{m-1} 可求出 h

性质 5:对任何一棵二叉树 T,如果其叶子数为 n_{0},度为 2 的结点数为 n_{2},则  n_{0} = n_{2}+1

2.3 满二叉树

特点:每一层上的结点数都达到最大,叶子全部在最底层。

编号规则:从根结点开始,自上而下,自左而右。

 因此:

  • 一棵深度为 k 且有 2^{k}-1个结点的二叉树称为满二叉树。
  • 按层序从1开始编号,结点为 i 的左孩子为 2i ,右孩子为 2i + 1,结点 i 的父节点为 \frac{i}{2}(注意是向下取整)

2.4 完全二叉树

定义:深度为 k 的具有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号为 1~ n 的 结点一一对应时,称之为完全二叉树。

特点:叶子只可能分布在层次最大的两层上。 对任一结点,如果其右子树的最大层次为 L,则其 左子树的最大层次必为 L 或 L + 1

因此:

  • 只有最后两层可能有叶子结点
  • 最多只有一个度为1的结点
  • 按层序从1开始编号,结点为 i 的左孩子为 2i ,右孩子为 2i + 1,结点 i 的父节点为 \frac{i}{2}(注意是向下取整)
  • 2i > n,则结点 i 无左孩子(即结点 i 为叶子结点),否则其左孩子为 2i
  • 2i + 1 > n,则结点 i 无右孩子,否则其右孩子为 2i + 1

常见考点:

 考点 1:具有 n 个结点的完全二叉树的深度为:

 \left \lfloor log_{2}n \right \rfloor+1 (向下取整) 或者 \left \lceil log_{2} (n+1)\right \rceil(向上取整)

考点 2 对于完全二叉树,可以由结点数 n 推出度为0、1和2的结点个数n_{0}n_{1} 和 n_{2}

2.5 二叉排序树

二叉排序树。一棵二叉树或者是空二叉树, 或者是具有如下性质的二叉树:

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字

左子树和右子树又各是一棵二叉排序树。

好处:二叉排序树可用于元素的排序、搜索,都是相当高效的!

2.6 平衡二叉树

平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。

好处:搭配二叉排序树可以达到更高的搜索效率!

2.7 二叉树的顺序存储结构

#define MaxSize 100
struct TreeNode{
	ElemType value; //结点中的数据元素
	bool isEmpty; //结点是否为空 
} t[MaxSize + 1]; 

【1】对于完全二叉树:用一组地址连续的存储单元依次自上而下自左至右存储结点元素,即将编号为 i 的结点元素存储在一维数组中下标为 i 的分量中。

这里常见的考点就是:

  • i 的左孩子
  • i 的右孩子
  • 判断 i 是否有右孩子?
  • 判断 i 是否是叶子结点或分支结点? 

这些都是完全二叉树的重要性质,上文已讲解。 

⚠️注意:如果 i 从0开始,那么将现有编号 + 1,再按公式算,算出结果后,再 -1,不要糊涂!


对于一般二叉树:将其每个结点与完全二叉树上的结点相对照,存储在一 维数组的相应分量中。 但是这种存储已经不满足完全二叉树的性质。

因此为了能够满足这种性质,就需要为这个非完全二叉树依次补上虚拟结点空间。

但是这种存储形式,在最坏情况下,深度为 k 的且只有 k 个结点的右单支树需要 长度为 2^{k-1} 的一维数组,因此链式存储的优势也就显而易见了。

2.8 二叉树的链式存储结构


typedef struct BiTNode{
	ElemType data; //数据域 
	struct BiTNode *lchild , *rchild; //左、右孩子指针 
} BiTNode , *BiTree; 

除了根结点外,每个结点上面都必与一个指针 "相连" ,共有 n - 1 个指针,那么剩余的空指针数量应为: 2n - (n - 1) = n + 1(个空指针)。

找某一结点P的左孩子和右孩子(相当轻松):

printf("p结点的左孩子结点为:%d\n", p->lchild->data);
printf("p结点的右孩子结点为:%d\n", p->rchild->data);

但是如果要找到指定结点 P 的父结点,就只能从根结点开始遍历寻找,因此你可以考虑在结构体中再加一个父节点指针

typedef struct BiTNode{
	ElemType data; //数据域 
	struct BiTNode *lchild , *rchild; //左、右孩子指针 
	struct BiTNode *parent; //父节点指针 
} BiTNode , *BiTree; 

这种形式也叫做 " 三叉链表 "。

 2.9 二叉树的先中后序遍历

遍历:就是按某种次序把所有结点都访问一遍。

这种递归式的遍历,只有搞懂递归过程,才能真正的理解它的遍历过程,因此请务必理解它们的递归代码!

对于这三种遍历,我将基于如下这种图进行讲述:

先序遍历

void PreOrderTraverse(BiTree T) {
    //如果二叉树存在,则遍历二叉树
    if (T) {
        printf("%d",T->data); //输出结点值
        PreOrderTraverse(T->lchild);//访问该结点的左孩子
        PreOrderTraverse(T->rchild);//访问该结点的右孩子
    }
}

先访问根节点,再遍历左子树,最后遍历右子树。

遍历结果:5 2134 8697

⚠️注意:根据它的代码,我们发现,在递归左右子树前,一定是先输出当前结点值,并且只有左子树递归完后,才会返回上一层递归右子树,说明对于每个子树:先输出父结点、再输出左孩子,最后输出右孩子。因此记住这三句代码的顺序就对遍历过程了如指掌了。

详细递归过程:

输出根节点 5;

进入 5 的左子树,执行同样的步骤:
    输出结点 2;
    进入 2 的左子树,执行同样的步骤:
        输出结点 1;
        结点 1 没有左子树;
        结点 1 没有右子树;
    进入 2 的右子树,执行同样的步骤:
        输出结点 3;
        进入 3 的左子树,执行同样的步骤:
             输出结点4;
             结点 4 没有左子树;
             结点 4 没有右子树;
        进入 3 的右子树,没有,直接返回(因为 5 的左子树都已经遍历完了,所有一直返回到进入 5 的右子树)
            
进入 5 的右子树,执行同样的步骤:
    输出结点 8;
    进入 8 的左子树,执行同样的步骤:
        输出结点 6;
        进入 6 的左子树,没有,直接返回,再进入 6 的右子树;
        进入 6 的右子树,执行同样的步骤:
             输出结点9;
             结点 9 没有左子树;
             结点 9 没有右子树;
    进入 8 的右子树,执行同样的步骤:
        输出结点 7;
        结点 7 没有左子树;
        结点 7 没有右子树;
    //运行到这里结点 5 的右子树都已经遍历完了,所有就递归回去,直到函数结束! 
    

中序遍历

void INOrderTraverse(BiTree T) {
    if (T) {
        INOrderTraverse(T->lchild);//遍历当前结点的左子树
        printf("%d ",T->data);     //输出当前结点
        INOrderTraverse(T->rchild);//遍历当前结点的右子树
    }
}

先遍历左子树,再访问根节点,最后遍历右子树。

遍历结果:1243 5 6987

⚠️注意:输出结点值的位置放在了中间,说明对于每个子树:先输出左孩子,再输出它的父结点,最后输出其右孩子。

递归过程同上!可以自己动手试试画出递归过程~

后序遍历

void PostOrderTraverse(BiTree T) {
    if (T) {
        PostOrderTraverse(T->lchild);//遍历左孩子
        PostOrderTraverse(T->rchild);//遍历右孩子
        printf("%d ", T->data);
    }
}

先遍历左子树,再遍历右子树,最后访问根节点。

遍历结果:1432 9678 5

⚠️注意:输出结点值的位置放在了最后面,说明对于每个子树:先输出左孩子,再输出右孩子,最后输出其父结点


 相关题目练习:

【1】二叉树的先序遍历中,任一结点均先于它的左、右子女(如果存在)访问,所有这句话是对的。

【2】在二叉树的定义那里已经讲的非常清楚了,这是错的。

【3】因为后序遍历的最后一个结点一定是根结点,并且前序遍历的第一个结点也是根结点,所以排除法,选择D。

【4】 根据前序遍历的结果可知, a 是根结点。由中序遍历的结果dgbaechf 可知,d、g、b  是左子树的结点, e、c、h、f 是右子树的结点。再由前序遍历的结果bdg 可知, b 是a左边子树的根,由cefh 可知, c 是a右边子树的根。再由中序遍历的结果dgb 可知, d,g 是b 左边子树的结点 ,g为d的右孩子。至此,a 的左子树已完全弄清楚了。同样的道理,可以弄清楚以c为根的子树的结点位置。所以可知后序遍历的结果是D

【5】很明显选择A。

【6】 过程如下:


2.10 先中后序遍历的扩展

 【1】我们只需要对先序、中序、后序遍历的过程稍加修改,就可以设计出构建二叉树的函数:

比如通过先序遍历,构建下图二叉树,只需要少许代码量就能构建好:

void CreateBiTree(BiTree* T) {
    int num;
    scanf("%d", &num);
    //如果输入的值为 0,表示无此结点
    if (num == 0) {
        *T = NULL;
    }
    else
    {
        //创建新结点
        *T = (BiTree)malloc(sizeof(BiTNode));
        (*T)->data = num;
        CreateBiTree(&((*T)->lchild));//创建该结点的左孩子
        CreateBiTree(&((*T)->rchild));//创建该结点的右孩子
    }
}

当我们输入的num数,依次为:5 210034000 860900700

就能将如图的二叉树用链表存储起来!超级方便!


【2】统计二叉树中叶子结点的个数 ,实现此操作只需对二叉树“遍历”一遍,并在遍历过程中对 “叶子结点计数”即可。显然这个遍历的次序可以随意,只是为了在遍历时进行计数,需要在算法的参数中设一个“计数器”。

void CountLeaf (BiTree T, int &count) 
{ 
   // 先序遍历二叉树以 count 返回二叉树中叶子结点的数目
   if ( T )
    { 
      if ((!T−>Lchild) && (!T−>Rchild)) // 无左、右子树
         {
          count ++; // 对叶子结点计数
         }

       CountLeaf ( T−>Lchild, count); 
       CountLeaf ( T−>Rchild, count); 
    }  
} 

【3】求二叉树的深度(后序)。

 二叉树的深度 = MAX(左子树深度,右子树深度)+ 1 。

int BiTreeDepth(BiTree T) 
{ 
    if (!T)  depth = 0; 
    else { 
        depthleft = BiTreeDepth(T->Lchild); 
        depthright = BiTreeDepth(T->Rchild); 
        depth = max(depthleft, depthright) + 1; 
        } 
    return depth; 
}// BiTreeDepth

【4】 通过这种左右子树递归的思想,可以计算二叉树的所有叶子的带权路径长度之和WPL。一个叶子结点的带权路径长度为: 该结点的权重weight * 该结点的深度depth.

(1)算法思想:

递归遍历二叉树的所有叶节点,计算每个叶节点的带权路径长度,然后累加得到二叉树的带权路径长度WPL。

(2)

typedef struct TreeNode {
    int weight; 
    struct TreeNode* left;
    struct TreeNode* right;
} TreeNode;

(3)

int calculate_WPL(TreeNode *root , int depth)
{
	if (root == NULL) return 0; //如果为空树 WPL为 0 
	if (root -> left == NULL && root -> right == NULL) return root -> weight * depth; //当前叶子结点的权值乘路径长度 
	return calculate_WPL(root -> left , depth + 1) + calculate_WPL(root -> right , depth + 1); //递归求二叉树中所有叶结点的带权路径长度之和
 }

【5】 通过这种左右子树递归的思想,我们还可以处理下述问题:

(1) 算法思想:

因为括号反映操作符的计算次序,观察这两个表达式树的输出结果,不难看出,应采取中缀表达式的递归遍历算法,对于每递归到 "深度 > 1"并且不是叶子结点时,打印左括号 “( ”,当该结点的子孙遍历完后,再打印右括号“  )”。

(2)

#include <stdio.h>
#include <stdlib.h>

// 表达式树的结构体
typedef struct node {
    char data[10]; 
    struct node* left , *right; 
} BTree;

void infixExpression(BTree* root, int depth) {
	if (root == NULL) return;
	else if (root -> left == NULL && root -> right == NULL){ //叶子结点需格外输出 
		printf("%c",root -> data[0]);
		return;
	}
	else {
		if (depth > 1) printf("(");
		infixExpression(root -> left , depth + 1);
		printf("%c", root -> data[0]);
		infixExpression(root -> right , depth + 1);
		if (depth > 1) printf(")");
	}
}

int main() {
	
    // 构造第一个表达式树(可以运行检验自己是否写错哦)
    BTree* root1 = (BTree*)malloc(sizeof(BTree));
    root1->data[0] = '*';
    root1->left = (BTree*)malloc(sizeof(BTree));
    root1->left->data[0] = '+';
    root1->left->left = (BTree*)malloc(sizeof(BTree));
    root1->left->left->data[0] = 'a';
    root1->left->left->left = NULL;
    root1->left->left->right = NULL;
    root1->left->right = (BTree*)malloc(sizeof(BTree));
    root1->left->right->data[0] = 'b';
    root1->left->right->left = NULL;
    root1->left->right->right = NULL;
    root1->right = (BTree*)malloc(sizeof(BTree));
    root1->right->data[0] = '*';
    root1->right->left = (BTree*)malloc(sizeof(BTree));
    root1->right->left->data[0] = 'c';
    root1->right->left->left = NULL;
    root1->right->left->right = NULL;
    root1->right->right = (BTree*)malloc(sizeof(BTree));
    root1->right->right->data[0] = '-';
    root1->right->right->left = NULL;
    root1->right->right->right =(BTree*)malloc(sizeof(BTree));
    root1->right->right->right->data[0] = 'd';
    root1->right->right->right->left = NULL;
    root1->right->right->right->right = NULL;
    
    printf("该表达式树的带括号的等价中缀表达式为:");
    infixExpression(root1, 1);

    return 0;
}

2.11 递归实现(顺序表存储)

先序遍历:

这里数组的下标是从0开始的,即根节点是从0开始存储的。

void PreOrderTraverse(Tree T, int p_node) {
    if (T[p_node].empty) {
        printf("%d ", T[p_node].value);
        //先序遍历左子树
        if ((2 * p_node + 1 < MaxSize) && (T[2 * p_node + 1].empty) {
            PreOrderTraverse(T, 2 * p_node + 1);
        }
        //最后先序遍历右子树
        if ((2 * p_node + 2 < MaxSize) && (T[2 * p_node + 2].empty)) {
            PreOrderTraverse(T, 2 * p_node + 2);
        }
    }
}

中序遍历:

void INOrderTraverse(Tree T, int p) {
	if (T[p].empty)
	{
         //递归遍历左子树
         if (((2 * p + 1) < MaxSize) && (T[2 * p + 1].empty)) {
              INOrderTraverse(T, 2 * p + 1);
           }
        //访问当前结点
         printf("%d ", T[p].value);
       //递归遍历右子树
        if (((2 * p + 2) < MaxSize) && (T[2 * p + 2].empty)){
              INOrderTraverse(T, 2 * p + 2);
           }  
	}
}

 后序遍历:

void PostOrderTraverse(Tree T, int p) {
	if (T[p].empty)
	{
		
        if ((p * 2 + 1 < MaxSize) && (T[p * 2 + 1].empty)) {
           PostOrderTraverse(T, 2 * p + 1);
        }
        if ((p * 2 + 2 < MaxSize) && (T[p * 2 + 2].empty)) {
           PostOrderTraverse(T, 2 * p + 2);
        }
        printf("%d ", T[p].value);
   }
}

2.12 二叉树的层次遍历

所谓层次遍历二叉树,就是从树的根结点开始,一层一层按照从左往右的次序依次访问树中的结点。

层次遍历用链表存储的二叉树,可以借助链式队列存储结构实现,具体方案是:

  1. 将根结点入队;
  2. 从队列的头部提取一个结点并访问它,将该结点的左孩子和右孩子依次入队;
  3. 重复执行第 2 步,直至队列为空;

这里以构建这个二叉树,以此为例,进行层次遍历。

#include <stdio.h>
#include <stdlib.h>
//定义二叉树 
typedef struct BiTNode {
    int data;//数据域
    struct BiTNode* lchild, * rchild;//左右孩子指针
}BiTNode, * BiTree;
//定义链队列 
typedef struct LinkNode{
	BiTNode *value;
	struct LinkNode *next;
} LinkNode;
typedef struct{
	LinkNode *front , *rear;
}LinkQueue;

void InitQueue(LinkQueue &q) //初始化带头结点的链队列
{
	q.front = q.rear = (LinkNode *)malloc(sizeof(LinkNode));
	q.front->next = NULL;
}
//通过先序遍历构建二叉树 
void CreateBiTree(BiTree* T) {
    int num;
    scanf("%d", &num);
    //如果输入的值为 0,表示无此结点
    if (num == 0) {
        *T = NULL;
    }
    else
    {
        //创建新结点
        *T = (BiTree)malloc(sizeof(BiTNode));
        (*T)->data = num;
        CreateBiTree(&((*T)->lchild));//创建该结点的左孩子
        CreateBiTree(&((*T)->rchild));//创建该结点的右孩子
    }
}
//入队函数
void EnQueue(LinkQueue &q, BiTree node) {
	LinkNode *x = (LinkNode *)malloc(sizeof(LinkNode));
	x->value = node;
	x->next = NULL;
	q.rear->next = x; 
	q.rear = x;
}
//出队函数
BiTNode* DeQueue(LinkQueue &q) {
   if (q.front == q.rear)
   {
   	  printf("队列已空"); 
   	  exit(0); 
   }
   LinkNode *x = q.front->next;//从队头开始出队
   BiTNode* p_node =  x->value;
   q.front->next = x->next;
   if (q.rear == x)
   {
   	 q.front = q.rear;
   }
   return p_node;
}
//层次遍历二叉树
void LevelOrderTraverse(BiTree T) {
    //如果二叉树存在,才进行层次遍历
    if (T) {
        
        LinkQueue q;
        InitQueue(q); 
        //根结点入队
        EnQueue(q, T);
        //重复执行,直至队列为空
        while (q.front != q.rear)
        {
            //从队列取出一个结点
            BiTNode *p = DeQueue(q);
            //访问当前结点
            printf("%d ", p->data);
            //将当前结点的左右孩子依次入队
            if (p->lchild) {
                EnQueue(q, p->lchild);
            }
            if (p->rchild) {
                EnQueue(q, p->rchild);
            }
        }
    } 
}
//后序遍历二叉树,释放树占用的内存
void DestroyBiTree(BiTree T) {
    if (T) {
        DestroyBiTree(T->lchild);//销毁左孩子
        DestroyBiTree(T->rchild);//销毁右孩子
        free(T);//释放结点占用的内存
    }
}
int main() {
    BiTree Tree;
    CreateBiTree(&Tree);
    LevelOrderTraverse(Tree);
    DestroyBiTree(Tree);
    return 0;
}

输入:

5 2 1 0 0 3 4 0 0 0 8 6 0 9 0 0 7 0 0

输出:

5 2 8 1 3 6 7 4 9

2.13 通过二叉树的先、中、后序遍历重构二叉树

由二叉树的遍历序列构造唯一的二叉树:

  1. 先序+中序
  2. 后序+中序
  3. 层序+中序

 看了这两篇博客,我想让大家明白两点:

  1. 为什么包含中序遍历就能唯一确定一个二叉树
  2. 先序+后序,在某些情况下也能唯一确定一个二叉树

三、线索二叉树

3.1 基本概念 

二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化

对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。

根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
⚠️注意:线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题。

线索二叉树中的线索能记录每个结点前驱和后继信息。为了区别线索指针和孩子指针,在每个结点中设置两个标志 ltagrtag
当 ltag 和 rtag 为0时 ,leftChild 和 rightChild分别是指向左孩子和右孩子的指针;否则,leftChild是指向结点前驱的线索(pre),rightChild是指向结点的后继线索(suc)。由于每个标志只占用一个int,每个结点所需要的存储空间节省很多。 

现将二叉树的结点结构重新定义如下:

其中:ltag = 0 时lchild指向左儿子;ltag = 1 时lchild指向前驱;rtag=0 时rchild指向右儿子;rtag=1 时rchild指向后继。

线索二叉树存储结构:

typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode  *lchild , *rchild;
	int ltag , rtag;
}ThreadNode , *ThreadTree;

 3.2 中序线索化

以中序遍历序列为依据进行 “线索化”。

构建:

#include <stdio.h>
#include <stdlib.h>
typedef struct ThreadNode{ //将此二叉树线索化 
	char data;
	ThreadNode *lchild , *rchild;
	int ltag  , rtag ;
}ThreadNode , *ThreadTree;

ThreadNode *pre = NULL; //全局变量pre,指向当前访问结点前驱 

void CreateTree(ThreadTree &tree) //先序遍历构建二叉树 
{
	char node;
	scanf(" %c",&node); //%c前面加空格,为了过滤空格和回车的影响
	if (node == '0')
	{
		tree = NULL;
    }
    else{
    	tree = (ThreadNode *)malloc(sizeof(ThreadNode)) ;
    	tree->data = node;
        tree->ltag = 0;
        tree->rtag = 0;
    	CreateTree(tree->lchild);
    	CreateTree(tree->rchild);
	}
}

void InThread(ThreadTree &T) ;

void visit(ThreadNode *&q) ;

void Create_ThreadTree(ThreadTree &T) //中序遍历二叉树,一边遍历,一边线索化 
{
	if (T != NULL)
	{
		InThread(T) ;
		if (pre->rchild == NULL) //处理遍历的最后一个结点 
		{
			pre->rtag = 1;
		}
	}
}

void InThread(ThreadTree &T)  //中序遍历
{
	if (T != NULL)
	{
		InThread(T->lchild);
		visit(T);
		InThread(T->rchild);
	}
}

void visit(ThreadNode *&q) //线索化
{
	if (q->lchild == NULL){ //左子树为空,建立前驱线索 
		q->lchild = pre;
		q->ltag = 1;
	}
	if (pre != NULL && pre->rchild == NULL)
	{
		pre->rchild = q; //建立前驱结点的后继线索 
		pre->rtag = 1; 
	} 
	pre = q;//记得修改当前访问结点的前驱 
}

void Demo(ThreadTree tree , ThreadNode *G) 
{
	G = tree->lchild->lchild->rchild; // 求指定结点“G ” 的前驱结点和后继结点 
	
	if (G->ltag == 1)
	{
		printf("结点“G ”的前驱结点为 %c \n" , G->lchild->data);
	}
	if (G->rtag == 1)
	{
		printf("结点“G ”的后继结点为 %c" , G->rchild->data);
	}
}
int main()
{

	ThreadTree threadtree;
	CreateTree(threadtree); 
	Create_ThreadTree(threadtree);
	ThreadNode G_node;
	Demo(threadtree , &G_node); 
}

输入:

A B D 0 G 0 0 E 0 0 C F 0 0 0

输出:

结点“G ”的前驱结点为 D
结点“G ”的后继结点为 B

3.3 先序线索化

构建: 

void Create_ThreadTree(ThreadTree &T) //先序遍历二叉树,一边遍历,一边线索化 
{
	if (T != NULL)
	{
		PreThread(T) ;
		if (pre->rchild == NULL) //处理遍历的最后一个结点 
		{
			pre->rtag = 1;
		}
	}
}
void PreThread(ThreadTree &T) 
{
	if (T != NULL)
	{
        visit(T);
        if (T->ltag == 0) //【解释】
        {
		   PreThread(T->lchild);
        }
		PreThread(T->rchild);
	}
}
void visit(ThreadNode *&q) 
{
	if (q->lchild == NULL){ //左子树为空,建立前驱线索 
		q->lchild = pre;
		q->ltag = 1;
	}
	if (pre != NULL && pre->rchild == NULL)
	{
		pre->rchild = q; //建立前驱结点的后继线索 
		pre->rtag = 1; 
	} 
	pre = q;//记得修改当前访问结点的前驱 
}

上述代码,基本和中序线索化一模一样,只是PreThread函数做了一点变化!

【解释】

假设此时,pre 在 B结点 ,q 在D结点,即执行完visit(D)后,D -> lchild = pre = B  ; D -> ltag = 1 ; pre = q。然后接着执行PreThread(D -> lchild) ,但是D -> lchild 已经被修改成了指向 B 结点。如果这里不加 if (T -> ltag== 0) 这个判断,那么接下来,就要重新又执行回到B结点,开始不断的在B 和 D 间  ”转圈圈“ …………


3.4 后序线索化

后序没有 ”转圈圈“ 的问题!

void Create_ThreadTree(ThreadTree &T) //后序遍历二叉树,一边遍历,一边线索化 
{
	if (T != NULL)
	{
		PosThread(T) ;
		if (pre->rchild == NULL) //处理遍历的最后一个结点 
		{
			pre->rtag = 1;
		}
	}
}
void PosThread(ThreadTree &T) 
{
	if (T != NULL)
	{
		PosThread(T->lchild);
		PosThread(T->rchild);
        visit(T);
	}
}
void visit(ThreadNode *&q) 
{
	if (q->lchild == NULL){ //左子树为空,建立前驱线索 
		q->lchild = pre;
		q->ltag = 1;
	}
	if (pre != NULL && pre->rchild == NULL)
	{
		pre->rchild = q; //建立前驱结点的后继线索 
		pre->rtag = 1; 
	} 
	pre = q;//记得修改当前访问结点的前驱 
}

 

3.5 线索二叉树找前驱和后继

【1】中序线索二叉树

上面我们已经讲解了中序线索二叉树的线索化,并且通过线索化,我们可以在O(1)的复杂度找到某个结点(Tag == 1)的前驱和后继结点,例如G结点的前驱为D,后继为B。

但是如果 Tag == 0,就不能通过线索直接找到它的前驱和后继结点。例如B结点的中序后继我们无法直接得到,【这里不要以为 B -> rchild就可以了,这里只是一种特殊的情况,如果E结点后面还连接很多子树,那么B的中序后继结点就可能在其子树中】。 

找后继

假设 E结点 后还有p1 、p2、p3 三个结点

那么根据中序遍历的规则,肯定是……B p3 p1 E p2.......

所以B的中序后继结点一定是 B 右子树中最左下结点,即 p3

//在中序线索二叉树中找到结点P的后继结点
 
ThreadNode *Nextnode(ThreadNode *p)
{
	if (p->rtag == 0) return Firstnode(p->rchild);
	else return p->rchild;
}
//找到 E 为根的子树中,第一个被中序遍历的结点
ThreadNode * Firstnode(ThreadNode *p) 
{
	while(p->ltag == 0) p= p->lchild;
	return p; 
}

理解了这个算法思想,我们还可以利用它们,将递归式的线索化过程,改为非递归的线索化过程。

这样做的好处,可以将空间复杂度降到O(1)。

void Inorder(ThreadNode *&T)
{
	for (ThreadNode *p = Firstnode(T) ; p != NULL ; p = Nextnode(p))
	{
		visit(p);
	}
}

 找前驱

同理,找上图中B结点的前驱结点,因为其B -> ltag = 0,所以不能直接通过线索找到它的前驱结点。因此根据中序遍历的特点,某结点 p 的中序前驱结点一定是其左子树中最右下结点

ThreadNode * Prenode(ThreadNode *p)
{
	if (p->ltag == 0) return Firstnode(p->lchild);
	else return p->lchild;
}
ThreadNode* Lastnode(ThreadNode *p)
{
	while(p->rtag == 0) p = p->rchild;
	return p;
}

根据这个算法思想,我们还可以对中序线索二叉树进行逆向的中序遍历。

void RevInorder(ThreadNode *T)
{
	for (ThreadNode *p = Lastnode(T) ; p != NULL ; p = Prenode(p))
	{
		visit(p);
	}
}

【2】先序线索二叉树

找后继:

根据先序遍历的特点,若 p -> rtag = 0。若 p 结点有左孩子,则其先序后继一定为其左孩子。若 p结点没有左孩子,则其先序后继一定为其右孩子。

ThreadNode* findnextnode(ThreadNode *p)
{
	if (p->rtag == 0)
	{
		if (p->lchild != NULL) return p->lchild;
		else return p->rchild;
	}
	else return p->rchild;
}

找前驱:

因为先序遍历中,左右子树中的结点只可能是根的后继,不可能是其前驱,因此要想找到 p->ltag = 0 的 p结点的先序前驱结点,只能再先序遍历一遍。

BiTNode *p ; //p结点是目标结点(即找它的前驱结点)

BiTNode *pre = NULL; //指向当前访问结点的前驱

BiTNode *final = NULL;//用于记录最终结果(即p的前驱结点) 
void findPrenode(BiTree T)
{
	if (T != NULL)
	{
		visit(T);
		findPrenode(T->lchild);
		findPrenode(T->rchild);
	}
}

void visit(BiTNode *q)
{
	if (q == p)
	{
		final = pre;
	}
	else pre = q;

}

 【3】后序线索二叉树

找前驱:

根据后序遍历的特点,若 p -> ltag = 0。若 p 结点有右孩子,则其后序前驱一定为其右孩子。若 p结点没有右孩子,则其后序前驱一定为其左孩子。

ThreadNode* findprenode(ThreadNode *p)
{
	if (p->ltag == 0)
	{
		if (p->rchild != NULL) return p->rchild;
		else return p->lchild;
	}
	else return p->lchild;
}

找后继:

因为后序遍历中,左右子树中的结点只可能是根的前驱,不可能是其后继,因此要想找到 p->rtag = 0 的 p结点的后序后继结点,只能再后序遍历一遍。

BiTNode *p ; //p结点是目标结点(即找它的后继结点)

int flag = 0; 

BiTNode *final = NULL;//用于记录最终结果(即p的后继结点) 
void findnextnode(BiTree T)
{
	if (T != NULL)
	{
		findnextnode(T->lchild);
		findnextnode(T->rchild);
		visit(T);
	}
}

void visit(BiTNode *q)
{
	if (flag == 1)
	{
		final = q;
		Flag = 0;
	}
	if (q == p)
	{
		Flag = 1;
	}
}

四、树的存储结构

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

实现:定义结构数组存放树的结点.

每个结点含两个域:

  • 数据域:存放结点本身信息
  • 双亲域:指示本结点的双亲结点在数组中的位置。
#define MAX_TREE_SIZE 100
typedef struct PTNode {
    TElemType data;
    int parent; // 双亲位置域
} PTNode; 

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

​从这个顺序存储结构,我们不难看出查找指定结点的双亲结点很方便,但是查找指定结点的孩子只能从头开始遍历一遍。

特点:找双亲容易,找孩子难

4.2 孩子表示法(顺序+链式)

把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,用顺序表(含 n 个元素的结构数组)存储。  

typedef struct CTNode {
    int child; //孩子结点在数组中的位置 
    struct CTNode *next; //下一个孩子 
} *ChildPtr;

typedef struct {
    TElemType data;
    ChildPtr firstchild; // 孩子链表头指针,也是第一个孩子 
} CTBox;

typedef struct {
    CTBox nodes[MAX_TREE_SIZE];
    int n, r; // 结点数和根结点的位置
} CTree;

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

实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点下一个兄弟结点 

⚠️注意:孩子兄弟链表的结构形式与二叉链表完全相同,但存储结点中指针的含义不同:二叉链表中结点的左右指针分别指向该结点的左右孩子;而孩子兄弟链表结点的左右指针分别指向它的“长子” 和“大弟”。

typedef struct CSNode{
	ElemType data;   //数据域 
	struct CSNode *firstchild , *nextsibling; //第一个孩子 和 右兄弟指针 
} CSNode , *CSTree; 

这种解释上的不同正是 树 与 二叉树 相互转化的内在基础!

 4.4 森林和二叉树的转换

森林:森林是 m (m >= 0) 棵互不相交的树的集合。

你可以假想有一个结点是 B、C、D的父节点。

同理二叉树转换成森林,也是一样的。

总结:

五、树和森林的遍历

5.1 树的先根遍历

若树不空,则先访问根结点,然后依次先根遍历各棵子树。

和先序遍历换汤不换药!

5.2 树的后根遍历

若树不空,则先依次后根遍历各棵子树,然后访问根结点。 

​ 5.3 树的层次遍历

若树不空,则自上而下自左至右访问树中每个结点。

5.4 森林的先序遍历

先序遍历森林中(除第一棵树之外)其余树构成的森林。  即:依次从左至右对森林中的每一棵树进行先根遍历。 

当然将森林转换成二叉树,然后根据二叉树的先序遍历,得出的结果也是一样的。

5.5 森林的中序遍历

中序遍历森林中(除第一棵树之外)其余树构成的森林。 即:依次从左至右对森林中的每一棵树进行后根遍历(注意不是中序哦!!!)

​ 当然将森林转换成二叉树,然后根据二叉树的中序遍历,得出的结果也是一样的。

六、哈夫曼树

基本概念:

路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。

结点的路径长度:两结点间路径上的分支数。

树的路径长度:从树根到每一个结点的路径长度之和。记作:TL

TL(a)=0+1+1+2+2+3+3+4+4=20

TL(b)=0+1+1+2+2+2+2+3+3=16

完全二叉树是路径长度最短的二叉树。

 ​​​​

:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。

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

树的带权路径长度:树中所有叶子结点的带权路径长度之和。 记作:

定义:

哈夫曼树(也叫最优树) :带权路径长度 (WPL) 最短的树。

⚠️注意:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。

带权路径长度 (WPL) 最短的二叉树叫最优二叉树。

因为构造这种树的算法是由哈夫曼于 1952 年提出的, 所以被称为哈夫曼树,相应的算法称为哈夫曼算法。

哈夫曼树的构造

观察上图,我们可以推出如下重要的结论: 

  • 包含 n 棵树的森林要经过 n–1 次合并才能形成哈夫曼树,共产生 n–1 个新结点

  • 包含 n 个叶子结点 的哈夫曼树中共有 2n – 1 个结点。

  • 哈夫曼树的结点的 度数为 0 或 2, 没有度为 1 的结点。

  • 权值越小的叶子结点到根节点的路径长度越大

  • 哈夫曼树并不唯一,但WPL必然相同且为最优

 哈夫曼编码

哈夫曼树的应用很广,哈夫曼编码就是其在电讯通信中的应 用之一。在电讯通信业务中,通常用二进制编码来表示字母或其他字符,并用这样的编码来表示字符序列。

一个好的编码一定:

  • 编码总长度更短
  • 译码的唯一性问题

首先解决编码总长度更短的问题,就是解决数据的最小冗余编码问题

实际应用中各字符的出现频度不相同 ,为了达到数据的最小冗余编码,就要用(长)编码表示频率(小)的字符,使得编码序列的总长度最小,使所需总空间量最少 。

 为了解决译码的唯一性问题,要求任一字符的编码都不能是另一字符编码的前缀

这种编码称为前缀编码(其实是非前缀码)。

而利用最优二叉树可以很好地解决上述两个问题 

由哈夫曼树得到的二进制前缀编码称为哈夫曼编码

​ 当然,上述发送的电文还比较短,如果几百上千,那么它的总长度就会大幅度缩短,这种可变的二进制长度编码显然比固定二进制长度编码好!

译码

从哈夫曼树根开始,对待译码电文逐位取码。若编码是“0”, 则向左走;若编码是“1”,则向右走,一旦到达叶子结点,则译出 一个字符;再重新从根出发,直到电文结束。

​ 结尾

最后,非常感谢大家的阅读。我接下来还会更新 图 ,如果本文有错误或者不足的地方请在评论区(或者私信)留言,一定尽量满足大家,如果对大家有帮助,还望三连一下啦!

数据结构》(C语言)是为“数据结构”课程编写的教材,也可作为学习数据结构及其算法的C程序设计的参数教材。学了数据结构后,许多以前写起来很繁杂的代码现在写起来很清晰明了. 本书的前半部分从抽象数据类型的角度讨论各种基本类型的数据结构及其应用;后半部分要讨论查找和排序的各种实现方法及其综合分析比较。 全书采用类C语言作为数据结构和算法的描述语言。 本书概念表述严谨,逻辑推理严密,语言精炼,用词达意,并有配套出的《数据结构题集》(C语言),便于教学,又便于自学。 本书后附有光盘。光盘内容可在DOS环境下运行的以类C语言描述的“数据结构算法动态模拟辅助教学软件,以及在Windows环境下运行的以类PASCAL或类C两种语言描述的“数据结构算法动态模拟辅助教学软件”。内附 数据结构算法实现(严蔚敏配套实现程序) 目录: 第1章 绪论 1.1 什么是数据结构 1.2 基本概念和术语 1.3 抽象数据类型的表现与实现 1.4 算法和算法分析 第2章 线性表 2.1 线性表的类型定义 2.2 线性表的顺序表示和实现 2.3 线性表的链式表示和实现 2.4 一元多项式的表示及相加 第3章 栈和队列 3.1 栈 3.2 栈的应有和举例 3.3 栈与递归的实现 3.4 队列 3.5 离散事件模拟 第4章 串 4.1 串类型的定义 4.2 串的表示和实现 4.3 串的模式匹配算法 4.4 串操作应用举例 第5章 数组和广义表 5.1 数组的定义 5.2 数组的顺序表现和实现 5.3 矩阵的压缩存储 5.4 广义表的定义 5.5 广义表的储存结构 5.6 m元多项式的表示 5.7 广义表的递归算法第6章 二叉树 6.1 定义和基本术语 6.2 二叉树 6.2.1 二叉树定义 6.2.2 二叉树的性质 6.2.3 二叉树的存储结构 6.3 遍历二叉树和线索二叉树 6.3.1 遍历二叉树 6.3.2 线索二叉树 6.4 和森林 6.4.1 的存储结构 6.4.2 森林与二叉树的转换 6.4.3 和森林的遍历 6.5 与等价问题 6.6 赫夫曼及其应用 6.6.1 最优二叉树(赫夫曼) 6.6.2 赫夫曼编码 6.7 回溯法与的遍历 6.8 的计数 第7章 图 7.1 图的定义和术语 7.2 图的存储结构 7.2.1 数组表示法 7.2.2 邻接表 7.2.3 十字链表 7.2.4 邻接多重表 7.3 图的遍历 7.3.1 深度优先搜索 7.3.2 广度优先搜索 7.4 图的连通性问题 7.4.1 无向图的连通分量和生成 7.4.2 有向图的强连通分量 7.4.3 最小生成 7.4.4 关节点和重连通分量 7.5 有向无环图及其应用 7.5.1 拓扑排序 7.5.2 关键路径 7.6 最短路径 7.6.1 从某个源点到其余各顶点的最短路径 7.6.2 每一对顶点之间的最短路径 第8章 动态存储管理 8.1 概述 8.2 可利用空间表及分配方法 8.3 边界标识法 8.3.1 可利用空间表的结构 8.3.2 分配算法 8.3.3 回收算法 8.4 伙伴系统 8.4.1 可利用空间表的结构 8.4.2 分配算法 8.4.3 回收算法 8.5 无用单元收集 8.6 存储紧缩 第9章 查找 9.1 静态查找表 9.1.1 顺序表的查找 9.1.2 有序表的查找 9.1.3 静态表的查找 9.1.4 索引顺序表的查找 9.2 动态查找表 9.2.1 二叉排序和平衡二叉树 9.2.2 B和B+ 9.2.3 键 9.3 哈希表 9.3.1 什么是哈希表 9.3.2 哈希函数的构造方法 9.3.3 处理冲突的方法 9.3.4 哈希表的查找及其分析 第10章 内部排序 10.1 概述 10.2 插入排序 10.2.1 直接插入排序 10.2.2 其他插入排序 10.2.3 希尔排序 10.3 快速排序 10.4 选择排序 10.4.1 简单选择排序 10.4.2 形选择排序 10.4.3 堆排序 10.5 归并排序 10.6 基数排序 10.6.1 多关键字的排序 10.6.2 链式基数排序 10.7 各种内部排序方法的比较讨论 第11章 外部排序 11.1 外存信息的存取 11.2 外部排序的方法 11.3 多路平衡归并的实现 11.4 置换一选择排序 11.5 最佳归并 第12章 文件 12.1 有关文件的基本概念 12.2 顺序文件 12.3 索引文件 12.4 ISAM文件和VSAM文件 12.4.1 ISAM文件 12.4.2 VSAM文件 12.5 直接存取文件(散列文件) 12.6 多关键字文件 12.6.1 多重表文件 12.6.2 倒排文件 附录A 名词索引 附录B 函数索引 参考书目
本课件是一个动态演示数据结构算法执行过程的辅助教学软件, 它可适应读者对算法的输入数据和过程执行的控制方式的不同需求, 在计算机的屏幕上显示算法执行过程中数据的逻辑结构或存储结构的变化状况或递归算法执行过程中栈的变化状况。整个系统使用菜单驱动方式, 每个菜单包括若干菜单项。每个菜单项对应一个动作或一个子菜单。系统一直处于选择菜单项或执行动作状态, 直到选择了退出动作为止。 本系统内含84个算法,分属13部分内容,由菜单显示,与《数据结构》教科书中自第2章至第11章中相对应。各部分演示算法如下: 1. 顺序表 (1)在顺序表中插入一个数据元素(ins_sqlist) (2)删除顺序表中一个数据元素(del_sqlist) (3)合并两个有序顺序表(merge_sqlist) 2. 链表 (1)创建一个单链表(Crt_LinkList) (2)在单链表中插入一个结点(Ins_LinkList) (3)删除单链表中的一个结点(Del_LinkList) (4)两个有序链表求并(Union) (5)归并两个有序链表(MergeList_L) (6)两个有序链表求交(ListIntersection_L) (7)两个有序链表求差(SubList_L) 3. 栈和队列 (1)计算阿克曼函数(AckMan) (2)栈的输出序列(Gen、Perform) (3)递归算法的演示  汉诺塔的算法(Hanoi)  解皇后问题的算法(Queen)  解迷宫的算法(Maze)  解背包问题的算法(Knap) (4)模拟银行(BankSimulation) (5)表达式求值(Exp_reduced) 4. 串的模式匹配 (1)古典算法(Index_BF) (2)求Next 函数值(Get_next)和按Next 函数值进行匹配 (Index_KMP(next)) (3)求 Next 修正值(Get_nextval)和按 Next 修正值进行匹配(Index_KMP(nextval)) 5. 稀疏矩阵 (1)矩阵转置 (Trans_Sparmat) (2)快速矩阵转置 (Fast_Transpos) (3)矩阵乘法 (Multiply_Sparmat) 6. 广义表 (1)求广义表的深度(Ls_Depth) (2)复制广义表(Ls_Copy) (3)创建广义表的存储结构(Crt_Lists) 7. 二叉树 (1)遍历二叉树二叉树的线索化  先序遍历(Pre_order)  中序遍历(In_order)  后序遍历(Post_order) (2) 按先序建二叉树(CrtBT_PreOdr) (3) 线索二叉树二叉树的线索化  生成先序线索(前驱或后继) (Pre_thre)  中序线索(前驱或后继) (In_thre)  后序线索(前驱或后继) (Post_thre)  遍历中序线索二叉树(Inorder_thlinked)  中序线索的插入(ins_lchild_inthr)和删除(del_lchild_inthr)结点 (4)建赫夫曼和求赫夫曼编码(HuffmanCoding) (5)森林转化成二叉树(Forest2BT) (6)二叉树转化成森林(BT2Forest) (7)按表达式建(ExpTree)并求值(CalExpTreeByPostOrderTrav) 8. 图 (1)图的遍历  深度优先搜索(Travel_DFS)  广度优先搜索(Travel_BFS) (2)求有向图的强连通分量(Strong_comp) (3)有向无环图的两个算法  拓扑排序(Toposort)  关键路径(Critical_path) (4)求最小生成  普里姆算法(Prim)  克鲁斯卡尔算法(Kruscal) (5)求关节点和重连通分量(Get_artical) (6)求最短路径  弗洛伊德算法(shortpath_Floyd)  迪杰斯特拉算法(shortpath_DIJ) 9. 存储管理 (1)边界标识法 (Boundary_tag_method) (2)伙伴系统 (Buddy_system) (3)紧缩无用单元 (Storage_compaction) 10. 静态查找 (1)顺序查找(Search_Seq) (2)折半查找 (Serch_Bin) (3)插值查找 (Search_Ins) (4)斐波那契查找 (Search_Fib) (5)次优查找(BiTree_SOSTree) 11. 动态查找 (1)在二叉排序上进行查找(bstsrch)、插入结点(ins_bstree)和删除结点(del_bstree) (2)在二叉平衡上插入结点(ins_AVLtree) 和删除结点(del_AVLtree) (3)在 B-上插入结点(Ins_BTree) 和 删除结点(Del_BTree) (4)在 B+上插入结点(Ins_PBTree) 和 删除结点(Del_PBTree) 12. 内部排序 (1)简单排序法  直接插入排序(Insert_sort)  表插入排序(内含插入(Ins_Tsort) 重排(Arrange)两个算法)  起泡排序(BubbleSort)  简单选择排序(SelectSort) (2)复杂排序法  堆排序(HeapSort)  快速排序(QuickSort)  锦标赛排序(Tournament) (3)其他  快速地址排序(QkAddrst)  基数排序(RadixSort) 13. 外部排序 (1)多路平衡归并排序(K-Merge) (2)置换-选择排序(Repl_Selection)
数据结构(C语言)》严蔚敏、吴伟民+课本算法源码与习题解析 其中算法源码与解析涵盖了《数据结构》课本和习题集两部分,.所有源码实现均使用C语言,遵循C99标准,使用C-Free 5(C-Free置gcc编译器,编译时,需要在菜单栏,定位到构建-->构建选项-->类别-->C Language,勾选第三个:"ISO C99 plus GNU extensions [-std=gnu99]",即编译选项用-std=gnu99,而不是-std=c89或者-std=c99)测试通过(不要在CFree里创建工程,如果确实想在工程里运行,那文件互相引用的方式需要改写)。为了便于引用、查阅,各章内容在计算机中分文件夹存放,其中,《▲课本算法实现》中存放对课本中算法的实现,《▼配套习题解析》存放对题集中习题的解答,各源文件按章、节组织,组织方式见附录二。 《数据结构》(C语言)是为“数据结构”课程编写的教材,也可作为学习数据结构及其算法的C程序设计的参数教材。 本书的前半部分从抽象数据类型的角度讨论各种基本类型的数据结构及其应用;后半部分要讨论查找和排序的各种实现方法及其综合分析比较。其内容和章节编排1992年4月出的《数据结构》(第二)基本一致,但在本书中更突出了抽象数据类型的概念。全书采用类C语言作为数据结构和算法的描述语言。 本书概念表述严谨,逻辑推理严密,语言精炼,用词达意,并有配套出的《数据结构题集》(C语言),便于教学,又便于自学。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吹往北方的风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值