前端面试拼图-数据结构与算法(二)

摘要:最近,看了下慕课2周刷完n道面试题,记录下...

1. 求一个二叉搜索树的第k小值

        二叉树(Binary Tree)

        是一棵树

        每个节点最多两个子节点

        树节点的数据结构{value, left?, right?}

        二叉树的遍历

        前序遍历:root→left→right

        中序遍历:left→root→right

        后序遍历:left→right→root

        二叉搜索树BST(Binary Search Tree)

        left(包括其后代) value ≤ root value

        right (包括其后代) value ≥ root value

        可使用二分法进行快速查找

        解题思路:BST中序遍历,从小到大的排序

                          找到排序后的第k个值

/**
* 二叉搜索树
*/
interface ITreeNode {
  value: number
  left: ITreeNode | null
  right: ITreeNode | null
}

const arr: number[] = []

/**
* 二叉树前序遍历
*/
function preOrderTraverse(node: ITreeNode | null) {
  if ( node == null) return
  //console.log(node.value)
  arr.push(node.value)
  preOrderTraverse(node.left)
  preOrderTraverse(node.right)
}

/**
* 二叉树中序遍历
*/
function inOrderTraverse(node: ITreeNode | null) {
  if ( node == null) return
  inOrderTraverse(node.left)
  // console.log(node.value)
  arr.push(node.value)
  inOrderTraverse(node.right)
}

/**
* 二叉树后序遍历
*/
function postOrderTraverse(node: ITreeNode | null) {
  if ( node == null) return
  postOrderTraverse(node.left)
  postOrderTraverse(node.right)
  // console.log(node.value)
  arr.push(node.value)
}

/**
* **寻找BST中的第k小值**
*/
function getKthValue(node: ITreeNode, k: number): number | null {
  inOrderTraverse(node)
  console.log(arr)
  return arr[k-1] || null
}

const bst: ITreeNode = {
  value: 5,
  left: {
    value: 3,
    left: {
      value: 2,
      left: null,
      right: null
    },
    right: {
      value: 4,
      left: null,
      right: null
    }
  },
  right: {
      value: 7,
      left: {
        value: 6,
        left: null,
        right: null
      },
      right: {
        value: 8,
        left: null,
        right: null
      }
   }
}

//preOrderTraverse(tree)

平衡二叉树 | HZFE - 剑指前端 Offer题目描述icon-default.png?t=N7T8https://febook.hzfe.org/awesome-interview/book1/algorithm-balanced-binary-trees        扩展:为何二叉树如此重要,而不是三叉树、四叉树?

        性能、性能、还是性能!重要的事情说三遍

        数组:查找快O(1),增删慢O(n);链表:查找慢O(n),增删快O(1)

        二叉搜索树BST:查找快、增删快—"木桶效应"

        平衡二叉树

        BST如果不平衡,那就又成了链表

        所以要尽量平衡:平衡二叉搜索树BBST(其增删查,时间复杂度都是O(logn),即树的高度)

        红黑树:本质是一种自平衡二叉树

        分为红/黑两种颜色,通过颜色转换来维持输的平衡

        相对于普通平衡二叉树,它维持平衡的效率更高

        B树

        物理上是多叉树,但逻辑上是二叉树

        一般用于高效I/O, 关系型数据库常用B树 来组织数据

        扩展2:堆有什么特点?和二叉树又什么关系?

        堆栈模型

        JS执行时,值类型变量,存储在栈中;引用类型变量,存储在堆中

        堆是完全二叉树

        最大堆:父节点 ≥子节点

        最小堆:父节点≤子节点

        满二叉树(又叫完美二叉树):所有层的节点都被填满;

        完全二叉树:最底层节点靠左填充,其它层节点全被填满

7.1   二叉树 - Hello 算法动画图解、一键运行的数据结构与算法教程icon-default.png?t=N7T8https://www.hello-algo.com/chapter_tree/binary_tree/#1_1        逻辑结构 VS 物理结构

        堆:逻辑结构是一颗二叉树,但它的物理结构式一个数组

        堆的使用场景

        特别适合"堆栈模型"

        堆的数据,都是在栈中引用的,不需要从root遍历

        堆恰巧是数组形式,根据栈的地址,可用O(1)找到目标

2. JS计算斐波那契数列的第n个值

/**
* 斐波那契额数列(递归)
*/
function fibonacci(n:number): number{
  if(n <=1 ) return n
  return fibonacci(n-1) + fibonacci(n-2)
}

        递归有大量重复计算,其时间复杂度是O(2^n)

        优化:不用递归用循环,记录中间结果,时间复杂度O(n)

