数据结构与算法——二分搜索树 动画演示


  二叉搜索树是用来解决查找问题的,在介绍二叉搜索树之前,先学习二分查找法。

一、二分查找法

  二分查找法只能对于有序数列使用(排序后的数组),在中间找一个元素v如果不是v,这在<v和>v两部分查找,时间复杂度为O(logn),如下图所示:
在这里插入图片描述
二分查找代码:

// 二分查找法,在有序数组arr中,查找target
// 如果找到target,返回相应的索引index
// 如果没有找到target,返回-1
template<typename T>
int binarySearch(T arr[], int n, T target){
    // 在arr[l...r]之中查找target
    int l=0,r=n-1;

    while(r>l){
        //int mid = (l + r)/2;

        // 防止极端情况下的整形溢出,使用下面的逻辑求出mid
        int mid = l + (r-l)/2;
        //如果刚好找到
        if( arr[mid] == target )
            return mid;
        //没找到就向两边搜索
        if(arr[mid]>target)
            r=mid-1;
        else
            l=mid+1;
    }
    return -1;
}

用递归的方式写二分查找法:

template<typename T>
int binarySearch2(T arr[], int l, int r, T target){
    
    if( l > r )
        return -1;

    //int mid = (l+r)/2;
    // 防止极端情况下的整形溢出,使用下面的逻辑求出mid
    int mid = l + (r-l)/2;
    
    if( arr[mid] == target )
        return mid;
    else if( arr[mid] > target )
        return binarySearch2(arr, l, mid-1, target);
    else
        return binarySearch2(arr, mid+1, r, target);
}

  比较非递归和递归写法的二分查找的效率非递归算法在性能上有微弱优势。

二分查找法变变种:floor和ceil

  这前的二分查找的方法是假设数组中没用重复元素,假设数组中后很多重复元素时,用上面的方法可能会有很多可能。
  floor函数就是返回重复元素中第一次出现的索引,ceil函数就是返回重复元素中最后一次出现的索引。如下图所示,红色区域v时重复元素:
在这里插入图片描述
  当要查找的元素不存在时,floor函数返回最后一个比查找的元素小的索引,ceil函数返回第一个比查找的元素大的索引,如下图所示:
在这里插入图片描述
floor函数:

// 二分查找法, 在有序数组arr中, 查找target
// 如果找到target, 返回第一个target相应的索引index
// 如果没有找到target, 返回比target小的最大值相应的索引, 如果这个最大值有多个, 返回最大索引
// 如果这个target比整个数组的最小元素值还要小, 则不存在这个target的floor值, 返回-1
template<typename T>
int floor(T arr[], int n, T target){

    assert( n >= 0 );

    // 寻找比target小的最大索引
    int l = -1, r = n-1;
    while( l < r ){
        // 使用向上取整避免死循环
        int mid = l + (r-l+1)/2;
        if( arr[mid] >= target )//等于也向左取整,找到最后一个比target小的索引
            r = mid - 1;
        else
            l = mid;//要包含mid,因为mid可能是最后一个比target小的索引
    }

    assert( l == r );

    // 如果该索引+1就是target本身, 该索引+1即为返回值
    if( l + 1 < n && arr[l+1] == target )
        return l + 1;

    // 否则, 该索引即为返回值
    return l;
}

ceil函数:

// 二分查找法, 在有序数组arr中, 查找target
// 如果找到target, 返回最后一个target相应的索引index
// 如果没有找到target, 返回比target大的最小值相应的索引, 如果这个最小值有多个, 返回最小的索引
// 如果这个target比整个数组的最大元素值还要大, 则不存在这个target的ceil值, 返回整个数组元素个数n
template<typename T>
int ceil(T arr[], int n, T target){

    assert( n >= 0 );

    // 寻找比target大的最小索引值
    int l = 0, r = n;
    while( l < r ){
        // 使用普通的向下取整即可避免死循环
        int mid = l + (r-l)/2;
        if( arr[mid] <= target )
            l = mid + 1;
        else // arr[mid] > target
            r = mid;
    }

    assert( l == r );

    // 如果该索引-1就是target本身, 该索引+1即为返回值
    if( r - 1 >= 0 && arr[r-1] == target )
        return r-1;

    // 否则, 该索引即为返回值
    return r;
}

二、二分搜索树

  二分搜索树是用来实现查找表,或者称为字典。

