1. 程式人生 > >演算法進階之Leetcode刷題記錄

演算法進階之Leetcode刷題記錄

目錄

引言

       雖然說前端設計的演算法複雜度並不高,但是像我這種懶龍,還是希望能通過演算法優化來解決問題,並且升職加薪,調戲女媛,忽悠實習生的。所以學習演算法成了我日常撩妹,偶爾裝X的關鍵。同時那種解題的高潮快感,還能讓我忘記單身的煩惱。感覺學好演算法的話,我的人生都能變得豐富多彩(美女環繞)。但是我光學不寫,不能拿出來裝X,豈不是失去了學習的樂趣,少了撩妹的渠道。所以,必須記錄,必須寫出來!沒準睡醒一覺,就有妹子關注我了,嘿嘿嘿~

題目

1.兩數之和

題目

給定一個整數陣列和一個目標值,找出陣列中和為目標值的兩個數。

你可以假設每個輸入只對應一種答案,且同樣的元素不能被重複利用。

    示例:
    給定 nums = [2, 7, 11, 15], target = 9
    因為 nums[0] + nums[1] = 2 + 7 = 9
    所以返回 [0, 1]

解題筆記

這道題,我總共用時1小時。寫出了2鍾思路,3鍾寫法。首先上第一種思路,也是最容易被想到的思路。

1)思路1,寫法1——暴力迴圈法。測試用時大概在160ms上下浮動。
function twoSum(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i,j];
      }
    }
  }
}

這種寫法,相信是大多數人腦子裡蹦出的第一種寫法。兩次迴圈,再給索引加上一點氣泡排序的思想,大功告成。

2)思路1,寫法2——隱性暴力迴圈法。測試用時大概在170ms上下浮動。
function twoSum(nums, target) {
    for (let i = 0;i < nums.length;i++) {
        let index = nums.indexOf(target - nums[i],i+1);
        if(index !== -1) return [i,index];
    }        
}

這種寫法,跟第一種暴力迴圈在方式和思想上是一樣的。也是迴圈兩次,只不過第二次是通過indexOf迴圈索引值。
實際用時的話,比暴力迴圈要慢上10ms左右,所以也就是看著程式碼少而已,效能並不高。

3)思路2,寫法1——物件屬性查詢。測試用時大概在70ms上下波動,減少了一倍執行時間,最快用時64ms。
function twoSum(nums, target) {
    let obj = {};
    for (let i = 0;i < nums.length;i++) {
        if (obj[nums[i]] !== undefined) {
            return [obj[nums[i]],i];
        }
        obj[target - nums[i]] = i;
    }
}

       這種思路是通過將陣列中的每個元素以{key : value} = {target - nums[index] : i}的形式儲存在物件中,所以只要obj[nums[i]]存在,就說明target - nums[index] = nums[i],那麼nums[index] + nums[i] = target就成立。
所以此時obj[nums[i]]對應的value值及當前的迴圈的i值,就是對應的目標的索引值,返回它們的陣列,就是我們想要得到的結果。
       優勢在於只執行了一次遍歷,雖然if判斷也等同於在物件中遍歷屬性,但效能比for迴圈要高。而且,第二次的物件屬性遍歷,是一個由少增多的過程。所以只要不出現目標值位於陣列兩端的情況,物件遍歷的次數是一定小於前兩種方法的。(就算真要出現正好位於陣列兩端的情況,
也不能更換暴力迴圈法,必須保持程式碼的裝X優雅)

7.反轉整數

題目

給定一個 32 位有符號整數,將整數中的數字進行反轉。
示例 1:
    輸入: 123
    輸出: 321
示例 2:
    輸入: -123
   輸出: -321
示例 3:
    輸入: 120
    輸出: 21
注意:
    假設我們的環境只能儲存 32 位有符號整數,其數值範圍是 [−2^31,  2^31 − 1]。根據這個假設,如果反轉後的整數溢位,則返回 0。

解題筆記

       這道題用了多長時間我已經忘了,因為試了很多種方法和不同寫法,但實際在用時上,差距都沒有實質上的飛躍,最快能到84ms,最慢也不過在120ms前後。所以這道題我著重對各種方法進行了理解,而對於效能上並沒有太多的關注。

1)陣列方法
var reverse = function(x) {
    let arrX = x.toString().split(""),
    newX = arrX.reverse().join("");
    return newX[newX.length - 1] === "-" ? Number("-" + parseInt(newX)) >= Math.pow(-2, 31) && Number("-" + parseInt(newX)) <= Math.pow(2, 31) ? Number("-" + parseInt(newX)) : 0 : Number(newX) >= Math.pow(-2, 31) && Number(newX) <= Math.pow(2, 31) ? Number(newX) : 0;
}

       這種方法是我最先想到的方法了,將數字轉換成字串,再通過split()將字串分割成陣列,然後用陣列的reverse()倒排序,再將reverse()後的陣列join()成字串,最後判斷翻轉後的陣列最後一位是不是負號(-),再判斷是不是在規定範圍內,返回結果。
最先想到的往往也最悲催就是了……

2)陣列方法·改
var reverse = function(x) {
    let min = Math.pow(-2, 31),
    max = Math.pow(2, 31) - 1;
    let str = Math.abs(x).toString().split("").reverse().join("");
    let result = x > 0 ? +str : -str;
    return result >= min && result <= max ? result : 0;
}

       核心方法還是split()、reverse()、join()。改進在於,通過Math.abs()直接取絕對值,單純對數字進行反轉操作,然後通過判斷引數x本來的正負號,再給反轉後的數字新增正負號,最後判斷是否在規定範圍內,返回結果。

       在原陣列方法的基礎上,避免了正負號對反轉操作的影響,並且通過判斷引數的正負號更直接。

