“探究二叉搜索树:从原理到实现“

简介: “探究二叉搜索树:从原理到实现“

目录


  • 1、定义
  • 2、数据表示
  • 3、创建节点函数(初始化)
  • 4、遍历
  • 5、查找
  • 6、插入
  • 7、删除
  • 8、菜单—测试


正文


1、定义


一颗二叉查找树(BST)是一颗二叉树,其中每个节点都含有一个Comparable的键(以及相关联的值)且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键红宝书中的定义

这里说说自己的理解,二叉搜索树其实可以理解为二叉树的派生类,它在二叉树的基础上多了个有序;有序并不是线性结构的有序(前面和后面比大小)而是树状结构的有序(左根右或者右根左)中序遍历的结果完全可以看成树状结构的线性有序


2、数据表示


我们这里定义一个结构体,里面存储一个数据域以及两个分别指向每个节点左右子树的指针

遍历,我们这里把先序、中序、后序都列举出来实现了,所以我们这里使用枚举来提高代码的可读性

typedef struct searchThree 
{
  int data;
  struct searchThree* _pLeft;
  struct searchThree* _pRight;
}threeNode;
#define SIZE sizeof(threeNode)
enum threeType{_PRE,_MID,_LST};//先,中,后


3、创建节点函数(初始化)


初始化时需要把新节点的数据赋值以及指针直空,这里有更简便的写法,其一使用memset函数一步到位,其二使用calloc,两者都能使代码跟简练,这里为了描述更加清晰,使用了较为笨拙的方法

threeNode* createNode(int newData)
{
  threeNode* newNode = malloc(SIZE);
  assert(newNode);
  newNode->data = newData;
  newNode->_pLeft = newNode->_pRight = NULL;
  return newNode;
}


4、遍历


遍历函数比较简单,这里使用了菜单式以及递归

void trval(threeNode* root, enum threeType type)
{
  switch (type)
  {
  case _PRE:
    printf("先序遍历:");
    _preTrval(root);
    printf("\n");
    break;
  case _MID:
    printf("中序遍历:");
    _midTrval(root);
    printf("\n");
    break;
  case _LST:
    printf("后序遍历:");
    _lstTrval(root);
    printf("\n");
    break;
  default:
    printf("输入错误!!!");
    break;
  }
}
void _preTrval(threeNode* root)
{
  if (NULL == root) return;
  printf("%2d ", root->data);
  _preTrval(root->_pLeft);
  _preTrval(root->_pRight);
}
void _midTrval(threeNode* root)
{
  if (NULL == root) return;
  _midTrval(root->_pLeft);
  printf("%2d ", root->data);
  _midTrval(root->_pRight);
}
void _lstTrval(threeNode* root)
{
  if (NULL == root) return;
  _lstTrval(root->_pLeft);
  _lstTrval(root->_pRight);
  printf("%2d ", root->data);
}


5、查找


二叉搜索树的查找方法和二分法极为的类似,如果跟节点就是要查找的值就返回根节点,如果小于就递归查找左子树,否则就递归查找右子树

threeNode* find_Node(threeNode* root, int findData)
{
  if (NULL == root) return NULL;
  if (root->data == findData) return root;
  else if (findData < root->data) return find_Node(root->_pLeft,findData);
  return find_Node(root->_pRight, findData);
}


6、插入


插入的思想就是基于二叉树的有序性,如果是一个空树,直接插入到根节点,如果新数据大于根节点的数据,那么递归插入到右子树中,反之如果新数据小于根节点的数据,那么就递归插入左子树中


由于C语言没有引用,所以这里使用了二级指针,那么该如何理解使用了二级指针?

这个函数使用了二级指针的原因是因为要通过修改指针的指向来改变树的结构,也就是说,要动态地修改树的节点指针,而不是简单地修改节点的值。在这个函数中,root 是一个指向 treeNode* 类型的指针,也就是一个指向指针的指针,它存储了树的根节点的指针。使用二级指针可以使函数在修改根节点的指向时,直接修改 root 指针的值,而不是返回新的根节点指针,提高了函数的效率和灵活性。因此,传递二级指针是为了在函数中修改节点指针的指向,而不是节点的值。如果只传一级指针,函数中对该指针的操作仅仅是对指针所指向的内存空间的修改,不会修改指针本身的指向。因此,如果传入一级指针,函数中修改节点指针的指向将不会反映在原来的指针上,也就是说树的结构不会改变。因此,必须使用二级指针来修改指针的指向,从而改变树的结构。简单来说就是如果要修改函数里面一级指针,那就要传入二级指针,这样才会修改外面的变量,这里创建新节点creaNode,就需要修改

