6.二叉树

6.二叉树

6.1.二叉树的概念和性质

6.1.1.定义和术语

  • 二叉树是一种递归数据结构
  • 左子树 右子树
  • 孩子 双亲 兄弟(同一个双亲的孩子之间可互称兄弟)
  • 结点的孩子个数成为结点的度
  • 度为0的结点称为叶子结点,非叶子节点称为内部结点或分支结点
  • 结点的层次从根结点开始定义,根为第一层,根的孩子为第二层。二叉树中结点的最大层数称为二叉树的高度或深度。

6.1.2.性质

  • 看书P128
  • 完全二叉树:深度为k且含n个结点的二叉树,其每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应

6.2.二叉树的存储结构

6.2.1.顺序存储结构

1.类型定义
typedef char TElemType;  //假设二叉树结点的元素类型为字符
typedef struct {
    TElemType *elem;  //0号单元闲置,1号单元放根
    int lastIndex;   //二叉树最后一个结点的编号
}SqBiTree;           //顺序存储的二叉树
2.判断v结点是否为u结点的子孙
Status is_Desendant(SqBiTree T, int u, int v) {
    if(u<1 || u>T.lastIndex || v<1 || v>T.lastIndex || v<=u) 
        return FALSE;
    while(v>u) {
        v = v/2;
        if(v==u) return TRUE;
    }
    return FALSE;
}

对于一般二叉树,如果其形态接近于完全二叉树(空缺节点较少),则宜使用顺序存储结构存储。

6.2.2.链式存储结构

1.二叉链表
  • 二叉树的结点存储结构应当包括一个数据域和两个指针域
  • 类型定义:
typedef struct BiTNode {
    TElemType data;
    struct BiTNode *lchild, *rchild;
}BiTNode,*BiTree;
  • 接口定义:
void InitBiTree(BiTree &T); //创建一棵空二叉树 T==NULL
BiTree MakeBiTree(TElemType e,BiTree L,BiTree R);
//创建一棵二叉树T,其中根结点的值为e,L和R分别作为左子树和右子树
void DestroyBiTree(BiTree &T);//销毁二叉树
Status BiTreeEmpty(BiTree T); //对二叉树判空,返回TRUE或FALSE
Status BreakBiTree(BiTree &T,BiTree &L,BiTree &R); //将一棵二叉树T分解城根、左子树、右子树三个部分
Status ReplaceLeft(BiTree &T,BiTree &LT);//替换左子树。若T非空,则用LT替换T的左子树,并用LT返回T的原有左子树
Status ReplaceRight(BiTree &T,BiTree &RT);//替换右子树。若T非空,则用RT替换T的右子树,并用RT返回T的原有右子树
1.1.创建二叉树
BiTree MakeBiTree(TElemType e,BiTree L,BiTree R) {
 //创建一棵二叉树T,其中根结点的值为e,L和R分别作为左子树和右子树
    BiTree t;
    t = (BiTree)malloc(sizeof(BiTNode));
    if(NULL == t) return NULL;
    t->data = e;
    t->lchild = L;
    t->rchild = R;
    return t;
}
1.2.替换左子树
Status ReplaceLeft(BiTree &T,BiTree &LT) {
    //替换左子树。若T非空,则用LT替换T的左子树,并用LT返回T的原有左子树
    BiTree temp;
    if(NULL == T) return ERROR;
    temmp = T->lchild;
    T->lchild = LT;
    LT = temp;
    return OK;
}
2.三叉链表
  • 在二叉链表的基础上增加一个指向双亲结点的指针域parent
  • 类型定义:
typedef struct TriTNode {
    TElemType data;
    TriTNode *parent, *lchild, *rchild;
}TriTNode, *TriTree;

6.3.遍历二叉树

6.3.1遍历策略

区别是访问根结点的时机不同

  • 先序遍历:

第一个结点为根;

下一个结点:①有左子树:则为左孩子

​ ②无左有右:则为右孩子

​ ③该结点左右子树均为空,找它的祖先,祖先有右孩子,且不是它自己,下一个结点为停下来祖先结点的右结点

  • 中序遍历:遍历根结点的左子树 -> 访问根结点 -> 遍历根结点的右子树
  • 后序遍历:从左到右先叶子后结点的方式遍历左右子树,最后是访问根结点