实现查找表的比较:

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

  普通数组,查找元素需要遍历整个数组查找,复杂度为O(n);插入元素时先遍历整个数组查找是否存在再插入,复杂度为O(n);删除元素也是遍历整个数组,复杂度为O(n)。
  顺序数组,查找元素可以使用二分查找法,复杂度为O(logn);插入和删除操作和普通数组一样,复杂度为O(n)。
  二分搜索树不仅在查找元素上高效,插入删除元素也高效。索引二分搜索树为我们提供了高效的动态维护数据的方式。

二分搜索树定义

  二分搜索树是一棵二叉树,每个结点的键值大于左节点,每个节点的键值大于右节点。也就是每个结点的键值大于左边所有的节点,每个结点的键值小于右边所有的节点,对于每个节点都成立。如下图所示:在这里插入图片描述
  二叉搜索树常用递归来实现,二叉搜索树没有完全二叉树的限制,如下图的二叉树也是二叉搜索树:
在这里插入图片描述
  先写一个二叉搜索树的骨架:

// 二分搜索树
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;
    }

};

插入元素

  二分搜索树插入元素,与根节点比较,如果比根节点小就插入左边的二叉搜索树中,比根节点大就插入右边的二叉搜索树中。如下图所示:
在这里插入图片描述
  当插入元素相同时,则将相同元素替换成新的元素:
在这里插入图片描述
  插入操作代码(递归实现):

public:
    // 向二分搜索树中插入一个新的(key, value)数据对
    void insert(Key key, Value value){
        root = insert(root, key, value);
    }

private:
    // 向以node为根的二分搜索树中, 插入节点(key, value), 使用递归算法
    // 返回插入新节点后的二分搜索树的根
    Node* insert(Node *node, Key key, Value value){

        if( node == NULL ){
            count ++;
            return new Node(key, value);
        }

        if( key == node->key )//相等,则替换成新的结点
            node->value = value;
        else if( key < node->key )
            node->left = insert( node->left , key, value);
        else    // key > node->key
            node->right = insert( node->right, key, value);

        return node;//除了边界结点,其他结点返回自身
    }

查找元素

  查找元素和插入元素的过程类似:
  查找成功:
在这里插入图片描述
  查找失败:
在这里插入图片描述
  contain函数表示二叉搜索树中是否包含要查找的元素,contain函数和search函数同质,只是返回值不同。

contain函数:

public:
    // 查看二分搜索树中是否存在键key
    bool contain(Key key){
        return contain(root, key);
    }

private:
    // 查看以node为根的二分搜索树中是否包含键值为key的节点, 使用递归算法
    bool contain(Node* node, Key key){

        if( node == NULL )
            return false;

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

search函数代码:

public:
     // 在二分搜索树中搜索键key所对应的值。如果这个值不存在, 则返回NULL
    Value* search(Key key){
        return search( root , key );
    }

private:
    // 在以node为根的二分搜索树中查找key所对应的value, 递归算法
    // 若value不存在, 则返回NULL
    Value* search(Node* node, Key key){

        if( node == NULL )
            return NULL;

        if( key == node->key )
            return &(node->value);//返回地址
        else if( key < node->key )
            return search( node->left , key );
        else // key > node->key
            return search( node->right, key );
    }

三、二分搜索树的遍历

二分搜索树的遍历(深度优先遍历)

  前序遍历:先访问当前结点,再递归访问左右子树;
  中序遍历:先递归访问左子树,再访问自身,再递归访问右子树;
  后序遍历:先递归访问左右子树,再访问自身结点。

  可以将二叉树的遍历抽象为下图访问三个点的过程,如下图所示。上面三种遍历方法只是访问这三个点的顺序不同。
在这里插入图片描述
动画演示:
  前序遍历的动画演示(将访问到的值打印出来):
  先访问当前结点,再递归访问左右子树。
在这里插入图片描述
  中序遍历的动画演示(将访问到的值打印出来):
  对于二分搜索树想要将输出结果排序的话,只需要进行中序遍历即可。
在这里插入图片描述
  后序遍历的动画演示(将访问到的值打印出来):
  将左右两个子树全部遍历完才访问自身,应用:释放二叉树
在这里插入图片描述
前序遍历代码:

// 对以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;
    }
}

后序遍历的应用:
  释放二叉树。析构函数调用此方法释放二叉树

public:
    // 析构函数, 释放二分搜索树的所有空间
    ~BST(){
        destroy( root );
    }
