IT虾米网

【算法】树

sanshao 2022年05月13日 编程语言 163 0

最需要重视的是二叉树。

二叉树的深度优先搜索

3中遍历方式:

function dfs(root) { 
    if(!root) return 
 
    // 前序遍历 
    dfs(root.left) 
    // 中序遍历 
    dfs(root.right) 
    // 后序遍历 
}

中序遍历的定义,如果输入的二叉树的根节点不为空,则先遍历它的左子树,然后遍历根节点,最后遍历右子树。

不管是哪种深度优先搜索算法,也不管是递归代码还是迭代代码,如果二叉树有n个节点,那么它们的时间复杂都是O(n)。如果二叉树的深度为h,那么它们的空间复杂度都是O(h)。在二叉树中,二叉树的深度h的最小值是log2(n+1),最大值为n。例如,包含7个节点的二叉树,最少只有3层(二叉树的第1层有1个节点,第2层有2个节点,第3层有4个节点),但最多可能有7层(二叉树中除了叶节点,其他每个节点只有1个子节点)。

面试题47:二叉树剪枝

题目:一棵二叉树的所有节点的值要么是0要么是1,请剪除该二叉树中所有节点的值全都是0的子树。
 
var pruneTree = function(root) { 
    if(root == null) return null; 
    root.left = pruneTree(root.left) 
    root.right = pruneTree(root.right) 
 
    if(root.left == null && root.right == null && root.val == 0) { 
        return null 
    } 
    return root 
}; 
/** 
 * @param {TreeNode} root 
 * @return {TreeNode} 
 */ 
var pruneTree = function(root) { 
    const res = dfs(root) 
    if(res == 0) return null; 
    return root 
}; 
 
var dfs = function(root) { 
    if(!root) return 0 
    let left = dfs(root.left) 
    let right = dfs(root.right) 
    const res = root.val + dfs(root.left) + dfs(root.right) 
    if(left == 0) { 
        root.left = null 
    } 
    if(right == 0) { 
        root.right = null 
    } 
    return root.val + left + right; 
} 
 
var pruneTree = function(root) { 
    if(root == null) return null; 
    root.left = pruneTree(root.left) 
    root.right = pruneTree(root.right) 
 
    if(root.left == null && root.right == null && root.val == 0) { 
        return null 
    } 
    return root 
}; 

上述代码实质上是实现了递归的后序遍历。每当遍历到一个节点,如果该节点符合条件,则将该节点删除。由于是后序遍历,因此先对根节点root的左右子树递归调用函数pruneTree删除左右子树中节点值全是0的子树。只有当root的左右子树全部为空,并且它自己的值也是0时,这个节点才能被删除。所谓删除一个节点,就是返回null给它的父节点,这样这个节点就从这棵二叉树中消失。

  1. 序列化和反序列化二叉树
题目:请设计一个算法将二叉树序列化成一个字符串,并能将该字符串反序列化出原来二叉树的算法。
  1. 从根节点到叶节点的路径数字之和
 
/** 
 * Definition for a binary tree node. 
 * function TreeNode(val) { 
 *     this.val = val; 
 *     this.left = this.right = null; 
 * } 
 */ 
 
/** 
 * Encodes a tree to a single string. 
 * 
 * @param {TreeNode} root 
 * @return {string} 
 */ 
var serialize = function(root) { 
    return rserialize(root, ''); 
}; 
 
var rserialize = function(root, str) { 
 
    if(root == null) { 
        str += "None," 
    } else { 
        str += root.val + '' + ","; 
        str = rserialize(root.left, str) 
        str = rserialize(root.right, str) 
    } 
    return str 
} 
 
/** 
 * Decodes your encoded data to tree. 
 * 
 * @param {string} data 
 * @return {TreeNode} 
 */ 
var deserialize = function(data) { 
    const dataArray = data.split(","); 
    return rdeserialize(dataArray); 
}; 
 
 
var rdeserialize = (dataList) => { 
    if(dataList[0] === "None") { 
        dataList.shift(); 
        return null; 
    } 
    const root = new TreeNode(parseInt(dataList[0])); 
    dataList.shift(); 
    root.left = rdeserialize(dataList) 
    root.right = rdeserialize(dataList) 
    return root; 
}
  1. 从根节点到叶节点的路径数字之和
var sumNumbers = function(root) { 
    return dfs(root, 0) 
}; 
 
var dfs = function(root, path) { 
    if(root == null) return 0; 
    path = path * 10 + root.val 
    if(root.left == null && root.right == null) { 
        return path 
    } 
    return dfs(root.left, path) + dfs(root.right, path) 
 
}
  1. 向下的路径节点值之和
