算法练习笔记

463 阅读5分钟

前言

由于很多小伙伴看了 《普通人如何进大厂》这篇文章对我的笔记比较感兴趣,所以我花时间整理提炼了一下跟大家分享,不保证内容完全正确,仅供参考哈

本文内容来自修言的《前端算法与数据结构面试:底层逻辑解读与大厂真题训练》掘金小册子的核心总结,侵权删。

其他笔记传送门

数据结构类型:数组、栈、队列、链表、树

栈 - 先进后出

对应数组的 进:push ,出:pop

进栈出栈

let stack = []
stack.push('a')
stack.push('b')
stack.push('c')

while(stack.length){
  const top = stack[stack.length-1]
  console.log('出栈顺序',top)
  stack.pop()
}

队列 queue - 先进先出

对应数组的进:push ,出:shift

排队出队

let queue = []
queue.push('a')
queue.push('b')
queue.push('c')

while(queue.length){
  const top = queue[0]
  console.log('出栈顺序',top)
  stack.shift()
}

链表

function ListNode(val) {
    this.val = val;
    this.next = null;
}
const node = new ListNode(1)
node.next = new ListNode(2)

形成一个2个结点的链表

查找链表的第10个元素

let index = 10
let node = head

while(index<10 && node){
node = node.next
 index++
}

二叉树

1、是一个空树

2、不是空树,则必须由跟结点和左结点和右结点组成,并且子结点也是二叉树

二叉树的遍历

  1. 递归遍历
  • 前序遍历
  • 中序遍历
  • 后序遍历
  1. 迭代遍历
  • 层次遍历
// 先序遍历
function preorder(root){
  if(!root) return
  console.log(root.val)
  preorder(root.left)
  preorder(root.right)
}

// 中序遍历
function inorder(root){
  if(!root) return
  preorder(root.left)
 console.log(root.val)
  preorder(root.right)
}
// 后序遍历
function postorder(root){
  if(!root) return
  preorder(root.left)
  preorder(root.right)
 console.log(root.val)
}

数组的应用

合并两个有序数组-双指针

const merge = function(nums1,m,num2,n){
 let i = m-1,j=n-1,k=m+n-1
 while(i>=0 && j>=0){
    if(nums1[i]>=nums2[j]){
      nums1[k]=nums1[i]
      i--
    }else{
      nums1[k]=nums2[j]
      j--
     }
   k--
 }
 while(j>=0){
  nums1[k]=nums2[j]
  k--
  j--
 }
}

三数求和-对碰指针

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const threeSum = function(nums) {
    // 用于存放结果数组
    let res = [] 
    // 给 nums 排序
    nums = nums.sort((a,b)=>{
        return a-b
    })
    // 缓存数组长度
    const len = nums.length
    // 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
    for(let i=0;i<len-2;i++) {
        // 左指针 j
        let j=i+1 
        // 右指针k
        let k=len-1   
        // 如果遇到重复的数字,则跳过
        if(i>0&&nums[i]===nums[i-1]) {
            continue
        }
        while(j<k) {
            // 三数之和小于0,左指针前进
            if(nums[i]+nums[j]+nums[k]<0){
                j++
               // 处理左指针元素重复的情况
               while(j<k&&nums[j]===nums[j-1]) {
                    j++
                }
            } else if(nums[i]+nums[j]+nums[k]>0){
                // 三数之和大于0,右指针后退
                k--         
               // 处理右指针元素重复的情况
               while(j<k&&nums[k]===nums[k+1]) {
                    k--
                }
            } else {
                // 得到目标数字组合,推入结果数组
                res.push([nums[i],nums[j],nums[k]])            
                // 左右指针一起前进
                j++  
                k--        
                // 若左指针元素重复,跳过
                while(j<k&&nums[j]===nums[j-1]) {
                    j++
                }  
               // 若右指针元素重复,跳过
               while(j<k&&nums[k]===nums[k+1]) {
                    k--
                }
            }
        }
    } 
    // 返回结果数组
    return res
};

字符串的应用

判断是否回文

// 简单方式
function isPalindrome(str){ return str.split('').reverse().join()===srt}
// 对折方式
function isPalindrome(str){ 
const len =str.length
 for(let i =0;i<len/2;i++){
   if(srt[i]!==str[len-i-1]){
    return false
   }
  }
 return true
}

删除一个数字是否成为回文