3)官方提示演算法
var reverse = function(x) {
    let min = Math.pow(-2, 31);
    let max = Math.pow(2, 31) - 1;
    let newX = Math.abs(x);
    let len = newX.toString().length;
    let arr = [];
    for (let i = 0; i < len; i++) {
        arr.push(newX % 10);
        newX = Math.floor(newX / 10);
    }
    result = arr.join("");
    if (x > 0 && +result <= max) return +result;
    if (x < 0 && -result >= min) return -result;
    return 0;
}

       官方提示的方法,用到的是取餘的操作。實現思路就是通過對10取餘,得到個位數的數值。然後再將結果真的除以10,並取整。實現的思路和倒計時相似,通過取餘再取整,將數值不斷的減少一位,然後獲得個位數數值。

       在我第一次實現官方提示演算法時,還是利用了陣列去操作,沒有用到資料操作的特性,所以從方法上看,思路是優化了,但是寫法還沒有優化。

4)官方提示演算法·改
var reverse = function(x) {
    let min = Math.pow(-2, 31);
    let max = Math.pow(2, 31) - 1;
    let len = x.toString().length;
    let result = 0;
    for (let i = x > 0 ? len - 1 : len - 2; i >= 0; i--) {
        result += x % 10 * Math.pow(10, i);
        x = x > 0 ? Math.floor(x / 10) : Math.ceil(x / 10);
    }
    if (result > 0 && result <= max) return result;
    if (result < 0 && result >= min) return result;
    return 0;
}

       其實算不上是優化,只是又換了種更裝X的寫法。直接將取餘後的數再乘以相應的位數對應的10的i次方,然後相加,達到直接將數值反轉的結果。麻煩點就是正數與負數的取整不同。

5)效能最佳寫法
var reverse = function(x) {
    let min = Math.pow(-2, 31);
    let max = Math.pow(2, 31) - 1;
    let strX = Math.abs(x).toString();
    let result = "";
    for (let i = strX.length - 1; i >= 0; i--) {
         result += strX[i];
    }
    if (x > 0 && +result <= max) return +result;
    if (x < 0 && -result >= min) return -result;
    return 0;
}

寫完官方提示方法之後,實在想不出其他辦法了,就去看了下最佳的寫法。其實最佳寫法跟官方寫法,在效能上是不分上下的,差距非常小。

但最佳方法應用到了字串拼接的特性,就讓人眼前一亮。

通過反向遍歷,將字串倒拼接,就實現了反轉字串的效果。最後再判斷正負及數值範圍,返回結果。

9.迴文數

題目

判斷一個整數是否是迴文數。迴文數是指正序(從左向右)和倒序(從右向左)讀都是一樣的整數。
示例 1:
    輸入: 121
    輸出: true
    
示例 2:
    輸入: -121
    輸出: false
    解釋: 從左向右讀, 為 -121 。 從右向左讀, 為 121- 。因此它不是一個迴文數。
    
示例 3:
    輸入: 10
    輸出: false
    解釋: 從右向左讀, 為 01 。因此它不是一個迴文數。
    
進階:
    你能不將整數轉為字串來解決這個問題嗎?

解題筆記

       這道題,我總共用時1小時,只寫出了純數學方法的思路。經測試,最佳時間在前99%裡面,所以我開始還比較肯定我的思路和寫法。直到我看了最佳寫法,感受到了暴擊,思路跟大神還是沒法比。

1)我的方法——最佳用時264ms
var isPalindrome = function(x) {
  if (x < 0) return false;
  if(x >=0 && x < 10) return true;
  const len = Math.floor(Math.log10(x) + 1);
  const count = len % 2 ? Math.floor(len / 2) : len / 2,
        flag = len % 2 ?  Math.ceil(len / 2) : len / 2,
        target = Math.floor(x / Math.pow(10,flag));  
  let num = 0;
  for (let i = count;i > 0 ;i--) {
    num += (x % 10) * Math.pow(10,i - 1);
    x = Math.floor(x / 10);
  }
  if (num === target) return true;
  return false;
}

       從我的寫法上面可以看出,我是受了前面幾道題的方法影響的,尤其是受了第7題反轉整數的影響;
這道題在思路上,我的方向是沒錯的,只需要取對稱長度,然後對後一半的數字進行反轉,與前一半的數值去進行比較,如果相等就是迴文數;
但是在方法上,我還是沒有脫離字串和陣列的方法邏輯——取長度,然後根據長度迴圈。對傳入數字引數取餘後再乘以相應的位數值,將引數減一位,通過迴圈實現數字的反轉。這完全就是第7題官方思路的實踐;
唯一的難點在於對數字長度的獲取,這個地方卡了半小時左右,最後想到通過log10的方法獲取長度。只要長度知道了,後邊的操作就比較簡單了;

       總結這一方法,思路還是太固定了。可以說沒有擴大思考空間。雖然沒有使用字串和陣列方法,但是思路上還是太拘泥於慣性。可以說即沒有方法上的突破,也沒有寫法上的突破。就是死板的按照條件實現了結果。

       沒想到的點是:1)其實target變數就是迴圈後的x;2)可以通過判斷len的奇偶性,直接操作迴圈後的x;3)迴圈次數可以是一個值,不必分奇偶性。
所以count、flag、target其實都是沒必要的。

