leetcode算法学习-树

245 阅读13分钟

1.定义

树其实是链表的变种,树是一种非线性的数据结构,相对于线性的数据结构(链表、数组)而言,树的平均运行时间更短(往往与树相关的排序时间复杂度都不会高)

分类

二叉树: 树中节点的度不大于2的有序树。

满二叉树: 就是所有叶子结点都是满的,即用每个节点都有两个子节点。

完全二叉树:只要保证最后一个节点之前都是齐全就ok。

二叉查找树:当前节点的左边一定小于当前,右边一定大于当前。

二叉堆:是一种完全的二叉树,包含 最大堆 和 最小堆

  • 最大堆:任何一个父节点都大于等于左右两个字节点的值。

  • 最小堆:任何一个父节点都小于等于左右两个字节点的值。

二叉堆

最大堆的顶堆是整个堆中最大的元素。

最小堆的顶堆是整个堆中最大的元素。

操作

堆的自我调整:把不复合堆特性的完全二叉树调整为 一个堆。

插入

插入的位置从二叉树最后一个位置开始,如下图,在5下面插入0,然后发现0比5小,替换位置, 插入元素0

image.png

image.png

删除

先从最后一个元素替换堆顶元素,然后删除最后元素,堆顶元素 重现排序调整。 删除顶堆1 image.png

image.png

构建

就是把当前无序的完全二叉树调整为 二叉堆,让所有非叶子结点依次下沉。从倒数第二开始下沉。

image.png

image.png

image.png

代码实现

最大堆 递归方法

图一中 2 作为父节点小于子节点,很显然不符合最大堆性质。maxHeapify 函数可以把每个不符合最大堆性质的节点调换位置,从而满足最大堆性质的数组。

调整步骤:

  1. 调整分支节点 2 的位置(不满足最大堆性质)
  2. 获取父节点 2 的左右节点 ( 12 , 5 ) ,从 ( 2 , 15 , 5 ) 中进行比较
  3. 找出最大的节点与父节点进行交换,如果该节点本身为最大节点则停止操作
  4. 重复 step2 的操作,从 2 , 4 , 7 中找出最大值与 2 做交换(递归) image.png

堆的数组关系 核心逻辑:

左子节点: 2* parent +1

右子节点: 2* parent +2

同理

父节点 : (左子节点 - 1) /2

父节点 : (右子节点 - 2) /2

/**
 * 最大堆
 */

function left(i) {
  return (i * 2) + 1;
}

function right(i) {
  return (i * 2) + 2;
}

function swap(A, i, j) {
  const t = A[i];
  A[i] = A[j];
  A[j] = t;
}

class Heap {
  constructor(arr) {
    this.data = [...arr];
    this.size = this.data.length;
    this.rebuildHeap = this.rebuildHeap.bind(this);
    this.isHeap = this.isHeap.bind(this);
    this.sort = this.sort.bind(this);
    this.insert = this.insert.bind(this);
    this.delete = this.delete.bind(this);
    this.maxHeapify = this.maxHeapify.bind(this);
  }

//1.找到所有分支节点 Math.floor( N / 2 )(不包括叶子节点)
//2.将找到的子节点进行 maxHeapify 操作
  /**
   * 重构堆,形成最大堆
   */
  rebuildHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i--) {
      this.maxHeapify(i);
    }
  }


//  生成一个升序的数组
//1.swap 函数交换首尾位置
//2.将最后一个从堆中拿出相当于 size - 1
//3.执行 maxHeapify 函数进行根节点比较找出最大值进行交换
//4.最终 data 会变成一个升序的数组
  sort() {
    for (let i = this.size - 1; i > 0; i--) {
      swap(this.data, 0, i);
      this.size--;
      this.maxHeapify(0);
    }
  }


  isHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i--) {
      const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
      const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;

      const max = Math.max(this.data[i], l, r);

      if (max !== this.data[i]) {
        return false;
      }
      return true;
    }
  }
  
  //插入方法  做上浮操作
