数据结构与算法第五章 树

度:结点子女个数为该结点的度;树中各结点度最大值树的度

结点层次:根结点在第一层,其子女结点层次等于其层次\(+1\)

结点深度:结点深度即结点层次离根最远结点层次即为树的深度

结点高度:叶结点的高度为1,其双亲结点高度等于自己高度\(+1\)

有序树/无序树,树中结点的各子树有次序/无次序,可以/不可以交换位置。


二叉树

根据先序、中序序列/中序、后序序列确定一棵二叉树

对二叉树,设度为\(i\)结点有\(n_i\)个,则\(n_0=n_2+1\)

二叉树的链表表示(二叉链表,三叉链表)

利用二叉树前序遍历建立二叉树

以递归方式建立二叉树。输入结点值的顺序必须对应二叉树结点前序遍历的顺序,并约定以输入序列中不可能出现的值作为空结点的值以结束递归,

线索化二叉树

通过二叉树遍历,可将二叉树中所有结点的数据排列在一个线性序列中,可以找到某数据在这种排列下它的前驱和后继。

希望不必每次都通过遍历找出这样的线性序列。只要事先做预处理,将某种遍历顺序下的前驱、后继关系记在树的存储结构中,以后就可以高效地找出某结点的前驱、后继。

ltag 和 rtag,指明指针是指示子女还是前驱/后继。后者称为线索。

ltag (rtag) = 0,表示相应指针指示左子女(右子女);ltag (rtag) = 1, 表示相应指针指示前驱(后继)线索。

根据给出的二叉树画出相对应遍历顺序的线索树

\(n\)个结点的线索二叉树上含有的线索数为\(\boxed{n+1}\)


树与森林

树的存储方式

1)广义表表示

2)双亲表示(存data和parent)

3)子女链表表示(无序树情形链表中各结点顺序任意,有序树必须自左向右链接各个子女结点。)

4)子女-指针表示:每一个结点包含的指针个数为树的度,记为\(d\),则空指针数为\(nd-n+1\)

5)子女—兄弟表示(存第一个子女与兄弟)

先根次序遍历(对应子女—兄弟表示二叉树表示前序遍历),后根次序遍历(根最后访问)(对应子女—兄弟表示二叉树表示中序遍历)

广度优先(层次次序)遍历

将一般树化为二叉树表示就是用树的子女-兄弟表示来存储树的结构。森林与二叉树表示的转换可以借助树的二叉树表示来实现。

将森林借助子女—兄弟表示法转化为二叉树,其中根均为兄弟,先转各树,再转森林

森林的两种遍历同树的两种遍历

完全二叉树,且每个节点均小于/均大于其左右孩子

会画直接调整,单次插入,单次删除的流程

将一组用数组存放的任意数据调整成堆的方法:逐个插入,\(O(n\log_2n)\);直接调整,\(O(n)\),选用优的方式,构建堆的时间复杂度为\(O(n)\)

最小堆的每次插入,时间复杂度为\(O(\log_2n)\)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
template <class E>
MinHeap<E>::MinHeap (int sz) 
{
     maxHeapSize = (DefaultSize < sz) ? sz : DefaultSize;
     //三目运算符的解释:wei'zhen
     heap = new E[maxHeapSize];//创建堆空间
     if (heap == NULL) 
     {
          cerr << 堆存储分配失败!” << endl;  exit(1);
     }
     currentSize = 0;//建立当前大小
};
template <class E>
MinHeap<E>::MinHeap (E arr[], int n) 
{
     maxHeapSize = (DefaultSize < n) ? n : DefaultSize;
     heap = new E[maxHeapSize]; 
     if (heap == NULL) 
     {
        cerr << 堆存储分配失败!” << endl;  exit(1);
     }
     for (int i = 0; i < n; i++) heap[i] = arr[i];
     currentSize = n;//复制堆数组, 建立当前大小
     int currentPos = (currentSize-2)/2;    
     //找最初调整位置:最后分支结点
     while (currentPos >= 0) 
     {      //逐步向上扩大堆
           siftDown (currentPos, currentSize-1);
            //局部自上向下下滑调整
           currentPos--;
     }                  
};