private:
    // 释放以node为根的二分搜索树的所有节点
    // 采用后续遍历的递归算法
    void destroy(Node* node){

        if( node != NULL ){
            destroy( node->left );
            destroy( node->right );

            //释放空间
            delete node;
            count --;
        }
    }

二分搜索树的层序遍历(广度优先遍历)

  之前的遍历实际上是深度优先遍历,只不过是打印的顺序不同而已。
  实现广度优先遍历需要借助队列这种数据结构,队列是一种先进先出的数据结构。先将根节点入队,出队时将该节点的孩子节点全部入队,以此类推。动画演示:
在这里插入图片描述
层序遍历代码如下:

// 二分搜索树的层序遍历
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 );
    }
}

四、二分搜索树删除节点

删除最大值,最小值

找到最值
  二分搜索树的左孩子比根节点小,右孩子比根节点大。所以一直往左找就可以找到最小值,最大值同理。
找到最小值和最大值代码如下:

public:
   // 寻找二分搜索树的最小的键值
    Key minimum(){
        assert( count != 0 );
        Node* minNode = minimum( root );
        return minNode->key;
    }

    // 寻找二分搜索树的最大的键值
    Key maximum(){
        assert( count != 0 );
        Node* maxNode = maximum(root);
        return maxNode->key;
    }

private:
    // 返回以node为根的二分搜索树的最小键值所在的节点
    Node* minimum(Node* node){
        if( node->left == NULL )
            return node;

        return minimum(node->left);
    }

    // 返回以node为根的二分搜索树的最大键值所在的节点
    Node* maximum(Node* node){
        if( node->right == NULL )
            return node;

        return maximum(node->right);
    }

删除最值
  如果最小值没有右孩子,那么直接将最小值删除即可。如果最小值有右孩子,那么直接将右孩子替换删除的元素即可,因为右孩子还是比根节点小。删除最大值同理。
  最小值有右孩子动画演示:
在这里插入图片描述
  最大值有左孩子动画演示:
在这里插入图片描述

  删除最大值和最小值代码如下:

public:
    // 从二分搜索树中删除最小值所在节点
    void removeMin(){
        if( root )
            root = removeMin( root );
    }

    // 从二分搜索树中删除最大值所在节点
    void removeMax(){
        if( root )
            root = removeMax( root );
    }
    
private:
    // 删除掉以node为根的二分搜索树中的最小节点
    // 返回删除节点后新的二分搜索树的根
    Node* removeMin(Node* node){

        if( node->left == NULL ){

            Node* rightNode = node->right;//兼顾了两种情况
            delete node;
            count --;
            return rightNode;
        }

        //左节点不断递归赋值回去,最后返回根节点
        node->left = removeMin(node->left);
        return node;
    }

    // 删除掉以node为根的二分搜索树中的最大节点
    // 返回删除节点后新的二分搜索树的根
    Node* removeMax(Node* node){

        if( node->right == NULL ){

            Node* leftNode = node->left;
            delete node;
            count --;
            return leftNode;
        }

        node->right = removeMax(node->right);
        return node;
    }    

删除任意节点

  删除只有左孩子或只有右孩子的结点时,可以采用和删除最大值和最小值类似的方法,动画演示如下:

  删除只有左孩子或只有右孩子的结点时,应该先找到该节点右孩子的最小值,然后进行替换。因为所有右孩子比根节点大,找到该节点右孩子的最小值进行替换后任然是二分搜索树。

  如下图(节点58为d,节点59为s),要删除元素58,只需要找到元素58右孩子中的最小值58,然后代替该节点。
在这里插入图片描述
  delMin(d->right)是删除以60为根节点的子树的最小值,并返回根节点60。将60赋值给s的右继,d的左节点赋值给s的左继。删除节点d。
在这里插入图片描述
代码如下:

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 *successor = new Node(minimum(node->right));//新建一个结点,并赋值为比待删除节点大的最小节点
            count ++;//新建一个结点数量+1,后面的removeMin函数会-1

            successor->right = removeMin(node->right);
            successor->left = node->left;

            delete node;
            count --;

            return successor;//返回新的结点给上一层的父节点
        }
    }

补充:还可以用左子树的最大值节点来替换要删除的结点。这种删除操作的时间复杂度是O(logn)。

有时间看一下:
5-9 二分搜索树的顺序性
5-10 二分搜索树的局限性
5-11 树形问题和更多树。

附录

