[Leetcode problem solving algorithm weekly] issue 1

Posted by Namadoor on Mon, 28 Oct 2019 12:18:05 +0100

First appeared in the WeChat public's front-end growth notes, written in 2019.10.28

background

This paper records the whole thinking process in the process of writing questions for reference. The main contents include:

  • Assumption of topic analysis
  • Write code validation
  • Consult others' solution
  • Thinking and summing up

Catalog

Easy

1. Sum of two numbers

Title address

Title Description

Given an integer array nums and a target value target, please find the two integers with and as the target value in the array and return their array subscripts.

You can assume that each input corresponds to only one answer. However, you cannot reuse the same elements in this array.

Example:

Given nums = [2, 7, 11, 15], target = 9

Because nums[0] + nums[1] = 2 + 7 = 9
 So return [0, 1]

Assumption of topic analysis

This question first shows that each input only corresponds to one answer, and cannot use the same elements in the array, which means that a number cannot be used twice, that is, [0,0] is unreasonable.

Seeing this question, I have several directions to try to answer:

  • Violent point, direct cycle twice, worst performance estimate
  • IndexOf, most cycles, not recommended
  • Space for time, use HashMap, reduce one cycle

Write code validation

I. violence law

Code:

// Violence point
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    for(let i = 0; i < nums.length; i++) {
        // j starting from i+1, remove some useless operations
        for(let j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] === target) {
                return [i,j];
            }
        }
    }
};

Result:

  • 29/29 cases passed (124 ms)
  • Your runtime beats 60.13 % of javascript submissions
  • Your memory usage beats 66.05 % of javascript submissions (34.5 MB)
  • Time complexity: O(n^2)

Ⅱ.IndexOf

The performance is the worst, and each judgment needs to traverse the remaining array (extremely not recommended, just one more implementation scheme)

Code:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        const remainArr = nums.slice(i + 1)
        if (remainArr.indexOf(dif) !== -1) {
            return [i, remainArr.indexOf(dif) + i + 1]
        }
    }
};

Result:

  • 29/29 cases passed (212 ms)
  • Your runtime beats 22.39 % of javascript submissions
  • Your memory usage beats 5 % of javascript submissions (49 MB)
  • Time complexity: O(n^2), factorial time complexity is O(n)

Ⅲ.HashMap

Code:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let hash = {}
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        if (hash[dif] !== undefined) {
            return [hash[dif], i]
        } else {
            hash[num] = i
        }
    }
};

Result:

  • 29/29 cases passed (60 ms)
  • Your runtime beats 98.7 % of javascript submissions
  • Your memory usage beats 19.05 % of javascript submissions (35.3 MB)
  • Time complexity: O(n)

Compared with violence method, HashMap scheme has a significant improvement in speed.

Consult others' solution

Here are two other ways. Let's try them first.

I. replace HashMap with array

Code:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let arr = []
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        if (arr[dif] !== undefined) {
            return [arr[dif], i]
        } else {
            arr[num] = i
        }
    }
};

Result:

  • 29/29 cases passed (60 ms)
  • Your runtime beats 98.7 % of javascript submissions
  • Your memory usage beats 17.89 % of javascript submissions (35.4 MB)
  • Time complexity: O(n)

There is little difference in performance with HashMap.

II. Traversing HashMap twice

Code:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let res = new Map()
    for(let i = 0; i < nums.length; i++) {
        res.set(nums[i], i)
    }
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        const idx = res.get(dif)
        if (idx !== undefined && idx !== i) {
            return [i, idx]
        }
    }
};

Result:

  • 29/29 cases passed (64 ms)
  • Your runtime beats 96.76 % of javascript submissions
  • Your memory usage beats 10.94 % of javascript submissions (35.9 MB)
  • Time complexity: O(n)

Thinking and summing up

Here I do a simple check: input [2,2,2], 4, and find that the expected output is [0,2], not [0,1], so there are several solutions above that can not actually pass. If it is to satisfy this kind of output, my recommended solution is to traverse the HashMap twice. But I personally think it is more reasonable for HashMap to traverse once.

7. Integer inversion

Title address

Title Description

Given a 32-bit signed integer, you need to reverse the number on each of the integers.

Example:

Input: 123
 Output: 321

Input: -123
 Output: -321

Input: 120
 Output: 21

Be careful:

Assuming that our environment can only store 32-bit signed integers, the value range is [− 2 ^ 31, 2 ^ 31 − 1]. Based on this assumption, if the integer overflows after inversion, 0 will be returned.

Assumption of topic analysis

There are several points to pay attention to in terms of the stem of the question:

  • Overflow return 0
  • 0 needs to be removed to get the natural number

