数据结构 3.0

目录

引子

3.1 树 

3.1.1 树的定义

3.1.2 树的基本术语

3.1.3 树的表示方法

3.2 二叉树

3.2.1 特殊的二叉树

3.2.3 二叉树的存储结构 

3.2.4 二叉树的操作

 3.3 二叉搜索树

3.3.1 二叉搜索树的定义

 3.3.2 二叉搜索树的动态查找

3.3.3 二叉搜索树的插入

3.3.4 二叉搜索树的删除

 3.4 平衡二叉树

3.4.1 平衡二叉树的定义

3.4.2 平衡二叉树的调整

3.5 树的应用 

3.5.1 堆及其操作

3.5.2 哈夫曼树

3.5.3 集合及其运算


引子

查找(Searching)在人们的日常生活中是一个常用的操作。

查找是可以分为静态查找动态查找的。

所谓静态查找,是指集合中的记录是固定的,仅仅查找,不涉及插入和删除的操作,不改变集合的元素。一般用数组就可以满足了。

动态查找,集合中的元素是会频繁变化的,除了查找功能,还要求具备插入和删除功能

接下来了解下面两种静态查找方法。

第一种:顺序查找(时间复杂度为n)

#define MAX 100
int SequentialSearch(int a[MAX],int length,int x)//x为查找元素
{
    int num;
    int i;
    for(i = 0;i<length;i++)
    {
        if(a[i]==x)
        {
            num = i;
            break;
        }
    }
    if(i==length)
    {
        printf("此元素不存在/n");
        return -1;
    }
    else
        return num;//返回元素位置
}

第二种:二分查找(时间复杂度为log n)

注* 二分法要求数组是有序的

#define MAX 100
int BinarySearch(int a[MAX],int length,int x)
{
    int left = 0;
    int right = length - 1;
    while(left<=right)
    {
    	int mid = (right + left) / 2;
    	if(x>a[mid])
    		left = mid + 1;
    	else if(x<a[mid])
    		right = mid - 1;
    	else
    		return mid;
	}
	return -1;
}

若数据不是有序时,我们可以采用其他的非线性表数据结构---树

3.1 树 

3.1.1 树的定义

树(Tree)是n(n>=0)个结点的有限集。它是一种一对多的数据结构。

n = 0 时为空树。

在任意一颗非空树中:

  1. 根(Root)结点仅有一个
  2. 当 n > 1时,其余顶点均互不相交,余下任意一结点都是一棵树,这些树被称为根的子树(SubTree). 树的结点具有相同数据类型及层次关系。 
3.1.2 树的基本术语
  1. 结点的度:一个结点的度是其子树的个数;
  2. 树的度:树的所有结点中最大的度数;
  3. 叶结点:度数为0的结点;
  4. 父结点:有子树的结点;
  5. 结点的层次:规定根结点在第一层其他任意结点的层数是其父结点的层数加一;
  6. 树的深度:树所有结点中的最大层次。
3.1.3 树的表示方法

在这里只讲孩子兄弟表示法。(它最常用)

它的结点结构如图所示:

为了更好的理解,也可以参考下面这个图:

以上图中,Data是数据域,左孩子、右孩子都是指针域,左孩子存储的是该结点的左结点的存储地址,右结点存储的是该结点的右结点的存储地址。 

结构定义代码如下:

struct TNode
{
    int data;//结点数据
    struct TNode* left;//指向左节点
    struct TNode* right;//指向右结点
};

3.2 二叉树

二叉树(Binary Tree)是n(n>=0)个结点的有限集合。它是由根节点和称为其左子树和右子树的两个不相交的二叉树组成。

一般二叉树可具有五种基本形态,如下图所示:

3.2.1 特殊的二叉树

