满二叉树 完全二叉树的定义
- 满二叉树就是每一层节点个数都是满的二叉树。第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
完全二叉树的性质
- 总结点个数个层数的关系(根节点就有一层) :nodeNumber = pow(2, n)-1; 一半的完全二叉树的节点个数是 pow(2, n-1)。
- 完全二叉树的高度不断left就能得到,不用套用一般的二叉树的高度公式.
- 关于完全二叉树的插入删除相对简单,因此不考这两个点。
关于堆的内容(对首先是完全二叉树)
- 堆分为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