二叉搜索树中第K小的元素[中等]

优质博文:IT-BLOG-CN

一、题目

给定一个二叉搜索树的根节点root,和一个整数k,请你设计一个算法查找其中第k个最小元素(从1开始计数)。

示例 1:

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

示例 2:

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

树中的节点数为n
1 <= k <= n <= 104
0 <= Node.val <= 104

进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

二、代码

【1】中序遍历: 二叉搜索树具有如下性质:
  ● 结点的左子树只包含小于当前结点的数。
  ● 结点的右子树只包含大于当前结点的数。
  ● 所有左子树和右子树自身必须也是二叉搜索树。

二叉树的中序遍历即按照访问左子树——根结点——右子树的方式遍历二叉树;在访问其左子树和右子树时,我们也按照同样的方式遍历;直到遍历完整棵树。

思路和算法: 因为二叉搜索树和中序遍历的性质,所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第k个最小元素。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            --k;
            if (k == 0) {
                break;
            }
            root = root.right;
        }
        return root.val;
    }
}

时间复杂度: 时间复杂度:O(H+k),其中H是树的高度。在开始遍历之前,我们需要O(H)到达叶结点。当树是平衡树时,时间复杂度取得最小值O(log⁡N+k);当树是线性树(树中每个结点都只有一个子结点或没有子结点)时,时间复杂度取得最大值O(N+k)
空间复杂度: O(H),栈中最多需要存储H个元素。当树是平衡树时,空间复杂度取得最小值O(log⁡N);当树是线性树时,空间复杂度取得最大值O(N)

【2】记录子树的结点数: 如果你需要频繁地查找第k小的值,你将如何优化算法?

思路和算法: 在方法一中,我们之所以需要中序遍历前k个元素,是因为我们不知道子树的结点数量,不得不通过遍历子树的方式来获知。因此,我们可以记录下以每个结点为根结点的子树的结点数,并在查找第k小的值时,使用如下方法搜索:
  ● 令node等于根结点,开始搜索。
  ● 对当前结点node进行如下操作:
    ○ 如果node的左子树的结点数left小于k−1,则第k小的元素一定在node的右子树中,令node等于其的右子结点,k等于k−left−1,并继续搜索;
    ○ 如果node的左子树的结点数left等于k−1,则第k小的元素即为node,结束搜索并返回node即可;
    ○ 如果node的左子树的结点数left大于k−1,则第k小的元素一定在node的左子树中,令node等于其左子结点,并继续搜索。

在实现中,我们既可以将以每个结点为根结点的子树的结点数存储在结点中,也可以将其记录在哈希表中。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        MyBst bst = new MyBst(root);
        return bst.kthSmallest(k);
    }
}

class MyBst {
    TreeNode root;
    Map<TreeNode, Integer> nodeNum;

    public MyBst(TreeNode root) {
        this.root = root;
        this.nodeNum = new HashMap<TreeNode, Integer>();
        countNodeNum(root);
    }

    // 返回二叉搜索树中第k小的元素
    public int kthSmallest(int k) {
        TreeNode node = root;
        while (node != null) {
            int left = getNodeNum(node.left);
            if (left < k - 1) {
                node = node.right;
                k -= left + 1;
            } else if (left == k - 1) {
                break;
            } else {
                node = node.left;
            }
        }
        return node.val;
    }

    // 统计以node为根结点的子树的结点数
    private int countNodeNum(TreeNode node) {
        if (node == null) {
            return 0;
        }
        nodeNum.put(node, 1 + countNodeNum(node.left) + countNodeNum(node.right));
        return nodeNum.get(node);
    }

    // 获取以node为根结点的子树的结点数
    private int getNodeNum(TreeNode node) {
        return nodeNum.getOrDefault(node, 0);
    }
}

时间复杂度: 预处理的时间复杂度为O(N),其中N是树中结点的总数;我们需要遍历树中所有结点来统计以每个结点为根结点的子树的结点数。搜索的时间复杂度为O(H),其中H是树的高度;当树是平衡树时,时间复杂度取得最小值O(log⁡N);当树是线性树时,时间复杂度取得最大值O(N)
空间复杂度: O(N),用于存储以每个结点为根结点的子树的结点数。

【3】平衡二叉搜索树: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第k小的值,你将如何优化算法?

方法三需要先掌握 平衡二叉搜索树(AVL树) 的知识。平衡二叉搜索树具有如下性质:
  ● 平衡二叉搜索树中每个结点的左子树和右子树的高度最多相差1
  ● 平衡二叉搜索树的子树也是平衡二叉搜索树;
  ● 一棵存有 nnn 个结点的平衡二叉搜索树的高度是O(log⁡n)

思路和算法: 我们注意到在方法二中搜索二叉搜索树的时间复杂度为O(H),其中H是树的高度;当树是平衡树时,时间复杂度取得最小值O(log⁡N)。因此,我们在记录子树的结点数的基础上,将二叉搜索树转换为平衡二叉搜索树,并在插入和删除操作中维护它的平衡状态。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        // 中序遍历生成数值列表
        List<Integer> inorderList = new ArrayList<Integer>();
        inorder(root, inorderList);

        // 构造平衡二叉搜索树
        AVL avl = new AVL(inorderList);

        // 模拟1000次插入和删除操作
        int[] randomNums = new int[1000];
        Random random = new Random();
        for (int i = 0; i < 1000; ++i) {
            randomNums[i] = random.nextInt(10001);
            avl.insert(randomNums[i]);
        }
        shuffle(randomNums); // 列表乱序
        for (int i = 0; i < 1000; ++i) {
            avl.delete(randomNums[i]);
        }

        return avl.kthSmallest(k);
    }

    private void inorder(TreeNode node, List<Integer> inorderList) {
        if (node.left != null) {
            inorder(node.left, inorderList);
        }
        inorderList.add(node.val);
        if (node.right != null) {
            inorder(node.right, inorderList);
        }
    }

    private void shuffle(int[] arr) {
        Random random = new Random();
        int length = arr.length;
        for (int i = 0; i < length; i++) {
            int randIndex = random.nextInt(length);
            int temp = arr[i];
            arr[i] = arr[randIndex];
            arr[randIndex] = temp;
        }
    }
}

// 平衡二叉搜索树(AVL树):允许重复值
class AVL {
    Node root;

    // 平衡二叉搜索树结点
    class Node {
        int val;
        Node parent;
        Node left;
        Node right;
        int size;
        int height;

        public Node(int val) {
            this(val, null);
        }

        public Node(int val, Node parent) {
            this(val, parent, null, null);
        }

        public Node(int val, Node parent, Node left, Node right) {
            this.val = val;
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.height = 0; // 结点高度:以node为根节点的子树的高度(高度定义:叶结点的高度是0)
            this.size = 1; // 结点元素数:以node为根节点的子树的节点总数
        }
    }

    public AVL(List<Integer> vals) {
        if (vals != null) {
            this.root = build(vals, 0, vals.size() - 1, null);
        }
    }

    // 根据vals[l:r]构造平衡二叉搜索树 -> 返回根结点
    private Node build(List<Integer> vals, int l, int r, Node parent) {
        int m = (l + r) >> 1;
        Node node = new Node(vals.get(m), parent);
        if (l <= m - 1) {
            node.left = build(vals, l, m - 1, node);
        }
        if (m + 1 <= r) {
            node.right = build(vals, m + 1, r, node);
        }
        recompute(node);
        return node;
    }

    // 返回二叉搜索树中第k小的元素
    public int kthSmallest(int k) {
        Node node = root;
        while (node != null) {
            int left = getSize(node.left);
            if (left < k - 1) {
                node = node.right;
                k -= left + 1;
            } else if (left == k - 1) {
                break;
            } else {
                node = node.left;
            }
        }
        return node.val;
    }

    public void insert(int v) {
        if (root == null) {
            root = new Node(v);
        } else {
            // 计算新结点的添加位置
            Node node = subtreeSearch(root, v);
            boolean isAddLeft = v <= node.val; // 是否将新结点添加到node的左子结点
            if (node.val == v) { // 如果值为v的结点已存在
                if (node.left != null) { // 值为v的结点存在左子结点,则添加到其左子树的最右侧
                    node = subtreeLast(node.left);
                    isAddLeft = false;
                } else { // 值为v的结点不存在左子结点,则添加到其左子结点
                    isAddLeft = true;
                }
            }

            // 添加新结点
            Node leaf = new Node(v, node);
            if (isAddLeft) {
                node.left = leaf;
            } else {
                node.right = leaf;
            }

            rebalance(leaf);
        }
    }

