基于C#實現(xiàn)哈夫曼樹算法
赫夫曼樹又稱最優(yōu)二叉樹,也就是帶權(quán)路徑最短的樹,對于赫夫曼樹,我想大家對它是非常的熟悉,也知道它的應(yīng)用場景,但是有沒有自己親手寫過,這個我就不清楚了,不管以前寫沒寫,這一篇我們來玩一把。
一、概念
赫夫曼樹里面有幾個概念,也是非常簡單的,先來看下面的圖:

- 節(jié)點的權(quán): 節(jié)點中紅色部分就是權(quán),在實際應(yīng)用中,我們用“字符”出現(xiàn)的次數(shù)作為權(quán)。
- 路徑長度:可以理解成該節(jié)點到根節(jié)點的層數(shù),比如:“A”到根節(jié)點的路徑長度為 3。
- 樹的路徑長度:各個葉子節(jié)點到根節(jié)點的路徑長度總和,用 WPL 標(biāo)記。
最后我們要討論的的赫夫曼樹也就是帶權(quán)路徑長度最小的一棵樹。
二、構(gòu)建
由于要使 WPL 最短,赫夫曼樹的構(gòu)建采用自低向上的方式,這里我們采用小根堆來存放當(dāng)前需要構(gòu)建的各個節(jié)點,我們的方式是每次從小根堆中取出最小的兩個節(jié)點,合并后放入堆中,然后繼續(xù)取兩個最小的節(jié)點,一直到小根堆為空,最后我們采用自底向上構(gòu)建的赫夫曼樹也就完畢了。

好了,赫夫曼樹的典型應(yīng)用就是在數(shù)據(jù)壓縮方面,下面我們就要在赫夫曼樹上面放入赫夫曼編碼了,我們知道普通的 ASCII 碼是采用等長編碼的,即每個字符都采用 2 個字節(jié),而赫夫曼編碼的思想就是采用不等長的思路,權(quán)重高的字符靠近根節(jié)點,權(quán)重低的字符遠(yuǎn)離根節(jié)點,標(biāo)記方式為左孩子“0”,右孩子“1”,如下圖。

