前端算法入门之路(十六)(手撕红黑树)

142 阅读10分钟

红黑树

红黑树的平衡条件

  • 每个节点非黑即红
  • 根节点为黑色
  • 叶子节点(NIL)为黑色
  • 如果一个节点是红色的,那么它的两个子节点是黑色的
  • 从根节点到叶子节点的所有路径上,黑色节点的数量是相同的

性质

  • 最长路径最大是最短路径的2倍
  • 插入的节点是红色,如果插入黑色一定会引发失衡,插入红色节点只是可能会失衡

平衡调整法门

  • 插入调整要在祖父节点向下看
  • 删除调整要在父节点向下看
  • 调整之前路径上的黑色节点数量等于调整之后的数量

插入调整

  • 情况1
    从祖父节点向下看,新插入节点的父节点和叔叔节点都是红色,把父节点和叔叔节点改为黑色,祖父节点改为红色 image.png
  • 情况2
    从祖父节点向下看,左子树和左子树的左子树发生失衡,而右子树根节点是黑色的情况,这种也是LL型失衡
    先大右旋,此时上三层的节点颜色都是确定的,调整颜色有两种方式,将上两层的红色上浮或下沉

image.png
红色下沉 image.png 红色上浮 image.png

如果是LR型失衡,将15和19号节点进行小左旋并不影响路径平衡,这样就将LR型转化为上述的LL型失衡,RR型和RL型也是类似,只是反过来而已

image.png

删除调整

Q:删除什么样节点会引发失衡?(度指出度)

  • 由于度为2的节点可以转化为度为1的节点来处理,所以只需要考虑四种情况,度为1的红黑节点和度为0的红黑节点
  • 度为1的红色节点不存在,因为红色节点子树只能是黑色,度为1则不符合红黑树平衡条件
  • 度为0的红色节点可以直接删除,不影响平衡
  • 度为1的黑色节点唯一子孩子必定是度为0的红色节点,否则失衡,所以删除度为1的黑色节点只需要把子孩子挂在父节点上并改为黑色
  • 度为0的黑色节点删除会引发失衡,将删除的黑色节点位置补上NIL节点并染成双重黑色,并且触发平衡调整。删除的平衡调整,调整的就是双重黑的节点

平衡调整策略

  • 情况1
    站在父节点向下看,90号节点出现了双重黑,其兄弟节点9号是黑色并且子节点8号和20号也都是黑色
    将90号节点双重黑去掉一层,兄弟节点9号调整为红色,根节点43加上一重黑色,然后继续回溯向上调整

image.png

  • 情况2
    • 站在父节点38号向下看,28号节点出现了双重黑,其兄弟节点51号是黑色并且右子树72号是红色
      这种失衡称为RR类型失衡,所以将38号大左旋得到右图
    • 分析颜色确定情况,由于是RR型,所以28号和51号是确定的黑色,72号是确定的红色,64号和85号是确定的黑色
      • 若如图所示的树中,如果38号节点时红色的,但是由于48号节点的颜色不确定,所以38号节点只能调整为黑 色,这样左子树多了一个黑色节点,所以把根节点51号调整成为红色,右子树又少了一个黑色节点,因为72号是确定的红色,所以将72改为黑色就可以完成红黑树的平衡调整
      • 如果38号节点是黑色,那么旋转后为了保证每条路径黑色节点数量一致,需要把51号根节点改为黑色,72号节点改为黑色
    • 综上所述,RR类型平衡调整法门,先大左旋,原右子树(51)节点颜色改为根节点(38)的颜色,旋转过后的两个子树节点(38和72)改为黑色,双重黑28号去掉一层黑色

image.png

image.png

  • 情况3
    • 站在父节点38号向下看,28号节点出现了双重黑,其兄弟节点72号是黑色并且有且仅有左子树51号是红色
    • 这种失衡称为RL类型失衡,所以将72号小右旋得到右图
    • 分析颜色确定情况,由于是RL型,所以51号是确定的红色,72号、85号、48号和64号是确定的黑色,所以可以将72号和其左子树51号颜色对调完成小右旋和颜色调整,从而将问题转化为RR型,进行情况2操作
    • LL型和LR型也是类似,只是反过来而已

image.png

  • 情况4
    • 站在父节点20号向下看,10号节点出现了双重黑,其兄弟节点51号是红色
    • 分析颜色确定情况,51号是确定的红色,10号、20号、38号和72号是确定的黑色,所以可以将20号和其右子树51号颜色对调完成大左旋和颜色调整,从而将问题转化为以上几种双重黑的兄弟节点是黑色节点的情况进行操作

image.png

手撕红黑树

let NIL = {left: null, right: null, val: null, color: 1}
class Node {
    constructor(val) {
        this.val = val
        this.left = this.right = NIL
        this.color = 0
    }
}
// 插入操作
function __insert(root, val) {
    if (root == NIL) return new Node(val)
    if (val < root.val) root.left = __insert(root.left, val)
    else root.right = __insert(root.right, val)
    root = insert_maintain(root)
    return root
}
function insert(root, val) {
    root = __insert(root, val)
    root.color = 1
    return root
}
// 删除操作
function __delNode(root, val) {
    if (root == NIL) return root
    if (val < root.val) root.left = __delNode(root.left, val)
    else if (val > root.val) root.right = __delNode(root.right, val)
    else {
        if (root.left == NIL || root.right == NIL) {
            let temp = root.left == NIL ? root.right : root.left
            temp.color += root.color // 直接处理四种度为1的颜色情况
            delete root
            return temp
        } else {
            let temp = getPreNode(root)
            root.val = temp.val
            root.left = __delNode(root.left, temp.val)
        }
    }
    return del_maintain(root)
}
function delNode(root, val) {
    root = __delNode(root, val)
    root.color = 1
    return root
}
// 获取前驱节点
function getPreNode(root) {
    let temp = root.left
    while (temp.right != NIL) temp = temp.right
    return temp
}
// 插入调整
function insert_maintain(root) {
    let flag = 0
    if (root.left.color == 0 && has_red_child(root.left)) flag = 1
    if (root.right.color == 0 && has_red_child(root.right)) flag = 2
    if (flag == 0) return root
    // 情况1
    if (root.left.color == 0 && root.right == 0) {
        root.color = 0
        root.left.color = root.right.color = 1
        return root
    }
    // 情况2
    if (flag == 1) {
        if (root.left.right.color == 0) { // LR
            root.left = left_rotate(root.left)
        }
        root = right_rotate(root) // LL
    } else {
        if (root.right.left.color == 0) { // RL
            root.right = right_rotate(root.right)
        }
        root = left_rotate(root) // RR
    }
    root.color = 0
    root.left.color = root.right.color = 1
    return root
}
// 删除调整
function del_maintain(root) {
    if (root.left.color != 2 && root.right.color != 2) return root
    if ((root.left.color == 1 && !has_red_child(root.left)) || (root.right.color == 1 && !has_red_child(root.right))) { // 情况1
        root.left.color -= 1
        root.right.color -= 1
        root.color += 1
        return root
    }
    if (has_red_child(root)) { // 情况4
        if (root.right.color == 0) {
            root.color = 0
            root = left_rotate(root)
            root.color = 1
            root.left = del_maintain(root.left)
        } else {
            root.color = 0
            root = right_rotate(root)
            root.color = 1
            root.right = del_maintain(root.right)
        }
        return root
    }
    // 情况2和情况3
    if (root.right.color == 1) {
        root.left.color = 1
        if (root.right.right.color != 0) { // RL
            root.right.color = 0
            root.right = right_rotate(root.right)
            root.right.color = 1
        }
        // RR
        root = left_rotate(root)
        root.color = root.left.color
    } else {
        root.right.color = 1
        if (root.left.left.color != 0) { // LR
            root.left.color = 0
            root.left = left_rotate(root.left)
            root.left.color = 1
        }
        // LL
        root = right_rotate(root)
        root.color = root.right.color
    }
    root.left.color = root.right.color = 1
    return root
}
function has_red_child(root) {
    if (root == NIL) return false
    return root.left.color == 0 || root.right.color == 0
}
// 左旋
function left_rotate(root) {
    let temp = root.right
    root.right = temp.left
    temp.left = root
    return temp
}
// 右旋
function right_rotate(root) {
    let temp = root.left
    root.left = temp.right
    temp.right = root
    return temp
}

LeetCode肝题

    1. 分裂二叉树的最大乘积
// 和不变的情况下两个数越接近乘积越大,先求出各个节点之和sum,然后求出最靠近sum/2的值,最后返回乘积
var maxProduct = function(root) {
    let ans = 0, avg = 0, sum
    let getSum = (root) => {
        if (!root) return 0
        let val = 0
        val = root.val + getSum(root.left) + getSum(root.right)
        if (Math.abs(val - avg) < Math.abs(ans - avg)) ans = val
        return val
    }
    sum = getSum(root)
    ans = sum
    avg = sum / 2
    getSum(root)
    return ans * (sum - ans) % 1000000007
};
    1. 基于时间的键值存储
// 用key对照一个obj,obj的key是时间戳,value是入参value,如果查询不到入参的时间戳,继续向前查询
var TimeMap = function() {
    this.data = {}
};
TimeMap.prototype.set = function(key, value, timestamp) {
    let map = this.data[key] || {}
    map[timestamp] = value
    this.data[key] = map
};
TimeMap.prototype.get = function(key, timestamp) {
    let map = this.data[key]
    if (!map) return ''
    while (!map[timestamp] && timestamp >= 0) timestamp--
    if (map[timestamp]) return this.data[key][timestamp]
    return ''
};
    1. 翻转二叉树以匹配先序遍历