/**
* 斐波那契额数列(循环)
*/
function fibonacci(n:number): number{
  if(n <=1 ) return n
  let n1 = 1  //记录n-1的结果
  let n2 = 0  //记录n-2的结果
  let res = 0
  
  for(let i = 2; i <= n; i++) {
    res = n1 + n2;
    // 记录中间结果
    n2 = n1
    n1 = res
  } 
  return res
}

        动态规划:

        把一个大问题,拆解为一个小问题,逐级向下拆解

        用递归的思想去分析问题,再改用循环来实现

        算法三大思维:贪心、二分、动态规划

        扩展:青蛙跳台阶问题,一只青蛙,一次可跳1级,也可跳两级,请问青蛙跳到n级台阶,总共有多少种方式?

        第一次跳1级则有f(n-1)种方式,跳2级则有f(n-2)种方式,则结果和斐波那契额数列一样。

3. 将数组的0 移动到末尾

        如输入[1,0,3,0,11,0],输出[1,3,11,0,0,0],只移动0,其他顺序不变;必须在原数组进行操作

        传统思路

        遍历数组,遇到0则push到数组末尾

        用splice截取当前元素

        时间复杂度O(n^2)—算法不可用

        数组是连续存储,要慎用splice unshift 等API

/**
* 移动0到数组末尾(嵌套循环)
*/
function moveZero1(arr:number[]):void {
  const length = arr.length
  if(length === 0) return
  
  let zeroLength = 0
  // **O(n^2)**
  for (let i = 0; i < length - zeroLength; i++) {
    if (arr[i] === 0) {
      arr.push(0)
      arr.splice(i,1)  // 本身就有O(n)
      i-- //数组接去了一个元素,i要递减,否则连续0就会有错误
      zeroLength++ // 累加0的长度
    }
  }
}

        双指针思路(解决嵌套循环的有效)

        定义j指向第一个0,i指向j后面的第一个非0

        交换i和j的值,继续向后移动

        只遍历一次,所以时间复杂度是O(n)

/**
* 移动0到数组末尾(双指针)
*/
function moveZero2(arr:number[]):void {
  const length = arr.length
  if(length === 0) return
  
  let i
  let j = -1 // 指向第一个0
  for(i=0; i < length; i++) {
    if(arr[i] === 0) {
      if (j < 0) {   // 第一个0
        j = i
      }
    }
    if(arr[i] !== 0 && j >=0 ) {
      const n = arr[i]
      arr[i] = arr[j]
      arr[j] = n
      j++
    }
  }
}

4. 获取字符串中连续最多的字符,以及次数

        如输入'abbcccddeeee1234',计算得到连输最多的字符是'e',为4次

        传统思路

        嵌套循环,找出每个字符的连续次数,并记录

        看似时间复杂度是O(n^2)

        但实际时间复杂度是多少?—O(n),因为有'跳步'

/**
* 求连续最多的字符和次数(嵌套循环)
*/
interface IRes {
  char: string
  length: number
}
function findContinuousChars1(str:string):IRes {
  const res:IRes = {
    char: '',
    length: 0
  }
  const length = str.length
  if (length === 0) return res
  
  let tempLength = 0 //临时记录当前连续字符串的长度
  // 时间复杂度O(n)
  for(let i = 0; i < length; i++) {
    tempLength = 0 // 重置
    for(let j = i; j < length; j++) {
      if (str[i] === str[j]) {
        tempLength++
      }
      if(str[i] !== str[j] || j === length-1) {
        // 不相等,或者已经到最后一个元素。要去判断最大值
        if (tempLength > res.length) {
          res.char = str[i]
          res.length = tempLength
        }
        if (i < length - 1) {
          i = j -1   // 跳步
        }
        break
      }
    }
  }  
  return res
}

        双指针思路(适用于解决嵌套循环类问题)

        定义指针i和j;j不动,i继续移动

        如果i和j的值一直相等,则i继续移动

        直到i和j的值不相等,记录处理,让j追上i。继续第一步

/**
* 求连续最多的字符和次数(双指针)
*/
interface IRes {
  char: string
  length: number
}
function findContinuousChars2(str:string):IRes {
  const res:IRes = {
    char: '',
    length: 0
  }
  const length = str.length
  if (length === 0) return res
  
  let tempLength = 0 //临时记录当前连续字符串的长度
  // 时间复杂度O(n)
  let i = 0
  let j = 0
  for(; i < length; i++) {
    if(str[i] === str[j]) {
      tempLength++
    }
    if(str[i] !== str[j] || i === length-1) {
      // 不相等,或者i到了字符串的末尾
      if(tempLength > res.length) {
        res.char = str[j]
        res.length = tempLength
      }
      tempLength = 0  //重置长度
      
      if(i < length - 1) {
        j = i //让j"追上" i
        i--
      }
    }
  }
 
  return res
}

