【Algorithm】用JS刷劍指offer

1 二維數組的查找

題目描述

在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。

題目分析

該二維數組中的一個數,它左邊的數都比它小,下邊的數都比它大。因此,從右上角開始查找,就可以根據 target 和當前元素的大小關係來縮小查找區間,當前元素的查找區間爲左下角的所有元素。

在這裏插入圖片描述

代碼

function Find(target, array) {
  // 邊界條件
  if (array == null || array.length === 0 || array[0].length === 0) {
    return false;
  }
  var rows = array.length;
  var cols = array[0].length;
  var r = 0;
  var c = cols - 1;
  while (r <= rows - 1 && c >= 0) {
    if (target == array[r][c]) {
      return true;
    } else if (target > array[r][c]) {
      r++;
    } else {
      c--;
    }
  }
  return false;
}

2 替換空格

題目描述

請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串爲We Are Happy.則經過替換之後的字符串爲We%20Are%20Happy。

題目分析

我們如果要替換空格,兩步

  1. 先知道空格的位置
  2. 替換,但是字符串中有多個空格,所以我們就要循環,替換完之後再去查找字符串空格位置

當然你也可以選擇用正則

代碼

function replaceSpace(str)
{
  return str.replace(/\s/g, '%20')
}

3 從尾到頭打印鏈表

題目描述

輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。

題目分析

在這裏插入圖片描述

代碼

function printListFromTailToHead(head)
{
    // write code here
    let arr = [];
    while(head) {
        arr.push(head.val);
        head = head.next
    }
    return arr.reverse();
}

4 重建二叉樹

題目描述

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。

題目分析

前序遍歷的第一個值爲根節點的值,使用這個值將中序遍歷結果分成兩部分,左部分爲樹的左子樹中序遍歷結果,右部分爲樹的右子樹中序遍歷的結果。
在這裏插入圖片描述

代碼

/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
function reConstructBinaryTree(pre, vin)
{
    // write code here
    // 邊界條件
    if(pre.lenght === 0 || vin.length === 0) {
        return null;
    }
    
    let index = vin.indexOf(pre[0]);
    let left = vin.slice(0, index);
    let right = vin.slice(index + 1);
    
    return {
        val: pre[0],
        left: reConstructBinaryTree(pre.slice(1, index + 1), left),
        right: reConstructBinaryTree(pre.slice(index + 1), right)
    }
}

5 用兩個棧實現隊列

題目描述

用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。

題目分析

in 棧用來處理入棧(push)操作,out 棧用來處理出棧(pop)操作。一個元素進入 in 棧之後,出棧的順序被反轉。當元素要出棧時,需要先進入 out 棧,此時元素出棧順序再一次被反轉,因此出棧順序就和最開始入棧順序是相同的,先進入的元素先退出,這就是隊列的順序。

在這裏插入圖片描述

代碼

var inStack = [];
var outStack = [];

function push(node)
{
    // write code here
    inStack.push(node);
    
}
function pop()
{
    if(!outStack.length) {
        while(inStack.length) {
            outStack.push(inStack.pop());
        }
    }
    return outStack.pop();
}

6 旋轉數組中的最小數字

題目描述

把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。 NOTE:給出的所有元素都大於0,若數組大小爲0,請返回0。

題目分析

在一個有序數組中查找一個元素可以用二分查找,二分查找也稱爲折半查找,每次都能將查找區間減半,這種折半特性的算法時間複雜度都爲O(logN)

本題可以修改二分查找算法進行求解:

  • 當 nums[mid] <= nums[high] 的情況下,說明解在 [low, mid] 之間,此時令 high = mid;
  • 否則解在 [mid + 1, high] 之間,令 low = mid + 1。

代碼

暴力使用sort

function minNumberInRotateArray(rotateArray)
{
    // 邊界條件
    if(rotateArray.length === 0) return 0;
    return rotateArray.sort(function(a, b){return a - b})[0];
}

二分法

