关于满二叉树、完全二叉树以及完全二叉树的大根堆小根堆

满二叉树  完全二叉树的定义

  • 满二叉树就是每一层节点个数都是满的二叉树。第n层节点个数 = pow(2,n - 1), 总的节点个数 =  pow(2, n)。
  • 完全二叉树就是满二叉树从最后一层的最右侧开始去节点得的树,这个去节点只能从右往左去。

    满二叉树    完全二叉树

关于完全二叉树的几个面试题目一:判断是不是完全二叉树

  • 思路:层次遍历:当前节点左子树不空放入queue中,右子树不空放入queue中。当前节点左子树为空和右子树不空,直接返回False。当前节点右子树为空,不管左子树空不空,在从queue中拿出的节点的左右子树都是空才是完全二叉树,否则不是。
//C++代码      queue是队列,deque是双端队列,知道一些常用的函数
#include<iostream>
using namespace std;
class Solution {
public:
    bool isCompleteTree(TreeNode* root) {
        if(root == NULL)
            return false;
        // 注意queue的队列,deque是双端队列,二者不同
        queue<TreeNode *> q;
        q.push(root);
        TreeNode * node;
        while(!q.empty()){
            node = q.front();
            q.pop();
            if(node->left)
                q.push(node->left);
            if(node->right)
                q.push(node->right);

            if(node->left == NULL && node->right != NULL){
                return false;
            }

            if(node->right == NULL){
                while(!q.empty()){
                    node = q.front();
                    q.pop();
                    if(!node->left && !node->right)
                        continue;
                    else
                        return false;
                }
            }
        }
        return true;
    }
};
#python代码简单
class Solution:
    def isCompleteTree(self, root: TreeNode) -> bool:
        if root == None:
            return False
        queue = [root]
        while queue:
            node = queue.pop(0)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

            if node.left == None and node.right:
                return False

            if node.right == None:
                while queue:
                    node = queue.pop(0)
                    if node.left == None and node.right == None:
                        continue
                    else:
                        return False
            
        return True

获取完全二叉树的节点个数,换一种问法就是 给定一棵完全二叉树,返回最后一层的最右边的节点,一个题目

思路:首先完全二叉树是一般的树,对于一般的树获取节点个数就是递归或者非递归遍历:非递归就是遍历呗,递归更简单。写一下递归算法:        递归非常简单,按照二叉树的基本递归模板与逻辑模板写这个非常easy。

def countNodes(self, root: TreeNode) -> int:
        return self.countNodes(root.left) + self.countNodes(root.right) + 1 if root else 0

思路二:那么思路二就是看上图的完全二叉树,发现如果左子树高度等于右子树高度,那么最终节点在右子树上。如果不等于,那么最终节点在左子树上。基于这个理论,我们采用左右遍历。那么每一次遍历发现,如果root往右走,左侧是满二叉树的一半,(满二叉树节点总数等于pow(2, n)-1,一半的节点(包含根节点)个数是pow(2, n-1)),二者节点个数公式记住;如果往左侧走,右侧是满二叉树的一半,所以我们每次不管往左走还是往右走,只要把这个当前的node的height算出来就行了,因为高度算出来,代入公式结果就出来了。只是里面有一个小点,往左走的时候,右子树这个完全二叉树的高度是当前node的height-1,所以带入上述公式就变成了pow(2, n-2)-1,这就是下面的公司的由,因为我们这里的完全二叉树的高度是用左子树算的。时间复杂度就是树高。

上述二种思路的总结点如下:

  • 层次遍历,输出最后一个节点,时间复杂度:O(N)
  • 递归,求子树的高度:如果左子树高度>右子树高度,则在左子树继续递归过程;否则在右子树继续递归。如果当前节点为叶子节点,则返回;由于是完全二叉树,求高度时只需一直往左遍历即可。每次递归都下降一层,每次都求树的高度,时间复杂度为O(lgN * lgN)。
class Solution:
    def getDeep(self, node):
        return self.getDeep(node.left) + 1 if node else 0

    def countNodes(self, root: TreeNode) -> int:
        nodeNumber = 0
        while root:
            if self.getDeep(root.left) == self.getDeep(root.right):
                nodeNumber += pow(2, self.getDeep(root)-1)
                root = root.right
            else:
                nodeNumber += pow(2, self.getDeep(root)-2)
                root = root.left
        # 包含root == None
        return nodeNumber

完全二叉树的性质

  1. 总结点个数个层数的关系(根节点就有一层) :nodeNumber = pow(2, n)-1; 一半的完全二叉树的节点个数是 pow(2, n-1)。
  2. 完全二叉树的高度不断left就能得到,不用套用一般的二叉树的高度公式.
  3. 关于完全二叉树的插入删除相对简单,因此不考这两个点。

关于堆的内容(对首先是完全二叉树)

  • 堆分为2种,大根堆和小根堆,大根堆就是根节点大于左右子树且左右子树也是大根堆的树。可以发现大根堆的每一层都比下面的数大,但是每一层左右的数大小关系不知道,这点要注意。小根堆和大根堆定义一致。同时注意和二叉排序树进行区分。
  • 首先是完全二叉树,就像平衡二叉树首先是二叉排序树,当然更是二叉树。所以题目的解法往往有二叉树最简单的方法,也有完全二叉树的方法,更有堆自己的特性和方法。
  • 堆,hu堆产生的直接目的不是为了查找,是为了获取排序的序列,特别是个处理前K大的数这类问题。获取前k大的值就构建小根堆,反正构建大根堆,具体原因自己想明白。

