(1)像普通的二叉树一样,我们可以按照前序、中序和后序来遍历一个二叉搜索树。 但是值得注意的是,对于二叉搜索树,我们可以通过中序遍历得到一个递增的有序序列。因此,中序遍历是二叉搜索树中最常用的遍历方法。
(2)删除二叉搜索树中的节点
删除要比我们前面提到过的两种操作复杂许多。有许多不同的删除节点的方法,这篇文章中,我们只讨论一种使整体操作变化最小的方法。我们的方案是用一个合适的子节点来替换要删除的目标节点。根据其子节点的个数,我们需考虑以下三种情况:
- 如果目标节点没有子节点,我们可以直接移除该目标节点。
- 如果目标节只有一个子节点,我们可以用其子节点作为替换。
- 如果目标节点有两个子节点,我们需要用其中序后继节点或者前驱节点来替换,再删除该目标节点。
(3)问题描述:设计一个类,求一个数据流中第k大的数。
一个很显而易见的解法是,先将数组降序排列好,然后返回数组中第k个数。
但这个解法的缺点在于,为了在O(1)时间内执行搜索操作,每次插入一个新值都需要重新排列元素的位置。从而使得插入操作的解法平均时间复杂度变为O(N)。因此,算法总时间复杂度会变为O(N^2)。
鉴于我们同时需要插入和搜索操作,为什么不考虑使用一个二叉搜索树结构存储数据呢?
我们知道,对于二叉搜索树的每个节点来说,它的左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于它的根结点的值。
换言之,对于二叉搜索树的每个节点来说,若其左子树共有m个节点,那么该节点是组成二叉搜索树的有序数组中第m + 1个值。
(4)利用数组实现大顶堆和小顶堆(大顶堆用来进行升序操作,小顶堆用来进行降序操作)
(5)平衡二叉树
一个高度平衡的二叉搜索树(平衡二叉搜索树)是在插入和删除任何节点之后,可以自动保持其高度最小。也就是说,有 N 个节点的平衡二叉搜索树,它的高度是 logN 。并且,每个节点的两个子树的高度不会相差超过 1。
但一个普通的二叉搜索树,在最坏的情况下,它可以退化成一个链。
因此,具有 N 个节点的二叉搜索树的高度在 logN 到 N 区间变化。也就是说,搜索操作的时间复杂度可以从 logN 变化到 N 。这是一个巨大的性能差异。
所以说,高度平衡的二叉搜索树对提高性能起着重要作用。
高度平衡的二叉搜索树在实际中被广泛使用,因为它可以在 O(logN) 时间复杂度内执行所有搜索、插入和删除操作。
平衡二叉搜索树的概念经常运用在 Set 和 Map 中。Set 和 Map 的原理相似。 我们将在下文中重点讨论 Set 这个数据结构。
Set(集合)是另一种数据结构,它可以存储大量 key(键)而不需要任何特定的顺序或任何重复的元素。 它应该支持的基本操作是将新元素插入到 Set 中,并检查元素是否存在于其中。
通常,有两种最广泛使用的集合:散列集合(Hash Set)和 树集合(Tree Set)。
树集合,Java 中的 Treeset 或者 C++ 中的 set ,是由高度平衡的二叉搜索树实现的。因此,搜索、插入和删除的时间复杂度都是 O(logN) 。
散列集合,Java 中的 HashSet 或者 C++ 中的 unordered_set ,是由哈希实现的,但是平衡二叉搜索树也起到了至关重要的作用。当存在具有相同哈希键的元素过多时,将花费 O(N) 时间复杂度来查找特定元素,其中N是具有相同哈希键的元素的数量。 通常情况下,使用高度平衡的二叉搜索树将把时间复杂度从 O(N) 改善到 O(logN) 。
哈希集和树集之间的本质区别在于树集中的键是有序的。
public class Heap {
public void heapSort(int[] num) {
int n = num.length;
//从i=n/2开始进行判断是否需要进行调整
for (int i = n / 2; i >= 0; i--) {
heapAdjust(num, i, n);
}
//将堆顶点元素与末尾元素进行换位,然后继续进行调整
for (int j = n - 1; j >= 0; j--) {
swap(num, 0, j);
heapAdjust(num, 0, j);
}
}
public void swap(int[] num, int i, int j) {
int temp = num[i];
num[i] = num[j];
num[j] = temp;
}
public void heapAdjust(int[] num, int begin, int n) {
//注意这里的i值
for (int i = begin; i < n; ) {
int max = i;
//2*i+1:判断该结点有左子树,num[2*i+1]>num[max]:左子树的值比父节点值大
if ((2 * i + 1) < n && num[2 * i + 1] > num[max]) {
max = 2 * i + 1;
}
//2*i+2:判断该结点有右子树,num[2*i+2]>num[max]:右子树的值比父节点值大
if ((2 * i + 2) < n && num[2 * i + 2] > num[max]) {
max = 2 * i + 2;
}
if (max != i) {
swap(num, max, i);
i = max;
} else {
break;
}
}
}
}
1.验证二叉搜索树
/*
执行用时:2 ms, 在所有 Java 提交中击败了31.63%的用户
内存消耗:38.1 MB, 在所有 Java 提交中击败了99.33%的用户
*/
解题思路:如上面所示,二叉搜索树经过中序排序之后能得到一个递增的有序序列,我们判断经过中序遍历的序列是否是有序的就可。
class Solution {
public boolean isValidBST(TreeNode root) {
ArrayList<Integer> temp = new ArrayList<>();
if (root==null){
return true;
}
inorderTraversal(root,temp);
for (int i = 1;i<temp.size();i++){
if(temp.get(i)<=temp.get(i-1)){
return false;
}
}
return true;
}
private void inorderTraversal(TreeNode root, ArrayList<Integer> temp) {
if (root.left!=null){
inorderTraversal(root.left, temp);
}
temp.add(root.val);
if (root.right!=null){
inorderTraversal(root.right, temp);
}
}
}
2.二叉搜索树迭代器
/*
执行用时:24 ms, 在所有 Java 提交中击败了70.91%的用户
内存消耗:43.2 MB, 在所有 Java 提交中击败了99.35%的用户
*/
class BSTIterator {
int count = 0;
ArrayList<Integer> res = new ArrayList<>();
public BSTIterator(TreeNode root) {
if (root != null) {
travelsal(root, res);
}
}
private void travelsal(TreeNode root, ArrayList<Integer> res) {
if (root.left != null) {
travelsal(root.left, res);
}
res.add(root.val);
if (root.right != null) {
travelsal(root.right, res);
}
}
/**
* @return the next smallest number
*/
public int next() {
return res.get(count++);
}
/**
* @return whether we have a next smallest number
*/
public boolean hasNext() {
if (count + 1 <= res.size()) {
return true;
} else {
return false;
}
}
}
3.二叉搜索树中的搜索
/*
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39 MB, 在所有 Java 提交中击败了93.66%的用户
*/
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if(root==null){
return null;
}
return treeTravel(root,val);
}
private TreeNode treeTravel(TreeNode root, int val) {
if (root==null){
return null;
}
if (root.val==val){
return root;
}else if (root.val>val){
return treeTravel(root.left, val);
}else {
return treeTravel(root.right, val);
}
}
}
4.二叉搜索树中的插入操作
/*
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39.3 MB, 在所有 Java 提交中击败了91.47%的用户
*/
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root==null){
return new TreeNode(val);
}
treeTravel(root,val);
return root;
}
private void treeTravel(TreeNode root, int val) {
if (root.val>val){
if (root.left==null){
TreeNode temp = new TreeNode(val);
root.left=temp;
}else {
treeTravel(root.left, val);
}
}else {
if (root.right==null){
TreeNode temp = new TreeNode(val);
root.right = temp;
}else {
treeTravel(root.right, val);
}
}
}
}
4.删除二叉搜索树中的节点
5.数据流中的第K大元素
/*
评论区大佬做法,利用PriorityQueue实现一个小顶堆。
执行用时:16 ms, 在所有 Java 提交中击败了98.02%的用户
内存消耗:42.4 MB, 在所有 Java 提交中击败了99.00%的用户
*/
class KthLargest {
final PriorityQueue<Integer> q ;
final int k;
public KthLargest(int k, int[] nums) {
this.k = k;
q = new PriorityQueue<Integer>(k);
for(int i: nums) {
add(i);
}
}
public int add(int val) {
if(q.size() < k) {
q.offer(val);
}
else if(q.peek() < val) {
q.poll();
q.offer(val);
}
return q.peek();
}
}
6.二叉搜索树的最近公共祖先
/*
执行用时:6 ms, 在所有 Java 提交中击败了99.89%的用户
内存消耗:39.2 MB, 在所有 Java 提交中击败了98.77%的用户
*/
解题思路:二叉搜索树本身具有左子树值比父结点值小,右子树值比父结点值大的特点。而p、q两值的存在有三种情况。
1.一个在左子树,一个在右子树,返回根节点
2.都在左子树
3.都在右子树
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root==null){
return root;
}
while (root!=null){
//当p、q值都比根节点的值大时,说明p、q都在根节点的右边
if (p.val>root.val&&q.val>root.val){
root = root.right;
}
//当p、q值都比根节点的值小时,说明p、q都在根节点的左边
else if (p.val<root.val&&q.val<root.val){
root = root.left;
}
else {
return root;
}
}
return root;
}
}
/*
执行用时:7 ms, 在所有 Java 提交中击败了39.54%的用户
内存消耗:39.2 MB, 在所有 Java 提交中击败了98.38%的用户
*/
解题思路:针对于普通二叉树的解题思路,没有用到二叉搜索树的特性
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null||root==p||root==q){
return root;
}
TreeNode leftTree = lowestCommonAncestor(root.left,p,q);
TreeNode rightTree = lowestCommonAncestor(root.right,p,q);
if(leftTree!=null&&rightTree!=null){
return root;
}else if(leftTree==null){
return rightTree;
}else if(rightTree==null){
return leftTree;
}
return null;
}
}
7.存在重复元素 III
8.平衡二叉树
/*
执行用时:1 ms, 在所有 Java 提交中击败了99.90%的用户
内存消耗:38.5 MB, 在所有 Java 提交中击败了95.75%的用户
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if(root==null){
return true;
}
if (Math.abs(getMaxHigh(root.left)-getMaxHigh(root.right))<=1){
return isBalanced(root.right)&&isBalanced(root.left);
}
return false;
}
private int getMaxHigh(TreeNode root) {
if(root==null) return 0;
return (Math.max(getMaxHigh(root.left),getMaxHigh(root.right))+1);
}
}
9.将有序数组转换为二叉搜索树
/*
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:38.4 MB, 在所有 Java 提交中击败了93.74%的用户
*/
解题思路:考察二分查找以及二叉搜索树的定义
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
if (nums==null||nums.length==0){
return null;
}
return bulidBst(nums,0,nums.length-1);
}
private TreeNode bulidBst(int[] nums, int left, int right) {
if (left>right) {
return null;
}
int mid = left+(right-left)/2;
TreeNode root = new TreeNode(nums[mid]);
root.left = bulitBst(nums, left, mid-1);
root.right = bulitBst(nums,mid+1,right);
return root;
}
}