void insert(threeNode** root, int insertData)
{
  if (NULL == root) return;
  if (*root== NULL)
  {
    *root = createNode(insertData);
    return;
  }
  if (insertData < (*root)->data) insert(&((*root)->_pLeft), insertData);
  else insert(&((*root)->_pRight), insertData);
}


7、删除


删除操作也是二叉搜索树的核心


要删的是根节点


有右孩子

if (pDel->_pRight) 
{
  //找到要删除节点的右孩子的最左孩子
  pTemp = pDel->_pRight;
  while (pTemp->_pLeft)pTemp = pTemp->_pLeft;
  //要删除节点的左孩子成为最左孩子的左孩子
  pTemp->_pLeft = pDel->_pLeft;
  //要删除节点的右孩子成为根节点
  (*root) = pDel->_pRight;
}

cb6ab00d1a644bc282a409ab845f60bc.png


没有右孩子

1.png


else //没有右孩子,要删除节点的左孩子成为新的根节点
{
  (*root) = pDel->_pLeft;
}


要删除的不是根节点


要删出的不是根节点第一步要找到需要删除的节点的父节点,因为除了删除的是叶子节点的情况下,其它的情况都需要进行连接,不能因为删一个节点,把要删除节点后面的节点都删除了

这里采用的快慢指针来查找父节点,先让要删除的节点指向根节点pDel,如果该值等于要删除的值,直接把该节点存放起来,然后返回,如果不是先让代表父节点的指针pPartent跟上pDel,然后判断pDel指向节点的值和要删除节点的值的大小关系,如果pDel的值大,则pdel就往该节点的左子树走,反之往右子树走,如此循环下去,直到找到要删除的节点,此时pPartent就是要删除节点的父节点,示意图如下


//不是根节点,需要父节点
threeNode* _partentNode = NULL;
pDel = *root;
//找父节点
while (1)
{
  if (pDel->data == delData) break;
  _partentNode = pDel;
  pDel = ((delData < pDel->data) ? (pDel->_pLeft) : (pDel->_pRight));
}

075d536f08895460bf65e9297a7cb46.jpg


有右孩子


和有根节点的删除方式差不多,这里就是多了一个要判断需要删除的节点是父节点的左孩子还是右孩子,连接的时候需要用到

if (pDel->_pRight) 
{
  pTemp = pDel->_pRight;
  while (pTemp->_pLeft) pTemp = pTemp->_pLeft;
  pTemp->_pLeft = pDel->_pLeft;
  if (pDel == _partentNode->_pLeft)
  _partentNode->_pLeft = pDel->_pRight;
  else
  _partentNode->_pRight = pDel->_pRight;
}

2.png


没有右孩子


直接连接

else
{
  if (pDel == _partentNode->_pLeft)
    _partentNode->_pLeft = pDel->_pLeft;
  else
    _partentNode->_pRight = pDel->_pLeft;
}

3.png

删除综合代码

bool deleteNode(threeNode** root, int delData)
{
  // NULL == *root必须放在后面,因为||有短路功能,这个条件在find函数已经判断过了
  if (NULL == root || NULL == *root) return false;
  //找节点
  threeNode* pDel = find_Node(*root, delData);
  assert(pDel);
  //临时节点
  threeNode* pTemp = NULL;
  //如果是根节点
  if (pDel == (*root)) 
  {
    //如果有右孩子
    if (pDel->_pRight) 
    {
      //找到要删除节点的右孩子的最左孩子
      pTemp = pDel->_pRight;
      while (pTemp->_pLeft)pTemp = pTemp->_pLeft;
      //要删除节点的左孩子成为最左孩子的左孩子
      pTemp->_pLeft = pDel->_pLeft;
      //要删除节点的右孩子成为根节点
      (*root) = pDel->_pRight;
    }
    else //没有右孩子,要删除节点的左孩子成为新的根节点
    {
      (*root) = pDel->_pLeft;
    }
    free(pDel);
    pDel = NULL;
    return true;
  }
  //不是根节点,需要父节点
  threeNode* _partentNode = NULL;
  pDel = *root;
  //找父节点
  while (1)
  {
    if (pDel->data == delData) break;
    _partentNode = pDel;
    pDel = ((delData < pDel->data) ? (pDel->_pLeft) : (pDel->_pRight));
  }
  if (pDel->_pRight) 
  {
    pTemp = pDel->_pRight;
    while (pTemp->_pLeft) pTemp = pTemp->_pLeft;
    pTemp->_pLeft = pDel->_pLeft;
    if (pDel == _partentNode->_pLeft)
      _partentNode->_pLeft = pDel->_pRight;
    else
      _partentNode->_pRight = pDel->_pRight;
  }
  else
  {
    if (pDel == _partentNode->_pLeft)
      _partentNode->_pLeft = pDel->_pLeft;
    else
      _partentNode->_pRight = pDel->_pLeft;
  }
  free(pDel);
  pDel = NULL;
  return true;
}


