备战秋招之数据结构

算法性能分析

时间复杂度

一.算法时间复杂度排行

O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶

二.算法时间复杂度的化简
  1. 去掉加减常数项
  2. 去掉常数项系数
  3. 保留最高项
三.递归的时间复杂度

递归的时间复杂度由:递归的次数 *每次递归中的操作次数决定

空间复杂度

  1. O(1):开辟的内存空间不变化
  2. O(n): 开辟的内存空间呈现线性变化
  3. O(nlogn):递归的时候

数组

一.数组常用api
  1. 改变原数组:
    push、pop
    unshift、shift
    reverse
    splice(删除元素的起始点,删除长度,添加到数组中的元素)
    sort
    fill
  2. 不改变原数组(生成新数组)
    concat()
    filter()
    slice():由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)
    map(处理函数):返回新数组,数组中的元素为按原始数组元素顺序调用函数处理后的值
  3. 一般api
    indexOf()
    join()/toString ()转换成字符串
    split(分割符):将字符串转成数组
    some(function):判断数组中的元素是否有一个或以上符合条件
    every(function):判断数组中的元素是否全部符合条件
    reduce():接收一个函数,根据函数返回累加的值
    reduceright():与reduce()相反,从右开始执行
    includes()
    Array.from():将类数组或者可迭代对象转换为数组
    Array.of():将参数中所有值作为参数形成数组
二.数组遍历方法
  1. for 循环
  2. forEach(item,index)
  3. map(item,index)
三.数组题型与解法
  1. 二分法
    有序数组中搜索目标值,注意区间右面的闭合与while循环的边界相关
    时间复杂度:O(logn)
  • 题1:有序数组中搜索目标值,返回目标值的下标
var search = function(nums, target) {
    var left = 0
    var right = nums.length - 1
    while(left<=right){
        let middle = left+Math.floor((right-left)/2)
        if(target<nums[middle]){
            right = middle -1
        }else if(target>nums[middle]){
            left = middle + 1
        }else{
            return middle
        }
    }
    return -1
};
  • 题2:有序数组中搜索目标值,有则返回目标值的下标,无则插入该值并返回下标
var searchInsert = function(nums, target) {
    var left = 0
    var right = nums.length -1
    var pos = nums.length - 1
    while(left<=right){
        var middle = left+Math.floor((right-left)/2)
        if(target<nums[middle]){
            pos = middle
            right = middle -1
        }else if(target>nums[middle]){
            left = middle + 1
        }else {
            return middle
        }
    }
    return left
}
  • 题3:求解非负整数x的平方根
/* 二分法求解算术平方根:
让mid的平方无限逼近x,则mid即为其算术平方根 */
var search = function(x){
    let left = 0
    let right = x
    let ans = -1
    while(left < right){
        let mid = left+Math.floor((right-left)/2)
        if(mid*mid<=x){
            ans = mid
            left = mid+1
        }else{
            right = mid - 1
        }
    }
    return ans
}
  • 题4:求旋转数组的最小值
/* 无脑解法:
升序排序后,取第一个元素 */
 var minArray = function(numbers) {
    array1 = numbers.sort((a,b)=>a-b)
    return array1[0]
}; 

/* 二分法:
旋转数组等同于将两个有序数组拼接在一起,拼接处第二个数组的第一个元素最小 */
var minArray = function(numbers) {
    let left = 0
    let right = numbers.length - 1
    while(left<right){
        let mid = left+Math.floor((right-left)/2)
        if(numbers[mid]<numbers[right]){
            right=mid
        }else if(numbers[mid] >numbers[right]){
            left = mid+1
        }else {
            right--
        }
    }
    return numbers[left]
};
  1. ** 双指针法**
    使用前提:数组有序
  • 题1:合并有序数组(同方向指针):注意合并时,两个的长度不相等的处理
  • 题2:三数之和(对撞指针):先考虑一般指针,再考虑对撞指针
  • 题3:求两个数组的交集
var intersection = function(nums1,nums2){
    nums1.sort((a,b)=>a-b)
    nums2.sort((a,b)=>a-b)
    const res =[]
    let i=0
    let j=0
    while(i<nums1.length&&j<nums2.length){
        const n1 = nums1[i],n2 = nums2[j]
        if(n1===n2){
            if(!res.length||n1!==res[res.length-1]){
                res.push(n1)
            }
            i++;
            j++;
        }else if(n1>n2){
            j++
        }else{
            i++
        }
    }
    return res
}
  • 题4:判断一个序列是否是另一个序列的子序列
/* 双指针法:
    js本来提供字符串数组排序的方法array.sort(),但是本题若排序会导致顺序错乱
    用双指针法来确定父序列中是否有该序列中的元素且元素顺序一致,只有当最后的i指向整个子序列后面一位时,才算是父序列的子序列
    将字符串转为数组,可以使用Array.of()、Array.from(),不能使用new Array(),会导致数组只有一个元素
注意区别:new Set() new Map() new Array(),第一个可以使用字符串作为参数,生成的set尺寸为字符串的长度;第二个不能使用字符串作为参数,使用数组作为参数时,必须保证数组是二维的;第三个使用字符创作为参数,将导致生成数组长度为1
 */
var isSubsequence = function (s, t) {
    let i = 0
    let j = 0
    while (i < s.length && j < t.length) {
        if (s[i] == t[j]) {
            i++
            j++
        } else {
            j++
        }
    }
    return i == t.length
};

-题5:有序数组的平方。给一个可能含有负数的有序数组,按非递减顺序返回每个元素的平方的有序数组。

/* 无脑解法
 */
var sortedSquares = function (nums) {
    let res = []
    for (i = 0; i < nums.length; i++) {
        res.push(nums[i] * nums[i])
    }
    return res.sort((a, b) => a - b)
};

/*双指针法之对撞指针法
因为原数组有序,且可以看成是两个有序数组的合并,因此数组元素平方后的最大值一定在原数组的两端;设定一个空数组,左右两个指针指向首尾(与两个有序数组合并一致),将两个数组元素求平方后合并
*/
var sortedSquares = function (nums) {
    let right = nums.length - 1
    let left = 0
    let res =[]
    // 也可以设置一个pos标志,指向新生成数组的末尾,并随着循环依次向前移动
    while (left <= right) {
        if(nums[right]*nums[right]<=nums[left]*nums[left]){
            res.unshift(nums[left])
        }else{
            res.unshift(nums[right])
        }
    }
    return res
};
  • 题6:求元素和满足条件的长度最小的子数组
* 暴力解法(解法有问题) */
/* var minSubArrayLen = function(target, nums) {
    let sum,curMinLength
    let result = Number.MAX_VALUE
    for(i=0;i<nums.length;i++){
       sum = 0
       result = 0
       for(j=i;j<nums.length;j++){
        sum = sum + nums[j]
        if(sum>=target){
          curMinLength = j - i + 1
          result = result<curMinLength?result : curMinLength
          break;
        }
       }  
    }
    return result === initial ? 0:result
}; */


/* 双指针之大小变化的滑动窗口法
 */
var minSubArrayLen = function(target, nums) {
    let len = nums.length
    if(len==0){
        return 0
    }
    let res =Number.MAX_VALUE
    let start = 0
    let end = 0
    let sum = 0
    while(end<len){
        sum += nums[end]
        while(sum>=target){
            res = Math.min(res,end-start+1)
            sum -= nums[start]
            start++
        }
        end++
    }
    return res == Number.MAX_VALUE ? 0:res
};
  • 题7:移除元素,移除数组中指定的元素
/* 数组中的元素在内存空间中的地址是连续的,不能直接删除,只能覆盖(该说法在js中并不准确) */
/*暴力解法:两层循环,里层用于更新数组,外层用于遍历数组查找有无对应元素
  */
 var removeElement = function(nums, val) {
    var len = nums.length
    for(i=0;i<len;i++){
        if(nums[i]==val){
            // 将后面的元素都往前推一位
            for(j=i+1;j<len;j++){
                nums[j-1]=nums[j]
            }
            i--
            len--
        }
    }
    return len
}; 

/*双指针之快慢指针解法*/
 var removeElement = function(nums, val) {
    let slow = 0
    for(fast=0;fast<nums.length;fast++){
        if(nums[fast]!==val){
            nums[slow]=nums[fast]
            slow++
        }
    }
    return slow
};
 
/* 双指针之对撞指针解法:
    左指针:指向当前元素,右指针:若前面有与目标值相等的元素则用该指针指向的元素覆盖左指针指向的元素
 */
var removeElement = function(nums, val) {
    let left =0
    let right = nums.length
    while(left<right){
        if(nums[left]===val){
            nums[left]=nums[right-1]
            right--
        }else{
            left++
        }
    }
    return left
};
  1. Map、Set的使用
  • 题1:对有序数组中两个元素求和得到目标值,返回下标的数组(注意map中的key-value值设定)
  • 题1的升级版:有序数组下标从1开始,两个元素求和得到目标值,按大小返回两个元素的下标数组(若限制在常量空间的解法,则map方法不行,因为空间复杂度为O(n))
/* map方法(题1初级版也用该方法):
利用map的键名与键值可以是任意形式的数据,只遍历一遍数组,使用has()方法来判断map中的元素是否等于目标值-当前元素的值 
*/
var twoSum = function(numbers, target) {
    var map = new Map
    var len = numbers.length
    var res = []
    for(let i= 0;i<len;i++){
        if(map.has(target-numbers[i])){
            res.push(map.get(target-numbers[i]),i+1)
        }else{
            map.set(numbers[i],i+1)
        }
    }
    return res
}


