返回文章归档

秃头的搜索算法

本页目录29 个章节
  1. 二叉排序树,AVL树,R-B树,B树,B+树
  2. 尼娅可爱馁

二叉排序树

定义

二叉查找树(Binary Search Tree),也称为二叉查找树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指具有以下性质的树

  1. if任意节点左子树非空;左子树上所有节点都小于根节点的值;
  2. if任意节点右子树非空;右子树上所有节点都大于根节点的值;
  3. 任意节点左、右子树也分别是二叉排序树

中序遍历可以得到一个递增有序序列

查找

这个查找还是比较简单的

#include <stdio.h>
#include <stdlib.h>
typedef struct bst_node {
  int val;
  struct bst_node *lchild, *rchild;
} bst_node;

bst_node *bst_search(bst_node *root, int key) {
  if (key == root->val)
    return root;
  else if (key < root->val)
    return root->lchild ? bst_search(root->lchild, key) : NULL;
  else
    return root->rchild ? bst_search(root->rchild, key) : NULL;
}

插入

二叉排序树是在边查找,边插入而不是一次性生成的。在找一个树找到了空结点则插入;

bst_node *bst_node_init(int val) {
  bst_node *node;
  node = (bst_node *)malloc(sizeof(bst_node));
  node->lchild = NULL;
  node->rchild = NULL;
  node->val = val;
  return node;
}

int bst_insert(bst_node *root, int k) {
  if (root == NULL) {
    root = bst_node_init(k);
    return 1; // insert success
  } else if (k == root->val) {
    return 0; // node exist
  } else if (k < root->val) {
    return bst_insert(root->lchild, k);
  } else {
    return bst_insert(root->rchild, k);
  }
}

构造

构造的话就按输入顺序在一棵空树上插入就好了。

删除

对二叉排序树进行删除,删了节点还要把删掉了的节点的子树接起来(麻烦捏),设被删除的节点是delete_node,有三种情况:

  1. if delete_node是叶(终端)节点,直接删除开开心心

  2. if delete_node只有左子树或者右子树,那么让子树成为delete_node->parent的子树就好啦

  3. if delete_node既有左子树,又有右子树。那么从两个子树中找到合适的节点替代delete_node,直接前驱(左子树)或者直接后继(右子树),再删除这个直接前驱或者及直接后继,这个直接前驱是左子树的最右下节点,可能存在左子树但不会存在右子树;直接后继为右子树的最左下节点,可能存在右子树但不存在左子树,所以就转变成了1/2的删除情况

nt bst_delete(bst_node *p) {
  // 该节点为叶子节点,直接删除
  bst_node *q, *s;
  if (!p->rchild && !p->lchild) { //叶子节点
    free(p);
    p = NULL;
  } else if (!p->rchild) { // 右子树空则只需重接它的左子树
    q = p->lchild;         //获取右子树
    /*
    p->val = p->lchild->val;
    p->lchild=p->lchild->lchild;
    p->rchild=p->lchild->rchild;
    不是很好free
    */
    p->val = q->val;
    p->lchild = q->lchild;
    p->rchild = q->rchild;
    free(q);
  } else if (!p->lchild) { // 左子树空只需重接它的右子树
    q = p->rchild;
    /*
    p->val = p->rchild->val;
    p->lchild=p->rchild->lchild;
    p->rchild=p->rchild->rchild;
    不很好free
    */
    p->val = q->val;
    p->lchild = q->lchild;
    p->rchild = q->rchild;
    free(q);
  } else { // 左右子树均不空
    q = p;
    s = p->lchild; //寻找直接前驱
    while (s->rchild) {
      q = s;
      s = s->rchild;
    }                // 转左,然后向右到尽头,q为s的父节点
    p->val = s->val; // s指向被删结点的直接前驱
    if (q != p) {
      q->rchild = s->lchild; // 重接q的右子树
    } else {                 // q==p 接到左边
      q->lchild = s->lchild; // 重接q的左子树
    }
    free(s);
  }
  return 1;
}

