C Primer+Plus(十七)高级数据表示(三)

第十七章 高级数据表示

四、二叉搜索树

先梳理一下链表和数组方式对几种操作的利弊特点:

a.访问:链表形式,必须从首节点开始找起,直到要访问的节点为止,这个称为顺序访问。而数组方式则方便的多,可直接定位到某个元素,这称为随机访问。

b.插入/删除:对于链表形式,插入或删除操作仅需要修改前续和后续节点就可以完成;而数组方式需修改被增加或删除元素后面所有的元素。

c.查找/搜索:其实也是一种访问形式。一般情况,对于顺序排列的“数据堆”,可以通过"折半查找法”非常效率的进行查找。可折半查找,首先得定位到中间的元素,对数组而言,因为有下标,很容易定位;而对于链表定位中间元素,首先要知道数据堆里总共有多少个节点,然后从首节点开始,pnode->next一直操作下去,直至定位到中间元素,再进行比较。

如果需要一种既方便插入/删除,又方便搜索查找的数据堆形式,那该选用哪种形式呢?我们暂时不必去想如何去设计,去想出这样一种形式我们已经有了答案:二叉树。

二叉树的特点:每个节点除去item本身信息外,描述该节点还需要两个指针成员,分别为左指针,右指针。和单链表不一样的是,二叉树每个节点链接下去有两个后续节点。

为使搜索工作可以利用“折半法”,使每个节点按一种顺序的关系排列:对于节点和他的左子节点、右子节点的关系为:左子节点<节点<右子节点。

这样的一种二叉树,称为“二叉搜索树”(binary search tree.bst)。

1、二叉搜索树ADT的数据定义描述

//项目依据实际所需去定义
typedef something Item;

//二叉树节点定义
typedef struct node
{
    Item item;
    struct node *left;
    struct node *right;
}Node;
//树的定义
//通过定位树的根节点可以找到该数,同时增加树的节点数目丰富树的信息
typedef struct tree
{
    Node *root;
    int size;
}Tree; 

2、二叉搜索树的操作定义

(1)初始化一棵树

void InitializeTree(Tree *ptree)
{
     ptree->root=NULL;
     ptree->size=0;
}

(2)判断树是否为空(判断为满也类似)

int TreeIsEmpty(Tree *ptree)
{
    return (ptree->size==0);
}

(3)向树中添加一个项目

因为二叉搜索树中不能含有项目相同的节点,所以这里假设树中没有与被添加项目相同的节点项目,并且假设树非满。添加一个项目的几个主要步骤如下:

step1:找到合适位置;

step2:设置一个新节点,将添加项目纳入该节点,并使该节点的两个指针成员为空;

step3:将该新节点放进合适位置,具体为:使合适位置的父节点与其链接

其中第2、3步都可以简单实现,关键是第1步,如何找到合适位置?

考虑二叉搜索树的特征:左<父<右。

那么,先判断x(设x为要添加的项目)在树根节点的左还是右边;

假设在左,如果刚好左为空,则找到了合适位置;如果左不为空,那就要继续判断:x再以该左节点为根节点的树的左还是右?依次类推进行下去。这里自然想到递归方法。

关键函数代码如下:

//Toleft和ToRight函数为判断左右函数
//对ToLeft(),若左则true,若右则false;ToRight()类似
void AddNode(Node *newnode,Node *root)
{
   if(ToLeft(&(newnode->item),&(root->item))
   {
       if(root->left==NULL)
         root->left=newnode;
       else
         AddNode(newnode,root->left);
   }
   if(ToRight(&(newnode->item),&(root->item))
   {
       if(root->right==NULL)
         root->right=newnode;
       else
         AddNode(newnode,root->right);
    }
}

(4)查找项目:查找树中包含相同项目的节点;往往查找项目是为了后续操作服务(比如,删除),因此查找项目的函数须返回该项目的父节点,同时也需要返回包含该项目的节点,若没有找到,该返回项即为NULL。

要返回两个节点指针,一般函数只能返回某个数据类型的单个变量,这里自然又想到创建一个结构,匹配这两个要返回的数据:

typedef struct pair
{
   Node *parent;   //指向父节点
   Node *child;    //指向找到的节点
}Pair;

 用递归方法实现:

//未测试,以后检查
Pair SeekItem(const Item *pi,const Tree *ptree)
{
   Pair look;
   Tree newtree;
   newtree=*ptree;
   if(*pi==newtree.root->item)
   {
      look.parent=NULL;
      look.child=newtree.root;
      return look;
    }
    else 
       if(ToLeft(pi,&(newtree.root->item)))
       {
            newtree.root=newtree.root->left;
            SeekItem(pi,&newtree);
        }
       if(ToRight(pi,&(newtree.root->item)))
       {
            newtree.root=newtree.root->right;
            SeekItem(pi,&newtree);
        }
        else
        {
             look.parent=NULL;
             look.child=NULL;
             return look;
        }
}

用while()方法实现:

Pair SeekItem(const Item *item,const Tree *ptree)
{
    Pair look;
    look.parent=NULL;
    look.child=ptree->root;
    while(look.child!=NULL)
    {
         if(ToLeft(item,&(look.child->item)))
         {
             look.parent=look.child;
             look.child=look.child->left;
             continue;
          }
         if(ToRight(item,&(look.child->item)))
         {
             look.parent=look.child;
             look.child=look.child->right;
             continue;
          }
          else
             break;
      }
      return look;
}
       

(4)删除项目:

第一种情况:被删除节点没有子节点,则将被删除节点的父节点的相应成员设为NULL。

第二种情况:被删除节点只有一个子节点,考虑二叉搜索树的特性,可以知道要做的只是把被删除节点的子树链接到父节点的相应成员。

第三种情况:被删除节点有两个子节点。分析二叉搜索树的特性:左、右子树任意节点必定都同时大于或小于被删节点的父节点,因此这两个子树都应被归并到父节点的同一边去;同时右子树任意节点都大于左子树任意节点,因此:

若被删节点位于父节点左边,说明左右子树均小于父节点,则将其左子节点链接到父节点左边,同时在左子节点为根节点的树的右边寻找空位去添加右子树,这个空位是左子树的右边中第一个空缺的右节点(因为这个位置相对于被删除节点的左子树是最大的位置)。

若被删节点位于父节点右边,说明左右子树均大于父节点,则将其右子节点链接到父节点右边,同时在右子节点为根节点的树的左边寻找空位去添加左子树,这个空位是右子树的左边中第一个空缺的右节点(因为这个位置相对于被删除节点的右子树是最小的位置)。

关键函数:

//删除结点函数,参数如何描述?
//删除操作后被删除结点的父节点的“指向被删结点的指针成员”要重新赋值
//所以要用指向“父节点指针成员”的指针去描述
//而父节点指针成员本身是一个Node*指针型,所以参数类型为Node **
//注意:这里的ptr是指向父节点指针成员(指向被删结点)的指针类型
//*ptr描述的是父节点中指向被删结点的指针成员,也即被删结点的地址指针类型
//**ptr则是描述被删除的结点类型
static void DeleteNode(Node **ptr)
{
  Node *temp;
  puts((*ptr)->item.petname);
//*ptr为被删结点父节点的指针成员,该成员指向被删结点,
//(*ptr)->left即指向被删结点的左子节点
//若其左子节点为空,将其右子节点链接到父节点原先指向被删结点的指针成员
  if((*ptr)->left==NULL)   
  {
    temp=*ptr;
    *ptr=(*ptr)->right;
    free(temp);
  }
//若其右子节点为空,将其左子节点链接到父节点原先指向被删结点的指针成员
  else if((*ptr)->right==NULL)
  {
    temp=*ptr;
    *ptr=(*ptr)->left;
    free(temp);
  }
  else  //被删除结点有两个子结点
  {
    for(temp=(*ptr)->left;temp->right!=NULL;temp=temp->right)
        continue;
    temp->right=(*ptr)->right;
    temp=*ptr;
    *ptr=(*ptr)->left;
    free(temp);
  }
}

 (5)遍历树

void InOrder(const Node *root,void(*pfun)(Item item))
{
   if(root!=NULL)
   {
      InOrder(root->left);
      (*pfun)(root->item);
      InOrder(root->right);
    }
}
//pfun函数先处理左节点,再处理根节点,最后处理右节点

(6)清空树

void DeleteTree(Tree *ptree)
{
   if(ptree!=NULL)
         DeleteAllNodes(ptree->root);
  ptree->root=NULL;
  ptree->size=0;
}

static void DeleteAllNodes(Node *root)
{
    Node *pright;
    if(root!=NULL)
    {
       pright=root->right;
       DeleteAllNodes(root->left);
       free(root);
       DeleteAllNodes(pright);
     }
}

 

posted @ 2013-07-12 20:59  tsembrace  阅读(526)  评论(0编辑  收藏  举报