力扣DAY40-45 | 熱100 | 二叉樹:直徑、層次遍歷、有序數組->二叉搜索樹、驗證二叉搜索樹、二叉搜索樹中第K小的元素、右視圖

前言

簡單、中等 √ 好久沒更了,感覺二叉樹來回就那些。有點變懶要警醒,不能止步于笨方法!!

二叉樹的直徑

我的題解

遍歷每個節點,左節點最大深度+右節點最大深度+當前節點=當前節點為中心的直徑。如果左節點深度更大,向左遍歷,直到直徑不再更新。

class Solution {
public://最大深度int deepOfTree(TreeNode* root){if (!root)return 0;return max(deepOfTree(root->left), deepOfTree(root->right))+1;}int diameterOfBinaryTree(TreeNode* root) {TreeNode* node = root;int d = 0;int maxd = 0;while (node){int deepl = deepOfTree(node->left);int deepr = deepOfTree(node->right);d = deepl + deepr;maxd = max(maxd, d);if (deepl > deepr)node = node->left;else if (deepl < deepr)node = node->right;elsebreak;}return maxd;}
};

?上述方法耗時較長,原因是求最大深度和遍歷每個節點的直徑步驟重復了。優化后把直徑設為全局節點。保留遍歷最大深度函數,遍歷過程中順便更新直徑maxd = max(maxd, deepl+deepr);

class Solution {
public:int maxd = 0;//最大深度int deepOfTree(TreeNode* node){if (!node)return 0;int deepl = deepOfTree(node->left);int deepr = deepOfTree(node->right);maxd = max(maxd, deepl+deepr);return max(deepl, deepr)+1;}int diameterOfBinaryTree(TreeNode* root) {deepOfTree(root);return maxd;}
};

官解

與筆者的方法二一致

心得

最大深度的延申題。

二叉樹的層次遍歷

我的題解

廣度優先搜索,用一個隊列存節點和深度,根據深度保存答案。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {if (!root) return {};vector<vector<int>> ans;queue<pair<TreeNode*, int>> q;q.push({root, 0});while (!q.empty()){TreeNode* node = q.front().first;int level = q.front().second;if (level >= ans.size())ans.push_back({node->val});elseans[level].push_back(node->val);if (node->left) q.push({node->left, level + 1});if (node->right) q.push({node->right, level + 1});q.pop();}return ans;}
};

官解

廣度優先搜索
思路和算法

我們可以用廣度優先搜索解決這個問題。

我們可以想到最樸素的方法是用一個二元組 (node, level) 來表示狀態,它表示某個節點和它所在的層數,每個新進隊列的節點的 level 值都是父親節點的 level 值加一。最后根據每個點的 level 對點進行分類,分類的時候我們可以利用哈希表,維護一個以 level 為鍵,對應節點值組成的數組為值,廣度優先搜索結束以后按鍵 level 從小到大取出所有值,組成答案返回即可。

考慮如何優化空間開銷:如何不用哈希映射,并且只用一個變量 node 表示狀態,實現這個功能呢?

我們可以用一種巧妙的方法修改廣度優先搜索:

首先根元素入隊
當隊列不為空的時候
求當前隊列的長度?
依次從隊列中取 元素進行拓展,然后進入下一次迭代
它和普通廣度優先搜索的區別在于,普通廣度優先搜索每次只取一個元素拓展,而這里每次取 元素。在上述過程中的第 i 次迭代就得到了二叉樹的元素。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector <vector <int>> ret;if (!root) {return ret;}queue <TreeNode*> q;q.push(root);while (!q.empty()) {int currentLevelSize = q.size();ret.push_back(vector <int> ());for (int i = 1; i <= currentLevelSize; ++i) {auto node = q.front(); q.pop();ret.back().push_back(node->val);if (node->left) q.push(node->left);if (node->right) q.push(node->right);}}return ret;}
};

心得

官解用了一個巧妙的方法節省了節點的深度信息。簡單來說就是同時把同一層的節點遍歷完。用currentLevelSize = q.size();記錄當前層有多少個節點,然后內嵌循環把這些節點都遍歷并且輸出。后續我也嘗試了這個方法但是漏了記錄當前層節點的關鍵步驟。之后會留意..

將有序數組轉換為二叉搜索樹

我的題解

有點像分治法,取中點作為當前節點建樹,左子樹取左邊數組作為新的數組建樹,右子樹取右邊。