    // 删除值为v的结点 -> 返回是否成功删除结点
    public boolean delete(int v) {
        if (root == null) {
            return false;
        }

        Node node = subtreeSearch(root, v);
        if (node.val != v) { // 没有找到需要删除的结点
            return false;
        }

        // 处理当前结点既有左子树也有右子树的情况
        // 若左子树比右子树高度低,则将当前结点替换为右子树最左侧的结点,并移除右子树最左侧的结点
        // 若右子树比左子树高度低,则将当前结点替换为左子树最右侧的结点,并移除左子树最右侧的结点
        if (node.left != null && node.right != null) {
            Node replacement = null;
            if (node.left.height <= node.right.height) {
                replacement = subtreeFirst(node.right);
            } else {
                replacement = subtreeLast(node.left);
            }
            node.val = replacement.val;
            node = replacement;
        }

        Node parent = node.parent;
        delete(node);
        rebalance(parent);
        return true;
    }

    // 删除结点p并用它的子结点代替它,结点p至多只能有1个子结点
    private void delete(Node node) {
        if (node.left != null && node.right != null) {
            return;
            // throw new Exception("Node has two children");
        }
        Node child = node.left != null ? node.left : node.right;
        if (child != null) {
            child.parent = node.parent;
        }
        if (node == root) {
            root = child;
        } else {
            Node parent = node.parent;
            if (node == parent.left) {
                parent.left = child;
            } else {
                parent.right = child;
            }
        }
        node.parent = node;
    }

    // 在以node为根结点的子树中搜索值为v的结点,如果没有值为v的结点,则返回值为v的结点应该在的位置的父结点
    private Node subtreeSearch(Node node, int v) {
        if (node.val < v && node.right != null) {
            return subtreeSearch(node.right, v);
        } else if (node.val > v && node.left != null) {
            return subtreeSearch(node.left, v);
        } else {
            return node;
        }
    }

    // 重新计算node结点的高度和元素数
    private void recompute(Node node) {
        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
        node.size = 1 + getSize(node.left) + getSize(node.right);
    }

    // 从node结点开始(含node结点)逐个向上重新平衡二叉树,并更新结点高度和元素数
    private void rebalance(Node node) {
        while (node != null) {
            int oldHeight = node.height, oldSize = node.size;
            if (!isBalanced(node)) {
                node = restructure(tallGrandchild(node));
                recompute(node.left);
                recompute(node.right);
            }
            recompute(node);
            if (node.height == oldHeight && node.size == oldSize) {
                node = null; // 如果结点高度和元素数都没有变化则不需要再继续向上调整
            } else {
                node = node.parent;
            }
        }
    }

    // 判断node结点是否平衡
    private boolean isBalanced(Node node) {
        return Math.abs(getHeight(node.left) - getHeight(node.right)) <= 1;
    }

    // 获取node结点更高的子树
    private Node tallChild(Node node) {
        if (getHeight(node.left) > getHeight(node.right)) {
            return node.left;
        } else {
            return node.right;
        }
    }

    // 获取node结点更高的子树中的更高的子树
    private Node tallGrandchild(Node node) {
        Node child = tallChild(node);
        return tallChild(child);
    }

    // 重新连接父结点和子结点(子结点允许为空)
    private static void relink(Node parent, Node child, boolean isLeft) {
        if (isLeft) {
            parent.left = child;
        } else {
            parent.right = child;
        }
        if (child != null) {
            child.parent = parent;
        }
    }

    // 旋转操作
    private void rotate(Node node) {
        Node parent = node.parent;
        Node grandparent = parent.parent;
        if (grandparent == null) {
            root = node;
            node.parent = null;
        } else {
            relink(grandparent, node, parent == grandparent.left);
        }

        if (node == parent.left) {
            relink(parent, node.right, true);
            relink(node, parent, false);
        } else {
            relink(parent, node.left, false);
            relink(node, parent, true);
        }
    }

    // trinode操作
    private Node restructure(Node node) {
        Node parent = node.parent;
        Node grandparent = parent.parent;

        if ((node == parent.right) == (parent == grandparent.right)) { // 处理需要一次旋转的情况
            rotate(parent);
            return parent;
        } else { // 处理需要两次旋转的情况:第1次旋转后即成为需要一次旋转的情况
            rotate(node);
            rotate(node);
            return node;
        }
    }