function minNumberInRotateArray(rotateArray) {
  if (rotateArray.length == 0) return 0;
  var low = 0;
  var high = rotateArray.length - 1;
  while (low < high) {
    var mid = low + high >> 1;
    if (rotateArray[mid] <= rotateArray[high])
      high = mid;
    else
      low = mid + 1;
  }
  return rotateArray[low];
}

7 斐波那契數列

題目描述

大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。
n<=39

題目分析

在這裏插入圖片描述

代碼

遞歸
雖然可以實現,但是運行超時:您的程序未能在規定時間內運行結束,請檢查是否循環有錯或算法複雜度過大。

function Fibonacci(n)
{
    // write code here
    if(n >= 0 && n < 2) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

動態規劃

function Fibonacci(n)
{
    // write code here
    let arr = [];
    arr[0] = 0;
    arr[1] = 1;
    for (let i = 2; i <= n; i++) {
        arr[i] = arr[i - 1] + arr[i - 2];
    }
    return arr[n];
}

8 跳臺階

題目描述

一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。

題目分析

在這裏插入圖片描述
稍微分析就知道這是斐波那契數列,所以可以動態規劃來做

  • 如果兩種跳法,1階或者2階,那麼假定第一次跳的是一階,那麼剩下的是n-1個臺階,跳法是f(n-1)
  • 假定第一次跳的是2階,那麼剩下的是n-2個臺階,跳法是f(n-2)
  • 由1、2假設可以得出總跳法爲:f(n) = f(n-1) + f(n-2)
  • 通過實際的情況可以得出:只有一階的時候 f(1) = 1 ,只有兩階的時候可以有 f(2) = 2
  • 可以發現最終得出的是一個斐波那契數列

代碼

遞歸

function jumpFloor(number)
{
    // write code here
    if (number <= 2) return number;
    return jumpFloor(number - 1) + jumpFloor(number - 2);
}

動態規劃

function jumpFloor(number)
{
    // write code here
    let arr = new Array(number + 1).fill(null);
    arr[0] = 0;
    arr[1] = 1;
    arr[2] = 2;
    for(let i = 3; i <= number; i++) {
        arr[i] = arr[i - 1] + arr[i - 2];
    }
    return arr[number]
}

9 變態跳臺階

題目描述

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

題目分析

根據上一個題目可以知道,青蛙只跳1或2可以得出是一個斐波那契問題,即a[n] = a[n-1] + a[n-2],那麼能跳1,2,3個臺階時a[n] = a[n-1] + a[n-2] + a[n-3] + ... + a[1]

那麼有:

a[n]   = a[n-1] + a[n-2] + ... + a[1]    ①
a[n-1] = a[n-2] + a[n-3] + ... + a[1]    ②

② - ①可得:a[n] = 2*a[n-1]

代碼

function jumpFloorII(number)
{
    // write code here
    let acc = 1;
    while(--number) {
        acc = acc * 2;
    }
    return acc;
}

10 矩形覆蓋

題目描述

我們可以用21的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個21的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?

題目分析

其本質就是一個斐波那契數列
在這裏插入圖片描述

代碼

function rectCover(number)
{
    // write code here
    var arr = [];
    arr[0] = 0;
    arr[1] = 1;
    arr[2] = 2;
    for (let i = 3; i <= number; i++) {
        arr[i] = arr[i - 2] + arr[i - 1];
    }
    return arr[number];
}

11 二進制中1的個數

題目描述

題目分析

如果一個整數與1做與運算的結果是1,則表示該整數最右邊一位是1,否則是0

那麼解法就出來了:一個一個向右移位,並且判斷最右邊的那個位是否爲1,爲1就count++

但是這樣輸入負數時會陷入死循環,因爲負數右移時,最高位補得是1,那麼這樣會有無數個1

此時這時候有兩個解決辦法:

  1. 既然不能對要操作的數一個一個右移位,那麼我們可以考慮對另一個數1進行左移位計算
  2. 把一個整數減去1,再和原整數做與運算,會把該整數最右邊一個1變成0。那麼一個整數的二進制有多少個1,就可以進行多少次這樣的操作

代碼

方法1

function NumberOf1(n) {
    let count = 0;
    let flag = 1;
    while (flag) {
        // 循環的次數等於整數二進制的位數,32位的整數需要循環32位
        if (flag & n) count++;
        flag = flag << 1;
    }
    return count;
}

方法2

function NumberOf1(n) {
    let count = 0;
    while (n) {
    	// 有幾位就循環幾次,效率高
        n = n & n - 1
        count++;
    }
    return count;
}

Java

public class Solution {
    public int NumberOf1(int n) {
        int count = 0;
        int flag = 1;
        while(flag != 0) {
            if((n & flag) != 0) {
                count++;
            }
            flag = flag << 1;
        }
        return count;
    }
}

12 數值的整數次方

題目描述

給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。

題目分析

在這裏插入圖片描述

  • js中所有數字都是浮點數,所以3 / 2 === 1.5,所以在進行位運算和乘除運算時,最好都使用parseInt()
  • 用右移運算(>>)代替除運算(/),所以parseInt(3) / 2 === parseInt(3) >> 1,直接3 >> 1也可以,但是浮點數位運算效率十分低
  • 用位與運算代替求餘運算(%),所以parseInt(3) % 2 === parseInt(3) & 1,直接3 & 1也可以,但是浮點數位運算效率十分低

代碼

function Power(base, exponent) {
  // write code here
  if (exponent === 0) return 1;
  if (exponent === 1) return base;
  var isNegative = false;
  if (exponent < 0) {
    exponent = -exponent;
    isNegative = true;
  }
  var pow = Power(base * base, parseInt(exponent) >> 1);
  if (parseInt(exponent) & 1 !== 0) pow = pow * base;
  return isNegative ? 1 / pow : pow;
}

13 調整數組順序使奇數位於偶數前面

題目描述

輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。

題目分析

兩個變量作爲奇數和偶數的下標,我們可以維護兩個指針

  • 第一個指針初始化時指向數組的第一個數字,它只向後移動
  • 第二個指針初始化時指向數組的最後一個數字,它只向前移動

在兩個指針相遇之前,第一個指針總是位於第二個指針的前面。如果第一個指針指向的數字是偶數,並且第二個指針指向的數字是奇數,則交換這兩個數字

在這裏插入圖片描述

代碼

function reOrderArray(array)
{
    // write code here
    let oddbegin = 0;
    let oddcount = 0;
    let arr = [];
    for (let i = 0; i < array.length; i++) {
        if(array[i] & 1) oddcount++
    }
    for (let i = 0; i < array.length; i++) {
        if (array[i] & 1){
            arr[oddbegin++] = array[i];
        } else{
            arr[oddcount++] = array[i];
        }
    }
    return arr;
}

14 鏈表中倒數第k個節點

題目描述

輸入一個鏈表,輸出該鏈表中倒數第k個結點。

題目分析

在這裏插入圖片描述

代碼

我的解法

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function FindKthToTail(head, k)
{
    // write code here
    if (head == null || k <= 0) return null;
    let arr = [];
    while(head) {
        arr.push(head);
        head = head.next;
    }
    return arr[arr.length - k];
}

引用類型不是共享內存嗎?應使用淺拷貝

/* function ListNode(x){
 this.val = x;
 this.next = null;
 }*/
function FindKthToTail(head, k) {
  if (head === null || k <= 0) return null;
  let pNode1 = head;
  let pNode2 = head;
  while (--k) {
    if (pNode2.next !== null) {
      pNode2 = pNode2.next;
    } else {
      return null;
    }
  }
  while (pNode2.next !== null) {
    pNode1 = pNode1.next;
    pNode2 = pNode2.next;
  }
  return pNode1;
}

15 反轉鏈表

題目描述

輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。

題目分析

在這裏插入圖片描述
所以第一步要把當前節點的next記住

定義3個指針

  • 當前遍歷到的節點
  • 它的前一個節點
  • 它的後一個節點

代碼

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function ReverseList(pHead)
{
    // write code here
    // 鏈表題都要判斷邊界條件,下句比較通用,都可以寫
    if (!pHead || !pHead.next) return pHead;
   	// 記錄當前節點
    let current = pHead;
    // 記錄當前節點的前一個節點
    let pre = null;
    // 記錄當前節點的後一個節點
    let next;
    
    while(current) {
    	// 先記錄當前節點的下一個節點,到時候斷掉就找不到了
        next = current.next;
        // 將當前節點的下一節點指向前一個節點
        current.next = pre;
        // 前一個節點後移1位
        pre = current;
        // 當前節點後移1位
        current = next;
    }
    return pre;
}

16 合併兩個排序的鏈表

題目描述

輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調遞增規則。

題目分析

運用遞歸
在這裏插入圖片描述

代碼

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function Merge(pHead1, pHead2)
{
    // write code here
    if(!pHead1) return pHead2;
    if(!pHead2) return pHead1;
    let pMergeHead = null;
    if (pHead1.val < pHead2.val) {
        pMergeHead = pHead1;
        pMergeHead.next = Merge(pHead1.next, pHead2);
    } else {
        pMergeHead = pHead2;
        pMergeHead.next = Merge(pHead1, pHead2.next);
    }
    return pMergeHead;
}

17 樹的子結構❓

題目描述

輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

題目分析

分析如何判斷樹B是不是樹A的子結構,只需要兩步。很容易看出來這是一個遞歸的過程。一般在樹的求解方面都和遞歸有關。

  1. 在樹A中找到和B的根結點的值一樣的結點R
  2. 判斷樹A中以R爲根結點的子樹是不是包含和樹B一樣的結點

代碼

/* function TreeNode(x) {
 this.val = x;
 this.left = null;
 this.right = null;
 } */
function HasSubtree(pRoot1, pRoot2) {
  let res = false;
  if (pRoot1 === null || pRoot2 === null) return false;
  if (pRoot1.val === pRoot2.val) res = doesTree1HasTree2(pRoot1, pRoot2);
  if (!res) res = HasSubtree(pRoot1.left, pRoot2);
  if (!res) res = HasSubtree(pRoot1.right, pRoot2);
  return res;
}

function doesTree1HasTree2(pRoot1, pRoot2) {
  if (pRoot2 === null) return true;
  if (pRoot1 === null) return false;
  if (pRoot1.val !== pRoot2.val) return false;
  return doesTree1HasTree2(pRoot1.left, pRoot2.left) && doesTree1HasTree2(pRoot1.right, pRoot2.right);
}

18 二叉樹的鏡像

題目描述

操作給定的二叉樹,將其變換爲源二叉樹的鏡像
二叉樹的鏡像定義:源二叉樹
在這裏插入圖片描述

題目分析

遞歸交換左右節點

代碼

/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
function Mirror(root)
{
    // write code here
    if(root === null) return;
    if(root.left === null && root.right === null) return;
    let temp = root.left;
    root.left = root.right;
    root.right = temp;
    if(root.left) Mirror(root.left);
    if(root.right) Mirror(root.right);
}

19 順時針打印矩陣

題目描述

輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字,例如
在這裏插入圖片描述

題目分析

用左上和右下的座標定位出一次要旋轉打印的數據,一次旋轉打印結束後,往對角分別前進和後退一個單位。

代碼

function printMatrix(matrix)
{
    // write code here
    if(matrix == null) return;
    const rows = matrix.length;
    const cols = matrix[0].length;
    let start = 0;
    let result = [];
    while (rows > start * 2 && cols > start * 2) {
        result = result.concat(printMatrixInCircle(matrix, rows, cols, start));
        start++;
    }
    return result;
}

function printMatrixInCircle(matrix, rows, cols, start) {
    const endX = cols - 1 - start;
    const endY = rows - 1 - start;
    let result = [];
    // 從左往右
    for (let i = start; i <= endX; i++) {
        result.push(matrix[start][i]);
    }
    // 從上往下
    for(let i = start + 1; i <= endY; i++) {
        result.push(matrix[i][endX]);
    }
    // 從右往左
    for(let i = endX - 1; i >= start && endY > start; i--) {
        result.push(matrix[endY][i]);
    }
    // 從下往上
    for(let i = endY -1; i >= start + 1 && endX > start; i--) {
        result.push(matrix[i][start]);
    }
    return result;
}

20 包含min函數的棧   中等  輔助棧

21 棧的壓入、彈出序列   中等 輔助棧

22 從上往下打印二叉樹  簡單  廣度遍歷、隊列

23 二叉樹的後續遍歷序列  中等  畫圖

24 二叉樹和爲某一值的遍歷序列  中等  深度遍歷、遞歸

25 複雜鏈表的複製   難  map保存<N,N’> || N->N’得S->S‘

26 二叉搜索樹與雙向鏈表  中等偏難  遞歸、中序遍歷

27 字符串的排列  難  回溯法 || 遞歸全排列法

28 數組中出現次數超過一半的數  中等  partion法 || times變量變化法

29 最小的k個數   中等  partion法

30 連續子數組的最大值  中等  找規律、動態規劃、注意判斷條件

31 (~n整數中1出現的次數   中等  位運算 || 數學分析

把數組排成最小的數  簡單偏難  改變排序規則

醜數   難  動態規劃、注意判斷條件

第一個只出現一次的字符   哈希表記錄

數組中的逆序對  難+  基於歸併排序、臨時數組

兩個鏈表中的第一個公共節點   簡單  雙指針法

數字在排序數組中出現的次數   簡單偏難  二分法改造

二叉樹的深度  簡單  遞歸

平衡二叉樹  簡單  遞歸

數組中只出現一次的數字  簡單  indexOf || map記錄 || 異或

和爲S的連續正數序列  中等  數學分析

和爲S的字符串   簡單  雙指針

左旋轉字符串  簡單  裁剪拼接

單次翻轉序列  簡單  轉數組,對每項反序

撲克牌順子  中等  注意題目條件、位運算判斷數字重複

孩子們的遊戲  難  數學分析得出公式 || 畫圖按題目做、注意下標

求1+2+3+…+n  中等  位運算、遞歸

不用加減乘除做加法  中等  位運算

把字符串轉成整數  中等  位運算

數組中重複的數字  中等  將值放到對應位置上

構建乘積數組  中等偏上  藉助中間變量存儲後面的乘積

正則表達式的匹配  難  注意判斷條件、遞歸

表示數值的字符串  中等  正則

字符流中第一個不重複的數字  中等  map記錄 || indexOf法

鏈表中環的入口節點  中等  雙指針法、數學分析

刪除鏈表中重複的節點  中等  加頭節點、注意多個重複

二叉樹的下一個節點   中等  畫圖、分析各種情況

對稱的二叉樹   中等  遞歸、對稱遍歷

按之字形順序打印二叉樹   難  廣度遍歷、兩個棧

把二叉樹打印成多行   中等偏難  隊列+兩個記錄變量

序列化二叉樹   中等  數組代表流、遞歸

二叉搜索樹的第k個節點   中等  中序遍歷+計數變量

數據流的中位數   中等  partion法 || 維持排序 || 排序鏈表法 || AVL樹 || 最大堆和最小堆

滑動窗口中的最大值   難  改變參考對象、雙端隊列、存下標

矩陣中的路徑   中等  回溯法

機器人的運動範圍   中等  回溯法

參考資料

[1] https://www.cnblogs.com/wuguanglin/p/code-interview.html
[2] https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/劍指 Offer 題解 - 目錄.md
[3] https://www.cnblogs.com/wuguanglin/p/SummaryOfJSDoAlgorithmProblem.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章