Skip to content

Compression: Huffman Coding #1513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions Compression/Huffman.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Huffman Coding is a lossless data compression algorithm that uses variable-length codes to represent characters.
*
* The algorithm works by assigning shorter codes to characters that occur more frequently. This results in a compressed representation of the data.
*
* Huffman Coding is widely used in a variety of applications, including file compression, data transmission, and image processing.
*
* More information on Huffman Coding can be found here: https://en.wikipedia.org/wiki/Huffman_coding
*/

/**
* Builds a frequency table from a string.
* @example
* buildFrequencyTable('this is an example for huffman encoding')
* returns { ' ': 6, a: 2, c: 1, d: 1, e: 4, f: 3, g: 1, h: 2, i: 3, l: 1, m: 1, n: 4, o: 1, p: 1, r: 1, s: 2, t: 2, u: 1, x: 1 }
* @param {string} data - The string to build the frequency table from.
* @returns {Object} - The frequency table.
*/
function buildFrequencyTable(data) {
const freqTable = {}

for (const char of data) {
freqTable[char] = (freqTable[char] || 0) + 1
}

return freqTable
}

/**
* A Huffman Node is a node in a Huffman tree.
* @class HuffmanNode
* @property {string} char - The character represented by the node.
* @property {number} freq - The frequency of the character.
* @property {HuffmanNode} left - The left child of the node.
* @property {HuffmanNode} right - The right child of the node.
*/
class HuffmanNode {
constructor(char, freq) {
this.char = char
this.freq = freq
this.left = null
this.right = null
}
}

/**
* Builds a Huffman tree from a frequency table.
* @param {Object} freqTable - The frequency table to use for building the tree.
* @returns {HuffmanNode} - The root node of the Huffman tree.
*/
function buildHuffmanTree(freqTable) {
const nodes = Object.keys(freqTable).map(
(char) => new HuffmanNode(char, freqTable[char])
)

while (nodes.length > 1) {
nodes.sort((a, b) => a.freq - b.freq)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you sort in every iteration?

Copy link
Contributor Author

@aladin002dz aladin002dz Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because when pushing a new node, the order may change.

Copy link
Collaborator

@appgurueu appgurueu Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more appropriate to use a max heap here (our existing implementations should work, you just need to import and use them), given that you always extract the most frequent ones and only push new ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would add a lot more complexity, wouldn't it?

Copy link
Collaborator

@appgurueu appgurueu Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the API of our heap should be pretty straightforward to use. A heap is pretty much the data structure for this use case; it would significantly help the time complexity. If you want me to, I can make the necessary changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to implement it myself, then I'll need your valuable feedback and guidance.

const left = nodes.shift()
const right = nodes.shift()
const parent = new HuffmanNode(null, left.freq + right.freq)
parent.left = left
parent.right = right
nodes.push(parent)
}

return nodes[0]
}

/**
* Builds a Huffman code table from a Huffman tree.
* @param {HuffmanNode} root - The root node of the Huffman tree.
* @param {string} [prefix=''] - The prefix to use for the Huffman codes.
* @param {Object} [codes={}] - The Huffman code table.
* @returns {Object} - The Huffman code table.
*/
function buildHuffmanCodes(root, prefix = '', codes = {}) {
if (root) {
if (root.char) {
codes[root.char] = prefix
}
buildHuffmanCodes(root.left, prefix + '0', codes)
buildHuffmanCodes(root.right, prefix + '1', codes)
}
return codes
}

/**
* Encodes a string using Huffman Coding.
* @param {string} data - The string to encode.
* @param {Object} freqTable - The frequency table to use for encoding.
* @returns {string} - The encoded string.
*/
function encodeHuffman(data, freqTable) {
const root = buildHuffmanTree(freqTable)
const codes = buildHuffmanCodes(root)

let encodedData = ''
for (let char of data) {
encodedData += codes[char]
}

return encodedData
}

/**
* Decodes a string using Huffman Coding.
* @param {string} encodedData - The string to decode.
* @param {HuffmanNode} root - The root node of the Huffman tree.
* @returns {string} - The decoded string.
*/
function decodeHuffman(encodedData, root) {
let decodedData = ''
let currentNode = root
for (let bit of encodedData) {
if (bit === '0') {
currentNode = currentNode.left
} else {
currentNode = currentNode.right
}

if (currentNode.char) {
decodedData += currentNode.char
currentNode = root
}
}

return decodedData
}

// Example usage
const data = 'this is an example for huffman encoding'
const freqTable = buildFrequencyTable(data)
const root = buildHuffmanTree(freqTable)
const encodedData = encodeHuffman(data, freqTable)
console.log('Encoded Data:', encodedData)

const decodedData = decodeHuffman(encodedData, root)
console.log('Decoded Data:', decodedData)

export {
buildHuffmanCodes,
buildHuffmanTree,
encodeHuffman,
decodeHuffman,
buildFrequencyTable
}
33 changes: 33 additions & 0 deletions Compression/test/Huffman.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
buildHuffmanCodes,
buildHuffmanTree,
encodeHuffman,
decodeHuffman,
buildFrequencyTable
} from '../Huffman'