2)我的方法·改
var isPalindrome = function (x) {
  if (x < 0 || x % 10 === 0 && x !== 0) return false;
  if (x >= 0 && x < 10) return true;
  let length = parseInt(Math.log10(x));
  let num = 0;
  for (let i = parseInt((length - 1) / 2); i >= 0; i--) {
    num += x % 10 * Math.pow(10, i);
    x = parseInt(x / 10);
  };
  length % 2 ? null : x = parseInt(x / 10);
  if (num === x) return true;
  return false;
}
3)最佳效能寫法——最佳用時236ms
var isPalindrome = function(x) {
  if(x < 0 || ( x !==0 && x % 10 === 0)) return false;
  if(x > = 0 && x < 10) return true;
  let intNum = 0;
  while (x > intNum){
    intNum = intNum * 10 + x % 10;
    x = parseInt(x / 10);
  }
  return x === intNum || x===parseInt(intNum / 10);
}

雖然說效能上的差別說不上質的提升,但是思路上真是讓我感受到了人與人之間的差距;

首先:

       在開始的if判斷上,只要<0或者是10的倍數,就判定不是迴文;

然後:

       對於迴圈的定義:比較出彩的地方是對兩個數字的迴圈操作;

       因為對迴圈次數不確定,所以用while來迴圈;然後通過一個數字位數相加,另一個數字位數相減來控制迴圈結束;

       如果是迴文數字,只會出現兩種情況——1)num === intNum;此時引數x的長度是偶數;2)num < intNum;此時引數x的長度是奇數,當intNum比num多一位時,迴圈肯定停止;

       而至於不是迴文數的情況,不論奇偶性,只要出現intNum的位數比num多一位,迴圈就會停止。所以迴圈的次數,最多就是x引數長度的一半再+1。

再來看迴圈內部的操作:

       首先intNum * 10,實際上是給intNum當前值增加一位,將個位數的位置騰出來給num % 10,同時也給intNum其他數字往前進一位。

       然後num%10則是把num的個位數取出來,通過intNum * 10的進位操作,num&10的個位數,將調整到它應該在的位置。

       再然後將num / 10並取整,等同於將num長度-1,不斷抹除個位數的數字。這實際上就是反轉num。對數字反轉的思路上,我想到了,但是寫法上,真是差距太大了。

       最後: 如果是迴文數,並且x的長度是偶數,則num === intNum。如果是x的長度是奇數,那麼intNum會比num多出處於x中間的一位數,這個數位於intNum的尾部。 所以將intNum / 10取整,抹除intNum的最後一位數字後,再進行比較。

                  如果不是迴文數,則不論x的奇偶性,兩種情況都有可能出現,但是都不可能相等。

這種方法,相比於我自己想出來的方法,在迴圈、數字反轉以及數字長度的操作上,完全是碾壓性的。自己在演算法上的提升,看來還有十分巨大的空間。

13.羅馬數字轉整數

題目

羅馬數字包含以下七種字元: I, V, X, L,C,D 和 M。
        字元          數值
        I             1
        V             5
        X             10
        L             50
        C             100
        D             500
        M             1000  
    例如, 羅馬數字 2 寫做 II ,即為兩個並列的 1。12 寫做 XII ,即為 X + II 。 27 寫做  XXVII, 即為 XX + V + II 。
    通常情況下,羅馬數字中小的數字在大的數字的右邊。但也存在特例,例如 4 不寫做IIII,而是IV。數字1在數字5的左邊,所表示的數等於大數 5 減小數 1 得到的
數值 4 。同樣地,數字 9 表示為 IX。這個特殊的規則只適用於以下六種情況:
        I 可以放在 V (5) 和 X (10) 的左邊,來表示 4 和 9。
        X 可以放在 L (50) 和 C (100) 的左邊,來表示 40 和 90。 
        C 可以放在 D (500) 和 M (1000) 的左邊,來表示 400 和 900。
    
給定一個羅馬數字,將其轉換成整數。輸入確保在 1 到 3999 的範圍內。
    示例 1:
        輸入: "III"
        輸出: 3
        
    示例 2:
        輸入: "IV"
        輸出: 4
        
    示例 3:
        輸入: "IX"
        輸出: 9
        
    示例 4:
        輸入: "LVIII"
        輸出: 58
    解釋: L = 50, V= 5, III = 3.
    
    示例 5:
        輸入: "MCMXCIV"
        輸出: 1994
    解釋: M = 1000, CM = 900, XC = 90, IV = 4

解題筆記

       這道題,我總共用時1小時,只寫出了純數學方法的思路。經測試,最佳時間在前99%裡面,所以我開始還比較肯定我的思路和寫法。直到我看了最佳寫法,感受到了暴擊,思路跟大神還是沒法比。

1)向後比較法
var romanToInt = function(s) {
  const obj = {
    "I" : 1,
    "V" : 5,
    "X" : 10,
    "L" : 50,
    "C" : 100,
    "D" : 500,
    "M" : 1000
  }
  
  let result = 0;
  for (let i = 0;i < s.length;i++) {
    if (obj[s[i]] < obj[s[i + 1]]) {
      result += obj[s[i + 1]] - obj[s[i]];
      i++;
    }
    
    else {result += obj[s[i]]};
  }
  
  return result;
}

       動手之前,先找規律。

       其實題幹已經解釋的很清楚了,特殊情況分為3類。而根據示例中給出的種種個例,可以找出其中的規律——除了3種特殊情況外,羅馬數字從左到右是數值是依次減小的。也就是正常寫法的話,按順序是1000-500-100-50-10-5-1,即M-D-C-L-X-V-I
