【数据结构】树笔记

  1. 基础知识

树,有父节点和子节点。

二叉树分为:满二叉树,完全二叉树,完美二叉树

满二叉树:在当前树中,父节点不存在缺失左儿子或右儿子的现象。

完全二叉树:基于满二叉树,当前树中从上往下,从左至右数结点,不存在跳过空结点的现象

完美二叉树:基于完全二叉树,根节点左右子树等高

  1. 二叉树

1. 创建二叉树

树的开始处是根节点,结点可能有两个子节点。

#include<iostream>
using namespace std;
template<class t>
struct p{
    t data;
    p<t> *left_p;
    p<t>*right_p;
    p(t num){
        left_p=NULL;
        right_p=NULL;
        data=num;
    }
};

然后尝试设置一个初始化树的函数。

template<class t>
class tree{
private:
    p<t> *father;
public:
    tree(t n){
        father=new p<t>(n);
    }
    ~tree(){

    }

树初始化好了,出现了new结点,所以将来还要记得把树销毁掉,即析构函数。

2. 添加新结点

首先思考需要怎样才能添加一个确定的(或者说符合用户心意的)结点。

新结点的值是必定的,还需要指定新结点的父节点,并指定是要插入父节点的左子树还是右子树。

不能让用户自己创建一个结点指定,用户的输入必须简单。这里我们假设这棵树中无重复值,所以用户需要输入的树中目标结点的值。

这里涉及到一个问题,如何找到这个目标结点。于是我们先写查找结点的函数。

3. 查找结点

遍历树:先看当前结点的左子树,看完且没有找到后再看右子树->典型的递归函数

template<class t>
class tree{
private:
    p<t> *father;
    p<t>* refind(t n,p<t>*r){
        if(r==NULL){return NULL;}
        if(r->data==n){return r;}
        p<t>*temp_p=refind(n,r->left_p);
        if(temp_p==NULL){
            temp_p=refind(n,r->right_p);
        }
        return temp_p;
    }
public:
    tree(t n){
        father=new p<t>(n);
    }
    ~tree(){

    }
    p<t>* findn(t n){
        return refind(n,father);
    }
};

回过头来说插入结点。

找到之后,首先看,如果我们要插入的是左结点,那看找到的这个结点它左结点满没满,没满,插入,满了,插入失败,插入右结点同理

int insert(t n,int flag,t num){
        p<t>*temp_p=refind(n,father);
        if(temp_p==NULL){return 0;}
        if(flag==0){
            if(temp_p->left_p==NULL){
                p<t>*new_p=new p<t>(num);
                temp_p->left_p=new_p;
                return 1;
            }
            return 0;
        }
        else{
            if(temp_p->right_p==NULL){
                p<t>*new_p=new p<t>(num);
                temp_p->right_p=new_p;
                return 1;
            }
            return 0;
        }
    }
4. 遍历二叉树

已经写完了查找,遍历是一样的。

template<class t>
class tree{
private:
    p<t> *father;
    void repre(p<t>*temp_p){
        if(temp_p==NULL ){
            return;
        }
        cout<<temp_p->data<<" ";
        repre(temp_p->left_p);
        repre(temp_p->right_p);
    }
public:
    tree(t n){
        father=new p<t>(n);
    }
    ~tree(){

    }
    void pre(){
        repre(father);
        cout<<endl;
    }
};

以上方法是前序遍历。前序遍历就是:输出顺序为:父节点,左子树,右子树

后面还有中序遍历,后序遍历。

中序遍历输出顺序为:左子树,父节点,右子树

后序遍历就是:左子树,右子树,根节点。

很明显这两个和前序遍历写法一样,除了要移动一下输出的位置。

#include<iostream>
using namespace std;
template<class t>
struct p{
    t data;
    p<t> *left_p;
    p<t>*right_p;
    p(t num){
        left_p=NULL;
        right_p=NULL;
        data=num;
    }
};
template<class t>
class tree{
private:
    p<t> *father;
    void repre(p<t>*temp_p,int n){
        for(int i=0;i<n;i++){
            cout<<" ";
        }
        if(temp_p==NULL){
            cout<<"[/]"<<endl;
            return;
        }
        cout<<temp_p->data<<endl;
        if(temp_p->left_p==NULL and temp_p->right_p==NULL){
            return;
        }
        repre(temp_p->left_p,n+2);
        repre(temp_p->right_p,n+2);
    }
    void remi(p<t>*temp_p){//中序递归
        if(temp_p==NULL){
            return ;
        }
        remi(temp_p->left_p);
        cout<<temp_p->data<<" ";
        remi(temp_p->right_p);
    }
    void relast(p<t>*temp_p){//后序递归
        if(temp_p==NULL){
            return;
        }
        relast(temp_p->left_p);
        relast(temp_p->right_p);
        cout<<temp_p->data<<" ";
    }
    p<t>* refind(t n,p<t>*r){
        if(r==NULL){return NULL;}
        if(r->data==n){return r;}
        p<t>*temp_p=refind(n,r->left_p);
        if(temp_p==NULL){
            temp_p=refind(n,r->right_p);
        }
        return temp_p;
    }
public:
    tree(t n){
        father=new p<t>(n);
    }
    ~tree(){

    }
    void preprint(){//前序
        repre(father,0);
    }
    void midprint(){//中序
        remi(father);
        cout<<endl;
    }
    void lastprint(){//后序
        relast(father);
        cout<<endl;
    }
    p<t>* findn(t n){
        return refind(n,father);
    }
    int insert(t n,int flag,t num){
        p<t>*temp_p=refind(n,father);
        if(temp_p==NULL){return 0;}
        if(flag==0){
            if(temp_p->left_p==NULL){
                p<t>*new_p=new p<t>(num);
                temp_p->left_p=new_p;
                return 1;
            }
            return 0;
        }
        else{
            if(temp_p->right_p==NULL){
                p<t>*new_p=new p<t>(num);
                temp_p->right_p=new_p;
                return 1;
            }
            return 0;
        }
    }
};
int main(){
    tree<int>t(11);
    t.insert(11,0,22);
    t.insert(11,1,33);
    t.insert(22,0,44);
    t.insert(33,0,55);
    t.preprint();
    t.midprint();
    t.lastprint();
}

虽然上述都是用递归实现的,但如果树过大,系统的栈空间已满,递归将失败。所以考虑用迭代实现。

递归其实也是用栈实现的,所以说,我们完全可以用栈来代替递归过程

前序:(翻译一下递归的实现方式就好了)

void repre(){
        stack<p<t>*>s;
        s.push(father);
        while(!s.empty()){
            p<t> *temp_p=s.top();
            s.pop();
            cout<<temp_p->data<<" ";
            if(temp_p->right_p!=NULL){
                s.push(temp_p->right_p);
            }
            if(temp_p->left_p!=NULL){
                s.push(temp_p->left_p);
            }
        }
    }

中序:一个数要访问两次才能输出,所以再拿一个栈存。不管右结点有没有,都把它加入。

翻译一下当时做递归中序时的方法:只有空的时候才返回,说迭代的语言就是,只有空的时候才弹栈。

(这样才能越级输出根节点)

void remid(){
        stack<p<t>*>s;//存放第一次遍历的结点
        stack<p<t>*>s1;//存放第二次遍历的结点
        s.push(father);
        while(!s.empty()){
            p<t>*temp_p=s.top();
            s.pop();
            if(temp_p==NULL){
                if(!s1.empty()){
                    cout<<s1.top()->data<<" ";
                    s1.pop();
                }
            }
            else if (temp_p->left_p==NULL and temp_p->right_p==NULL) {
                cout<<temp_p->data<<" ";
                cout<<s1.top()->data<<" ";
                s1.pop();
            }
            else{
                s1.push(temp_p);
                s.push(temp_p->right_p);
                s.push(temp_p->left_p);
            }
        }

后序遍历的迭代方法和中序遍历是一样的,关键点是处理什么时候彻底弹栈(即弹出s1栈)。本来想设置两个栈的,后来意识到其实我们是没有办法区分左右子树的。也就是,弹主栈的操作遇到两次,s1就要弹出一个。可以自己画图试着理解。

void relast(){
        stack<p<t>*>s;
        stack<p<t>*>s1;
        int cont=0;
        s.push(father);
        while(!s.empty()){
            p<t>*temp_p=s.top();
            s.pop();
            if(temp_p==NULL){
                cont++;
                if(cont==2){
                    if(!s1.empty()){
                        cout<<s1.top()->data<<" ";
                        s1.pop();
                        cont=1;
                    }
                }
            }
            else{
                s1.push(temp_p);
                s.push(temp_p->right_p);
                s.push(temp_p->left_p);
            }
        }
        cout<<endl;
    }

层序遍历:一层一层从左到右读出节点的值

void relevel(){
        queue<p<t>*>s;
        s.push(father);
        while(!s.empty()){
            if(s.front()==NULL){
                s.pop();
                continue;
            }
            cout<<s.front()->data<<" ";
            s.push(s.front()->left_p);
            s.push(s.front()->right_p);
            s.pop();
        }
        cout<<endl;
    }

我觉得周老师有一句话讲的很好:在动手决定数据结构之前,先思考一下人工是怎么实现的。访问父节点,接下来是它的左儿子,然后是它的右儿子。再接下去是左儿子的左儿子,左儿子的右儿子,右儿子的左儿子,右儿子的右儿子。第二次先访问了左儿子,第三次还是得从左儿子的左儿子开始。所以是:先进先出——>队列。

  1. 二叉查找树(BST)

这棵树左节点小于根节点,右节点大于根节点。

所以看起来,找的时候只要向一边找就行了,每次舍弃掉一半的点,算法课算过,效率是O(lgn)。

但是如果树长得很丑,比如全部只有右子树(退化成链表),这样就不存在舍弃点,所以效率就退化回O(n)

  1. 找最大值/最小值/某个值

以找最大值为例,和普通二叉树一样,可以用迭代,也可以用递归。我们之前讲过,首先思考它是怎么做的,然后再选择方法。左小右大,所以找最大值只需要一路向右,直到该节点没有右节点。这个是迭代可以轻松解决的,不过递归也可以。但是递归存在一个系统栈空间的限制,从时间、空间上优先选择迭代。

找最小值和找指定值同理。

template <class t>
class bst:public tree<t>{
protected:
public:
    bst(){
        this->father=NULL;
    }
    p<t>* findmax(){
        p<t>*temp_p=this->father;
        if(temp_p==NULL){return NULL;}
        while(temp_p->right_p!=NULL){
            temp_p=temp_p->right_p;
        }
        return temp_p;
    }
    p<t>* findmin(){
        p<t>*temp_p=this->father;
        if(temp_p==NULL){return NULL;}
        while(temp_p->left_p!=NULL){
            temp_p=temp_p->left_p;
        }
        return temp_p;
    }
    p<t>*findx(t x){
        p<t>*temp_p=this->father;
        while(true){
            if(temp_p==NULL){return NULL;}
            if(temp_p->data==x){return temp_p;}
            else if (temp_p->data>x){
                temp_p=temp_p->left_p;
            }
            else{
                temp_p=temp_p->right_p;
            }
        } 
    }
};
  1. 插入某个值

两种方法,迭代、递归都可以。

这是迭代:

p<t>*insert(t n){
        p<t>*temp_p=father;
        if(father==NULL){
            father=new p<t>(n);
            return father;
        }
        while(true){
            if(n>temp_p->data){
                if(temp_p->right_p==NULL){
                    p<t>* new_p=new p<t>(n);
                    temp_p->right_p=new_p;
                    return temp_p->right_p;
                }
                else{
                    temp_p=temp_p->right_p;
                }
            }
            else if(n==temp_p->data){
                return temp_p;
            }
            else{
                if(temp_p->left_p==NULL){
                    p<t>* new_p=new p<t>(n);
                    temp_p->left_p=new_p;
                    return temp_p->left_p;
                }
                else{
                    temp_p=temp_p->left_p;
                }
            }
        }
    }
};

为了给后序的VAL树做铺垫,再用递归写一次。

同样,由于是递归需要用到指针的移动,作为类封装,不能让用户自己创建指针变量指向根节点。所以写一个递归封装在private中防止用户调用。

p<t> *m_insert(t n,p<t>*temp_p){
        if(n>temp_p->data){
            if(temp_p->right_p==NULL){
                temp_p->right_p=new p<t>(n);
                return temp_p->right_p;
            }
            else{
                m_insert(n,temp_p->right_p);
            }
        }
        else if (n==temp_p->data){
            return temp_p;
        }
        else{
            if(temp_p->left_p==NULL){
                temp_p->left_p=new p<t>(n);
                return temp_p->left_p;
            }
            else{
                m_insert(n,temp_p->left_p);
            }
        }
    }

然后在public执行以上递归函数

p<t>*insert(t n){
        if(father==NULL){
            father=new p<t>(n);
            return father;
        }
        return m_insert(n,father);
    }

需要注意的是,如果根节点为空,需要单独拿出来讨论。