ps:算法题尽量使用低级的代码,慎用语法糖或者高级API

5. 用JS实现快速排序,并说明时间复杂度

        固定算法和思路

  • 找到中间位置midValue
  • 遍历数组,小于midValue放在left,否则放在right
  • 继续递归,最后concat拼接,返回

        获取midValue的两种方式:

        使用splice,会修改原数组

        使用slice,不会修改原数组 — 更推荐

/**
* 快速排序(使用splice)
*/
function quickSort1(arr: number[]):number[] {
  const length = arr.length
  if(length === 0) return arr
  
  const modIndex = Math.floor(length / 2)
  const midValue = arr.splice(midIndex, 1)[0]  // splice和slice返回的都是数组
  
  const left: number[] = []
  const right: number[] = []
  
  // O(n*logn)
  for(let i = 0; i < arr.length; i++) {  // 细节,不直接使用length是由于splice已经改变数组
    const n  = arr[i]
    // 二分后递归遍历O(logn)
    if (n < midValue) {
      // 小于midValue, 则放在left
      left.push(n)
    } else{
      // 大于midValue,则放在right
      right.push(n)
    }
  }
  
  return quickSort1(left).concat([midValue], quickSort1(right))
}

/**
* 快速排序(使用slice)
*/
function quickSort2(arr: number[]):number[] {
  const length = arr.length
  if(length === 0) return arr
  
  const modIndex = Math.floor(length / 2)
  const midValue = arr.slice(midIndex, midIndex + 1)[0]  // splice和slice返回的都是数组
  
  const left: number[] = []
  const right: number[] = []
  
  for(let i = 0; i < length; i++) { 
    if (i !== midIndex) {
      const n  = arr[i]
      if (n < midValue) {
        // 小于midValue, 则放在left
        left.push(n)
      } else{
        // 大于midValue,则放在right
        right.push(n)
      }
    }    
  }
  
  return quickSort2(left).concat([midValue], quickSort2(right))
}

        快速排序,有遍历有二分时间复杂度为O(nlogn); 常规排序,嵌套循环,复杂度是O(n^2)

        此处,splice和slice没区分出来

        算法本身时间复杂度就够高O(nlogn)

        外加,splice是逐步二分后执行的,二分会快速消减数量级

        如果单独使用splice和slice,效果会很明显

6. 获取1-10000之前所有的对称数

        例如:1, 2, 11, 22, 101, 232, 1221……

        思路1 使用数组反转、比较

  • 数字转换为字符串,再反转为数组
  • 数组reverse,再join为字符串
  • 前后字符串进行比较
/**
* 查询1-max的所有对称数(数组反转)
*/
function findPalindromeNumbersa(max: number): number[] {
  const res: number[] = []
  if (max <= 0) return res
  
  for(let i = 1; i <= max; i++) {
    // 转换为字符串,转换为数组,再反转, 比较
    const s = i.toString()
    if (s === s.split('').reverse().join('')) {
      res.push(i)
    }
  }
  return res
}

        思路2 字符串头尾比较

        数字转换为字符串

        字符串头尾字符比较

        (也可以用栈,像括号匹配,但需要注意奇偶数 )

/**
* 查询1-max的所有对称数(字符串前后比较)
*/
function findPalindromeNumbersa(max: number): number[] {
  const res: number[] = []
  if (max <= 0) return res
  
  for(let i = 1; i <= max; i++) {
    // 转换为字符串,转换为数组,再反转, 比较
    const s = i.toString()
    const length = s.length
    
    // 字符串头尾比较
    let flag = true
    let startIndex = 0 //字符串开始
    let endIndex = length -1 //字符串结束
    while(startIndex < endIndex) {
      if (s[startIndex] != s[endIndex]) {
        flag = false
        break
      } else {
        // 继续比较
        startIndex++
        endIndex--
      }
    }
    if(flag) res.push(i)
  }
  return res
}

        思路3生成反转数

        使用%和Math.floor生成反转数

        前后数字进行对比(全程操作数字,没有字符串类型)

/**
* 查询1-max的所有对称数(反转数)
*/
function findPalindromeNumbers3(max: number): number[] {
  const res: number[] = []
  if (max <= 0) return res
  
  for(let i = 1; i <= max; i++) {
    let n = i
    let rev = 0 //存储反转数
    
    // 生成反转数
    while (n > 0) {
      rev = rev * 10 + n % 10
      n = Math.floor(n/10)
    }
    if(i === rev) res.push(i)
  }
  return res
}

        总结:思路1 看似是O(n)的复杂度,但是数组转换、操作都需要时间,所以慢;

        思路2和思路3,操作数字更快(电脑原型就是计算器);

        思路2中要用栈,不合适,因为栈一般也是数组实现的,会慢

        尽量不要转换数据结构,尤其是数组这种有序结构;尽量不要内置API,如reverse,不好识别复杂度;数字操作最快,其次是字符串