唯獨不按照大小順序書寫的情況,就是那特殊的3大類6種情況中的3種——也就是4,40,400的情況

       那麼由最後的例項5,可以明顯看出數字轉換的計算方法——根據數值逐項累加。而遇到特殊的3種情況時,則將緊鄰的2個字母看做是一個(通過i++跳過迴圈實現),而在數值上,則是後一項減去當前項

       最後返回累加後的結果。

2)最佳效能寫法
var romanToInt = function(s) {
    if (s === null || s === undefined || s.length === 0) {
        return 0;
    }
    var romanNumber = {
        "I":1,
        "V":5,
        "X":10,
        "L":50,
        "C":100,
        "D":500,
        "M":1000
    }
    
    let total = 0;
    let prevVal = Number.MAX_VALUE;
    for (let i = 0; i < s.length; i++) {
        let currentNum = romanNumber[s[i]];
        total = total + currentNum;
        if (currentNum <= prevVal) {
            prevVal = currentNum;
        }
        else {
            total = total - (2 * prevVal)
        }
    }
    return total;
}

       思路上與向後比較法是一個思路,只是寫法稍有不同。

       首先在函式最前加了特殊情況的判斷。

       然後,在比較上也是一個思路,只不過最佳寫法這裡用了一種效率更高的寫法。將一個羅馬數字首位對應值賦值給一個變數,如果當前值比這個變數的值小,就再將當前值賦值給變數,以此類推,如果出現當前值大於變數值的情況,就減去當變數值的2倍。
實際上就是比較上一個值和當前值哪個大,如果當前值大,說明出現了4,40,400的3鍾情況。

       最後因為上一次迴圈已經將IV,XL,CD前面的數值加上了,所以要減去2倍的變數值。比如:10 + 50 - 10 - 10 = 40。
最後返回結果。

14.最長公共字首

題目

編寫一個函式來查詢字串陣列中的最長公共字首。 如果不存在公共字首,返回空字串 ""。
    示例 1:
        輸入: ["flower","flow","flight"]
        輸出: "fl"
        
    示例 2:
        輸入: ["dog","racecar","car"]
        輸出: "" 
        解釋: 輸入不存在公共字首。  
        說明:所有輸入只包含小寫字母 a-z 。

解題筆記

       這道題,我總共用時1小時,在最開始的思路上,方向就找對了。只是寫法上還是不如最佳效能的方案。

1)單迴圈重複法——最佳用時64ms
var longestCommonPrefix = function(strs) {
  if (!strs.length) return "";
  if (strs.length === 1) return strs[0];
  let str = strs[0];
  for (let i = 1;i < strs.length;i++) {
    if (!strs[i]) str = "";
    if (strs[i].indexOf(str) === 0) continue;
    str = str.slice(0,--str.length);
    if (!str) break;
    i--;
  }
  return str;
}

       通過不停地修改、測試,嘗試出來的一種寫法;

       核心思路是先找出陣列第一項與第二項的公共字串。然後往下逐項匹配。通過indexOf檢查字串str是否處於當前陣列項的開頭位置,然後進行str字串刪除末尾字元的操作,對陣列進行迴圈;

       之所以叫單迴圈重複法,是因為每執行一次str字串的末尾刪除操作,就將i--,重複迴圈當前陣列項,直到符合條件為止;

       與最佳的提交程式碼,在思路上一致;

2)最佳效能寫法
var longestCommonPrefix = function(strs) {
    if (strs.length == 0) return "";
    let str = strs[0];
    for (let i = 1; i < strs.length; i++)
        while (strs[i].indexOf(str) != 0) {
            str = str.substring(0, str.length - 1);
            if (str.length == 0) return "";
        }
    return str;
}

       思路一致,相比於單迴圈重複法,缺少了特殊條件的判斷,但是利用了while的迴圈次數不確定的特性,自我感覺是各有千秋,平分秋色。

20.有效的括號

題目

給定一個只包括 '(',')','{','}','[',']' 的字串,判斷字串是否有效。
有效字串需滿足:
    左括號必須用相同型別的右括號閉合。
    左括號必須以正確的順序閉合。
    注意空字串可被認為是有效字串。
    
示例 1:
    輸入: "()"
    輸出: true
    
示例 2:
    輸入: "()[]{}"
    輸出: true
    
示例 3:
    輸入: "(]"
    輸出: false
    
示例 4:
    輸入: "([)]"
    輸出: false
    
示例 5:
    輸入: "{[]}"
    輸出: true

解題筆記

       這道題目前看來是碰到的第一道,沒有獨立寫出答案,最後靠著官方提示才完成的題目。斷斷續續寫了兩天寫出了兩種錯誤思路,然後看了提示,最終寫出正確答案。

1)錯誤寫法
var isValid = function(s) {
  const obj = {
    "(" : ")",
    "{" : "}",
    "[" : "]"
  }
  
  let flag = true;
  for (let i = 0;i < s.length;i++) {
    if (obj[s[i]] === undefined) break;
    if (!s[i]) continue;
    if (obj[s[i]] === s[i + 1]) {
      i++;
      continue;
    }
    if (obj[s[i]] === s[s.length - i - 1]) continue;
    
    flag = false; 
  }
  
  return flag;
}

       寫出第一個答案時,單純的我,認為括號的匹配只有兩種情況——相鄰或者字串首尾對稱。最後掛掉……