/* 二分查找法:
数组有序,使用二分查找法,注意尽量不要使用闭包,使得内存溢出
*/
var twoSum = function(numbers, target) {
  const len = numbers.length
  const res = []
  let left,right
  for(let i = 0; i<len; i++){
    left = i+1
    right = len - 1
    while(left<=right){
        let mid = left+Math.floor((right-left)/2)
        if(numbers[mid]<target-numbers[i]){
            left = mid + 1
        }else if(numbers[mid]>target-numbers[i]){
            right = mid -1
        }else{
           return [i+1,mid+1]
        }
    }
  }
}

/* 双指针法:
    数组有序,使用双指针法
 */
var twoSum = function(numbers, target) {
      let left = 0
      let right = numbers.length-1
      while(left<right){
          if(numbers[right]+numbers[left] == target){
              return [left+1,right+1]
          }else if(numbers[right]+numbers[left]>target){
              right--
          }else{
             left++
          }
      }
    }
  • 题2:求两个数组的交集(Map或Set都可以)
var intersection = function (nums1, nums2) {
    const set1 = new Set(nums1)
    const set2 = new Set(nums2)
    const res = []
    for (let key of set1) {
        if (set2.has(key)) {
            res.push(key)
        }
    }
    return res
} 
  1. (模拟)循环思想的练习
  • 螺旋矩阵:给定正整数n,生成一个包含 1 到 n*n的所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩
/* 主要思想就是模拟,确定好每次循环的逻辑与操作
 */
var generateMatrix = function (n) {
    let startX = startY = 0
    let loop = Math.floor(n / 2)
    let mid = Math.floor(n / 2)//n为奇数时需要单独填充,确定单独填充的位置
    let offset = 1//每一层填充元素个数
    let count = 1//填充数字
    let res = new Array(n).fill(0).map(() => new Array(n).fill(0))

    // 开始循环
    while (loop--) {
        let row = startX
        let col = startY
        for (; col < startY + n - offset; col++) {
            res[row][col] = count++
        }

        for (; row < startX + n - offset; row++) {
            res[row][col] = count++
        }

        for (; col > startY; col--) {
            res[row][col] = count++
        }

        for (; row > startX; row--) {
            res[row][col] = count++

        }

        //每一个loop后更新下一轮loop起始位置、填充的元素个数
        startX++
        startY++
        offset += 2
    }
    if (n % 2 === 1) {
        res[mid][mid] = count
    }
    return res
};
数组总结

图源自代码随想录

链表

一.链表的实现
  1. 单链表的实现
    在这里插入图片描述
/* 链表,由数据域与指针域构成的数据结构,和数组相似为有序的列表,都是线性结构。链表实现主要依靠两个类*/

// 实现链表结点
class Node {
    constructor(val) {
        this.val = val;
        this.next = null;
    }
}

// 实现链表主体
class singleLinked {
    constructor() {
        this.head = new Node('head')  //记录链表的头指针
        this.size = 0 //用来记录单链表的长度或结点个数
        this.curNode = ''//记录当前结点
    }

    // 获取链表的长度
    getLength() {
        return this.size
    }

    // 判断链表是否为空
    isEmpty() {
        return this.size === 0
    }

    // 查找指定节点
    find(target) {
        var curNode = this.head
        while (curNode.val != target) {
            curNode = curNode.next
        }
        return curNode
    }

    // 查找指定节点的前一个节点
    findPreious(target) {
        var curNode = this.head
        while (curNode.next.val != target) {
            curNode = curNode.next
        }
        return curNode
    }

    // 在指定节点后面插入节点(先设置该节点的next,再设置前面节点的next指向被插入节点)
    insert(newNode, target) {
        var newNode = new Node(newNode)
        var curNode = this.find(target)
        newNode.next = curNode.next
        curNode.next = newNode
    }

    // 遍历链表
    disPlayList() {
        var list = ''
        var curNode = this.head //从链表的头指针开始
        while (curNode) {//若当前结点不为空,则表明当前结点中存在数据
            list += curNode.val
            // 让当前结点的指针指向下一节点
            curNode = curNode.next
        }
        console.log(list)
    }

    // 获取链表最后一个结点
    findLast() {
        var curNode = this.head
        while (curNode.next) {
            curNode = curNode.next
        }
        return curNode
    }

    //删除结点(关键点:找到被删除结点的前驱结点)
    deleteNode(target) {
        // 首先遍历,找到需要删除的结点位置
        //方法一:此时curNode为目标结点
       /*  var leftNode = null
        var currNode = this.head
        while (currNode.val != target) {
            leftNode = currNode
            currNode = currNode.next
        }
        leftNode.next = currNode.next
        this.size-- */

        //方法二:此时curNode为目标节点的上一个节点
        /* var curNode = this.head
        while(curNode.next.val != target){
            curNode = curNode.next
        } */
        var curNode = this.findPreious(target)
        curNode.next = curNode.next.next
        this.size--
    }


    // 尾插法在链表尾部添加元素,即链表实现
    appendNode(element) {
        var curNode = this.findLast()//先找到链表的最后一个结点
        var newNode = new Node(element)//创建一个新的节点
        // 将新创建的结点放入链表的最后一个结点后面(即将新结点插入链表尾部)
        curNode.next = newNode
        newNode.next = null
        this.size++//因为新插入了结点,使得链表长度加一
    }
}

// 测试链表
var slist = new singleLinked()
var arr = [1001, 1234, 1006, 7788, 5512, 6129]
for (var i = 0; i < arr.length; i++) {
    slist.appendNode(arr[i])
}
slist.disPlayList()
slist.deleteNode(1001) 
slist.disPlayList()
  1. 双向链表
    定义:有两个指针域,一个指向前驱节点,一个指向后继节点
    在这里插入图片描述
/* 双向链表:有两个指针域,一个指向前驱节点,一个指向后继节点 */
// 实现链表结点
class Node {
    constructor(val) {
        this.val = val;
        this.next = null;
        this.pre = null;
    }
}

// 实现链表主体
class doubleLinked {
    constructor() {
        this.head = new Node('head')  //记录链表的头指针
        this.size = 0 //用来记录单链表的长度或结点个数
        this.curNode = ''//记录当前结点
    }

    // 获取链表的长度
    getLength() {
        return this.size
    }

    // 判断链表是否为空
    isEmpty() {
        return this.size === 0
    }

    // 查找指定节点
    find(target) {
        var curNode = this.head
        while (curNode.val!== target) {
            curNode = curNode.next
        }
        return curNode
    }

    // 在指定节点后面插入节点(先设置该节点的next,再设置前面节点的next指向被插入节点)
    insert(newNode, target) {
        var newNode = new Node(newNode)
        var curNode = this.find(target)
        newNode.next = curNode.next
        curNode.next.pre = curNode
        curNode.next = newNode
    }

    // 遍历链表(正序)
    disPlayList() {
        var list = ''
        var curNode = this.head //从链表的头指针开始
        while (curNode !== null) {//若当前结点不为空,则表明当前结点中存在数据
            list += curNode.val
            // 让当前结点的指针指向下一节点
            curNode = curNode.next
        }
        console.log(list)
    }

    // 遍历链表(反序)
    disPlayListRe(){
        var list = ''
        var curNode = this.head
        curNode =this.findLast()
        while(curNode.pre!==null){
            list += curNode.val
            curNode = curNode.pre
        }
        console.log(list)
    }

    // 获取链表最后一个结点
    findLast() {
        var curNode = this.head
        while (curNode.next) {
            curNode = curNode.next
        }
        return curNode
    }

    //删除结点(关键点:相比单向链表,删除更加方便,不需要再次查找前驱节点)
    deleteNode(target){
      var curNode = this.find(target)
      if(curNode.next!== null){
        curNode.pre.next = curNode.next
        curNode.next.pre = curNode.pre
        // 回收
        curNode.next = null
        curNode.pre = null
        this.size--
      } 
    }


    // 尾插法在链表尾部添加元素,即链表实现
    appendNode(element) {
        var curNode = this.findLast()//先找到链表的最后一个结点
        var newNode = new Node(element)//创建一个新的节点
        // 将新创建的结点放入链表的最后一个结点后面(即将新结点插入链表尾部)
        curNode.next = newNode
        newNode.pre = curNode
        newNode.next = null
        this.size++//因为新插入了结点,使得链表长度加一
    }
}

// 测试链表
var slist = new doubleLinked()
var arr = [1001, 1234, 1006, 7788, 5512, 6129]
for (var i = 0; i < arr.length; i++) {
    slist.appendNode(arr[i])
}
slist.disPlayList()
slist.deleteNode(5512) 
slist.disPlayList()
slist.disPlayListRe()
  1. 循环链表
    定义:链表首尾相连,即链表的头节点指向本身
    在这里插入图片描述
二.链表的优点

相较于数组,明确了删除的位置,链表便于插入与删除;但是访问某个节点时,需要遍历链表访问效率不高

  • 注:JS数组不再具有其他语言数组的特征,其低层使用哈希映射分配内存空间,由对象链表来分配内存空间
三.链表常见题型
  1. 删除链表元素
  • 删除链表中指定元素的值:注意头节点的判断,最好使用dummy节点
/* 删除链表中等于给定值的元素:
注意移除第一个节点与其他节点不一样,一般的节点移除都依赖于该节点的前一个节点,为处理移除第一个节点的特殊情况,可单独对头节点进行判断,或设置dummy节点
*/

/* 设置dummy节点 */
var removeElements = function (head, val) {
    // dummy节点的设置
    const ret = new ListNode(0, head)
    var curNode = ret
    while (curNode.next !== null) {
        if (curNode.next.val == val) {
            curNode.next = curNode.next.next
            continue
        }
        curNode = curNode.next
    }
    return ret.next
};

/* 单独对第一个节点进行判断 */
var removeElements = function (head, val) {
    // 处理头部节点
    while (head !== null && head.val == val) {
        head = head.next
    }
    if (head == null) return head
    // 处理非头部节点
    let pre = head
    let curNode = head.next
    while (curNode) {
        if (curNode.val == val) {
            pre.next = curNode.next
        } else {
            pre = pre.next
        }
        curNode = curNode.next
    }
    return head
};
  • 删除链表中倒数第n个节点:双指针法