8、菜单—测试


最后编写一个菜单测试测试效果

4.png

void menu(threeNode** root)
{
  printf("二叉树测试系统:\n");
  printf("    1、插入\n");
  printf("    2、删除\n");
  printf("    3、遍历\n");
  printf("    4、退出\n");
  int n;
  while (1)
  {
    int data;
    printf("请输入选择:");
    scanf("%d", &n);
    switch (n) 
    {
    case 1:
      printf("请输入插入数据:");
      scanf("%d", &data);
      insert(root, data);
      break;
    case 2:
      printf("请输入删除数据:");
      scanf("%d", &data);
      deleteNode(root, data);
      break;
    case 3:
      trval(*root, _PRE);
      trval(*root, _MID);
      trval(*root, _LST);
      break;
    case 4:
      printf("欢迎使用!!!");
      return;
      break;
    default:
      printf("输入错误!!!");
      break;
    }
  }
}

相关文章
|
8月前
|
机器学习/深度学习 存储 算法
【数据结构与算法篇】 二叉树的性质(补充)
【数据结构与算法篇】 二叉树的性质(补充)
55 0
|
19天前
|
C++
【C++高阶(三)】AVL树深度剖析&模拟实现
【C++高阶(三)】AVL树深度剖析&模拟实现
|
1月前
|
算法 搜索推荐 数据库
【数据结构】二叉树算法讲解(定义+算法原理+源码)
【数据结构】二叉树算法讲解(定义+算法原理+源码)
|
5月前
|
C++
【数据结构&C++】超详细一文带小白轻松全面理解 [ 二叉平衡搜索树-AVL树 ]—— [从零实现&逐过程分析&代码演示&简练易懂]
【数据结构&C++】超详细一文带小白轻松全面理解 [ 二叉平衡搜索树-AVL树 ]—— [从零实现&逐过程分析&代码演示&简练易懂]
|
9月前
|
存储 算法
二叉搜索树:原理与应用
二叉搜索树:原理与应用
88 0
|
存储 算法 Java
【算法练习】有趣的括号匹配问题(思路+ 图解 +优化)基于java实现
1.题目描述 小洛看着一堆只包含’(‘和’)‘的括号序列犯愁了,小洛想知道这串序列里最长正确匹配的序列长度是多少,你能帮帮小洛吗?
【算法练习】有趣的括号匹配问题(思路+ 图解 +优化)基于java实现
|
11月前
|
存储 算法 Java
Java数据结构与算法分析(十)B树图文详解(含完整代码)
迄今为止,已经介绍了《 二叉查找树 》和《 AVL树 》,我们始终假设可以把整个数据结构存储在内存中。可是,如果数据多到内存装不下,这就意味着必须把数据放在磁盘上,显然这些数据结构不再适用。 问题在于磁盘的I/O速度是远远不如内存访问速度的,然而从一棵树中查找到某个元素,必须从根节点一层层往下找,这每一次查找便是一次I/O操作。为了提高性能,就必须要减少查找的次数。 如能减少树的高度、增加每个节点中的元素数,便是种有效的解决方案。实现这种想法的一种方法是使用B树。
81 0
|
存储 消息中间件 JavaScript
图解B+树的生成过程!
图解B+树的生成过程!
二叉搜索树的模拟实现——Java数据结构
二叉搜索树的模拟实现——Java数据结构
|
存储 Java Linux
C++ 第八节&数据结构 第七节 ——二叉搜索树 AVL树 红黑树(底层原理图+模拟实现)
每一个关键码key,都有与之对应的值Value,即&lt;Key, Value&gt;的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文&lt;word, chinese&gt;就构成一种键值对;
196 0
C++ 第八节&数据结构 第七节 ——二叉搜索树 AVL树 红黑树(底层原理图+模拟实现)