//最小堆下滑调整算法
template <class E>
void MinHeap<E>::siftDown (int start, int m ) 
{
//私有函数: 从结点start开始到m为止, 自上向下比较, 
//如果子女的值小于父结点的值,则关键码小的上浮, 
//继续向下层比较, 将一个集合局部调整为最小堆。
//由于堆存储在下标从 0 开始计数的一维数组中,故结点下标不超过n-1
    int i = start,j = 2*i+1;//j是i的左子女位置
    E temp = heap[i];           
    while (j <= m) 
    {//检查是否到最后位置
        if ( j < m && heap[j] > heap[j+1] ) j++;//让j指向两子女中的小者
        if ( temp <= heap[j] ) break;//小则不做调整
        else 
        { 
            heap[i] = heap[j];  
            i = j; 
            j = 2*j+1; 
        }//否则小者上移, i, j下降
     }
     heap[i] = temp;//回放temp中暂存的元素,即将i的值放到它应该要去的位置
};

//最小堆的插入
//每次插入都加在堆的最后,再自下向上执行调整,使之重新形成堆
template <class E>
bool MinHeap<E>::Insert (const E& x )  {
//公共函数: 将x插入到最小堆中
    if ( currentSize == maxHeapSize )
    { 
        cerr << "Heap Full" << endl;
        return false; 
    }
    heap[currentSize] = x;//插入
    siftUp (currentSize);//向上调整
    currentSize++;//堆计数加1,此处借当前插入个数与末尾下标玩了个花招
    return true;
};
template <class E>
void MinHeap<E>::siftUp (int start) 
{
//私有函数: 从结点start开始到结点0为止, 自下向上
//比较, 如果子女的值小于父结点的值, 则相互交换, 
//这样将集合重新调整为最小堆。关键码比较符<=
//在E中定义。
    int j = start,i = (j-1)/2;//j表示起始结点位置,i表示其父亲位置 
    E temp = heap[j];//这一句是什么意思?也就是这个j的值应该要去的位置。
    while (j > 0) //j=0则不用向上调整了。
    {//沿父结点路径向上直达根
        if (heap[i] <= temp) break;//父结点值小, 不调整
        else 
        { 
            heap[j] = heap[i];//这里的赋值只是换掉原先的位置,对于最终位置的赋值没有考虑
            j = i;  
            i = (i-1)/2; 
        }
        //父结点结点值大, 调整
    }   
    heap[j] = temp;//回送
};

//最小堆的删除(注意维持堆的完全二叉树的形态)只能删除头节点(也就是优先队列队头)
template <class E>
bool MinHeap<E>::Remove (E& x) 
{
    if ( !currentSize ) 
    {
        cout << "Heap empty" << endl;  
        return false;
    }
    x = heap[0];  
    heap[0] = heap[currentSize-1];
    currentSize--;
    siftDown(0, currentSize-1);//自上向下调整为堆
    return true;            //返回最小元素
};

Huffman树

路径长度(PL):连接两结点路径上分支构成路径,长度为分支数。

外部路径长度(\(EPL\)):各叶结点(外结点)到根结点的路径长度之和。

内部路径长度(\(IPL\)):各非叶结点(内结点)到根结点的路径长度之和。

树的路径长度为\(EPL+IPL\)

\(n\)个节点的二叉树路径数不小于下述数列前\(n\)项和:$\displaystyle \sum_{i=1}^{n} floor( \log_2i) \(,\)\displaystyle \sum_{i=1}^{n} \lfloor \log_2 i \rfloor$,完全二叉树满足此要求。

带权路径长度,权\(w_i\)外结点所带,其到根的路径长度为\(l_i\),树的带权路径长度为\(\displaystyle WPL=\sum_{i=1}^{n}w_i*l_i\)

注意:带权路径长度的计算只考虑外结点

带权路径长度达到最小的扩充二叉树即为Huffman树。在Huffman树中,权值大的结点离根最近。

Huffman编码,实现数据压缩。先进行Huffman树构建,再进行分支编码。而且此种编码是一种前缀编码,即任一个二进制编码不是其他二进制编码的前缀,解码时不会混淆,例如A=0,T=00,来了00是解码成T还是AA呢?