pta 是否完全二叉搜索树_逻辑结构.树.二叉搜索树

BST

Why BST ?

BST全称Binary Search Tree,可见这个数据结构和搜索有着密切的关系,为什么要有这种数据结构呢?每一种新技术的出现必然是能够满足旧技术的功能,并且进一步补上一些旧技术的短板。回顾一下线性结构链表和数组,在这两种结构中进行某个值的搜索只能顺序遍历,效率较低;后来由于算法的助力,对于一个有序的数组,可以使用二分查找来大幅提升查找效率为。二分查找之所以快,是因为其并没有遍历每一个元素,而是利用整体有序的性质不断通过计算中位值来将搜索的区间折半

b6aad1e731e8bb83e9fc216db30d7385.png

为了将二分查找的高效附加在数据结构上,让其天然具有高效的搜索性质,大神发明了BST。BST就是二分查找在数据结构上的体现,不断将一个有序数组的中值上拉为根,根的左侧为值比其小的区间,根的右侧为值比其大的区间,简而言之,左小右大,就变成了一棵BST了。

41a762bcc9b24d5ea15f0f7ea06e5121.png

你看,此时如果我们要在树上查找某个结点,根据左小右大搜索路径查找,是不是本质和二分查找一模一样?

BST的性质

更加准确地来看看二叉搜索树,二叉搜索树是满足以下条件的特殊的二叉树:

  • 存在可比较的关键码key。
  • 左子树的任意结点key不大于根的key;右子树的任意结点key不小于根的key。简而言之,左小右大
  • 左右子树也是二叉搜索树。

可以发现,bst的定义是一种递归的定义,所以跟bst相关的==大部分问题都可以很方便地使用递归解决==。

二叉树的高度 & 深度

  • 某个结点的高度,是指该结点到其最远叶子结点的路径长度;

  • 某个结点的深度,是指该结点到根节点的路径长度。

9bca4b02970b532857bff8dcd600739f.png

而对于树本身而言,高度和深度分别就是所有结点中高度的最大值、深度的最大值。所以对于树而言,高度和深度的值是一样的;但对于某个结点而言,其高度和深度就不一定相同。

实操体会

在真正的使用中,我们并不可能完全看见存储在计算机中的树结构,因为说到底树只是我们构想出来的逻辑结构,而计算机中只有一个个物理地址单元来存储数据,即树上的每一个结点,故结点之间的联系就是通过地址来相连,也就是结点对象的引用

4e80719f2d84645ccbd0b62fa1f6cd08.png

同时由于结点的地址并不是连续的,不像数组,我们不能够随机访问某个结点,访问一棵树的唯一切入点就是根结点,有了根节点就相当于有了一棵树。

记住那句话:大部分树的问题都可以使用递归解决,因为树本身就是递归定义的。递归的分析的注意点:

  1. 找出同类型的子问题
  2. 找出递归的基例

开始吧,先定义结点。

class Node {
    Node left, right;
    K key;
    public Node(K key) {
        this.key = key;
    }
}
Node root; // 整棵树的唯一根结点

前序、中序、后序遍历二叉树

所谓的这三种遍历分别对应的遍历顺序为根左右、左根右、左右根,根就是指根结点,左右就是指左右子树。以中序遍历为例子,中序遍历左子树,访问根,中序遍历右子树。可以发现这就是一个递归的过程

90b41e9e7ab5fe35c0cc66b24bf156f5.png 12a156e342864ca039ee5df30ac4cff9.png ee42b5849e59c0c90f2b70e7e44da356.png f441108e5e9672cc2344dee3f4a9bc92.png 912a855626c9cacadca4f9c25cf55d2b.png aa63fd4ed08124fd8328203c57aa0ab9.png 268a7e367b5efb97e429c3441c32bbdf.png

可以发现,由于bst左小右大的性质,中序遍历的结果就是一个递增的序列。前边提到过,二叉树相关的问题大部分都可以使用递归来解决,从递归的角度分析就是:

  • 中序遍历树(左根右),就是中序遍历根的左子树(同类子问题),访问根节点,中序遍历根的右子树(同类子问题)。
  • 基例:子树为空。