2)錯誤寫法·plus
var isValid = function(s) {
    if (!s) return true;

    const obj = {
        "(": ")",
        "{": "}",
        "[": "]"
    }

    if (obj[s[0]] === undefined) return false;

    let len = s.length;
    while (s.length === len) {
        if (!s[0]) {
            s = s.slice(1);
            len -= 1;
        } else if (s.length === 1) {
            return false;
        } else {
            let index = 1;
            while (len === s.length) {
                if (s[index] === undefined) return false;
                if (s[index] === obj[s[0]]) {
                    s = index === 1 ? s.slice(2) : s.slice(1, index) + (s[index + 1] ? s.slice(index + 1) : "");
                }
                index += 2;
            }
            len -= 2;
         }
    }
    return !s;
}

       在有了第一個錯誤之後,聰明的我靈光一閃,構思了另一種思路。這次我覺得匹配模式不應該只有相鄰和首尾對稱,應該是相隔偶數項(包括0)。並且找到一對就拆散一對刪除一對。為此我不惜又在迴圈了加入了一個迴圈,通過index+=2的方式,隔2項查詢。最後通過s是否為空字串來進行判斷。

       這次一定可以成功,哼哼哼……撲街……

       "[([]])"這種錯誤情況無法識別……

3)官方提示寫法

var isValid = function(s) {
  if (!s) return true;
  if (s.length === 1 && s[0] !== " ") return false;
  const obj = {
    "(": ")",
    "{": "}",
    "[": "]"
  }
  
  const arr = [];
  
  for (let i = 0;i < s.length;i++) {
    if (s[i] === " ") continue;
    if (obj[arr[arr.length - 1]] === s[i]) {
      arr.pop();
      continue;
    }
    arr.push(s[i]);
  }
  
  return !arr.length;
}

       看完官方提示之後,我覺得我的第二種思路其實已經接近正確寫法,但可惜就差臨門一腳,最終還是不得入;

       思路的最終導向,還是回到對稱本身上;

       往陣列arr中push的元素,最終會以同樣的順序再被pop掉,如果arr陣列為空,那麼說明字串是符合輸入規則的;

4)最佳效能寫法

var isValid = function(s) {
  if (s !== " " && s.length === 1) return false;
  if (!s || s === " ") return true;
  
  const obj = new Map();
  obj.set(")","(");
  obj.set("]","[");
  obj.set("}","{");
  
  const arr = [];
  for (let i of s) {
    if (i === " ") continue;
    if (!obj.has(i)) {
      arr.push(i);
    }
    else {
      if (obj.get(i) !== arr[arr.length - 1]) return false;
      arr.pop();
    }
  }
  
  return !arr.length;
}

       思路不變,只是用到了新的物件Map以及for-of迴圈,可以說是充分利用了最新的特性。

       對於Map物件,在寫這道題前,並沒有接觸到過,關於Map的方法,請看下圖。

Map物件屬性

26.刪除排序陣列中的重複項

題目

給定一個排序陣列,你需要在原地刪除重複出現的元素,使得每個元素只出現一次,返回移除後陣列的新長度。
不要使用額外的陣列空間,你必須在原地修改輸入陣列並在使用 O(1) 額外空間的條件下完成。
    示例 1:
        給定陣列 nums = [1,1,2], 
        函式應該返回新的長度 2, 並且原陣列 nums 的前兩個元素被修改為 1, 2。
        你不需要考慮陣列中超出新長度後面的元素。
        
    示例 2:
        給定 nums = [0,0,1,1,1,2,2,3,3,4],
        函式應該返回新的長度 5, 並且原陣列 nums 的前五個元素被修改為 0, 1, 2, 3, 4。
        你不需要考慮陣列中超出新長度後面的元素。
        
    說明:
        為什麼返回數值是整數,但輸出的答案是陣列呢?
        請注意,輸入陣列是以“引用”方式傳遞的,這意味著在函式裡修改輸入陣列對於呼叫者是可見的。
        
    你可以想象內部操作如下:
        // nums 是以“引用”方式傳遞的。也就是說,不對實參做任何拷貝
        int len = removeDuplicates(nums);
        // 在函式裡修改輸入陣列對於呼叫者是可見的。
        // 根據你的函式返回的長度, 它會打印出陣列中該長度範圍內的所有元素。
        for (int i = 0; i < len; i++) {
            print(nums[i]);
        }

解題筆記

       總共用時1個半小時,寫出兩種方法,其中第二種方法為最佳寫法。

1)物件迴圈法
var removeDuplicates = function(nums) {
  const obj = new Map();
  for (let i of nums) {
    if (!obj.has(i)) {obj.set(i,i)};
  }
  
  nums.splice(0,obj.size,...Array.from(obj.keys()))
  
  return obj.size;
}

       通過Map物件配合for-of迴圈遍歷陣列,將不重複的值新增進物件;

       然後通過splice方法,將nums陣列頭部元素替換成Map物件的值;

       用到了Array.from轉陣列,以及擴充套件運算子這兩種語法糖;

2)最佳效能寫法
var removeDuplicates = function(nums) {
  let index = 0;
  for (let i = 0;i < nums.length;i++) {
    if (nums[i] !== nums[index]) {
      nums[++index] = nums[i]
    }
  }
  
  return index + 1;
}

       通過宣告一個index變數儲存索引;

       因為index初始值為0,所以nums[index]的初試值就是nums[0];

       通過遍歷nums陣列,只要跟nums[index]的值不相同,那就讓nums[index]的下一值,等於這個與nums[index]不相等的值;

       核心思想用到了去重以及陣列賦值;

