【Data Structure】二叉搜索树 [part 1]

二叉搜索树 [part 1]

●二分查找法

●二叉搜索树

●插入新节点(递归实现)

●查找

●前中后序遍历

●层序遍历(广度优先遍历)

●删除节点


1  二分查找法

二分查找法只能作用在一个有序的序列中,时间复杂度为O(logn)

/*
非递归形式实现二分查找
在有序数组arr中,查找target
如果找到target,返回相应的索引index,如果没有找到target,返回-1
*/
template <typename Key, typename Value>
int binarySearch(T arr[], int n, T target){
    int l = 0, r = n - 1;//在[l, r]左闭右闭区间之中查找target,注意:r = n - 1
    while(l <= r){//注意:循环终止条件为l<=r,而不是<
        int mid = l + (r - l)/2;//避免l+r整型溢出问题
        if(arr[mid] == target)
            return mid;
        if(arr[mid] < target)
            l = mid + 1;
        else
            r = mid - 1;
    }
    return -1;//target不在数组中
}
//递归形式实现二分查找
template <typename Key, typename Value>
int __binarySearch(T arr[], int l, int r, T target){//和非递归形式不一样,这里直接传入定义好的l和r
    if(l > r)//和非递归中的循环终止条件含义相同(一个是l <= r时继续, 另一个是l > r时终止  )
        return -1;

    int mid = l + (r-l)/2;
    if(arr[mid] == target)
        return mid;
    if(arr[mid] > target)
        __binarySearch(arr, 0, mid-1, target);
    else
        __binarySearch(arr, mid+1, r, target);
}

template<typename T>
int binarySearch2(T arr[], int n, T target){
    //写成两个函数是为了解决非递归中存在的索引从0到n-1的问题
    return __binarySearch(arr, 0, n-1, target);
}

利用100W量级数据进行测试,得到两种方法的时间差距为:

2  二叉搜索树

2.1  优势

查找表”(字典)是一种将数据以“键值对”的形式存在的数据结构,通常使用二叉搜索树实现
如果使用数组实现查找表,会存在以下缺点:
1、必须使用整数来表示key值
2、当key值较为稀疏时,使用该数组在空间上不经济

不同数据结构实现查找表的时间复杂度对比:

  查找元素 插入元素 删除元素
普通数组 O(n) O(n) O(n)
顺序数组 O(logn) O(n) O(n)
二分搜索树 O(logn) O(logn) O(logn)

(对于顺序数组的查找,可以使用二分查找法,在O(logn)的时间复杂度内完成)

二叉搜索树实现查找表的优点:

1、在查找、插入、删除数据上的高效

2、可以方便地回答很多数据之间的关系问题,例如:min、max、floor(前驱)、ceil(后继)、rank(排名)、select(选择排名第X位的数据)

2.2  定义

1、二叉搜索树首先是一棵二叉树

2、每个节点的键值都大于左孩子,每个节点的键值都小于右孩子

3、以左右孩子为根的子树仍然是一棵二叉搜索树

注意:二叉搜索树不一定是一棵完全二叉树

2.3  初始化

template <typename Key, typename Value>
class BST{
private:
    struct Node{
        Key key; 
        Value value;
        Node *left;
        Node *right;
        Node(Key key, Value value){//结构体的构造函数
            this->key = key;
            this->value = value;
            this->left = this->right = NULL;//左右孩子初始化为空
        }
    };
    Node *root;//根节点
    int count;//二叉搜索树的节点数
    
public:
    BST(){//构造函数
        root = NULL;
        count = 0;
    }
    ~BST(){//析构函数
        //TODO: ~BST()
    }
    int size(){//返回二叉搜索树的节点数
        return count;
    }
    bool isEmpty(){//判断二叉搜索树是否为空
        return count == 0;
    }
};

3  插入新节点(递归实现)

不断与根节点作比较,小于则递归左子树,大于则递归右子树,直到遇见空的可插入左/右子树(遇见相同值则覆盖)

template <typename Key, typename Value>
class BST{
......
public:
    ...
    void insert(Key key, Value value){
    //首先在类中定义一个public的insert()方法,用于调用private的insert()方法
        root = insert(root, key, value);
    }

private:
    //向以node为根的二叉搜索树中插入节点(key, value),返回插入新节点后的二叉搜索树的根
    //把向整棵二叉树中插入新节点利用递归转化为向子树中插入新节点
    Node* insert(Node *node, Key key, Value value){
        
        if(node == NULL){//当节点为空,传入参数key和value,创建一个新节点,总节点数加 1
            count++;
            return new Node(key, value);
        }
        if(key == node->key)//如果key相同,则覆盖value的值
            node->value = value;
        else if(key < node->key)//小于则递归插入左子树
            node->left = insert(node->left, key, value);
        else                    //大于则递归插入右子树
            node->right = insert(node->right, key, value);
        return node;
    }
};