//Insert 函数作为插入节点函数,首先
//1.往 data 结尾插入节点
//2.因为节点追加,size + 1
//3.因为一个父节点拥有 2 个子节点,我们可以根据这个性质通过 isHeap 函数获取第一个叶子节点,可以通过第一个叶子节点获取新插入的节点,然后进行 3 个值的对比,找出最大值,判断插入的节点。如果跟父节点相同则不进行重构(相等满足二叉堆性质),否则进行 rebuildHeap 重构堆
  insert(key) {
    this.data[this.size++] = key;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  delete(index) {
    if (index >= this.size) {
      return;
    }
    this.data.splice(index, 1);
    this.size--;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  /**
   // 下沉
   * 交换父子节点位置,符合最大堆特征
   * @param {*} i
   */
  maxHeapify(i) {
    let max = i;

    if (i >= this.size) {
      return;
    }

    // 求左右节点中较大的序号
    const l = left(i);
    const r = right(i);
    if (l < this.size && this.data[l] > this.data[max]) {
      max = l;
    }

    if (r < this.size && this.data[r] > this.data[max]) {
      max = r;
    }

    // 如果当前节点最大,已经是最大堆
    if (max === i) {
      return;
    }

    swap(this.data, i, max);

    // 递归向下继续执行 //这里找到的最大值就是下一个要比较的值
    return this.maxHeapify(max);
  }
}

module.exports = Heap;


 //测试代码
const arr = [15, 12, 8, 2, 5, 2, 3, 4, 7];
const fun = new Heap(arr);
fun.rebuildHeap(); // 形成最大堆的结构
fun.sort();// 通过排序,生成一个升序的数组
console.log(fun.data) // [2, 2, 3, 4, 5, 7, 8, 12, 15] 

优先队列-二叉堆实现

优先队列:有别于队列,是哪个值最大就优先出列,可以直接是用二叉堆的插入和删除实现,实现入队和出队。

  • 入队: 从尾部插入然后上浮。
  • 出队:从头部删除,然后叶子节点替换再下沉。

结构

树是逻辑结构,依赖与“物理结构”的来实现。(链表与数组都属于物理结构)

树属于逻辑结构,可以使用多种物理结构,如:链式结构,数组。

链表

使用结构

node : {
    data:xxx
    left:node1,
    right:node2
}

数组

根据父节点找子节点:

左节点: 2* parent + 1

右节点: 2* parent + 2

根据子节点找父节点:

根据左节点找父: leftChild -1 /2

根据右节点找父:rightChild -1 /2

2.遍历

广度遍历:前,中,后,序遍历

  • 前序遍历:先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
  • 中序遍历:先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
  • 后序遍历:先访问左节点,然后访问右节点,最后访问根节点(左->右->根)

前,中,后, 都是按根节点的访问顺序确定。

深度遍历:层序遍历

一层一层横向遍历各个节点。

先遍历根节点,root插入数组。推出数组第一个root元素,同时插入root的左右两边节点。 继续循环,推出数组第一个元素,再查两边插入,一直循环。

image.png

//代码实现
function levelOrder(root) {
    let queue = [];
    queue.push(root) 
    while(queue.length > 0) {
        let item = queue.pop()
        console.log(item.data)
        if(item.left) {
            queue.push(item.left)
        }
        if(item.right) {
            queue.push(item.right)
        }
    }
}

3.leetcode刷题

100. 相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入: p = [1,2,3], q = [1,2,3]
输出: true

示例 2:

输入: p = [1,2], q = [1,null,2]
输出: false

答题:

/**
 * 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) {
    if(p == null && q == null) {
        return true
    }else 
    if(p == null || q == null) {
        return false
    }else 
    if(p.val != q.val) {
        return false
    }else {
        return  isSameTree(p.left,q.left) && isSameTree(p.right,q.right)
    }
   
};

226. 翻转二叉树

翻转一棵二叉树。 示例: 输入:

     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) {
    if(root == null) {
        return root
    }
    //这里递归 一直赋值,注意最后一个如:3下面是左右都是null ,会做两个null的对换
    root.left = invertTree(root.right)
    root.right = invertTree(root.left) 
    return root
};
 //优化代码
 var invertTree = function(root) {
    if(root == null) {
        return root
    }
    [root.left,root.right] = [invertTree(root.right),invertTree(root.left)]
    return root
};

144. 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序遍历。

示例 1:

输入: root = [1,null,2,3]
输出: [1,2,3]

示例 2:

输入: root = []
输出: []

答题:

/**
 * 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[]}
 */
 //递推 + 双 while + stack 
 var preorderTraversal = function(root,arr = []) {
    let res = []
    let stack = []
    let cur = root
    while (cur || stack.length > 0 ) {
        while (cur) {
            res.push(cur.val)
            stack.push(cur)
            cur = cur.left
        }
        cur = stack.pop()
        cur = cur.right
    }
    return res
}; 
 
 // 递归
 var preorderTraversal = function(root,arr = []) {
    if(root != null) {
        arr.push(root.val)
        preorderTraversal(root.left,arr)
        preorderTraversal(root.right,arr)
    }
    return arr
};

145. 二叉树的后序遍历

/**
 * 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[]}
 */ 
 // 后序 遍历 关系是  左  右 根  则我们只需要求出  根 右 左,再使用 reverse 反转
var postorderTraversal = function(root,arr = []) {
    let res = []
    let stack = []
    let cur = root
    while (cur || stack.length > 0 ) {
        while (cur) {
            res.push(cur.val)
            stack.push(cur)
            cur = cur.right
        }
        cur = stack.pop() 
        cur = cur.left
    }
    res.reverse()
    return res
};
 
 // 递归
var postorderTraversal = function(root,arr = []) {
    if(root != null) {
        postorderTraversal(root.left,arr)
        postorderTraversal(root.right,arr)
        arr.push(root.val)
    }
    return arr
};

94. 二叉树的中序遍历

/**
 * 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[]}
 */
 //while + stack
 var inorderTraversal = function(root) {
    let res = []
    let stack = []
    let cur = root
    while (cur || stack.length > 0 ) {
        while (cur) {
            stack.push(cur)
            cur = cur.left
        }
        cur = stack.pop()  
        res.push(cur.val) //这里就是所有节点加入的关键时间,包括左中右
        cur = cur.right
    }
    return res
};
 
 //递归 
    var inorderTraversal = function(root,arr = []) {
        if(root != null) { 
            inorderTraversal(root.left,arr)
            arr.push(root.val)
            inorderTraversal(root.right,arr)
        }
        return arr
    }; 

while+stack 迭代逻辑分析

image.png

image.png image.png

98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入: root = [2,1,3]
输出: true

示例 2:

输入: root = [5,1,4,null,null,3,6]
输出: false
解释: 根节点的值是 5 ,但是右子节点的值是 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 {boolean}
 */
 //中序遍历 + 迭代 + 实时判断
 var isValidBST = function(root) {
    let stack = [];
    let inorder = -Infinity;

    while (stack.length || root !== null) {
        while (root !== null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
        // 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
        if (root.val <= inorder) {
            return false;
        }
        inorder = root.val;
        root = root.right;
    }
    return true;
}; 

//中序遍历+ 递归 + 判断是否是从小到大排序
var isValidBST = function(root ,arr = []) {
    if(root != null) {
        isValidBST(root.left,arr)
        arr.push(root.val)
        isValidBST(root.right,arr)
    }
    //判断当前的顺序是否正确
    for (var i = 0 ; i < arr.length - 1; i ++) {
        if(arr[i] >= arr[i+1]){
            return false
        }
    }
    return true
};

//递归判断, 这里一直左右两边递归,直到子节点都为null,就为true,
//-Infinity ,和 Infinity处理固定最小值和最大值 左右两边的判断,
//
const helper = (root, lower, upper) => {
    if (root === null) {
        return true;
    }
    if (root.val <= lower || root.val >= upper) {
        return false;
    }
    //helper(root.left, lower, root.val) 判断左边节点 是否大于中节点,是则返回否,因为是至上而下,所以每次只需要处理
    //helper(root.left, lower, root.val) 判断右边节点 是否小于中节点,是则返回否
    return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
}
var isValidBST = function(root) {
    return helper(root, -Infinity, Infinity);
}; 

104. 二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 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 maxDepth = function(root) {
    if(!root) {
        return null
    }
    return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1
};

235. 二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

搜索树也叫:排序树,其实就是所有节点都是从左往右,从小到大排好的序。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6

示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

答题

利用排好的顺序,左边小,右边大的特点。是中心节点不断移动判断的逻辑。 判断中心点 都在 两个节点都在的左边,则中心点右移 判断中心点 都在 两个节点都在的右边,则中心点左移 直到不同时在左边,并且不同时在右边,证明找到,否则要么为空


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

/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
 //递推 迭代方式
 var lowestCommonAncestor = function(root, p, q) {
    while(root) {
        let prev = root
        if(root.val < p.val && root.val < q.val) {//中心点都两点的左边,进行右移
          root = root.right
        }else  if(root.val > p.val && root.val > q.val) {//中心点都两点的右边,进行左移
            root = root.left
        }
       else { //即不在两个节点左边,也不在两个节点右边 , 则找到了
           return root
       }
    }
    return null
};
 
 
 //递归
var lowestCommonAncestor = function(root, p, q) {
    if(!root) {
        return root
    }
    if(root.val < p.val && root.val < q.val) {
      return  lowestCommonAncestor(root.right,p,q)
    }else if(root.val > p.val && root.val > q.val) {
        return  lowestCommonAncestor(root.left,p,q)
    }else {
        return root
    }
};

236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

这次不是二叉搜索树,所有树是没有排序的。

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3

示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

问题分析

分四种情况

  1. root是null或者root等于p或q,说明root是p,q的公共祖先,
  2. 递归左右子树,如果左右子树递归函数返回的都不为空,则root就是p,q的公共祖先
  3. 左子树递归函数返回的值为空,则p,q都在右子树,
  4. 右子树递归函数返回的值为空,则p,q都在左子树

答题

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
 // 递归 判断左边没有就去右边找
var lowestCommonAncestor = function(root, p, q) {
    if(root == null || root == p || root == q) { // 确定递归终止条件
        return root
    }
    let left = lowestCommonAncestor(root.left,p,q) //递归单层 左边逻辑
    let right = lowestCommonAncestor(root.right,p,q)//递归单层 右边边逻辑
    if(left == null) { //如果左子树没找到就说明p,q都在右子树
        return right //这里是递归过的右边集合
    }
    if(right == null) {//如果右子树没找到就说明p,q都在左子树
        return left //这里是递归过的左边集合
    }
      //能执行到这里,证明left和right都不为空,如果在某一个节点的左右子树都能找到p和q说明这个节点就是公共祖先
    return root  
};

257. 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

示例 1:

输入: root = [1,2,3,null,5]
输出: ["1->2->5","1->3"]

示例 2:

输入: root = [1]
输出: ["1"]

答题

/**
 * @param {TreeNode} root
 * @return {string[]}
 */
var binaryTreePaths = function(root) {
    let arr = [] //全局返回数组
    let getNodeStr = (node,str) => {//该方法只深度递归,不返回信息
        if(!node) {
            return ""
        }
        str += node.val //每次都把上一次的内容,与当前累加
        if(node.left == null && node.right == null) {
            arr.push(str) //如果是叶子页面直接加入数组
        }else {
            let path = str + "->" //不是叶子,递归嵌套两个左右节点
            getNodeStr(node.left,path)
            getNodeStr(node.right,path)
        }
    }
    getNodeStr(root,"")
    return arr
}; 

617. 合并二叉树

给你两棵二叉树: root1 和 root2 。其实就是把对应所有节点相加

返回合并后的二叉树。

注意:  合并过程必须从两个树的根节点开始。

示例 1:

输入: root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出: [3,4,5,5,4,null,7]

示例 2:

输入: root1 = [1], root2 = [1,2]
输出: [2,2]

答题


/**
 * @param {TreeNode} root1
 * @param {TreeNode} root2
 * @return {TreeNode}
 */
 //递归
var mergeTrees = function(root1, root2) {
    if(!root1) return root2
    if(!root2) return root1
    root1.val = root1.val + root2.val
    root1.left = mergeTrees(root1.left,root2.left)
    root1.right = mergeTrees(root1.right,root2.right)
    return root1
};