原理:对折判断字符串是否相等,如不想等则往前或者往后跳一步,若前后组成的字符串是回文则命题成立

  var validPalindrome = function (str) {
        let len = str.length;
        let i = 0,
          j = len - 1;
        while (i < j && str[i] === str[j]) {
          i++;
          j--;
        }
        if (isPalindrome(i + 1, j)) {
          return true;
        }
        if (isPalindrome(i, j + 1)) {
          return true;
        }
        return false;
        function isPalindrome(start, end) {
          while (start < end) {
            if (str[start] !== str[end]) {
              return false;
            }
            start++;
            end--;
          }
          return true;
        }
      };

取整数中的数字

   const myAtoi=function(str){
        const max=Math.pow(2,31)-1,min=-max-1
        const group=str.match(/^\s*([-+]?[0-9]+).*$/)
        let num=0
        if(group) {
          num=+group[1]
        }
        if(num>max){num=max}
        if(num<min){num=min}
        return num
     }

链表的应用

链表合并

const mergeTwoLists = function(l1, l2) {
  // 定义头结点,确保链表可以被访问到
  let head = new ListNode()
  // cur 这里就是咱们那根“针”
  let cur = head
  // “针”开始在 l1 和 l2 间穿梭了
  while(l1 && l2) {
      // 如果 l1 的结点值较小
      if(l1.val<=l2.val) {
          // 先串起 l1 的结点
          cur.next = l1
          // l1 指针向前一步
          l1 = l1.next
      } else {
          // l2 较小时,串起 l2 结点
          cur.next = l2
          // l2 向前一步
          l2 = l2.next
      } 
      // “针”在串起一个结点后,也会往前一步
      cur = cur.next 

  }
  // 处理链表不等长的情况
  cur.next = l1!==null?l1:l2
  // 返回起始结点
  return head.next
};

删除重复的值

      function delDuplicates(head) {
        let curr = head;
        while (curr && curr.next) {
          if (curr.val === curr.next.val) {
            curr.next = curr.next.next;
          } else {
            curr = curr.next;
          }
        }
        return head;
      }

快慢指针

     // 删除第n个结点
      function delDuplicatesII(head,n) {
        let dummy = new NodeList()
        dummy.next = head
        let fast = dummy,slow=dummy
        while(n>0){
          fast=fast.next
          n--
        }
        while(fast.next){
          fast=fast.next
          slow = slow.next
        }
        slow.next=slow.next.next
        return dummy.next
      }

多指针

// 反转链表
const reverseList=function(head){
  let cur=head
  let pre=null
  while(cur){
   let next = curr.next
   cur.next=pre
   pre=cur
   cur = next
}
 return pre
}

环形链表

// 求环形起点
const cycleList = function(head){
  while(head){
    if(head.flag){ return head}
    else{ head.flag = true;
        head = head.next
        } 
   } 
  return null
}

栈与队

有效括号-对称性

const isValid = function(s){
  let letftToright = {
   '(':')',
   '{':'}',
   '[':']'
  }
 let res =[]
 for(let i =0;i<s.length;i++){
   if(s[i]==='('||s[i]==='{'||s[i]==='['){
     res.push(letftToright[s[i]])
   }else{
    if(res.length<1 || s[i]!==res.pop()){
     return false
   }
  }
 }
 return true
}

每日温度

维持一个递减序列

const dailyTemprature = function(arr){
   let res = (new Array(arr.length)).fill(0) 
   let stack = []
   for(let i =0;i<arr.length;i++){
    while(stack.length>0 && arr[stack[stack.length-1]]<arr[i]) {
     let j= stack.pop()
     res[j]=i-j
    }
    stack.push(i)
   }
  return res
}

最小栈问题

var MinStack = function() {
    this.stack=[]
    this.stack2=[]

};
MinStack.prototype.push = function(x) {
    this.stack.push(x)
    if(this.stack2.length===0 || this.stack2[this.stack2.length-1]>=x){
      this.stack2.push(x)
    }
};
MinStack.prototype.pop = function() {
 if(this.stack.pop()===this.stack2[this.stack2.length-1]){
   this.stack2.pop()
 }
};
MinStack.prototype.top = function() {
  return this.stack[this.stack.length-1]
};
MinStack.prototype.getMin = function() {
  return this.stack2[this.stack2.length-1]
};

滑窗最大值