/* 双指针法之快慢指针:
使用一次遍历解决问题,注意循环的条件
 */
var removeNthFromEnd = function(head, n) {
    let link = new ListNode(0,head)
    let slow = link
    let fast = link
    while (n>0) {
        fast = fast.next
        n--
    }
    while(fast.next){
        slow = slow.next
        fast = fast.next
    }
    slow.next = fast
    return link.next
};
  1. 反转链表
  • 反转整个链表:多指针法 pre cur post
/* 双指针法:left指向当前节点的前驱,temp指向当前节点的后继 */
var reverseList = function(head) {
    let curNode = head
    let temp
    let left = null
    if(!head.next || !head) return head
    while(curNode.next){
        temp = curNode.next
        curNode.next = left

        left = curNode
        curNode = temp
    }
    return left
};

/*递归法:从前往后翻转next指针*/
var reverse = function(pre, head) {
    if(!head) return pre;
    const temp = head.next;
    head.next = pre;
    pre = head
    return reverse(pre, temp);
}

var reverseList = function(head) {
    return reverse(null, head);
};

/* 递归法:从后往前翻转next指针 */
var reverse = function(head) {
    if(!head || !head.next) return head;
    // 从后往前翻
    const pre = reverse(head.next);
    head.next = pre.next;
    pre.next = head;
    return head;
}

var reverseList = function(head) {
    let curNode = head;
    while(curNode && curNode.next) {
        curNode = curNode.next;
    }
    reverse(head);
    return cur;
};
  • 局部反转链表:与全部反转相似
var reverseBetween = function(head, left, right) {
    /* 多指针法:pre指向当前节点的前驱,cur指向当前节点,post指向后继节点,leftHead指向整个left,right区间的前驱节点 */
    let dummy = new ListNode()
    dummy.next = head
    let pre,cur,post,leftHead
    let p = dummy //p为游标,用于遍历

    // p往前走m-1步,至m节点的前驱节点
    for(let i = 0; i < left -1;i++){
        p = p.next
    }

    // 缓存该节点至leftHead
    leftHead = p

    // start 为反转区间的第一个结点
    let startNode = leftHead.next
    pre = startNode
    cur = pre.next
    for(let i = left;i<right;i++){
        post = cur.next
        cur.next = pre

        pre = cur
        cur =post
    }
    leftHead.next = pre
    startNode.next = cur
    return dummy.next
};
  1. 循环链表
  • 判断链表是否循环:使用flag来标志
/* 判断链表中是否有环:立falg */
var hasCycle = function(head) {
    while(head){
        if(head.flag){
            return true
        }else{
            head.flag =true
            head = head.next
        }
    }
    return false
}
  • 判断链表是否循环,并返回第一个入环的节点
/* 判断有无环,若有环,找到入环的起始节点
入环的起点一定是第一个标记为falg的点
*/
// 也可用Map来存储flag
const detectCycle = function(head) {
    while(head){
        if(head.flag){
            return head;
        }else{
            head.flag = true;
            head = head.next;
        }
    }
    return null;
};
  1. 其余题型
  • 判断两个不同长度的链表是否相交:重点在尾部对齐
/*链表的相交: 要注意是末尾对齐的相交,处理两个链表不同长度 */
var getIntersectionNode = function (headA, headB) {
    let cur1 = headA
    let cur2 = headB
    let A = 0
    let B = 0

    // 求两个链表的长度
    while (cur1) {
        cur1 = cur1.next
        A++
    }
    while (cur2) {
        cur2 = cur2.next
        B++
    }

    //让两个链表从头开始
    cur1 = headA
    cur2 = headB

    // 根据两个链表长度进行对比
    // 让A始终代表长度较长的链表
    if (A < B) {
        [cur1, cur2] = [cur2, cur1];//分号的重要性
        [A, B] = [B, A]
    }
    let i = A - B
    while (i > 0) {
        cur1 = cur1.next
        i--
    }
    while (cur1 && cur1 !== cur2) {
        cur1 = cur1.next
        cur2 = cur2.next
    }
    return cur1
}

字符串

一.字符串常用api

  1. string.length 获取字符串长度
  2. string[index] 获得索引处的字符
  3. striing.split(“分隔符”):将字符串按照分隔符分割为数组 相对的,reverse()将数组转为字符串
  4. Array.from(string):将字符串中的每个字符转为数组
  5. swap函数不能用于字符串
  6. string.trim():去掉字符串头尾空格,返回去掉空格的字符串

二. 字符串常见题型

  1. KMP算法
    代码随想录详解KMP
  • 文本串:待匹配的字符串 模式串:被匹配的字符串
  • 前缀与后缀
  • 最长相等前缀表 next数组prefix(一般next为原来的前缀表或者将前缀表-1,再或者将前缀B表右移一位,并将首位赋值-1
  • 匹配核心:若文本串中遇到不匹配的字符,找到该字符对应的前缀表中的数字,返回模式串中的索引为前缀表数字的位置进行比较,循环进行
    普遍时间复杂度为:O(m+n)
    next字符串构造(3步):
  • 初始化。两个指针,j指向前缀末尾,i指向后缀末尾
  • 处理前后缀不相同的情况
  • 处理前后缀相同的情况
    题型
  • 实现strStr():给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1
/*前缀表统一 -1*/
/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function (haystack, needle) {
    if (needle.length === 0)
        return 0;

    const getNext = (needle) => {
        let next = [];
        let j = -1;
        next.push(j);

        for (let i = 1; i < needle.length; ++i) {
            while (j >= 0 && needle[i] !== needle[j + 1])
                j = next[j];
            if (needle[i] === needle[j + 1])
                j++;
            next.push(j);
        }

        return next;
    }

    let next = getNext(needle);
    let j = -1;
    for (let i = 0; i < haystack.length; ++i) {
        while (j >= 0 && haystack[i] !== needle[j + 1])
            j = next[j];
        if (haystack[i] === needle[j + 1])
            j++;
        if (j === needle.length - 1)
            return (i - needle.length + 1);
    }

    return -1;
};
/*前缀表不变*/
var strStr = function (haystack, needle) {
    if (needle.length === 0)
        return 0;

    const getNext = (needle) => {
        let next = [];
        let j = 0;
        next.push(j);

        for (let i = 1; i < needle.length; ++i) {
            while (j > 0 && needle[i] !== needle[j])
                j = next[j - 1];
            if (needle[i] === needle[j])
                j++;
            next.push(j);
        }

        return next;
    }

    let next = getNext(needle);
    let j = 0;
    for (let i = 0; i < haystack.length; ++i) {
        while (j > 0 && haystack[i] !== needle[j])
            j = next[j - 1];
        if (haystack[i] === needle[j])
            j++;
        if (j === needle.length)
            return (i - needle.length + 1);
    }

    return -1;
};
  • 判断一个字符串是否由另一个子字符串构成
//判断一个字符串是否由另一个子串构成
/* 移动匹配法:
若一个字符串由一个字串重复构成,则两个字符串拼接起来掐头去尾后,从中仍能找到该字符串 */
var repeatedSubstringPattern = function (s) {
    let ss = s + s
    let Array1 = Array.from(ss)
    Array1.pop()
    Array1.shift()
    let Array3 = []
    let map = new Map()
    // 对数组掐头去尾 遍历n次
    for (let i = 0; i < Array1.length; i++) {
        map.set(i, Array1[i])
    }
    // 将数组转为Map


    // 判断去头掐尾后是否有s(这步好像行不通,因为只能判断单个字符是否存在)
    if (map.has(s)) {
        return true
    }
    return false
}

console.log(repeatedSubstringPattern(''))

/* KPM算法:next数组统一减一的情况下 */
var repeatedSubstringPattern = function (s) {
    // 字符串长度为零时
    if (s.length === 0) {
        return false
    }

    const getNext = (s) => {
        let next = []
        let j = -1
        next[0] = j

        for (i = 1; i < s.length; i++) {
            while (j >= 0 && s[i] !== s[j + 1]) {
                j = next[j]
            }
            if (s[i] === s[j + 1]) {
                j++
            }
            next.push(j)
        }
        return next
    }

    let next = getNext(s)

    if (next[next.length - 1] !== -1 && s.length % (s.length - (next[next.length - 1] + 1)) === 0) {
        return true
    }
    return false
} 
  1. 反转字符串
  • 反转一个字符串
/* 调用内置API法:不满足原地修改数组 */
var reverseString = function(s) {
    const res = s.split(",").reverse.join()
    return res
}

/* 循环法
类似于冒泡排序。使用循环元素从头冒到尾部
 */
var reverseString = function(s) {
    for(let i = 0;i<s.length-1;i++){
        for(j=0;j<s.length - 1-i;j++){
           [s[j],s[j+1]] =[s[j+1],s[j]]
        }
    }
}
 
/*双指针法:对撞指针
两个指针所指的元素相交换
*/
var reverseString =function (s) {
    let left = 0
    let right = s.length-1
    while (left<=right) {
       [s[left],s[right]] = [s[right],s[left]] 
       left++
       right--
    }
}
  • 反转字符串2:每2k反转一次,反转前面k个;若剩下不足k个,则全部反转,若大于等于k个小于2k或大于2k则反转前面k个
/*双指针法:对撞指针
需要考虑每次右指针指的位置
*/
var reverseStr = function(s, k) {
    const len = s.length;
    let resArr = s.split(""); 
    for(let i = 0; i < len; i += 2 * k) {
        let l = i, r = i + k > len ? len -1: i + k -1;
        while(++l <= --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]];
    }
    return resArr.join("");
};
  • 反转字符串中的单词
