二叉树
一棵二叉树(binary tree)由结点(node)的有限集合组成,这个集合或者为空(empty),或者由一个根结点(root)以及两棵不相交的二叉树组成,这两棵二叉树分别称为这个根的左子树(left subtree)和右子树(right subtree)。这两棵子树的根称为此二叉树根结点的子结点(children);从一个结点到其两个子结点都有边(edge)相连,这个结点称为其子结点的父结点(parent)。
如果一棵树有一串结点n1,n2,...,nk,且ni是ni+1的父结点(1<=i<k),则n1,n2,...,nk称为一条有n1到nk的路径(path),其长度为k-1。有一条路径从结点R至结点M,则R称为M的祖先(ancestor),M称为R的子孙(descendant)。
结点M的深度(depth)就是从根结点到M的路径的长度。任何深度为d的结点的层数(level)都为d,其中根结点的层数为0,深度也为0。树的高度(height)等于最深结点的深度加1。
满二叉树(full binary tree)的每一个结点或者是一个分支结点,并恰好有两个非空子结点,或者是叶节点。
完全二叉树(complete binary tree)有严格的形状要求:从根节点起每一层从左到右填充。一棵高度为d的完全二叉树除了d-1层,每一层都是满的,且按层编号,则编号连续
特点:叶子结点只出现在最下两层,倒数第二层层叶结点集中在右边连续位置上,最底层叶结点集中在左边连续位置上。如果结点度为1,则只有左孩子;同样结点数的二叉树,完全二叉树的深度最小。
性质:二叉树第i层至多2^(i-1)个结点
深度为k的二叉树至多有2^k - 1个结点
具有n个结点的完全二叉树的深度为[logN] + 1
具有n个结点的完全二叉树的结点按层编号,如果i=1,无双亲,i>1,双亲为[i/2]
如果2i > n, 则i无左孩子,否则左孩子为2i;如果2i + 1> n, 则i无右孩子,否则右孩子为2i +1
(在堆排序中有用到,堆是一棵有序的完全二叉树)
二叉树顺序存储结构:对于深度为k的树,需要2^k - 1个存储空间,浪费空间。
二叉链表:用一个数据域加两个指针域表示结点
按照一定的顺序访问二叉树的结点,称为一次遍历(traversal),先访问结点,后访问其子结点,这种访问方法称为前序遍历(preorder traversal);后序遍历(postorder traversal)先访问结点的子结点(包括其子树),再访问该结点;中序遍历(inorder traversal)先访问左子结点(包括其子树),然后访问该结点,最后访问右子结点(包括其子树)。逐层从左至右访问结点成为层次遍历(level order traversal)。
注意:已知前序或后序和中序遍历序列可以唯一确定一棵二叉树,但前序和后序是不能确定一颗二叉树的。
完全二叉树有其实际的用途,例如堆数据结构,被用来实现优先队列和外排序算法。n个结点的完全二叉树只可能有一种形状,假设逐层而下、从左到右,其结点的位置可完全由序号确定。因此,数组可以有效地存储完全二叉树的结构,把每一个数据存放在其结点对应序号的位置上,这意味着不存在结构性开销。
二叉查找树
二叉查找树(Binary Search Tree, BST),或称二叉检索树、二叉排序树。二叉查找树的任何一个结点,设其值为K,则该结点左子树中任意一个结点的值都小于K,该结点右子树中任意一个结点的值都大于K。查找、插入和删除的时间代价均取决于对应结点的深度,最差的情况等于树的深度,故尽量保持二叉查找树的平衡。平衡二叉树每次操作的平均时间代价为O(logn),而严重不平衡的BST在最差情况下平均每次操作的时间代价为O(n)。二叉树平衡的时候其实现简单且效率高,但它为非平衡状态的可能性很大。
AVL树
具有平衡条件的二叉查找树,每个结点的左子树和右子树的高度最多差1的二叉查找树,除了插入可能破坏平衡性外,其他操作的时间复杂度均为O(logn),为了解决插入带来的失衡,可以对从插入点到根节点路径上的结点进行简单修正,称其为旋转。
B树(多路搜索树)
一个m阶B树(balanced tree of order m)定义为有以下特性:
- 根或者是一个叶结点,或者至少有两个子女(不是二叉树);
- 除了根结点以外,每个内部结点有m/2(向上取整)到m个子女;
- 所有叶结点在树结构的同一层,因此树结构总是树高平衡的。
2-3树是一个三阶B树。B树插入是2-3树插入的推广:第一步是找到应当包含待插入关键码的叶结点,并检查是否有空间;如果该结点中有地方,则插入关键码,否则把结点分裂成两个结点,并把中间的关键码提升到父结点;如果父结点也满了,就再分裂父结点,并再次提升中间的关键码。插入过程保证所有结点至少半满。
B+树
B+树只在叶结点存储记录,内部结点存储关键码值,用于引导检索,并把每个关键码与一个指向子女结点的指针关联起来。B+树的叶结点一般链接起来,形成一个双链表,这样通过访问链表中的所有叶结点,就可以按排序的次序遍历全部记录。
Huffman编码树
Huffman编码树(Huffman coding tree),简称Huffman树,是一棵满二叉树,其每个叶结点对应一个字母,叶结点的权重为对应字母的出现频率。Huffman树具有最小外部路径权重(minimum external path weight),即对于给定的叶结点集合,具有最小加权路径长度。一个叶结点的加权路径长度(weighted path length)定义为权乘以深度。
建立n个结点的Huffman树的过程很简单。首先,创建n棵初始的Huffman树,每课树只包含单一的叶结点,这n棵树按照权值(如频率)大小顺序排列;接着,拿走前两棵树(即权值最小的两棵),把它们标记为一棵新的Huffman树的一个分支结点的两个叶子结点,分支结点的权值为两个叶结点权值之和,新树放回序列中适当的位置。重复上述步骤,直至序列中只剩下一个元素,则Huffman建立完毕。
一旦Huffman树构造完成,可把每个字母用代码标记。从根结点开始,分别把“0”或“1”标于树的每条边上,“0”对应于连接左子结点的那条边,“1”则对应于连接右子结点的边。字母的Huffman编码就是从根结点到对应于该字母叶结点路径的二进制代码。对信息代码反编码的过程为:从左到右逐位判别代码串,直至确定一个字母。如果一组代码中的任何一个代码都不是另一个代码的前缀,则称这组代码符合前缀特性(prefix property),这种前缀特性保证了代码串被反编码时不会有多种可能。
附关键代码:
二叉树的前序遍历递归实现
void preOrder(TreeNode* root)
{
if (root != NULL)
{
cout << root -> val << endl;
preOrder(root -> left);
preOrder(root -> right);
}
}
二叉树的前序遍历非递归实现
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if(root == NULL)
return res;
stack<TreeNode*> st;
st.push(root);
while(! st.empty())
{
TreeNode* cur = st.top();
st.pop();
res.push_back(cur -> val);
if(cur -> right != NULL)
st.push(cur -> right);
if(cur -> left != NULL)
st.push(cur -> left);
}
return res;
}
二叉树的中序遍历递归实现
void inOrder(TreeNode* root)
{
if (root != NULL)
{
inOrder(root -> left);
cout << root -> val << endl;
inOrder(root -> right);
}
}
二叉树的中序遍历非递归实现
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if(root == NULL)
return res;
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur || !st.empty())
{
if(cur != NULL)
{
st.push(cur);
cur = cur -> left;
}
else
{
cur = st.top();
st.pop();
res.push_back(cur -> val);
cur = cur -> right;
}
}
return res;
}
二叉树的后序遍历递归实现
void postOrder(TreeNode* root)
{
if (root != NULL)
{
postOrder(root -> left);
postOrder(root -> right);
cout << root -> val << endl;
}
}
二叉树的后序遍历非递归实现
思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了 每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if(root == NULL)
return res;
stack<TreeNode*> st;
st.push(root);
TreeNode* pre = NULL;
while(! st.empty())
{
TreeNode* cur = st.top();
if((cur -> left == NULL && cur -> right == NULL) || (pre != NULL && (pre == cur -> left || pre == cur -> right)))
{
res.push_back(cur -> val);
st.pop();
pre = cur;
}
else
{
if(cur -> right)
st.push(cur -> right);
if(cur -> left)
st.push(cur -> left);
}
}
return res;
}
二叉树的后序遍历非递归实现(另一种简便方法)
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if(root == NULL)
return res;
stack<TreeNode*> st;
st.push(root);
while(!st.empty())
{
TreeNode* p = st.top();
res.push_back(p -> val);
st.pop();
if(p -> left != NULL)
st.push(p -> left);
if(p -> right != NULL)
st.push(p -> right);
}
reverse(res.begin(), res.end());
return res;
}
二叉树的层次遍历(一维输出)
vector<int> levelOrder(TreeNode* root) {
vector<int> res;
if(root == NULL)
return res;
queue<TreeNode*> q;
q.push(root);
while(!q.empty())
{
TreeNode* cur = q.front();
q.pop();
res.push_back(cur -> val);
if(cur -> left)
q.push(cur -> left);
if(cur -> right)
q.push(cur -> right);
}
return res;
}
二叉树的层次遍历(二维输出)
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> res;
if(root == NULL)
return res;
queue<TreeNode*> q1, q2;
q1.push(root);
while(!q1.empty())
{
vector<int> temp;
while(!q1.empty())
{
TreeNode* p = q1.front();
q1.pop();
temp.push_back(p -> val);
if(p -> left)
q2.push(p -> left);
if(p -> right)
q2.push(p -> right);
}
res.push_back(temp);
swap(q1,q2);
}
return res;
}