效率分析

就平均查找时间二叉排序树和二分查找差不多,查找过程也差差不多,但是二叉排序树是不唯一的,根据关键字输入的顺序会生成不同的二叉排序树。插入和删除操作平均执行时间O(log2n)O(log_2n),假如他是单支的,即每次插入时都是有序的,那么它的查找效率就会降低到O(n)O(n)

对与二叉排序树的ASL计算,就是看他在二叉树上比较了多少次,也就是每一层有多少个结点,这些节点的比较次数就是层数累加后除以nn就是;

nodeinode_i表示第i层上的结点数目;

ASL=∑i=1hnodeinASL=\sum_{i=1}^{h}\frac{node_i}{n}

这也是其他关键字在结点上的二叉树查找的成功时的计算方法

AVL树

定义

AVL树(Adelson-Velsky and Landis Tree)是计算机科学中最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log2n)O(log_2n)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。

平衡因子:用左子树的高度减去右子树的高度(又是相反)。如果平衡因子为±1,0\pm1,0则该节点被认为是平衡的,如果平衡因子为±2\pm2则被认为是不平衡的,需要通过旋转实现平衡。

插入

由于新的数据的插入导致不平衡,首先找到里插入节点最近的平衡因子的绝对值为22的节点unblance_root,再对以unblance_root为根节点的子树,进行转圈圈。

  • LL平衡旋转(右单旋转)
展开详情

LL大法好!fl都过了6年的a...

由节点unblance_root的左孩子(L)的左子树(L)上插入了新节点,导致的不平衡。以unblacnce_root->lchid为根进行右旋;

  • RR平衡旋转(左单旋转)

与LL对称

  • LR平衡旋转(先左后右双旋转)

由节点unblance_root的左孩子(L)的右子树(R)上插入的新节点,导致的不平衡。以unblance_root->lchild->rchild为根进行做左旋,让它变到unblance_root->lchild的位置,这是情况就是LL了,再以unblance_root->lchild为根进行一次右旋;

  • RL平衡旋转(先右后左双旋转)

与LR对称

由于这个代码不是很好写,就直接用wiki上的图和Erlang(这又是啥语言:dizzy_face:)实现来看看好了。

rotate rotate_gif

balance(null) -> null;
balance({null, _, null}=Tree) -> Tree;
balance({Left, Value, Right}=Tree) ->
	Diff = count(Left)-count(Right),
	if (Diff < 2) and (Diff > -2)	->	{balance(Left), Value, balance(Right)};
	   (Diff > 1)				->	balance(rotate_right(Tree));
	   (Diff< -1)				->	balance(rotate_left(Tree));
	   true					->	exit('This is impossible!')
	end.

rotate_right({Left, Value, Right}) ->
	merge_max(Left, {null, Value, Right}).

rotate_left({Left, Value, Right}) ->
	merge_min(Right, {Left, Value, null}).

merge_min({null, Value, Right}, Tree2) ->
	{Tree2, Value, Right};
merge_min({Left, _, _}, Tree2) ->
	merge_min(Left, Tree2).

merge_max({Left , Value, null}, Tree2) ->
	{Left, Value, Tree2};
merge_max({_, _, Right}, Tree2) ->
	merge_max(Right, Tree2).

删除

以删除节点delete_node为例说明删除,

  1. 以二叉排序树的方法删除节点和连接节点;(用直接前驱或者直接后继替换)
  2. 从delete_node开始向上回溯,找到第一个不平衡节点delete_root(最小不平衡子树),y为delete_root高度最高的孩子,x为y高度最高的孩子节点;(就是找到删除的结点是导致了什么样得不平衡)
  3. 对以delete_root为根的子树进行平衡调正;

