二叉搜索樹中第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),用于存儲平衡二叉搜索樹。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/212226.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/212226.shtml
英文地址,請注明出處:http://en.pswp.cn/news/212226.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RHEL8_Linux硬盤管理

主要介紹Linux磁盤管理 了解分區的概念對硬盤進行分區常見的分區swap分區的管理 1.了解分區的概念 1&#xff09;新的硬盤首先需要對其進行分區和格式化&#xff0c;下面來了解以下硬盤的結構&#xff0c;如圖。 2&#xff09;硬盤的磁盤上有一個個圈&#xff0c;每兩個圈組…

JVM虛擬機系統性學習-類加載子系統

類加載子系統 類加載的時機 類加載的時機主要有 4 個&#xff1a; 遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時&#xff0c;如果對應的類沒有初始化&#xff0c;則要先進行初始化 new 關鍵字創建對象時讀取或設置一個類型的靜態字段時&#xff08;被 …

javaSwing酒店管理系統

一、 使用方法&#xff1a; 在使用前&#xff0c;需要到druid.properties 配置文件中&#xff0c;修改自己對應于自己數據庫的屬性&#xff1b;如用戶名&#xff0c;密碼等 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql:///hotel?useUnicodetrue&characterEn…

midwayjs從零開始創建項目,連接mikro-orm框架(必須有java的springboot基礎)

前言&#xff1a; 我一直都是用java的springboot開發項目&#xff0c;然后進來新公司之后&#xff0c;公司的后端是用node.js&#xff0c;然后框架用的是 midwayjs &#xff0c;然后網上的資料比較少&#xff0c;在此特地記錄一波 文檔&#xff1a; 1.官方文檔&#xff1a;介紹…

vue 前端crypto-js 如何實現加密解密

npm 安裝 crypto-js 引用 import CryptoJS from "crypto-js"; 或者 import CryptoJS from "crypto-js"; //秘鑰 var aesKey "s10dfc3321ba59abbe123057f20f883e"; //將秘鑰轉換成Utf8字節數組 var key CryptoJS.enc.Utf8.parse(aesKey); /…

Spring Boot 3.0 : 集成flyway數據庫版本控制工具

目錄 Spring Boot 3.0 : 集成flyway數據庫版本控制工具flyway是什么為什么使用flyway主要特性支持的數據庫&#xff1a; flyway如何使用spring boot 集成實現引入依賴配置sql版本控制約定3種版本類型 運行SpringFlyway 8.2.1及以后版本不再支持MySQL&#xff1f; 個人主頁: 【?…

常見web漏洞的流量分析

常見web漏洞的流量分析 文章目錄 常見web漏洞的流量分析工具sql注入的流量分析XSS注入的流量分析文件上傳漏洞流量分析文件包含漏洞流量分析文件讀取漏洞流量分析ssrf流量分析shiro反序列化流量分析jwt流量分析暴力破解流量分析命令執行流量分析反彈shell 工具 攻擊機受害機wi…

Unity DOTS中的baking(一) Baker簡介

Unity DOTS中的baking&#xff08;一&#xff09; Baker簡介 baking是DOTS ECS工作流的一環&#xff0c;大概的意思就是將原先Editor下的GameObject數據&#xff0c;全部轉換為Entity數據的過程。baking是一個不可逆的過程&#xff0c;原先的GameObject在運行時不復存在&#x…

leetcode 股票DP系列 總結篇

121. 買賣股票的最佳時機 你只能選擇 某一天 買入這只股票&#xff0c;并選擇在 未來的某一個不同的日子 賣出該股票。 只能進行一次交易 很簡單&#xff0c;只需邊遍歷邊記錄最小值即可。 class Solution { public:int maxProfit(vector<int>& prices) {int res …

Vue-安裝及安裝vscode相應插件

安裝Vue 安裝nodejs&#xff0c; 地址&#xff1a;https://nodejs.org/en 下載后直接安裝。 安裝后重新打開命令行工具&#xff0c;輸入 node -v PS C:\Users\zcl36> node -v v20.10.0 2. 安裝vue包npm install -g vue/cli安裝之后&#xff0c;你就可以在命令行中訪問 vue…

【git】關于git二三事

文章目錄 前言一、創建版本庫1.通過命令 git init 把這個目錄變成git可以管理的倉庫2.將修改的內容添加到版本庫2.1 git add .2.2 git commit -m "Xxxx"2.3 git status 2.4 git diff readme.txt3.版本回退3.1 git log3.2 git reset --hard HEAD^ 二、理解工作區與暫存…

操作系統內部機制學習

切換線程時需要保存什么 函數需要保存嗎&#xff1f;函數在Flash上&#xff0c;不會被破壞&#xff0c;無需保存。函數執行到了哪里&#xff1f;需要保存嗎&#xff1f;需要保存。全局變量需要保存嗎&#xff1f;全局變量在內存上&#xff0c;無需保存。局部變量需要保存嗎&am…

Leetcode—337.打家劫舍III【中等】

2023每日刷題&#xff08;五十二&#xff09; Leetcode—337.打家劫舍III 算法思想 實現代碼 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(null…

I.MX6ULL_Linux_驅動篇(46)linux LCD驅動

LCD 是很常用的一個外設&#xff0c;在Linux 下LCD 的使用更加廣泛&#xff0c;在搭配 QT 這樣的 GUI 庫下可以制作出非常精美的 UI 界面。本章我們就來學習一下如何在 Linux 下驅動 LCD 屏幕。 Linux 下 LCD 驅動簡析 Framebuffer 設備 先來回顧一下裸機的時候 LCD 驅動是怎…

前端入門:HTML初級指南,網頁的簡單實現!

代碼部分&#xff1a; <!DOCTYPE html> <!-- 上方為DOCTYPE聲明&#xff0c;指定文檔類型為HTML --> <html lang"en"> <!-- html標簽為整個頁面的根元素 --> <head> <!-- title標簽用于定義文檔標題 --> <title>初始HT…

單點登錄方案調研與實現

作用 在一個系統登錄后&#xff0c;其他系統也能共享該登錄狀態&#xff0c;無需重新登錄。 演進 cookie → session → token →單點登錄 Cookie 可以實現瀏覽器和服務器狀態的記錄&#xff0c;但Cookie會出現存儲體積過大和可以在前后端修改的問題 Session 為了解決Co…

【其他數學】結式 resultant

結式 resultant 2023年11月30日 #analysis 文章目錄 結式 resultant介紹Sylvester矩陣應用在消元中的應用傳遞函數的化簡 下鏈 介紹 結式用來計算曲線的交點、消元、找參數化曲線的隱含方程。 為了引出定義&#xff0c;思考如下問題&#xff1a; f ( x ) x 2 ? 5 x 6 g (…

UVM建造測試用例

&#xff08;1&#xff09;加入base_test 在一個實際應用的UVM驗證平臺中&#xff0c;my_env并不是樹根&#xff0c;通常來說&#xff0c;樹根是一個基于uvm_test派生的類。真正的測試用例都是基于base_test派生的一個類。 class base_test extends uvm_test;my_env e…

14-2(C++11)類型推導、類型計算

14-2&#xff08;C11&#xff09;類型推導、類型計算 類型推導auto關鍵字auto類型推斷本質auto與引用 聯用auto關鍵字的使用限制 類型計算類型計算分類與類型推導相比四種類型計算的規則返回值后置 類型推導 auto關鍵字 C98中&#xff0c;auto表示棧變量&#xff0c;通常省略…

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 { …