题目:给定一棵二叉树和一个值sum,求二叉树中节点值之和等于sum的路径的数目。路径的定义为二叉树中顺着指向子节点的指针向下移动所经过的节点,但不一定从根节点开始,也不一定到叶节点结束。例如,在如图8.5所示中的二叉树中有两条路径的节点值之和等于8,其中,第1条路径从节点5开始经过节点2到达节点1,第2条路径从节点2开始到节点6。
/** 
 * @param {TreeNode} root 
 * @param {number} targetSum 
 * @return {number} 
 */ 
var pathSum = function(root, targetSum) { 
    if (root == null) { 
        return 0; 
    } 
     
    let ret = rootSum(root, targetSum); 
    ret += pathSum(root.left, targetSum); 
    ret += pathSum(root.right, targetSum); 
    return ret; 
}; 
 
const rootSum = (root, targetSum) => { 
    let ret = 0; 
 
    if (root == null) { 
        return 0; 
    } 
    const val = root.val; 
    if (val === targetSum) { 
        ret++; 
    }  
 
    ret += rootSum(root.left, targetSum - val); 
    ret += rootSum(root.right, targetSum - val); 
    return ret; 
}
  1. 节点值之和最大的路径
题目:在二叉树中将路径定义为顺着节点之间的连接从任意一个节点开始到达任意一个节点所经过的所有节点。路径中至少包含一个节点,不一定经过二叉树的根节点,也不一定经过叶节点。给定非空的一棵二叉树,请求出二叉树所有路径上节点值之和的最大值。例如,在如图8.6所示的二叉树中,从节点15开始经过节点20到达节点7的路径的节点值之和为42,是节点值之和最大的路径。
 
var maxPathSum = function(root) { 
    let maxSum = [-Infinity] 
    dfs(root, maxSum) 
    return maxSum[0] 
}; 
 
 
var dfs = function(root, maxSum) { 
    if(root == null) return 0 
    let maxSumLeft = [-Infinity] 
    let maxSumRight = [-Infinity] 
    let left = Math.max(0, dfs(root.left, maxSumLeft)) 
    let right = Math.max(0, dfs(root.right, maxSumRight)) 
 
    maxSum[0] = Math.max(maxSumLeft[0], maxSumRight[0]); 
    maxSum[0] = Math.max(maxSum[0], root.val + left + right) 
 
    return root.val + Math.max(left, right) 
}  

二叉搜索树

二叉搜索树是一类特殊的二叉树,它的左子节点总是小于或等于根节点,而右子节点总是大于或等于根节点。

image-20220405154140539

二叉树的3种不同的深度优先搜索算法都适用于二叉搜索树,但中序遍历是解决二叉搜索树相关面试题最常用的思路,这是因为中序遍历按照节点值递增的顺序遍历二叉搜索树的每个节点

var searchBST = function(root, val) { 
    let cur = root 
    while(cur != null) { 
        if(cur.val == val) { 
            break; 
        } 
        if(cur.val < val) { 
            cur = cur.right; 
        } else { 
            cur = cur.left; 
        } 
    } 
    return cur 
}
  1. 展平二叉搜索树
题目:给定一棵二叉搜索树,请调整节点的指针使每个节点都没有左子节点。调整之后的树看起来像一个链表,但仍然是二叉搜索树。例如,把图8.8(a)中的二叉搜索树按照这个规则展平之后的结果如图8.8(b)所示。
/** 
 * @param {TreeNode} root 
 * @return {TreeNode} 
 */ 
var increasingBST = function(root) { 
    let res = [] 
    let cur = root 
    let prev = null 
    let first = null 
    while(cur != null || res.length) { 
        while(cur != null) { 
            res.push(cur) 
            cur = cur.left 
        } 
        cur = res.pop() 
        if(prev == null) { 
            first = cur 
        } else { 
            prev.right = cur 
        } 
 
        prev = cur 
        cur.left = null 
        cur = cur.right 
    } 
    return first 
};
  1. 二叉搜索树的下一个节点
题目:给定一棵二叉搜索树和它的一个节点p,请找出按中序遍历的顺序该节点p的下一个节点。假设二叉搜索树中节点的值都是唯一的。例如,在图8.9的二叉搜索树中,节点8的下一个节点是节点9,节点11的下一个节点是null。
/** 
 * @param {TreeNode} root 
 * @param {TreeNode} p 
 * @return {TreeNode} 
 */ 
var inorderSuccessor = function(root, p) { 
    let stack = [] 
    let cur = root  
    let found = false 
    while(cur != null || stack.length) { 
        while(cur !== null) { 
            stack.push(cur) 
            cur = cur.left 
        } 
        cur = stack.pop() 
        if(found) { 
            break; 
        } else if(p.val == cur.val) { 
            found = true 
        } 
        cur = cur.right 
    } 
    return cur 
};
  1. 所有大于或等于节点的值之和