首先是斜二叉树(也叫退化二叉树)所有结点都只有左(右)子树的二叉树。 

 接着是完美二叉树(又叫满二叉树)所有分支结点都存在左子树和右子树,并且所有的叶子都在同一层。

 最后是完全二叉树对树中n个结点进行编号(按从上到下、从左到右的顺序),编号位置与完美二叉树的编号位置相同。(下图中:第一个是完美二叉树,第二个是完全二叉树)

 3.2.2 二叉树的性质

  1.  在二叉树第i层最多有2^{i-1}个结点;

  2. 深度为k的二叉树最多有2^{k}-1个结点;
  3. 对任意一个非空的二叉树T,若n_{0}表示叶节点的个数、n_{2}是度数为2的非叶结点个数,那么两者关系满足n_{0} = n_{2} + 1;
  4. 有n个结点的完全二叉树的深度为\left \lfloor log_{2}n \right \rfloor+1;(\left \lfloor x \right \rfloor表示不大于x的最大整数)
  5. 若有一颗有n个结点的完全二叉树,其结点按顺序编号,对任意结点i有:
    (1)i = 1,则结点i是二叉树的根;
    (2)2i > n,则无左结点;
    (3)2i + 1> n,则无右结点.
3.2.3 二叉树的存储结构 

存储二叉树时,除了存储它的每个结点的数据,结点之间的逻辑关系也应该得到体现。

1. 二叉树的顺序存储结构 

这种结构是用一组连续的存储单元进行存储的(数组),结点的父子关系是通过它们的相对位置来反映的,无需指针。下图是一个完全二叉树:

2. 二叉树的链式存储结构 

 下图是一个一般的二叉树(已补充为完全二叉树):

从图中可以看出,为了补充成完全二叉树而增加的“虚”结点,浪费了存储单元。

为了解决这一问题,还是采用链表来存储。

结构图仍然可以参考这个:

3.2.4 二叉树的操作

判断二叉树是否为空比较简单,所以这里重点写遍历和创建二叉树。

1. 二叉树的遍历

树的遍历是指访问树的每个结点,且每个结点仅被访问一次。

二叉树的遍历根据二叉树的构成以及访问顺序可分为先序遍历、中序遍历、后序遍历和层序遍历

(1)中序遍历:即先访问左子树,再访问根结点,最后访问右子树。 

void InorderTraversal(BinaryTree tree)
{
    if(tree)
    {
        InorderTraversal(tree->left);
        printf("%d ",tree->data);
        InorderTraversal(tree->right);
    }
}

 (2)先序遍历:先访问根结点,再访问左子树,最后访问右子树。

void PerorderTraversal(BinaryTree tree)
{
    if(tree)
    {
        printf("%d ",tree->data);
        PerorderTraversal(tree->left);
        PerorderTraversal(tree->right);
    }
}

(3)后序遍历:先访问左子树,再访问右子树,最后访问根节点。

void PostorderTraversal(BinaryTree tree)
{
    if(tree)
    {
        PostorderTraversal(tree->left);
        PostorderTraversal(tree->right);
        printf("%d ",tree->data);
    }
}

“先”、“中”、“后”其实主要是看根结点的顺序,除此都是先遍历左子树,再遍历右节点。

(4)以上都是递归的算法,下面是一个非递归的遍历。(以中序遍历为例)

void InorderTraversal(BinaryTree tree)
{
    BinaryTree t;
    Stack S = CreateStack();//创建空栈S
    t = tree;//从根结点开始
    while(t||!IsEmpty(S))
    {
        while(t)//一直向左,并将沿途的结点压入堆栈
        {
            Push(S,t);
            t = t->left;
        }
        t = Pop(S,t);
        printf("%d ",t->data);//打印结点
        t = t->right;//转向右子树
    }
}

(5)层序遍历:按树的层次,从上到下、从左到右的顺序依次访问。(先遇到的先访问)

void LevelorderTraversal(BinaryTree tree)
{
    Queue Q;
    BinaryTree t;
    if(!tree)
        return;//空树直接返回
    Q = CreateQueue();//创建空队列Q
    Add(Q,tree);//将根结点指针入队
    while(!IsEmpty(Q))
    {
        t = DeleteQ(Q);//队列中取出一个元素
        printf("%d ",t->data);//访问取出队列的结点
        if(t->left)
            Add(Q,t->left);//左孩子非空入队
        if(t->right)
            Add(Q,t->right);//右孩子非空入队
    }
}

