目录
1 二叉树
1.1 树的定义
- 树(tree)是包含n(n>=0)个结点的有穷集,其中:
- 每个元素称为结点(node);
- 有一个特定的结点被称为根结点或树根(root)。
- 除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
1.2 二叉树
- 二叉树定义:每个节点最多有2个子节点的数
- 性质:
- (1)二叉树第i层的最大节点数为;
- (2)深度为k(第0层深度为1)的二叉树有最大节点数;
- (3)度为2的非叶节点个数+1 = 叶节点个数
1.3 完全二叉树
- 满二叉树定义:除了叶节点的所有节点都有左右子节点的二叉树
- 完美二叉树定义:叶节点都位于同一层上的满二叉树
- 完全二叉树定义:对应元素的位置跟完美二叉树相同的二叉树
2 二叉搜索树
- 定义:满足以下条件的二叉树
- (1)非空左子树的键值小于当前节点键值;
- (2)非空右子树的键值小于当前节点键值;
- (3)每棵子树都是二叉搜索树。
2.1 插入节点
- 二叉搜索树插入节点时,主要考虑的问题是维护整棵树的有序性,即保证上面定义中的性质。
- 插入节点的流程(迭代思想):
- 当前节点是否为空:如果是,返回一个值为新元素的新节点指针;
- 判断插入的元素是否比当前节点的元素大,如果是,将其插入至右子树;
- 判断插入的元素是否比当前节点的元素小,如果是,将其插入至左子树;
- 判断插入的元素是否等于当前节点元素,如果是,返回当前节点。
- 用迭代方法实现(以下包含AVL树的平衡调整):
TreeNode* insert_(TreeNode* node, T element){
if(node == nullptr){ //如果当前树是空树:直接返回一个新节点
return new TreeNode(element);
}
if(node->data < element) { //如果元素比当前节点元素大:插入到右子树
node->rightChild = insert_(node->rightChild, element);
if(getHeight_(node->leftChild) - getHeight_(node->rightChild) == -2){ //如果不平衡
if(element < node->rightChild->data ){ // RL型
node = rightLeftRotate_(node);
}else{ //RR型
node = rightRotate_(node);
}
}
}else if(node->data > element){ //如果元素比当前节点元素小:插入到左子树
node->leftChild = insert_(node->leftChild, element);
int a = getHeight_(node->leftChild);
int b = getHeight_(node->rightChild);
if((getHeight_(node->leftChild) - getHeight_(node->rightChild)) == 2){ //如果不平衡
if(element > node->leftChild->data ){ // LR型
node = leftRightRotate_(node);
}else{ //LL型
node = leftRotate_(node);
}
}
}
return node;
}
2.2 搜索节点
- 搜索流程:
- (1)要找的节点键值是不是比当前节点键值小:如果是,去左子树找;如果不是,去右子树找
- (2)如果当前节点键值就是目标键值,则返回true;
- (3)如果遍历完了都没有,就返回false
bool find(const T& data){
TreeNode *tmp = root_;
while(tmp != nullptr){
if(tmp->data > data){
tmp = tmp->leftChild;
}else if(tmp->data == data){
return true;
} else{
tmp = tmp->rightChild;
}
}
return false;
}
2.3 删除节点
TreeNode* deleteElement(TreeNode *node, T data){
if(node == nullptr){ //如果是空树
return nullptr;
} else{ //如果不是空树
if(node->data == data){ //如果当前节点是目标节点
if(node->rightChild != nullptr && node->leftChild != nullptr){ //如果这个节点有两个子节点
TreeNode* tmp = findMax(node->leftChild);
node->data = tmp->data; //用左子树中最大数据代替这个元素的数据
node->leftChild = deleteElement(node->leftChild, tmp->data); //删除左子树中最大数据节点
} else if(node->leftChild != nullptr || node->rightChild != nullptr){ //如果这个节点有一个子节点
TreeNode* tmp = node->leftChild == nullptr ? node->rightChild : node->leftChild;
delete node;
return tmp;
}else{ //如果这个节点没有子节点
delete node;
return nullptr;
}
} else { //如果当前节点不是目标节点
if(data > node->data){
node->rightChild = deleteElement(node->rightChild, data);
}else{
node->rightChild = deleteElement(node->leftChild, data);
}
return node;
}
}
}
2.4 遍历
void firstOrderTraversal( TreeNode* node, std::ostream &os){
if(node == nullptr)return;
os << " "<< node->data;
firstOrderTraversal(node->leftChild, os);
firstOrderTraversal(node->rightChild, os);
}
2.4.2 中序遍历
void midOrderTraversal(TreeNode* node, std::ostream &os){
if(node == nullptr)return;
midOrderTraversal(node->leftChild, os);
os << " " << node->data;
midOrderTraversal(node->rightChild, os);
}
2.4.3 后序遍历
void lastOrderTraversal( TreeNode* node, std::ostream &os){
if(node == nullptr)return ;
lastOrderTraversal(node->leftChild, os);
lastOrderTraversal(node->rightChild, os);
os << " " << node->data ;
}
2.4.4 层次遍历
void levelOrderTraversal(TreeNode* node, std::ostream &os){
if(node == nullptr)return;
std::queue<TreeNode*> queue;
queue.push_back(root_);
while(queue.front() != nullptr){
TreeNode* tmp = queue.pop();
os << tmp->data <<" ";
queue.push(tmp->leftChild);
queue.push(tmp->rightChild);
}
}
3 自平衡二叉查找树(AVL树)
3.1 需要调整的情况以及方法
3.2 LL、RR、RL、LR实现
TreeNode* rightRotate_(TreeNode* n3){
//1.获取n2的指针
TreeNode* n2 = n3->rightChild;
//2.n2的左子树成为n3右子树
n3->rightChild = n2->leftChild;
//3.n3成为n2左子树
n2->leftChild = n3;
return n2;
}
TreeNode* leftRightRotate_(TreeNode* n3){
//1.n1对n2(n3的左子树)进行右单旋
n3->leftChild = rightRotate_(n3->leftChild);
//2.n1对n3进行左单旋
return leftRotate_(n3);
}
TreeNode* rightLeftRotate_(TreeNode* n3){
//1.n1对n2(n3的右子树)进行左单旋
n3->rightChild = leftRotate_(n3->rightChild);
//2.n1对n3进行右单旋
return rightRotate_(n3);
}
TreeNode* leftRotate_(TreeNode* n3){
//1.获取n2的指针
TreeNode* n2 = n3->leftChild;
//2.n2的右子树成为n3左子树
n3->leftChild = n2->rightChild;
//3.n3成为n2右子树
n2->rightChild = n3;
return n2;
}
4 堆(heap)——以最大堆为例
4.1 堆的定义
- 堆:每个节点的键值都大于(或小于)所有子节点键值的完全二叉树
- 最大堆:每个节点的键值都大于所有子节点键值的堆
- 最小堆:每个节点的键值都小于所有子节点键值的堆
- 堆的性质:所有子节点的键值不大于父节点键值的值
4.2 堆的实现
4.2.1 用数组实现堆
- 由于堆是一种特殊的完全二叉树,且近似于满二叉树(自左向右填充),因此可用数组实现堆
- 用数组实现堆时。需要维护的变量如下:
private:
T* _data; //各节点键值
size_type _capacity; //堆最大容量
size_type _size; //堆当前大小
-
_data是存放节点数据的数组
- _capacity是最大容量,在申请内存时要多申请一个T类型的空间,原因下面解释:
4.2.3 访问父节点、子节点
- 如果让堆顶元素的引索为1,则号节点的父节点为,左子节点为,右子节点为
- 因此申请内存时要多申请一个
4.2.4 入堆
- 入堆流程如下:
- (1)新加入的元素放在最后一个节点的后面:即_size+1处
- (2)判断这个新加入元素和父节点的关系:若大于父节点,则与父节点交换位置;否则该元素留在当前位置,完成入堆;
- (3)交换位置后,继续与当前的父节点比较;直至遇到比这个元素大的父节点或该节点已到堆顶(到了_data[1]的位置),则完成入堆。
- 以上步骤由_shiftUp函数完成
bool push(T element){
if(_size == _capacity)return false;
_data[++_size] = element;
_shiftUp();
return true;
}
void _shiftUp() {
size_type index = _size;
size_type nextIdx;
while(index != 1){
nextIdx = _getParentNodeIndex(index);
if( _data[index] > _data[nextIdx] ){
_swap(index, nextIdx);
index = nextIdx;
}else{
break;
}
}
}
4.2.5 出堆
- 出堆流程如下:
- (1)用临时变量保存堆顶元素(_data[1])
- (2)用堆尾元素代替堆顶元素,称为新的堆顶元素
- (3)新堆顶元素与子节点对比(这里涉及三种情况:左右子节点的引索都没超出当前堆大小、左子节点引索超出当前堆大小、没有子节点,具体处理见代码),若比两个子节点中的较小值小,则与该节点交换位置;否则该元素留在这个位置,并跳过第(4)步
- (5)交换位置后,继续与当前子节点对比,直到该节点比两个子节点大或无子节点为止。
- (6)返回(1)中的临时变量
T pop(){
if(_size == 0)throw std::runtime_error("Empty heap.");
T ret = _data[1];
_data[1] = _data[_size--];
_sink();
return ret;
}
void _sink() {
size_type index = 1;
//三种情况:(1)左子节点在范围内,右节点不在;(2)左右节点都在范围内;(3)子节点不在范围内(不用处理)
while(_getLeftChildNodeIndex(index) <= _size ){ //while处理(1)、(2)两种情况
size_type nextIdx;
if(_getRightChildNodeIndex(index) <= _size){ //(2)情况
nextIdx = _data[_getRightChildNodeIndex(index)] > _data[_getLeftChildNodeIndex(index)] ?
_getRightChildNodeIndex(index) : _getLeftChildNodeIndex(index);
}else{ //(1)情况
nextIdx = _getLeftChildNodeIndex(index);
}
if(_data[index] < _data[nextIdx]){
_swap(index, nextIdx);
index = nextIdx;
}else{ //比子节点都大(包括(3))
break;
}
}
}
5 哈夫曼树(Huffman Tree)、哈夫曼编码
5.1 哈夫曼树
- 哈夫曼树定义:带权路径长度最小的二叉树(也称为最优二叉树)
5.2 哈夫曼树的构造
- 构造流程:
- (1)将所有目标元素存放在一个个哈夫曼树节点中,这些节点保存在一个最小堆中;
- (2)创建一个新的哈曼夫树节点,弹出堆的顶部两个节点,分别设为该新节点的左右子树;
- (3)将该新节点放入堆中。
- (4)重复(2),直至堆中只剩一个元素,这个元素就是哈夫曼树的根节点。
(实现放在哈夫曼编码中一起讲)
5.3 哈夫曼编码
5.2.1 主要思想
- 当一段文本(如电报)中有一些字符出现频率较高时(比如"ABCABCABCQUAAAA",ABC字符的比例明显高于其他),可以对这些字符使用一些较短的编码,以达到压缩编码的目的。
5.2.2 编码方法
HuffmanTree(initializer_list<elementType> initList){
vector<_TreeNode*> parametersVector;
//获得参数
for(auto e : initList){
parametersVector.push_back(new _TreeNode(e));
}
//构造Huffman树
auto lessThan = [](_TreeNode* lhs, _TreeNode* rhs)->bool {return !(lhs->element.second < rhs->element.second);}; //make_heap
make_heap(parametersVector.begin(), parametersVector.end(), lessThan); //根据键值创建最小堆
while(parametersVector.size() > 1){
auto huffmanTree = new _TreeNode();
huffmanTree->left = parametersVector.front();
huffmanTree->left->code = "0";
pop_heap(parametersVector.begin(), parametersVector.end(), lessThan);
parametersVector.pop_back();
huffmanTree->right = parametersVector.front();
huffmanTree->right->code = "1";
pop_heap(parametersVector.begin(), parametersVector.end(), lessThan);
parametersVector.pop_back();
huffmanTree->element.second = (huffmanTree->right->element.second + huffmanTree->left->element.second);
parametersVector.push_back(huffmanTree);
push_heap(parametersVector.begin(), parametersVector.end(), lessThan);
}
_root = parametersVector[0];
_root->code = "";
}
string getCode(_TreeNode* node, char c){
if(node == nullptr)return ""; //如果是空节点
if(node->left == nullptr && node->right == nullptr){ //如果是子节点
if(node->element.first != c)return "";//不是目的字符
else return node->code;
}else{ //如果不是子节点
string ret = getCode(node->left, c) + getCode(node->right, c);
if(ret == "")return ret; //如果左子树加右子树
else{
return node->code + ret;
}
}
6 红黑树(留坑)
网上资料说红黑树也是一种平衡树,但是一种不太严格的平衡树;AVL树每次插入或删除都有可能导致树结构的调整,而红黑树不会,所以红黑树比AVL树更适合频繁插入删除的场合(但搜索效率较AVL低)。这里先留一个坑,以后再补上~