// 将root树前序遍历,遍历的过程中和voyage数组中的值对比,因为根节点下一个肯定是左子树的值
// 如果不相同则交换左右子树继续向左右子树递归查找对比
var flipMatchVoyage = function(root, voyage) {
    let ans = [], ind = 0
    let dfs = (root) => {
        if (!root) return true
        if (root.val != voyage[ind]) {
            ans = [-1]
            return false
        }
        ind++
        if (ind + 1 == voyage.length) return true
        if (root.left && (root.left.val != voyage[ind])) {
            [root.left, root.right] = [root.right, root.left]
            ans.push(root.val)
        }
        if (!dfs(root.left)) return false
        if (!dfs(root.right)) return false
        return true
    }
    dfs(root)
    return ans
};
    1. 填充每个节点的下一个右侧节点指针 II
// 每一层连接下一层的所有节点,p指向当前层,pre指向下一层的节点,new_head指向下一层的第一个节点
// 每次连接完返回下一层的第一个节点,如果为空说明连接完成
var connect = function(root) {
    let p = root
    let connectNode = (head) => {
        let p = head,  pre, new_head
        while (p) {
            if (p.left) {
                if (pre) pre.next = p.left
                pre = p.left
            }
            if (!new_head) new_head = pre
            if (p.right) {
                if (pre) pre.next = p.right
                pre = p.right
            }
            if (!new_head) new_head = pre
            p = p.next
        }
        return new_head
    }
    while (p) p = connectNode(p)
    return root
};
  1. 剑指 Offer II 053. 二叉搜索树中的中序后继
// 中序遍历的时候记录一下节点值等于p的节点,到下一个的时候赋值给ans即可
var inorderSuccessor = function(root, p) {
    let flag = false, ans
    let getNextNode = (root, val) => {
        if (!root) return null
        getNextNode(root.left, val)
        if (flag) {
            ans = root
        }
        if (root.val == val) flag = true
        else flag = false
        getNextNode(root.right, val)
    }
    getNextNode(root, p.val)
    return ans
};
    1. 序列化和反序列化二叉搜索树
// 也可以转化为数组再与字符串互转
var serialize = function(root) {
    return JSON.stringify(root)
};
var deserialize = function(data) {
    return JSON.parse(data)
};
    1. 子集
// 深搜利用回溯,这种子集不像全排列需要顺序不同的排列,所以每次递归下标加1也不需要对自己判重
var subsets = function(nums) {
    let ans = [], buff = []
    let dfs = (index) => {
        ans.push(buff.slice())
        for(let i = index; i < nums.length; i++) {
            buff.push(nums[i])
            dfs(i + 1)
            buff.pop()
        }
    }
    dfs(0)
    return ans
};

7.1 90. 子集 II

// 子集2题目中有相同元素,同全排列2一样,所有重复元素永远从第一个开始选
var subsetsWithDup = function(nums) {
    nums = nums.sort((a, b) => a - b)
    let ans = [], buff = [], vis = []
    let dfs = (index) => {
        ans.push(buff.slice())
        for(let i = index; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1]) continue
            buff.push(nums[i])
            vis[i] = 1
            dfs(i + 1)
            buff.pop()
            vis[i] = 0
        }
    }
    dfs(0)
    return ans
};
    1. 全排列 II
// 先给数组排序,方便处理重复元素,使用vis记录已经选择过的元素
// 深搜,每选择一个元素再到数组里挨个匹配,如果当前元素被选择继续下一个循环
// 如果当前元素和前一个元素相等并且前一个元素没有被选择,那么当前元素也不选继续下一个循环
// 也就是说,遇到相同的元素永远从第一个开始选,可以去掉所有重复元素带来的排列序列
// 如果每个序列长度和nums一样说明有一条路径排列出来了,放入ans数组中
var permuteUnique = function(nums) {
    nums = nums.sort((a, b) => a - b)
    let ans = [], buff = [], vis = []
    let dfs = () => {
        if (buff.length == nums.length) {
            ans.push(buff.slice())
            return
        }
        for (let i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1]) continue
            if (vis[i]) continue
            vis[i] = 1
            buff.push(nums[i])
            dfs()
            buff.pop()
            vis[i] = 0
        }
    }
    dfs()
    return ans
};
    1. 存在重复元素 III