3)最佳效能寫法·官方答案
var removeDuplicates = function(nums) {
  let index = 0;
  nums.forEach(n => {
    if (nums[index] !== n) {
      nums[++index] = n;
    }
  })
  
  return index + 1;
}

       用forEach減少了點程式碼量,更有bigger。

27.移除元素

題目

給定一個數組 nums 和一個值 val,你需要原地移除所有數值等於 val 的元素,返回移除後陣列的新長度。
不要使用額外的陣列空間,你必須在原地修改輸入陣列並在使用 O(1) 額外空間的條件下完成。
元素的順序可以改變。你不需要考慮陣列中超出新長度後面的元素。
    
    示例 1:
        給定 nums = [3,2,2,3], val = 3,
        函式應該返回新的長度 2, 並且 nums 中的前兩個元素均為 2。
        你不需要考慮陣列中超出新長度後面的元素。
        
    示例 2:
        給定 nums = [0,1,2,2,3,0,4,2], val = 2,
        函式應該返回新的長度 5, 並且 nums 中的前五個元素為 0, 1, 3, 0, 4。
        注意這五個元素可為任意順序。
        你不需要考慮陣列中超出新長度後面的元素。
        
說明:為什麼返回數值是整數,但輸出的答案是陣列呢?
    請注意,輸入陣列是以“引用”方式傳遞的,這意味著在函式裡修改輸入陣列對於呼叫者是可見的。
    你可以想象內部操作如下:
        // nums 是以“引用”方式傳遞的。也就是說,不對實參作任何拷貝
        int len = removeElement(nums, val);
        // 在函式裡修改輸入陣列對於呼叫者是可見的。
        // 根據你的函式返回的長度, 它會打印出陣列中該長度範圍內的所有元素。
        for (int i = 0; i < len; i++) {
            print(nums[i]);
        }
        

解題筆記

       這道題比較簡單,總共用時半小時,寫出最普通的寫法,後來看了官方統計資料,寫法的思路還是比較單一的,就是O(n)遍歷。

1)通常寫法
var removeElement = function(nums, val) {
  for (let i = 0;i < nums.length;i++) {
    if (nums[i] === val) {
      nums.splice(i,1),
      i--;
    }
  }    
};

       思路比較簡單,遍歷nums陣列,如果跟val值相等的話,就通過splice方法刪除。

2)通常寫法·plus
var removeElement = function(nums, val) {
  for (let i = nums.length - 1;i >= 0;i--) {
    if (nums[i] === val) { nums.splice(i,1); }
  }    
};

       相比較於上一種寫法,增加了一點小思路。如果倒序查詢的話,因為查詢nums元素時是left-to-right的,所以就算刪除了元素,也不需要再對索引值進行操作。

3)冒泡查詢
var removeElement = function(nums, val) {
    let i = 0;
    let j = nums.length - 1;
    while (i <= j) {
        if (nums[i] === val) {
            [nums[i],nums[j]] = [nums[j],nums[i]]; //位置互換
            j--;
        }
        else {
            i++;
        }
    }
};

       這種寫法沒有對原陣列進行刪除,而是將重複值都拋到陣列項的末尾,有點類似於氣泡排序與陣列去重時的位置互換思想。

再迴圈次數上,這3鍾方法沒有區別,所以效能上難分伯仲。解題思路上,由於題目比較簡單,也沒有出彩的地方。所以從題目角度看,這道題價值不大。

35.搜尋插入位置

題目

給定一個排序陣列和一個目標值,在陣列中找到目標值,並返回其索引。如果目標值不存在於陣列中,返回它將會被按順序插入的位置。
    
    你可以假設陣列中無重複元素。
        示例 1:
            輸入: [1,3,5,6], 5
            輸出: 2
            
        示例 2:
            輸入: [1,3,5,6], 2
            輸出: 1
        
        示例 3:
            輸入: [1,3,5,6], 7
            輸出: 4
            
        示例 4:
            輸入: [1,3,5,6], 0
            輸出: 0

解題筆記

       這道題難度不大,寫法也比較單一,通過率很高。總共用時1小時,最終結果為效能最佳寫法之一。

1)迴圈計數
var searchInsert = function(nums, target) {
  let count = 0;
  while (nums[count] < target) {
    count++;
  }
  return count;
}

       這是我最終提交的答案,思路很簡單。就不過多解釋了

2)迴圈陣列
var searchInsert = function(nums, target) {
    for (let i=0; i<nums.length; i++){
        if (nums[i] === target || nums[i] > target) {
            return i;
        }
    }
    return nums.length;
}

       這種應該是最容易被想到的寫法,切著題乾的描述

3)快排思路
let searchInsert = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let mid = ~~((left + right) / 2);
        if (nums[mid] === target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;
}

       利用的快排演算法的思想,不停取中間值,縮小範圍,最終定靶。

38.報數

題目

報數序列是一個整數序列,按照其中的整數的順序進行報數,得到下一個數。其前五項如下:
    1.     1
    2.     11
    3.     21
    4.     1211
    5.     111221
    
1 被讀作  "one 1"  ("一個一") , 即 11。
11 被讀作 "two 1s" ("兩個一"), 即 21。
21 被讀作 "one 2",  "one 1" ("一個二" ,  "一個一") , 即 1211。
    
給定一個正整數 n(1 ≤ n ≤ 30),輸出報數序列的第 n 項。
注意:整數順序將表示為一個字串。
    示例 1:
        輸入: 1
        輸出: "1"
        
    示例 2:
        輸入: 4
        輸出: "1211"