/* 调用api法:本方法使用了额外的空间
split()将字符串转换成数组;再删除数组中的空字符串元素;进行数组的反转;join()转换成字符串 */
var reverseWords = function (s) {
    let strArr = s.split(" ")//注意分割符的妙用
    let fast = 0
    let slow = 0
    for (fast = 0; fast < strArr.length; fast++) {
        if (strArr[fast] !== '') {
            strArr[slow] = strArr[fast]
            slow++
        }
    }
    let newArr = []
    while (slow >= 0) {
        newArr.push(strArr[slow])
        slow--
    }
    newArr.join('')
}; 

/*双指针法:对撞指针法。
先将字符串全局翻转;再删除其中空格;局部翻转每个单词  */
 var reverseWords = function(s) {
    // 字符串转数组
    const strArr = Array.from(s);
    // 移除多余空格
    removeExtraSpaces(strArr);
    // 翻转
    reverse(strArr, 0, strArr.length - 1);
 
    let start = 0;
 
    for(let i = 0; i <= strArr.length; i++) {
      if (strArr[i] === ' ' || i === strArr.length) {
        // 翻转单词
        reverse(strArr, start, i - 1);
        start = i + 1;
      }
    }
 
    return strArr.join('');
 };
 
 // 删除多余空格
 function removeExtraSpaces(strArr) {
   let slowIndex = 0;
   let fastIndex = 0;
 
   while(fastIndex < strArr.length) {
     // 移除开始位置和重复的空格
     if (strArr[fastIndex] === ' ' && (fastIndex === 0 || strArr[fastIndex - 1] === ' ')) {
       fastIndex++;
     } else {
       strArr[slowIndex++] = strArr[fastIndex++];
     }
   }
 
   // 移除末尾空格
   strArr.length = strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex;
 }
 
 // 翻转从 start 到 end 的字符
 function reverse(strArr, start, end) {
   let left = start;
   let right = end;
 
   while(left < right) {
     // 交换
     [strArr[left], strArr[right]] = [strArr[right], strArr[left]];
     left++;
     right--;
   }
 }
  • 左旋字符串
/* 调用api法:
注意:字符串不能用swap函数
 */
var reverseLeftWords = function (s, n) {
    const length = s.length;
    let i = 0;
    while (i < length - n) {
        s = s[length - 1] + s;
        i++;
    }
    return s.slice(0, length);
};

/* 双指针法:
分别反转k之前后k之后的元素;再整体反转
注意:除非输入字符串默认为字符串数组,否则需要将字符串转为数组才可生效 */
var reverseLeftWords = function (arr, n) {
    // 定义反转函数
    function reverse(s, left, right) {
        let start = left
        let end = right
        while (start <= end) {
            [s[start], s[end]] = [s[end], s[start]]
            start++
            end--
        }
    }
    //局部反转k之前和之后的字符串
    reverse(arr, 0, n - 1)
    reverse(arr, n, arr.lengthg - 1)
    reverse(arr, 0, arr.length - 1)
};

  1. 回文字符串(理解什么是回文字符串)
  • 判断一个字符串是否是回文字符串
/*反转字符串法:
将字符串反转后与原字符串对比
*/
function isPalindrome(str) {
    // 先反转字符串
    const reversedStr = str.split('').reverse().join('')
    // 判断反转前后是否相等
    return reversedStr === str
}

/*利用回文字符串的对称性:
回文字符串的前半部分与后半部分完全对称,可遍历字符串验证对称性
*/
function isPalindrome(str) {
    // 缓存字符串的长度
    const len = str.length
    // 遍历前半部分,判断和后半部分是否对称
    for(let i=0;i<len/2;i++) {
        if(str[i]!==str[len-i-1]) {
            return false
        }
    }
    return true
}
  • 回文字符串衍生问题:删除一个字符,是否能成为回文字符串
const validPalindrome = function(s) {
    // 缓存字符串的长度
    const len = s.length

    // i、j分别为左右指针
    let i=0, j=len-1
    
    // 当左右指针均满足对称时,一起向中间前进
    while(i<j&&s[i]===s[j]) {
        i++ 
        j--
    }
    
    // 尝试判断跳过左指针元素后字符串是否回文
    if(isPalindrome(i+1,j)) {
      return true
    }
    // 尝试判断跳过右指针元素后字符串是否回文
    if(isPalindrome(i,j-1)) {
        return true
    }
    
    // 工具方法,用于判断字符串是否回文
    function isPalindrome(st, ed) {
        while(st<ed) {
            if(s[st] !== s[ed]) {
                return false
            }
            st++
            ed--
        } 
        return true
    }
    
    // 默认返回 false
    return false 
};
  1. 替换空格:将字符串中的空格替换成指定字符
/*调用api方法:
splice()注意插入、删除的位置的索引
join()数组转字符串 split()字符串转数组 二者注意分割符的使用
  */
var replaceSpace = function (s) {
    let a = s.split(" ")
    let len = a.length
    let i = 1
    while (i < len + len - 1) {
        a.splice(i,0,'20%')
        i +=2
    }
    return a.join("")
};

/*双指针法:
跟调用api方法一致,先将字符串转为数组再扩容;再使用双指针从后往前进行数组元素的填充
Array.from()字符串转为数组,默认分割符为"" */
var replaceSpace = function(s) {
    // 字符串转为数组
   const strArr = Array.from(s);
   let count = 0;
 
   // 计算空格数量
   for(let i = 0; i < strArr.length; i++) {
     if (strArr[i] === ' ') {
       count++;
     }
   }
 
   let left = strArr.length - 1;
   let right = strArr.length + count * 2 - 1;
 
   while(left >= 0) {
     if (strArr[left] === ' ') {
       strArr[right--] = '0';
       strArr[right--] = '2';
       strArr[right--] = '%';
       left--;
     } else {
       strArr[right--] = strArr[left--];
     }
   }
 
   // 数组转字符串
   return strArr.join('');
 };
  1. 字符串与正则表达式
  • 设计一个支持addWorld(word)与searchWorld(word)的数据结构,其中searchWord应该可以搜索正则表达式 . 字符串或者文字(字母a-z)
/**
 * 构造函数
 */
const WordDictionary = function () {
  // 初始化一个对象字面量,承担 Map 的角色
  this.words = {}
};

/**
  添加字符串的方法
 */
WordDictionary.prototype.addWord = function (word) {
  // 若该字符串对应长度的数组已经存在,则只做添加
  if (this.words[word.length]) {
    this.words[word.length].push(word)
  } else {
    // 若该字符串对应长度的数组还不存在,则先创建
    this.words[word.length] = [word]
  }

};

/**
  搜索方法
 */
WordDictionary.prototype.search = function (word) {
  // 若该字符串长度在 Map 中对应的数组根本不存在,则可判断该字符串不存在
  if (!this.words[word.length]) {
    return false
  }
  // 缓存目标字符串的长度
  const len = word.length
  // 如果字符串中不包含‘.’,那么一定是普通字符串
  if (!word.includes('.')) {
    // 定位到和目标字符串长度一致的字符串数组,在其中查找是否存在该字符串
    return this.words[len].includes(word)

  }

  // 否则是正则表达式,要先创建正则表达式对象
  const reg = new RegExp(word)

  // 只要数组中有一个匹配正则表达式的字符串,就返回true
  return this.words[len].some((item) => {
    return reg.test(item)
  })
};
  • 字符串与数字之间的转换问题(将字符串转换为整数)
// 入参是一个字符串
const myAtoi = function(str) {
    // 编写正则表达式
    const reg = /\s*([-\+]?[0-9]*).*/
    // 得到捕获组
    const groups = str.match(reg)
    // 计算最大值
    const max = Math.pow(2,31) - 1
    // 计算最小值
    const min = -max - 1
    // targetNum 用于存储转化出来的数字
    let targetNum = 0
    // 如果匹配成功
    if(groups) {
        // 尝试转化捕获到的结构
        targetNum = +groups[1]
        // 注意,即便成功,也可能出现非数字的情况,比如单一个'+'
        if(isNaN(targetNum)) {
            // 不能进行有效的转换时,请返回 0
            targetNum = 0
        }
    }
    // 卡口判断
    if(targetNum > max) {
        return max
    } else if( targetNum < min) {
        return min
    }
    // 返回转换结果
    return targetNum
};

栈与队列

一.栈与队列的相互实现

1.栈实现队列

/* @return {void}
 */
MyQueue.prototype.push = function(x) {
    this.stack1.push(x)
    return this.stack1
}

/**
 * @return {number}
 */
MyQueue.prototype.pop = function() {
//    stack2为空,则将stack1的元素转移进来
if(this.stack2.length <= 0){
    // stack1不为空时,出栈
    while(this.stack1.length !== 0){
        // stack1的元素压入stack2中,stack2栈顶元素为队头元素
        this.stack2.push(this.stack1.pop())
    }
}
return this.stack2.pop() 
};

/**
 * @return {number}
 */
MyQueue.prototype.peek = function() {
   if(this.stack2.length <= 0){
    // stack1不为空时,出栈
    while(this.stack1.length !== 0){
        // stack1的元素压入stack2中,stack2栈顶元素为队头元素
        this.stack2.push(this.stack1.pop())
    }
}
    return this.stack2[this.stack2.length-1]
};  

/**
 * @return {boolean}
 */
MyQueue.prototype.empty = function() {
// 若stack1与stack2为空,则队列都为空
return !this.stack1.length&&!this.stack2.length
};
  1. 队列实现栈

// 使用两个队列实现
/**
 * Initialize your data structure here.
 */
 var MyStack = function() {
    this.queue1 = [];
    this.queue2 = [];
};

/**
 * Push element x onto stack. 
 * @param {number} x
 * @return {void}
 */