思想是被删除结点后有一边的子树少了一个结点,就可能会导致原本平衡的情况不平衡,向上找到不平衡的结点,再找另一边的孩子节点(高度最高的),再找孩子节点中高度最高的结点,这样删除导致的高度不一致问题和添加导致的高度不一致问题就一样了;(知道了是哪里高了导致的不平衡,做出对应的旋转)

  • y为delete_root的左孩子,x为y的左孩子(LL)
  • y为delete_root的左孩子,x为y的右孩子(LR)
  • y为delete_root的右孩子,x为y的右孩子(RR)
  • y为delete_root的右孩子,x为y的左孩子(RL)

查找

查找与BST相同,以NhN_h表示AVL在h行含有的最少节点数,Nh=Fh+2−1N_h=F_{h+2}-1(Fh+2F_{h+2}是斐波那契数列的第h+2项,根据斐波那契多项式得来)。且

N0=0N_0=0 N1=1N_1=1 N2=2N_2=2 Nh=Nh−1+Nh−2+1N_h=N_{h-1}+N_{h-2}+1

含有nn个节点的AVL最大深度为O(log2n)O(log_2n),同时平均查找长度也为它;

红黑树

感觉王道说的我不是很明白,可能是因为没有代码,去看了wiki的边记边看边想想。

红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。

性质