從圖中我們可以看到各個字符的赫夫曼編碼了,獲取字符的編碼采用從根往下的方式收集路徑上的‘0,1’,如:
A:110。
B:111。
C:0。
D:10。
最后我們來比較他們的 WPL 的長度: ASCII 碼=102+202+402+802=300
赫夫曼碼=103+203+402+801=250
可以看到,赫夫曼碼壓縮了 50 個 0,1 字符。
三、代碼
1、樹節(jié)點
我們采用 7 元節(jié)點,其中 parent 方便我們在 DFS 的時候找到從葉子節(jié)點到根節(jié)點的路徑上的赫夫曼編碼。
#region 赫夫曼節(jié)點
/// <summary>
/// 赫夫曼節(jié)點
/// </summary>
public class Node
{
/// <summary>
/// 左孩子
/// </summary>
public Node left;
/// <summary>
/// 右孩子
/// </summary>
public Node right;
/// <summary>
/// 父節(jié)點
/// </summary>
public Node parent;
/// <summary>
/// 節(jié)點字符
/// </summary>
public char c;
/// <summary>
/// 節(jié)點權(quán)重
/// </summary>
public int weight;
//赫夫曼“0"or“1"
public char huffmancode;
/// <summary>
/// 標(biāo)記是否為葉子節(jié)點
/// </summary>
public bool isLeaf;
}
#endregion
2、構(gòu)建赫夫曼樹(Build)
上面也說了,構(gòu)建赫夫曼編碼樹我們采用小根堆的形式構(gòu)建,構(gòu)建完后,我們采用 DFS 的方式統(tǒng)計各個字符的編碼,復(fù)雜度為 N*logN。
#region 構(gòu)建赫夫曼樹
/// <summary>
/// 構(gòu)建赫夫曼樹
/// </summary>
public void Build()
{
//構(gòu)建
while (queue.Count() > 0)
{
//如果只有一個節(jié)點,則說明已經(jīng)到根節(jié)點了
if (queue.Count() == 1)
{
root = queue.Dequeue().t;
break;
}
//節(jié)點1
var node1 = queue.Dequeue();
//節(jié)點2
var node2 = queue.Dequeue();
//標(biāo)記左孩子
node1.t.huffmancode = '0';
//標(biāo)記為右孩子
node2.t.huffmancode = '1';
//判斷當(dāng)前節(jié)點是否為葉子節(jié)點,hufuman無度為1點節(jié)點(方便計算huffman編碼)
if (node1.t.left == null)
node1.t.isLeaf = true;
if (node2.t.left == null)
node2.t.isLeaf = true;
//父節(jié)點
root = new Node();
root.left = node1.t;
root.right = node2.t;
root.weight = node1.t.weight + node2.t.weight;
//當(dāng)前節(jié)點為根節(jié)點
node1.t.parent = node2.t.parent = root;
//將當(dāng)前節(jié)點的父節(jié)點入隊列
queue.Eequeue(root, root.weight);
}
//深度優(yōu)先統(tǒng)計各個字符的編碼
DFS(root);
}
#endregion
3、編碼(Encode,Decode)
樹構(gòu)建起來后,我會用字典來保存字符和”赫夫曼編碼“的對應(yīng)表,然后拿著明文或者密文對著編碼表翻譯就行了, 復(fù)雜度 O(N)。
#region 赫夫曼編碼
/// <summary>
/// 赫夫曼編碼
/// </summary>
/// <returns></returns>
public string Encode()
{
StringBuilder sb = new StringBuilder();
foreach (var item in word)
{
sb.Append(huffmanEncode[item]);
}
return sb.ToString();
}
#endregion
#region 赫夫曼解碼
/// <summary>
/// 赫夫曼解碼
/// </summary>
/// <returns></returns>
public string Decode(string str)
{
StringBuilder decode = new StringBuilder();
string temp = string.Empty;
for (int i = 0; i < str.Length; i++)
{
temp += str[i].ToString();
//如果包含 O(N)時間
if (huffmanDecode.ContainsKey(temp))
{
decode.Append(huffmanDecode[temp]);
temp = string.Empty;
}
}
return decode.ToString();
}
#endregion
最后我們做個例子,壓縮 9M 的文件,看看到底能壓縮多少?
public static void Main()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1 * 10000; i++)
{
sb.Append("人民網(wǎng)北京12月8日電 (記者 宋心蕊) 北京時間8日晚的央視《新聞聯(lián)播》節(jié)目出現(xiàn)了直播失誤。上一條新聞尚未播放完畢時,播就將畫面切換回了演播間,主播李梓萌開始播報下一條新聞,導(dǎo)致兩條新聞出現(xiàn)了“混音”播出。央視新聞官方微博賬號在21點09分發(fā)布了一條致歉微博:【致歉】今晚《新聞聯(lián)播》因?qū)РT口令失誤,導(dǎo)致畫面切換錯誤,特此向觀眾朋友表示歉意。央視特約評論員楊禹在個人微博中寫道:今晚《新聞聯(lián)播》出了個切換錯誤,@央視新聞 及時做了誠懇道歉。聯(lián)播一直奉行“金標(biāo)準(zhǔn)”,壓力源自全社會的高要求。其實報紙亦都有“勘誤”一欄,坦誠糾錯與道歉。《新聞聯(lián)播》是中國影響力最大的電視新聞節(jié)目。它有不可替代的符號感,它有失誤,更有悄然的進(jìn)步。新的改進(jìn)正在或即將發(fā)生,不妨期待");
}
File.WriteAllText(Environment.CurrentDirectory + "http://1.txt", sb.ToString());
Huffman huffman = new Huffman(sb.ToString());
Stopwatch watch = Stopwatch.StartNew();
huffman.Build();
watch.Stop();
Console.WriteLine("構(gòu)建赫夫曼樹耗費(fèi):{0}", watch.ElapsedMilliseconds);
//將8位二進(jìn)制轉(zhuǎn)化為ascII碼
var s = huffman.Encode();
var remain = s.Length % 8;
List<char> list = new List<char>();
var start = 0;
for (int i = 8; i < s.Length; i = i + 8)
{
list.Add((char)Convert.ToInt32(s.Substring(i - 8, 8), 2));
start = i;
}
var result = new String(list.ToArray());
//當(dāng)字符編碼不足8位時, 用‘艸'來標(biāo)記,然后拿出'擦‘以后的所有0,1即可
result += "艸" + s.Substring(start);
File.WriteAllText(Environment.CurrentDirectory + "http://2.txt", result);
Console.WriteLine("壓縮完畢!");
Console.Read();
//解碼
var str = File.ReadAllText(Environment.CurrentDirectory + "http://2.txt");
sb.Clear();
for (int i = 0; i < str.Length; i++)
{
int ua = (int)str[i];
//說明已經(jīng)取完畢了 用'艸'來做標(biāo)記
if (ua == 33401)
sb.Append(str.Substring(i));
else
sb.Append(Convert.ToString(ua, 2).PadLeft(8, '0'));
}
var sss = huffman.Decode(sb.ToString());
Console.Read();
}