6.3.2.二叉树的递归遍历

1.前序遍历
void PreOrderTraverse(BiTree T) {
    if(NULL == T) return;
    printf("%c",T->data);//可以更改为其他对结点的操作
    PreOrderTraverse(T->lchild);//先序遍历左子树
    PreOrderTraverse(T->rchild);//先序遍历右子树
}
2.中序遍历
  • 递归回去:从哪里来的回哪里去
  • 代码实现:
Status InOrderTraverse(BiTree T, Status (*visit)(TElemType e)) {
    //中序遍历二叉树,visit是对数据元素操作的应用函数
    if(NULL == T) return NULL;
    if(ERROR == InOrderTraverse(T->lchild,visit)) return ERROR; //递归遍历T的左子树
    if(ERROR == visit(T->data)) return ERROR;//访问结点的数据域
    return InOrderTraverse(T->rchild,visit);//递归遍历T的右子树
}
3.后序遍历
void PostOrderTraverse(BiTree T) {
    if(NULL == T) return;
    PostOrderTraverse(T->lchild);
    PostOrderTraverse(T->rchild);
    printf("%c",T->data);
}

已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树

已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树

而已知前序和后序,无法确定

6.3.3.二叉树的非递归遍历

第一个结点在哪 -> 下一个结点在哪

1.使用栈的中序非递归遍历
  • 思路:指针T从根结点出发,向左走到底,并依次将指向沿途结点的左孩子指针入栈。

    重复以下步骤,直到T为空:

    ①访问T结点

    ②若T结点的右孩子存在,则令T指向右孩子,然后向左走到底,并以此将指向沿途结点的指针入栈。若不存在,则判断栈是否为空。若非空则将栈顶的指针退栈并赋予T;若空,则强制T为空(遍历结束)

  • 代码:

BiTNode *GoFarLeft(BiTree T, LStack &S) {//LStack为链栈
    //从T结点出发,沿左分支走到底,沿途结点的指针入栈S,返回左下结点的指针
    if(NULL == T) return NULL;
    while(T->lchild != NULL) {
        Push_LS(S,T);
        T = T->lchild;
    }
    return T;
}
//注意最左下的结点没有入栈

void InOrderTraverse_I(BiTree T,Status (*visit)(TElemType e)) {
    //中序非递归遍历二叉树T,visit是对数据元素操作的应用函数
    LStack S;
    InitStack_LS(S);
    BiTree p;
    p = GoFarLeft(T,S);//找到最左下的结点,并将沿途结点的指针入栈S
    while(p!= NULL) {
        visit(p->data);
        if(p->rchild != NULL) p = GoFarLeft(p->rchild,S);
        //令p指向其右孩子为根的子树的最左下结点
        else if(StackEmpty_LS(S)!=TRUE) Pop_LS(S,p);//栈不空时退栈
        else p = NULL;//栈空表明遍历结束
    }
}
2.不使用栈的先序非递归遍历
  • 采用三叉链表存储结构
  • 代码实现:
Status PreOrderTraverse(TriTree T,Status (*visit)(TElemType e)) {
    //先序非递归遍历二叉树T,visit的实参是对数据元素操作的应用函数
    TriTree p,pr;
    if(T!=NULL) {
        p = T;
        while(p!=NULL) {
            visit(p->data);
            if(p->lchild!=NULL) p = p->lchild;//若有左孩子,继续访问
            else if(p->rchild!=NULL) p = p->rchild;//若有右孩子,继续访问
            else {
                //沿双亲指针链查找,找到第一个有右孩子的p结点,找不到则结束
                do{
                    pr = p; p = p->parent;
                }while(p!=NULL && (p->rchild==pr || NULL == p->rchild));
                if(p!=NULL) p = p->rchild;//找到后,p指向右孩子结点(真正的下一个结点)
            }
        }
    }
    return OK;
}
3.层次遍历——使用队列的非递归遍历
  • 层次遍历是按二叉树的层次从小到大且每层从左到右的顺序依次访问结点

  • 执行步骤:

    (1)访问根结点,并将根结点入队

    (2)当队列不空时,重复以下操作:

    ①队头结点出队

    ②若其有左孩子,则访问左孩子并入队

    ③若其有右孩子,则访问右孩子并入队

  • 代码实现:

void LevelOrderTraverse(BiTree T,Status (*visit)(TElemType e)) {
    if(T!=NULL) {
        LQueue Q;
        InitQueue_LQ(Q);
        BiTree p = T;//初始化
        visit(p->data);
        EnQueue_LQ(Q,p);//访问根结点,并将根结点入队
    }
    while(OK == DeQueue_LQ(Q,p)) {//当队非空时重复执行操作,出队
        if(p->lchild != NULL) {//访问左孩子并入队
            visit(p->lchild->data);
            EnQueue_LQ(Q,p->lchild);
        }
        if(p->rchild != NULL) {//访问右孩子并入队
            visit(p->rchild->data);
            EnQueue_LQ(Q,p->rchild);
        }
    }
}

6.3.4.遍历的应用

1.销毁二叉树
void DestroyBiTree(BiTree &T) {
    //销毁二叉树T,基于后序遍历
    if(T!=NULL) {
        DestroyBiTree(T->lchild);
        DestroyBiTree(T->rchild);
        free(T);//释放根结点
    }
}
2.求二叉树的深度
//一、分治法
int BiTreeDepth(BiTree T) {
    //返回二叉树深度,T为树根的指针
    //问题规模分解:根+左子树+右子树
    int depthLeft,depthRight;
    if(NULL == T) return 0;
    else {
        depthLeft = BiTreeDepth(T->lchild);
        depthRight = BiTreeDepth(T->rchild);
        return 1 + (depthLeft > depthRight ? depthLeft : depthRight);
    }
}
//二、基于遍历
void Depth_T(BiTree T,int lev,int &dep) {
    //lev与T配套,lev代表T结点所对应的层次
    if(NULL == T) return;
    if(lev>dep) dep = lev;
    Depth_T(T->lchild,lev+1,dep);
    Depth_T(T->rchild,lev+1,dep);
}
3.求结点总数
int Number(BiTree T) {
    if(T == NULL) return;
    return Number(T->lchild) + Number(T->rchild) + 1;
}
4.二叉树的叶子结点计数
void CountLeaf(BiTree T, int &count) {
    if(T!=NULL) {
        if(NULL == T->lchild && NULL == T->rchild) count++;//对叶子结点计数
        CountLeaf(T->lchild,count);//对左子树进行递归计数
        CountLeaf(T->rchild,count);//对右子树进行递归计数
    }
}
5.构造二叉树
  • 可以利用先序遍历框架,依次生成结点,建立二叉树的存储结构。在先序遍历序列中,插入表示空子树的符号#,以构成二叉树的树形描述序列。
  • 代码实现:
BiTree CreateBiTree(char * defBT, int &i) {
    //基于先序遍历框架构造二叉树。defBT为树形描述序列,i为defBT的当前位标,初值为0
    BiTree T;
    TElemType ch;
    ch = defBT[i++];
    if('#' == ch) InitBiTree(T);//空树
    else {
        T = MakeBiTree(ch,NULL,NULL);//构造结点ch
        T->lchild = CreateBiTree(defBT,i);//构造左子树
        T->rchild = CreateBiTree(defBT,i);//构造右子树
    }
    return T;
}
6.求以指定结点为根结点的子树深度
//求二叉树中以值为x的结点为根的子树的深度
int Depth(BiTree T) {//求深度
  //结束,当树为空树时,返回0
   if(T == NULL) return 0;
   //分解
   int l,r;
   //问题规模:左子树 + 右子树 + 根
   l = Depth(T->lchild);
   r = Depth(T->rchild);
   //组合:将最大的值返回
   return 1 + (l>r ? l : r);
}

BiTree Search(BiTree T, TElemType e) {
  if(T == NULL) return NULL;
  if(T->data == e) return T;
  BiTree res = Search(T->lchild,e);
  if(res!= NULL) return res;
  return Search(T->rchild,e);
}