解題筆記

       用時3小時,耗時不在思路上,主要在寫法與自我懷疑上。總覺得應該會有規律,但是看到最後各種答案的時候,心裡還是萬草泥馬奔騰的。整體看,這道題思路比較單一,而且寫法暴力,寫完不是很過癮。

1)雙迴圈
var countAndSay = function(n) {
    let arr = ["1"];

    let str = "1";
    let count = 1;

    for (let i = 0;i < n - 1;i++) {
      let copy = "";

      for (let j = 0; j < str.length; j++) {

        if (arr[0] === str[j + 1]) {
            count++;
            continue;
          }

          copy += count + arr[0];
          arr = [str[j + 1]];
          count = 1;
        }

        str = copy;
        arr = [str[0]];
      }

      return str;
}

       我在寫的時候,主要的卡點就在第一個字元和最後一個字元的判斷上。主要思考點在——1)如果第一個字元就是個單獨的字元,那麼該怎麼才能返回第一個字元的報數情況,然後繼續判斷第二個;2)最後一個迴圈時,如何把最後的報數新增到字串中;

       比較繞的地方就是n - 1、資料的比較、count的初始化及計數、以及arr的賦值

       首先n - 1,迴圈少一次的邏輯。 因為初始化,給str賦值了1,所以n = 1的情況是跳過的, 通過初始化已經實現。那麼整體的迴圈就隨之少了1次

       然後是資料的比較。 如果跟字串當前值進行比較的話,那麼count的初始化不好操作。因為count的初始化會出現在兩個位置,一個是字串的首位(即首位與第二位不相等的情況)、另一個出現不相等值,需要停止計數時。 這兩種情況的本質區別是第一種是相等判斷,第二種是不等判斷。

       第一種情況因為出現在字串的首位,如果跟當前索引值比較,那麼肯定是要判斷相等的,這樣如果初始化count值是0的話,才能通過+1來計數。那麼為什麼count不直接初始化為1呢? 因為從寫法邏輯上來看,每判斷一次相等,count應該+1, 所以從首位的情況看,count的初始化值,應該為0,那麼這就與第二種情況矛盾了

       第二種情況的出現,要執行兩步操作首先將之前的計數情況插入到字串中,給arr重新賦值,然後初始化count。這裡就需要注意了,因為比較的是當前的索引值,所以必須將count初始化為1,因為本次已經是下一迴圈的第一次了。
而如果count初始化仍為0的話,通過 --操作,再執行一次當前迴圈,又耗費了效能。不難看出,跟當前索引值進行比較,實現起來過於複雜。 所以,通過跟當前索引的下一值進行比較,達到統一的效果。並且最後一次的判斷必定為false,可以執行新增的操作

       再然後是count的初始化及計數。 因為是與當前索引值的下一個值進行比較,所以實際上每次的查詢都是少了一次的。而且如果出現當前值僅有一個的情況下,相等判斷必為flase,而對當前值的報數又為1。所以在這種邏輯下,count的初始化值必須為1,每次判斷相等時,count就+1

       最後是arr的賦值。 分兩種情況——1) 是字串迴圈內的賦值;2) 是對於報數次數迴圈內的賦值。首先是第一種, 對於字串迴圈內的賦值,因為比較的是當前索引值的下一項,而且count計數為1。所以每次一個count的判斷週期結束,都將arr的賦值為下一個要計數的字元值;第二種是報數次數迴圈內的賦值。 因為報數迴圈每迴圈一次,意味著str更新了一次,那麼將從首位開始迴圈。那麼根據邏輯,每個count計數開始時,當前索引值都是跟陣列值相等的, 所以每次報數迴圈結束,都將arr賦值為新str的首位(即str[0]);
對於字串的初始化與賦值,邏輯簡單,不再過多贅述;

2)雙函式
var countAndSay = function(n) {
    let str = "1";
    for (let i = 2; i <= n; i++) {
        str = saySomething(str);
    }
    return str;
};

var saySomething = function (s) {
    s += '';
    let str = "";
    let j;
    for (let i = 0; i < s.length;) {
        j = i + 1;
        while (s[i] == s[j]) {
            j++;
        }
        str += ((j - i) + s[i]);
        i = j;
    }
    return str;
}

       雙函式與雙迴圈,思路都是一樣的,只是拆分成2個函式

3)暴力法

       暴力法就不上程式碼了,太長了。

       因為限定了輸入範圍,所以最快的寫法就是把所有的結果都寫在了一個數組裡,陣列第一項為空,然後直接返回以傳入引數為索引的對應值。

       可以說是相當暴力了……哈哈哈

53.最大子序和

題目

給定一個整數陣列 nums ,找到一個具有最大和的連續子陣列(子陣列最少包含一個元素),返回其最大和。
    示例:
        輸入: [-2,1,-3,4,-1,2,1,-5,4],
        輸出: 6
        解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。
    進階:
        如果你已經實現複雜度為 O(n) 的解法,嘗試使用更為精妙的分治法求解。

解題筆記

       斷斷續續寫了大概兩天,最後還是看了別人的答案……有點傷。