MyStack.prototype.push = function(x) {
    this.queue1.push(x);
};

/**
 * Removes the element on top of the stack and returns that element.
 * @return {number}
 */
MyStack.prototype.pop = function() {
    // 减少两个队列交换的次数, 只有当queue1为空时,交换两个队列
    if(!this.queue1.length) {
        [this.queue1, this.queue2] = [this.queue2, this.queue1];
    }
    while(this.queue1.length > 1) {
        this.queue2.push(this.queue1.shift());
    }
    return this.queue1.shift();
};

/**
 * Get the top element.
 * @return {number}
 */
MyStack.prototype.top = function() {
    const x = this.pop();
    this.queue1.push(x);
    return x;
};

/**
 * Returns whether the stack is empty.
 * @return {boolean}
 */
MyStack.prototype.empty = function() {
    return !this.queue1.length && !this.queue2.length;
};


// 使用一个队列实现
/**
 * Initialize your data structure here.
 */
var MyStack = function() {
    this.queue = [];
};

/**
 * Push element x onto stack. 
 * @param {number} x
 * @return {void}
 */
MyStack.prototype.push = function(x) {
    this.queue.push(x);
};

/**
 * Removes the element on top of the stack and returns that element.
 * @return {number}
 */
MyStack.prototype.pop = function() {
    let size = this.queue.length;
    while(size-- > 1) {
        this.queue.push(this.queue.shift());
    }
    return this.queue.shift();
};

/**
 * Get the top element.
 * @return {number}
 */
MyStack.prototype.top = function() {
    const x = this.pop();
    this.queue.push(x);
    return x;
};

/**
 * Returns whether the stack is empty.
 * @return {boolean}
 */
MyStack.prototype.empty = function() {
    return !this.queue.length;
};
二.栈与队列常见题型
  1. 栈相关
  • 括号的匹配:利用左括号的进栈顺序与右括号的出栈顺序一致(利用栈的对称性)
/* 使用栈的对称性
见到括号与对称性问题,考虑使用栈,左括号的进栈顺序与右括号的出栈顺序一致
*/
/* 解法一 */
var isValid = function (s) {
    const stack = [];
    for (let i = 0; i < s.length; i++) {
      let c = s[i];
      switch (c) {
        case '(':
          stack.push(')');
          break;
        case '[':
          stack.push(']');
          break;
        case '{':
          stack.push('}');
          break;
        default://判断左右括号是否匹配
          if (c !== stack.pop()) {
            return false;
          }
      }
    }
    return stack.length === 0;//左边括号比右边元素括号多
  };

  /*解法二 */
  // 用一个 map 来维护左括号和右括号的对应关系
const leftToRight = {
    "(": ")",
    "[": "]",
    "{": "}"
  };
  const isValid = function(s) {
    // 结合题意,空字符串无条件判断为 true
    if (!s) {
      return true;
    }
    // 初始化 stack 数组
    const stack = [];
    // 缓存字符串长度
    const len = s.length;
    // 遍历字符串
    for (let i = 0; i < len; i++) {
      // 缓存单个字符
      const ch = s[i];
      // 判断是否是左括号,这里我为了实现加速,没有用数组的 includes 方法,直接手写判断逻辑
      if (ch === "(" || ch === "{" || ch === "[") stack.push(leftToRight[ch]);
      // 若不是左括号,则必须是和栈顶的左括号相配对的右括号
      else {
        // 若栈为空,或栈顶的左括号没有和当前字符匹配上,那么判为无效
        if (!stack.length || stack.pop() !== ch) {
          return false;
        }
      }
    }
    // 若所有的括号都能配对成功,那么最后栈应该是空的
    return !stack.length;
  };
  • 删除字符串中的重复项
/* 从前往后反复删除字符串中的重复字符
方法一:
基本思想:使用队列与栈的思想(先进先出、后进先出),核心思想为将字符串顺序放入栈中
注:数组与字符串的相互转换 
    字符串转数组:Array from()、split(分割符) 、...扩展运算符
    数组转字符串:join(分割符) 、toString()默认分割符为,
     */
var removeDuplicates = function (s) {
    const queue = Array.from(s)
    const stack = []
    let temp
    for (let i = 0; i < queue.length; i++) {
        temp = queue[i]
        if (stack.length <= 0) {
            stack.push(temp)
        } else {
            if (temp == stack[stack.length - 1]) {
                stack.pop()
            } else {
                stack.push(temp)
            }
        }
    }
    return stack.join('')
}

/* 直接使用栈
基本思想:与方法一一致。按顺序将字符串压入栈,查看当前字符串元素与栈顶元素是否相等, */
var removeDuplicates = function (s) {
    const stack = [];
    for (const x of s) {
        let c = null;
        if (stack.length && x === (c = stack.pop())) continue;
        c && stack.push(c);
        stack.push(x);
    }
    return stack.join("");
};

/* 方法三:双指针法模拟栈
基本思想:将    
 */
// 原地解法(双指针模拟栈)
var removeDuplicates = function (s) {
    s = [...s];
    let top = -1; // 指向栈顶元素的下标
    for (let i = 0; i < s.length; i++) {
        if (top === -1 || s[top] !== s[i]) { // top === -1 即空栈
            s[++top] = s[i]; // 入栈
            console.log(s)
        } else {
            top--; // 推出栈
        }
    }
    s.length = top + 1; // 栈顶元素下标 + 1 为栈的长度
    return s.join('')
};
  • 逆波兰表达式:与删除栈中的重复项一致,都是根据进栈的元素来进行进一步判断
/* 逆波兰表达式:一种后缀表达式,即运算符写在后面。
适合栈操作运算,遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,将结果压入栈中*/
/* 逆波兰表达式求值:实际与删除相邻相同字符一致 */
var evalRPN = function(tokens) {
        const stack = []
        let temp,symb
        const s = new Map([
        ["+", (a, b) => a * 1  + b * 1],
        ["-", (a, b) => b - a],
        ["*", (a, b) => b * a],
        ["/", (a, b) => (b / a) | 0]
        ])
        for (let i = 0; i < tokens.length; i++) {
            temp = tokens[i]
            if (!s.has(temp)) {
                    stack.push(temp)
                } else {
                    stack.push(s.get(temp)(stack.pop(),stack.pop()))
                }  
        }
        return stack.pop()
}
  1. 队列相关
  • 滑动窗口最大值:实际考察的是单调递减的双端队列
/* 给定一个数组与大小为k的窗口,返回滑动窗口中的最大值 */
/* 双指针+遍历法
基本思想:以每个窗口为单位,找到每个窗口的最大值,遍历整个数组 
*/
var maxSlidingWindow = function (nums, k) {
    /*  双指针法 */
    let i = 0, j = k - 1;
    let len = nums.length
    let res = [], max = nums[i]
    while (j < len) {
        for (q = i; q < j + 1; q++) {
            // 寻找最大值,可直接用Math.max()
            if (max < nums[q]) {
                max = nums[q]
            }
        }
        res.push(max)
        i++;
        j++;
    }
    return res
};


/* 双端队列法
基本思想:使用递减双端队列(实际存储每个元素索引值),队头元素是当前滑动窗口的最大值 
1. 检查队尾元素,看是不是都满足大于等于当前元素的条件。如果是的话,直接将当前元素入队。否则,将队尾元素逐个出队、直到队尾元素大于等于当前元素为止。
2.将当前元素入队
3. 检查队头元素,看队头元素是否已经被排除在滑动窗口的范围之外了。如果是,则将队头元素出队。
4. 判断滑动窗口的状态:看当前遍历过的元素个数是否小于 k。如果元素个数小于k,这意味着第一个滑动窗口内的元素都还没遍历完、第一个最大值还没出现,此时我们还不能动结果数组,只能继续更新队列;如果元素个数大于等于k,这意味着滑动窗口的最大值已经出现了,此时每遍历到一个新元素(也就是滑动窗口每往前走一步)都要及时地往结果数组里添加当前滑动窗口对应的最大值(最大值就是此时此刻双端队列的队头元素)。
*/
const maxSlidingWindowT = 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
}
  • 返回非空数组的前k个高频元素:考察优先队列
/* 返回非空数组的前k个高频元素 
方法一:map+遍历法
基本思想:1.将数组中出现的元素及其频次存入map 2.将map转换为数组,对出现频次的数字数组进行降序排序 3.获取降序排序后的前k个数组的元素
注:map.set(键名a,键值)方法在map中本来存在键名a时,会覆盖掉之前键名a对应的值
    Array.from()将map转换为二维数组,二维数组的每一个元素(一维数组)有两个元素(一个是map中的键名,一个是键值)
    sort()可以对元素为对象的数组进行排序
*/
var topKFrequent = function(nums, k) {
    let m = new Map()
    let array = new Array()
    // 存入map
    for(let i = 0;i<nums.length;i++){
        if(!m.has(nums[i])){
            m.set(nums[i],1)
        }else{
            m.set(nums[i],m.get(nums[i])+1)
        }
    }
   let arr = [...m]//将map转换成数组,效果等于Array.from(m),map转换为二维数组,数组的每个元素(一维数组)的元素为map的key与value
   let temp = m.entries()

   let res =[] //将二维数组展开为一维数组,以便排序
   arr.forEach((item)=>{
        res.push({
            number:item[0],
            frequency:item[1]
        })
   })
   res.sort((a,b)=>b.frequency-a.frequency)

   let ans = [] //取k频次前的
   for(let i = 0;i<k;i++){
    ans.push(res[i]['number'])
   }
   return ans
  
};

/*方法二:优先队列解法 */
var topKFrequent = function(nums, k) {
    // 用map记录元素对应的频率
    const map = new Map();
    for(const num of nums) {
      map.set(num, (map.get(num) || 0) + 1);
    }
  
    // 创建小顶堆
    const priorityQueue = new PriorityQueue((a, b) => a[1] - b[1]);
  
    // entry 是一个长度为2的数组,0位置存储key,1位置存储value
    for (const entry of map.entries()) {//map.entries()返回一组元素为[key,value] 的二元数组
      priorityQueue.push(entry);
      if (priorityQueue.size() > k) {
        priorityQueue.pop();
      }
    }
  
    //将优先队列中的元素的key值(即numbers数字)存入ret数组
    const ret = [];
    for(let i = priorityQueue.size() - 1; i >= 0; i--) {
      ret[i] = priorityQueue.pop()[0];
    }
    return ret;
  };
  
  /*************************************************/
  /* 优先队列实现 */
  function PriorityQueue(compareFn) {
    this.compareFn = compareFn;
    this.queue = [];
  }
  
  // 添加
  PriorityQueue.prototype.push = function(item) {
    this.queue.push(item);
    let index = this.queue.length - 1;
    let parent = Math.floor((index - 1) / 2);
    // 上浮
    while(parent >= 0 && this.compare(parent, index) > 0) {
      // 交换
      [this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]];
      index = parent;
      parent = Math.floor((index - 1) / 2);
    }
  }
  
  // 获取堆顶元素并移除
  PriorityQueue.prototype.pop = function() {
    const ret = this.queue[0];
  
    // 把最后一个节点移到堆顶
    this.queue[0] = this.queue.pop();
  
    let index = 0;
    // 左子节点下标,left + 1 就是右子节点下标
    let left = 1;
    let selectedChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
  
    // 下沉
    while(selectedChild !== undefined && this.compare(index, selectedChild) > 0) {
      // 交换
      [this.queue[index], this.queue[selectedChild]] = [this.queue[selectedChild], this.queue[index]];
      index = selectedChild;
      left = 2 * index + 1;
      selectedChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
    }
  
    return ret;
  }
  
  PriorityQueue.prototype.size = function() {
    return this.queue.length;
  }
  
  // 使用传入的 compareFn 比较两个位置的元素
  PriorityQueue.prototype.compare = function(index1, index2) {
    if (this.queue[index1] === undefined) {
      return 1;
    }
    if (this.queue[index2] === undefined) {
      return -1;
    }
  
    return this.compareFn(this.queue[index1], this.queue[index2]);
  }
  • 求数组中第k大的元素:考察优先队列(“第k大”或者“第k高“这样的关键字时,考虑使用优先队列)
    优先队列的特性
    》队列的头部元素,也即索引为0的元素,就是整个数组里的最值——最大值或者最小值
    》对于索引为 i 的元素来说,它的父结点下标是 (i-1)/2(上面咱们讲过了,这与完全二叉树的结构特性有关)
    》对于索引为 i 的元素来说,它的左孩子下标应为2i+1,右孩子下标应为2i+2。
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
const findKthLargest = function(nums, k) {
   // 初始化一个堆数组
   const heap = []  
   // n表示堆数组里当前最后一个元素的索引
   let n = 0
   // 缓存 nums 的长度
   const len = nums.length  
   // 初始化大小为 k 的堆
   function createHeap() {
       for(let i=0;i<k;i++) {
           // 逐个往堆里插入数组中的数字
           insert(nums[i])
       }
   }
   
   // 尝试用 [k, n-1] 区间的元素更新堆
   function updateHeap() {
       for(let i=k;i<len;i++) {
           // 只有比堆顶元素大的才有资格进堆
           if(nums[i]>heap[0]) {
               // 用较大数字替换堆顶数字
               heap[0] = nums[i]  
               // 重复向下对比+交换的逻辑
               downHeap(0, k)
           }
       }
   }
   
   // 向下对比函数
   function downHeap(low, high) {
       // 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
       let i=low,j=i*2+1 
       // 当 j 不超过上界时,重复向下对比+交换的操作
       while(j<=high) {
           // // 如果右孩子比左孩子更小,则用右孩子和根结点比较
           if(j+1<=high && heap[j+1]<heap[j]) {
               j = j+1
           }
           
           // 若当前结点比孩子结点大,则交换两者的位置,把较小的结点“拱上去”
           if(heap[i] > heap[j]) {
               // 交换位置
               const temp = heap[j]  
               heap[j] = heap[i]  
               heap[i] = temp
               
               // i 更新为被交换的孩子结点的索引
               i=j  
               // j 更新为孩子结点的左孩子的索引
               j=j*2+1
           } else {
               break
           }
       }
   }
   
   // 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
   function upHeap(low, high) {
       // 初始化 i(当前结点索引)为上界
       let i = high  
       // 初始化 j 为 i 的父结点
       let j = Math.floor((i-1)/2)  
       // 当 j 不逾越下界时,重复向上对比+交换的过程
       while(j>=low)  {
           // 若当前结点比父结点小
           if(heap[j]>heap[i]) {
               // 交换当前结点与父结点,保持父结点是较小的一个
               const temp = heap[j] 
               heap[j] = heap[i]  
               heap[i] = temp
               
               // i更新为被交换父结点的位置
               i=j   
               // j更新为父结点的父结点
               j=Math.floor((i-1)/2)  
           } else {
               break
           }
       }
   }

   // 插入操作=将元素添加到堆尾部+向上调整元素的位置
   function insert(x) {
       heap[n] = x  
       upHeap(0, n)
       n++
   }
   
   // 调用createHeap初始化元素个数为k的队
   createHeap()
   // 调用updateHeap更新堆的内容,确保最后堆里保留的是最大的k个元素
   updateHeap()
   // 最后堆顶留下的就是最大的k个元素中最小的那个,也就是第k大的元素
   return heap[0]
};

二叉树

一. 二叉树的种类与实现

一般二叉树节点的定义

 function TreeNode(val,left,right){
        this.val = (val===undefined?null:val)
        this.left = (left===undefined?null:left)
        this.right = (right===undefined?null:right)
    }
  1. 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k的满二叉树,有2^k-1个节点
  2. 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
  3. 二叉搜索树:一个有序树
  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉有序树
    :二叉搜索树的中序遍历序列是有序的!
  1. 平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二. 二叉树的存储方式
  1. 链式存储:使用指针来进行存储

在这里插入图片描述
2. 顺序存储:使用数组来存储、如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。在这里插入图片描述

三. 二叉树的遍历

基本遍历方式:深度优先遍历、广度优先遍历
深度优先遍历

  • 前序遍历
  • 中序遍历
  • 后序遍历
  1. 递归遍历
  • 递归遍历三要素:递归函数的参数及返回值,单层递归的逻辑,递归边界
  • 前序遍历:
var preorderTraversal = function(root) {
 let res=[];
 const dfs=function(root){
     if(root===null)return ;
     //先序遍历所以从父节点开始
     res.push(root.val);
     //递归左子树
     dfs(root.left);
     //递归右子树
     dfs(root.right);
 }
 //只使用一个参数 使用闭包进行存储结果
 dfs(root);
 return res;
};
  • 中序遍历
var inorderTraversal = function(root) {
    let res=[];
    const dfs=function(root){
        if(root===null){
            return ;
        }
        dfs(root.left);
        res.push(root.val);
        dfs(root.right);
    }
    dfs(root);
    return res;
};
  • 后序遍历
var postorderTraversal = function(root) {
    let res=[];
    const dfs=function(root){
        if(root===null){
            return ;
        }
        dfs(root.left);
        dfs(root.right);
        res.push(root.val);
    }
    dfs(root);
    return res;
};
  1. 迭代遍历
前序遍历:

// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function(root, res = []) {
    if(!root) return res;
    const stack = [root];
    let cur = null;
    while(stack.length) {
        cur = stack.pop();
        res.push(cur.val);
        cur.right && stack.push(cur.right);
        cur.left && stack.push(cur.left);
    }
    return res;
};

中序遍历:

// 入栈 左 -> 右
// 出栈 左 -> 中 -> 右

var inorderTraversal = function(root, res = []) {
    const stack = [];
    let cur = root;
    while(stack.length || cur) {
        if(cur) {
            stack.push(cur);
            // 左
            cur = cur.left;
        } else {
            // --> 弹出 中
            cur = stack.pop();
            res.push(cur.val); 
            // 右
            cur = cur.right;
        }
    };
    return res;
};

后序遍历:

// 入栈 左 -> 右
// 出栈 中 -> 右 -> 左 结果翻转

var postorderTraversal = function(root, res = []) {
    if (!root) return res;
    const stack = [root];
    let cur = null;
    do {
        cur = stack.pop();
        res.push(cur.val);
        cur.left && stack.push(cur.left);
        cur.right && stack.push(cur.right);
    } while(stack.length);
    return res.reverse();
};

广度优先遍历:层序遍历
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑。

  • 基本层序遍历
/* 基础版,使用BFS进行层序遍历 */
function BFS(root) {
    const queue = [] // 初始化队列queue
    // 根结点首先入队
    queue.push(root)
    // 队列不为空,说明没有遍历完全
    while(queue.length) {
        const top = queue[0] // 取出队头元素  
        // 访问 top
        console.log(top.val)
        // 如果左子树存在,左子树入队
        if(top.left) {
            queue.push(top.left)
        }
        // 如果右子树存在,右子树入队
        if(top.right) {
            queue.push(top.right)
        }
        queue.shift() // 访问完毕,队头元素出队
    }
}
  • 升级版层序遍历,需要按照层级返回每一层的节点
/* 升级版,使用层序遍历并按照层级返回每一层的节点 */
var levelOrder = function(root) {
    //二叉树的层序遍历
    let res=[],queue=[];
    queue.push(root);
    if(root===null){
        return res;
    }
    while(queue.length!==0){
        // 记录当前层级节点数
        let length=queue.length;
        //存放每一层的节点 
        let curLevel=[];
        for(let i=0;i<length;i++){
            let node=queue.shift();
            curLevel.push(node.val);
            // 存放当前层下一层的节点
            node.left&&queue.push(node.left);
            node.right&&queue.push(node.right);
        }
        //把每一层的结果放到结果数组
        res.push(curLevel);
    }
    return res;
};
四. 二叉树常见题型
  1. 二叉树的翻转
  • 递归版本的前序遍历翻转
var invertTree = function(root) {
     // 终止条件
    if (!root) {
        return null;
    }
    // 交换左右节点
    const rightNode = root.right;
    root.right = invertTree(root.left);
    root.left = invertTree(rightNode);
    return root;
};
  • 迭代版本的前序遍历
var invertTree = function(root) {
    //我们先定义节点交换函数
    const invertNode=function(root,left,right){
        let temp=left;
        left=right;
        right=temp;
        root.left=left;
        root.right=right;
    }
    //使用迭代方法的前序遍历 
    let stack=[];
    if(root===null){
        return root;
    }
    stack.push(root);
    while(stack.length){
        let node=stack.pop();
        if(node!==null){
            //前序遍历顺序中左右  入栈顺序是前序遍历的倒序右左中
            node.right&&stack.push(node.right);
            node.left&&stack.push(node.left);
            stack.push(node);
            stack.push(null);
        }else{
            node=stack.pop();
            //节点处理逻辑
            invertNode(node,node.left,node.right);
        }
    }
    return root;
};
  • 层序遍历
var invertTree = function(root) {
    //我们先定义节点交换函数
    const invertNode=function(root,left,right){
        let temp=left;
        left=right;
        right=temp;
        root.left=left;
        root.right=right;
    }
    //使用层序遍历
    let queue=[];
    if(root===null){
        return root;
    } 
    queue.push(root);
    while(queue.length){
        let length=queue.length;
        while(length--){
            let node=queue.shift();
            //节点处理逻辑
            invertNode(node,node.left,node.right);
            node.left&&queue.push(node.left);
            node.right&&queue.push(node.right);
        }
    }
    return root;
};
  1. 对称二叉树
  • 通过递归判断是否是对称二叉树
var isSymmetric = function(root) {
    //使用递归遍历左右子树 进行递归三步骤
    //1.确定递归参数,root.left root.right与返回值 true false
    const compareNode = function(left,right){
    //2.确认终止条件(递归边界)
        if(left == null&&right!==null||left!==null&&right==null){
            return false
        }else if(right==null&&left==null){
            return true
        }else if(left.val!==right.val){
            return false
        }

    //3.确定递归单层逻辑
    let outSide = compareNode(left.left,right.right)
    let inSide = compareNode(left.right,right.left)
    return outSide&&inSide
    }

    //首先判断根节点是否为空
    if(root === null){
        return true
    }
    return compareNode(root.left,root.right)
};
  • 通过队列实现迭代判断是否为对称二叉树
var isSymmetric = function(root) {
    //迭代方法判断是否是对称二叉树
    //首先判断root是否为空
    if(root===null){
        return true;
    }
    let queue=[];
    queue.push(root.left);
    queue.push(root.right);
    while(queue.length){
        let leftNode=queue.shift();//左节点
        let rightNode=queue.shift();//右节点
        if(leftNode===null&&rightNode===null){
            continue;
        }
        if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
            return false;
        }
        queue.push(leftNode.left);//左节点左孩子入队
        queue.push(rightNode.right);//右节点右孩子入队
        queue.push(leftNode.right);//左节点右孩子入队
        queue.push(rightNode.left);//右节点左孩子入队
    }
    return true;
  };
  • 通过栈实现迭代判断是否为对称二叉树
  /*通过栈实现迭代判断是否是对称二叉树 */
  var isSymmetric = function(root) {
    //迭代方法判断是否是对称二叉树
    //首先判断root是否为空
    if(root===null){
        return true;
    }
    let stack=[];
    stack.push(root.left);
    stack.push(root.right);
    while(stack.length){
        let rightNode=stack.pop();//左节点
        let leftNode=stack.pop();//右节点
        if(leftNode===null&&rightNode===null){
            continue;
        }
        if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
            return false;
        }
        stack.push(leftNode.left);//左节点左孩子入队
        stack.push(rightNode.right);//右节点右孩子入队
        stack.push(leftNode.right);//左节点右孩子入队
        stack.push(rightNode.left);//右节点左孩子入队
    }
    return true;
  };
  1. 二叉树的最大深度
/* 求二叉树的最大深度:
二叉树的深度定义:二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 */
var maxDepth = function (root) {
    //首先解决特殊情况,root为空
    if (root === null) return 0
    //使用递归方法 递归三步骤
    //1.确定递归参数与返回值
    const getdepth = function (node) {
    //2.递归终止条件(递归边界)
        if (node === null) {
            return 0
        }
    //3.确定单层逻辑(即反复调用自己)
        let leftDepth = getdepth(node.left)
        let rightDepth = getdepth(node.right)
        let depth = 1 + Math.max(leftDepth, rightDepth)
        return depth
    }
    return getdepth(root)
};
  1. 二叉树的最小深度
/* 求二叉树的最小深度
最小深度:从根节点到最近叶子节点的最短路径上的节点数量 */
var minDepth = function(root) {
    if(root === null) return 0

    // 开始递归判断最小
    //1/ 首先确定递归的参数与返回值
    const getdepth = function(node){
        //2.确定递归边界
        if(node === null) return 0;
        //3. 递归单层逻辑
        let leftDepth = getdepth(node.left)
        let rightDepth = getdepth(node.right)
        //当左子树为空,右子树不为空时,并不是最小深度
        if(node.left == null&&node.right!=null){
            return 1+rightDepth
        }
        //当右子树为空,左子树不为空时,并不是最小深度
        if(node.right == null&&node.left!=null){
            return 1+leftDepth
        }
        return 1+Math.min(leftDepth,rightDepth)
    
    }
    return getdepth(root)
};
  1. 求完全二叉树的节点数
  • 从完全二叉树的角度出发求解节点数量
/*完全二叉树求法:
完全二叉树:除了最底层节点没有填满,其余每层节点数达到了最大值,且下面一层的节点都集中在该层最左边的若干位置。完全二叉树又可以分为满二叉树的情况与最后一层叶子节点没有铺满的情况
 基本思想:将待判断深度的二叉树使用递归判断其是否为满二叉树,是满二叉树的话就可以使用2^k-1来计算该满二叉树的节点数量
*/
var countNodes = function(root) {
    //利用完全二叉树的特点
    if(root===null){
        return 0;
    }

    //每层递归的逻辑
    let left=root.left;
    let right=root.right;
    let leftDepth=0,rightDepth=0;
    while(left){
        left=left.left;
        leftDepth++;
    }
    while(right){
        right=right.right;
        rightDepth++;
    }

    //终止条件
    if(leftDepth==rightDepth){
        return Math.pow(2,leftDepth+1)-1;
    }
    return countNodes(root.left)+countNodes(root.right)+1;
};
  • 从普通二叉树的角度出发递归求解
/* 普通二叉树求法:递归方法 */
var countNodes = function(root) {
    if(!root) return 0
    function getNodes(node){ 
        if(!node) return 0 
        let leftNumber = getNodes(node.left)
        let rightNumber = getNodes(node.right)
        let curNumber = leftNumber + rightNumber+1
        return curNumber
    }
    return getNodes(root)
};
  1. 判断一棵树是否为高度平衡树:高度平衡树是指一个二叉树上的每个节点的左右两个子树的高度差不超过1
  • 递归后序遍历
/* 判断一棵树是否为高度平衡树
高度平衡树的定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
区别高度与深度:高度是从下往上(后序遍历)、深度是从上往下(前序遍历)
基本思想:本题求解高度相关,使用后序遍历递归 */
var isBalanced = function(root) {
    //还是用递归三部曲 + 后序遍历 左右中 当前左子树右子树高度相差大于1就返回-1
    // 1. 确定递归函数参数以及返回值
    const getDepth = function(node) {
        // 2. 确定递归函数终止条件
        if(node === null) return 0;
        // 3. 确定单层递归逻辑
        let leftDepth = getDepth(node.left); //左子树高度
        // 当判定左子树不为平衡二叉树时,即可直接返回-1
        if(leftDepth === -1) return -1;
        let rightDepth = getDepth(node.right); //右子树高度
        // 当判定右子树不为平衡二叉树时,即可直接返回-1
        if(rightDepth === -1) return -1;
        if(Math.abs(leftDepth - rightDepth) > 1) {
            return -1;
        } else {
            return 1 + Math.max(leftDepth, rightDepth);
        }
    }
    return !(getDepth(root) === -1);
};
  1. 二叉搜索树的实现
  • 二叉搜索树节点插入(亦称为二叉搜索树的实现)
/*工信出版社解法:主要是新节点的插入,为迭代法*/

 function TreeNode(val,left,right){
        this.val = (val===undefined?null:val)
        this.left = (left===undefined?null:left)
        this.right = (right===undefined?null:right)
        this,show = show
    }
  function show(){
        return this.val
    }

    function BST(){
        this.root = null
        this.insert = insert
        this.inOrder = inOrder
    }
    function insert(data){
        // 需要插入的新节点(此时生成该节点,即将该节点作为TreeNode对象)
        var n =  new TreeNode(data,null,null)
        
        // 若当前二叉树没有节点,则该新节点为该二叉树的根节点
        if(this.root == null){
            this.root = n
        }

        //若有根节点,则遍历该二叉树找到合适位置进行新节点的插入
        else{
            var cur = this.root
            var parent
            while(true){
                parent = cur
                // 若新节点比根节点的值小,则往下(左)继续比较
                if(data<cur.val){
                    cur = cur.left
                    //若当前节点的左节点不存在,则新节点为当前的左节点
                    if(cur == null){
                        parent.left = n
                        break;
                    }
                }
                // 若新节点比根节点的值大,则往下(右)继续比较
                else{
                    cur = cur.right 
                    if(cur == null){
                        parent.right = n
                        break;
                    }
                }
            }
        }
    }