  1. 删除某个值
  1. 假如删除的这个结点下无任何儿子结点,删除则删除。

  1. 假如该结点下有一个儿子结点,儿子顶上。

  1. 假如该结点下有两个儿子结点,如何在不影响二叉树结构的情况下填补窟窿?

=>这个窟窿必须由一个大于该结点右儿子且小于该结点左儿子的结点填上,且该结点要易于操作,不能是有两个儿子的(总不能拆东墙补西墙吧!)。由二叉树性质可知,左子树的最大值一定小于右子树的顶端。反之亦然。而之前我们查找过最大值和最小值,可以知道,如果该结点是最大结点,则该结点下一定无右子树,最小亦然。

=>以左子树为例,可以将左子树最大值结点填上要被删除的结点(直接赋值),将最大结点的左子树接到最大结点的父节点上。

=>不想为了父节点再遍历,怎么办

=>递归,返回值是子节点。父节点每次都去remove一下可能会被修改的子节点,不修改,返回的就是原来的点,修改了,返回的就是修改过后的新子树根。

主旨:只要抓住了树根,就获得了一棵树!!!

首先是放在private中的递归

p<t>*remove(p<t>*temp_p,t n){
        if(temp_p==NULL)throw -1;
        if(temp_p->data>n){
            temp_p->left_p=remove(temp_p->left_p,n);
        }
        else if (temp_p->data<n){
            temp_p->right_p=remove(temp_p->right_p,n);
        }
        else{
            p<t>*tmp;
            if(temp_p->left_p!=NULL and temp_p->right_p!=NULL){
                tmp=findmax(temp_p->left_p);
                temp_p->data=tmp->data;
                temp_p->left_p=remove(temp_p->left_p,temp_p->data);
            }
            else{
                tmp=temp_p;
                if(temp_p->left_p!=NULL){
                    temp_p=temp_p->left_p;
                }
                else{
                    temp_p=temp_p->right_p;
                }
                delete(tmp);
            }
        }
        return temp_p;
    }

当找不到这个点,删除不了时,public中的函数就报异常。

bool remove(t n){
        try{
            remove(father,n);
        }
        catch(int e){
            return false;
        }
        return true;
   }
  1. 平衡二叉树AVLtree