int Depthx(BiTree T, TElemType x) //查找x
{   // Add your code her
    if(NULL == T) return NULL;
    BiTree temp = Search(T,x);
    if(temp == NULL) return NULL;
    return Depth(temp);
}
7.求分支结点总数
//一、基于遍历,后序中序前序都可以
void countNodes(BiTree T,int &count) {
    if(T==NULL) return;
    if(T->lchild!=NULL || T->rchild!=NULL) count++;
    countNodes(T->lchild,count);
    countNodes(T->rchild,count);
}

//二、分治法
int countNodes(BiTree T) {
    if(T==NULL || T->lchild!=NULL && T->rchild!=NULL) return 0;
    int l,r;
    l = countNodes(T->lchild);
    r = countNodes(T->rchild);
    return l + r + 1;
}

6.4.堆

堆是一类完全二叉树

6.4.1.堆的定义

  • 堆的所有非叶子结点均不大于(或不小于)其左右孩子结点
  • 若堆的所有非叶子结点均不大于其左右孩子结点,则称为小顶堆(小根堆);

在这里插入图片描述

  • 大顶堆(大根堆)
  • 堆中的子树称为子堆
  • 堆中根结点的位置称为堆顶,最后结点的位置称为堆尾,结点个数称为堆长度
  • 类型定义:
typedef struct {
    RcdType *rcd;//堆基址,0号单元闲置,顺序存储
    int n;//堆长度
    int size;//堆容量
    int tag;//小顶堆与大顶堆的标志:tag=0为小顶堆,tag=1为大顶堆
    int (*prior)(KeyType,KeyType);//函数变量,用于关键字优先级比较
}Heap;//堆类型
  • 假设关键字类型为整数,则大小堆顶的优先函数可分别定义如下:
int greatPrior(int x, int y) {return x>=y;}//大堆顶优先函数
int lessPrior(int x, int y) {return x<=y;}//小堆顶优先函数
  • 接口定义:❗❗的为重点,⭐以❗❗为基础
int InitHeap(Heap &H,int size,int tag,int(*prior)(KeyType,KeyType));//prior为相应的优先函数
//初建最大容量为size的空堆H,当tag为0或1时分别表示小堆顶和大堆顶
void MakeHeap(Heap &H,RcdType *E,int n,int size,int tag,int(*prior)(KeyType,KeyType));
//用E建长度为n的堆H,容量为size,当tag为0或1的时候分别表示小顶堆或大顶堆
Status DestroyHeap(Heap &H);//销毁堆H
❗❗void ShiftDown(Heap &H,int pos);
//对堆H中位置为pos的结点做筛选,将以pos为根的子树调整为子堆
Status InsertHeap(Heap &H,RcdType e);//将e插入堆
⭐Status RemoveFirstHeap(Heap &H,RcdType &e);//删除堆H的堆顶结点,并用e将其返回
⭐Status RemoveHeap(Heap &H,int pos,RcdType &e);
//删除位置pos的结点,用e返回其值

6.4.2.基本操作

1.堆的筛选操作

将堆中指定的以pos结点为的子树调整为子堆,其前提是pos结点的左右子树均为子堆

对深度为k的完全二叉树,做一次筛选最多需要进行2(k-1)次比较。n个结点的完全二叉树的深度为⌊log2n⌋+1,因此筛选算法的时间复杂度为O(logn)

Status swapHeapElem(Heap &H, int i, int j) {
    //交换堆H中的第i结点和第j结点
    RcdType t;
    if(i<=0 || i>H.n || j<=0 || j>H.n) return ERROR;
    t = H.rcd[i]; H.rcd[i] = H.rcd[j]; H.rcd[j] = t;
    return OK;
}
void ShiftDown(Heap &H,int pos) {
    int c,rc;
    while(pos<=H.n/2) {//(H.n/2)为最后一个分支结点,若pos结点为叶子结点,循环结束
        c = pos*2;//c为pos结点的左孩子位置
        rc = pos*2 + 1;//c为pos结点的右孩子位置
        if(rc<=H.n && H.prior(H.rcd[rc].key, H.rcd[c].key))
            c = rc;//c为pos结点的左右孩子较优先者的位置
        if(H.prior(H.rcd[pos].key, H.rcd[c].key))
            return;//若pos结点较优先,则筛选结束
        swapHeapElem(H,pos,c);//否则pos和较为优先者c交换位置
        pos = c;//继续向下调整
    }
}
2.堆的插入操作