红黑树每个节点都带有颜色数学,红色或者黑色,在一般的二叉搜索树的要求上还有以下五个要求;

  1. 节点是红色或者黑色;
  2. 根是黑色;
  3. 所有叶子节点是黑色(叶子节点是NIL节点;
  4. 每个红色节点必须有两个黑色子节点;
  5. 从任一节点到其每个叶子节点的每个简单路径都包含相同数目的黑色节点;

rb-tree

展开详情

要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

插入

以BST的插入方法插入新的节点,并且规定红色(黑色太麻烦了,对性质5被破坏的调整太麻烦课捏。虽然红色也是还是很麻烦,可以同通过颜色调换color flips和旋转进行调整)

  • 性质1与性质3总是保持;
  • 性质4只在增加红色节点,重绘黑色节点为红色,或旋转会遭到破环;
  • 性质5只在增加黑色节点,重绘红色节点为黑色,或旋转会遭到破坏;

设插入节点为N,父节点为P,叔节点为U,爷节点为G;


node* get_g(node *n){
	return n->parent->parent;
}

node* get_u(node *n){
	if(n->parent == get_g(n)->lchild){
		return get_g(n)->rchild;
	}else{
		return get_g(n)->lchild;
	}
}

case 1 N位于树根上,涂成黑色满足性质2,且每个路径的黑节点数目++满足性质5

void insert_case1(node *n){
	if(n->parent == NULL){
		n->color = BLACK;
	}else{
		insert_case2(n);
	}
}

case 2 P为黑色,性质4不是失效,性质5不受影响;

void insert_case2(node *n){
	if(n->parent->color == BLACK){
		return;
	}else{
		insert_case3(n);
	}
}

case 3 P和U皆为红色。可以将P和U重绘为黑色,将G重绘为红色,以保持性质5。此使对N来说是满足红黑树的条件的,但是有可能破坏了G的条件,G有可能是根节点被破坏了性质2,或者G的父节点为红色节点被破坏了性质4。所以对G递归调用insert_case(G)来弥补可能破坏的情况(假装G是新加入的红色节点,好像不用假装)。

case3

void insert_case3(node *n){
	if(get_u(n) != NULL && get_u(n)->color == RED){
		n->parent->color = BLACK;
		get_u(n)->color = BLACK;
		get_g(n)->color = RED;
		insert_case1(get_g(n));
	}else{
		insert_case4(n);
	}
}

case 4(LR) P是红色,U是黑色或者缺少(也是黑的),N为P的右节点,P为G的左节点。类似在AVL里很熟悉的LR,先针对P进行一次左旋,这是以前的父节点P就失效了,以case 5的方法来解决,此时变换虽然会改变前往一些节点的路径,但两个节点都是红色der,情况5还是满足的

case4

void insert_case4(node *n){
	if(n == n->parent->rchild && n->parent == get_g(n)->lchild){ //LR
		rotate_left(n);
		n = n->left;
	}else if(n == n->parent->lchid && n->parent == get_g(n)->rchild){ //RL
		rodate_right(n);
		n = n->right;
	}
	insert_case(5);

}

case 5(LL) P是红色,U是黑色或者缺少的,N为P的左节点,P为G的左节点。类似是AVL的LL,对G进行一次右旋。旋转后,P的地址为N的数据,G的地址为P的数据,U的地址为G的数据。但是P和N都是红色还是连着的,当然G是黑色的,所以得重新上个色,切换以前的P和G的颜色,同时能满足性质4和性质5,原先的的G现在的P还是黑的。

case5

void insert_case5(node *n){
	n->parent->color = BLACK;
	get_g(n)->color = RED;
	if(n = n->parent->rchild && n->parent = get_d(n)->rchild){
		right_rotate(get_d(n));
	}else if(n = n->parent->lchild && n->parent = get_d(n)->lchild){
		left_rotate(get_d(n));
	}
}

删除

简化问题:如果需要删除的节点有两个儿子,那么问题可以转换为删除另一个只有一个儿子的节点。(第一眼看一脸懵逼:shit:)

接下来就只用讨论删除只有一个儿子的节点辣!如果儿子为空都是那个假装的叶子节点,就随便选一个看作儿子。

如果删除一个红色节点(就插入知识是看是会比较简单的),它的父亲和儿子节点都是黑色,可以简单的用儿子节点替换,通过路径只是少了一个红色节点,没有问题保证了性质5。

还有一种比较简单的情况,删除一个黑色节点,他的儿子节点是红色,如果直接上儿子节点上来的话,会破坏性质5黑色的少了,还可能红红相连破坏性质4,直接把这个红色节点变成黑色就没有问题了。

复杂的是,如果删除一个黑色节点,他的儿子节点也是黑色。 首先需要把被删除的节点替换成儿子节点N,他的兄弟则为S,P为N的父亲即被删除节点的父亲,SLS_L和SRS_R分别为S的左右儿子。

node* get_s(node *n){
  if(n == n->parent->lchild){
    return n->parent->rchild;
  }else{
    return n->parent->lchild;
  }
}

叶节点代指空节点,但是用实际节点更方便。

void delete_node(node *n){
	//n最多只有一个儿子的节点
  node *child = is_leaf(n->rchild) ? n->lchild : n->lchild; //找出存在的那一部分的节点
  replace(child,n); //使用儿子节点替换删除节点
  if(n->color == BLACK){ //删除节点节点是黑色
    if(child->color == RED){ //替换节点是红色
      child->color = BLACK;
    }else{ //替换节点是黑色,会产生黑色节点少一个,特殊处理
      delete_case1(child);
    }		
  }
  free(n);
}

N和他的初始的被删除的父亲都是黑色,删除父亲导致的性质5错误,需要重新平衡树。

:::tip n为替换后的节点 :::

case 1 删完后,N是新根,他没有父亲,从所有路径删除了一个黑色节点,没有破坏性质5;

void delete_case1(node *n){
  if(n->parent != NULL){
    delete_case2(n);
  }
}

case 2 S为红色,对P进行左旋,将S变为N的祖父,对调P与S的颜色,路径上黑色节点数没变,保持性质5,N有了一个红色的P和黑色的G

LEAVE A LITTLE NOTE

聊聊这篇记录

匿名也可以留言。昵称、邮箱和网址均为选填,邮箱不会公开。评论审核后显示。

搜索文章

输入关键词,寻找一段记忆。

Tab 切换结果 · Enter 打开 · Esc 关闭