注:二叉搜索树,二叉排序树,二叉查找树是同一种数据结构

定义

二叉排序树要么是空二叉树,要么具有如下特点:

  • 二叉排序树中,如果其根结点有左子树,那么左子树上所有结点的值都小于根结点的值;
  • 二叉排序树中,如果其根结点有右子树,那么右子树上所有结点的值都大小根结点的值;
  • 二叉排序树的左右子树也要求都是二叉排序树;

103FJ439-0.png

增删改查

查找

二叉搜索树中查找某关键字时,查找过程类似于次优二叉树,在二叉搜索树不为空树的前提下,首先将被查找值同树的根结点进行比较,会有 3 种不同的结果:

  • 如果相等,查找成功;
  • 如果比较结果为根结点的关键字值较大,则说明该关键字可能存在其左子树中;
  • 如果比较结果为根结点的关键字值较小,则说明该关键字可能存在其右子树中;

实现函数为:(运用递归的方法)

  1. BiTree SearchBST(BiTree T,KeyType key)
  2. {
  3. //如果递归过程中 T 为空,则查找结果,返回NULL;或者查找成功,返回指向该关键字的指针
  4. if (!T || key==T->data)
  5. {
  6. return T;
  7. }else if(key<T->data)
  8. {
  9. //递归遍历其左孩子
  10. return SearchBST(T->lchild, key);
  11. }else
  12. {
  13. //递归遍历其右孩子
  14. return SearchBST(T->rchild, key);
  15. }
  16. }

插入

二叉搜索树中插入某关键字时,大概分为两步

  1. 查找该元素所处的位置
  2. 插入该元素

当查找失败时访问的最后一个结点的左孩子或者右孩子,具体实现代码为:

  1. BOOL SearchBST(BiTree T,KeyType key,BiTree f,BiTree *p)
  2. {
  3. //如果 T 指针为空,说明查找失败,令 p 指针指向查找过程中最后一个叶子结点,并返回查找失败的信息
  4. if (!T)
  5. {
  6. *p=f;
  7. return false;
  8. }
  9. //如果相等,令 p 指针指向该关键字,并返回查找成功信息
  10. else if(key==T->data)
  11. {
  12. *p=T;
  13. return true;
  14. }
  15. //如果 key 值比 T 根结点的值小,则查找其左子树;反之,查找其右子树
  16. else if(key<T->data)
  17. {
  18. return SearchBST(T->lchild,key,T,p);
  19. }else
  20. {
  21. return SearchBST(T->rchild,key,T,p);
  22. }
  23. }
  24. //插入函数
  25. BOOL InsertBST(BiTree T,ElemType e)
  26. {
  27. BiTree p=NULL;
  28. //如果查找不成功,需做插入操作
  29. if (!SearchBST(T, e,NULL,&p))
  30. {
  31. //初始化插入结点
  32. BiTree s=(BiTree)malloc(sizeof(BiTree));
  33. s->data=e;
  34. s->lchild=s->rchild=NULL;
  35. //如果 p 为NULL,说明该二叉排序树为空树,此时插入的结点为整棵树的根结点
  36. if (!p)
  37. {
  38. T=s;
  39. }
  40. //如果 p 不为 NULL,则 p 指向的为查找失败的最后一个叶子结点,只需要通过比较 p 和 e 的值确定 s 到底是 p 的左孩子还是右孩子
  41. else if(e<p->data)
  42. {
  43. p->lchild=s;
  44. }else
  45. {
  46. p->rchild=s;
  47. }
  48. return true;
  49. }
  50. //如果查找成功,不需要做插入操作,插入失败
  51. return false;
  52. }

删除

在查找过程中,如果在使用二叉搜索树表示的动态查找表中删除某个数据元素时,需要在成功删除该结点的同时,依旧使这棵树为二叉排序树。假设要删除的为结点 p,则对于二叉排序树来说,需要根据结点 p 所在不同的位置作不同的操作,有以下 3 种可能:

  • 结点 p 为叶子结点,此时只需要删除该结点,并修改其双亲结点的指针即可;
  • 结点 p 只有左子树或者只有右子树,如果 p 是其双亲节点的左孩子,则直接将 p 节点的左子树或右子树作为其双亲节点的左子树;反之也是如此,如果 p 是其双亲节点的右孩子,则直接将 p 节点的左子树或右子树作为其双亲节点的右子树;
  • 结点 p 左右子树都有,此时有两种处理方式:

    • 令结点 p 的左子树为其双亲结点的左子树;结点 p 的右子树为其自身直接前驱结点的右子树,如图所示;


    103FL5J-2.png

  • 用结点 p 的直接前驱(或直接后继)来代替结点 p,同时在二叉排序树中对其直接前驱(或直接后继)做删除操作。如图使用直接前驱代替结点 p:

103FM531-3.png

图 4中,在对左图进行中序遍历时,得到的结点 p 的直接前驱结点为结点 s,所以直接用结点 s 覆盖结点 p,由于结点 s 还有左孩子,根据第 2 条规则,直接将其变为双亲结点的右孩子。

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #define TRUE 1
  4. #define FALSE 0
  5. #define ElemType int
  6. #define KeyType int
  7. /* 二叉排序树的节点结构定义 */
  8. typedef struct BiTNode
  9. {
  10. int data;
  11. struct BiTNode *lchild, *rchild;
  12. } BiTNode, *BiTree;
  13. //二叉排序树查找算法
  14. int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
  15. {
  16. //如果 T 指针为空,说明查找失败,令 p 指针指向查找过程中最后一个叶子结点,并返回查找失败的信息
  17. if (!T)
  18. {
  19. *p = f;
  20. return FALSE;
  21. }
  22. //如果相等,令 p 指针指向该关键字,并返回查找成功信息
  23. else if (key == T->data)
  24. {
  25. *p = T;
  26. return TRUE;
  27. }
  28. //如果 key 值比 T 根结点的值小,则查找其左子树;反之,查找其右子树
  29. else if (key < T->data)
  30. {
  31. return SearchBST(T->lchild, key, T, p);
  32. }
  33. else
  34. {
  35. return SearchBST(T->rchild, key, T, p);
  36. }
  37. }
  38. int InsertBST(BiTree *T, ElemType e)
  39. {
  40. BiTree p = NULL;
  41. //如果查找不成功,需做插入操作
  42. if (!SearchBST((*T), e, NULL, &p))
  43. {
  44. //初始化插入结点
  45. BiTree s = (BiTree)malloc(sizeof(BiTNode));
  46. s->data = e;
  47. s->lchild = s->rchild = NULL;
  48. //如果 p 为NULL,说明该二叉排序树为空树,此时插入的结点为整棵树的根结点
  49. if (!p)
  50. {
  51. *T = s;
  52. }
  53. //如果 p 不为 NULL,则 p 指向的为查找失败的最后一个叶子结点,只需要通过比较 p 和 e 的值确定 s 到底是 p 的左孩子还是右孩子
  54. else if (e < p->data)
  55. {
  56. p->lchild = s;
  57. }
  58. else
  59. {
  60. p->rchild = s;
  61. }
  62. return TRUE;
  63. }
  64. //如果查找成功,不需要做插入操作,插入失败
  65. return FALSE;
  66. }
  67. //删除函数
  68. int Delete(BiTree *p)
  69. {
  70. BiTree q, s;
  71. //情况 1,结点 p 本身为叶子结点,直接删除即可
  72. if (!(*p)->lchild && !(*p)->rchild)
  73. {
  74. *p = NULL;
  75. }
  76. else if (!(*p)->lchild)
  77. { //左子树为空,只需用结点 p 的右子树根结点代替结点 p 即可;
  78. q = *p;
  79. *p = (*p)->rchild;
  80. free(q);
  81. }
  82. else if (!(*p)->rchild)
  83. {//右子树为空,只需用结点 p 的左子树根结点代替结点 p 即可;
  84. q = *p;
  85. *p = (*p)->lchild;//这里不是指针 *p 指向左子树,而是将左子树存储的结点的地址赋值给指针变量 p
  86. free(q);
  87. }
  88. else
  89. {//左右子树均不为空,采用第 2 种方式
  90. q = *p;
  91. s = (*p)->lchild;
  92. //遍历,找到结点 p 的直接前驱
  93. while (s->rchild)
  94. {
  95. q = s;
  96. s = s->rchild;
  97. }
  98. //直接改变结点 p 的值
  99. (*p)->data = s->data;
  100. //判断结点 p 的左子树 s 是否有右子树,分为两种情况讨论
  101. if (q != *p)
  102. {
  103. q->rchild = s->lchild;//若有,则在删除直接前驱结点的同时,令前驱的左孩子结点改为 q 指向结点的孩子结点
  104. }
  105. else
  106. {
  107. q->lchild = s->lchild;//否则,直接将左子树上移即可
  108. }
  109. free(s);
  110. }
  111. return TRUE;
  112. }
  113. int DeleteBST(BiTree *T, int key)
  114. {
  115. if (!(*T)) {//不存在关键字等于key的数据元素
  116. return FALSE;
  117. }
  118. else
  119. {
  120. if (key == (*T)->data)
  121. {
  122. Delete(T);
  123. return TRUE;
  124. }
  125. else if (key < (*T)->data)
  126. {
  127. //使用递归的方式
  128. return DeleteBST(&(*T)->lchild, key);
  129. }
  130. else
  131. {
  132. return DeleteBST(&(*T)->rchild, key);
  133. }
  134. }
  135. }
  136. void order(BiTree t)//中序输出
  137. {
  138. if (t == NULL)
  139. {
  140. return;
  141. }
  142. order(t->lchild);
  143. printf("%d ", t->data);
  144. order(t->rchild);
  145. }
  146. int main()
  147. {
  148. int i;
  149. int a[5] = { 3,4,2,5,9 };
  150. BiTree T = NULL;
  151. for (i = 0; i < 5; i++)
  152. {
  153. InsertBST(&T, a[i]);
  154. }
  155. printf("中序遍历二叉排序树:\n");
  156. order(T);
  157. printf("\n");
  158. printf("删除3后,中序遍历二叉排序树:\n");
  159. DeleteBST(&T, 3);
  160. order(T);
  161. }

运行结果:
中序遍历二叉排序树:
2 3 4 5 9
删除3后,中序遍历二叉排序树:
2 4 5 9

时间复杂度

使用二叉搜索树在做查找操作的时间复杂度同建立的二叉树本身的结构有关。即使查找表中各数据元素完全相同,但是不同的排列顺序,构建出的二叉排序树大不相同。
例如:查找表 {45,24,53,12,37,93} 和表 {12,24,37,45,53,93} 各自构建的二叉排序树图下图所示:

103FK300-4.png

使用二叉排序树实现动态查找操作的过程,实际上就是从二叉排序树的根结点到查找元素结点的过程,所以时间复杂度同被查找元素所在的树的深度(层次数)有关。

最好的情况是 O(logn),存在于完全二叉排序树情况下,其访问性能近似于折半查找;
最差时候会是 O(n),比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才