7. 如何实现高效的英文单词前缀匹配

        如有一个英文单词库(数组),里面有几十万格英文单词;输入一个字符串,快速判断是不是某个单词的前缀(说明思路不用写代码)

        常规思路

        第一,遍历单词库数组

        第二,indexOf判断前缀

        实际时间复杂度超过了O(n), 因为要考虑indexOf的计算量

        优化:

        英文字母一共26个,可以提前把单词库数组拆分为26个

        既然第一层拆分为26个,第二层、第三层,还可以继续拆分

        最后,把单词库拆分为一棵树(对象或者hash表取key时间复杂度为O(1))

        性能分析:

        如果遍历数组,时间复杂度至少O(n)起步(n是数组长度)

        而改为树,时间复杂度降低到O(m)(m为单词的长度)

        PS:哈希表(对象)通过key查询,时间复杂度O(1)

        总结:

        考虑优化原始数据结构(需要和面试官沟通确认)

        有明确的数据(如26个英文字母),考虑使用哈希表(对象)

        以空间换时间,定义数据结构最重要

8. 用JS实现数字千分位格式

        将数组千分位格式化,输出字符串;如数字12050100,输出字符串12,050,100(注意:逆序判断)

        常见思路

        转换为数组,reverse,每3位拆分

        使用正则表达式(慢)

        使用字符串拆分

/**
* 千分位格式化(使用数组)
*/
export function format1(n: number): string {
  n = Math.floor(n)  // 只考虑整数
  
  const s = n.toString()
  const arr = s.split('').reverse()
  return arr.reduce((prev,val,index) => {
    if (index % 3 === 0) {
      if (prev) {
        return val + ',' + prev
      } else {
        return val
      }
    } else {
      return val + prev
    }
  }, '')
}

/**
* 千分位格式化(字符串分析)
*/
export function format2(n: number): string {
  n = Math.floor(n)  // 只考虑整数
  
  let res = ''
  const s = n.toString()
  const length = s.length
  for (let i = length - 1; i >= 0; i--) {
    const j = length -i   //
    if (j % 3 ==== 0) {
      if (i === 0) {
        res = s[i] + res
      } else {
        res = ',' + s[i] + res
      }
    } else {
      res = s[i] + res
    }
  }
  return res
}

        性能分析

        使用数组,转换影响影响性能

        使用正则表达式,性能较差

        使用字符串,性能较好(推荐)

9. 用JS切换字母大小写

        输入一个字符串,切换其中的字母大小写:如,输入字符串12aBc34,输出字符串12ABC34

        常见思路

        正则表达式

        通过ASCII码判断

/**
* 切换字母大小写(正则表达式) 
*/
function switchLetterCase(s: string): string {
  let res = ''
  
  const length = s.length
  if (length === 0) return res
  
  const reg1 = /[a-z]/
  const reg2 = /[A-Z]/
  for (let  i = 0; i < length; i++) {
    const c = s[i]
    if (reg1.test(c)) {
      res += c.toUpperCas()
    } else if (reg2.test(c)) {
      res += c.toLowerCas()
    } else {
      res += c
    }
  }
  
  return res
}

/**
* 切换字母大小写(ASCII编码) 
*/
function switchLetterCase(s: string): string {
  let res = ''
  
  const length = s.length
  if (length === 0) return res
  
  for (let  i = 0; i < length; i++) {
    const c = s[i]
    const code = c.charCodeAt(0)   //ASCII 码值
    if (code >= 65 && code <= 90) {
      res += c.toLowerCase()
    } else if (code >= 97 && code <= 122) {
      res += c.toUpperCase()
    } else {
      res += c
    }
  }
  
  return res
}

        性能分析

        使用正则表达式,性能较差

        使用ASCII码判断,性能较好(推荐)

10. 为什么0.1+0.2≠0.3

        计算机使用二进制存储数据

        整数转换二进制没有误差,如9转换为二进制是1001

        而小数可能无法使用二进制准确表达,如0.2转换为1.1001100...

        不光JS,其他编程语言也这样

        可使用第三方库:math.js | an extensive math library for JavaScript and Node.jsMath.js is an extensive math library for JavaScript and Node.js. It features big numbers, complex numbers, matrices, units, and a flexible expression parser.icon-default.png?t=N7T8https://mathjs.org/

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值