Here I have two ideas:

  • Using array reverse to reverse and then do natural number conversion
  • Take the remainder and get the number on each, then add, sign and overflow.

Write code validation

I. array inversion

Code:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    const isNegative = x < 0
    const rev = Number(Math.abs(x).toString().split('').reverse().join(''))
    if (isNegative && -rev >= -Math.pow(2, 31)) {
        return -rev
    } else if (!isNegative && rev <= Math.pow(2,31) - 1) {
        return rev
    } else {
        return 0
    }
};

Result:

  • 1032/1032 cases passed (96 ms)
  • Your runtime beats 73.33 % of javascript submissions
  • Your memory usage beats 28.03 % of javascript submissions (35.9 MB)
  • Time complexity: O(1)

II. Surplus

Code:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    const isNegative = x < 0
    let res = 0
    while(x !== 0) {
        res = res * 10 + x % 10
        x = parseInt(x / 10)
    }
    if ((isNegative && res >= -Math.pow(2, 31)) || (!isNegative && res <= Math.pow(2,31) - 1)) {
        return res
    } else {
        return 0
    }
};

Result:

  • 1032/1032 cases passed (80 ms)
  • Your runtime beats 96.71 % of javascript submissions
  • Your memory usage beats 56.8 % of javascript submissions (35.7 MB)
  • Time complexity: O(log10(n))

It is found that the performance of using the way of remainder is better than that of array inversion.

Consult others' solution

There are basically two ways of thinking, and no solution with different directions has been found.

Thinking and summing up

It is found that there are still some inconsistencies to be considered. For example, some special values can be returned directly to avoid operation. Here I also do a simple verification: input - 0, and find that the expected output is 0 instead of - 0. Therefore, my code here is optimized as follows:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    if (x === 0) return 0
    function isOverflow (num) {
        return num < -Math.pow(2, 31) || (num > Math.pow(2,31) - 1)
    }
    if (isOverflow(x)) return 0
    let res = 0
    while(x !== 0) {
        res = res * 10 + x % 10
        x = parseInt(x / 10)
    }
    return isOverflow(res) ? 0 : res
};

9. palindrome number

Title address

Title Description

Determines whether an integer is a palindrome. Palindromes are integers that are read in the same positive (left to right) and reverse (right to left) order.

Example:

Input: 121
 Output: true

Input: -121
 Output: false
 Explanation: read from left to right, it is - 121. Read right to left, 121 -. So it's not a palindrome number.

Input: 10
 Output: false
 Explanation: read from right to left, it's 01. So it's not a palindrome number.

Advance:

Can you solve this problem without converting integers to strings?

Assumption of topic analysis

The first feeling of this question is similar to the expansion of the integer inversion in the previous question, so we start from two directions:

  • Integer to string
  • Take the rest and judge one by one

In the process of writing, some operations need to be taken out: exclude < 0 and - 0, because negative numbers and - 0 must not be palindromes; a positive integer must be palindromes; except 0, the ending number 0 is not palindromes.

Write code validation

I. string conversion

Code:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    const rev = parseInt(x.toString().split('').reverse().join(''))
    return rev === x
};

Result:

  • 11509/11509 cases passed (252 ms)
  • Your runtime beats 79.41 % of javascript submissions
  • Your memory usage beats 52 % of javascript submissions (45.7 MB)
  • Time complexity: O(1)

Here, you can use the Object.is of ES6 to judge whether it is - 0. Of course, you can also judge ES5 as follows:

function (x) {
    return x === 0 && 1 / x < 0;    // -Infinity
}

Is it possible for someone to ask that there is no need to consider the issue of digital overflow?

The input number does not overflow. If it is a palindrome number, the output number must not overflow. If it is not a palindrome number, no matter whether it overflows or not, it returns false.

II. Surplus

Code:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    let div = 1
    while (x / div >= 10) { // To find out the number of digits, such as 121, then find 100 and get the whole digit.
        div *= 10
    }
    while(x > 0) {
        let left = parseInt(x / div); // The left side counts.
        let right = x % 10; // Number on the right side
        if (left !== right) return false;

        x = parseInt((x % div) / 10);   // Remove left and right digits

        div /= 100; // Divide by two
    }
    return true;
};

Result:

  • 11509/11509 cases passed (232 ms)
  • Your runtime beats 86.88 % of javascript submissions
  • Your memory usage beats 67.99 % of javascript submissions (45.5 MB)
  • Time complexity: O(log10(n))

Consult others' solution

Here we see a more ingenious way, which only needs to be turned in half. For example, 1221 only needs to be flipped to two digits 21.

