那些經典算法:貪心算法


貪心算法和分治算法、動態規划算法、回溯算法都是一種編程思想,深入理解這些編程思想,我們也可以根據實際情況設計自己的算法。

一 貪心算法原理

貪心算法的原理比較簡單,就是對問題求解的時候,每步都選擇當前的最優解,然后已期望得到全局最優解。
貪心算法的適用場景是每次選擇是沒有狀態的,也就是不會對后面的步驟產生影響。

二 貪心算法舉例

同樣用老師課件中的兩個例子:
背包問題:
假如我們有一個可以裝100kg物品的背包,我們有5種豆子,每種豆子的總量和總價值各不相同。為了讓背包中所裝的物品的總價值最大,我們如何選擇裝哪些豆子,每種裝多少?
豆子
分析
直觀來想,我們的總重是100kg是限制的,要求裝的物品總價值最大,那么我們可以把各種豆子的單價計算下每種豆子的單價,然后按照從高到低排序,每次裝完最有價值的豆子后,再繼續裝稍次價值的豆子,直到裝滿整個背包。

分糖果:
我們有m個糖果和n個孩子,現在要把糖果分給這些孩子吃,但是糖果少m<n,所以只有一部分孩子能夠得到糖果,每個糖果的大小不一樣,m個糖果大小分別為s1,s2,s3….sm.除此之外,每個孩子對糖果大小需求不一樣,假設這些孩子對糖果的需求大小分別為g1,g2…gn,只有糖果大小大於孩子對糖果需求的時候,他們才會滿足,求我們如何分糖果才能夠滿足最多數量的孩子。

分析:
這個問題是我們選擇一部分小孩分給他們糖果,要滿足一共最多只能分給m個孩子,還要讓滿意的孩子最多。和上一個問題類似,就是我們按照孩子對糖果滿足從小到大排序,然后依次選能夠滿足孩子需求的最小糖果,這樣依次分下去就可以達到滿足最多孩子的目的。

這類題目的特點:
1) 都是有限制的請求下求解
背包問題限制是100kg,分糖果問題限制問題是最多有m個糖果。
2)都是求限制條件下的最優解,背包問題是求最大價值,分糖果是為了求滿足最多孩子。
3)每步都是局部最優的,比如背包問題的時候,因為要求最大價值,所以先裝最貴的,這樣質量不變的情況下,增加的價值最大;分糖果也一樣,是在最小糖果的情況下滿足一個孩子,滿足孩子的都是一個,那么我們需要減少分出去的糖果。

三 霍夫曼編碼

霍夫曼編碼是一種廣泛用於數據文件壓縮的編碼方法,壓縮率通常在20%到90%之間,霍夫曼編碼算法根據字符出現頻率,用不同的0,1串來標識字符,從而達到縮短字符串,達到壓縮的目的。
還是拿課程中的算法舉例:
假設有一個包含1000字符的文件,每個字符占1byte,一共需要8000bits來存儲。
如果這1000個字符只包括6種字符,分別是a,b,c,d,e,f那么我們通過3個bit(最多可以標識8個字符)來表示這6個字符,那么總共需要3000bits就可以表示這個字符串了。

用這種三個bit標識一個字符,編碼和解碼比較簡單,但是沒有充分考慮每個字符在文件中出現的頻率。而霍夫曼編碼是結合字符在文件中的頻率來進行對字符編碼的,出現字符多的編碼更短,由於霍夫曼編碼的長短是不一樣的,所以如何不讓兩種不同的編碼之間產生混淆?那就需要保證每種編碼不能為另一種編碼的前綴。

假設這些字符在文件中出現的頻率如下:
文件中的字符頻率
總bits = 1*450 +2*350+3*90+4*60+5*30+5*20 = 2100

2100bits比原來3000bits又壓縮了近1/3, 下面問題就是如何進行霍夫曼編碼了。
王爭老師的算法很巧妙也簡單:
1)把所有涉及到的字符按照出現頻率的高低放入到優先級隊列中區。
2)我們從隊列中取出頻率最小的兩個字符上圖中為f和e,然后新建個字符比如X,頻率為f和e的頻率之和,然后X作為f和e的父親節點。
3)再把X節點放入到優先級隊列中。
4)轉到2繼續指向,直到隊列為空。
霍夫曼編碼求解過程

構造完一顆二叉樹之后,我們給每條邊都做個編碼,比如左邊的邊為0,右邊的為1,得到如下:
霍夫曼編碼樹
這樣每個節點的編碼可以用從根節點到此節點的邊來表示:
1)比如a這個節點編碼為1
2)c這個編碼為001。

四 代碼實現

在網上找一段求霍夫曼編碼的C++代碼,可以對照理解下:

#include <iostream>
using namespace std;