    // 返回以node为根结点的子树的第1个元素
    private static Node subtreeFirst(Node node) {
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }

    // 返回以node为根结点的子树的最后1个元素
    private static Node subtreeLast(Node node) {
        while (node.right != null) {
            node = node.right;
        }
        return node;
    }

    // 获取以node为根结点的子树的高度
    private static int getHeight(Node node) {
        return node != null ? node.height : 0;
    }

    // 获取以node为根结点的子树的结点数
    private static int getSize(Node node) {
        return node != null ? node.size : 0;
    }
}

时间复杂度: 预处理的时间复杂度为O(N),其中N是树中结点的总数。插入、删除和搜索的时间复杂度均为 O(log⁡N)
空间复杂度: O(N),用于存储平衡二叉搜索树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/234288.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

编译Android14 AOSP原生代码并运行X86模拟器镜像过程记录

最近在研究Android Entreprise部分的特性&#xff0c;需要在Android手机上分析WorkProfile相关的源码&#xff0c;因为新买的Pixel样机还未到货&#xff0c;看了几天Android源码&#xff0c;迫切需要上真机对比分析。 又听说最近几年Android模拟器已经有些进步&#xff0c;至少…

【利用二手车数据进行可视化分析】

利用二手车数据进行可视化分析 查看原始数据去除重复数据需求分析1.统计全国总共有多少量二手车&#xff0c;用KPI图进行展示2.统计安徽总共有多少量二手车&#xff0c;用KPI图进行展示3.统计合肥总共有多少量二手车&#xff0c;用KPI图进行展示4.取最贵的10辆二手车信息&#…

游戏提示找不到d3dx10_43.dll怎么办?5种方法教你如何修复

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“缺少d3dx10_43.dll文件”。这个错误提示通常出现在运行某些游戏或应用程序时&#xff0c;它意味着系统无法找到所需的动态链接库文件。本文将详细介绍d3dx10_43.dll文件的作用以及导致其丢…

class066 一维动态规划【算法】

class066 一维动态规划 算法讲解066【必备】从递归入手一维动态规划 code1 509斐波那契数列 // 斐波那契数 // 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 // 该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。…

从关键新闻和最新技术看AI行业发展(2023.11.20-12.3第十一期) |【WeThinkIn老实人报】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【WeThinkIn老实人报】旨在整理&挖掘AI行业的关键新闻和最新技术&#xff0c;同时Rocky会对这些关键信息进行解读&#xff0c;力求让读者们能从容跟随AI科技潮流。也欢迎大家提出宝贵的优化建议&#xff0c;一起交流学习&…

睡觉时总在凌晨3、4点醒来,再无睡意,一般暗示四大疾病,别忽视!

良好的睡眠是人体健康的重要因素&#xff0c;正常情况下的八小时睡眠时间在人们一天的时间中占比三分之一&#xff0c;睡眠是人体器官进行自我修复和调整的关键时间&#xff0c;睡眠质量不好&#xff0c;则会影响各个器官的健康&#xff0c;人们可以从睡眠中发现身体的异常&…

常见web漏洞的流量分析

常见web漏洞的流量分析 文章目录 常见web漏洞的流量分析工具sql注入的流量分析XSS注入的流量分析文件上传漏洞流量分析文件包含漏洞流量分析文件读取漏洞流量分析ssrf流量分析shiro反序列化流量分析jwt流量分析暴力破解流量分析命令执行流量分析反弹shell 工具 攻击机受害机wi…

midwayjs从零开始创建项目,连接mikro-orm框架(必须有java的springboot基础)

前言&#xff1a; 我一直都是用java的springboot开发项目&#xff0c;然后进来新公司之后&#xff0c;公司的后端是用node.js&#xff0c;然后框架用的是 midwayjs &#xff0c;然后网上的资料比较少&#xff0c;在此特地记录一波 文档&#xff1a; 1.官方文档&#xff1a;介绍…

Linux C/C++ 从内存转储中恢复64位ELF可执行文件

ELF&#xff08;Executable and Linking Format&#xff09;是一种对象文件的格式&#xff0c;它主要用于定义ELF&#xff08;Executable and Linking Format&#xff09;是一种对象文件的格式&#xff0c;它主要用于定义不同类型的对象文件中的内容以及它们的存储方式。一个EL…

深入了解对象与内置构造函数

