7.二叉搜索树

[TOC]

写在前面

本篇文章整理《数据结构(C++语言版)》关于二叉树这种线性结构知识点。
整个数据结构部分章节列表如下:
1 向量
2 列表
3 栈
4 队列

5 二叉树
6 图
7 二叉搜索树

7 二叉搜索树

设计初衷:高效的动态修改(如insert, remove)与高高效的静态查找(search)能否兼顾?

循关键码访问

类似于python中的dict类型,词条对象拥有关键码key与值value并通过key来寻找对应节点。

template<typename K, typename V> struct Entry {  //词条模板类
  K key; V value;
  Entry( K k = K(), V v = V() ) : key(k), value(v) {};  //默认构造函数
  Entry( Entry<K, V> const & e) : key(e.key), value(e.value) {};
// 比较器、判断器,可用词条直接代替关键码
  bool operator< ( Entry<K, V> const & e ) { return key < e.key; }
  bool operator> ( Entry<K, V> const & e ) { return key > e.key; }
  bool operator== ( Entry<K, V> const & e ) { return key == e.key; }
  bool operator!= ( Entry<K, V> const & e ) { return key != e.key; }
}

7.1 基础操作

任意一颗二叉树是二叉搜索树当且仅当其中序遍历序列单调非降。

BST与中序遍历

从上图可以看出,二叉搜索树(BST)中序遍历序列形式上与有序向量相同。
BST模板类
这里假定基本元素类型T或者直接支持比较和判别操作,或者已经重载过对应操作符。

template<typename T> class BST : public BinTree {  //由二叉树(BT)派生
public:  //以virtual修饰,强制要求派生类根据各自规则重写接口
  virtual BinNodePosi(T) & search( cosnt T & );
  virtual BinNodePosi(T) insert( const T & );
  virtual bool remove ( const T & );
protected:
  BinNodePosi(T) _hot;  //命中节点的父亲
  BinNodePosi(T) connect34(  //3+4重构
      BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), 
      BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T) );  
  BinNodePosi(T) rotateAt( BinNodePosi(T) );  //旋转调整
}

关于虚函数virtual
当基类Base指针ptr指向派生类Derived的成员函数时,调用派生类对应的成员函数而不是基类成员函数(一般情况下,ptr是Base指针,其指向应为Base成员函数)
>>>
class Base {
public:
void echo1() { std::cout << "called from Base" << endl; }
virtual void echo2() { std::cout << "called from Base" << endl; }
}
class Derived : public Base {
public:
void echo1() { std::cout << "called from Derived" << endl; }
void echo2() { std::cout << "called from Derived" << endl; }
}
int main() {
Base * ptr1 = new Derived;
ptr->echo1; //调用Base成员函数,打印--called from Base
ptr->echo2; //调用Derived成员函数,打印--called from Derived
}

7.1.1 search接口

与二分查找类似,BST节点的中序遍历即为单调有序向量形式。
如下图,待查找节点e与当前节点比较,若e较大,则深入该节点右子树...依次递归。

查找算法

若查找失效,则将此空节点视作哨兵节点
这样无论是否查找成功,命中节点v都指向e应当存在的位置,_hot指向v的父亲
接口语义

template<typename T> BinNodePosi(T) & BST<T>::search(const T& e) {
  return searchIn(_root, e, _hot = NULL);
}
template<typename T>
static BinNodePosi(T) & searchIn(BinNodePosi(T) & v, const T& e, BInNodePosi(T) & hot){
  if( !v || ( e == v->data ) ) return v;  //待search值e已找到或递归节点v为空
  hot = v;  //hot指向“命中节点”的父亲
  return searchIn( ( ( e < v->data ) ? v->lc : v->rc ), e, hot);
}

7.1.2 插入操作

根据search接口,命中节点v即为待查找节点e应当存在的位置,因此只需将待插入节点作为命中节点v父亲_hot的孩子插入即可。

template<typename T> BinNodePosi(T) BST<T>::insert( const T& e) {
  if( search(e) ) return search(e);  //若待插入节点存在,则停止插入
  BinNodePosi(T) & x = new BinNode<T> (e, _hot);  //创建新节点x,以e为关键码,_hot为父
  _size++;
  updateHeightAbove(x);
  return x;
}  

7.1.3 删除操作

与插入操作类似,首先通过search接口查找需要删除节点位置。
以下是删除操作的代码框架

template<typename T> bool BST<T>::remove( cosnt T& e ) {
  BinNodePosi(T) & x = search(e);
  if(!x) return false;  //待删除元素不存在,返回
  removeAt(x, _hot);  //删除操作,需考虑两种情况。
  size--;
  updateHeightAbove(_hot);
  return true;
}

分为两种情况考虑,待删除节点只有一个分支或有两个分支。
出现这种分歧的原因是BST是按照中序遍历序列排列的,插入或删除操作都需要维持原始BST的有序性。
对于单分支,待删除节点下只有比它大(小)的节点,将其孩子直接代替删除位置,不影响BST有序性。
对于多分支,待删除节点的左(右)孩子替换删除位置后,可能会导致新节点的左(右)子树中存在比当前节点大(小)的元素。
如下图,若直接将58代替36会导致40、46、53等比58小的节点存在右子树中。


1 单分支
这种情况下,直接删除节点,并将其孩子替换掉原在位置。
2 多分支
保持BST有序性的核心在于将距离待删除节点最近的节点代替删除位置,随后重复单分支操作情况。
综上所述,removeAt()实现代码如下。

static BinNodePosi(T) removeAt( BinNodePosi(T) & x, BinNodePosi & hot ) {
  BinNodePosi(T) w = x;
  BinNodePosi(T) succ = NULL;
  if( ! HasLChild(*x) ) succ = x = x->rc;  //直接将rc覆盖x即可删除x节点
  else if( ! HasRChild(*x) ) succ = x =x->lc;
  else {
    w = w->succ();  //通过中序遍历获得x的直接后继,即大于x的最小节点
    swap( x->data, w->data );  //只将两节点data交换,节点对应的左右孩子指向未变
    BinNodePosi(T) u = w->parent;
    ( ( u == x) ? u->rc : u->lc ) = succ = w->rc;
  }
  hot = w->parent;
  if(succ) succ->parent = hot;
  release( w->data ); release(w);
  return succ;
} 

7.2 AVL树

平衡二叉树----在节点数目固定的前提下,尽可能降低树的高度,即尽可能使兄弟子树的高度彼此接近。渐进意义下树高不超过O(log n)。
平衡因子----节点v的左右子树高度差。

balFac(v) = height(lc(v)) - height(rc(v))

AVL树----各节点平衡因子的绝对值不超过1.
接口定义----由BST派生得来。

/**********************************
#define  stature(p) ((p) ? (p)->height : -1) //节点高度(与“空树高度为-1”的约定相统一)
**********************************/
#define Balanced(x) ( stature( (x).lc ) == stature( (x).rc ) )  //理想平衡条件
#define BalFac(x) ( stature( (x).lc ) - stature( (x).rc ) )  //平衡因子
#define AvlBalanced(x) ( ( -2 < BalFac(x) ) && ( BalFac(x) < 2) )  //AVL平衡条件
template<typename T> class AVL : public BST<T> {
public:
  BinNodePosi(T) insert( const T& e);
  bool remove( const T& e);
}

7.2.1 平衡性

设高度为h的子树至少含有S(h)个节点,则根据AVL平衡条件有如下等式成立。
S(h) = S(h-1) + S(h-2) + 1

平衡性

节点删除--失衡
1.节点删除只可能会导致其父节点失衡
2.被删除节点父亲的树高未变(子树高度最大者+1),因而其祖先的平衡因子不会改变。
节点删除

节点插入--失衡
1.节点插入可能会导致其祖先发生一连串的失衡。
2.被删除节点父亲的树高可能发生变化(子树高度最大者+1),因而其祖先的平衡因子可能会打破。
节点插入

7.2.2 重平衡

插入重平衡
只需维护插入节点x导致第一个失衡祖先即可,其余失衡祖先可随之一并重平衡。
设g是首次失衡祖先,通过如下代码即可完成由g找到x的祖先p或v。

/**************************************
#define IsLChild(x) ( ! IsRoot(x) && ( & (x) == (x).parent->lc ) )
该部分在BinNode中定义
**************************************/
#define tallerChild(x) ( \
    stature( (x)->lc ) > stature( (x)->rc ) ? (x)->lc : ( /*左子树高*/ \
    stature( (x)->lc ) < stature( (x)->rc ) ? (x)->rc : ( /*右子树高*/ \
    IsLChild( * (x) ) ? (x)->lc : (x)->rc /*等高,与父亲同侧者优先*/ \
    ) \
    ) \
)

单旋----g, p, v同侧
BST中的等价变换为上下可变,左右不变

单旋操作

双旋----g, p, v不在同侧
双旋

template<typename T> BInNodePosi(T) AVL<T>::insert( const T& e ) {
  BinNodePosi(T) & x = search(e); if(x) return x;  //判断待插入节点是否存在
  BinNodePosi(T) xx = x = new BinNode<T> (e, _hot); _size++; //创建新节点
//重平衡
  for(BInNodePosi(T) g = _hot; g; g = g->parent) {  //查找首次遇到失衡祖先节点g
    if( !AvBalanced(*g) ) { // 一旦发现g失衡,采用“3+4”算法重平衡
      FromParentTo(*g) = rotateAt( tallerChild ( tallerChild(g) ) );
      break;
    } else //若未失衡,只需更新树高
      updateHeight(g);
  }
  return xx;
}

删除重平衡
如下图操作,重平衡后子树的高度可能继续降低,从而导致失衡向上传播。
单旋

单旋

双旋
双旋

template<typename T> bool AVL<T>::remove( const T& e ) {
  BinNodePosi(T) x = search(e); if(!x) return false; //待删除节点不存在,返回
  removeAt( x, _hot); _size--;
  for(BinNodePosi(T) g = _hot; g; g = g->parent) {
    if( !AvlBalanced(*g) )
      g = FromParentTo(*g) = rotateAt( tallerChild( tallerChild(g) ) );
    updateHeight(g);
  }
  return true;
}

7.2.3 3+4算法

将操作节点x与其先失衡祖先g以及g更高一侧的子树的孩子节点p, v设置为独立的模块。
整理得到如下两类模块:
3节点模块:g, p, v
4子树模块:g不含p的子树T1, p不含v的子树T2, v的两个子树T3, T4
最终只需按着满足中序遍历方法将上述模块重组为符合BST结构即可。


3+4算法
template<typename T> BinNodePosi(T) BST<T>::connect34 (
  BinNodePosi(T) a, BinNodePosi(T) b, BinNodePosi(T) c,
  BinNodePosi(T) T0, BinNodePosi(T) T1, BinNodePosi(T) T2, BinNodePosi(T) T3
) {
  a->lc = T0; if(T0) T0->parent = a;
  a->rc = T1; if(T1) T1->parent = a;
  c->lc = T2; if(T2) T2->parent = c;
  c->rc = T3; if(T3) T3->parent = c;
  b->lc = a; a->parent = b;
  b->rc = c; c->parent = b;
  updateHeight(b);
}

继而可以将单旋、双旋等操作直接整合如下。

template<typename T> BinNodePosi(T) BST<T>::rotateAt(BinNodePosi(T) v) {
  BinNodePosi(T) p = v->parent;
  BinNodePosi(T) g = p->parent;
//视p, v, g相对位置分为四种情况
  if( IsLChild(*p) ) /*zig*/
    if( IsLChild(*v) ) {/*zig-zig*/
      p->parent = g->parent;
      return connect34(v, p, g, v->lc, v->rc, p->rc, g->rc);
    } else { /*zig-zag*/
       v->parent = g->parent;
      return connect34(p, v, g, p->lc, v->lc, v->rc, g->rc);
  } 
  else /* zag*/
    if( IsRChild(*v) ) { /*zag-zag*/
      p->parent = g->parent;
      return connect34(g, p, v, g->lc, p->lc, v->lc, v->rc);
    } else { /*zag-zig*/
      v->parent = g->parent;
      return connect34(g, v, p, g->lc, v->lc, v->rc, p->rc);
    }  
}   
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 树的概述 树是一种非常常用的数据结构,树与前面介绍的线性表,栈,队列等线性结构不同,树是一种非线性结构 1.树的定...
    Jack921阅读 4,432评论 1 31
  • 红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。 BST 二叉查找树(Binary...
    kanehe阅读 1,370评论 0 8
  • -二叉搜索树 查找问题:静态查找和动态查找,静态查找可以用二分查找-判定树,那么针对动态查找数据如何组织?(树的动...
    Spicy_Crayfish阅读 1,341评论 0 2
  • 1 概述 二叉搜索树,顾名思义,其主要目的用于搜索,它是二叉树结构中最基本的一种数据结构,是后续理解B树、B+树、...
    CodingTech阅读 3,123评论 5 35
  • 端午放假几天,有好几件事都挺触动。 重来没有用过简书,突然想用简书发表出来,不是作为什么写手,作为作家,自认不到这...
    博佳_阅读 202评论 0 0