//最大字符編碼數組長度
#define MAXCODELEN 100
//最大哈夫曼節點結構體數組個數
#define MAXHAFF 100
//最大哈夫曼編碼結構體數組的個數
#define MAXCODE 100
#define MAXWEIGHT  10000;


typedef struct Haffman
{
    //權重
    int weight;
    //字符
    char ch;
    //父節點
    int parent;
    //左兒子節點
    int leftChild;
    //右兒子節點
    int rightChild;
}HaffmaNode;

typedef struct Code
{
    //字符的哈夫曼編碼的存儲
    int code[MAXCODELEN];
    //從哪個位置開始
    int start;
}HaffmaCode;

HaffmaNode haffman[MAXHAFF];
HaffmaCode code[MAXCODE];

void buildHaffman(int all)
{
    //哈夫曼節點的初始化之前的工作, weight為0,parent,leftChile,rightChile都為-1
    for (int i = 0; i < all * 2 - 1; ++i)
    {
        haffman[i].weight = 0;
        haffman[i].parent = -1;
        haffman[i].leftChild = -1;
        haffman[i].rightChild = -1;
    }
    std::cout << "請輸入需要哈夫曼編碼的字符和權重大小" << std::endl;
    for (int i = 0; i < all; i++)
    {
        std::cout << "請分別輸入第個" << i << "哈夫曼字符和權重" << std::endl;
        std::cin >> haffman[i].ch;
        std::cin >> haffman[i].weight;
    }
    //每次找出最小的權重的節點,生成新的節點,需要all - 1 次合並
    int x1, x2, w1, w2;
    for (int i = 0; i < all - 1; ++i)
    {
        x1 = x2 = -1;
        w1 = w2 = MAXWEIGHT;
        //注意這里每次是all + i次里面便利
        for (int j = 0; j < all + i; ++j)
        {
            //得到最小權重的節點
            if (haffman[j].parent == -1 && haffman[j].weight < w1)
            {
                //如果每次最小的更新了,那么需要把上次最小的給第二個最小的
                w2 = w1;
                x2 = x1;

                x1 = j;
                w1 = haffman[j].weight;
            }
            //這里用else if而不是if,是因為它們每次只選1個就可以了。
            else if(haffman[j].parent == -1 && haffman[j].weight < w2)
            {
                x2 = j;
                w2 = haffman[j].weight;
            }
        }
        //么次找到最小的兩個節點后要記得合並成一個新的節點
        haffman[all + i].leftChild = x1;
        haffman[all + i].rightChild = x2;
        haffman[all + i].weight = w1 + w2;
        haffman[x1].parent = all + i;
        haffman[x2].parent = all + i;
        std::cout << "x1 is" << x1 <<" x1 parent is"<<haffman[x1].parent<< " x2 is" << x2 <<" x2 parent is "<< haffman[x2].parent<< " new Node is " << all + i << "new weight is" << haffman[all + i].weight << std::endl;
    }
}

//打印每個字符的哈夫曼編碼
void printCode(int all)
{
    //保存當前葉子節點的字符編碼
    HaffmaCode hCode;
    //當前父節點
    int curParent;
    //下標和葉子節點的編號
    int c;
    //遍歷的總次數
    for (int i = 0; i < all; ++i)
    {
        hCode.start = all - 1;
        c = i;
        curParent = haffman[i].parent;
        //遍歷的條件是父節點不等於-1
        while (curParent != -1)
        {
            //我們先拿到父節點,然后判斷左節點是否為當前值,如果是取節點0
            //否則取節點1,這里的c會變動,所以不要用i表示,我們用c保存當前變量i
            if (haffman[curParent].leftChild == c)
            {
                hCode.code[hCode.start] = 0;
                std::cout << "hCode.code[" << hCode.start << "] = 0" << std::endl;
            }
            else
            {
                hCode.code[hCode.start] = 1;
                std::cout << "hCode.code[" << hCode.start << "] = 1" << std::endl;
            }
            hCode.start--;
            c = curParent;
            curParent = haffman[c].parent;
        }
        //把當前的葉子節點信息保存到編碼結構體里面
        for (int j = hCode.start + 1; j < all; ++j)
        {
            code[i].code[j] = hCode.code[j];
        }
        code[i].start = hCode.start;
    }

}

int main()
{
    std::cout << "請輸入有多少個哈夫曼字符" << std::endl;
    int all = 0;
    std::cin >> all;
    if (all <= 0)
    {
        std::cout << "您輸入的個數有誤" << std::endl;
        return -1;
    }
    buildHaffman(all);
    printCode(all);
    for (int i = 0; i < all; ++i)
    {
        std::cout << haffman[i].ch << ": Haffman Code is:";
        for (int j = code[i].start + 1; j < all; ++j)
        {
            std::cout << code[i].code[j];
        }
        std::cout << std::endl;
    }
    return 0;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM