[数据结构和算法04] 链表 和 树

322 阅读15分钟

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式
[封装03-设计模式] Decorator 装饰器模式在前端的应用
[封装04-设计模式] Publish Subscribe 发布订阅模式在前端的应用
[封装05-ElementUI源码01] Row Col Container Header Aside Main Footer

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序
[数据结构和算法02] 回文字符串
[数据结构和算法03] 栈 和 队列
[数据结构和算法04] 链表 和 树

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

复习笔记-01
复习笔记-02
复习笔记-03
复习笔记-04

(一) 前置知识

(1) 一些单词

linkedList 链表
invert 翻转
binary tree 二叉树
balance 平衡 // balance binary tree 平衡二叉树
symmetric 对称的
average 平均值

preorder 前序遍历
inorder 中序遍历
postorder 后续遍历
traversal 遍历 n
deep first search 深度优先 ----- DFS
breath first search 广度优先 --- BFS

(二) 链表

(1) 链表的一些概念

  • 链表是一组 ( 节点 ) 组成的集合
  • 每个节点都是一个对象,对象的引用指向下一个节点,指向下一个节点的引用叫做 ( 链 )
  • 节点
    • data 保存数据
    • next 下一个节点的引用
    • prev 上一个节点的引用
  • 头节点
    • 因为链表头节点的确定比较麻烦,因此在最前面添加一个特殊的节点,叫做头节点
    • 表示链表头部
  • 有头节点的链表的 ( 插入 ) 操作
    • 有头节点的链表的插入操作,效率比较高
  • 特点
    • 插入和删除的时间复杂度
      • 单链表插入和删除的时间复杂度是 O(1)
      • 首先遍历寻找的时间复杂度O(n),然后添加删除的时间复杂度是O(1) => O(n)
    • 非连续性
      • 链表是非连续的,所以不需要一整块完整连续的内存空间
      • 对比数组:数组则需要是完整的连续内存空间
  • 链表对比数组
    • 查找
      • 数组查找:因为是连续的内存空间,只需要计算偏移量,效率高
      • 链表查找:因为内存空间不连续,每次都必须从头节点开始遍历通过指针找到目标节点,效率低
    • 添加和删除
      • 数组添加和删除:添加删除元素后,其余的元素要来占据删除的元素的位置,所以要移动很多节点,效率低
      • 链表查找:直接操作通过指针执行,不需要移动节点,效率高

(2) js实现单链表

单链表的实现
---


// 合并两个有序链表

// 为什么不把 NodeConstructor 放在 LinkedList 的外面?
// 因为可以内部维护,添加和删除时关注value本身
// function NodeConstructor(value) {
//   this.value = value; // 值
//   this.next = null; // 指针
// }

function LinkedList() {
  // NodeConstructor 构造函数,用来生成节点
  function NodeConstructor(el) {
    this.el = el; // 值
    this.next = null; // 指针
  }
  this.head = null; // 单链表头节点
  this.length = 0; // 单链表长度

  // 1
  // append 在尾部添加节点
  this.append = (el) => {
    const node = new NodeConstructor(el);
    if (!this.head) {
      this.head = node;
    } else {
      let current = this.head; // 这里current和this.head都指向同一个引用,对象类型可以使用const
      while (current.next) {
        // head节点存在,不断通过next指针找到尾节点,尾节点next指向null
        current = current.next;
      }
      current.next = node;
    }

    this.length++;
  };

  // 2
  // find
  this.find = (id) => {
    let currentId = 0;
    let current = this.head;
    while (currentId < this.size()) {
      if (currentId === id) {
        return current;
      }
      currentId++;
      current = current.next;
    }
  };

  // 3
  // findIndex indexOf 寻找链表中的节点
  this.findIndex = this.indexOf = (el) => {
    let current = this.head; // 缓存
    let currentId = 0;

    // 遍历所有节点,如果找到节点,就返回节点的id,否则继续向后寻找
    while (currentId < this.size()) {
      if (current.el === el) return currentId;
      currentId++;
      current = current.next;
    }
  };

  // 4
  // insert 插入节点
  // 实现插入节点逻辑首先我们要考虑边界条件,如果插入的位置在头部或者比尾部位置还大,我们就没必要从头遍历一遍处理了,这样可以提高性能
  this.insert = (position, el) => {
    const node = new NodeConstructor(el);
    let prevNode = null;
    let currentNode = this.head;
    let currentId = 0;

    if (position >= 0 && position <= this.length) {
      if (position === 0) {
        node.next = currentNode; // 插入节点的next指向头节点
        this.head = node; // 再重新赋值给头节点,则更新头节点,注意头插时一定要更新头节点,一半情况下不需要更新头节点
      } else {
        while (currentId < position) {
          currentId++;
          prevNode = currentNode;
          currentNode = currentNode.next;
        }
        // 找到位置后
        node.next = currentNode;
        prevNode.next = node;
        this.length++; // 插入完成后,length++
        return true;
      }
    } else {
      return false; // 不在链表的范围内,简单的处理成不做处理
    }
  };

  // 5
  // removeAt
  // 移除指定位置的元素
  // 移除指定位置的节点也需要判断一下边界条件,可插入节点类似,但要注意移除之后一定要将链表长度-1
  this.removeAt = (position) => {
    // position表示位置,是number类型
    // 检测边界条件
    if (position >= 0 && position < this.length) {
      let prevNode = null; // 缓存当前节点,也可以理解为是当前节点的前一个节点
      let current = this.head; // 当前节点
      let currentIndex = 0; // 当前节点位置

      if (position === 0) {
        this.head = current.next; // 如果删除链表的第一个节点,则直接把第二个节点赋值给第第一个节点 ( head = head.next )
      } else {
        // 这里没有直接判断是不是最后一个,因为没有最后一个的标志位指针,还是要从头遍历 ( 即没有做这样的判断 position === length -1 )
        while (currentIndex++ < position) {
          prevNode = current;
          current = current.next; // 不断重头往后遍历,知道找到position所在的节点
        }
        prevNode.next = current.next;
        // 将 ( 要删除的节点的前一个节点的next ) 指向 ( 将要删除的节点的下一个节点 )
        // 这里包含了最后一个节点的删除,当是最后一个节点时,current.next === null,prevNode.next = current.next = null 表达式也是成立的
      }
      this.length--; // 操作完成,长度-1
      return current.el; // 返回删除的节点上的数据el
    } else {
      return null; // 随便返回一个值,表示没做任何操作,因为位置都不在链表上
    }
  };

  // 6
  // 移除指定节点
  // 移除指定节点实现非常简单,我们只需要利用之前实现好的查找节点先找到节点的位置,然后再用实现过的removeAt即可
  this.remove = (el) => {
    let index = this.indexOf(el);
    this.removeAt(index);
  };

  // 7
  // 判断链表是否为空
  // 只需要判断 length 是否为0,返回boolean
  this.isEmpty = () => {
    return this.length === 0;
  };

  // 8
  // 将链表转化为数组返回
  this.toArray = () => {
    let current = this.head;
    let resultList = [];
    while (current.next) {
      // 节点的next存在,就把el添加进数组,直到最后一个节点,最后一个节点的 ( lastNde.next = null )
      resultList.push(current.el);
      current = current.next; // 不断 next
    }
    // 注意尾节点的处理
    resultList.push(current.el);
    return resultList;
  };

  this.size = () => this.length;
}

const linkedList = new LinkedList();
linkedList.append(10);
linkedList.append(20);
linkedList.append(30);
linkedList.append(40);
console.log(`linkedList.indexOf(40)`, linkedList.indexOf(40));
console.log(`linkedList.length`, linkedList.length);
linkedList.insert(2, 800);
console.log(`linkedList.length`, linkedList.length);
console.log(`linkedList.findIndex(800)`, linkedList.findIndex(800));
console.log(`linkedList.findIndex(40)`, linkedList.findIndex(40));
linkedList.remove(10);
console.log(`linkedList.find(0)`, linkedList.find(0));
const arr = linkedList.toArray();
console.log(`arr`, arr);

(3) 反转链表

// 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
// ---
// 输入: head = [1,2,3,4,5]
// 输出: [5,4,3,2,1]
// ---
// 输入:head = []
// 输出:[]
// ---

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
  if (!head || !head.next) return head; // 边界

  let [pre, cur] = [null, head];
  // pre 用来标记反转后的头节点
  // cur 当前正序的处理的节点

  while (cur) {
    const temp = cur.next; // 用来缓存当前正序的下一个节点,因为之后会去修改cur,就不知道下一个节点是指向谁了,所以先缓存起来
    cur.next = pre; // 反转链表,指向反转链表的头节点

    // 以下是反转链表后的处理
    // 2022-09-29更新 --- 即 ( 往后移动指针 ),进行下一次遍历
    pre = cur; // 反转链表的头节点就是cur
    cur = temp; // 正序的链表指针向后移,进行下一轮处理
  }

  return pre; // 返回反转后的链表头节点
};
2022.04.06 - 反转链表
---
function reverseLinkedList(head) {
  if (head === null) return null; // 边界

  let [prev, current] = [null, head]; // 头节点的上一个节点是null,current为head节点

  while (current) {
    const nextTemp = current.next; // 缓存当前节点的下一个节点,因为下面会修改 current.next,就无法找到正序遍历时的下一个节点
    current.next = prev; // 反转指针指向

    // (反转后的处理) - 修改指针指向后的处理,为下一次循环做准备
    // 其实就是移动指针,往后移动
    prev = current; // 将当前遍历的节点的上一个元素,指向当前节点 ----- 即遍历后移
    current = nextTemp; // 将当前遍历的元素,重新设置为下一个元素 --- 即遍历后移
  }

  return prev;
}

image.png

(4) 合并两个有序链表

// 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
// 输入:l1 = [1,2,4], l2 = [1,3,4]
// 输出:[1,1,2,3,4,4]
// ---
// 输入:l1 = [], l2 = []
// 输出:[]
// ---
// 输入:l1 = [], l2 = [0]
// 输出:[0]

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} list1
 * @param {ListNode} list2
 * @return {ListNode}
 */
var mergeTwoLists = function (list1, list2) {
  if (list1 === null) return list2; // 如果其中一个为空,就直接返回另一个链表
  if (list2 === null) return list1;

  let list3 = new ListNode(null); // 先生成一个空节点,val=null next=null
  let cacheList3Head = list3;
  // 这里缓存一下list3的头节点,该头节点是我们自己定义的头节点,和我们最终返回的头节点不一样
  // 问题:为什么要缓存头节点呢?
  // 回答:因为list我们在不断的重新赋值next,那么执行后指针不是在头节点了

  while (list1 && list2) {
    // 遍历两个链表,直到其中一个为空
    if (list1.val < list2.val) {
      // 比较两个链表中的最前面的节点,取小的添加给list链表,然后移动list的next指针到尾节点,同时移动小的链表的指针next,继续比较list.next.val 和 list2.val
      // 问题:为什么要移动 list3
      // 回答:因为list3中还要添加新的节点进来,必须在尾节点上的 next 指向新的节点
      list3.next = list1;
      list3 = list3.next;
      list1 = list1.next;
    } else {
      list3.next = list2;
      list3 = list3.next;
      list2 = list2.next;
    }
  }
  
  // 当上面的整个while循环结束,表示其中一个链表已经遍历完了,继续遍历有剩余节点的链表,添加给list3
  // while (list1) {
  //   list3.next = list1;
  //   list3 = list3.next;
  //   list1 = list1.next;
  // }

  // while (list2) {
  //   list3.next = list2;
  //   list3 = list3.next;
  //   list2 = list2.next;
  // }
  
  // 2022-09-29 优化如下
  if (list1) {
    list3.next = list1;
  }
  if (list2) {
    list3.next = list2;
  }

  return cacheList3Head.next;
  // 最后返回新节点的头节点,不然我们初始化list3的时的头节点并不是答案中的头节点,把我们自己的头节点的next才是真正的返回的合并后的头节点
};

image.png

(三) 树

(1) 树的一些概念

  • 二叉树是一种非线性结构,二叉树是递归定义的,其结点有左右子树之分
  • 二叉树通常采用链式存储结构
  • 节点
    • 和链表中的节点一样,其实就是一个对象,具有值和指针
    • 指的是 ( 节点拥有的子树的数量 )
  • 叶子节点
    • 指的是:度为0的节点,即没有子节点的节点
  • 高度
    • 高度 = 深度 = 层数
  • 二叉树
    • 满二叉树
      • 深度为k,具有 2 ^ k - 1 个节点的树,叫做满二叉树
      • 特点
        • 深度和节点的关系:深度为k,具有 2 ^ k - 1 个节点
        • 第i层的节点数:2 ^ (i - 1) 个节点
    • 完全二叉树
      • 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树
      • 特点
        • 相对于满二叉树,完全二叉树的最后一层不完成,并且是右边部分缺失
    • 平衡二叉树
      • 每个节点的 ( 左右子树 ) 的高度差的绝对值不超过 1
    • 二叉搜索树
        1. 节点的 左子树 只包含 小于 当前节点的数
        1. 节点的 右子树 只包含 大于 当前节点的数
        1. 所有 左子树和右子树 自身必须也是二叉搜索树
  • 遍历
    • 深度优先 - DFS
      • 前序遍历(先根遍历): -> 左子树 -> 右子树
      • 中序遍历(中根遍历):左子树 -> -> 右子树
      • 后续遍历(后根遍历):左子树 -> 右子树 ->
      • 一般用递归
    • 广度优先 - BFS
      • 一般用队列

(2) 翻转二叉树

// 翻转一棵二叉树
// 前
//      4
//    /   \
//   2     7
//  / \   / \
// 1   3 6   9
// 后
//      4
//    /   \
//   7     2
//  / \   / \
// 9   6 3   1

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function (root) {
  reverse(root);

  function reverse(root) {
    if (!root) return; // 递归结束条件

    [root.left, root.right] = [root.right, root.left]; // 递归交换左右子树

    reverse(root.left); // 递归
    reverse(root.right);
  }

  return root;
};

image.png

(3) 中序遍历二叉树

// 中序遍历二叉树 - 递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
  // 中序遍历指的是:左子树 - 根 - 右子树
  // 递归实现

  const result = [];

  inorderTraversal(root);

  // 问题:这里为什么要再声明一个函数,而不直接使用inorderTraversal
  // 回答:因为要使 result 不被重复声明,类似充当一个闭包变量或者全局变量
  function inorderTraversal(root) {
    if (root) {
      inorderTraversal(root.left); // 左子树的递归
      result.push(root.val); // 根节点,最左边的叶子节点最先进入数组
      inorderTraversal(root.right); // 右子树的递归
    }
  }

  return result;
};

image.png

image.png

(4) 前序遍历 - 递归的方法和中序差不多

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {

    const result = []

    function preorderTraversal(root) {
        if (root) {
            result.push(root.val)
            preorderTraversal(root.left)
            preorderTraversal(root.right)
        }
    }

    preorderTraversal(root)

    return result
};

image.png

(5) 二叉树的最大深度 - 递归

// 求二叉树的最大深度
/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 **/
// ---------
//   3
// /   \
// 9  20
//   /  \
// 15   7
// 返回它的最大深度 3
// 思路:深度是 左子树深度 和 右子树 深度的较大值,然后递归
// ---------
// 【1】
// recursive(root.left)
// --> 9
// --> return Math.max(recursive(root.left), recursive(root.right)) + 1
// --> return Math.max(0, 0) + 1
// --> 1
// 【2】
// recursive(root.right)
// 20
// --> left -> 15 -> 1
// --> right -> 7 -> 1
// -->  Math.max(recursive(root.left), recursive(root.right)) + 1 --> Math.max(1,1) + 1 --> 2
// 【3】
// Math.max(recursive(root.left), recursive(root.right)) + 1 --> Math.max(1, 2) + 1 --> 3

var maxDepth = function (root) {
  function recursive(root) {
    if (!root) return 0; // 递归结束的条件,返回值
    return Math.max(recursive(root.left), recursive(root.right)) + 1; // 递归的返回值
  }

  return recursive(root);
};

image.png

(6) 判断是否是平衡二叉树

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */

// getDeep
// - 获取二叉树的最大高度
// - 左右子树高度的较大值,递归回溯
function getDeep(root) {
  if (!root) return 0;
  const leftDeep = getDeep(root.left);
  const rightDeep = getDeep(root.right);
  return Math.max(leftDeep, rightDeep) + 1;
}

// isBalanced
// - 是否是平衡二叉树
var isBalanced = function (root) {
  if (!root) return true; // ------- 递归结束的条件,是平衡二叉树
  const leftHeight = getDeep(root.left); // 参数节点的左子树的高度
  const rightHeight = getDeep(root.right); // 参数节点的右子树的高度
  const diff = Math.abs(leftHeight - rightHeight); // 左右子树的高度差的绝对值
  if (diff > 1) return false; // --- 左右子树高度差绝对值大于1,就不是平衡二叉树

  return isBalanced(root.left) && isBalanced(root.right); // 递归判断左右子树是否是平衡二叉树
};

image.png

image.png

(7) 二叉树是否对称

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */

//     1
//    / \
//   2   2
//  / \ / \
// 3  4 4  3

var isSymmetric = function (root) {
  if (!root) return true;

  function check(left, right) {
    if (!left && !right) return true; // 比较的两个节点不存在 -> 对称
    if (!left || !right) return false; // 比较的两个节点,一边存在,一边不存在 -> 非对称
    if (left.val !== right.val) return false; // 比较的两个节点,不等 -> 非对称

    return check(left.left, right.right) && check(left.right, right.left);
    // ( 左子树的左节点 和 右子树的右节点 相等 ) 并且 ( 左子树的右节点 和 右子树的左节点 相等) -> 对称
  }

  return check(root.left, root.right);
};

image.png

image.png

(8) 合并两颗树

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root1
 * @param {TreeNode} root2
 * @return {TreeNode}
 */
var mergeTrees = function (root1, root2) {
  function merge(root1, root2) {
    if (!root1) return root2;
    if (!root2) return root1;

    // 我们以root1作为基准,合并后,返回合并后的root1
    root1.val = root1.val + root2.val;
    root1.left = merge(root1.left, root2.left); //递归
    root1.right = merge(root1.right, root2.right);

    return root1;
  }

  return merge(root1, root2);
};

image.png

(9) 二叉搜索树中的搜索

// 题目 二叉搜索树中的搜索

// 二叉搜索树的定义
// - 1. 节点的 左子树 只包含 小于 当前节点的数
// - 2. 节点的 右子树 只包含 大于 当前节点的数
// - 3. 所有 左子树和右子树 自身必须也是二叉搜索树

// 思路
// - 要查找的值 和 树的root节点比较,大于在right中递归查找,小于在left中递归查找,等于直接返回
// - 递归结束的条件是
//    -  !root 不存在
//    -  或者root和要查找的值相等

// 示例
// 给定二叉搜索树:
//     4
//    / \
//   2   7
//  / \
// 1   3
// 和值: 2
// 应该返回如下子树:
//   2
//  / \
// 1   3
// 如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} val
 * @return {TreeNode}
 */
var searchBST = function (root, val) {
  function search(root, val) {
    // 递归结束的两个条件
    if (!root) return null;
    if (val === root.val) return root;

    return val > root.val
      ? search(root.right, val)
      : search(root.left, val);
  }

  return search(root, val);
};

image.png

image.png

(10) 二叉搜索树中两个节点之和

给定一个二叉搜索树的 **根节点** `root` 和一个整数 `k` , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 `k` 
假设二叉搜索树中节点的值均唯一。
---

输入: root = [8,6,10,5,7,9,11], k = 12
输出: true
解释: 节点 5 和节点 7 之和等于 12

---

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {boolean}
 */
var findTarget = function(root, k) {
    // 1. 先遍历
    const res = [] // 将树转成数组
    function traversal(root) {
        if (!root) return
        traversal(root.left)
        res.push(root.val)
        traversal(root.right)
    }
    traversal(root)

    // 2. 数组中寻在结果
    for(let i = 0; i < res.length; i++) {
        const dis = k - res[i];
       if (res.includes(dis) && dis !== res[i]) return true
    }
    return false
};

(11)二叉树的层序遍历

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if (!root) return []
    // queue -> 我们用队列 queue 去维护节点的存取
    // result -> 结果多维数组
    const queue = [root], result = []

    while(queue.length) {
        const temp = [] // 收集每一层的result,然后最终都push到result中去

        const queueLength = queue.length
        // queueLength
        // - 注意这里必须缓存queue.length
        // - 如果写成 for(let i = 0; i < queue.length; i++) 是不行的,因为 queue.length 在动态变化

        for(let i = 0; i < queueLength; i++) {
            const current = queue.shift() // 队列,先进先出,push入队,shift出队
            temp.push(current.val)

            if (current.left) queue.push(current.left);
            if (current.right)  queue.push(current.right);
        }

        temp.length && result.push(temp) // temp数组有成员时,就将temp数组添加到result中去
    }

    return result
};

image.png

image.png

(12) 二叉搜索树的第k大节点

// 题目
// - 二叉搜索树的第k大节点
// - 给定一棵二叉搜索树,请找出其中第 k 大的节点的值

// 示例
// ---
// 输入: root = [3,1,4,null,2], k = 1
//   3
//   / \
// 1   4
//   \
//   2
// 输出: 4
// ---
// 输入: root = [5,3,6,2,4,null,null,1], k = 3
//       5
//       / \
//     3   6
//     / \
//   2   4
//   /
// 1
// 输出: 4

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {number}
 */
var kthLargest = function (root, k) {
  // 先遍历
  const res = [];
  function traversal(root) {
    if (!root) return;
    res.push(root.val);
    traversal(root.left);
    traversal(root.right);
  }
  traversal(root);

  // 再排序,倒序
  res.sort((a, b) => b - a);

  // 再查找
  const result = res.find((val, index) => {
    return index === k - 1;
  });

  return result;
};

image.png

(13) 二叉树的层平均值

// 题目
// - 二叉树的层平均值
// - Average of Levels in Binary Tree
// - 给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。

// 思路
// - 和 BFS 遍历的大概思路相同,唯一不同的时收到到的层成员数组是平和的平均值

// 英语
// - average 平均值

// 示例
// 输入:
//     3
//   / \
//   9  20
//     /  \
//   15   7
// 输出:[3, 14.5, 11]
// 解释:
// 第 0 层的平均值是 3 ,  第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11]

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var averageOfLevels = function (root) {
  // 先 BSF,在bfs的过程中,搜集每一层的成员数组,然后求平均
  if (!root) return [];
  const queue = [root];
  const result = [];

  while (queue.length) {
    const temp = []; // 没层的节点组成的数组
    const len = queue.length;

    for (let i = 0; i < len; i++) {
      const current = queue.shift();
      temp.push(current.val);
      if (current.left) {
        queue.push(current.left);
      }
      if (current.right) {
        queue.push(current.right);
      }
    }

    // 下面是和 BFS 广度优先遍历不同的点
    if (temp.length) {
      const a = temp.reduce((a, b) => a + b) / temp.length; // 这里是否要保留小数,可以 number.toFixed(2) 保留两位有效数字
      result.push(a);
    }
  }

  return result;
};

image.png

(14) 相同的树

  • 对比对称树的判断,两者类似
// 题目
// - 相同的树
// - 给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同
// - 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的

// 对比
// 相同树 和 对称树 的判断类似

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function (p, q) {
  // 递归判断
  function traversal(p, q) {
    if (!p && !q) return true; // 保证此条件在最前面,表示如果直到不能遍历了还是相等的,必然相等
    if (!p || !q) return false; // 结构不一样,必然不相等
    if (p.val !== q.val) return false; // 每个节点值不一样,必然不相等

    return traversal(p.left, q.left) && traversal(p.right, q.right); // 保证左子树和右子树同时为true才为true
  }

  return traversal(p, q);
};

image.png

资料