堆的插入操作是将插入元素加到堆尾,此时需判断堆尾和其双亲结点是否满足堆特性,不满足,则需进行向上调整。

插入操作是从叶子结点向上调整的过程,和筛选操作方向相反,最坏情况下,比较次数为堆的高度减1,因此算法时间复杂度为O(logn)

Status InsertHeap(Heap &H,RcdType e) {
    int curr;
    if(H.n>=H.size-1) return ERROR;//堆已满,插入失败
    curr = ++H.n; H.rcd[curr] = e;//将插入元素加到堆尾
    while(1!=curr && H.prior(H.rcd[curr].key,H.rcd[curr/2].key))
    {
        //1==curr是到顶了的情况
        swapHeapElem(H,curr,curr/2);
        curr/=2;
    }
    return OK;
}
3.删除堆顶结点的操作

删除堆顶结点时,用堆尾结点代替堆顶结点,不影响其左右子堆的特性。但需要对新的堆顶结点重新筛选。

删除后,堆顶元素仍然存在,只是被放在了最后一个位置

主要工作是筛选,时间复杂度为O(logn)

Status RemoveFirstHeap(Heap &H,RcdType &e) {
    if(H.n<=0) return ERROR;
    e = H.rcd[1];//取出堆顶结点
    swapHeapElem(H,1,H.n); H.n--;//交换堆顶与堆尾结点,堆长度减1
    if(H.n>1) ShiftDown(H,1);//从堆顶位置向下筛选
    return OK;
}
4.建堆

由于单个结点的完全二叉树满足堆特性,所以叶子结点都是堆。对n个结点的完全二叉树建堆的过程是,依次将编号为n/2、n/2-1…1的结点为根的子树筛选为子堆。

深度为h的堆中第i层上的结点为2^(k-1),以它们为根的二叉树的深度为h-i+1,筛选算法中进行的关键字比较的次数为2(h-i),则建堆总共进行次数不会超过4n,其时间复杂度为O(n)

void MakeHeap(Heap &H,RcdType *E,int n,int size,int tag,int(*prior)(KeyType,KeyType)) {
    //用E建长度为n的堆H,容量为size,当tag为0或1的时候分别表示小顶堆或大顶堆
    int i;
    H.rcd = E;//E[1,…n]是堆的n个结点,0号单元闲置
    H.n = n; H.size = size; H.tag = tag; H.prior = prior;
    for(i=n/2;i>0;i--) ShiftDown(H,i);//对以i结点为根的子树进行筛选
}

6.4.3.堆排序

  • 堆排序利用堆的特性进行排序。采用大顶堆可以进行升序排序。首先将待排序列建成一个大顶堆,使得堆顶结点最大;将堆顶结点与堆尾结点交换位置,堆长度减一(即最大记录排序到位);然后调整剩余结点为堆,得到次大值结点;重复这一过程,即可得到一个升序序列。 选择类排序、数形排序
  • 堆排序是不稳定的排序方法
  • 堆排序算法,运行时间主要集中在建立初始堆和交换数据元素后的反复筛选上,它们均是通过调用筛选算法实现的
  • 堆排序的最坏时间复杂度为O(nlogn)
  • 堆排序仅需要一个记录供交换结点时使用,空间复杂度为O(1)
void HeapSort(RcdSqList &L) {
    Heap H; int i;
    RcdType e;
    MakeHeap(H,L.rcd,L.length,L.size,1,greatPrior);//待排序列建大顶堆
    for(i=H.n;i>0;i--) RemoveFirstHeap(H,e);//堆顶与堆尾结点交换,堆长度减1,筛选出新的堆顶结点
}

6.5.二叉查找树

