1. 概述

定义和性质

二叉树是n个有限元素(结点,nodes)的集合,该集合要么为空,要么由一个根元素**(root)**及两个不相交的左,右子树(同为二叉树)组成,是有序树.
image.png

  • 高度&深度

每个二叉树有以下属性
Ch5 二叉树 - 图2

  • 满&完全

根据二叉树形态可以分为
Ch5 二叉树 - 图3
image.png

  • 结构性开销

Ch5 二叉树 - 图5

有关定理

Ch5 二叉树 - 图6


2. 遍历

根据树/子树根结点被访问的顺序可以分为三种二叉树遍历方法:以图中二叉树为例,三种对应遍历序列
image.png
Ch5 二叉树 - 图8

:::info 讨论已知前/中/后序遍历结果中的几个可确定唯一二叉树

  • 已知(前+中)或(后+中):可确定唯一二叉树,因为可确定根结点与子树
  • 已知(前+后):不可确定唯一二叉树
    :::

3. 数组实现CBT

Ch5 二叉树 - 图9
image.png


4. 二叉检索树(BST)

对任意一个结点,其左子树任意结点值都<k,右子树任意结点值≥k

| 当我们要检索一个值k,从根结点开始
- root值=k,检索jies
- root值>k,进入左子树检索
- root值 👉右图,若要检索32:
- 37>32进入左子树
- 24<32进入右子树
- 32=32检索成功
| **image.png | | —- | :—-: |

实现

BST.java

方法详解

  • findhelp

检索,对比当前结点key和检索值k,再深入左右子树
时间开销:Θ(log**2n)~Θ(**n),取决于平衡性(左右子树规模)

  1. private E findhelp(BSTNode<Key, E> rt, Key k)
  2. {
  3. if (rt == null)
  4. return null;
  5. if (rt.key().compareTo(k) > 0)
  6. return findhelp(rt.left(), k);
  7. else if (rt.key().compareTo(k) == 0)
  8. return rt.element();
  9. else
  10. return findhelp(rt.right(), k);
  11. }
  • inserthelp

插入新结点,类似检索过程,找到合适的位置后插入为新的叶结点
时间开销:Θ(log**2n)~Θ(**n),取决于平衡性

  1. private BSTNode<Key, E> inserthelp(BSTNode<Key, E> rt, Key k, E e)
  2. {
  3. if (rt == null)
  4. return new BSTNode<Key, E>(k, e);
  5. if (rt.key().compareTo(k) > 0)
  6. rt.setLeft(inserthelp(rt.left(), k, e));
  7. else
  8. rt.setRight(inserthelp(rt.right(), k, e));
  9. return rt;
  10. }
  • deletemin/getmin

删除/获取当前树中最小的key,即一路沿左侧枝杈找到末端叶结点

  1. private BSTNode<Key, E> deletemin(BSTNode<Key, E> rt)
  2. {
  3. if (rt.left() == null)
  4. return rt.right();
  5. rt.setLeft(deletemin(rt.left()));
  6. return rt;
  7. }
  8. private BSTNode<Key, E> getmin(BSTNode<Key, E> rt)
  9. {
  10. if (rt.left() == null)
  11. return rt;
  12. return getmin(rt.left());
  13. }
  • removehelp

删除指定的X结点,分情况(见下图):

  1. X只有LC,则:X.LC变成X.parent的LC
  2. X只有RC,则:X.RC变成X.parent的RC
  3. X兼具LC,RC,则:X右子树最小值变成X.parent

BST-remove.png

  1. private BSTNode<Key, E> removehelp(BSTNode<Key, E> rt, Key k)
  2. {
  3. if (rt == null)
  4. return null;
  5. if (rt.key().compareTo(k) > 0)
  6. rt.setLeft(removehelp(rt.left(), k));
  7. else if (rt.key().compareTo(k) < 0)
  8. rt.setRight(removehelp(rt.right(), k));
  9. else
  10. { // Found it
  11. if (rt.left() == null)
  12. return rt.right();
  13. else if (rt.right() == null)
  14. return rt.left();
  15. else
  16. { // Two children
  17. BSTNode<Key, E> temp = getmin(rt.right());
  18. rt.setElement(temp.element());
  19. rt.setKey(temp.key());
  20. rt.setRight(deletemin(rt.right()));
  21. }
  22. }
  23. return rt;
  24. }
  • printhelp

实现前中后序遍历结果打印,以下为中序遍历结果打印,调整语句顺序可改变打印顺序(*BST**中序遍历结果即从小到大排序)
时间开销:**Θ(**n),需要遍历n个结点

  1. private void printhelp(BSTNode<Key,E> rt)
  2. {
  3. if (rt == null) return;
  4. printhelp(rt.left());
  5. printVisit(rt.element());
  6. printhelp(rt.right());
  7. }

5. 堆(heap)

堆是由数组实现的完全二叉树,同时必须是部分有序的(partially ordered),并由此分为

  • 最大堆max heap:任意结点的值都大于等于其子节点值
  • 最小堆min heap:任意结点的值都小于等于其子节点值

    实现

    MaxHeap.java

    方法详解

  • buildheap