I. turning half

Code:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    let rev = 0;    // Flipped number

    while(x > rev) {
        rev = rev * 10 + x % 10
        x = parseInt(x / 10)
    }

    return x === rev || x === parseInt(rev / 10);   // If the number is odd, the middle number should be removed for comparison.
};

Result:

  • 11509/11509 cases passed (188 ms)
  • Your runtime beats 99.62 % of javascript submissions
  • Your memory usage beats 92.69 % of javascript submissions (44.8 MB)
  • Time complexity: O(log10(n))

Thinking and summing up

To sum up, the solution of turning half is recommended.

13. Roman numeral to integer

Title address

Title Description

Roman numerals contain the following seven characters: I, V, X, L, C, D, and M.

character numerical value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

For example, the Roman numeral 2 is written as II, that is, two parallel ones. 12 write XII, that is, X + II. 27 write XXVII, that is XX + V + II.

Usually, the small numbers in roman numbers are to the right of the big ones. But there are also special cases. For example, 4 does not write IIII, but IV. The number 1 is to the left of the number 5. The number represented is equal to the number 4 obtained by subtracting the decimal 1 from the large number 5. Similarly, the number 9 is expressed as IX. This special rule only applies to the following six cases:

  • I can be placed to the left of V (5) and X (10) to represent 4 and 9.
  • X can be placed to the left of L (50) and C (100) to represent 40 and 90.
  • C can be placed to the left of D (500) and M (1000) to represent 400 and 900.

Given a Roman number, convert it to an integer. Enter to make sure it is in the range of 1 to 3999.

Example:

Input: "III"
Output: 3

Input: "IV"
Output: 4

Input: "IX"
Output: 9

Input: "LVIII"
Output: 58
 Explanation: L = 50, V= 5, III = 3.

Input: "MCMXCIV"
Output: 1994
 Explanation: M = 1000, CM = 900, XC = 90, IV = 4.

Assumption of topic analysis

This question has a more intuitive idea. Because of the limited enumeration of special cases, I have two directions here:

  • Enumerate all special combinations, and then do string traversal
  • Direct string traversal to determine the size of the current bit and the next bit

Write code validation

I. enumeration special combination

Code:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'IV': 4,
        'V': 5,
        'IX': 9,
        'X': 10,
        'XL': 40,
        'L': 50,
        'XC': 90,
        'C': 100,
        'CD': 400,
        'D': 500,
        'CM': 900,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length;) {
        if (i < s.length - 1 && hash[s.substring(i, i + 2)]) { // In the hash table, the description is a special combination
            res += hash[s.substring(i, i + 2)]
            i += 2
        } else {
            res += hash[s.charAt(i)]
            i += 1
        }
    }
    return res
};

Result:

  • 3999/3999 cases passed (176 ms)
  • Your runtime beats 77.06 % of javascript submissions
  • Your memory usage beats 80.86 % of javascript submissions (39.8 MB)
  • Time complexity: O(n)

II. Direct ergodic

Code:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length; i++) {
        if (i === s.length - 1) {
            res += hash[s.charAt(i)]
        } else {
            if (hash[s.charAt(i)] >= hash[s.charAt(i + 1)]) {
                res += hash[s.charAt(i)]
            } else {
                res -= hash[s.charAt(i)]
            }
        }
    }
    return res
};

Result:

  • 3999/3999 cases passed (176 ms)
  • Your runtime beats 84.42 % of javascript submissions
  • Your memory usage beats 90.55 % of javascript submissions (39.6 MB)
  • Time complexity: O(n)
Consult others' solution

Here we also see a way to add all the data first. If the previous bit is less than the next bit, directly subtract the positive and negative difference by 2 / 20 / 200. Take a look at the code:

I. difference operation

Code:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length; i++) {
        res += hash[s.charAt(i)]
        if (i < s.length - 1 && hash[s.charAt(i)] < hash[s.charAt(i + 1)]) {
            res -= 2 * hash[s.charAt(i)]
        }
    }
    return res
};

Result:

  • 3999/3999 cases passed (232 ms)
  • Your runtime beats 53.57 % of javascript submissions
  • Your memory usage beats 80.05 % of javascript submissions (39.8 MB)
  • Time complexity: O(n)

Change soup without changing medicine, just do an addition operation, there is not much essential difference.

Thinking and summing up

To sum up, we haven't seen some inconsistent solutions in the direction. I recommend the solution of direct traversal of strings here, with the best performance.

14. Longest common prefix

Title address

Title Description

Write a function to find the longest common prefix in the string array.

Returns the empty string '' if no public prefix exists.

