1. 程式人生 > >哈夫曼樹的原理與實現

哈夫曼樹的原理與實現



一、哈夫曼樹的介紹

Huffman Tree,中文名是哈夫曼樹或霍夫曼樹,它是最優二叉樹。

定義:給定n個權值作為n個葉子結點,構造一棵二叉樹,若樹的帶權路徑長度達到最小,則這棵樹被稱為哈夫曼樹。 這個定義裡面涉及到了幾個陌生的概念,下面就是一顆哈夫曼樹,我們來看圖解答。

(1) 路徑和路徑長度

定義:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從根結點到第L層結點的路徑長度為L-1。
例子:100和80的路徑長度是1,50和30的路徑長度是2,20和10的路徑長度是3。

(2) 結點的權及帶權路徑長度

定義:若將樹中結點賦給一個有著某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積。
例子:節點20的路徑長度是3,它的帶權路徑長度= 路徑長度 * 權 = 3 * 20 = 60。

(3) 樹的帶權路徑長度

定義:樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為WPL。
例子:示例中,樹的WPL= 1*100 + 2*50 + 3*20 + 3*10 = 100 + 100 + 60 + 30 = 290。

比較下面兩棵樹

上面的兩棵樹都是以{10, 20, 50, 100}為葉子節點的樹。

左邊的樹WPL=2*10 + 2*20 + 2*50 + 2*100 = 360
右邊的樹WPL=290

左邊的樹WPL > 右邊的樹的WPL。你也可以計算除上面兩種示例之外的情況,但實際上右邊的樹就是{10,20,50,100}對應的哈夫曼樹。至此,應該對哈夫曼樹的概念有了一定的瞭解了,下面看看如何去構造一棵哈夫曼樹。

二、哈夫曼樹的圖文解析

假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設為 w1、w2、…、wn,哈夫曼樹的構造規則為:

1. 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點);
2. 在森林中選出根結點的權值最小的兩棵樹進行合併,作為一棵新樹的左、右子樹,且新樹的根結點權值為其左、右子樹根結點權值之和;
3. 從森林中刪除選取的兩棵樹,並將新樹加入森林;
4. 重複(02)、(03)步,直到森林中只剩一棵樹為止,該樹即為所求得的哈夫曼樹。

以{5,6,7,8,15}為例,來構造一棵哈夫曼樹。

第1步:建立森林,森林包括5棵樹,這5棵樹的權值分別是5,6,7,8,15。
第2步:在森林中,選擇根節點權值最小的兩棵樹(5和6)來進行合併,將它們作為一顆新樹的左右孩子(誰左誰右無關緊要,這裡,我們選擇較小的作為左孩子),並且新樹的權值是左右孩子的權值之和。即,新樹的權值是11。 然後,將"樹5"和"樹6"從森林中刪除,並將新的樹(樹11)新增到森林中。
第3步:在森林中,選擇根節點權值最小的兩棵樹(7和8)來進行合併。得到的新樹的權值是15。 然後,將"樹7"和"樹8"從森林中刪除,並將新的樹(樹15)新增到森林中。
第4步:在森林中,選擇根節點權值最小的兩棵樹(11和15)來進行合併。得到的新樹的權值是26。 然後,將"樹11"和"樹15"從森林中刪除,並將新的樹(樹26)新增到森林中。
第5步:在森林中,選擇根節點權值最小的兩棵樹(15和26)來進行合併。得到的新樹的權值是41。 然後,將"樹15"和"樹26"從森林中刪除,並將新的樹(樹41)新增到森林中。
此時,森林中只有一棵樹(樹41)。這棵樹就是我們需要的哈夫曼樹!

三、哈夫曼樹的基本操作

哈夫曼樹的重點是如何構造哈夫曼樹。本文構造哈夫曼時,用到了以前介紹過的"(二叉堆)最小堆"。下面對哈夫曼樹進行講解。

1. 基本定義

public class HuffmanNode implements Comparable, Cloneable {
    protected int key;              // 權值
    protected HuffmanNode left;     // 左孩子
    protected HuffmanNode right;    // 右孩子
    protected HuffmanNode parent;   // 父結點

