數據結構筆記-樹

相關

  • 靜態查找 :集合是固定的,沒有插入和刪除

  • 動態查找:集合是動態的,可能發生插入和刪除

  • 二分查找

    • 將複雜度降爲log2N\log_2N,相當於將數組變爲樹。
    • 前提:數組是排好序的

定義

樹:n(n0)n (n\geq0)各節點構成的有限集合
n=0時,是空樹。沒有節點也是樹

  • 非根節點可分爲m個不相交的有限集合,每個集合又是一棵樹,成爲原樹的子樹。
  • 除了根節點,每個節點僅有一個父節點
  • n個節點有n-1條邊
  • 節點的度:子樹的個數
  • 樹的度:所有節點中最大的度數
  • 路徑和路徑長度:從節點n1n_1nkn_k的路徑爲一個節點序列n1,n2,...nkn_1,n_2,...n_knin_ini+1n_{i+1}的父節點。路徑所包含的邊數爲路徑長度
  • 節點的層次,根節點在1層。
  • 樹的深度:所有節點中最大層次

二叉樹表示

兒子兄弟法可以將所有樹變爲二叉樹。

二叉樹的五種基本形態

  • 空樹
  • 只有根節點
  • 只有左孩子
  • 只有右孩子
  • 有左,右孩子

幾種特殊的二叉樹

  • 斜二叉樹(skewed Binary tree)。即鏈表,所有節點只有一個孩子
  • 完美二叉樹(perfect Binary tree) 或 滿二叉樹(full binary tree) .每層節點數量達到最大
  • 完全二叉樹(complete binary tree) 對節點從上到下,從左到右進行編號,與滿二叉樹編號相同

二叉樹的性質

  • 第i層最大節點數爲2i1,i12^{i-1},i\geq1
  • 深度爲k的二叉樹最多有2i12^i-1節點
  • 對於非空二叉樹,n0n_0爲葉子節點數,n2n_2是度爲二的非葉子節點個數,那麼滿足n0=n2+1n_0 =n_2 +1

存儲

完全二叉樹

  • 順序存儲 i = index+1
  • 節點爲i,左孩子爲2I,右孩子爲2i+1
  • 節點爲i,父節點爲 i/2 (向下取整)

一般二叉樹

  • 鏈表存儲,用數組會造成空間浪費

樹的遍歷

樹有兩大類遍歷方法,深度優先和廣度優先。對於二叉樹來說,深度優先又分爲先序,中序,後序三種遍歷方式。如果從根開始遍歷,最後回到根,每個節點會被遍歷三次,對應前序,中序,後序。深度優先的遍歷線路時固定的,只是什麼時候遍歷該節點是不確定的。而先中後三種是針對根節點訪問順序而言的。
對於一個nn叉樹來說,深度優先遍歷時,每個節點會被遍歷n+1n+1次。
在這裏插入圖片描述Figure1 Figure 1 深度優先遍歷路徑

遞歸遍歷(針對深度優先遍歷)

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)= hLhRh_L-h_R,hLh_LhRh_R分別是左右子樹的高度

AVL定義

  • 空樹或者任意節點的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個葉子節點,每個葉子節點帶有權值wkw_k,從根節點到每個葉子節點的長度爲IkI_k,則每個葉子節點的帶權路徑長度之和就是WPL=k=1nwklkWPL=\sum^n_{k=1}w_kl_k

最優二叉樹或哈弗曼樹: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或自身等特殊標記。
爲了標記每個集合元素數量,可以在跟的父節點中記錄集合元素數量的相反數,既表明了是根節點,又記錄集合中的數量元素。

操作

可以對並查集進行化簡,每個節點的父親節點是數組中的值,自身的數據用數組下標表示。如果自身數據到數組下標這個轉化較複雜,可以單獨寫一個映射。

  • 初始化
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章