数据结构笔记-树

相关

  • 静态查找 :集合是固定的,没有插入和删除

  • 动态查找:集合是动态的,可能发生插入和删除

  • 二分查找

    • 将复杂度降为 log ⁡ 2 N \log_2N log2N,相当于将数组变为树。
    • 前提:数组是排好序的

定义

树: n ( n ≥ 0 ) n (n\geq0) n(n0)各节点构成的有限集合
n=0时,是空树。没有节点也是树

  • 非根节点可分为m个不相交的有限集合,每个集合又是一棵树,成为原树的子树。
  • 除了根节点,每个节点仅有一个父节点
  • n个节点有n-1条边
  • 节点的度:子树的个数
  • 树的度:所有节点中最大的度数
  • 路径和路径长度:从节点 n 1 n_1 n1 n k n_k nk的路径为一个节点序列 n 1 , n 2 , . . . n k n_1,n_2,...n_k n1,n2,...nk n i n_i ni n i + 1 n_{i+1} ni+1的父节点。路径所包含的边数为路径长度
  • 节点的层次,根节点在1层。
  • 树的深度:所有节点中最大层次

二叉树表示

儿子兄弟法可以将所有树变为二叉树。

二叉树的五种基本形态

  • 空树
  • 只有根节点
  • 只有左孩子
  • 只有右孩子
  • 有左,右孩子

几种特殊的二叉树

  • 斜二叉树(skewed Binary tree)。即链表,所有节点只有一个孩子
  • 完美二叉树(perfect Binary tree) 或 满二叉树(full binary tree) .每层节点数量达到最大
  • 完全二叉树(complete binary tree) 对节点从上到下,从左到右进行编号,与满二叉树编号相同

二叉树的性质

  • 第i层最大节点数为 2 i − 1 , i ≥ 1 2^{i-1},i\geq1 2i1,i1
  • 深度为k的二叉树最多有 2 i − 1 2^i-1 2i1节点
  • 对于非空二叉树, n 0 n_0 n0为叶子节点数, n 2 n_2 n2是度为二的非叶子节点个数,那么满足 n 0 = n 2 + 1 n_0 =n_2 +1 n0=n2+1

存储

完全二叉树

  • 顺序存储 i = index+1
  • 节点为i,左孩子为2I,右孩子为2i+1
  • 节点为i,父节点为 i/2 (向下取整)

一般二叉树

  • 链表存储,用数组会造成空间浪费

树的遍历

树有两大类遍历方法,深度优先和广度优先。对于二叉树来说,深度优先又分为先序,中序,后序三种遍历方式。如果从根开始遍历,最后回到根,每个节点会被遍历三次,对应前序,中序,后序。深度优先的遍历线路时固定的,只是什么时候遍历该节点是不确定的。而先中后三种是针对根节点访问顺序而言的。
对于一个 n n n叉树来说,深度优先遍历时,每个节点会被遍历 n + 1 n+1 n+1次。
在这里插入图片描述 F i g u r e 1 深 度 优 先 遍 历 路 径 Figure 1 深度优先遍历路径 Figure1

递归遍历(针对深度优先遍历)

void preOrder(vector v ,int root){
  if(root<v.size()){
       visit(v[root]);
       preOrder(vector,root*2);
       preOrder(vector v ,root*2+1);       
  }
}

该顺序是先遍历根节点,再遍历左子树,最后右子树。从根到各个叶子是从左到右的顺序遍历。若要先遍历右侧路径,换一个位置即可.对于递归遍历,只需要调节三条语句的顺序,即可实现三种遍历。

非递归遍历

因为递归和深度遍历都使用了栈,所以可以通过递归来实现深度优先遍历。如果使用非递归实现深度优先遍历,只需要配合一个栈即可。

  • 中序遍历
Tree t;
stack s;
while(t || !s.empty()){
  while(t){
     s.push(t);
     t = t.left;
  }
  if(!s.empty()){
     t = s.top();
     s.pop();
     visit(t);
     t = t.right;
  }
}
  • 先序遍历
Tree t;
stack s;
while(t || !s.empty()){
  while(t){
     s.push(t);
      visit(t); //在第一次遇到就进行遍历
     t = t.left;
  }
  if(!s.empty()){
    t = s.top();
    s.pop();
    // visit(t); 将此处遍历上移
    t = t.right;
  }
}
  • 后序遍历
stack s;
tree t ,last = null;
while(t || ! s.empty()){
  while(t){
    s.push(t);
    t = t.left;
  }
  
  while(!s.empty()){
      if(!s.top.right ||(! last && s.top.right == last) || ! last ){
           t = s.top();
           s.pop();
           last = t;
           visit(t);
      }else{
         t = s.top().right;
         break;
      }
  }
  
}