2. 二叉树的创建 

树是非线性结构,创建一颗二叉树必须先确定树中结点的输入顺序,常见的方法有先序创建和层序创建。

(1)层序创建:结点输入顺序是从上到下、从左到右的,空结点输入0。 理解可参考下图:

 3.3 二叉搜索树

3.3.1 二叉搜索树的定义

二叉搜索树(Binary Search Tree)也叫二叉排序树或二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。

二叉搜索树可以为空,若不为空,它就满足:

  1. 非空左子树的所有键值小于其根结点的键值;
  2. 非空右子树的所有键值大于其根结点的键值;(左小右大)
  3. 左、右子树都是二叉搜索树。
 3.3.2 二叉搜索树的动态查找

 二叉搜索树是施加了一定约束的特殊二叉树,它一般用链表存储,结点与普通二叉树的代码完全一样。以下三个函数是普通二叉树所没有的:

  1. Position Find(BinTree tree,ElementType x): 从树中查找x并返回其地址。
  2. Position FindMin(BinTree tree): 从树中查找最小值并返回其地址。
  3. Position FindMax(BinTree tree): 从树中查找最大值并返回其地址。

(1)Find查找

Position Find(BinTree tree,ElementType x)
{
    if(!tree)
        return NULL;
    if(x>tree->data)
        return Find(tree->right,x);
    else if(x<tree->data)
        return Find(tree->left,x);
    else
        return tree;
}

 以上采用的递归方法,接下来使用非递归函数。

Position Find(BinaryTree tree,ElementType x)
{
    while(tree)
    {
        if(x>tree->data)
            tree = tree->right;
        else if(x<tree->data)
            tree = tree->left;
        else
            break;
    }
    return tree;
}

(2)查找最大值和最小值

根据二叉搜索树的性质特点,最小元素一定在树的最左分支的叶节点上,最大元素一定在树的最右分支的叶结点上。

查找最小元素的递归函数:

Position FindMin(BinaryTree tree)
{
    //最小值在最左边
    if(!tree)//二叉搜索树为空,直接返回NULL
        return NULL;
    else if(!tree->left)//找到最左端返回
        return tree;
    else //沿左分支递归查找
        return FindMin(tree->left);
}

查找最大元素的非递归函数:

Position FindMax(BinaryTree tree)
{
    if(tree)
        while(tree->right)
            tree = tree->right;//沿着右分支一直向下,直到最右端
    return tree;
}
3.3.3 二叉搜索树的插入

首先要在tree里查找x,若存在,可以不用进行插入;若不存在,查找停止的位置就是要插入的位置。

BinaryTree Insert(BinaryTree tree,ElementType x)
{
    if(!tree)//若树为空,生成并返回一个结点的二叉搜索树
    {
        tree = (BinaryTree)malloc(sizeof(struct TNode));
        tree->data = x;
        tree->left = tree->right = NULL;
    }
    else
    {
        //寻找x的位置
        if(x<tree->data)
            tree->left = Insert(tree,x);//递归插入左子树
        else if(x>tree->data)
            tree->right = Insert(tree,x);//递归插入右子树
    }
    return tree;
}
3.3.4 二叉搜索树的删除

二叉搜索树的删除操作相比上面的操作要更为复杂。

删除操作,首先要先找到需要删除元素的位置,根据位置的不同,删除操作也有所不同。一般是考虑以下三种情况:

首先删除的是叶节点。这是最简单的一种。可以直接删除,再将其父结点的指针置空即可。

接着删除的是只有一个孩子的结点。这种情况,在删除之前需要修改其父结点的指针,指向要删除节点的孩子结点。

 最后删除的是有左、右两颗子树的结点。这里关于用哪一颗子树的根节点来填充删除结点的位置,有两种选择:一种选择是取其右子树中的最小元素;另外一种则是取其左子树的最大元素。

 参考代码如下:

BinaryTree Delete(BinaryTree tree,ElementType x)
{
    struct TNode* T = (struct TNode*)malloc(sizeof(TNode));
    if(!tree)
        printf("删除结点未找到\n");
    else
    {
        if(x<tree->data)
            tree->left = Delete(tree->left,x);
        else if(x>tree->data)
            tree->right = Delete(tree->right,x);
        else
        {
            //tree已经为待删结点
            if(tree->left&&tree->right)//若待删结点既有左结点,又有右结点
            {
                T = FindMin(tree->right);//此处采用的是第一种选择,右子树的最小值
                //也可换为 T = FindMax(tree->left);
                tree->data = T->data;
                tree->right = Delete(tree->right,tree->data);//再从右子树中删除最小值
            }
            else//待删除结点有一个子结点或没有子结点
            {
                T = tree;
                if(!tree->left)//只有右孩子或者没有子结点
                    tree = tree->right;
                else//只有左结点
                    tree = tree->left;
                free(T);
            }
        }
    }
return tree;
}

 3.4 平衡二叉树

二叉搜索树查找的时间复杂度是由查找过程中的比较次数来衡量的,而比较是从根结点到叶结点的路径进行的,它又取决于树的深度。

树的深度又受二叉树的类型影响,例如:对于完全二叉树来说,它的查找复杂度为O(log N);若是二叉树退化为一颗单枝树的极端(斜二叉树)时,它的查找复杂度为O(N)。

假定二叉搜索树中每个结点的查找概率都是相同的,那么我们称查找所有结点的比较次数的平均值为树的“平均查找长度"(Average Search Length,ASL)。

一棵树的ASL值越小,与完全二叉树就越接近,它的查找时间复杂度也就越接近O(log N)。

3.4.1 平衡二叉树的定义

平衡二叉树(Balanced Binary Tree)又称为AVL树,它的查找、删除、插入均可在O(log N)时间内完成。AVL树可以为空,若不为空,则具有以下性质:

  1. 任一结点的左、右子树均为AVL树;
  2. 根结点左、右子树高度的绝对值不超过1.

平衡因子(Balance Factor,BF),简单地说就是左、右子树的高度差,BF(T) = h_{L} - h_{R}

n_{h}是高度为h平衡二叉树的最小结点数。F_{h}表示斐波那契序列。

hn_{h}F_{h}
011
121
242
373
.........
有两种关系式: n_{h} = n_{h-1} + n_{h-2} +1
                          n_{h} = F_{h+2} -1

  

给定结点数为n的AVL树的最大高度为O(log_{2} n).                     

3.4.2 平衡二叉树的调整

当向AVL树中插入新的结点时,可能会破坏了树的平衡,这时候就需要做树的“平衡化”处理

1. 单旋调整

 如上图所示,(1)是一颗平衡树,平衡因子的绝对值最大都没有超过1;(2)是在(1)的基础上开始插入新的结点“4”,此时平衡因子的绝对值最大已经超过1,在(2)中,“1”为“发现者”,“4”为“麻烦结点”,“4”在“1”的右子树的右边,这个叫RR插入,它需要RR旋转(右单旋)。

(1)是最开始的AVL树,(2)在(1)的基础上插入了一个新结点,此时“3”是“发现者”,“5”是那个“破坏结点”, “5”在“3”的左子树的左边,这个叫LL插入,它需要LL旋转(左单旋)。

2. 双旋调整

双旋分为两种,“左-右双旋”和“右-左双旋”,它是两次单旋的合成结果。

上图为“左-右双旋”,它的调整方式是:将E置于A的位置,A及其右子树调整为E的右子树。

 上图为“右-左双旋”,它的调整方式是:将D置于A的位置,A及其左子树调整为D的左子树。

3.5 树的应用 

3.5.1 堆及其操作