4  查找

查找操作与插入操作类似,不同点在于如果最终查找到了一个空节点,那么代表查找失败,元素不存在与该二叉搜索树中。
二叉搜索树的包含contain和查找search同质(contain:在二叉搜索树中是否包含键值为key的元素)

template <typename Key, typename Value>
class BST{
......
public:
    ...
    bool contain(Key key){//新建一个新的public函数contain(),调用同名private递归函数contain()
        return contain(root, key);
    }

private:
    ...
    //查看以node为根的二叉搜素树中是否包含键值为key的节点
    bool contain(Node* node, Key key){
        
        if(node == NULL)//找不到键值为key的节点,返回false
            return false;
            
        if(key == node->key)//找到了键值为key的节点,返回true
            return true;
        else if(key < node->key)
            return contain(node->left, key);
        else  //key > node->key
            return contain(node->right, key);
    }
}; 

在写search()函数的时候,需要考虑search()函数的返回值是什么。
1、如果返回的是所查找到的节点,那么就需要将类中定义的private类型的Node结构体改为public类型,这就无法将数据结构隐藏,与封装的设计理念不符
2、如果返回的是键值key所指向的value的值,在search()函数查找不到的情况下,会出现value为空的情况。为了避免这种情况,这里可以通过先执行contain()函数,来确定键值key所指向的value的值是存在的,再进行查找操作
这里采用返回一个value*的形式,value*作为一个指针,可以存储一个空元素,如果查找失败,返回NULL,如果查找成功,返回的就是键值key指向的value的指针,用户在方法外部也可以通过该指针修改该元素

template <typename Key, typename Value>
class BST{
......
public:
    ...
    Value* search(Key key){
        return search(root, key)
    }

private:
    ...
    //在以node为根的二叉搜索树中查找key所对应的value
    Value* search(Node* node, Key key){
        if(node == NULL)
            return NULL;
        
        if(key == node->key)
        //由于函数返回值为指向地址的指针,所以需要返回节点的value值所对应的地址
            return &(node->value);
        else if (key < node->key)
            return search(node->left, key);
        else   //key > node->key
            return search(node->right, key);
    }
};   

5  前中后序遍历

前序遍历:先访问当前节点,再依次递归访问左右子树
中序遍历:先递归访问左子树,再访问自身,再递归访问右子树
后序遍历:先递归访问左右子树,再访问自身节点

template <typename Key, typename Value>
class BST{
......
public:
    ...
    // 前序遍历
    void preOrder(){
        preOrder(root);
    }

    // 中序遍历
    void inOrder(){
        inOrder(root);
    }

    // 后序遍历
    void postOrder(){
        postOrder(root);
    }

private:
    ...
    // 对以node为根的二叉搜索树进行前序遍历
    void preOrder(Node* node){

        if( node != NULL ){
            cout<<node->key<<endl;
            preOrder(node->left);
            preOrder(node->right);
        }
    }

    // 对以node为根的二叉搜索树进行中序遍历
    void inOrder(Node* node){

        if( node != NULL ){
            inOrder(node->left);
            cout<<node->key<<endl;
            inOrder(node->right);
        }
    }

    // 对以node为根的二叉搜索树进行后序遍历
    void postOrder(Node* node){

        if( node != NULL ){
            postOrder(node->left);
            postOrder(node->right);
            cout<<node->key<<endl;
        }
    }
};
    

destory()函数的逻辑就是利用一次后序遍历,即先释放左右孩子节点,再释放根节点

template <typename Key, typename Value>
class BST{
......
private:
    ...
    void destroy(Node* node){
        if(node != NULL){
            destroy(node->left);
            destroy(node->right);
            
            delete node;
            count--; 
        }
    }
};

6  层序遍历(广度优先遍历)

通过引入队列(FIFO)来实现广度优先遍历

template <typename Key, typename Value>
class BST{
......
public:
    ...
    //层序遍历
    void levelOrder(){
        queue<Node*> q;
        q.push(root);
        while(!q.empty()){
            Node  *node = q.front();
            q.pop();
            
            cout<<node->key<<endl;
            
            if(node->left)
                q.push(node->left);
            if(node->right)
                q.push(node->right);
        }
    }
......
};
    

以上所有的遍历方式时间复杂度都为O(n),归并排序与快速排序本质上就是一棵二叉树的深度优先遍历过程

7  删除节点

删除操作本身很容易,难点在于删除一个节点之后如何操纵其左右孩子节点,使整棵二叉树保持二叉搜索树的性质

