一 特性应用
BST(Binary Search Tree)特性:
1、对于 BST 的每一个节点
node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。2、对于 BST 的每一个节点
node,它的左侧子树和右侧子树都是 BST。从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)。
1.1 二叉搜索树中第K小的元素
我们可以利用这种特性来解题:
230. 二叉搜索树中第K小的元素 | 力扣 open in new window | LeetCode open in new window |给定一个二叉搜索树的根节点root,和一个整数k,请你设计一个算法查找其中第k个最小元素(从 1 开始计数)。
根据BST的特性,可以直接写出答案
int kthSmallest(TreeNode root, int k) { // 利用 BST 的中序遍历特性 traverse(root, k); return res; } // 记录结果 int res = 0; // 记录当前元素的排名 int rank = 0; void traverse(TreeNode root, int k) { if (root == null) { return; } traverse(root.left, k); /* 中序遍历代码位置 */ rank++; if (k == rank) { // 找到第 k 小的元素 res = root.val; return; } /*****************/ traverse(root.right, k); }
但是,这种遍历方式并不是最高效的,因为最坏的时间复杂度是
O(N),N 是 BST 的节点个数。要知道 BST 性质是非常牛逼的,像红黑树这种改良的自平衡 BST,增删查改都是
O(logN) 的复杂度,不过这个依赖于 BST 节点记录的信息有多少。对数级:就拿搜索某一个元素来说,BST 能够在对数时间找到该元素的根本原因还是在 BST 的定义里,左子树小右子树大嘛,所以每个节点都可以通过对比自身的值判断去左子树还是右子树搜索目标值,从而避免了全树遍历,达到对数级复杂度。
1.2 BST 转化累加树
TreeNode convertBST(TreeNode root) { traverse(root); return root; } // 记录累加和 int sum = 0; void traverse(TreeNode root) { if (root == null) { return; } traverse(root.right); // 维护累加和 sum += root.val; // 将 BST 转化成累加树 root.val = sum; traverse(root.left); }
核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。
简单总结下吧,BST 相关的问题,要么利用 BST 左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求
二 二叉搜索树操作(增删改查)
二叉搜索树的基础框架如下:
void BST(TreeNode root, int target) { if (root.val == target) // 找到目标,做点什么 if (root.val < target) BST(root.right, target); if (root.val > target) BST(root.left, target); }
2.1 判断BST的合法性
不能只判断一层左右子树是否符合合法性,不然会出现以下的情况。
出现问题的原因在于,对于每一个节点
root,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,root 的整个左子树都要小于 root.val,整个右子树都要大于 root.val。boolean isValidBST(TreeNode root) { return isValidBST(root, null, null); } /* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */ boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { // base case if (root == null) return true; // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST if (min != null && root.val <= min.val) return false; if (max != null && root.val >= max.val) return false; // 限定左子树的最大值是 root.val,右子树的最小值是 root.val return isValidBST(root.left, min, root) && isValidBST(root.right, root, max); }
2.2 BST中伸缩一个元素
力扣第 700 题「二叉搜索树中的搜索open in new window」就是让你在 BST 中搜索值为target的节点,函数签名如下:
TreeNode searchBST(TreeNode root, int target);
要利用BST的特性(左小右大),进行搜索优化,搜索复杂度应该为log2
TreeNode searchBST(TreeNode root, int target) { if (root == null) { return null; } // 去左子树搜索 if (root.val > target) { return searchBST(root.left, target); } // 去右子树搜索 if (root.val < target) { return searchBST(root.right, target); } return root; }
2.3 BST插入一个元素
插入时,找到合适的空位置,添加该元素即可。可以结合查询框架进行改变。
TreeNode insertIntoBST(TreeNode root, int val) { // 找到空位置插入新节点 if (root == null) return new TreeNode(val); // if (root.val == val) // BST 中一般不会插入已存在元素 if (root.val < val) root.right = insertIntoBST(root.right, val); if (root.val > val) root.left = insertIntoBST(root.left, val); return root; }
2.4 BST删除一个元素
删除一个元素的步骤是先找到,再删除。框架如下:
TreeNode deleteNode(TreeNode root, int key) { if (root.val == key) { // 找到啦,进行删除 } else if (root.val > key) { // 去左子树找 root.left = deleteNode(root.left, key); } else if (root.val < key) { // 去右子树找 root.right = deleteNode(root.right, key); } return root; }
但是删除时需要考虑多种情况:
情况一:删除为叶子结点
if (root.left == null && root.right == null) return null;
情况二:
A 只有一个非空子节点,那么它要让这个孩子接替自己的位置.// 排除了情况 1 之后 if (root.left == null) return root.right; if (root.right == null) return root.left;
情况三:有两个非空节点。
A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己// 这里以右子树中最小的那个节点代替自己举例。 if (root.left != null && root.right != null) { // 找到右子树的最小节点 TreeNode minNode = getMin(root.right); // 把 root 改成 minNode root.val = minNode.val; // 转而去删除 minNode root.right = deleteNode(root.right, minNode.val); }
通过以上三种情况完善框架,最终代码如下:
TreeNode deleteNode(TreeNode root, int key) { if (root == null) return null; if (root.val == key) { // 这两个 if 把情况 1 和 2 都正确处理了 if (root.left == null) return root.right; if (root.right == null) return root.left; // 处理情况 3 // 获得右子树最小的节点 TreeNode minNode = getMin(root.right); // 删除右子树最小的节点 root.right = deleteNode(root.right, minNode.val); // 用右子树最小的节点替换 root 节点 minNode.left = root.left; minNode.right = root.right; root = minNode; } else if (root.val > key) { root.left = deleteNode(root.left, key); } else if (root.val < key) { root.right = deleteNode(root.right, key); } return root; } TreeNode getMin(TreeNode node) { // BST 最左边的就是最小的 while (node.left != null) node = node.left; return node; }
总结一下吧,通过这篇文章,我们总结出了如下几个技巧:
1、如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。
2、在二叉树递归框架之上,扩展出一套 BST 代码框架:
void BST(TreeNode root, int target) { if (root.val == target) // 找到目标,做点什么if (root.val < target) BST(root.right, target); if (root.val > target) BST(root.left, target); }
3、根据代码框架掌握了 BST 的增删查改操作。
三 快速排序解析
快排的代码框架如下:
void sort(int[] nums, int lo, int hi) { if (lo >= hi) { return; } // 对 nums[lo..hi] 进行切分 // 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi] int p = partition(nums, lo, hi); // 去左右子数组进行切分 sort(nums, lo, p - 1); sort(nums, p + 1, hi); }
对比发现,快速排序就是一个二叉树的前序遍历,快排的过程就是一个构造二叉搜索树的过程
快速排序的核心无疑是
partition 函数, partition 函数的作用是在 nums[lo..hi] 中寻找一个切分点 p,通过交换元素使得 nums[lo..p-1] 都小于等于 nums[p],且 nums[p+1..hi] 都大于 nums[p]:从二叉树的视角,我们可以把子数组
nums[lo..hi] 理解成二叉树节点上的值,sort 函数理解成二叉树的遍历函数。