diff --git a/src/chapter1/ch1-q1.js b/src/chapter1/ch1-q1.js index 63a2dcc..e5fede8 100644 --- a/src/chapter1/ch1-q1.js +++ b/src/chapter1/ch1-q1.js @@ -11,14 +11,15 @@ * @return {boolean} True if unique characters, otherwise false */ export function hasUniqueCharactersSet(str) { - let chars = new Set(); + const chars = new Set(); - for (let i = 0; i < str.length; ++i) { - if (chars.has(str[i])) { + for (const ch of str) { + if (chars.has(ch)) { return false; } - chars.add(str[i]); + chars.add(ch); } + return true; } diff --git a/src/chapter1/ch1-q2.js b/src/chapter1/ch1-q2.js index 30bdc4f..54ff5dc 100644 --- a/src/chapter1/ch1-q2.js +++ b/src/chapter1/ch1-q2.js @@ -20,20 +20,17 @@ export function isPermutationMap(str1, str2) { let chars = new Map(); - for (let i = 0; i < str1.length; ++i) { - chars.set(str1[i], chars.get(str1[i]) + 1 || 1); // increment or set to 1 + for (const ch of str1) { + chars.set(ch, chars.get(ch) + 1 || 1); // increment or set to 1 } - for (let i = 0; i < str2.length; ++i) { - let count = chars.get(str2[i]); - if (!count) { - return false; - } - if (count === 1) { - chars.delete(str2[i]); - } - else { - chars.set(str2[i], count - 1); + for (const ch of str2) { + if (!chars.has(ch)) return false; // shortcircuit if a char doesn't exist in both strings + const nextCount = chars.get(ch) - 1; + if (nextCount === 0) { + chars.delete(ch); + } else { + chars.set(ch, nextCount); } } @@ -57,8 +54,13 @@ export function isPermutationSorted(str1, str2) { return false; } // sort string using quicksort - str1.sort(); - str2.sort(); + const sorted1 = sortString(str1); + const sorted2 = sortString(str2); + + return sorted1.every((v, i) => v === sorted2[i]); +} - return str1.every((v, i) => v === str2[i]); +// for the purposes of the question, ignore array creation and assume O(1) space sort as with arrays +function sortString(str) { + return Array.from(str).sort(); } diff --git a/src/chapter1/ch1-q2.spec.js b/src/chapter1/ch1-q2.spec.js index 68aefbb..2ebb6c9 100644 --- a/src/chapter1/ch1-q2.spec.js +++ b/src/chapter1/ch1-q2.spec.js @@ -1,10 +1,11 @@ -import { expect } from 'chai'; import * as funcs from './ch1-q2'; +import { expect } from 'chai'; + for (let key in funcs) { let func = funcs[key]; - describe('ch1-q2: ' + key, function() { + describe('ch1-q2: ' + key, function () { [ ['abcdefghi', 'ihgfedcba'], @@ -13,8 +14,8 @@ for (let key in funcs) { ['icarraci', 'carcarii'] ].forEach(args => { - it(`returns true for strings that are permutations: '${args[0]}' & '${args[1]}'`, function() { - expect(func(args[0].split(''), args[1].split(''))).to.be.true; + it(`returns true for strings that are permutations: '${args[0]}' & '${args[1]}'`, function () { + expect(func(args[0], args[1])).to.be.true; }); }); @@ -26,8 +27,8 @@ for (let key in funcs) { ['45678', '1239'] ].forEach(args => { - it(`returns false for strings that are not permutations: '${args[0]}' & '${args[1]}'`, function() { - expect(func(args[0].split(''), args[1].split(''))).to.be.false; + it(`returns false for strings that are not permutations: '${args[0]}' & '${args[1]}'`, function () { + expect(func(args[0], args[1])).to.be.false; }); }); diff --git a/src/chapter1/ch1-q3.js b/src/chapter1/ch1-q3.js index 71e120b..d9abcd5 100644 --- a/src/chapter1/ch1-q3.js +++ b/src/chapter1/ch1-q3.js @@ -17,8 +17,8 @@ export function encodeSpaces(url) { } let spaceCount = 0; - for (let i = 0; i < url.length; ++i) { - if (url[i] === ' ') { + for (let ch of url) { + if (ch === ' ') { ++spaceCount; } } diff --git a/src/chapter1/ch1-q4.js b/src/chapter1/ch1-q4.js index ac1b801..15cb07b 100644 --- a/src/chapter1/ch1-q4.js +++ b/src/chapter1/ch1-q4.js @@ -17,8 +17,8 @@ export function isPalindromePermutationsSet(str) { return false; } - let chars = new Set(); - for (let char of str) { + const chars = new Set(); + for (const char of str) { if (char !== ' ') { // ignore spaces if (chars.has(char)) { chars.delete(char); diff --git a/src/chapter1/ch1-q6.js b/src/chapter1/ch1-q6.js index 9520b01..001a402 100644 --- a/src/chapter1/ch1-q6.js +++ b/src/chapter1/ch1-q6.js @@ -8,15 +8,73 @@ * Time: O(N) * Additional space: O(N) * - * @param {string} str [description] - * @return {[type]} [description] + * @param {string} str The given string + * @return {string} The compressed string or the original string */ export function compressString(str) { if (!str) { return str; } - let cStr = ''; + let compressed = ''; + let countConsecutive = 0; + for (let i = 0; i < str.length; ++i) { + ++countConsecutive; + + if (i + 1 >= str.length || str[i] !== str[i + 1]) { + // JS does not have a StringBuilder/StringBuffer style class for creating strings + // string concatenation has been heavily optimised in JS implementations and + // is faster than creating a string via an array then using a .join('') at the end + compressed += str[i] + countConsecutive; + countConsecutive = 0; + } + } + + return compressed.length < str.length ? compressed : str; +} + +/** + * Takes an input string and counts contiguous sequences of the same character + * and replaces them with XC (X = count, C = character). + * + * It runs a first pass through the string to determine the compression length. + * If the compression length is not smaller than current length, we can just + * return the given string without ever creating the compressed version. + * + * N = |str| + * Time: O(N) + * Additional space: O(N) + * + * @param {string} str The given string + * @return {string} The compressed string or the original string + */ +export function compressStringOnlyIfNecessary(str) { + if (!str) { + return str; + } + + const compressedLength = getCompressionLength(str); + return compressedLength < str.length ? + compressStringOnlyIfNecessaryHelper(str) : + str; +} + +function getCompressionLength(str) { + let compressedLength = 0, + countConsecutive = 0; + for (let i = 0; i < str.length; ++i) { + ++countConsecutive; + + if (i + 1 >= str.length || str[i] !== str[i + 1]) { + compressedLength += 1 + String(countConsecutive).length; + countConsecutive = 0; + } + } + return compressedLength; +} + +function compressStringOnlyIfNecessaryHelper(str) { + let compressed = ''; for (let i = 0; i < str.length; ++i) { let char = str[i], start = i; @@ -26,8 +84,7 @@ export function compressString(str) { // JS does not have a StringBuilder/StringBuffer style class for creating strings // string concatenation has been heavily optimised in JS implementations and // is faster than creating a string via an array then using a .join('') at the end - cStr += (i - start + 1) + char; + compressed += char + (i - start + 1); } - - return cStr.length < str.length ? cStr : str; + return compressed; } diff --git a/src/chapter1/ch1-q6.spec.js b/src/chapter1/ch1-q6.spec.js index 925f43a..04993ee 100644 --- a/src/chapter1/ch1-q6.spec.js +++ b/src/chapter1/ch1-q6.spec.js @@ -1,17 +1,18 @@ -import { expect } from 'chai'; import * as funcs from './ch1-q6'; +import { expect } from 'chai'; + for (let key in funcs) { let func = funcs[key]; - describe('ch1-q6: ' + key, function() { + describe('ch1-q6: ' + key, function () { - it('returns input where null/undefined', function() { + it('returns input where null/undefined', function () { expect(func(null)).to.be.null; expect(func(undefined)).to.be.undefined; }); - it('returns input where empty string', function() { + it('returns input where empty string', function () { expect(func('')).to.equal(''); }); @@ -23,22 +24,23 @@ for (let key in funcs) { 'ababababccab' ].forEach(arg => { - it(`returns input string where compression doesn't use less space: '${arg}'`, function() { + it(`returns input string where compression doesn't use less space: '${arg}'`, function () { expect(func(arg)).to.eql(arg); }); }); [ - { arg: 'aaa', out: '3a' }, - { arg: 'bbbbbb', out: '6b' }, - { arg: 'abbbbbbc', out: '1a6b1c' }, - { arg: 'aaabccc', out: '3a1b3c' }, - { arg: 'hhellllllllooooo!', out: '2h1e8l5o1!' }, - { arg: 'woorrrllllddddd', out: '1w2o3r4l5d' } + { arg: 'aaa', out: 'a3' }, + { arg: 'bbbbbb', out: 'b6' }, + { arg: 'abbbbbbc', out: 'a1b6c1' }, + { arg: 'aaabccc', out: 'a3b1c3' }, + { arg: 'hhellllllllooooo!', out: 'h2e1l8o5!1' }, + { arg: 'woorrrllllddddd', out: 'w1o2r3l4d5' }, + { arg: 'aaaaaaaaaaabbbbbbbbbbbb', out: 'a11b12' } ].forEach(context => { - it(`returns ${context.out} with string ${context.arg}`, function() { + it(`returns ${context.out} with string ${context.arg}`, function () { expect(func(context.arg)).to.eql(context.out); }); diff --git a/src/chapter1/ch1-q8.js b/src/chapter1/ch1-q8.js index cbdbc5d..6539e3f 100644 --- a/src/chapter1/ch1-q8.js +++ b/src/chapter1/ch1-q8.js @@ -15,7 +15,7 @@ * @return {array} Matrix that has been zeroed, same object as input */ export function zeroMatrix(matrix) { - if (!matrix) { + if (!Array.isArray(matrix) || !Array.isArray(matrix[0])) { throw new Error('invalid matrix'); } if (matrix.length === 0) { @@ -47,3 +47,91 @@ export function zeroMatrix(matrix) { return matrix; } + +/** + * Instead of using two extra arrays as storage for rows and columns that will be + * nullified, use the first row and first column. + * + * N = matrix Y dimension + * M = matrix X dimension + * Time: O(N * M) + * Additional space: O(1) + * + * @param {array} matrix Matrix to be zeroed in-place + * @return {array} Matrix that has been zeroed, same object as input + */ +export function zeroMatrixConstantSpace(matrix) { + if (!Array.isArray(matrix) || !Array.isArray(matrix[0])) { + throw new Error('invalid matrix'); + } + if (matrix.length === 0) { + return matrix; + } + + // check if first row has a zero + let firstRowHasZero = false; + for (let col = 0; col < matrix[0].length; col++) { + if (matrix[0][col] === 0) { + firstRowHasZero = true; + break; + } + } + + // check if first column has a zero + let firstColHasZero; + for (let row = 0; row < matrix.length; row++) { + if (matrix[row][0] === 0) { + firstColHasZero = true; + break; + } + } + + // check for zeros in the rest of the rows/cols and update + // first row/column storage accordingly + for (let row = 0; row < matrix.length; row++) { + for (let col = 0; col < matrix[0].length; col++) { + if (matrix[row][col] === 0) { + matrix[row][0] = 0; + matrix[0][col] = 0; + } + } + } + + // nullify rows based on the values in the first column + for (let row = 1; row < matrix.length; row++) { + if (matrix[row][0] === 0) { + nullifyRow(matrix, row); + } + } + + // nullify columns based on the values in the first row + for (let col = 1; col < matrix[0].length; col++) { + if (matrix[0][col] === 0) { + nullifyCol(matrix, col); + } + } + + // nullify first row if necessary + if (firstRowHasZero) { + nullifyRow(matrix, 0); + } + + // nullify first col if necessary + if (firstColHasZero) { + nullifyCol(matrix, 0); + } + + return matrix; +} + +function nullifyRow(matrix, row) { + for (let col = 0; col < matrix[row].length; col++) { + matrix[row][col] = 0; + } +} + +function nullifyCol(matrix, col) { + for (let row = 0; row < matrix.length; row++) { + matrix[row][col] = 0; + } +}