首發於微信公衆號《前端成長記》,寫於 2019.11.21
本文記錄刷題過程當中的整個思考過程,以供參考。主要內容涵蓋:javascript
題目地址前端
給定兩個二進制字符串,返回他們的和(用二進制表示)。java
輸入爲非空字符串且只包含數字 1
和 0
。算法
示例:數組
輸入: a = "11", b = "1" 輸出: "100" 輸入: a = "1010", b = "1011" 輸出: "10101"
這道題又是一道加法題,因此記住下,直接轉數字進行加法可能會溢出,因此不可取。因此咱們須要遍歷每一位來作解答。我這有兩個大方向:補0後遍歷,和不補0遍歷。可是基本的依據都是本位相加,逢2進1便可,相似手寫10進制加法。微信
Ⅰ.補0後遍歷,先算先推函數
代碼:性能
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let times = Math.max(a.length, b.length) // 須要遍歷次數 // 補 0 while(a.length < times) { a = '0' + a } while(b.length < times) { b = '0' + b } let res = [] let carry = 0 // 是否進位 for(let i = times - 1; i >= 0; i--) { const num = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0) carry = num >= 2 ? 1 : 0 res.push(num % 2) } if (carry === 1) { res.push(1) } return res.reverse().join('') };
結果:測試
O(n)
Ⅱ.補0後遍歷,按位運算優化
代碼:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let times = Math.max(a.length, b.length) // 須要遍歷次數 // 補 0 while(a.length < times) { a = '0' + a } while(b.length < times) { b = '0' + b } let res = [] let carry = 0 // 是否進位 for(let i = times - 1; i >= 0; i--) { res[i] = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0) carry = res[i] >= 2 ? 1 : 0 res[i] %= 2 } if (carry === 1) { res.unshift(1) } return res.join('') };
結果:
O(n)
Ⅲ.不補0遍歷
固然處理方式仍是能夠選擇上面兩種,我這就採用先算先推來處理了。
代碼:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let max = Math.max(a.length, b.length) // 最大長度 let min = Math.min(a.length, b.length) // 最大公共長度 // 將長字符串拆成兩部分 let left = a.length > b.length ? a.substr(0, a.length - b.length) : b.substr(0, b.length - a.length) let right = a.length > b.length ? a.substr(a.length - b.length) : b.substr(b.length - a.length) // 公共長度部分遍歷 let rightRes = [] let carry = 0 for(let i = min - 1; i >= 0; i--) { const num = carry + (right.charAt(i) | 0) + (((a.length > b.length ? b : a)).charAt(i) | 0) carry = num >= 2 ? 1 : 0 rightRes.push(num % 2) } let leftRes = [] for(let j = max - min - 1; j >= 0; j--) { const num = carry + (left.charAt(j) | 0) carry = num >= 2 ? 1 : 0 leftRes.push(num % 2) } if (carry === 1) { leftRes.push(1) } return leftRes.reverse().join('') + rightRes.reverse().join('') };
結果:
O(n)
看到一些細節上的區別,我這使用 '1' | 0
來轉數字,有的使用 ''1' - '0''
。另外還有就是初始化結果數組長度爲最大長度加1後,最後判斷首位是否爲0須要剔除的,我這使用的是判斷最後是否還要進位補1。
這裏還看到用一個提案中的 BigInt
類型來解決的
Ⅰ.BigInt
代碼:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { return (BigInt("0b"+a) + BigInt("0b"+b)).toString(2); };
結果:
O(1)
經過 BigInt
的方案咱們能看到,使用原生方法確實性能更優。簡單說一下這個類型,目前還在提案階段,看下面的等式基本就能知道實現原理本身寫對應 Hack
來實現了:
BigInt(10) = '10n' BigInt(20) = '20n' BigInt(10) + BigInt(20) = '30n'
雖然這種方式很友好,可是仍是但願看到加法題的時候,能考慮到遍歷按位處理。
實現 int sqrt(int x)
函數。
計算並返回 x 的平方根,其中 x 是非負整數。
因爲返回類型是整數,結果只保留整數的部分,小數部分將被捨去。
示例:
輸入: 4 輸出: 2 輸入: 8 輸出: 2 說明: 8 的平方根是 2.82842..., 因爲返回類型是整數,小數部分將被捨去。
一樣,這裏類庫提供的方法 Math.sqrt(x)
就不說了,這也不是本題想考察的意義。因此這裏有幾種方式:
Ⅰ.暴力法
代碼:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0) return 0 let i = 1 while(i * i < x) { i++ } return i * i === x ? i : i - 1 };
結果:
O(n)
Ⅱ.二分法
代碼:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0) return 0 let l = 1 let r = x >>> 1 while(l < r) { // 這裏要用大於判斷,因此取右中位數 const mid = (l + r + 1) >>> 1 if (mid * mid > x) { r = mid - 1 } else { l = mid } } return l };
結果:
O(log2(n))
這裏看見了兩個有意思的解法:
Ⅰ.冪次優化
稍微解釋一下,二分法須要作乘法運算,他這裏改用加減法
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { let l = 0 let r = 1 << 16 // 2的16次方,這裏我猜是由於上限2^32因此取一半 while (l < r - 1) { const mid = (l + r) >>> 1 if (mid * mid <= x) { l = mid } else { r = mid } } return l };
結果:
1017/1017 cases passed (72 ms)
Your runtime beats 98.46 % of javascript submissions
Your memory usage beats 70.66 % of javascript submissions (35.4 MB)
O(log2(n))
Ⅱ.牛頓法
算法說明:
在迭代過程當中,以直線代替曲線,用一階泰勒展式(即在當前點的切線)代替原曲線,求直線與 xx 軸的交點,重複這個過程直到收斂。
首先隨便猜一個近似值 x
,而後不斷令 x
等於 x
和 a/x
的平均數,迭代個六七次後 x
的值就已經至關精確了。
公式能夠寫爲 X[n+1]=(X[n]+a/X[n])/2
代碼:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0 || x === 1) return x let a = x >>> 1 while(true) { let cur = a a = (a + x / a) / 2 // 這裏是爲了消除浮點運算的偏差,1e-5是我試出來的 if (Math.abs(a - cur) < 1e-5) { return parseInt(cur) } } };
結果:
O(log2(n))
這裏就提一下新接觸的牛頓法吧,其實是牛頓迭代法,主要是迭代操做。因爲在單根附近具備平方收斂,因此能夠轉換成線性問題去求平方根的近似值。主要應用場景有這兩個方向:
假設你正在爬樓梯。須要 n
階你才能到達樓頂。
每次你能夠爬 1
或 2
個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?
注意:給定 n
是一個正整數。
示例:
輸入: 2 輸出: 2 解釋: 有兩種方法能夠爬到樓頂。 1. 1 階 + 1 階 2. 2 階 輸入: 3 輸出: 3 解釋: 有三種方法能夠爬到樓頂。 1. 1 階 + 1 階 + 1 階 2. 1 階 + 2 階 3. 2 階 + 1 階
這道題很明顯能夠用動態規劃和斐波那契數列來求解。而後咱們來看看其餘正常思路,若是使用暴力法的話,那麼複雜度將會是 2^n
,很容易溢出,可是若是可以優化成 n
的話,其實還能夠求解的。因此這道題我就從如下三個方向來做答:
Ⅰ.哈希遞歸
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { let hash = {} return count(0) function count (i) { if (i > n) return 0 if (i === n) return 1 // 這步節省運算 if(hash[i] > 0) { return hash[i] } hash[i] = count(i + 1) + count(i + 2) return hash[i] } };
結果:
O(n)
Ⅱ.動態規劃
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { if (n === 1) return 1 if (n === 2) return 2 // dp[0] 多一位空間,省的後面作減法 let dp = new Array(n + 1).fill(0) dp[1] = 1 dp[2] = 2 for(let i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2] } return dp[n] };
結果:
O(n)
Ⅲ.斐波那契數列
其實斐波那契數列就能夠用動態規劃來實現,因此下面的代碼思路很類似。
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { if (n === 1) return 1 if (n === 2) return 2 let num1 = 1 let num2 = 2 for(let i = 3; i <= n; i++) { let count = num1 + num2 num1 = num2 num2 = count } // 至關於fib(n) return num2 };
結果:
O(n)
查看題解發現這麼幾種解法:
Ⅰ.斐波那契公式
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { const sqrt_5 = Math.sqrt(5) // 因爲 F0 = 1,因此至關於須要求 n+1 的值 const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2, n + 1) return Math.round(fib_n / sqrt_5) };
結果:
O(log(n))
Ⅱ.Binets 方法
算法說明:
使用矩陣乘法來獲得第 n 個斐波那契數。注意須要將初始項從 fib(2)=2,fib(1)=1
改爲 fib(2)=1,fib(1)=0
,來達到矩陣等式的左右相等。
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { function pow(a, n) { let ret = [[1,0],[0,1]] // 矩陣 while(n > 0) { if ((n & 1) === 1) { ret = multiply(ret, a) } n >> 1 a = multiply(a, a) } return ret; } function multiply(a, b) { let c = [[0,0], [0,0]] for (let i = 0; i < 2; i++) { for(let j = 0; j < 2; j++) { c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] } } return c } let q = [[1,1], [1, 0]] let res = pow(q, n) return res[0][0] };
結果:
測試用例能夠輸出,提交發現超時。
這個筆者還沒徹底理解,因此很抱歉,暫時沒有 js 相應代碼分析,後續會補上。也歡迎您補充給我,感謝!
Ⅲ.排列組合
代碼:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { // n 個臺階走 i 次1階和 j 次2階走到,推導出 i + 2*j = n function combine(m, n) { if (m < n) [m, n] = [n, m]; let count = 1; for (let i = m + n, j = 1; i > m; i--) { count *= i; if (j <= n) count /= j++; } return count; } let total = 0; // 取出全部知足條件的解 for (let i = 0,j = n; j >= 0; j -= 2, i++) { total += combine(i, j); } return total; };
結果:
O(n^2)
這種疊加的問題,首先就會想到動態規劃的解法,恰好這裏又知足斐波那契數列,因此我是推薦首選這兩種解法。另外經過查看他人解法學到了斐波那契公式,以及站在排列組合的角度去解,開拓了思路。
給定一個排序鏈表,刪除全部重複的元素,使得每一個元素只出現一次。
示例:
輸入: 1->1->2 輸出: 1->2 輸入: 1->1->2->3->3 輸出: 1->2->3
注意一下,給定的是一個排序鏈表,因此只須要依次更改指針就能夠直接得出結果。固然,也可使用雙指針來跳太重複項便可。因此這裏有兩個方向:
若是是無序鏈表,我會建議先獲得全部值而後去重後(好比經過Set)生成新鏈表做答。
Ⅰ.直接運算
代碼:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { // 複製一個用作操做,因爲對象是傳址,因此改指針指向便可 let cur = head while(cur !== null && cur.next !== null) { if (cur.val === cur.next.val) { // 值相等 cur.next = cur.next.next } else { cur = cur.next } } return head };
結果:
O(n)
Ⅱ.雙指針法
代碼:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { // 新建哨兵指針和當前遍歷指針 if (head === null || head.next === null) return head let pre = head let cur = head while(cur !== null) { debugger if (cur.val === pre.val) { // 當前指針移動 cur = cur.next } else { pre.next = cur pre = cur } } // 最後一項若是重複須要把head.next指向null pre.next = null return head };
結果:
O(n)
忘記了,這裏確實還可使用遞歸來做答。
Ⅰ.遞歸法
代碼:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { if(head === null || head.next === null) return head if (head.val === head.next.val) { // 值相等 return deleteDuplicates(head.next) } else { head.next = deleteDuplicates(head.next) } return head };
結果:
O(n)
關於鏈表的題目通常都是經過修改指針指向來做答,區分單指針和雙指針法。另外,遍歷也是能夠實現的。
給定兩個有序整數數組 nums1
和 nums2
,將 nums2
合併到 nums1
中,使得 num1
成爲一個有序數組。
說明:
nums1
和 nums2
的元素數量分別爲 m
和 n
。nums1
有足夠的空間(空間大小大於或等於 m + n
)來保存 nums2
中的元素。示例:
輸入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 輸出: [1,2,2,3,5,6]
以前咱們作過刪除排序數組中的重複項,其實這裏也相似。能夠從這幾個方向做答:
可是因爲題目有限定空間都在 nums1
,而且不要寫 return
,直接在 nums1
上修改,因此我這裏主要的思路就是遍歷,經過 splice
來修改數組。區別就在於遍歷的方式方法。
Ⅰ.從前日後
代碼:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 兩個數組對應指針 let p1 = 0 let p2 = 0 // 這裏須要提早把nums1的元素拷貝出來,要否則比較賦值後就丟失了 let cpArr = nums1.splice(0, m) // 數組指針 let p = 0 while(p1 < m && p2 < n) { // 先賦值,再進行+1操做 nums1[p++] = cpArr[p1] < nums2[p2] ? cpArr[p1++] : nums2[p2++] } // 已經有p個元素了,多餘的元素要刪除,剩餘的要加上 if (p1 < m) { // 剩餘元素,p1 + m + n - p = m + n - (p - p1) = m + n - p2 nums1.splice(p, m + n - p, ...cpArr.slice(p1, m + n - p2)) } if (p2 < n) { // 剩餘元素,p2 + m + n - p = m + n - (p - p2) = m + n - p1 nums1.splice(p, m + n - p, ...nums2.slice(p2, m + n - p1)) } };
結果:
O(m + n)
Ⅱ.從後往前
代碼:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 這種 nums1.length > nums2.length 而且 m = 0 nums1.splice(m, nums1.length - m) // 兩個數組對應指針 let p1 = m - 1 let p2 = n - 1 // 數組指針 let p = m + n - 1 while(p1 >= 0 && p2 >= 0) { // 先賦值,再進行-1操做 nums1[p--] = nums1[p1] < nums2[p2] ? nums2[p2--] : nums1[p1--] } // 可能nums2有剩餘,因爲指針是下標,因此截取數量須要加1 nums1.splice(0, p2 + 1, ...nums2.slice(0, p2 + 1)) };
結果:
O(m + n)
Ⅲ.合併後排序再賦值
代碼:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { arr = [].concat(nums1.splice(0, m), nums2.splice(0, n)) arr.sort((a, b) => a - b) for(let i = 0; i < arr.length; i++) { nums1[i] = arr[i] } };
結果:
O(m + n)
這裏看到一個直接用兩次 while
,而後直接用 m/n
來計算下標的,沒有額外空間,可是本質上也是從後往前遍歷。
Ⅰ.兩次while
代碼:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 這種 nums1.length > nums2.length 而且 m = 0 // nums1.splice(m, nums1.length - m) // 從後開始賦值 while(m !== 0 && n !== 0) { nums1[m + n - 1] = nums1[m - 1] > nums2[n - 1] ? nums1[--m] : nums2[--n] } // nums2 有剩餘 while(n !== 0) { nums1[m + n - 1] = nums2[--n] } };
結果:
O(m + n)
碰到數組操做,會優先考慮雙指針法,具體指針方向能夠由題目邏輯來決定。
(完)
本文爲原創文章,可能會更新知識點及修正錯誤,所以轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
若是能給您帶去些許幫助,歡迎 ⭐️ star 或 ✏️ fork
(轉載請註明出處: https://chenjiahao.xyz)