Example:

Input: ["flower","flow","flight"]
Output: "fl"

Input: ["dog","racecar","car"]
Output: "
Explanation: the input does not have a common prefix.

Explain:

All inputs contain only lowercase letters a-z.

Assumption of topic analysis

At first glance, this problem is sure to be a problem that needs to be traversed. It's nothing more than the advantages and disadvantages of the algorithm. I have three directions to try to solve the problem:

  • Traverse each column, take the first item of the array, and take each bit of the string one by one to traverse the array
  • Traverse each item, take out the first item of the array, step by step cut from the back, and judge whether to match each item in the array
  • Divide and Conquer: divide the array recursively into two parts, find the maximum match respectively, and then sum up to find the maximum match

Write code validation

I. traverse each column

Code:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    const str = strs.shift()
    for(let i = 0; i < str.length; i++) {
        const char = str.charAt(i)
        for(let j = 0; j < strs.length; j++) {
            if (i === strs[j].length || strs[j].charAt(i) !== char) {
                return str.substring(0, i)
            }
        }
    }
    return str
};

Result:

  • 118/118 cases passed (68 ms)
  • Your runtime beats 89.17 % of javascript submissions
  • Your memory usage beats 57.83 % of javascript submissions (34.8 MB)
  • Time complexity: O(n)

II. Traverse each term

Code:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    let str = strs.shift()
    for(let i = 0; i < strs.length; i++) {
        while (strs[i].indexOf(str) !== 0) {
            str = str.substring(0, str.length - 1);
            if (!str) return ''
        }
    }
    return str
};

Result:

  • 118/118 cases passed (64 ms)
  • Your runtime beats 94.63 % of javascript submissions
  • Your memory usage beats 96.69 % of javascript submissions (33.5 MB)
  • Time complexity: O(n)

III. Divide and conquer

Code:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    function arrayToString (arr, start, end) {
        if (start === end) {    // Indicates that there is only one item left in the array
            return arr[start]
        } else {
            const mid = parseInt((start + end) / 2)
            const leftStr = arrayToString(arr, start, mid)
            const rightStr = arrayToString(arr, mid + 1, end)
            return getCommonPrefix(leftStr, rightStr)
        }
    }
    // Two strings with the longest prefix
    function getCommonPrefix(left, right) {
        const min = Math.min(left.length, right.length)
        for(let i = 0; i < min; i++) {
            if (left.charAt(i) !== right.charAt(i)) {
                return left.substring(0, i)
            }
        }
        return left.substring(0, min)
    }
    return arrayToString(strs, 0, strs.length - 1)
};

Result:

  • 118/118 cases passed (60 ms)
  • Your runtime beats 98.09 % of javascript submissions
  • Your memory usage beats 34.54 % of javascript submissions (35.1 MB)
  • Time complexity: O(n)

Consult others' solution

We can also see that the dichotomy is slightly different from the divide and conquer. It is to discard the interval without the answer each time to reduce the amount of computation.

I. dichotomy

Code:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    // Find the shortest string length
    let minLen = 0
    for(let i = 0; i < strs.length; i++) {
        minLen = minLen === 0 ? strs[i].length : Math.min(minLen, strs[i].length)
    }

    function isCommonPrefix (arr, pos) {
        const str = arr[0].substring(0, pos)    // Take the first half of the first item
        for(let i = 0 ; i < arr.length; i++) {
            if (arr[i].indexOf(str) !== 0) {
                return false
            }
        }
        return true
    }

    let low = 1
    let high = minLen   // Maximum number of interceptions

    while (low <= high) {
        const mid = parseInt((low + high) / 2)
        if (isCommonPrefix(strs, mid)) {    // If the first half is
            low = mid + 1   // Continue to judge the second half
        } else {
            high = mid - 1  // Continue to judge the first half
        }
    }

    return strs[0].substring(0, (low + high) / 2)
};

Result:

  • 118/118 cases passed (64 ms)
  • Your runtime beats 94.63 % of javascript submissions
  • Your memory usage beats 93.96 % of javascript submissions (33.5 MB)
  • Time complexity: O(log(n))

Thinking and summing up

For example, the algorithm of divide and conquer can also be used in the fast sorting. Personal comparison recommends divide and conquer method and dichotomy to solve this problem.

(end)

This article is an original article, which may update knowledge points and correct errors. Therefore, please keep the original source for reprint to facilitate traceability, avoid misleading of old wrong knowledge, and have a better reading experience.
If you have any help, welcome to star or fork.
(for reprint, please indicate the source: https://chenjiahao.xyz)

Topics: Javascript REST less