describe('Huffman Coding', () => {
let data, freqTable, root

beforeEach(() => {
data = 'this is an example for huffman encoding'
freqTable = buildFrequencyTable(data)
root = buildHuffmanTree(freqTable)
})

it('should encode and decode a string correctly', () => {
const encodedData = encodeHuffman(data, freqTable)
const decodedData = decodeHuffman(encodedData, root)

expect(decodedData).toEqual(data)
})

it('should build Huffman codes correctly', () => {
const codes = buildHuffmanCodes(root)

expect(codes['t']).toEqual('01010')
expect(codes['h']).toEqual('11111')
expect(codes['i']).toEqual('1001')
expect(codes['s']).toEqual('0010')
})
})
2 changes: 1 addition & 1 deletion Data-Structures/Heap/test/BinaryHeap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('BinaryHeap', () => {
it('should handle insertion of duplicate values', () => {
// Check if the heap handles duplicate values correctly
minHeap.insert(2)
console.log(minHeap.heap);
console.log(minHeap.heap)
expect(minHeap.heap).toEqual([1, 3, 2, 4, 8, 6, 2])
})

Expand Down
40 changes: 20 additions & 20 deletions Data-Structures/Stack/EvaluateExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,53 @@
* @returns {number|null} - Result of the expression evaluation, or null if the expression is invalid.
*/
function evaluatePostfixExpression(expression) {
const stack = [];
const stack = []

// Helper function to perform an operation and push the result to the stack. Returns success.
function performOperation(operator) {
const rightOp = stack.pop(); // Right operand is the top of the stack
const leftOp = stack.pop(); // Left operand is the next item on the stack
const rightOp = stack.pop() // Right operand is the top of the stack
const leftOp = stack.pop() // Left operand is the next item on the stack

if (leftOp === undefined || rightOp === undefined) {
return false; // Invalid expression
return false // Invalid expression
}
switch (operator) {
case '+':
stack.push(leftOp + rightOp);
break;
stack.push(leftOp + rightOp)
break
case '-':
stack.push(leftOp - rightOp);
break;
stack.push(leftOp - rightOp)
break
case '*':
stack.push(leftOp * rightOp);
break;
stack.push(leftOp * rightOp)
break
case '/':
if (rightOp === 0) {
return false;
return false
}
stack.push(leftOp / rightOp);
break;
stack.push(leftOp / rightOp)
break
default:
return false; // Unknown operator
return false // Unknown operator
}
return true;
return true
}

const tokens = expression.split(/\s+/);
const tokens = expression.split(/\s+/)

for (const token of tokens) {
if (!isNaN(parseFloat(token))) {
// If the token is a number, push it to the stack
stack.push(parseFloat(token));
stack.push(parseFloat(token))
} else {
// If the token is an operator, perform the operation
if (!performOperation(token)) {
return null; // Invalid expression
return null // Invalid expression
}
}
}

return (stack.length === 1) ? stack[0] : null;
return stack.length === 1 ? stack[0] : null
}

export { evaluatePostfixExpression };
export { evaluatePostfixExpression }
29 changes: 14 additions & 15 deletions Data-Structures/Stack/test/EvaluateExpression.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { evaluatePostfixExpression } from '../EvaluateExpression.js';
import { evaluatePostfixExpression } from '../EvaluateExpression.js'

describe('evaluatePostfixExpression', () => {
it('should evaluate a valid expression', () => {
const expression = '3 4 * 2 / 5 +'; // (3 * 4) / 2 + 5 = 11
const result = evaluatePostfixExpression(expression);
expect(result).toBe(11);
});
const expression = '3 4 * 2 / 5 +' // (3 * 4) / 2 + 5 = 11
const result = evaluatePostfixExpression(expression)
expect(result).toBe(11)
})

it('should handle division by zero', () => {
const expression = '3 0 /'; // Division by zero
const result = evaluatePostfixExpression(expression);
expect(result).toBe(null);
});
const expression = '3 0 /' // Division by zero
const result = evaluatePostfixExpression(expression)
expect(result).toBe(null)
})

it('should handle an invalid expression', () => {
const expression = '3 * 4 2 / +'; // Invalid expression
const result = evaluatePostfixExpression(expression);
expect(result).toBe(null);
});

});
const expression = '3 * 4 2 / +' // Invalid expression
const result = evaluatePostfixExpression(expression)
expect(result).toBe(null)
})
})
9 changes: 3 additions & 6 deletions Maths/test/Determinant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ describe('Determinant', () => {
'Square matrix is required.'
],
[[1, 3, 2, [5, 8, 6], 3], 'Input is not a valid 2D matrix.']
])(
'Should return the error message.',
(matrix, expected) => {
expect(() => determinant(matrix)).toThrowError(expected)
}
)
])('Should return the error message.', (matrix, expected) => {
expect(() => determinant(matrix)).toThrowError(expected)
})
})