通过之前的学习,知道队列的基本特征“先进先出”,前面的元素未处理完,后面的元素只能等待。但是,实际应用中,总有些情况需要优先处理,需要特权,此时就要用到堆(Heap)。

1. 堆的定义

堆(Heap)是一种特殊的队列。从堆中取出元素的顺序是按照元素的优先级大小排序的。

2. 堆的表示

堆最常用的结构是二叉树,若非特指,它就是一颗完全二叉树。

由于结点排布及其规律,所以通常不必用指针,而是用数组来实现堆的存储。

用数组来表示完全二叉树是堆的第一个特性,称为堆的结构特性;堆的另一特性是有序性,即任一结点元素的数值与其子结点所存储的值是相关的。

据此,又分为了两种基本堆:最大堆(MaxHeap) 和最小堆(MinHeap)

最大堆,任一结点的值大于或等于其子结点的值。(根节点元素的值在整个堆中是最大的)

最小堆,任一结点的值小于或等于其子结点的值。(根节点元素的值是整个堆中最小的)

3. 最大堆操作

最大堆的操作一般就是创建、判断堆的空满、插入、删除。

首先看创建:

#define MAXDATA 1000
typedef struct HNode* Heap;//Heap为指向堆结构体的指针
struct HNode
{
    ElementType* data;//动态数组的指针,存放堆中的元素
    int size;//当前堆中的数量
    int capacity;//堆的容量
};
//将Heap重新命名为MaxHeap和MinHeap来表示最大堆和最小堆
typedef Heap MaxHeap;
typedef Heap MinHeap;
//创建最大堆
MaxHeap CreateHeap(int MaxSize)
{
    MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));//给结构体分配空间
    H->data = (ElementType*)malloc(sizeof(ElementType)*(MaxSize+1));//给动态数组分配空间
    H-> size = 0;//初始堆为空
    H->capacity = MaxSize;
    H->data[0] = MAXDATA;//最大堆的哨兵元素,保证根节点值最大
    //设好哨兵后,索引就从1开始
    return H;
}

接着看插入,插入之前是要判断堆是否已经满的情况:

bool IsFull(MaxHeap H)
{
    return(H->size==H->capacity);
}
bool Insert(MaxHeap H,ElementType x)
{
    int i;
    if(IsFull(H)){
        printf("堆已经满了\n");
        return false;
    }
    //i是指向插入后堆中的最后一个元素的位置
    for(i = H->size + 1;H->data[i/2]<x;i = i/2)// i/2 是叶结点的位置
        H->data[i] = H->[i/2];
    H->data[i] = x;
    return true;
}

 还有删除操作,删除之前需要判断堆是否为空的情况:(最大堆的删除就是取出根结点的元素)

#define ERROR -1
bool IsEmpty(MaxHeap H)
{
    return(H->size==0);
}
//删除最大堆中的最大值并返回
ElementType DeleteMax(MaxHeap H)
{
    int parent,child;
    ElementType MaxItem,x;
    if(IsEmpty(H))
    {
        printf("最大堆为空\n");
        return ERROR;
    }
    MaxItem = H->data[1];//把要删的结点保存起来
    x = H->data[H->size--];//x存放最后一个结点并减小堆的大小
    //整个for循环的目的就是寻找x的位置
    for(parent = 1;parent*2<=H->size;parent = child)
    {//从根节点开始
        child = parent * 2;//child为左子结点,child+1为右子结点
        if((child!=H->size)&&(H->data[child] < H->data[child+1]))
        //如果右子节点存在且比左子节点大
            child++;//child指向左节点
        if(x>H->data[child])//x节点的值比子节点大,则找到了插入位置,退出循环
            break;
        else
            H->data[parent] = H->data[child];//把子结点中大的那一个换(child)上去
    }
    H->data[parent] = x;
    return MaxItem;//返回已删除的数值
}
3.5.2 哈夫曼树

1. 哈夫曼树的定义

哈夫曼树(Huffman Tree)是一种用于数据压缩的树形结构。它是一种带权路径长度最短的二叉树,又称为最优二叉树

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