/* 掘金修言解法:递归法 */
function insertIntoBST(root, n) {
    // 若 root 为空,说明当前是一个可以插入的空位
    if(!root) { 
        // 用一个值为n的结点占据这个空位
        root = new TreeNode(n)
        return root
    }
    
    if(root.val > n) {
        // 当前结点数据域大于n,向左查找
        root.left = insertIntoBST(root.left, n)
    } else {
        // 当前结点数据域小于n,向右查找
        root.right = insertIntoBST(root.right, n)
    }

    // 返回插入后二叉搜索树的根结点
    return root
}
  • 二叉搜索树节点查找
/*工信出版社查找*/
//最大值查找
function getMax(){
 	var cur = this.root
 	while(cur.right!=null){
 		cur = cur.right
 	}
 	return cur.val
}
//最大值查找
function getMin(){
 	var cur = this.root
 	while(cur.left!=null){
 		cur = cur.left
 	}
 	return cur.val
}
//查找指定值
function search(root,n){
	if(root == null) return newNode(n)
	var cur = this.root
	while(cur != null){
		if(cur.val == n) {
			return cur
		}else if(cur.val > n){
			cur = cur.left
		}else{
		cur = cur.left
		}
	}
	return null
}


/*修言查找指定值*/
function search(root, n) {
    // 若 root 为空,查找失败,直接返回
    if(!root) {
        return 
    }
    // 找到目标结点,输出结点对象
    if(root.val === n) {
        console.log('目标结点是:', root)
    } else if(root.val > n) {
        // 当前结点数据域大于n,向左查找
        search(root.left, n)
    } else {
        // 当前结点数据域小于n,向右查找
        search(root.right, n)
    }
}
  • 二叉搜索树删除指定节点
/* 修言版删除指定节点 */
/* 修言版:
基本思想:1. 寻找节点是否存在,如果不存在,直接返回null
          2.找到节点,若为叶子节点,直接删除;若有左右一个结点,用其有的子节点替代其即可;若两个都有,可以用左子树的最大值(右子树的最小值)替代
*/
function deleteNode(root, n) {
    // 如果没找到目标结点,则直接返回
    if(!root) {
        return root
    }
    // 定位到目标结点,开始分情况处理删除动作
    if(root.val === n) {
        // 若是叶子结点,则不需要想太多,直接删除
        if(!root.left && !root.right) {
            root = null
        } else if(root.left) {
            // 寻找左子树里值最大的结点
            const maxLeft = findMax(root.left)
            // 用这个 maxLeft 覆盖掉需要删除的当前结点  
            root.val = maxLeft.val
            // 覆盖动作会删除掉原有的 maxLeft 结点
            root.left = deleteNode(root.left, maxLeft.val)
        } else {
            // 寻找右子树里值最小的结点
            const minRight = findMin(root.right)
            // 用这个 minRight 覆盖掉需要删除的当前结点  
            root.val = minRight.val
            // 覆盖动作会删除掉原有的 minRight 结点
            root.right = deleteNode(root.right, minRight.val)
        }
    } else if(root.val > n) {
        // 若当前结点的值比 n 大,则在左子树中继续寻找目标结点
        root.left = deleteNode(root.left, n)
    } else  {
        // 若当前结点的值比 n 小,则在右子树中继续寻找目标结点
        root.right = deleteNode(root.right, n)
    }
    return root
}

// 寻找左子树最大值
function findMax(root) {
    while(root.right) {
        root = root.right
    }
    return root 
}

// 寻找右子树的最小值
function findMin(root) {
    while(root.left) {
        root = root.left
    }
    return root
}


/* 简洁版:
基本思想:1. 寻找节点是否存在,如果不存在,直接返回null
          2.找到节点,若为叶子节点,直接删除;若有左右一个结点,用其有的子节点替代其即可;若两个都有,将左子树转移到其右子树的最左节点的左子树上,用右子树代替待删除节点位置
*/

四.特殊的二叉树
  • 前置知识:完全二叉树
    》从第一层到倒数第二层,每一层都是满的,也就是说每一层的结点数都达到了当前层所能达到的最大值
    》最后一层的结点是从左到右连续排列的,不存在跳跃排列的情况(也就是说这一层的所有结点都集中排列在最左边)
    》对于索引为n的结点来说,索引为 (n-1)/2 的结点是它的父结点,2 * n+1 的结点是它的左子结点,2 * n+2的结点是它的右孩子结点
  • 大顶堆:对一棵完全二叉树来说,它每个结点的结点值都不小于其左右孩子的结点值,这样的完全二叉树就叫做大顶堆
  • 若树中每个结点值都不大于其左右孩子的结点值,这样的完全二叉树就叫做“小顶堆”
  • 堆顶元素的删除(向下比较+交换)
// 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
function downHeap(low, high) {
    // 初始化 i 为当前结点,j 为当前结点的左孩子
    let i=low,j=i*2+1 
    // 当 j 不超过上界时,重复向下对比+交换的操作
    while(j <= high) {
        // 如果右孩子比左孩子更大,则用右孩子和根结点比较
        if(j+1 <= high && heap[j+1] > heap[j]) {
            j = j+1
        }
        
        // 若当前结点比孩子结点小,则交换两者的位置,把较大的结点“拱上去”
        if(heap[i] < heap[j]) {
            // 交换位置
            const temp = heap[j]  
            heap[j] = heap[i]  
            heap[i] = temp
            
            // i 更新为被交换的孩子结点的索引
            i=j  
            // j 更新为孩子结点的左孩子的索引
            j=j*2+1
        } else {
            break
        }
    }
}
  • 堆内元素的增加
// 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
function upHeap(low, high) {
    // 初始化 i(当前结点索引)为上界
    let i = high  
    // 初始化 j 为 i 的父结点
    let j = Math.floor((i-1)/2)  
    // 当 j 不逾越下界时,重复向上对比+交换的过程
    while(j>=low)  {
        // 若当前结点比父结点大
        if(heap[j]<heap[i]) {
            // 交换当前结点与父结点,保持父结点是较大的一个
            const temp = heap[j] 
            heap[j] = heap[i]  
            heap[i] = temp
            
            // i更新为被交换父结点的位置
            i=j   
            // j更新为父结点的父结点
            j=Math.floor((i-1)/2)  
        } else {
            break
        }
    }
}

回溯

一.回溯解题模板
function xxx(入参) {
    前期的变量定义、缓存等准备工作 
    
    // 定义路径栈(组合)
    const path = []
    
    // 进入 dfs
    dfs(起点) 
    
    // 定义 dfs
    dfs(递归参数) {
      if(到达了递归边界) {
        结合题意处理边界逻辑,往往和 path 内容有关
        return   
      }
      
      // 注意这里也可能不是 for,视题意决定
      for(遍历坑位的可选值) {
        path.push(当前选中值)
        处理坑位本身的相关逻辑
        path.pop()
      }
    }
  }
2.回溯常见题型
  • 组合问题:N个数里面按一定规则找出k个数的集合
const combine = function(n, k) {
    // 初始化结果数组
     const res = []   
     // 初始化组合数组
     const subset = []
     // 进入 dfs,起始数字是1
     dfs(1)  
 
     // 定义 dfs 函数,入参是当前遍历到的数字
     function dfs(index) {
         if(subset.length === k) {
             res.push(subset.slice())
             return 
         }
         
         // 从当前数字的值开始,遍历 index-n 之间的所有数字
         for(let i=index;i<=n;i++) {
             // 这是当前数字存在于组合中的情况
             subset.push(i) 
             // 基于当前数字存在于组合中的情况,进一步 dfs
             dfs(i+1)
             // 这是当前数字不存在与组合中的情况
             subset.pop()
         }
     }
     // 返回结果数组
     return res 
 };

》找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。解集中不能包含重复的数字

var combinationSum3 = function(k, n) {
    const backtrack = (start) => {
        const l = path.length;
        if (l === k) {
            const sum = path.reduce((a, b) => a + b);
            if (sum === n) {
                res.push([...path]);
            }
            return;
        }
        for (let i = start; i <= 9 - (k - l) + 1; i++) {
            path.push(i);
            backtrack(i + 1);
            path.pop();
        }
    }
    let res = [], path = [];
    backtrack(1);
    return res
}

》求2-9的字符串能映射出的字母组合

/* 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合 */
var letterCombinations = function (digits) {
    const res = [], path = []
    const map = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
    if (!digits.length) return res
    if (k === 1) return map[digits].split("")
    traceBack(digits,digits.length,0)


    function traceBack(n, k, a) {
        //回溯边界
        if (path.length == k ) {
            res.push(path.join(""))
            return res
        }

        for(const v of map[n[a]]){
            path.push(v)
            traceBack(n,k,a+1)
            path.pop()
        }
    }
};

》求数组中元素和等于目标数和的所有数字组合,数组中元素可以重复取

/* 该数组中的数字可以被重复多次选取 */
var combinationSum = function (candidates, target) {
    const path = [], res = []
    if (!candidates) return []
    candidates.sort((a,b)=>a-b)

    function traceBack(j,sum) {
        if (sum == target) {
            res.push(path.slice())
            return
        }
        
        for(let i = j;i<candidates.length;i++){
            const n = candidates[i]
            if(n>target-sum) break;
            path.push(n);
            sum += n
            traceBack(i,sum)
            path.pop()
            sum -= n
        }

        
    }
    traceBack(0,0)
    return res
};

》数组中元素和等于目标数和的所有数字组合,数组中元素不可以重复取,数组中又重复元素

  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值