层次遍历,广度优先遍历

广度优先遍历需要queue。从队列中取出一个元素并访问,将其子节点都压入队列。

queue q;
Tree t;
q.push(t);
while(!q.empty()){
   t = q.front();
   q.pop();
   visit(t);
   if(t.left)
      q.push(t.left);
   if(t.right)
      q.push(t.right);
}

应用

深度优先遍历中,通过两种遍历顺序推测一个二叉树树,必须含有中序遍历。没有中序遍历的话,无法判断左右子树。除非是真二叉树(没有度为1的节点,都是2或0)可以还原。
先序或后序遍历中找出根节点,在中序遍历中利用根节点将树分为左右两个。

Tree build(vector inorder ,vector post){
    Tree root = post[post.size()-1];
    int root_index = find(inorder ,root);
    vector  l_inoder = copy(inorder,0,root_index-1);
    vector l_post = copy(post,0,root_index-1);
    vector r_inorder = copy(inoder,root_index+1,inorder.size()-1);
    vector r_post = copy(post,root_index,post.size()-2);
    root ->left(l_inorder,l_post);
    root ->right (r_inorder,r_post);
    return root;
}

二叉搜索树(BST)

  • 又叫二叉排序/查找树
  • 对于非空的搜索树:
    • 非空左子树的所有值小于根节点的值
    • 右子树的所有值大于根节点的值
    • 左右子树都是二叉搜索树
    • 最大元素在最右端
    • 最小元素在最左端
  • 二叉搜索树的查找效率取决于树的高度

操作

  • 插入 ,类似查找,一定插入的是叶子节点
  • 删除
    • 删除叶子
      • 直接删除
    • 删除只有一个孩子的节点
      • 孩子和爷爷相连
    • 删除两个孩子的节点
      • 用右子树最小节点或左子树最大节点代替。需要将那条路径上所有节点进行移动,直到叶子节点。

平衡二叉树 AVL

是个查找树
平衡因子(balance factor ,BF) BF(T)= h L − h R h_L-h_R hLhR, h L h_L hL h R h_R hR分别是左右子树的高度

AVL定义

  • 空树或者任意节点的 ∣ B F ∣ |BF| BF不超过1

AVL调整

  • 插入节点在不平衡节点的左子树的左边
    在这里插入图片描述
  • 插入节点是不平衡节点的左子树的右侧
    在这里插入图片描述
  • 插入节点后,可能不需要调整,但BF可能需要更新

堆 heap

优先级队列

  • 取出元素的顺序是按优先级顺序
  • 可以用数组,链表,有序数组,查找树等实现。

如果用查找树实现,插入删除都比较省时间,但一直删除会导致树倾斜。

堆 定义

  • 优先级队列的完全二叉树表示

特性

  • 结构性,用数组表示完全二叉树
  • 有序性 ,任意节点都是其子树的最大值或最小值

每条路径是有序的

操作

全部是大根堆

  • 建堆
    • 插入 ,时间复杂度较大,T(n) = O(nlogn)
vector build(vector v){
  vector heap;
  for(i=0;i<v.size();i++){
      insert(heap,v[i]);
  }
   return heap;
}
  • 调整。类似递归,叶子一定是个堆,从最后一个非叶子节点开始调整,使之成堆。最坏只需移动书中所有节点高度之和 T(n)= O(n)
vector build(vector v){
   for(i=(v.size()-1)/2 ;i>0;i--){
      tem = v[i];
       for(p=i;p*2<v.size();p=c){
           c = p*2;
           if(p*2+1<v.size() && v[p*2+1]>v[p*2]){
                v[p] = v[c];
           }
       }
        v[p] = tem;
   }
}
  • 插入
    • 将新节点插入数组末尾,保证插入后满足二叉树的结构。再调整,判断该节点是否大于其父节点,若大于则交换,直到不大于父节点或到达根节点。
    • 实现中,可以再数组下标为0的元素中设置哨兵,远大于堆中其他元素;在与父节点交换值时,可以只改变子节点位置处的值,不修改父节点处的值,直到停止交换才修改
insert( vector heap ,int node){
   heap.push_back(node);// 将元素插入末尾
   for(i= heap.size()-1; heap[i] >heap[i/2]; i= i/2){ // 设有哨兵,保证不会越界 heap[0]>>heap[i] (i!=0)
        heap[i] = heap[i/2]; // 将子节点处的值进行修改
   }
   heap[i] = node;
}
// T(N) = O(logN)
  • 删除,返回最大值
    • 要保证二叉树的结构完整,只能删除最后一个元素。故记录最大元素,作为返回值;将最后一个元素放在根节点上,与其最大的孩子进行交换,向下移动至合适的位置。