//------------------ 中序遍历 ---------------------//
public void inOrder() {
    inOrder(root);
}

public void inOrder(Node r) {
    if (r == null) return;
    inOrder(r.left); // 中序遍历左子树
    System.out.println(r.key); // 访问根
    inOrder(r.right); // 中序遍历右子树
}
  • 前序遍历树(根左右),就是访问根,前序遍历左子树,前序遍历右子树。
  • 基例:子树为空。
//------------------ 前序遍历 ----------------------//
public void preOrder() {
    preOrder(root);
}

public void preOrder(Node r) {
    if (r == null) return;
    System.out.println(r.key); // 访问根
    preOrder(r.left); // 前序遍历左子树
    preOrder(r.right); // 前序遍历右子树
}
  • 后序遍历树(左右根),就是后序遍历左子树,后续遍历右子树,访问根。
  • 基例:子树为空。
//----------------- 后序遍历 ---------------------//
public void postOrder() {
    postOrder(root);
}

public void postOrder(Node r) {
    if (r == null) return;
    postOrder(r.left); // 后序遍历左子树
    postOrder(r.right); // 后序遍历右子树
    System.out.println(r.key); // 访问根
}

层次遍历二叉树

层次遍历意思是从上到下,每一层从左到右遍历整个二叉树。利用队列先进先出的特性就可以很方便地实现。

//----------------- 层次遍历 ---------------------//
public void levelTraversal() {
    if (root == null) return;
    Node tmp;
    Queue q = new LinkedList<>();
    q.add(root);while (!q.isEmpty()) {
        System.out.println((tmp = q.remove()).key);if (tmp.left != null) q.add(tmp.left);if (tmp.right != null) q.add(tmp.right);
    }
}

搜索

作为二分查找在数据结构上的体现,二叉树的查找就是二分的思想,要查找的关键码比根小则在左侧子树继续查找,否则在右侧子树上查找,利用递归的方式分析:

  • 在二叉搜索树上查找key,若key小于根结点关键码,则在左子树上查找(同类型子问题);否则在右子树上查找(同类型子问题)
  • 基例:树为空(失败),key值匹配(成功)
00b1de6f55ce9605733e93abf979bb94.png
public Node search(K key) {
    
}

private Node search(Node n, K key) {
    if (n == null) return null;
    int tmp;
    if ((tmp = key.compareTo(n.key)) 0) // 左小
        return search(n.left, key);
    else if (tmp > 0) // 右大
        return search(n.right, key);
    else return n; // 成功匹配
}

插入

要插入一个结点,就要通过查找找到位置,然后插入新节点。为了更易维护二叉搜索树的性质,插入总是尽量作为一个新的叶子,除非关键码匹配的结点已经在树中(此时替换值即可)。仍然以递归的视角来分析:

  • 要在bst插入一个关键码为key的结点
  • 若key小于bst的根结点,则在左子树上插入(同类型子问题);否则在右子树上插入(同类型子问题)
  • 基例:当前位置为空;关键码刚好匹配。
public void insert(K key) {
    root = insert(root, key);
}

private Node insert(Node n, K key) {
    if (n == null) return new Node(key); // 基例
    int tmp;
    if ((tmp = key.compareTo(n.key)) 0) {
        n.left = insert(n.left, key);
    } else if (tmp > 0) {
        n.right = insert(n.right, key);
    } else {
        // 刚好匹配,结点的数据进行更新即可
        // 定义的结点忽略了具体的数据,所以空操作即可
    }
    return n;
}

删除

bst的删除可以分为三种情况。

① 要删除的结点就是叶子结点:直接删除即可,因为不会影响bst的任何性质。

② 要删除的结点有一个孩子:将当前结点替换成孩子结点即可。

③ 要删除的结点有两个孩子:这个删除就比较巧妙,把中序遍历中的后继结点覆盖要删除的结点即可。其实就是将右子树中的最小值覆盖当前要删除的结点,接着在右子树中删除最小值。

b39187fe59c318843e54101595896831.png
188604bcf32a87a8ef1623297bbb8111.png
public void delete(K key) {
    root = delete(root, key);
}

private Node delete(Node n, K key) {
    if (n == null) return n;
    int tmp;
    if ((tmp = key.compareTo(n.key)) 0) {
        n.left = delete(n.left, key);
    } else if (tmp > 0) {
        n.right = delete(n.right, key);
    } else {
        if (n.left == null) // 只有一个孩子的情况 + 叶子的情况
            return n.right;
        else if (n.right == null)
            return n.left;
        Node min = minNode(n.right); // 有两个孩子的情况
        n.key = min.key; // 右子树中最小点,覆盖当前删除的点
        n.right = delete(n.right, n.key); // 在右子树中继续执行删除操作
    }
    return n;
}

// n为根的子树中的最小结点
private Node minNode(Node n) {
    Node tmp = null;
    while (n != null) {
        tmp = n;
        n = n.left;
    }
    return tmp;
}

根据前序遍历、中序遍历构建二叉树

根据遍历序列构建二叉树关键在于找到根,仍然从递归的角度分析:

  • 找到当前的根(前序序列的第一个元素)。
  • 根据找到的根元素,可以划分出左子树的前序、中序序列,右子树的前序、中序序列。
  • 在左子树中找根(同类型子问题),在右子树中找根(同类型子问题)……以此类推。
  • 基例:前序序列为空。
8e2475d37460b41e940f48bb2059fb17.png
/**
* 根据前序遍历和中序遍历构造bst
* @param pre 前序遍历key值
* @param in  中序遍历key值
* @return 构建完的bst的根
*/
public Node createBST(List pre, List in) {
    if (pre == null || pre.size() == 0) return null;
    K k = pre.get(0);   // 前序遍历的第一个点key就是根节点key
    Node rt = new Node(k); // 构建出根结点
    int leftSize = in.indexOf(k);    // 根节点在中序遍历中的位置,也是左子树的大小
    rt.left = createBST(
        pre.subList(1, 1 + leftSize),
        in.subList(0, leftSize)); // 找出左子树的根结点
    rt.right = createBST(
        pre.subList(1 + leftSize, pre.size()),
        in.subList(leftSize + 1, pre.size())); // 找出右子树的根结点
    return rt;
}

求二叉树的结点个数

对于二叉树这种递归定义的数据结构,利用递归解决问题真的是屡试不爽:

  • 二叉树的结点个数

    = 左子树的结点个数(同类型子问题) + 右子树的结点个数(同类型子问题) + 1(根)

  • 基例:树为空。

/**
* 求二叉树的节点数
* @return 二叉树的结点数
*/
public int size() {
    return size(root);
}

/**
* @param r 当前子树的根结点
* @return r为根的子树的结点数
*/
public int size(Node r) {
    if (r == null) return 0;
    return size(r.left) + size(r.right) + 1;
}

求二叉树第k层的结点个数

嗯,没错还是递归来看:

  • 二叉树第k层的结点个数

    = 左子树第k-1层的结点个数(同类子问题) + 右子树第k-1层的结点个数(同类子问题)

  • 基例:当前所在的层次 >= k

/**
* @param k 层次(从1开始)
* @return 第k层的结点个数
*/
public int levelSize(int k) {
    return levelSize(k, 1, root);
}

private int levelSize(int k, int currentLevel, Node n) {
    if (n == null) return 0;
    else if (currentLevel == k) return 1;
    else if (currentLevel > k) return 0;
    return levelSize(k, currentLevel + 1, n.left) // 左子树
        + levelSize(k, currentLevel + 1, n.right); // 右子树
}

求二叉树的叶子结点个数

递归角度分析:

  • 二叉树的叶子结点个数

    = 左子树的叶子结点个数 + 右子树的叶子结点个数

  • 基例:当前就是叶子结点

/**
* @return bst的叶子数
*/
public int leaves() {
    return leaves(root);
}