每个叶结点的带权路径长度之和就是这棵树的带权路径长度(Weighted Path Length,WPL).

例如:有四个结点,给定权值{11,21,31,41},用此组权值构造哈夫曼树,步骤如下:

(1)在给定的权值中,选择最小和次小的权值作为叶结点,其根结点为最小与次小数值相加;

(2)把其根结点加入剩余的权值中进行比较;

(3)重复(1)、(2)操作,直到只剩下一颗二叉树时,这就是哈夫曼树。

 *注:也许同一组权值所构成的哈夫曼树形态不同,但带权路径长度一定相同,并且一定是最小值。

2. 哈夫曼树的构造

typedef struct HTNode* HuffmanTree;//哈夫曼树类型
struct HTNode
{
    int weight;//结点权值
    HuffmanTree left;//指向左子树
    HuffmanTree right;//指向右指数
}
HuffmanTree Huffman(MinHeap H)//此处最小堆的元素类型为HuffmanTree
{
    int i,N;
    HuffmanTree T;
    BuildHeap(H);
    N = H->size;
    for(i=1;i<N;i++)
    {
        T = (HuffmanTree)malloc(sizeof(struct HTNode));
        T->left = DeleteMin(H);
        T->right = DeleteMin(H);
        T->weight = T->left->weight + T->right->weight;
        Insert(H,T);
    }
    return DeleteMin(H);
}

3. 哈夫曼编码

哈夫曼编码是一种高效的编码方式,在信息存储和传输过程中,用于对信息进行压缩。

首先要明白什么是编码。编码是把人类能看懂的信息转化为计算机能识别的二进制形式。

一般最常见的就是ASCII码。ASCII码中,把每一个字符表示成特定的8位二进制数,很显然,它是一个等长编码等长编码有一个很大的缺点,由于计算机的存储空间和网络传输的宽带是有限的,若等长编码太长会造成一定的空间浪费

当然,也有不等长编码。它解决了空间浪费上的问题,但是,它具有二义性。例如:0表示a,00表示b,那么000可以是aaa也可以是ab。

为了解决不等长编码的二义性,就不得不说哈夫曼编码了。

给定权值{12,17,18,21,26},画哈夫曼树,把结点左边当作0,结点右边当成1,哈夫曼树从根结点到每一个叶结点(权值)的路径构成的编码,这就是哈夫曼编码。如图所示:

 从图中可以看出,权值都在叶结点上。

它既避免了空间上的浪费,又没有二义性,每个值权(字符)的编码都是独一无二的。

3.5.3 集合及其运算

1. 集合
集合是一种常见的数据表示方式。

集合运算包括交、并、补、查以及判定一个数据是否是某一集合中的元素

查并集:集合并、查某元素属于什么集合

这里使用树的结构是最简便的,与之前父子关系指针有所不同,它是由子结点指向父结点的(双亲表示法)

这里选择用数组进行存储。

2. 集合运算

(1)查找某个元素所在的集合

int Find(setType s[],ElementType x)//x为查找的元素
{
    int i;
    for(i=0;i<Maxsize&&s[i].data!=x;i++)//Maxsize为全局变量,表示数组S的最大长度
    if(i>=Maxsize)    
        return -1;//未查找到该元素,返回-1
    for(;s[i].parent>=0;i=s[i].parent)
        return i;//返回x所在集合的根结点
}

(2)集合的并运算

就是要把两个元素所在的集合合并起来(两个元素不在同一集合)。

首先要查找x,y两个元素所在集合树的根结点;

接着判断,若根结点不同,则将其中一个根结点的父结点指针设置为另一个根结点的数组下标;

若根结点相同,则不做操作,因为本身就在同一集合中,没必要再次合并。

void Union(SetType s[],ElementType x,ElementType y)
{
    int Root1,Root2;
    Root1 = Find(s,x);
    Root2 = Find(s,y);
    if(Root1!=Root2)
        s[Root2].parent = Root1;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值