// 设置一个map,存入最多k对元素
// 遍历nums,将每一位的值除以t+1并向下取整得到对应的id,如果map里有相同的id,那么这两个数的差值一定是小于等于t的
// 同时如果map里存在id-1或者id+1的情况,也是有可能差值小于等于t的,需要判断一下
// 最后把id与num[i]的映射存入map中
var containsNearbyAlmostDuplicate = function(nums, k, t) {
    let map = new Map(), getId = (x) => {
        return Math.floor(x / (t + 1))
    }
    for(let i = 0; i < nums.length; i++) {
        let id = getId(nums[i])
        if (map.has(id)) return true
        else if (map.has(id - 1) && Math.abs(map.get(id - 1) - nums[i]) <= t) return true
        else if (map.has(id + 1) && Math.abs(map.get(id + 1) - nums[i]) <= t) return true
        map.set(id, nums[i])
        if (i >= k) map.delete(getId(nums[i - k]))
    }
    return false
};
    1. 缺失的第一个正数
// 排个序,判个重
var firstMissingPositive = function(nums) {
    nums = (nums.sort((a, b) => a - b))
    let n = 1
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] < 1) continue
        if (i > 0 && nums[i] == nums[i - 1]) continue
        if (nums[i] == n) {
            n++
            continue
        }
        break
    }
    return n
};
    1. 恢复二叉搜索树
// 找到两个破坏单调性的位置p、q,用pre记录上一次遍历的节点,中序遍历过程中数据是递增的
// 两个错误位置一个值较大另一个较小,较大的值在前面会大于它后面的节点,较小值在后面会小于它前面的节点
// 第一次找到pre大于root时,给p赋值pre,给q赋值root,如果找到第二个pre大于root节点则只更新q的值
// 最后交换p、q的val值
var recoverTree = function(root) {
    let pre, p, q, in_order = (root) => {
        if (!root) return
        in_order(root.left)
        if (pre && pre.val > root.val) {
            if (!p) p = pre
            q = root
        }
        pre = root
        in_order(root.right)
    }
    in_order(root);
    [p.val, q.val] = [q.val, p.val]
};
    1. 两数之和 IV - 输入 BST
// 由于不是连续的两个数之和,所以在中序遍历的过程中,每选定一个值,都要从根节点重新搜
// in_order:中序遍历+能否在树中找到当前节点和另外一个节点和为k的两个节点
// dfs:从给定的树中找k
var findTarget = function(root, k) {
    let head = root
    var in_order = (root) => {
        if (!root) return false
        if (in_order(root.left)) return true
        if (k == root.val * 2) return false
        if (dfs(head, k - root.val)) return true
        if (in_order(root.right)) return true
        return false
    }
    var dfs = (root, k) => {
        if (!root) return false
        if (root.val == k) return true
        else if (root.val < k) return dfs(root.right, k)
        else return dfs(root.left, k)
    }
    return in_order(root)
};
// 用vis标记root.val和k的差值,遍历的过程中如果能遇到该值则说明能找到
var findTarget = function(root, k, vis = {}) {
    if (!root) return false
    if (vis[root.val]) return true
    vis[k - root.val] = 1
    return findTarget(root.left, k, vis) || findTarget(root.right, k, vis)
};
    1. 计数质数
// 从2开始将所有质数的倍数标记上
var countPrimes = function(n) {
    let cut = 0, primes = new Array(n + 1).fill(0)
    for (let i = 2; i * i < n; i++) {
        if (primes[i]) continue
        for(let j = 2 * i; j < n; j += i) {
            primes[j] = 1
        }
    }
    for (let i = 2; i < n; i++) cut += primes[i] == 0 ? 1 : 0
    return cut
};
    1. 七进制数
// 每一次除以进位制取到的余数就是个位、十位、百位···上的数字,利用数组存起来最后反转并转化为字符串连接起来
var convertToBase7 = function(num) {
    if (num == 0) return '0'
    let str = num < 0 ? '-' : '', buff = []
    num = Math.abs(num)
    while(num) {
        buff.push(num % 7)
        num = Math.floor(num / 7)
    }
    str += buff.reverse().join('')
    return str
};
    1. 汉明距离
// ^异或操作可以将不同的位置标记为1,x&(x-1)每次可以去掉x的最后一位1,统计次数就是不同的数量
var hammingDistance = function(x, y) {
    x ^= y
    let cut = 0
    while(x) {
        x &= (x - 1)
        cut++
    }
    return cut
};
    1. 按权重随机选择
// 利用前缀和可以区分对应的权重,生成前缀和数组最后一位以内的随机数,然后在前缀和数组查找第一个大于等于它的位置
var Solution = function(w) {
    this.sum = [w[0]]
    for(let i = 1; i < w.length; i++) this.sum[i] = this.sum[i - 1] + w[i]
};
Solution.prototype.pickIndex = function() {
    let n = Math.random() * this.sum[this.sum.length - 1]
    let head = 0, tail = this.sum.length - 1
    while (head < tail) {
        let mid = (head + tail) >> 1
        if (n <= this.sum[mid]) tail = mid
        else head = mid + 1
    }
    return head
};