class Solution {
public:TreeNode* sort(vector<int>& nums, int l, int r){if (l > r)return nullptr;int mid = (l + r)/2;TreeNode* root = new TreeNode(nums[mid]);root->left = sort(nums, l, mid-1);root->right = sort(nums, mid+1, r);return root; }TreeNode* sortedArrayToBST(vector<int>& nums) {if (nums.empty())return nullptr;TreeNode* root = new TreeNode();root = sort(nums, 0, nums.size()-1);return root;}
};

官解

官解與筆者思路一致,只是策略不同(左、右,隨機節點為子節點)

心得

有序數組轉換為二叉搜索樹還是比較簡單的,可以研究下無序數組如何轉換并且維護,應該跟最大堆差不多?

驗證二叉搜索樹

我的題解

左右子樹有三個點要驗證:1)子樹值與當前節點值比對;2)子樹是否也是二叉搜索樹;3)子樹的最大(右)/小(左)節點值與當前節點值比對。凡是一點不符合直接返回false,否則返回true。

class Solution {
public:bool isValidBST(TreeNode* root) {if (!root)return true;if (root->left){if (root->val <= root->left->val)return false;if (!isValidBST(root->left))return false;if (root->left->right){TreeNode* node = root->left->right;while (node->right){node = node->right;}if (root->val <= node->val)return false;}}if (root->right){if (root->val >= root->right->val)return false;if (!isValidBST(root->right))return false;if (root->right->left){TreeNode* node = root->right->left;while (node->left){node = node->left;}if (root->val >= node->val)return false;}}return true;}
};

官解

遞歸

要解決這道題首先我們要了解二叉搜索樹有什么性質可以給我們利用,由題目給出的信息我們可以知道:如果該二叉樹的左子樹不為空,則左子樹上所有節點的值均小于它的根節點的值; 若它的右子樹不空,則右子樹上所有節點的值均大于它的根節點的值;它的左右子樹也為二叉搜索樹。

這啟示我們設計一個遞歸函數 helper(root, lower, upper) 來遞歸判斷,函數表示考慮以 root 為根的子樹,判斷子樹中所有節點的值是否都在 (l,r) 的范圍內(注意是開區間)。如果 root 節點的值 val 不在 (l,r) 的范圍內說明不滿足條件直接返回,否則我們要繼續遞歸調用檢查它的左右子樹是否滿足,如果都滿足才說明這是一棵二叉搜索樹。

那么根據二叉搜索樹的性質,在遞歸調用左子樹時,我們需要把上界 upper 改為 root.val,即調用 helper(root.left, lower, root.val),因為左子樹里所有節點的值均小于它的根節點的值。同理遞歸調用右子樹時,我們需要把下界 lower 改為 root.val,即調用 helper(root.right, root.val, upper)。

函數遞歸調用的入口為 helper(root, -inf, +inf), inf 表示一個無窮大的值。

class Solution {
public:bool helper(TreeNode* root, long long lower, long long upper) {if (root == nullptr) {return true;}if (root -> val <= lower || root -> val >= upper) {return false;}return helper(root -> left, lower, root -> val) && helper(root -> right, root -> val, upper);}bool isValidBST(TreeNode* root) {return helper(root, LONG_MIN, LONG_MAX);}
};

中序遍歷

基于方法一中提及的性質,我們可以進一步知道二叉搜索樹「中序遍歷」得到的值構成的序列一定是升序的,這啟示我們在中序遍歷的時候實時檢查當前節點的值是否大于前一個中序遍歷到的節點的值即可。如果均大于說明這個序列是升序的,整棵樹是二叉搜索樹,否則不是,下面的代碼我們使用棧來模擬中序遍歷的過程。

可能有讀者不知道中序遍歷是什么,我們這里簡單提及。中序遍歷是二叉樹的一種遍歷方式,它先遍歷左子樹,再遍歷根節點,最后遍歷右子樹。而我們二叉搜索樹保證了左子樹的節點的值均小于根節點的值,根節點的值均小于右子樹的值,因此中序遍歷以后得到的序列一定是升序序列。

class Solution {
public:bool isValidBST(TreeNode* root) {stack<TreeNode*> stack;long long inorder = (long long)INT_MIN - 1;while (!stack.empty() || root != nullptr) {while (root != nullptr) {stack.push(root);root = root -> left;}root = stack.top();stack.pop();// 如果中序遍歷得到的節點的值小于等于前一個 inorder,說明不是二叉搜索樹if (root -> val <= inorder) {return false;}inorder = root -> val;root = root -> right;}return true;}
};

心得

兩個官解都是好方法。遞歸:輸入最大值和最小值,遍歷每個節點的值,在限定范圍內,左子樹的最大值更新為當前節點值,右子樹的最小值更新為當前節點值,子樹都為二叉搜索樹則返回true;迭代;迭代:中序遍歷,由于二叉搜索樹遍歷出來應該是升序排列,故如果當前節點小于等于前一節點,直接返回false。這個題考察對二叉搜索樹的理解,我的方法雖然繞過了long long但是太笨了!

二叉搜索樹中第K小的元素

我的題解

中序遍歷到第k個元素,return。

class Solution {
public:int kthSmallest(TreeNode* root, int k) {stack<TreeNode*> forder;TreeNode* node = root;vector<int> ans;int count = 0;forder.push(node);while(!forder.empty()){while(node){forder.push(node);node = node->left;}node = forder.top();ans.push_back(node->val);forder.pop();node = node->right;count++;if (count == k)break;}return ans[k-1];}
};

官解

中序遍歷與筆者一致,不贅述。平衡二叉搜索樹的方法太長了,粗略看每個函數也不太難,性價比不高,以后再學習吧。

記錄子樹的結點數

我們之所以需要中序遍歷前 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 MyBst {
public:MyBst(TreeNode *root) {this->root = root;countNodeNum(root);}// 返回二叉搜索樹中第k小的元素int kthSmallest(int k) {TreeNode *node = root;while (node != nullptr) {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;}private:TreeNode *root;unordered_map<TreeNode *, int> nodeNum;// 統計以node為根結點的子樹的結點數int countNodeNum(TreeNode * node) {if (node == nullptr) {return 0;}nodeNum[node] = 1 + countNodeNum(node->left) + countNodeNum(node->right);return nodeNum[node];}// 獲取以node為根結點的子樹的結點數int getNodeNum(TreeNode * node) {if (node != nullptr && nodeNum.count(node)) {return nodeNum[node];}else{return 0;}}
};class Solution {
public:int kthSmallest(TreeNode* root, int k) {MyBst bst(root);return bst.kthSmallest(k);}
};

心得

中序遍歷的迭代寫法還需要再鞏固,以當前節點為判斷條件,而不是以下一節點為判斷條件。記錄子樹的結點數的方法:首先要用一個哈希表記錄每個節點有多少顆子樹,計算過程定義count函數,獲取過程定義get函數。然后遍歷每個節點,如果子樹數量小于k,移到right,如果等于k,返回當前節點,大于k,移到left。感覺這個方法不是很能復用到其他題目上,但是count和get(哈希)這種分開的方式很值得我學習,是一種安全高效的獲取方式。

二叉樹的右視圖

我的題解

對二叉樹進行層次遍歷,把每一層的最后一個元素取出放入ans容器中。

class Solution {
public:vector<int> rightSideView(TreeNode* root) {if (!root)return {};queue<pair<TreeNode*, int>> q;vector<vector<int>> bfs;vector<int> ans;TreeNode* node = root;int level = 0;q.push({node, level});while (!q.empty()){node = q.front().first;level = q.front().second;q.pop();if (node->left) q.push({node->left, level+1});if (node->right) q.push({node->right, level+1});if (level >= bfs.size()) bfs.push_back({node->val});else bfs[level].push_back(node->val);}for (int i = 0; i < bfs.size(); i++){ans.push_back(bfs[i].back());}return ans;}
};

官解

廣度優先搜索/層次遍歷與筆者思路一致,不贅述。

深度優先搜索

我們對樹進行深度優先搜索,在搜索過程中,我們總是先訪問右子樹。那么對于每一層來說,我們在這層見到的第一個結點一定是最右邊的結點。

算法

這樣一來,我們可以存儲在每個深度訪問的第一個結點,一旦我們知道了樹的層數,就可以得到最終的結果數組。

上圖表示了問題的一個實例。紅色結點自上而下組成答案,邊緣以訪問順序標號。

class Solution {
public:vector<int> rightSideView(TreeNode* root) {unordered_map<int, int> rightmostValueAtDepth;int max_depth = -1;stack<TreeNode*> nodeStack;stack<int> depthStack;nodeStack.push(root);depthStack.push(0);while (!nodeStack.empty()) {TreeNode* node = nodeStack.top();nodeStack.pop();int depth = depthStack.top();depthStack.pop();if (node != NULL) {// 維護二叉樹的最大深度max_depth = max(max_depth, depth);// 如果不存在對應深度的節點我們才插入if (rightmostValueAtDepth.find(depth) == rightmostValueAtDepth.end()) {rightmostValueAtDepth[depth] =  node -> val;}nodeStack.push(node -> left);nodeStack.push(node -> right);depthStack.push(depth + 1);depthStack.push(depth + 1);}}vector<int> rightView;for (int depth = 0; depth <= max_depth; ++depth) {rightView.push_back(rightmostValueAtDepth[depth]);}return rightView;}
}; 

心得

層次遍歷的解法是直觀的解法。深度優先搜索的解法則是定義了哈希表記錄每個深度最右的節點。維護一個節點棧和深度棧,然后對二叉樹進行后序遍歷,如果當前深度沒有最右節點,則放入ans中。這個解法其實也與層次遍歷類似,只是顯式地用棧來維護深度信息。

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

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

相關文章

頭歌數據庫【數據庫概論】第10-11章 故障恢復與并發控制

第1關&#xff1a;數據庫恢復技術 1、事務的&#xff08; A&#xff09;特性要求事務必須被視為一個不可分割的最小工作單元 A、原子性 B、一致性 C、隔離性 D、持久性 2、事務的&#xff08;C &#xff09;特性要求一個事務在執行時&#xff0c;不會受到其他事務的影響。 A、原…

windows下,cursor連接MCP服務器

1.下載并安裝node 安裝后&#xff0c;在cmd命令框中&#xff0c;輸入命令node -v可以打印版本號&#xff0c;證明安裝完成 2.下載MCP服務器項目 在MCP服務器找到對應項目&#xff0c;這里以server-sequential-thinking為例子 在本地cmd命令窗口&#xff0c;使用下面命令下載…

前端配置husky,commit-lint導致的git提交錯誤:git xx@0.0.0 lint:lint-staged

前端配置husky&#xff0c;commit-lint導致的git提交錯誤&#xff1a;git xx0.0.0 lint:lint-staged git commit -m "xxx"時出現以下報錯&#xff0c;可能是前端配置husky&#xff0c;commit-lint的原因 //報錯信息 git xx0.0.0 lint:lint-staged首先要知道出現這個錯…

各種場景的ARP攻擊描述筆記(超詳細)

1、ARP報文限速 上一章我們說過ARP報文也是需要上送CPU進行處理的協議報文,如果設備對收到的大量ARP報文全部進行處理,可能導致CPU負荷過重而無法處理其他業務。因此,在處理之前需要對ARP報文進行限速,以保護CPU資源。 1.根據源MAC地址或源IP地址進行ARP限速 當設備檢測到某一…

Django 創建CSV文件

Django使用Python內置的CSV庫來創建動態的CSV&#xff08;逗號分隔值&#xff09;文件。我們可以在項目的視圖文件中使用這個庫。 讓我們來看一個例子&#xff0c;這里我們有一個Django項目&#xff0c;我們正在實現這個功能。創建一個視圖函數 getfile() 。 Django CSV例子 …

HTTPS為何仍有安全漏洞?解析加密協議下的攻擊面

本文深度剖析HTTPS協議在傳輸層、證書體系、配置管理三個維度的安全盲區&#xff0c;揭示SSL/TLS加密掩蓋下的11類攻擊路徑。基于Equifax、SolarWinds等重大事件的技術復盤&#xff0c;提供包含自動化證書巡檢、動態協議升級、加密流量威脅檢測的立體防御方案。 HTTPS不等于絕…

MyBatis 動態 SQL 使用詳解

&#x1f31f; 一、什么是動態 SQL&#xff1f; 動態 SQL 是指根據傳入參數&#xff0c;動態拼接生成 SQL 語句&#xff0c;不需要寫多個 SQL 方法。MyBatis 提供了 <if>、<choose>、<foreach>、<where> 等標簽來實現這類操作 ? 二、動態 SQL 的優點…

樂觀鎖與悲觀鎖的使用場景

悲觀鎖的應用場景 悲觀鎖的基本思想是假設并發沖突會發生&#xff0c;因此在操作數據時會先鎖定數據&#xff0c;直到完成操作并提交事務后才釋放鎖。這種方式適用于寫操作較多、并發沖突可能性較高的場景。 高寫入比例的數據庫操作&#xff1a;如果系統中有很多寫操作&#x…

cpp(c++)win 10編譯GDAL、PROJ、SQLite3、curl、libtiff

cpp&#xff08;c&#xff09;編譯GDAL、PROJ、SQLite3 Sqlite3libtiffcurlprojGDAL Sqlite3 1、下載 Sqlite3 源碼、工具、二進制預編譯 exe Sqlite3 官網&#xff1a;https://www.sqlite.org/download.html 下載 sqlite-amalgamation-3430200.zipsqlite-dll-win64-x64-3430…

【愚公系列】《高效使用DeepSeek》062-圖書庫存管理

??【技術大咖愚公搬代碼:全棧專家的成長之路,你關注的寶藏博主在這里!】?? ??開發者圈持續輸出高質量干貨的"愚公精神"踐行者——全網百萬開發者都在追更的頂級技術博主! ?? 江湖人稱"愚公搬代碼",用七年如一日的精神深耕技術領域,以"…

鏈表算法中常用操作和技巧

目 1.常用技巧 1.1.畫圖 1.2.添加虛擬頭節點 1.3.大膽引入中間變量 1.4.快慢雙指針 1.4.1判斷鏈表是否有環 1.4.2找鏈表中環的入口 ?2.常用操作 2.1. 創建一個新節點 2.2.尾插 2.3.頭插 1.常用技巧 1.1.畫圖 畫圖可以讓一些抽象的文字語言更加形象生動 畫圖&#…

【9】數據結構的串篇章

目錄標題 串的定義順序串的實現初始化賦值打印串求串的長度復制串判斷兩個串長度是否相等連接兩個串比較兩個串內容是否相等插入操作刪除操作調試與代碼合集 串的模式匹配算法樸素的模式匹配算法KMP算法實現模式匹配 串的定義 定義&#xff1a;由0個或多個字符組成的有限序列&…

GMSL Strapping Pins CFG0/CFG1 應用

GMSL device 使用起來還是比較簡單 ADI 已經充分考慮了用戶的需求&#xff0c;盡可能的降低的芯片的使用和配置復雜度 一對加串器和解串器&#xff0c;只要工作模式匹配得當&#xff0c;Link Locked&#xff0c;便能夠正常工作 如果遇到 Link 無法建立&#xff08;Locked&…

`uia.WindowControl` 是什么:獲取窗口文字是基于系統的 UI 自動化接口,而非 OCR 方式

uia.WindowControl 是什么:獲取窗口文字是基于系統的 UI 自動化接口,而非 OCR 方式 uia.WindowControl 通常是基于 Windows 系統的 UI 自動化框架(如 pywinauto 中的 uia 模塊)里用于表示窗口控件的類。在 Windows 操作系統中,每個應用程序的窗口都可以看作是一個控件,ui…

Easysearch VS Opensearch 數據寫入與存儲性能對比

本文記錄 Easysearch 和 Opensearch 數據寫入和數據存儲方面的性能對比。 準備 壓測工具&#xff1a;INFINI Loadgen 對比版本&#xff1a; Easysearch 1.11.1&#xff08;lucene 8.11.4&#xff09;Opensearch 2.19.1&#xff08;lucene 9.12.1&#xff09; 節點 JVM 配置…

力扣題解:142. 環形鏈表 II

在鏈表學習中&#xff0c;我們已經了解了單鏈表和雙鏈表&#xff0c;兩者的最后一個結點都會指向NULL&#xff1b;今天我們介紹的循環列表則不同&#xff0c;其末尾結點指向的這是鏈表中的一個結點。 循環鏈表是一種特殊類型的鏈表&#xff0c;其尾節點的指針指向頭節點&#…

區間 dp 系列 題解

1.洛谷 P4342 IOI1998 Polygon 我的博客 2.洛谷 P4290 HAOI2008 玩具取名 題意 某人有一套玩具&#xff0c;并想法給玩具命名。首先他選擇 W, I, N, G 四個字母中的任意一個字母作為玩具的基本名字。然后他會根據自己的喜好&#xff0c;將名字中任意一個字母用 W, I, N, G …

天基光學圖像仿真原理簡介

一、原理簡介 天基光學圖像仿真通過數學模型和算法模擬空間目標在光學系統中的成像過程&#xff0c;核心原理可歸納為以下四部分&#xff1a; 1. 目標與背景建模? 目標運動建模?&#xff1a;利用軌道動力學模型&#xff08;如SGP4&#xff09;解析空間目標軌跡&#xff0c;…

Jetpack Compose 狀態保存機制全面解析:讓UI狀態持久化

在Android開發中&#xff0c;Jetpack Compose 的狀態管理是一個核心話題&#xff0c;而狀態保存則是確保良好用戶體驗的關鍵。本文將深入探討Compose中各種狀態保存技術&#xff0c;幫助你在配置變更和進程重建時保持UI狀態。 一、基礎保存&#xff1a;rememberSaveable reme…

【Json-Rpc #1】項目背景及環境搭建

&#x1f4c3;個人主頁&#xff1a;island1314 &#x1f525;個人博客&#xff1a;island ?? 歡迎關注&#xff1a;&#x1f44d;點贊 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; 生活總是不會一帆風順&#xff0c;前進…