1. 深入对象 1.1 创建对象的三种方式 1.2 构造函数 语法约定&#xff1a; 总结 构造函数可以快速创建多个对象大写字母开头的函数使用new关键字将对象实例化构造函数不需要返回值自动返回新的对象 new实例化的执行过程 创建空对象this指向对象执行代码&#xff0c;追加新…

class069 从递归入手三维动态规划【算法】

class069 从递归入手三维动态规划 code1 474. 一和零 // 一和零(多维费用背包) // 给你一个二进制字符串数组 strs 和两个整数 m 和 n // 请你找出并返回 strs 的最大子集的长度 // 该子集中 最多 有 m 个 0 和 n 个 1 // 如果 x 的所有元素也是 y 的元素&#xff0c;集合 x 是…

SVN修改已提交版本的日志方法

1.在工做中一直是使用svn进行項目的版本控制的&#xff0c;有时候因为提交匆忙&#xff0c;或是忘了添加Log&#xff0c;或是Log内容有错误。遇到此类状况&#xff0c;想要在查看项目的日志时添加log或是修改log内容&#xff0c;遇到以下错误&#xff1a; Repository has not b…

2023最新最全【Wireshark 】 安装教程(附安装包)

简介 wireshark是非常流行的网络封包分析工具&#xff0c;功能十分强大。可以截取各种网络封包&#xff0c;显示网络封包的详细信息。使用wireshark的人必须了解网络协议&#xff0c;否则就看不懂wireshark了。 为了安全考虑&#xff0c;wireshark只能查看封包&#xff0c;而…

公式识别任务各个链条全部打通

目录 引言公式识别任务是什么&#xff1f;公式识别任务解决方案初探使用建议写在最后 引言 随着LaTeX-OCR模型转换问题的解决&#xff0c;公式识别任务中各个链条已经全部打通。小伙伴们可以放开膀子干了。 解决业界问题的方案&#xff0c;并不是单独训练一个模型就完事了&am…

汽车网络安全--关于UN R155认证的思考

1.UN R155概述 2020年6月25日,联合国颁布了全球首个汽车网络安全强制性法规 -- UN 155,详细规定了关于评估网络安全措施的审核条款、制造商和供应商降低网络安全风险的方法以及实施风险评估的义务等。 法规适用于与信息安全相关的M类(4轮及以上载客汽车)、N类(四轮载货汽车)…

[MySQL--进阶篇]存储引擎的体系结构、简介、特点、选择

前言 ⭐Hello!这里是欧_aita的博客。 ⭐今日语录&#xff1a;不要在乎别人怎么看你&#xff0c;因为他们根本就没有时间&#xff0c;他们只关心他们自己。 ⭐个人主页&#xff1a;欧_aita ψ(._. )>⭐个人专栏&#xff1a; 数据结构与算法 MySQL数据库 存储引擎 前言MySQL体…

Leetcode刷题笔记题解(C++):25. K 个一组翻转链表

思路&#xff1a;利用栈的特性&#xff0c;K个节点压入栈中依次弹出组成新的链表&#xff0c;不够K个节点则保持不变 /*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ #include <stack> class Solution { …

【数据结构 — 排序 — 选择排序】

数据结构 — 排序 — 选择排序 一.选择排序1.基本思想2.直接选择排序2.1算法讲解2.2.代码实现2.2.1.函数定义2.2.2.算法接口实现2.2.3.测试代码实现2.2.4.测试展示 3.堆排序3.1.算法讲解3.2.代码实现3.2.1.函数定义3.2.2.算法接口实现3.2.3.测试代码实现3.2.4.测试展示 一.选择…

【数据结构实践课设】新生报道注册管理信息系统

目录 1.主要框架 2.写入文件 3.读取文件 4.注册学生信息 5.增加学生信息 6.删除学生信息 7.按姓名查询 8.按班级查询 9.按专业查询 10.打印学生信息 11.完整代码 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所…

【深度学习】强化学习(四)强化学习的值函数

文章目录 一、强化学习问题1、交互的对象2、强化学习的基本要素3、策略&#xff08;Policy&#xff09;4、马尔可夫决策过程5、强化学习的目标函数6、值函数1. 状态值函数&#xff08;State Value Function&#xff09;a. 状态值函数的定义b. 贝尔曼方程&#xff08;Bellman Eq…