const maxSlidingWindow = function (nums, k) {
  // 缓存数组的长度
  const len = nums.length;
  // 初始化结果数组
  const res = [];
  // 初始化双端队列
  const deque = [];
  // 开始遍历数组
  for (let i = 0; i < len; i++) {
    // 当队尾元素小于当前元素时
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      // 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
      deque.pop();
    }
    // 入队当前元素索引(注意是索引)
    deque.push(i);
    // 当队头元素的索引已经被排除在滑动窗口之外时
    while (deque.length && deque[0] <= i - k) {
      // 将队头元素索引出队
      deque.shift();
    }
    // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  // 返回结果数组
  return res;
};

DFS 与 BFS

dfs 和二叉树先序遍历类似,不撞南墙不回头,穷举首选

// 所有遍历函数的入参都是树的根结点对象
function preorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历左子树 
    preorder(root.left)  
    // 递归遍历右子树  
    preorder(root.right)
}

BFS实战:二叉树的层序遍历

function bfs(root){
    let queue=[]
    queue.push(root)
    while(queue.length>0){
      let top = queue[0]
      console.log('top',top.val)
      if(top.left) { queue.push(top.left)}
      if(top.right) { queue.push(top.right)}
       queue.shift()
    }
}

递归和回溯

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。

序列全排

要点:

  • 穷举首先想到 dfs

  • 触底条件是遍历层数>nums.length

  • 返回递归的路径,用 visited 记录当前路径节点避免重复,完成之后释放进行下一个路径

      const premute = function (nums) {
        let path = [],
          res = []
        let visited = {}
        let len = nums.length
        dfs(0)
        function dfs(nth) {
          if (nth === len) {
            res.push(path.slice())
            return
          }
          for (let i = 0; i < len; i++) {
            if (!visited[nums[i]]) {
              visited[nums[i]] = true             
              path.push(nums[i])
              dfs(nth + 1)
              path.pop()
              visited[nums[i]] = false
            }
          }
        }
      }

子组合问题

要点:

  • 穷举首先想到 dfs
  • dfs 是基于 i 非层级
const subsets= function(nums){
  const res = []
  const len = nums.length
  const subset=[]
  dfs(0)
  function dfs(index){
    res.push(subset.slice())
   for(let i=index;i<len;i++){
    subset.push(nums[i])
    dfs(i+1)
    subset.pop()
   }
  }
 return res
}

限定组合问题

要点:

  • 穷举首先想到 dfs

  • dfs 是基于 i 非层级

  • 触底条件为 组合长度===k

// 
const combine(num,k){
  const res = []
  const subset=[]
  dfs(1)
  function dfs(index){
   if(subset.length===k){
     res.push(subset.slice())
     return
   }
   for(let i=index;i<=num;i++){
    subset.push(i)
    dfs(i+1)
    subset.pop()
   }
  }
 return res
}

二叉树问题

迭代方式实现先序排序

要点:利用栈后进先出原理,先压入右结点再压入左结点,不断出栈,取栈顶结点遍历,重复以上过程

    function preorderTraversal(root) {
      let res = [];
      if (!root) return res;
      let stack = [];
      stack.push(root);
      while (stack.length) {
        let top = stack.pop();
        res.push(top.val);
        if (top.right) {
          stack.push(top.right);
        }
        if (top.left) {
          stack.push(top.left);
        }
      }
      return res;
    }

迭代方式实现后序排序

要点:

  • 先序排序为根->左->右,而后序排序为左->右->根

  • 通过相反的添加数据,即头部添加 unshift 方式实现右->左->根

  • 通过调换一下左右结点入栈顺序即可实现左->右->根

    function postorderTraversal(root) {
      let res = [];
      if (!root) return res;
      let stack = [];
      stack.push(root);
      while (stack.length) {
        let top = stack.pop();
        res.unshift(top.val);
        if (top.left) {
          stack.push(top.left);
        }
        if (top.right) {
          stack.push(top.right);
        }     
      }
      return res;
    }

迭代方式实现中序排序

要点:

  • 通过遍历左结点并推入栈中

  • 当到达叶子结点取值,然后通过 pop 回溯到父结点

  • 取当前结点值,然后遍历右结点

   function inorderTraversal(root) {
      let res = [];
      if (!root) return res;
      let stack = [];
      let cur = root
      while (cur || stack.length) {
        while(cur.left){ // 找到叶子结点,并把左结点入栈
         stack.push(cur.left)
          cur=cur.left
        }
        cur=stack.pop()
        res.push(cur.val)
        cur=cur.right // 遍历右结点   
      }
      return res;
    }