堆的构建

堆的构建,是和完全二叉树的构建是等同的,只是构建以后需要按照堆的规则进调整。这个和平衡二叉树的创建是等同的,也是先创建儿茶排序树,插入节点就是不断的左右对比么,然后调整就行了。这里插入就是直接插在最后面,然后向上调整。如果构建大根堆,向上调整就是比较当前节点和父节点的大小,如果比父节点大就和父节点交换,大根堆么,顶点肯定大。

那么问题是如何获取当前节点的父亲节点呢?   看这个图,若我们直接用数组下标从1开始,那么如果当前的节点的标号是i的话,儿子节点的标号就是 2*i,右儿子节点的标号就是 2*i+1,反之,如果当前节点的标号是i,那么父亲节点的标号一定是 i // 2,前提是list里面有一个元素了,这样主要是好记忆。

            heap = [0]
            # 求最小的k个数,建立大根堆,否则建立小根堆
            def creatBigHeap(x):
                heap.append(x)
                i = len(heap) - 1
                # 每扩充一个就开始调整,构建大根堆
                while i != 1:
                    if heap[i] > heap[i // 2]:
                        heap[i], heap[i // 2] = heap[i // 2], heap[i]
                        i = i // 2
                    else:
                        break

 

 

堆的替换    

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

上述堆构建了之后,他的节点个数就是k个,有时候我们想就维持k个节点的堆,但是我们要不断的替换,这就是寻找前k小的数的题目。这个题目就是这样:替换的思路和创建的思路刚好相反,创建是从树的下往上,替换是从根节点开始往下。

替换的时候,以大根堆举例子,根节点是最大的,先用num和根结点对比,只要比根节点小,就替换,否则就直接结束了。

比较当前节点的左子树和右指子树,取最大的值,如果当前的值比最大的值小,就替换。这里需要考虑没有左右子树、只有一个子树,左右子树都有三种如何统一写代码,最后发现下面的代码。如果一个子树不存在,那么设定的值就是-1,用这个来实现代码的统一。

其实替换的思路非常简答,但是实现代码的同意确实不是太好写,我就实现了这个。

def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
            if k == 0 or k > len(tinput):
                return []
            heap = [0]
            # 求最小的k个数,建立大根堆,否则建立小根堆
            def creatBigHeap(x):
                heap.append(x)
                i = len(heap) - 1
                # 每扩充一个就开始调整
                while i != 1:
                    if heap[i] > heap[i // 2]:
                        heap[i], heap[i // 2] = heap[i // 2], heap[i]
                        i = i // 2
                    else:
                        break
            # 创建k个元素的堆
            for val in tinput[:k]:
                creatBigHeap(val)

            # 后面的元素开始调整最大堆
            def adjust(num):      
                def exchage(k):
                    if k > len(heap):
                        return
                    left = heap[2*k] if 2*k < len(heap) else -1
                    right = heap[2*k+1] if 2*k+1 < len(heap) else -1
                    # 说明都是-1,表明是叶子节点,不存在左右子树
                    if left == right:
                        return
                    # 左子树节点数值大于右子数  和  右子树不存在
                    elif left > right:
                        c = 2*k
                    # 左子树节点数值小于右子数  和  左子树不存在
                    else:
                        c = 2*k+1
                    # 通过上面巧妙实现了代码的统一
                    if heap[k] < heap[c]:
                        heap[c],heap[k] = heap[k], heap[c]
                        exchage(c)
                # 处理跟节点  
                if num < heap[1]:
                    heap[1] = num
                    exchage(1)
                else:
                    return

            for i in tinput[k:]:
                adjust(i)
            heap = list(sorted(heap))
            return heap[1:]

堆的删除

类似于平衡二叉树的节点删除,我们考虑三种情况:如果左右子树均为空,直接删除后设定为None,让该节点的父节点指向None,采用递归法就好了,因为递归法自己找到父节点。如果左右子树一个不为空,直接接上就好了。如果都不为空,那么选取最大的节点替换被删除的节点的位置,剩下的一个做该节点的左子树,一个做右子树,实现代码参考平衡二叉树的删除,问题不大。

关于堆的分析

首先明白我们为什么要有堆?我们还是用上面的那个题目,取前k小的数据。我们可以使用下面的代码实现,先取k个数值组成一个list大小排序,然后再来的数值一个个比,然后移除去就行了,可以发现每取一个数和数组里面的数对比查询的时候,如果采用二分查找,时间复杂度是O(log N),当查找到后开始逐步的向后移动,这个时间复杂度是O(N),整体上查找+移除就是O(N),如果外面需要查找的数据是m的话,时间复杂度就是O(mn)。

# 构建数组处理
if k == 0 or k > len(tinput):
                return []
            result = sorted(tinput[:k])
            for insertVal in tinput[k:]:
                q = k - 1
                while q >= 0:
                    j = k - 1
                    if insertVal > result[q]:
                        while j - 1 > q:
                            result[j] = result[j-1]
                            j -= 1
                        if j != q:
                            result[j] = insertVal
                        break
                    else:
                        if q == 0:
                            while j > 0:
                                result[j] = result[j - 1]
                                j -= 1
                            result[0] = insertVal
                    q -= 1
            return result

但是如果是采用上述堆的话,查找的时间复杂度和替换的时间复杂度都是O(log N),所以整体的复杂度就是O(mlogN),这就是为什么使用堆,从查询时间和替换时间上进行了同步的缩短。这就是使用堆的原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值