private int leaves(Node n) {
    if (n == null) return 0;
    else if (n.right == null && n.left == null) return 1;
    else
        return leaves(n.right) // 左子树的叶子结点个数
         + leaves(n.left); // 右子树的叶子结点个数
}

求二叉树的深度&高度

之前提到过,对于树而言深度和高度数值上是没有区别的,这里就分析一下高度(到叶子的距离)。递归分析走起:

  • 二叉树的高度

    = max(左子树的高度,右子树的高度)+ 1

  • 基例:树为空。

/**
* @return bst的高度(最远的叶子到跟的路径长度)
*/
public int height() {
    if (root == null) return 0;
    return height(root);
}

/**
* @param n 当前子树的根结点
* @return 结点n的高度
*/
private int height(Node n) {
    if (n == null) return -1;
    return Math.max(height(n.left), height(n.right)) + 1;
}

求二叉树中结点的最大距离(树直径)

递归分析走起,树直径可以分为三种情况:只存在左子树中、只存在右子树中、横跨根结点,故:

  • 二叉树的直径

    = max(左子树直径,右子树直径,左子树的高度 + 右子树的高度 + 2)

  • 基例:树为空

/**
* @return 二叉树的直径
*/
public int diameter() {
    return diameter(root);
}

private int diameter(Node n) {
    if (n == null) return 0;
    int leftHeight = height(n.left);
    int rightHeight = height(n.right);
    int leftDiameter = diameter(n.left);
    int rightDiameter = diameter(n.right);
    return Math.max(
        leftHeight + rightHeight + 2, // 横跨根结点
        Math.max(leftDiameter, rightDiameter) // 只存在于左子树或者右子树
    );
}

判断二叉树是不是完全二叉树

首先完全二叉树是指这样一棵二叉树:

  • 除了最后一层,其余层结点都铺满。
  • 最后一层的结点从左往右开始出现。
9c868d3a0607bb338f8cdd316a404e80.png

仍然先尝试使用递归,但是对于这个问题使用递归来解决似乎就有些棘手,因为没有办法找到同类型的子问题。转变策略,再观察一下完全二叉树的结构,可以发现它整体是从左往右,从上到下,非常符合层次遍历的逻辑。那么,如果是完全二叉树,则层次遍历从左往右,从上到下一路遍历下来都不应该在完全遍历完之前遇到null空结点,否则不是完全二叉树。

99162c1c3d510527c0fe6bcbdcca0210.png
/**
* 判断当前二叉树是否是完全二叉树
*
* @return true:是完全二叉树
*/
public boolean isComplete() {
    if (root == null) return true;
    Queue q = new LinkedList<>();
    q.add(root);
    Node n;while (!q.isEmpty()) {
        n = q.remove();if (n == null) { // 一旦遇到空结点就跳出循环break;
        } else {
            q.add(n.left);
            q.add(n.right);
        }
    }while (!q.isEmpty()) { // 检查遇到了空结点时,是否还存在未遍历到的结点if (q.remove() != null) // 存在未遍历到的结点,不是完全二叉树return false;
    }return true;
}

二叉树镜像

当你照着一面镜子,你的左手对于镜子里的它就是右手,你的右手对于镜子里的它就是左手。对于二叉树同理,二叉树照镜子,其左子树就是镜子里的它的右子树,其右子树就是镜子里的它的左子树,即左右子树交换。而左右子树也在照镜子呀,所以对于左右子树是同类型的子问题,使用递归解决:

  • 二叉树镜像 = 左右子树镜像 then 交换左右子树的位置
  • 基例:树为空
656e18492b247c90dc7c64a934be1be1.png
/**
* 将二叉树镜像
*/
public void mirror() {
    root = mirror(root);
}

private Node mirror(Node n) {
    if (n == null) return null;
    n.left = mirror(n.left);
    n.right = mirror(n.right);
    Node tmp = n.left;
    n.left = n.right;   n.right = tmp;
    return n;
}

小结

这么一路下来,对二叉树终于有那么些感觉了吧~一个递归的产物,所以其大部分问题都可以递归解决,在分析递归的时候主要找到递归的同类型子问题和递归的最终基例,此路不通再寻它路。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值