5大排序

冒泡排序

要点:每轮相邻前后的值比较如后边的值比前边的值小,则交互位置

每轮获取一个最大值

复杂度:最优n,平均n^2

  function bubbleSort(arr) {
      const len = arr.length;
      for (let i; i < len; i++) {
        for (let j = 0; j < len - i - 1; j++) {
          if (arr[j] > arr[j + 1]) {
            [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
          }
        }
      }
      return arr;
    }

选择排序

要点:找最小值

每轮记一个最小值,若当前比最小值小,则更改最小值

复杂度:最优n^2,平均n^2

    function selectSort(arr) {
      const len = arr.length;
      let minIndex = 0;
      for (let i = 0; i < len; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
          if (arr[j] < arr[minIndex]) {
            minIndex = j;
          }
        }
        if (minIndex !== i) {
          [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
        }
      }
      return arr;
    }

插入排序

要点:找到当前元素插入到,插入到前面有序列表中正确到位置

复杂度:最优n,平均n^2

    function insertSort1(arr) {
      const len = arr.length;
      for (let i = 1; i < len; i++) {
        let j = i ;
        let temp = arr[i];
        while (j > 0 && arr[j-1] > temp) {
          arr[j] = arr[j - 1];
          j--;
        }
        arr[j] = temp;
      }
      return arr;
    }

    // 插入排序
    function insertSort(arr) {
      const len = arr.length;
      for (let i = 0; i < len; i++) {
        for (let j = i + 1; j > 0; j--) {
          // console.log('ij',i,j)
          if (arr[j] < arr[j - 1]) {
            let temp = arr[j];
            arr[j] = arr[j - 1];
            arr[j - 1] = temp;
            // console.log('arr[j]',j,arr[j-1],arr[j])
            // [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
          }
        }
      }
      return arr;
    }

归并排序

分割数组为左右组,知道分割长度变成1,则合并

    function merge(l1, l2) {
      console.log(l1, ':', l2);
      let len1 = l1.length;
      let len2 = l2.length;
      let i = 0;
      let j = 0;
      let res = [];
      while (len1 > i && len2 > j) {
        if (l1[i] < l2[j]) {
          res.push(l1[i]);
          i++;
        } else {
          res.push(l2[j]);
          j++;
        }
      }
      if (i < len1) {
        return res.concat(l1.slice(i));
      } else {
        return res.concat(l2.slice(j));
      }
    }
    function mergeSort(arr) {
      const len = arr.length;
      if (len <= 1) return arr;
      const mid = parseInt(len / 2);
      let leftArr = mergeSort(arr.slice(0, mid));
      let rightArr = mergeSort(arr.slice(mid, len));
      arr = merge(leftArr, rightArr);
      return arr;
    }

快速排序

要点:不断把数组拆分,设置基准值,小的值放左边大的值放右边,

细分回溯合并

    function quickSort(arr) {
      let len = arr.length;
      if (len <= 1) return arr;
      const index = parseInt(arr.length / 2);
      const flagValue = arr[index];
      let leftArr= [];
      let rightArr=[]
      for (let i = 0; i < len; i++) {
        if(i===index) continue
        if(arr[i]<=flagValue){
          leftArr.push(arr[i])
        }else{
          rightArr.push(arr[i])
        }
      }
      return [...quickSort(leftArr),flagValue,...quickSort(rightArr)]
    }

动态规划

运用条件:

  • 要求你给出达成某个目的的解法个数
  • 不要求你给出每一种解法对应的具体路径

思想:找状态转移和边界

树形思维模型将帮助我们更迅速地定位到状态转移关系,边界条件往往对应的就是已知子问题的解

最少硬币数

原理:

  • 找拿了一个硬币之后硬币总数最小值

  • 最小硬币数 关键找底和底-1的硬币数最小值 Math.min(f[i], f[i - coins[j]]+1)

  • 例如 36 则 min(min(36-1),min(36-2),min(36-5))

  const coinChange = function (coins, amount) {
        let f = [];
        f[0] = 0;
        for (let i = 1; i <= amount; i++) {
          f[i] = Infinity;
          for (let j = 0; j < coins.length; j++) {
            if (coins[j] <= i) {
              f[i] = Math.min(f[i], f[i - coins[j]] + 1);
            }
          }
        }
        if (f[amount] === Infinity) return -1;
        return f[amount];
      };