1)最佳效能寫法
var maxSubArray = function(nums) {
  let result = Number.MIN_SAFE_INTEGER;
  let num = 0;
  
  // nums.forEach(function(val) {
  //   num += val;
  //   result = Math.max(result,num);
  //   if (num < 0) num = 0;
  // })
  
  // for (let i in nums) {
  //   num += nums[i];
  //   result = Math.max(result,num);
  //   if (num < 0) num = 0;
  // }
    
  // for (let val of nums) {
  //   num += val;
  //   result = Math.max(result,num);
  //   if (num < 0) num = 0;
  // }
  
  for (let i = 0;i < nums.length;i++) {
    num += nums[i];
    result = Math.max(result,num);
    if (num < 0) num = 0;
  }
  
  return result;
}

       整個的思路是圍繞兩個點——全域性最大值區域性最大值

       全域性最大值result的確立:

              首先,每次迴圈都將區域性值num加上陣列nums的當前值,然後將全域性最大值result與區域性值num進行比較,取最大值;

              這樣操作,首先保證了全域性最大值的連續性——因為num是連續相加的;

              其次保證了全域性最大值在連續性上是最大的,因為一旦區域性值num變小,全域性值result將不再變化,直到區域性值再次超過全域性最大值;

       區域性(最大)值num的確立:

              區域性值num相對於全域性最大值result,在邏輯上稍微難理解一點。關鍵點在於任何值加上負數都是變小的;

              所以一旦區域性值num為負時,不管下次迴圈的數值是多少,兩個值的相加,對於下個值來講,都是變小的;

              所以不管下次迴圈的數值是什麼,之後再連續求和時,都不需要再加上本次的區域性值num。此時可以理解為,結束了一次區域性最大值查詢;

              所以,一旦區域性值num出現負值的情況,就將區域性值num重新賦值為0,開始下一次的區域性最大值確立;

       通過全域性最大值與區域性最大值的不斷比較,最後就得到了想要的結果。

       最後通過測試,for迴圈的效能是大於其他遍歷方法的!

67.二進位制求和

題目

給定兩個二進位制字串,返回他們的和(用二進位制表示)。
輸入為非空字串且只包含數字 1 和 0。
    示例 1:
        輸入: a = "11", b = "1"
        輸出: "100"
    
    示例 2:
        輸入: a = "1010", b = "1011"
        輸出: "10101"

解題筆記

       用時2小時,停留在一種思路上,最後沒能突破。

1)所有情況判斷法
var addBinary = function(a, b) {
  let result = "";
  let num = 0;

  let i = a.length - 1,
      j = b.length - 1;
            

 while (a[i] || b[j]) {
    if (!a[i]) {
       if (num) {
           if (+b[j] + num === 2) {
               result = "0" + result;
                 num = 1;
             } else {
               result = (+b[j] + num) + "" + result;
                 num = 0;
             }
             j--;
             continue;
          }
         result = b[j] + result;
         j--;
         continue;
      }
      
    if (!b[j]) {
        if (num) {
           if (+a[i] + num === 2) {
              result = "0" + result;
                num = 1;
             } else {
               result = (+a[i] + num) + "" + result;
                 num = 0;
             }
             i--;
             continue;
           }
           result = a[i] + result;
           i--;
           continue
       }

       if (a[i] === "0" && b[j] === "0") {
         if (+a[i] + +b[j] + num === 0) {
             result = "0" + result;
           } else {
             result = "1" + result;
               num = 0;
           }
           i--;
           j--;
           continue;
        }

        if (a[i] !== "0" || b[j] !== "0") {
          if (+a[i] + +b[j] + num === 2) {
             result = "0" + result;
               num = 1;
            } else if (+a[i] + +b[j] + num === 3) {
              result = "1" + result;
                num = 1;
            } else {
              result = "1" + result;
            }
            i--;
            j--;
            continue;
        }
  }

  if (num === 1) return "1" + result;
  return result;
}

       這是我第一次提交的答案,由於判斷直接,毫無技巧,嘗試了不知道多少次,可以說這個寫法是相當暴力了…

       核心思路遵循了二進位制的計算方法,然後把所有情況進行判斷計算;

       二進位制的計算方法,百度一下~

2)所有情況判斷法·plus
var addBinary = function(a, b) {
  let result = "";
  let num = 0;

  let i = a.length - 1,
      j = b.length - 1;
  
  while(i >= 0 || j >= 0 || num) {
    let sum = ~~a[i] + ~~b[j] + num;
    if (sum > 1 && sum < 3) {
      sum = 0;
      num = 1;
    }
    else if (sum === 3) {
      sum = 1;
      num = 1;
    }
    else num = 0;
    
    result = sum + result; 
    
    i--;
    j--;
  }
  
  return result;
}

       相比於第一次的寫法,優化了判斷機制,利用了 ** ~~ 計算符對於字串中字元不全為數字的情況,返回0的特性。這樣的話,當i或j<0時,通過 ~~ a[i]或 ~~ **b[j]將返回0;

       在while判斷中加入了對num的判斷,這樣當進行最後一次字串的計算時,將根據num的值選擇是否再執行一次迴圈,加上最後num的值;

       尷尬的地方是,雖然寫法優化了,但是效能降低了……

3)最佳效能寫法
var addBinary = function(a, b) {
    let s = '';
    
    let c = 0, i = a.length-1, j = b.length -1;
    
    while(i >= 0 || j >= 0 || c === 1) {
        c += i >= 0 ? a[i--] - 0 : 0;
        c += j >= 0 ? b[j--] - 0 : 0;
        
        s = ~~(c % 2) + s;
        
        c = ~~(c/2);
    }
    
    return s;
}

       看大神的寫法,總會產生一種自我懷疑,我這腦子是怎麼長的??

       最佳寫法將二進位制的進位值與兩個字串值的求和合併為一個變數,通過~~操作符配合取餘%以及/操作,判斷了求和值及進位值,數學方法用的真是666~