7.1  找到最小/最大值

首先考虑最简单的情况,删除最小/最大值。要删除最小/最大值,首先需要找到最小/最大值。通过二叉搜素树的性质可知,
沿着左孩子/右孩子不断向下寻找,直到左孩子/右孩子不存在,该节点就是整棵二叉搜素树的最小/最大值(注意:该节点不一定是叶子节点)

template <typename Key, typename Value>
class BST{
......
public:
    ...
    //寻找最小的键值
    Key minimun(){
        assert(count != 0);
        //使用递归的方式寻找二叉搜素树的最小值,返回值为最小值相应的Node
        Node* minNode = minimun(root);
        return minNode->key;
    }

private:
    ...
    //在以node为根的二叉搜索树中,返回最小键值的节点
    Node* minimun(Node* node){
        if(node->left == NULL)
            return node;
        return minimun(node->left);
    }
};   

7.2  删除最小/最大值

●首先要找到最小/最大节点(沿着根节点的左孩子/右孩子一路往下递归)

●如果为叶子节点,直接删除

  如果为根节点,直接将其右(min)/左(max)孩子节点提为根节点

/*
需要判断当前节点的左孩子是否为空,如果为空,代表着已经找到了最小的节点,需要做的就是删除该节点并保持二叉搜索树性质
先判断右孩子是否存在,如果存在,右孩子节点就代替当前节点,成为其父亲节点新的左孩子
但是,即使右节点不存在,也可以直接将当前的右节点(也就是空节点)直接返回
表示父亲节点新的左孩子为空,所以直接返回右孩子节点就可以涵盖两种情况(右节点为空/不为空)
*/template <typename Key, typename Value>
class BST{
......
public:
    ...
    //从二叉树中删除最小值所在的节点
    void removeMin(){
        if(root)//只有在根不为空时执行递归删除操作
            root = removeMin(root);
    }

private:
    ...
    //删除掉以node为根的二叉搜索树的最小节点,返回删除节点后新的二叉搜素树的根
    Node* removeMin(Node* node){
        if(node->left == NULL){
            Node* rightNode = node->right;
            delete node;
            count--;
            return rightNode;
        }
        //如果上述if语句不成立,则继续递归查找左孩子
        node->left = removeMin(node->left);
        return node;
    }
}; 

7.3  删除二叉搜索树中的任意节点(Hibbard Deletion)

上述删除最小值/最大值的节点对于删除只有一个左/右孩子或者同时没有左右孩子的情况,都是适用的(只有左孩子,提左孩子;只有右孩子,提右孩子

如下图所示,删除值为58的节点,该节点没有右孩子

那么如何删除左右都有孩子的节点?

使用右子树中的最小值(根节点的后继)代替

(或左子树中的最大值(根节点的前驱))

delMin伪码:
    删除左右都有孩子的节点d
    找到 s = min( d->right )
    s是d的后继
    s->right = delMin(d->right)//删除右子树中的最小节点,并返回该节点的根,作为后继的右孩子
    s->left = d->left
template <typename Key, typename Value>
class BST{
private:
    struct Node{
        ...
        Node(Node *node){//复制节点
            this->key = node->key;
            this->value = node->value;
            this->left = node->left;
            this->right = node->right;
        }
    };
    ...
public:
    ...
    // 从二叉树中删除键值为key的节点
    void remove(Key key){
        root = remove(root, key);
    }

private:
    ...
    // 删除掉以node为根的二分搜索树中键值为key的节点,返回删除节点后新的二分搜索树的根
    Node* remove(Node* node, Key key){

        if( node == NULL )
            return NULL;

        if( key < node->key ){
            node->left = remove( node->left , key );
            return node;
        }
        else if( key > node->key ){
            node->right = remove( node->right, key );
            return node;
        }
        else{   // key == node->key

            if( node->left == NULL ){
                Node *rightNode = node->right;
                delete node;
                count --;
                return rightNode;
            }

            if( node->right == NULL ){
                Node *leftNode = node->left;
                delete node;
                count--;
                return leftNode;
            }

            // node->left != NULL && node->right != NULL
            //找到当前节点右子树的最小节点(后继),复制该节点
            Node *successor = new Node(minimum(node->right));
            count ++;
            //删除该后继节点,返回该节点的根节点,作为之前提前复制的后继节点的右孩子
            successor->right = removeMin(node->right);
            successor->left = node->left;

            delete node;
            count --;

            return successor;//返回该后继节点,作为新的根节点
        }
    }
}; 

删除二叉搜索树的任意一个节点的时间复杂度为O(logn)

【Data Structure】二叉搜索树 [part 2]

猜你喜欢

转载自blog.csdn.net/qq_28038873/article/details/81537953