给定任意长度n的原始数组,从下到上建立最大堆:
从下到上**建堆可以忽略最后一层的叶结点,Ch5 二叉树 - 图13可确定堆倒数第二层开始的下标,从右到左依次siftdown,下沉到合适位置,然后是倒数第三层…最终到顶层元素下沉,执行完毕可得到一个最大堆
时间开销:Θ(**n)

  1. public void buildheap()
  2. {
  3. for (int i = n / 2 - 1; i >= 0; i--)
  4. siftdown(i);
  5. }
  • siftdown

下沉Heap[pos]到堆合适的位置
给定pos,**找出其两个子结点中较大的**;对比Heap[pos]和Heap[j],大的作为父结点
时间开销:Θ(log**2**n)

  1. /** Put element in its correct place */
  2. private void siftdown(int pos)
  3. {
  4. assert(pos >= 0) && (pos < n) : "Illegal heap position";
  5. while (!isLeaf(pos))
  6. {
  7. int j = leftchild(pos);
  8. if ((j < (n - 1)) && (Heap[j].compareTo(Heap[j + 1]) < 0))
  9. j++; // j is now index of child with greater value
  10. if (Heap[pos].compareTo(Heap[j]) >= 0)
  11. return;
  12. DSutil.swap(Heap, pos, j);
  13. pos = j; // Move down
  14. }
  15. }

:::info 举例:建堆
heap.png :::

  • insert

插入新元素进堆
首先插入数组末端,然后与parent的值对比,上浮到合适位置
时间开销:Θ(log**2**n)

  1. /** Insert val into heap */
  2. public void insert(E val)
  3. {
  4. assert n < size : "Heap is full";
  5. int curr = n++;
  6. Heap[curr] = val; // Start at end of heap
  7. // Now sift up until curr’s parent’s key > curr’s key
  8. while ((curr != 0) &&
  9. (Heap[curr].compareTo(Heap[parent(curr)]) > 0))
  10. {
  11. DSutil.swap(Heap, curr, parent(curr));
  12. curr = parent(curr);
  13. }
  14. }
  • removemax

移除堆顶(最大值)
实现时只需要交换堆顶/首元素和数组末尾元素,**同时—n,避免换到末尾的元素被访问,之后新堆顶重新下沉
时间开销:Θ(log2n)**

  1. public E removemax()
  2. {
  3. assert n > 0 : "Removing from empty heap";
  4. DSutil.swap(Heap, 0, --n); // Swap maximum with last value
  5. if (n != 0) // Not on last element
  6. siftdown(0); // Put new heap root val in correct place
  7. return Heap[n];
  8. }
  9. /** Remove and return element at specified position */
  • remove

移除指定位置元素
实现时只需要交换Heap[pos]元素和数组末尾元素**同时—n,避免换到末尾的元素被访问**Heap[pos]处新元素先上浮,再下沉
时间开销:Θ(log**2**n)

  1. public E remove(int pos)
  2. {
  3. assert(pos >= 0) && (pos < n) : "Illegal heap position";
  4. if (pos == (n - 1))
  5. n--; // Last element, no work to be done
  6. else
  7. {
  8. DSutil.swap(Heap, pos, --n); // Swap with last value
  9. // If we just swapped in a big value, push it up
  10. while ((pos > 0) &&
  11. (Heap[pos].compareTo(Heap[parent(pos)]) > 0))
  12. {
  13. DSutil.swap(Heap, pos, parent(pos));
  14. pos = parent(pos);
  15. }
  16. if (n != 0)
  17. siftdown(pos); // If it is little, push down
  18. }
  19. return Heap[n];
  20. }

:::info

  • 逻辑上建成的偏序Heap和数组元素的排序无直接关系,最大堆不代表数组从大到小排列
  • 对最大堆不断执行remove()后返回的所有元素依次排列为数组元素从大到小顺序 :::

6. 哈夫曼树(Huffman Tree)

给定文本,可以确定其中每个字母出现的频度,将频度作为权值,以这些”权值+字符”作为二叉树的叶子结点,构造一棵二叉树.每个叶子结点对应一个加权路径长度(WPL):Ch5 二叉树 - 图15,若该树的加权路径长度达到最小**,**称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree),由定义知权值较大的结点离根较近,权值低的结点离根远. :::danger

建树过程:不断执行建立最小堆过程,每次结束后执行两次removemin(),取出权重最小两树,合并,然后重新insert()入堆,再次取出权重最小两树…直到堆中只剩一个元素,即为哈夫曼树
::: :::info 应用:为哈夫曼树的枝杈标记01后即可把字符转换为二进制编码,则高频字符编码短,低频字符编码长,这样可以节省文本存储空间
例题:已知文本中字符和频度:image.png,建立哈夫曼树并求所有字符的哈夫曼编码与每个字符预期存储长度
image.png
Ch5 二叉树 - 图18
::: **