通常,二叉搜索树的中序遍历按照节点的值从小到大按顺序遍历,也就是当遍历到某个节点时比该节点的值小的节点都已经遍历过,因此也就知道了所有比该节点的值小的所有节点的值之和sum。可是题目要求把每个节点的值替换成大于或等于该节点的值的所有节点的值之和。因此,可以先遍历一遍二叉树求出所有节点的值之和total,再用total减去sum即可。

上面的思路需要遍历二叉搜索树两次,第1次不管用什么算法只要遍历所有节点即可,第2次则必须采用中序遍历。是否可以只遍历二叉搜索树一次呢?

如果能够按照节点值从大到小按顺序遍历二叉搜索树,那么只需要遍历一次就够了,因为遍历到一个节点之前值大于该节点的值的所有节点已经遍历过。通常的中序遍历是先遍历左子树,再遍历根节点,最后遍历右子树,由于左子树节点的值较小,右子树节点的值较大,因此总体上就是按照节点的值从小到大遍历的。如果要按照节点的值从大到小遍历,那么只需要改变中序遍历的顺序,先遍历右子树,再遍历根节点,最后遍历左子树,这样遍历的顺序就颠倒过来了。

/** 
 * @param {TreeNode} root 
 * @return {TreeNode} 
 */ 
var convertBST = function (root) { 
    let stack = []; 
    let sum = 0; 
    let cur = root; 
    while (cur != null || stack.length) { 
        while(cur != null) { 
            stack.push(cur) 
            cur = cur.right; 
        } 
        cur = stack.pop(); 
        sum += cur.val; 
        cur.val = sum 
        cur = cur.left; 
    } 
    return root; 
}
  1. 二叉搜索树迭代器
实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:

BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。
int next()将指针向右移动,然后返回指针处的数字。

 
/** 
 * @param {TreeNode} root 
 */ 
var BSTIterator = function(root) { 
    this.idx = 0; 
    this.arr = []; 
    this.inorderTraversal(root, this.arr); 
}; 
 
/** 
 * @return {number} 
 */ 
BSTIterator.prototype.next = function() { 
    return this.arr[this.idx++]; 
}; 
 
/** 
 * @return {boolean} 
 */ 
BSTIterator.prototype.hasNext = function() { 
    return this.idx < this.arr.length; 
}; 
 
BSTIterator.prototype.inorderTraversal = function(root, arr) { 
    if (!root) { 
        return; 
    } 
    this.inorderTraversal(root.left, arr); 
    arr.push(root.val); 
    this.inorderTraversal(root.right, arr); 
};
  1. 二叉搜索树中两个节点的值之和
给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。假设二叉搜索树中节点的值均唯一。
/** 
 * @param {TreeNode} root 
 * @param {number} k 
 * @return {boolean} 
 */ 
var findTarget = function(root, k) { 
    const set = new Set(); 
    const helper = (root, k) => { 
        if (!root) { 
            return false; 
        } 
        if (set.has(k - root.val)) { 
            return true; 
        } 
        set.add(root.val); 
        return helper(root.left, k) || helper(root.right, k); 
    } 
    return helper(root, k); 
};

TreeSet和TreeMap的应用

平衡二叉树(AVL)

查找、插入和删除在平均和最坏情况下的时间复杂度都是log{n}

  1. 值和下标差都在给定的范围内
给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。
  • 桶排序
var containsNearbyAlmostDuplicate = function(nums, k, t) { 
    const n = nums.length; 
    const mp = new Map(); 
 
    for (let i = 0; i < n; ++i) { 
        const x = nums[i]; 
        const id = getID(x, t + 1); 
        if (mp.has(id)) { 
            return true; 
        } 
        if (mp.has(id - 1) && Math.abs(x - mp.get(id - 1)) <= t) { 
            return true; 
        } 
        if (mp.has(id + 1) && Math.abs(x - mp.get(id + 1)) <= t) { 
            return true; 
        } 
        mp.set(id, x); 
        if (i >= k) { 
            mp.delete(getID(nums[i - k], t + 1)); 
        } 
    } 
    return false;         
 
} 
 
const getID = (x, w) => { 
    return x < 0 ? Math.floor((x + 1) / w) - 1 : Math.floor(x / w); 
} 
  1. 日程表

总结

要对前序遍历,中序遍历,后序遍历三种遍历的循环和递归代码烂熟于心。

中序遍历很适合二叉搜索树遍历,(从小到大)

参考文章


评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

从浅入深了解Koa2源码