数据结构 - 二叉树 - 笔记

24 篇文章 0 订阅

目录

1.树的定义、实现与基本操作

1.1 有关二叉树的重要定义

  • 路径 - 长度 — 祖先 - 子孙
  • 深度:从根结点到该结点的路径长度
  • 树的高度=最深节点的深度+1
  • 结点的层数=深度 (根结点结点层数为0,深度为0)
  • 叶节点、内部节点(/分支节点)
  • (定义有争议) 满二叉树:结点若是分支结点,定有两个非空子结点
  • (定义无争议) 完全二叉树:从根节点到每一层从左到右填充。即:二叉树的高度为d,除第 d-1层外,其它各层都是满的,第 d-1 层所有的结点都连续集中在最左边

※满二叉树定理

  1. 对非空满二叉树,其叶结点数=分支结点数+1
  2. 引理:对非空二叉树,其空子树的数目=结点数目+1

1.2 二叉树的ADT和两种实现方式

ADT:

template<typename E> class BinNode {
public:
    virtual ~BinNode() {}
    virtual E& element() = 0;
    virtual void setElement(const E&) = 0;
    virtual BinNode* left() const = 0;
    virtual void setLeft(const E&) = 0;
    virtual BinNode* right() const = 0;
    virtual void setRight(const E&) = 0;
    virtual bool isLeaf() = 0;
1.使用指针实现二叉树
  • 包含一个数据区和两个指向子节点的指针
  • 很大比例的空间被结构性开销占据
template<typename Key,typename E>
class BSTNode:public BinNode<E> {
private:
    Key k;	//为实现二叉检索树:存储关键码
    E it;
    BinNode *rc;
    BinNode *lc;
public:
    BSTNode() {lc=rc=NULL;}
    BSTNode(Key K,E e,BSTNode *l=NULL,BSTNode *r=NULL) {
        k=K; it=2; lc=l; rc=r;
    }
    E& element() {
        return it;
    }
    void setElement(const E& e) {
        it=e;
    }
    inline BinNode* left() const {
        return lc;
    }
    void setLeft(BinNode<E> *b) {
        lc=(BTSNode*)b;
    }
    BinNode* right() const {
        return rc;
    }
    void setRight(BinNode<E> *b) {
        rc=(BTSNode*)rc;
    }
    bool isLeaf() {
        return (lc==NULL)&&(rc==NULL);
    }
};

分支结点和叶结点是否使用相同的类来定义十分重要:为节省存储空间
方法: 给BinNode定义一个基类VarBinNode,再由该基类的子类来具体区分是分支结点IntlNode还是叶结点LeafNode
即:

使用复合设计模式
  • 使用一个虚基类和两个独立的结点类
  • 每个子类中对其实现可按照自己的需求进行

1.基类VarBinNode

class VarBinNode {
public:
    virtual ~VarBinNode() {}
    virtual bool isLeaf() = 0;
};

2.叶结点LeafNode

template<typename E>
class LeafNode:public VarBinNode {
private:
    E leafElmt;
public:
    LeafNode(E leafE) {
        leafElmt=leafE;
    }
    bool isLeaf() { return true; }
    E value() { return leafElmt; }
};

3.分支结点IntlNode

template<typename E>
class IntlNode:public VarBinNode {
private:
    //不知道左/右子结点是叶子还是中间节点
    //只能先定义基类结点的指针,最后再强制类型转换
    VarBinNode* left;
    VarBinNode* right;
    E inElmt;
public:
    IntlNode(const E inE,VarBinNode* l,VarBinNode* r) {
        inElmt=inE;
        left=l;
        right=r;
    }
    bool isLeaf() { return false; }
    VarBinNode* leftchild() { return left; }
    VarBinNode* rightchild() { return right; }

    void traverse(VarBinNode *root) {   //找到root树下的所有叶结点
        if(root==NULL) return;
        if(root->isLeaf()) {
            cout<<"Leaf:"<<((LeafNode *)root)->value() << endl;
        }
        traverse(((IntlNode*)root)->leftchild());
        traverse(((IntlNode*)root)->rightchild());
    }
};

※:另一种traverse写法: 在基类中定义traverse函数,子类自己具有traverse函数各自的实现方式(书p105)

1.2 使用数组实现完全二叉树
  • 简单紧凑地实现完全二叉树
  • 亲属结点的下标公式:(位置从数组的0号位开始)
    • Parent: (r-1)/2,当r≠0时 ——>(则:若有n个结点,叶结点的下标为(n-1)/2-1~n)
    • Leftchild: 2r+1(<n)
    • Rightchild: 2r+2(<n)
    • Leftsibling: r-1,r为偶数时
    • Rightsibling: r+1,r为奇数且r+1<n时

1.3 二叉树的遍历

  • 枚举:对每个结点都进行一次访问并将其列出
  • 遍历:按一定顺序访问二叉树的结点

1. 前序遍历
先访问结点,再访问子结点

递归实现:

template<typename E>
void preorder(BinNode<E> *root) {
    if(root==NULL) {
        return;
    }
    visit(root);	//对当前访问的结点进行的操作
    preorder(root->left());
    preorder(root->right());
}

栈实现:

在这里插入代码片

2.中序遍历
先访问左子结点(以及整棵左子树),再访问该节点,最后访问右子结点(以及整棵右子树)

※二叉检索树使用的遍历方法

递归实现:

template<typename E>
void inorder(BinNode<E>* root) {
    if(root==NULL) return;
    inorder(root->left());
    visit(root);	//与前序遍历visit位置不同
    midorder(root->right());
}

3.后序遍历
先访问子结点,在访问该结点
应用如:释放树中所有结点所占用的空间
递归实现:

template<typename E>
void postorder(BinNode<E>* root) {
    if(root==NULL) return;
    inorder(root->left());
    postorder(root->right());
    visit(root);	//三种遍历都是通过visit所在位置不同实现的
}

栈实现:

在这里插入代码片

4.层序遍历
队列实现

#include<queue>
using namespace std;
template<typename E>
void levelorder(BinNode<E>* root) {
    queue q;
    BinNode<E>* b;
    q.push(root);
    while(!q.empty()) {
        b=q.pop();
        visit(b);
        if(b->left()!=NULl) q.push(b->left());
        if(b->right()!=NULL) q.push(b->right());
    }
}

2.二叉检索树BST

  • 又称“二叉查找树”、“二叉排序树”
  • 使记录的插入和检索都能很快完成(继承字典的结构,时间复杂度:log(n))
  • 根结点开始,在BST中检索K值:十分有效->仅需检索两棵子树之一; 全过程直到K被找到或者遇到叶子结点(若仍没发现K,则K不在此BST树中)

2.1 BST的性质

  1. 对其中任意一个结点,其左子树中任意一个结点的值都小于该结点,右子树中任意一个结点的值都大于等于该结点
  2. 按中序遍历打印各节点,结果由小到大排列(由性质1容易得出)
  3. 将关键码和值存在树的结点中

2.1 使用字典实现BST

3. 堆(heap)与优先队列

  • 堆:又名存储池

3.1 堆的性质

  1. 堆是一棵完全二叉树(往往拿数组实现)
  2. 堆中存储的数据局部有序:结点存储的值与其子结点存储的值间有某些联系
    • 最大堆:任意一个结点存储的值都大于等于其任意一个子结点存储的值
      • 根节点存储了该数中的最大值
      • 实现堆排序
    • 最小堆:小于等于
      • 实现置换选择算法
  3. 兄弟结点间没有必然联系(※凭此区分BST和堆),堆只实现了局部排序

3.2 最大堆的实现

  1. insert::首先将要插入的结点置于堆的末尾位置n,再与其父节点比较,若大于父节点就与父节点交换位置,直到小于等于父节点,则达到正确位置
  2. void siftdown(int pos):从当前位置pos向下层进行重排列(非叶子结点的所有节点向下交换的重排列)
    关键:认为root下的左右子树都已经是堆,选择子树中根节点大的那个交换位置,然后递归到最后(?这个有问题,没解释清楚)
    1. 将当前位置和值最大的结点比较,若是最大子节点大于当前位置值则交换位置
    2. 重复步骤1,直到当前位置结点在树中找到了恰当位置
    ※:siftup:从当前位置pos向上层进行重排列(非根节点到最后一个叶子节点向上交换的重排列)
  3. removefirst:移去根节点后,令最后一个叶子节点做根节点,若此时堆中不止一个结点,就利用siftdown(0)将根放入正确的位置,实现重排序
  4. remove
#include<assert.h>
#include<iostream>
#define Assert(a,b) assert((a)&&(b))
using namespace std;
template<typename E> class heap {
private:
    E* Heap;
    int maxsize;
    int n;
	void siftup(int pos) {
        while(!isTop(pos)) {
            int j=parent(pos);
            if(Heap[pos]>Heap[j])   return;
            swapE(pos,j);
            pos=j;
        }
    }
    void siftdown(int pos) {
        while(!isLeaf(pos)) {
            int j=leftChild(pos);
            int rc=rightChild(pos);
            if(rc<n && Heap[j]<Heap[rc])    j=rc;
            if(Heap[pos]>Heap[j])   return;
            swapE(pos,j);
            pos=j;
        }
    }

    void swapE(int p1,int p2) {
        E temp=Heap[p1];
        Heap[p1]=Heap[p2];
        Heap[p2]=temp;
    }

public:
    heap(E* h,int num,int max) {
        Heap=h; n=num; maxsize=max;buildHeap();
    }
    int size() const {
        return n;
    }
    bool isLeaf(int pos) const {
        return (pos>=n/2)&&(pos<n);
    }
    int leftChild(int pos) const{
        return 2*pos+1;
    }
    int rightChild(int pos) const{
        return 2*pos+2;
    }
    int parent(int pos) const {
        return (pos-1)/2;
    }
    void buildHeap() {
//        for(int i=1; i<n; i++) {
//				siftup(i);}
        for(int i=n/2-1; i>=0; i--) siftdown(i);	//从第一个非叶节点开始
    }

    void insert(const E& it) {
        Assert(n<maxsize,"Heap is full");
        int curr=n++;
        Heap[curr]=it;
        while((curr!=0)&&(Heap[curr]>Heap[parent(curr)])) {
            swapE(curr,parent(curr));
        }
    }

    E removefirst() {
        Assert(n>0,"Heap is empty");
        E it=Heap[0];
        Heap[0]=Heap[--n];
        if(n!=0)    siftdown(0);
        return it;
    }
	
	//与parent交换到合适的位置之后,siftdown
    E remove(int pos) {
        Assert(n>0,"Heap is empty");
        E it=Heap[pos];
        if(pos==n-1) n--;
        else {
            Heap[pos]=Heap[--n];
            while((pos!=0)&&(Heap(pos)>Heap(parent(pos)))) {
                swapE(pos,parent(pos));
                pos=parent(pos);
            }
            if(n!=0)    siftdown(pos);
        }
        return it;
    }
    void printHeap() const {
        for(int i=0;i<n;i++) {
            cout<<Heap[i]<<"  ";
        }
        cout<<endl;
    }
    void heapsort() const {
        heap mh(Heap,n,maxsize);
        for(int i=0;i<n;i++) {
            Rational tmp=mh.removefirst();
            cout<<tmp<<"  ";
        }
        cout<<endl;
    }
};

4. Huffman编码树

  • 固定长度编码方法:ASCII码
    变长编码:Huffman编码
  • Huffman树:满二叉树
  • 具有最小外部路径权重的二叉树:加权路径之和最小(权重大的叶结点深度小)

4.1 Huffman编码树的建立过程

1.创建n个初始Huffman树(只包含单一的、记录了对应字母的叶结点) -> 放入一个森林中
2.将n棵树按权重由小到大排成一列
3.取出前两棵树,将其标记为同一结点的叶子,从而获得一颗权重是这两棵树权重之和的新树
4.按照该新树的权重,将其插回序列中
5.重复2~4,直到序列中只剩下一个元素
(exp:P120 例5.8)

4.1 Huffman编码

  • 0对应左节点,1对应右节点
  • 信息反编码
  • 前缀特性
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值