// 二分搜索树
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(Node *node){
            this->key = node->key;
            this->value = node->value;
            this->left = node->left;
            this->right = node->right;
        }
    };

    Node *root; // 根节点
    int count;  // 树中的节点个数

public:
    // 构造函数, 默认构造一棵空二分搜索树
    BST(){
        root = NULL;
        count = 0;
    }

    // 析构函数, 释放二分搜索树的所有空间
    ~BST(){
        destroy( root );
    }

    // 返回二分搜索树的节点个数
    int size(){
        return count;
    }

    // 返回二分搜索树是否为空
    bool isEmpty(){
        return count == 0;
    }

    // 向二分搜索树中插入一个新的(key, value)数据对
    void insert(Key key, Value value){
        root = insert(root, key, value);
    }

    // 查看二分搜索树中是否存在键key
    bool contain(Key key){
        return contain(root, key);
    }

    // 在二分搜索树中搜索键key所对应的值。如果这个值不存在, 则返回NULL
    Value* search(Key key){
        return search( root , key );
    }

    // 二分搜索树的前序遍历
    void preOrder(){
        preOrder(root);
    }

    // 二分搜索树的中序遍历
    void inOrder(){
        inOrder(root);
    }

    // 二分搜索树的后序遍历
    void postOrder(){
        postOrder(root);
    }

    // 二分搜索树的层序遍历
    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 );
        }
    }

    // 寻找二分搜索树的最小的键值
    Key minimum(){
        assert( count != 0 );
        Node* minNode = minimum( root );
        return minNode->key;
    }

    // 寻找二分搜索树的最大的键值
    Key maximum(){
        assert( count != 0 );
        Node* maxNode = maximum(root);
        return maxNode->key;
    }

    // 从二分搜索树中删除最小值所在节点
    void removeMin(){
        if( root )
            root = removeMin( root );
    }

    // 从二分搜索树中删除最大值所在节点
    void removeMax(){
        if( root )
            root = removeMax( root );
    }

    // 从二分搜索树中删除键值为key的节点
    void remove(Key key){
        root = remove(root, key);
    }

private:
    // 向以node为根的二分搜索树中, 插入节点(key, value), 使用递归算法
    // 返回插入新节点后的二分搜索树的根
    Node* insert(Node *node, Key key, Value value){

        if( node == NULL ){
            count ++;
            return new Node(key, value);
        }

        if( key == node->key )
            node->value = value;
        else if( key < node->key )
            node->left = insert( node->left , key, value);
        else    // key > node->key
            node->right = insert( node->right, key, value);

        return node;
    }

    // 查看以node为根的二分搜索树中是否包含键值为key的节点, 使用递归算法
    bool contain(Node* node, Key key){

        if( node == NULL )
            return false;

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

    // 在以node为根的二分搜索树中查找key所对应的value, 递归算法
    // 若value不存在, 则返回NULL
    Value* search(Node* node, Key key){

        if( node == NULL )
            return NULL;

        if( key == node->key )
            return &(node->value);
        else if( key < node->key )
            return search( node->left , key );
        else // key > node->key
            return search( node->right, key );
    }

    // 对以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;
        }
    }

    // 释放以node为根的二分搜索树的所有节点
    // 采用后续遍历的递归算法
    void destroy(Node* node){

        if( node != NULL ){
            destroy( node->left );
            destroy( node->right );

            delete node;
            count --;
        }
    }

    // 返回以node为根的二分搜索树的最小键值所在的节点, 递归算法
    Node* minimum(Node* node){
        if( node->left == NULL )
            return node;

        return minimum(node->left);
    }

    // 返回以node为根的二分搜索树的最大键值所在的节点, 递归算法
    Node* maximum(Node* node){
        if( node->right == NULL )
            return node;

        return maximum(node->right);
    }

    // 删除掉以node为根的二分搜索树中的最小节点, 递归算法
    // 返回删除节点后新的二分搜索树的根
    Node* removeMin(Node* node){

        if( node->left == NULL ){

            Node* rightNode = node->right;
            delete node;
            count --;
            return rightNode;
        }

        node->left = removeMin(node->left);
        return node;
    }

    // 删除掉以node为根的二分搜索树中的最大节点, 递归算法
    // 返回删除节点后新的二分搜索树的根
    Node* removeMax(Node* node){

        if( node->right == NULL ){

            Node* leftNode = node->left;
            delete node;
            count --;
            return leftNode;
        }

        node->right = removeMax(node->right);
        return node;
    }

    // 删除掉以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;
        }
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值