Структуры данных link Узел дерева link Каждый узел дерева Хаффмана содержит:
struct Node {
char symbol; // символ, хранящийся в узле; '\0' для внутренних узлов
long long freq; // частота (вес) этого узла/поддерева
shared_ptr<Node> left; // указатель на левого потомка (бит '0')
shared_ptr<Node> right; // указатель на правого потомка (бит '1')
// Конструктор для листьев
Node(const char s, const long long f)
: symbol(s), freq(f), left(nullptr), right(nullptr) {
}
// Конструктор для внутренних узлов: объединяет два поддерева
Node(const shared_ptr<Node> &l, const shared_ptr<Node> &r)
: symbol('\0'), freq(l->freq + r->freq), left(l), right(r) {
}
};
// Узел дерева Хаффмана
static class Node implements Comparable<Node> {
char symbol; // символ, хранящийся в узле; '\0' для внутренних узлов
int freq; // частота (вес) этого узла/поддерева
Node left; // указатель на левого потомка (бит '0')
Node right; // указатель на правого потомка (бит '1')
// Конструктор для листьев
Node(char symbol, int freq) {
this.symbol = symbol;
this.freq = freq;
this.left = this.right = null;
}
// Конструктор для внутренних узлов: объединяет два поддерева
Node(Node left, Node right) {
this.symbol = '\0';
this.freq = left.freq + right.freq;
this.left = left;
this.right = right;
}
// Сравнение узлов по частоте
@Override
public int compareTo(Node other) {
return Integer.compare(this.freq, other.freq);
}
}
Листья содержат конкретный символ и его исходную частоту.Внутренние узлы имеют symbol == '\0'
и freq = left->freq + right->freq
.
Порядок в очереди с приоритетом link
struct Compare {
bool operator()(const shared_ptr<Node> &a, const shared_ptr<Node> &b) const {
// Возвращает true, если a.freq > b.freq, чтобы меньшие частоты обрабатывались раньше
return a->freq > b->freq;
}
};
static class Node implements Comparable<Node> {
@Override
public int compareTo(Node other) {
return Integer.compare(this.freq, other.freq);
}
}
Построение дерева Хаффмана link
shared_ptr<Node> buildHuffmanTree(const string &data) {
// 1) Подсчёт частот символов
unordered_map<char, long long> freq;
for (const auto &c: data) {
freq[c]++;
}
// 2) Инициализация мин-кучи деревьев-одиночек
priority_queue<shared_ptr<Node>, vector<shared_ptr<Node> >, Compare> pq;
for (const auto &p: freq) {
pq.push(make_shared<Node>(p.first, p.second));
}
// 3) Объединяем, пока в куче не останется одно дерево
while (pq.size() > 1) {
// Извлекаем два узла с наименьшими частотами
auto left = pq.top();
pq.pop();
auto right = pq.top();
pq.pop();
// Создаём новый узел-родитель с суммарной частотой
pq.push(make_shared<Node>(left, right));
}
return pq.top();
}
public static Node buildHuffmanTree(String data) {
// 1) Подсчёт частот символов
Map<Character, Integer> freq = new HashMap<>();
for (char ch : data.toCharArray()) {
freq.put(ch, freq.getOrDefault(ch, 0) + 1);
}
// 2) Инициализация мин-кучи деревьев-одиночек
PriorityQueue<Node> pq = new PriorityQueue<>();
for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
pq.add(new Node(entry.getKey(), entry.getValue()));
}
// 3) Объединяем, пока в куче не останется одно дерево
while (pq.size() > 1) {
// Извлекаем два узла с наименьшими частотами
Node left = pq.poll();
Node right = pq.poll();
// Создаём новый узел-родитель с суммарной частотой
pq.add(new Node(left, right));
}
return pq.poll();
}
Step 1 collects frequencies in a hash map.Step 2 seeds the priority queue with leaf nodes.Step 3 repeatedly extracts two smallest‐weight nodes, merges them under a new internal node, and reinserts—ultimately yielding a single root.
Генерация таблицы кодов link После постройки дерева, обход в глубину присваивает каждому символу строку битов:
void generateCodes(const shared_ptr<Node> &node,
const string &prefix,
map<char, string> &codebook) {
if (!node) return;
if (node->symbol != '\0') {
// Лист: присваиваем накопленный префикс (или "0", если один символ)
codebook[node->symbol] = prefix.empty() ? "0" : prefix;
} else {
// Внутренний узел: идём влево (добавляем '0') и вправо (добавляем '1')
generateCodes(node->left, prefix + "0", codebook);
generateCodes(node->right, prefix + "1", codebook);
}
}
public static void generateCodes(Node node, String prefix, Map<Character, String> codeboo) {
if (node == null) return;
if (node.symbol != '\0') {
// Лист: присваиваем накопленный префикс (или "0", если один символ)
codebook.put(node.symbol, prefix.isEmpty() ? "0" : prefix);
} else {
// Внутренний узел: идём влево (добавляем '0') и вправо (добавляем '1')
generateCodes(node.left, prefix + '0', codebook);
generateCodes(node.right, prefix + '1', codebook);
}
}
В листьях сохраняем накопленную битовую строку. В внутренних узлах рекурсивно продолжаем обход, наращивая префикс.
Сборка итоговой программы link
int main() {
// Пример строки для кодирования
string text = "AAABBBBBAAABBDDCCCCCDDDDBBAAAAAAAAAAACCCCAAAEEEEAAADDDAADAAAABBAADDACCACAAAAEEEBBEEAAAADDDDAA";
// Построение дерева Хаффмана
auto root = buildHuffmanTree(text);
// Генерация таблицы кодов
map<char, string> codes;
generateCodes(root, "", codes);
// Вывод результатов
cout << "Codewords:\n";
for (auto &p: codes) {
cout << "'" << p.first << "': " << p.second << "\n";
}
return 0;
}
public static void main(String[] args) {
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
// Пример строки для кодирования
String text = "AAABBBBBAAABBDDCCCCCDDDDBBAAAAAAAAAAACCCCAAAEEEEAAADDDAADAAAABBAADDACCACAAAAEEEBBEEAAAADDDDAA";
// Построение дерева Хаффмана
Node root = buildHuffmanTree(text);
// Генерация таблицы кодов
Map<Character, String> codes = new HashMap<>();
generateCodes(root, "", codes);
// Вывод результатов
System.out.println("Codewords:");
for (Map.Entry<Character, String> entry : codes.entrySet()) {
System.out.println("'" + entry.getKey() + "': " + entry.getValue());
}
}
Вывод должен быть примерно таким:
Codewords:
'A': 0
'B': 110
'C': 101
'D': 111
'E': 100
Временная сложность link Общая сложность алгоритма Хаффмана складывается из этапов:
Подсчёт частот link Проход по входной строке длины $N$ за $O(N)$.
Построение кучи link Вставка $n$ уникальных символов в кучу за $O(n \log n)$.
Слияние деревьев link Выполняется $n−1$ шаг, каждый — три операции кучи по $O(logn)$, итого $O(nlogn)$.
Извлечение кодов link Обход всех узлов (количество узлов $\approx 2n−1$) за $O(n)$.
Итоговая оценка link $$
O(N)+O(n \log n)+O(n \log n)+O(n)=O(N+n \log n)
$$
Если алфавит фиксирован (ASCII, Unicode), $n$ считается константой, и сложность упрощается до $O(N)$. В худшем случае, когда $n=N$, получаем $O(N \log N)$. Таким образом алгоритм масштабируется почти линейно и остаётся эффективным для больших данных с умеренно большим алфавитом.
Код на GitHub link Код реализации алгоритма Хаффмана
Last updated 09 мая 2025, 23:43 +0500
. history