  1. 首先确定一点,平衡二叉树是基于二叉查找树的。二者的区别就是,平衡二叉树要确保它的每一个结点都是平衡的。平衡是指:该结点左右子树的高度差不会大于1。

  1. 根据不同的失衡模型,我们的模型一共有4种:LL,LR,RR,RL。解决方法模型网上动图很多,可以自己画画理解。

  1. 初始化AVL

不太会用继承就是说。。。重写了一个bst。这里由于是平衡二叉树要判断是否平衡,所以会涉及到每个结点的高度。不妨修改原来的node,添加一个属性h,记录每个结点的高度。另外添加一个私有函数求每个结点的高度(因为输入是node*,用户也不需要调用,秉持面向对象的原则,将其封装进private)

#include<iostream>
#include<algorithm>
using namespace std;
struct node{
    int num;
    node *right_n;
    node *left_n;
    int h;
    node(int n){
        num=n;
        right_n=NULL;
        left_n=NULL;
        h=0;
    }
};
class AVLtree{
private:
    int height(node *temp_p){
        if(temp_p==NULL){return -1;}
        return temp_p->h;
    }
public:
    AVLtree(){
        root=NULL;
    }
    ~AVLtree(){

    }
}
  1. AVL的插入

插入的基本部分是和bst一样的,都需要按顺序插入。但是有一个问题:插入之后,有没有可能失衡呢。

如果失衡,我们该在哪里调整呢?

在说二叉查找树的时候就有一点:抓住了树根就抓住了整棵树。

另外一点:对于递归函数,你要坚信它可以完成任务 => 这里的任务是:插入一个新结点。

所以插入一个新结点后(或者说,执行了插入函数)就要检查一下height。

失衡=>bst中是用右子树和左子树分开来插入的,要检查插入的是该子树的右还是左,对应那四种中的一个

以下是private中的。

node* rinsert(int n,node*temp_p){
        if(temp_p==NULL){
            temp_p=new node(n);
            return temp_p;
        }
        if(temp_p->num==n){throw -1;}
        else if(temp_p->num>n){
            temp_p->left_n=rinsert(n,temp_p->left_n);  
            if(height(temp_p->left_n)-height(temp_p->right_n)==2){
                if(n<temp_p->left_n->num){
                    temp_p=LLrotate(temp_p);
                }
                else{
                    temp_p=LRrotate(temp_p);
                }
            } 
        }
        else{
            temp_p->right_n=rinsert(n,temp_p->right_n);
            if(height(temp_p->left_n)-height(temp_p->right_n)==-2){
                if(n>temp_p->right_n->num){
                    temp_p=RRrotate(temp_p);
                }
                else{
                    temp_p=RLrotate(temp_p);
                }
            }   
        }
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        return temp_p;
    }

这个时候没有写LLrotate,LRrotate等等。不着急

3. 4种旋转

LL旋转,是把失衡结点的左儿子提上去,左子树的左儿子因此提高一层(失衡解决),失衡结点变成左儿子的右儿子,左子树的右儿子给失衡结点当左儿子。

RR对称。

LR旋转,自己仔细拆开来画画就会发现,是一个对失衡结点的左儿子进行一个RR旋转后,再对失衡结点进行LL旋转。

RL对称。

以下我也放在了private中,其实可以丢到public里去(好像也没什么必要)

node *LLrotate(node *temp_p){
        node* child=temp_p->left_n;
        temp_p->left_n=child->right_n;
        child->right_n=temp_p;
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        child->h=max(height(child->left_n),height(child->right_n))+1;
        return child;
    }
    node *LRrotate(node *temp_p){
        temp_p->left_n=RRrotate(temp_p->left_n);
        temp_p=LLrotate(temp_p);
        return temp_p;
    }
    node *RRrotate(node *temp_p){
        node *child=temp_p->right_n;
        temp_p->right_n=child->left_n;
        child->left_n=temp_p;
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        child->h=max(height(child->left_n),height(child->right_n))+1;
        return child;
    }
    node *RLrotate(node *temp_p){
        temp_p->right_n=LLrotate(temp_p->right_n);
        temp_p=RRrotate(temp_p);
        return temp_p;
    }

最后放一下AVL完整代码(包含测试)

#include<iostream>
#include<algorithm>
using namespace std;
struct node{
    int num;
    node *right_n;
    node *left_n;
    int h;
    node(int n){
        num=n;
        right_n=NULL;
        left_n=NULL;
        h=0;
    }
};
class AVLtree{
private:
    node *root;
    node *LLrotate(node *temp_p){
        node* child=temp_p->left_n;
        temp_p->left_n=child->right_n;
        child->right_n=temp_p;
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        child->h=max(height(child->left_n),height(child->right_n))+1;
        return child;
    }
    node *LRrotate(node *temp_p){
        temp_p->left_n=RRrotate(temp_p->left_n);
        temp_p=LLrotate(temp_p);
        return temp_p;
    }
    node *RRrotate(node *temp_p){
        node *child=temp_p->right_n;
        temp_p->right_n=child->left_n;
        child->left_n=temp_p;
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        child->h=max(height(child->left_n),height(child->right_n))+1;
        return child;
    }
    node *RLrotate(node *temp_p){
        temp_p->right_n=LLrotate(temp_p->right_n);
        temp_p=RRrotate(temp_p);
        return temp_p;
    }
    node* rinsert(int n,node*temp_p){
        if(temp_p==NULL){
            temp_p=new node(n);
            return temp_p;
        }
        if(temp_p->num==n){throw -1;}
        else if(temp_p->num>n){
            temp_p->left_n=rinsert(n,temp_p->left_n);  
            if(height(temp_p->left_n)-height(temp_p->right_n)==2){
                if(n<temp_p->left_n->num){
                    temp_p=LLrotate(temp_p);
                }
                else{
                    temp_p=LRrotate(temp_p);
                }
            } 
        }
        else{
            temp_p->right_n=rinsert(n,temp_p->right_n);
            if(height(temp_p->left_n)-height(temp_p->right_n)==-2){
                if(n>temp_p->right_n->num){
                    temp_p=RRrotate(temp_p);
                }
                else{
                    temp_p=RLrotate(temp_p);
                }
            }   
        }
        temp_p->h=max(height(temp_p->left_n),height(temp_p->right_n))+1;
        return temp_p;
    }
    int height(node *temp_p){
        if(temp_p==NULL){return -1;}
        return temp_p->h;
    }
    void rprint(node *temp_p,int n){
        for(int i=0;i<n;++i){
            cout<<" ";
        }
        if(temp_p==NULL){cout<<"[/]"<<endl;return ;}
        cout<<temp_p->num<<endl;
        rprint(temp_p->left_n,n+2);
        rprint(temp_p->right_n,n+2);
    }
public:
    AVLtree(){
        root=NULL;
    }
    ~AVLtree(){

    }
    bool insert(int n){
        try{
            root=rinsert(n,root);
        }
        catch(int e){
            return false;
        }
        return true;
    }
    void print(){
        rprint(root,0);
    }
};
int main(){
    AVLtree tree;
    tree.insert(50);
    tree.insert(30);
    tree.insert(60);
    tree.insert(20);
    tree.insert(35);
    tree.insert(33);
    tree.print();
}

5. 什么是面向对象(个人理解)

这几天复习数据结构,先用c写的,再用c++写的,面向对象相当于一个完整的类的封装。在用户眼里只有这个概念模型,不会关心具体是怎么实现的,只会关心有哪些功能,而模型具体如何实现也不会体现在模型附带的函数中。相当于将这个模型抽象出来了。比如栈,我们知道是后进先出,知道push等方法,但不会关心它究竟是用链表还是用数组实现的,传入的参数也不会体现栈的实现方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值