6.5.1.二叉查找树的定义

  • 二叉查找树又称二叉排序树,或者是一棵空二叉树,或者是具有如下特性的二叉树:

    (1)若左子树不空,则左子树上所有结点的值均小于根结点的值

    (2)若右子树不空,则右子树上所有结点的值均大于根结点的值

    (3)左、右子树也分别是二叉查找树

  • 二叉查找树中结点的值不允许重复,其中序遍历序列是有序的

  • 如何快速判断一棵树为二叉查找树:中序遍历得到递增序列

  • 类型定义:

typedef struct BSTNode {
    RcdType data;//数据元素
    struct BSTNode *lchild,*rchild;//左右孩子指针
}BSTNode, *BSTree;
  • 基本操作:
Status InitBST(BSTree &T);//构造一棵空的二叉查找树T
Status DestroyBST(BSTree &T);//二叉查找树T存在,销毁树T
❗❗BSTree SearchBST(BSTree T,KeyTpye key);
//若二叉查找树中存在值为key的结点,则返回该结点指针,否则返回NULL
⭐Status InsertBST(BSTree &T,RcdType e);
//若二叉查找树中不存在值为e.key的结点,则插入到T
⭐Status DeleteBST(BSTree &T,KeyType key);
//若二叉查找树中存在值为key的结点,则删除

6.5.2.二叉查找树的查找

  • 递归实现:
BSTree SearchBST(BSTree T,KeyTpye key) {
    if(NULL == T) return NULL;//查找失败
    if(T->data.key == key) return T;//查找成功
    if(T->data.key > key) return SearchBST(T->lchild,key);//在左子树上继续查找
    return SearchBST(T->rchild,key);//在右子树上继续查找
}
  • 非递归实现:
BSTree SearchBST_I(BSTree T,KeyTpye key) {
    while(T!=NULL) {
        if(T->data.key == key) return T;//查找成功
        else if(T->data.key > key) T= T->lchild;//在左子树中继续查找
        else T = T->rchild;//在右子树中继续查找
    }
    return NULL;
}

6.5.3.二叉查找树的插入

  • 为保证二叉查找树中结点的值不重复,需在插入前进行查找,不存在才能插入

  • 思路:若二叉查找树是空树,则创建新插入的结点作为根结点,算法结束;

    若插入结点的值小于根结点值,在左子树递归插入

    若插入结点的值大于根结点值,在右子树递归插入

  • Status InsertBST(BSTree &T,RcdType e) {
        if(NULL == T) {
            BSTNode *s;
            s = (BSTNode *)malloc(sizeof(BSTNode));
            if(NULL==s) return OVERFLOW;
            s->data = e; s->lchild = NULL; s->rchild = NULL;
            ❗❗T = s;
            return TRUE;
        }
        if(e.key<T->data.key) return InsertBST(T->lchild,e);
        //插入结点的值小于根结点的值,在左子树递归插入
        if(e.key>T->data.key) return InsertBST(T->rchild,e);
        //插入结点的值大于根结点的值,在右子树递归插入
        return FALSE;//e.key==T->data.key,结点已存在
    }
    

}




### 6.5.3.二叉查找树的插入

- 为保证二叉查找树中结点的值不重复,需在插入前进行查找,不存在才能插入

- 思路:若二叉查找树是空树,则创建新插入的结点作为根结点,算法结束;

  若插入结点的值小于根结点值,在左子树递归插入

  若插入结点的值大于根结点值,在右子树递归插入

- ```C
  Status InsertBST(BSTree &T,RcdType e) {
      if(NULL == T) {
          BSTNode *s;
          s = (BSTNode *)malloc(sizeof(BSTNode));
          if(NULL==s) return OVERFLOW;
          s->data = e; s->lchild = NULL; s->rchild = NULL;
          ❗❗T = s;
          return TRUE;
      }
      if(e.key<T->data.key) return InsertBST(T->lchild,e);
      //插入结点的值小于根结点的值,在左子树递归插入
      if(e.key>T->data.key) return InsertBST(T->rchild,e);
      //插入结点的值大于根结点的值,在右子树递归插入
      return FALSE;//e.key==T->data.key,结点已存在
  }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值