int del(vector heap){
  int max = heap[1];
  int item = heap[heap.size()-1];
  int size = heap.size()-1;
  for(parent = 1; parent *2 < size ;p = c){
    c = p*2;
    if(p*2+1 <size && heap[p*2+1]>heap[p*2]){
        c = p*2+1;
    }
    if(heap[p]<heap[c]){
       heap[p] = heap[c];
    }else{
      beak;
    }
  }
  heap[p] = item;
  return max;
}

哈夫曼树(Huffman Tree)与哈夫曼编码

哈夫曼树是根据节点不同的查找频率构建的搜索树

哈夫曼树定义

带权路径长度(WPL):设二叉树有n个叶子节点,每个叶子节点带有权值 w k w_k wk,从根节点到每个叶子节点的长度为 I k I_k Ik,则每个叶子节点的带权路径长度之和就是 W P L = ∑ k = 1 n w k l k WPL=\sum^n_{k=1}w_kl_k WPL=k=1nwklk

最优二叉树或哈弗曼树:WPL最小的二叉树

哈夫曼树操作

  • 构建
    • 将权重最小的两个进行合并,生成新的节点
    • 每个子树都是哈夫曼树
Tree build(minHeap heap){
  Node n1,n2,n3;
  for(i=0;i<heap.szie();i++){
     n1 = heap.del();
     n2 = heap.del();
     n3 = newNode(n1,n2);
     heap.insert(n3);  
  }
}
  • 特点
    • 没有度为1的节点
    • n个叶子节点的哈弗曼树共有2n-1个节点
    • 左右子树交换后还是哈弗曼树
    • 一组权值可能对应多个不同构的哈弗曼树
      • 当出现节点值相同时,无论是叶子节点还是非叶子节点,权值相同时,取点顺序不同会导致树不同

哈夫曼编码

不等长编码,出现的频率高,编码短;频率的,编码长。
不等长编码要处理二义性。

  • 前缀码 (prefix code):任何字符的编码都不是另一个字符的前缀,可进行无二意的解码
    • 要实现,所有编码必须在叶子上

用二叉树进行编码

  • 左孩子为0 ,右孩子为1
  • 字符在叶子上。(保证无前缀码)

用字符组建一棵哈夫曼树,左0右1,得到编码

判断哈夫曼编码时

  • 是否是最优的编码
  • 字符都在叶子上
  • 不需要判断有没有度为1 的节点。反证:如果有,则不是最优编码,某个节点只有一个孩子,那该节点不存字符,将该节点的子节点与该节点的父节点相连,没有影响其他节点,且叶子节点编码长度缩短一位,即原来不是最优编码,矛盾。

并查集-集合的表示和运算

主要用于合并集合,查找元素所属的集合
每个节点除了自身的数据外,还需要保存期父亲节点。
根的父亲节点使用-1或自身等特殊标记。
为了标记每个集合元素数量,可以在跟的父节点中记录集合元素数量的相反数,既表明了是根节点,又记录集合中的数量元素。

操作

可以对并查集进行化简,每个节点的父亲节点是数组中的值,自身的数据用数组下标表示。如果自身数据到数组下标这个转化较复杂,可以单独写一个映射。

  • 初始化
for(i=0;i<n;i++)
   set[i] = -1; // 父节点且集合中只有一个节点
  • 查找
int find(int item ,vector set){
  int a = item,t;
  while(set[item] >0){
      item = set[item];
  }
  // 路径压缩
   while(a != item){
      t = set[a];
      set[a] = item;
      a = t;
  }
   retrun item;
}
  • 合并
  • 最简单是直接合并,但会导致树不平衡,
  • 按秩归并
    • 将小集合并入大集合中
void  union (int a ,int b,vector set){
  int x,y;
  x = find(a,set);
  y = find(b,set);
  if(x != y){
  // 按秩归并
  if(set[x] >set[y])
      set[y]+=set[x];
     set[x] = y;
  }else{
     set[x] +=set[y];
     set[y] =x;
  }
}

code技巧

  1. 建树时如何判断根节点。遍历所有节点,标记各个节点的子节点,没有节点指向的为根节点。
  2. 判断两个序列形成的二叉搜索树是否一样。先建一颗树A,节点增加一个flag标记,用于记录是否被访问过。另一个序列B从头开始遍历,B中每个节点在已建好的树A上进行搜索,如果在搜索过程中A树上遇到了未访问的节点且不是要查找的节点,则树不一样。

reference

浙江大学 数据结构mook 树 https://www.icourse163.org/learn/ZJU-93001?tid=1003013004#/learn/announce

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值