看看,將 9M 的文件壓縮到了 4M。
主程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.IO;
namespace ConsoleApplication2
{
public class Program
{
public static void Main()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1 * 10000; i++)
{
sb.Append("人民網(wǎng)北京12月8日電 (記者 宋心蕊) 北京時間8日晚的央視《新聞聯(lián)播》節(jié)目出現(xiàn)了直播失誤。上一條新聞尚未播放完畢時,播就將畫面切換回了演播間,主播李梓萌開始播報下一條新聞,導(dǎo)致兩條新聞出現(xiàn)了“混音”播出。央視新聞官方微博賬號在21點09分發(fā)布了一條致歉微博:【致歉】今晚《新聞聯(lián)播》因?qū)РT口令失誤,導(dǎo)致畫面切換錯誤,特此向觀眾朋友表示歉意。央視特約評論員楊禹在個人微博中寫道:今晚《新聞聯(lián)播》出了個切換錯誤,@央視新聞 及時做了誠懇道歉。聯(lián)播一直奉行“金標(biāo)準(zhǔn)”,壓力源自全社會的高要求。其實報紙亦都有“勘誤”一欄,坦誠糾錯與道歉?!缎侣劼?lián)播》是中國影響力最大的電視新聞節(jié)目。它有不可替代的符號感,它有失誤,更有悄然的進(jìn)步。新的改進(jìn)正在或即將發(fā)生,不妨期待");
}
File.WriteAllText(Environment.CurrentDirectory + "http://1.txt", sb.ToString());
Huffman huffman = new Huffman(sb.ToString());
Stopwatch watch = Stopwatch.StartNew();
huffman.Build();
watch.Stop();
Console.WriteLine("構(gòu)建赫夫曼樹耗費(fèi):{0}", watch.ElapsedMilliseconds);
//將8位二進(jìn)制轉(zhuǎn)化為ascII碼
var s = huffman.Encode();
var remain = s.Length % 8;
List<char> list = new List<char>();
var start = 0;
for (int i = 8; i < s.Length; i = i + 8)
{
list.Add((char)Convert.ToInt32(s.Substring(i - 8, 8), 2));
start = i;
}
var result = new String(list.ToArray());
//當(dāng)字符編碼不足8位時, 用‘艸'來標(biāo)記,然后拿出'擦‘以后的所有0,1即可
result += "艸" + s.Substring(start);
File.WriteAllText(Environment.CurrentDirectory + "http://2.txt", result);
Console.WriteLine("壓縮完畢!");
Console.Read();
//解碼
var str = File.ReadAllText(Environment.CurrentDirectory + "http://2.txt");
sb.Clear();
for (int i = 0; i < str.Length; i++)
{
int ua = (int)str[i];
//說明已經(jīng)取完畢了 用'艸'來做標(biāo)記
if (ua == 33401)
sb.Append(str.Substring(i));
else
sb.Append(Convert.ToString(ua, 2).PadLeft(8, '0'));
}
var sss = huffman.Decode(sb.ToString());
Console.Read();
}
}
public class Huffman
{
#region 赫夫曼節(jié)點
/// <summary>
/// 赫夫曼節(jié)點
/// </summary>
public class Node
{
/// <summary>
/// 左孩子
/// </summary>
public Node left;
/// <summary>
/// 右孩子
/// </summary>
public Node right;
/// <summary>
/// 父節(jié)點
/// </summary>
public Node parent;
/// <summary>
/// 節(jié)點字符
/// </summary>
public char c;
/// <summary>
/// 節(jié)點權(quán)重
/// </summary>
public int weight;
//赫夫曼“0"or“1"
public char huffmancode;
/// <summary>
/// 標(biāo)記是否為葉子節(jié)點
/// </summary>
public bool isLeaf;
}
#endregion
PriorityQueue<Node> queue = new PriorityQueue<Node>();
/// <summary>
/// 編碼對應(yīng)表(加速用)
/// </summary>
Dictionary<char, string> huffmanEncode = new Dictionary<char, string>();
/// <summary>
/// 解碼對應(yīng)表(加速用)
/// </summary>
Dictionary<string, char> huffmanDecode = new Dictionary<string, char>();
/// <summary>
/// 明文
/// </summary>
string word = string.Empty;
public Node root = new Node();
public Huffman(string str)
{
this.word = str;
Dictionary<char, int> dic = new Dictionary<char, int>();
foreach (var s in str)
{
if (dic.ContainsKey(s))
dic[s] += 1;
else
dic[s] = 1;
}
foreach (var item in dic.Keys)
{
var node = new Node()
{
c = item,
weight = dic[item]
};
//入隊
queue.Eequeue(node, dic[item]);
}
}
#region 構(gòu)建赫夫曼樹
/// <summary>
/// 構(gòu)建赫夫曼樹
/// </summary>
public void Build()
{
//構(gòu)建
while (queue.Count() > 0)
{
//如果只有一個節(jié)點,則說明已經(jīng)到根節(jié)點了
if (queue.Count() == 1)
{
root = queue.Dequeue().t;
break;
}
//節(jié)點1
var node1 = queue.Dequeue();
//節(jié)點2
var node2 = queue.Dequeue();
//標(biāo)記左孩子
node1.t.huffmancode = '0';
//標(biāo)記為右孩子
node2.t.huffmancode = '1';
//判斷當(dāng)前節(jié)點是否為葉子節(jié)點,hufuman無度為1點節(jié)點(方便計算huffman編碼)
if (node1.t.left == null)
node1.t.isLeaf = true;
if (node2.t.left == null)
node2.t.isLeaf = true;
//父節(jié)點
root = new Node();
root.left = node1.t;
root.right = node2.t;
root.weight = node1.t.weight + node2.t.weight;
//當(dāng)前節(jié)點為根節(jié)點
node1.t.parent = node2.t.parent = root;
//將當(dāng)前節(jié)點的父節(jié)點入隊列
queue.Eequeue(root, root.weight);
}
//深度優(yōu)先統(tǒng)計各個字符的編碼
DFS(root);
}
#endregion
#region 赫夫曼編碼
/// <summary>
/// 赫夫曼編碼
/// </summary>
/// <returns></returns>
public string Encode()
{
StringBuilder sb = new StringBuilder();
foreach (var item in word)
{
sb.Append(huffmanEncode[item]);
}
return sb.ToString();
}
#endregion
#region 赫夫曼解碼
/// <summary>
/// 赫夫曼解碼
/// </summary>
/// <returns></returns>
public string Decode(string str)
{
StringBuilder decode = new StringBuilder();
string temp = string.Empty;
for (int i = 0; i < str.Length; i++)
{
temp += str[i].ToString();
//如果包含 O(N)時間
if (huffmanDecode.ContainsKey(temp))
{
decode.Append(huffmanDecode[temp]);
temp = string.Empty;
}
}
return decode.ToString();
}
#endregion
#region 深度優(yōu)先遍歷子節(jié)點,統(tǒng)計各個節(jié)點的赫夫曼編碼
/// <summary>
/// 深度優(yōu)先遍歷子節(jié)點,統(tǒng)計各個節(jié)點的赫夫曼編碼
/// </summary>
/// <returns></returns>
public void DFS(Node node)
{
if (node == null)
return;
//遍歷左子樹
DFS(node.left);
//遍歷右子樹
DFS(node.right);
//如果當(dāng)前葉節(jié)點
if (node.isLeaf)
{
string code = string.Empty;
var temp = node;
//回溯的找父親節(jié)點的huffmancode LgN 的時間
while (temp.parent != null)
{
//注意,這里最后形成的 “反過來的編碼”
code += temp.huffmancode;
temp = temp.parent;
}
var codetemp = new String(code.Reverse().ToArray());
huffmanEncode.Add(node.c, codetemp);
huffmanDecode.Add(codetemp, node.c);
}
}
#endregion
}
}
小根堆:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.IO;
namespace ConsoleApplication2
{
public class PriorityQueue<T> where T : class
{
/// <summary>
/// 定義一個數(shù)組來存放節(jié)點
/// </summary>
private List<HeapNode> nodeList = new List<HeapNode>();
#region 堆節(jié)點定義
/// <summary>
/// 堆節(jié)點定義
/// </summary>
public class HeapNode
{
/// <summary>
/// 實體數(shù)據(jù)
/// </summary>
public T t { get; set; }
/// <summary>
/// 優(yōu)先級別 1-10個級別 (優(yōu)先級別遞增)
/// </summary>
public int level { get; set; }
public HeapNode(T t, int level)
{
this.t = t;
this.level = level;
}
public HeapNode() { }
}
#endregion
#region 添加操作
/// <summary>
/// 添加操作
/// </summary>
public void Eequeue(T t, int level = 1)
{
//將當(dāng)前節(jié)點追加到堆尾
nodeList.Add(new HeapNode(t, level));
//如果只有一個節(jié)點,則不需要進(jìn)行篩操作
if (nodeList.Count == 1)
return;
//獲取最后一個非葉子節(jié)點
int parent = nodeList.Count / 2 - 1;
//堆調(diào)整
UpHeapAdjust(nodeList, parent);
}
#endregion
#region 對堆進(jìn)行上濾操作,使得滿足堆性質(zhì)
/// <summary>
/// 對堆進(jìn)行上濾操作,使得滿足堆性質(zhì)
/// </summary>
/// <param name="nodeList"></param>
/// <param name="index">非葉子節(jié)點的之后指針(這里要注意:我們
/// 的篩操作時針對非葉節(jié)點的)
/// </param>
public void UpHeapAdjust(List<HeapNode> nodeList, int parent)
{
while (parent >= 0)
{
//當(dāng)前index節(jié)點的左孩子
var left = 2 * parent + 1;
//當(dāng)前index節(jié)點的右孩子
var right = left + 1;
//parent子節(jié)點中最大的孩子節(jié)點,方便于parent進(jìn)行比較
//默認(rèn)為left節(jié)點
var min = left;
//判斷當(dāng)前節(jié)點是否有右孩子
if (right < nodeList.Count)
{
//判斷parent要比較的最大子節(jié)點
min = nodeList[left].level < nodeList[right].level ? left : right;
}
//如果parent節(jié)點大于它的某個子節(jié)點的話,此時篩操作
if (nodeList[parent].level > nodeList[min].level)
{
//子節(jié)點和父節(jié)點進(jìn)行交換操作
var temp = nodeList[parent];
nodeList[parent] = nodeList[min];
nodeList[min] = temp;
//繼續(xù)進(jìn)行更上一層的過濾
parent = (int)Math.Ceiling(parent / 2d) - 1;
}
else
{
break;
}
}
}
#endregion
#region 優(yōu)先隊列的出隊操作
/// <summary>
/// 優(yōu)先隊列的出隊操作
/// </summary>
/// <returns></returns>
public HeapNode Dequeue()
{
if (nodeList.Count == 0)
return null;
//出隊列操作,彈出數(shù)據(jù)頭元素
var pop = nodeList[0];
//用尾元素填充頭元素
nodeList[0] = nodeList[nodeList.Count - 1];
//刪除尾節(jié)點
nodeList.RemoveAt(nodeList.Count - 1);
//然后從根節(jié)點下濾堆
DownHeapAdjust(nodeList, 0);
return pop;
}
#endregion
#region 對堆進(jìn)行下濾操作,使得滿足堆性質(zhì)
/// <summary>
/// 對堆進(jìn)行下濾操作,使得滿足堆性質(zhì)
/// </summary>
/// <param name="nodeList"></param>
/// <param name="index">非葉子節(jié)點的之后指針(這里要注意:我們
/// 的篩操作時針對非葉節(jié)點的)
/// </param>
public void DownHeapAdjust(List<HeapNode> nodeList, int parent)
{
while (2 * parent + 1 < nodeList.Count)
{
//當(dāng)前index節(jié)點的左孩子
var left = 2 * parent + 1;
//當(dāng)前index節(jié)點的右孩子
var right = left + 1;
//parent子節(jié)點中最大的孩子節(jié)點,方便于parent進(jìn)行比較
//默認(rèn)為left節(jié)點
var min = left;
//判斷當(dāng)前節(jié)點是否有右孩子
if (right < nodeList.Count)
{
//判斷parent要比較的最大子節(jié)點
min = nodeList[left].level < nodeList[right].level ? left : right;
}
//如果parent節(jié)點小于它的某個子節(jié)點的話,此時篩操作
if (nodeList[parent].level > nodeList[min].level)
{
//子節(jié)點和父節(jié)點進(jìn)行交換操作
var temp = nodeList[parent];
nodeList[parent] = nodeList[min];
nodeList[min] = temp;
//繼續(xù)進(jìn)行更下一層的過濾
parent = min;
}
else
{
break;
}
}
}
#endregion
#region 獲取元素并下降到指定的level級別
/// <summary>
/// 獲取元素并下降到指定的level級別
/// </summary>
/// <returns></returns>
public HeapNode GetAndDownPriority(int level)
{
if (nodeList.Count == 0)
return null;
//獲取頭元素
var pop = nodeList[0];
//設(shè)置指定優(yōu)先級(如果為 MinValue 則為 -- 操作)
nodeList[0].level = level == int.MinValue ? --nodeList[0].level : level;
//下濾堆
DownHeapAdjust(nodeList, 0);
return nodeList[0];
}
#endregion
#region 獲取元素并下降優(yōu)先級
/// <summary>
/// 獲取元素并下降優(yōu)先級
/// </summary>
/// <returns></returns>
public HeapNode GetAndDownPriority()
{
//下降一個優(yōu)先級
return GetAndDownPriority(int.MinValue);
}
#endregion
#region 返回當(dāng)前優(yōu)先隊列中的元素個數(shù)
/// <summary>
/// 返回當(dāng)前優(yōu)先隊列中的元素個數(shù)
/// </summary>
/// <returns></returns>
public int Count()
{
return nodeList.Count;
}
#endregion
}
}
到此這篇關(guān)于基于C#實現(xiàn)哈夫曼樹算法的文章就介紹到這了,更多相關(guān)C#哈夫曼樹算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#使用ADO.Net連接數(shù)據(jù)庫與DbProviderFactory實現(xiàn)多數(shù)據(jù)庫訪問
這篇文章介紹了C#使用ADO.Net連接數(shù)據(jù)庫與DbProviderFactory實現(xiàn)多數(shù)據(jù)庫訪問的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05
c#中WinForm使用OpencvSharp4實現(xiàn)簡易抓邊
本文主要介紹了c#中WinForm使用OpencvSharp4實現(xiàn)簡易抓邊,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
C#連接ClickHouse數(shù)據(jù)庫的步驟指南
在 C# 中連接 ClickHouse 數(shù)據(jù)庫,您可以使用 ClickHouse.Client 庫,這個庫提供了對 ClickHouse 數(shù)據(jù)庫的高效訪問,以下是詳細(xì)的步驟指南,幫助您在 C# 項目中連接和操作 ClickHouse 數(shù)據(jù)庫,需要的朋友可以參考下2024-12-12
c# WinForm 窗體之間傳值的幾種方式(小結(jié))
這篇文章主要介紹了WinForm 窗體之間傳值的幾種方式(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09