    protected HuffmanNode(int key, HuffmanNode left, HuffmanNode right, HuffmanNode parent) {
        this.key = key;
        this.left = left;
        this.right = right;
        this.parent = parent;
    }

    @Override
    public Object clone() {
        Object obj=null;

        try {
            obj = (HuffmanNode)super.clone();//Object 中的clone()識別出你要複製的是哪一個物件。    
        } catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }

        return obj;    
    }

    @Override
    public int compareTo(Object obj) {
        return this.key - ((HuffmanNode)obj).key;
    }
}

HuffmanNode是哈夫曼樹的節點類。

public class Huffman {

    private HuffmanNode mRoot;  // 根結點

    ...
}

Huffman是哈夫曼樹對應的類,它包含了哈夫曼樹的根節點和哈夫曼樹的相關操作。

2. 構造哈夫曼樹

/* 
 * 建立Huffman樹
 *
 * @param 權值陣列
 */
public Huffman(int a[]) {
    HuffmanNode parent = null;
    MinHeap heap;

    // 建立陣列a對應的最小堆
    heap = new MinHeap(a);

    for(int i=0; i<a.length-1; i++) {   
        HuffmanNode left = heap.dumpFromMinimum();  // 最小節點是左孩子
        HuffmanNode right = heap.dumpFromMinimum(); // 其次才是右孩子

        // 新建parent節點,左右孩子分別是left/right;
        // parent的大小是左右孩子之和
        parent = new HuffmanNode(left.key+right.key, left, right, null);
        left.parent = parent;
        right.parent = parent;

        // 將parent節點資料拷貝到"最小堆"中
        heap.insert(parent);
    }

    mRoot = parent;

    // 銷燬最小堆
    heap.destroy();
}

首先建立最小堆,然後進入for迴圈。

每次迴圈時:

(1) 首先,將最小堆中的最小節點拷貝一份並賦值給left,然後重塑最小堆(將最小節點和後面的節點交換位置,接著將"交換位置後的最小節點"之前的全部元素重新構造成最小堆);
(2) 接著,再將最小堆中的最小節點拷貝一份並將其賦值right,然後再次重塑最小堆;
(3) 然後,新建節點parent,並將它作為left和right的父節點;
(4) 接著,將parent的資料複製給最小堆中的指定節點。

四、哈夫曼樹的完整原始碼

1. 哈夫曼樹的節點類(HuffmanNode.java)

package com.struction.source.tree.huffman.java;

/**
 * Huffman節點類(Huffman.java的輔助類)
 * 
 * @author Anndy
 */

public class HuffmanNode implements Comparable, Cloneable {
	protected int key; // 權值
	protected HuffmanNode left; // 左孩子
	protected HuffmanNode right; // 右孩子
	protected HuffmanNode parent; // 父結點

	protected HuffmanNode(int key, HuffmanNode left, HuffmanNode right,
			HuffmanNode parent) {
		this.key = key;
		this.left = left;
		this.right = right;
		this.parent = parent;
	}

	@Override
	public Object clone() {
		Object obj = null;

		try {
			obj = (HuffmanNode) super.clone();// Object 中的clone()識別出你要複製的是哪一個物件。
		} catch (CloneNotSupportedException e) {
			System.out.println(e.toString());
		}

		return obj;
	}

	@Override
	public int compareTo(Object obj) {
		return this.key - ((HuffmanNode) obj).key;
	}
}

2. 哈夫曼樹的實現檔案(Huffman.java)

package com.struction.source.tree.huffman.java;

/**
 * Huffman樹
 * 
 * @author Anndy
 */

public class Huffman {

	private HuffmanNode mRoot; // 根結點

	/*
	 * 建立Huffman樹
	 * 
	 * @param 權值陣列
	 */
	public Huffman(int a[]) {
		HuffmanNode parent = null;
		MinHeap heap;

		// 建立陣列a對應的最小堆
		heap = new MinHeap(a);

		for (int i = 0; i < a.length - 1; i++) {
			HuffmanNode left = heap.dumpFromMinimum(); // 最小節點是左孩子
			HuffmanNode right = heap.dumpFromMinimum(); // 其次才是右孩子

			// 新建parent節點,左右孩子分別是left/right;
			// parent的大小是左右孩子之和
			parent = new HuffmanNode(left.key + right.key, left, right, null);
			left.parent = parent;
			right.parent = parent;

			// 將parent節點資料拷貝到"最小堆"中
			heap.insert(parent);
		}

		mRoot = parent;

		// 銷燬最小堆
		heap.destroy();
	}

	/*
	 * 前序遍歷"Huffman樹"
	 */
	private void preOrder(HuffmanNode tree) {
		if (tree != null) {
			System.out.print(tree.key + " ");
			preOrder(tree.left);
			preOrder(tree.right);
		}
	}

	public void preOrder() {
		preOrder(mRoot);
	}

	/*
	 * 中序遍歷"Huffman樹"
	 */
	private void inOrder(HuffmanNode tree) {
		if (tree != null) {
			inOrder(tree.left);
			System.out.print(tree.key + " ");
			inOrder(tree.right);
		}
	}

	public void inOrder() {
		inOrder(mRoot);
	}

	/*
	 * 後序遍歷"Huffman樹"
	 */
	private void postOrder(HuffmanNode tree) {
		if (tree != null) {
			postOrder(tree.left);
			postOrder(tree.right);
			System.out.print(tree.key + " ");
		}
	}

	public void postOrder() {
		postOrder(mRoot);
	}

	/*
	 * 銷燬Huffman樹
	 */
	private void destroy(HuffmanNode tree) {
		if (tree == null)
			return;

		if (tree.left != null)
			destroy(tree.left);
		if (tree.right != null)
			destroy(tree.right);

		tree = null;
	}

	public void destroy() {
		destroy(mRoot);
		mRoot = null;
	}

	/*
	 * 列印"Huffman樹"
	 * 
	 * key -- 節點的鍵值 direction -- 0,表示該節點是根節點; -1,表示該節點是它的父結點的左孩子;
	 * 1,表示該節點是它的父結點的右孩子。
	 */
	private void print(HuffmanNode tree, int key, int direction) {

		if (tree != null) {

			if (direction == 0) // tree是根節點
				System.out.printf("%2d is root\n", tree.key);
			else
				// tree是分支節點
				System.out.printf("%2d is %2d's %6s child\n", tree.key, key,
						direction == 1 ? "right" : "left");

			print(tree.left, tree.key, -1);
			print(tree.right, tree.key, 1);
		}
	}

	public void print() {
		if (mRoot != null)
			print(mRoot, mRoot.key, 0);
	}
}

3. 哈夫曼樹對應的最小堆(MinHeap.java)

package com.struction.source.tree.huffman.java;

/**
 * 最小堆(Huffman.java的輔助類)
 *
 * @author Anndy
 */

import java.util.ArrayList;
import java.util.List;

public class MinHeap {

	private List<HuffmanNode> mHeap; // 存放堆的陣列

	/*
	 * 建立最小堆
	 * 
	 * 引數說明: a -- 資料所在的陣列
	 */
	protected MinHeap(int a[]) {
		mHeap = new ArrayList<HuffmanNode>();
		// 初始化陣列
		for (int i = 0; i < a.length; i++) {
			HuffmanNode node = new HuffmanNode(a[i], null, null, null);
			mHeap.add(node);
		}

		// 從(size/2-1) --> 0逐次遍歷。遍歷之後,得到的陣列實際上是一個最小堆。
		for (int i = a.length / 2 - 1; i >= 0; i--)
			filterdown(i, a.length - 1);
	}

	/*
	 * 最小堆的向下調整演算法
	 * 
	 * 注:陣列實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
	 * 
	 * 引數說明: start -- 被下調節點的起始位置(一般為0,表示從第1個開始) end -- 截至範圍(一般為陣列中最後一個元素的索引)
	 */
	protected void filterdown(int start, int end) {
		int c = start; // 當前(current)節點的位置
		int l = 2 * c + 1; // 左(left)孩子的位置
		HuffmanNode tmp = mHeap.get(c); // 當前(current)節點

		while (l <= end) {
			// "l"是左孩子,"l+1"是右孩子
			if (l < end && (mHeap.get(l).compareTo(mHeap.get(l + 1)) > 0))
				l++; // 左右兩孩子中選擇較小者,即mHeap[l+1]

			int cmp = tmp.compareTo(mHeap.get(l));
			if (cmp <= 0)
				break; // 調整結束
			else {
				mHeap.set(c, mHeap.get(l));
				c = l;
				l = 2 * l + 1;
			}
		}
		mHeap.set(c, tmp);
	}

	/*
	 * 最小堆的向上調整演算法(從start開始向上直到0,調整堆)
	 * 
	 * 注:陣列實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
	 * 
	 * 引數說明: start -- 被上調節點的起始位置(一般為陣列中最後一個元素的索引)
	 */
	protected void filterup(int start) {
		int c = start; // 當前節點(current)的位置
		int p = (c - 1) / 2; // 父(parent)結點的位置
		HuffmanNode tmp = mHeap.get(c); // 當前(current)節點

		while (c > 0) {
			int cmp = mHeap.get(p).compareTo(tmp);
			if (cmp <= 0)
				break;
			else {
				mHeap.set(c, mHeap.get(p));
				c = p;
				p = (p - 1) / 2;
			}
		}
		mHeap.set(c, tmp);
	}

	/*
	 * 將node插入到二叉堆中
	 */
	protected void insert(HuffmanNode node) {
		int size = mHeap.size();

		mHeap.add(node); // 將"陣列"插在表尾
		filterup(size); // 向上調整堆
	}

	/*
	 * 交換兩個HuffmanNode節點的全部資料
	 */
	private void swapNode(int i, int j) {
		HuffmanNode tmp = mHeap.get(i);
		mHeap.set(i, mHeap.get(j));
		mHeap.set(j, tmp);
	}

	/*
	 * 新建一個節點,並將最小堆中最小節點的資料複製給該節點。 然後除最小節點之外的資料重新構造成最小堆。
	 * 
	 * 返回值: 失敗返回null。
	 */
	protected HuffmanNode dumpFromMinimum() {
		int size = mHeap.size();

		// 如果"堆"已空,則返回
		if (size == 0)
			return null;

		// 將"最小節點"克隆一份,將克隆得到的物件賦值給node
		HuffmanNode node = (HuffmanNode) mHeap.get(0).clone();

		// 交換"最小節點"和"最後一個節點"
		mHeap.set(0, mHeap.get(size - 1));
		// 刪除最後的元素
		mHeap.remove(size - 1);

		if (mHeap.size() > 1)
			filterdown(0, mHeap.size() - 1);

		return node;
	}

	// 銷燬最小堆
	protected void destroy() {
		mHeap.clear();
		mHeap = null;
	}
}

4. 哈夫曼樹的測試程式(HuffmanTest.java)

package com.struction.source.tree.huffman.java;

/**
 * Huffman樹的測試程式
 * 
 * @author Anndy
 */

public class HuffmanTest {

	private static final int a[] = { 5, 6, 8, 7, 15 };

	public static void main(String[] args) {
		int i;
		Huffman tree;

		System.out.print("== 新增陣列: ");
		for (i = 0; i < a.length; i++)
			System.out.print(a[i] + " ");

		// 建立陣列a對應的Huffman樹
		tree = new Huffman(a);

		System.out.print("\n== 前序遍歷: ");
		tree.preOrder();

		System.out.print("\n== 中序遍歷: ");
		tree.inOrder();

		System.out.print("\n== 後序遍歷: ");
		tree.postOrder();
		System.out.println();

		System.out.println("== 樹的詳細資訊: ");
		tree.print();

		// 銷燬二叉樹
		tree.destroy();
	}
}