算法原理与实践(二叉树)
树的概念
树是一种能够分层储存数据的重要数据结构,树中的每个元素被称为树的节点,每个节点有若干个指针指向子节点。从节点的角度来看,树是由唯一的起始节点引出的节点集合。这个起始结点称为根(root)。树中节点的子树数目称为节点的度(degree)。
在面试中,关于树的面试问题非常常见,尤其是关于二叉树,二叉搜索树的问题。
二叉树
二叉树,是指对于树中的每个节点⽽而⾔言,⾄至多有左右两个⼦子节点,即任意节点的度⼩小于等于2
二叉树的第 i 层⾄至多有 2 ^ {i - 1}
深度为 k 的二叉树⾄至多有 2 ^ k - 1 个节点
二叉树结点常用定义,如下图所示:
二叉树概念
满二叉树(full binary tree):每个节点的子节点数为 0 或 2.(A full binary tree is a tree in which every node in the tree has either 0 or 2 children.)
完全二叉树(complete binary tree):每一层都是满的,但最后一层不一定是满的,最后一层的所有节点都尽可能地左对齐。
层数:对一棵树而言,从根节点到某个节点的路径长度称为该节点的层数(level),根节点为第 0 层,非根节点的层数是其父节点的层数加 1。
高度:该树中层数最大的叶节点的层数加 1,即相当于于从根节点到叶节点的最长路径加 1
二叉树的周游
以一种特定的规律访问树中的所有节点。常见的周游方式包括:
前序周游(Pre - order traversal):访问根结点;按前序周游左子树;按前序周游右子树。
中序周游(In - order traversal):按中序周游左子树;访问根结点;按中序周游右子树。特别地,对于二叉搜索树而言,中序周游可以获得一个由小到大或者由大到小的有序序列。
后续周游(Post - order traversal):按后序周游左子树;按后序周游右子树;访问根结点。
DFS
三种周游方式都是深度优先算法(depth - first search)
深度优先算法最自然的实现方式是通过递归实现,事实上,大部分树相关的面试问题都可以优先考虑递归。
深度优先的算法往往都可以通过使用栈数据结构将递归化为非递归实现。
层次周游
层次周游(Level traversal):首先访问第 0 层,也就是根结点所在的层;当第 i 层的所有结点访问完之后,再从左至右依次访问第 i + 1 层的各个结点。
层次周游属于广度优先算法(breadth - first search)。
Binary Tree Traversal
1. DFS 代码实现
2. 非递归代码实现
3. BFS(广度优先)
题目:构造一个如下图的二叉树,遍历并打印节点
第一阶 遍历出类似结果: 31#2##568###7##
#include <iostream> #include <string> #include <queue> #include <vector> using namespace std; const char INVALID = '#'; struct node { node() : left(nullptr), right(nullptr) {}; char val; struct node* left; struct node* right; }; class BinaryTree { public: BinaryTree(const string& initial) : root(nullptr) { string::const_iterator it = initial.cbegin(); while (it != initial.cend()) this->q.push(*it++); } void createRecursion() { this->createNodeRecursion(this->root); } void visitRecursion() { this->visitNodeRecursion(this->root); } ~BinaryTree() { //析构时释放内存,LIFO for (size_t i = this->v.size(); i != 0; i--) delete this->v[i - 1]; } private: void createNodeRecursion(struct node*& p) { if (this->q.front() == INVALID) { p = nullptr; this->q.pop(); return; } else { p = this->applyNode(); //申请节点 p->val = q.front(); this->q.pop(); this->createNodeRecursion(p->left); this->createNodeRecursion(p->right); } } void visitNodeRecursion(const struct node* p) { if (p == nullptr) { this->notify('#'); return; } else { this->notify(p->val); this->visitNodeRecursion(p->left); this->visitNodeRecursion(p->right); } } void notify(char c) { cout << c << " "; } /* 向内存池申请一块树节点 */ struct node* applyNode() { struct node* p = new node(); v.push_back(p); return p; } struct node* root; queue<char> q; vector<struct node*> v; //内存池 }; int main() { string s = "31#2##568###7##"; BinaryTree bt(s); bt.createRecursion(); bt.visitRecursion(); return 0; }
第二阶 遍历出类似效果(按路径遍历): [312][3568][357]
#include <iostream> #include <string> #include <queue> #include <utility> using namespace std; const char INVALID = '#'; struct node { node() : left(nullptr), right(nullptr) {} char val; struct node* left; struct node* right; }; class BinaryTree { public: BinaryTree(const string& initial) : root(nullptr) { string::const_iterator it = initial.cbegin(); while (it != initial.cend()) this->q.push(*it++); } void createRecursion() { this->createNodeRecursion(this->root); } void visitRecursion() { this->visitNodeRecursion(this->root); } ~BinaryTree() { //析构时释放内存池,LIFO for (size_t i = this->pool.size(); i != 0; i--) delete this->pool[i - 1]; } private: void createNodeRecursion(struct node*& p) { if (this->q.front() == INVALID) { p = nullptr; this->q.pop(); return; } else { p = this->applyNode(); p->val = q.front(); this->q.pop(); this->createNodeRecursion(p->left); this->createNodeRecursion(p->right); } } void visitNodeRecursion(const struct node* p) { if (p == nullptr) return; this->v.push_back(make_pair(p->val, 1)); if (p->left == nullptr && p->right == nullptr) { this->claimPath(); } if (p->left != nullptr && p->right != nullptr) { this->referenceAdd(); } if (p->left != nullptr || p->right != nullptr) { this->visitNodeRecursion(p->left); this->visitNodeRecursion(p->right); } } void referenceAdd() { vector<pair<char, unsigned>>::iterator it = this->v.begin(); while (it != this->v.end()) { it->second++; it++; } } void claimPath() { this->notify('['); vector<pair<char, unsigned>>::iterator it = this->v.begin(); while (it != this->v.end()) { this->notify(it->first); it->second--; if (it->second == 0) { it = this->v.erase(it); continue; } it++; } this->notify(']'); } void notify(char c) { cout << c << " "; } /* 向内存池申请一块树节点 */ struct node* applyNode() { struct node* p = new node(); pool.push_back(p); return p; } struct node* root; queue<char> q; vector<struct node*> pool; vector<pair<char, unsigned>> v; }; int main() { string s = "31#2##568###7##"; BinaryTree bt(s); bt.createRecursion(); bt.visitRecursion(); return 0; }
分治算法
分解(Divide):将原问题分解为若干子问题,这些子问题都是原问题规模较小的实例。
解决(Conquer):递归地求解各子问题。如果子问题规模足够小,则直接求解。
合并(Combine):将所有子问题的解合并为原问题的解。
i. 二分搜索
ii. 大整数乘法
iii. 归并排序
iv. 快速排序
Binary Search Tree
二分查找树(Binary Search Tree, BST)是二叉树的一种特例,对于二分查找树的任意节点,该节点储存的数值一定比左子树的所有节点的值大比右子树的所有节点的值小(该节点储存的数值一定比左子树的所有节点的值小比右子树的所有节点的值大)。
BST特性
由于二叉树第 L 层至多可以储存 2^L 个节点,故树的高度应在 logN 量级,因此,二叉搜索树的搜索效率为 O(logN) 。
当二叉搜索树退化为一个由小到大排列的单链表(每个节点只有右孩子),其搜索效率变为 O(n) 。
Balanced Binary Tree
Determine if a binary tree is a balanced tree.
一颗二叉树是平衡的,当且仅当左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
待实现 视频 二叉树(1) 19:00
更好的实现?
一种改进方式是,可以考虑利用动态规划的思想,将 TreeNode 指针作为 key,高度作为 value,一旦发现节点已经被计算过,直接返回结果,这样,level 函数对每个节点只计算一次。
另一种更为巧妙的方法是,isBalanced 返回当前节点的高度,用 - 1表示树不平衡。 将计算结果自底向上地传递,并且确保每个节点只被计算一次,复杂度 O(n)。