python数据结构与算法-17_二叉查找树

二叉查找树(BST)

二叉树的一种应用就是来实现堆,今天我们再看看用二叉查找树(Binary Search Tree, BST)。
前面有章节说到了查找操作,包括线性查找、二分查找、哈希查找等,线性查找效率比较低,二分又要求必须是有序的序列,
为了维持有序插入的代价比较高、哈希查找效率很高但是浪费空间。能不能有一种插入和查找都比较快的数据结构呢?二叉查找树就是这样一种结构,可以高效地插入和查询节点。

BST 定义

二叉查找树是这样一种二叉树结构,它的每个节点包含一个 key 和它附带的数据,对于每个内部节点 V:

  • 所有 key 小于 V 的都被存储在 V 的左子树
  • 所有 key 大于 V 的都存储在 V 的右子树

在这里插入图片描述

注意这个限制条件,可别和堆搞混了。说白了就是对于每个内部节点,左子树的 key 都比它小,右子树都比它大。
如果中序遍历(二叉树遍历讲过了)这颗二叉树,你会发现输出的顺序正好是有序的。
我们先来定义一下 BST 的节点结构:

class BSTNode(object):def __init__(self, key, value, left=None, right=None):self.key, self.value, self.left, self.right = key, value, left, right

构造一个 BST

我们还像之前构造二叉树一样,按照上图构造一个 BST 用来演示:

class BST(object):def __init__(self, root=None):self.root = root@classmethoddef build_from(cls, node_list):cls.size = 0key_to_node_dict = {}for node_dict in node_list:key = node_dict['key']key_to_node_dict[key] = BSTNode(key, value=key)   # 这里值暂时用 和 key一样的for node_dict in node_list:key = node_dict['key']node = key_to_node_dict[key]if node_dict['is_root']:root = nodenode.left = key_to_node_dict.get(node_dict['left'])node.right = key_to_node_dict.get(node_dict['right'])cls.size += 1return cls(root)NODE_LIST = [{'key': 60, 'left': 12, 'right': 90, 'is_root': True},{'key': 12, 'left': 4, 'right': 41, 'is_root': False},{'key': 4, 'left': 1, 'right': None, 'is_root': False},{'key': 1, 'left': None, 'right': None, 'is_root': False},{'key': 41, 'left': 29, 'right': None, 'is_root': False},{'key': 29, 'left': 23, 'right': 37, 'is_root': False},{'key': 23, 'left': None, 'right': None, 'is_root': False},{'key': 37, 'left': None, 'right': None, 'is_root': False},{'key': 90, 'left': 71, 'right': 100, 'is_root': False},{'key': 71, 'left': None, 'right': 84, 'is_root': False},{'key': 100, 'left': None, 'right': None, 'is_root': False},{'key': 84, 'left': None, 'right': None, 'is_root': False},
]
bst = BST.build_from(NODE_LIST)

BST 操作

查找

如何查找一个指定的节点呢,根据定义我们知道每个内部节点左子树的 key 都比它小,右子树的 key 都比它大,所以
对于带查找的节点 search_key,从根节点开始,如果 search_key 大于当前 key,就去右子树查找,否则去左子树查找。 一直到当前节点是 None 了说明没找到对应 key。

在这里插入图片描述

好,撸代码:

    def _bst_search(self, subtree, key):if subtree is None:   # 没找到return Noneelif key < subtree.key:return self._bst_search(subtree.left, key)elif key > subtree.key:return self._bst_search(subtree.right, key)else:return subtreedef get(self, key, default=None):node = self._bst_search(self.root, key)if node is None:return defaultelse:return node.value

获取最大和最小 key 的节点

其实还按照其定义,最小值就一直向着左子树找,最大值一直向右子树找,递归查找就行。

    def _bst_min_node(self, subtree):if subtree is None:return Noneelif subtree.left is None:   # 找到左子树的头return subtreeelse:return self._bst_min_node(subtree.left)def bst_min(self):node = self._bst_min_node(self.root)return node.value if node else None

插入

插入节点的时候我们需要一直保持 BST 的性质,每次插入一个节点,我们都通过递归比较把它放到正确的位置。
你会发现新节点总是被作为叶子结点插入。(请你思考这是为什么)
在这里插入图片描述

    def _bst_insert(self, subtree, key, value):""" 插入并且返回根节点:param subtree::param key::param value:"""if subtree is None:   # 插入的节点一定是根节点,包括 root 为空的情况subtree = BSTNode(key, value)elif key < subtree.key:subtree.left = self._bst_insert(subtree.left, key, value)elif key > subtree.key:subtree.right = self._bst_insert(subtree.right, key, value)return subtreedef add(self, key, value):node = self._bst_search(self.root, key)if node is not None:   # 更新已经存在的 keynode.value = valuereturn Falseelse:self.root = self._bst_insert(self.root, key, value)self.size += 1return True

删除节点

删除操作相比上边的操作要麻烦很多,首先需要定位一个节点,删除节点后,我们需要始终保持 BST 的性质。
删除一个节点涉及到三种情况:

  • 节点是叶节点
  • 节点有一个孩子
  • 节点有两个孩子

我们分别来看看三种情况下如何删除一个节点:

删除叶节点

这是最简单的一种情况,只需要把它的父亲指向它的指针设置为 None 就好。

在这里插入图片描述

删除只有一个孩子的节点

删除有一个孩子的节点时,我们拿掉需要删除的节点,之后把它的父亲指向它的孩子就行,因为根据 BST
左子树都小于节点,右子树都大于节点的特性,删除它之后这个条件依旧满足。

在这里插入图片描述

删除有两个孩子的内部节点

假如我们想删除 12 这个节点改怎么做呢?你的第一反应可能是按照下图的方式:
在这里插入图片描述

但是这种方式可能会影响树的高度,降低查找的效率。这里我们用另一种非常巧妙的方式。
还记得上边提到的吗,如果你中序遍历 BST 并且输出每个节点的 key,你会发现就是一个有序的数组。
[1 4 12 23 29 37 41 60 71 84 90 100]。这里我们定义两个概念,逻辑前任(predecessor)和后继(successor),请看下图:

在这里插入图片描述

12 在中序遍历中的逻辑前任和后继分别是 4 和 23 节点。于是我们还有一种方法来删除 12 这个节点:

  • 找到待删除节点 N(12) 的后继节点 S(23)
  • 复制节点 S 到节点 N
  • 从 N 的右子树中删除节点 S,并更新其删除后继节点后的右子树

说白了就是找到后继并且替换,这里之所以能保证这种方法是正确的,你会发现替换后依旧是保持了 BST 的性质。
有个问题是如何找到后继节点呢?待删除节点的右子树的最小的节点不就是后继嘛,上边我们已经实现了找到最小 key 的方法了。

在这里插入图片描述

我们开始编写代码实现,和之前的操作类似,我们还是通过辅助函数的形式来实现,这个递归函数会比较复杂,请你仔细理解:

    def _bst_remove(self, subtree, key):"""删除节点并返回根节点"""if subtree is None:return Noneelif key < subtree.key:subtree.left = self._bst_remove(subtree.left, key)return subtreeelif key > subtree.key:subtree.right = self._bst_remove(subtree.right, key)return subtreeelse:  # 找到了需要删除的节点if subtree.left is None and subtree.right is None:    # 叶节点,返回 None 把其父亲指向它的指针置为 Nonereturn Noneelif subtree.left is None or subtree.right is None:  # 只有一个孩子if subtree.left is not None:return subtree.left   # 返回它的孩子并让它的父亲指过去else:return subtree.rightelse:  # 俩孩子,寻找后继节点替换,并从待删节点的右子树中删除后继节点successor_node = self._bst_min_node(subtree.right)subtree.key, subtree.value = successor_node.key, successor_node.valuesubtree.right = self._bst_remove(subtree.right, successor_node.key)return subtreedef remove(self, key):assert key in selfself.size -= 1return self._bst_remove(self.root, key)

完整代码你可以在本章的 bst.py 找到。
另外推荐一个可以在线演示过程的网址大家可以手动执行下看看效果: https://www.cs.usfca.edu/~galles/visualization/BST.html

时间复杂度分析

上边介绍的操作时间复杂度和二叉树的形状有关。平均来说时间复杂度是和树的高度成正比的,树的高度 h 是 log(n),
但是最坏情况下以上操作的时间复杂度都是 O(n)。为了改善 BST 有很多变种,感兴趣请参考延伸阅读中的内容。

在这里插入图片描述

源码

# -*- coding: utf-8 -*-class BSTNode(object):def __init__(self, key, value, left=None, right=None):self.key, self.value, self.left, self.right = key, value, left, rightclass BST(object):def __init__(self, root=None):self.root = root@classmethoddef build_from(cls, node_list):cls.size = 0key_to_node_dict = {}for node_dict in node_list:key = node_dict['key']key_to_node_dict[key] = BSTNode(key, value=key)   # 这里值暂时用 和 key一样的for node_dict in node_list:key = node_dict['key']node = key_to_node_dict[key]if node_dict['is_root']:root = nodenode.left = key_to_node_dict.get(node_dict['left'])node.right = key_to_node_dict.get(node_dict['right'])cls.size += 1return cls(root)def _bst_search(self, subtree, key):if subtree is None:   # 没找到return Noneelif key < subtree.key:return self._bst_search(subtree.left, key)elif key > subtree.key:return self._bst_search(subtree.right, key)else:return subtreedef __contains__(self, key):"""实现 in 操作符"""return self._bst_search(self.root, key) is not Nonedef get(self, key, default=None):node = self._bst_search(self.root, key)if node is None:return defaultelse:return node.valuedef _bst_min_node(self, subtree):if subtree is None:return Noneelif subtree.left is None:   # 找到左子树的头return subtreeelse:return self._bst_min_node(subtree.left)def bst_min(self):node = self._bst_min_node(self.root)return node.value if node else Nonedef _bst_insert(self, subtree, key, value):""" 插入并且返回根节点:param subtree::param key::param value:"""if subtree is None:   # 插入的节点一定是根节点,包括 root 为空的情况subtree = BSTNode(key, value)elif key < subtree.key:subtree.left = self._bst_insert(subtree.left, key, value)elif key > subtree.key:subtree.right = self._bst_insert(subtree.right, key, value)return subtreedef add(self, key, value):node = self._bst_search(self.root, key)if node is not None:   # 更新已经存在的 keynode.value = valuereturn Falseelse:self.root = self._bst_insert(self.root, key, value)self.size += 1return Truedef _bst_remove(self, subtree, key):"""删除节点并返回根节点"""if subtree is None:return Noneelif key < subtree.key:subtree.left = self._bst_remove(subtree.left, key)return subtreeelif key > subtree.key:subtree.right = self._bst_remove(subtree.right, key)return subtreeelse:  # 找到了需要删除的节点if subtree.left is None and subtree.right is None:    # 叶节点,返回 None 把其父亲指向它的指针置为 Nonereturn Noneelif subtree.left is None or subtree.right is None:  # 只有一个孩子if subtree.left is not None:return subtree.left   # 返回它的孩子并让它的父亲指过去else:return subtree.rightelse:  # 俩孩子,寻找后继节点替换,并删除其右子树的后继节点,同时更新其右子树successor_node = self._bst_min_node(subtree.right)subtree.key, subtree.value = successor_node.key, successor_node.valuesubtree.right = self._bst_remove(subtree.right, successor_node.key)return subtreedef remove(self, key):assert key in selfself.size -= 1return self._bst_remove(self.root, key)NODE_LIST = [{'key': 60, 'left': 12, 'right': 90, 'is_root': True},{'key': 12, 'left': 4, 'right': 41, 'is_root': False},{'key': 4, 'left': 1, 'right': None, 'is_root': False},{'key': 1, 'left': None, 'right': None, 'is_root': False},{'key': 41, 'left': 29, 'right': None, 'is_root': False},{'key': 29, 'left': 23, 'right': 37, 'is_root': False},{'key': 23, 'left': None, 'right': None, 'is_root': False},{'key': 37, 'left': None, 'right': None, 'is_root': False},{'key': 90, 'left': 71, 'right': 100, 'is_root': False},{'key': 71, 'left': None, 'right': 84, 'is_root': False},{'key': 100, 'left': None, 'right': None, 'is_root': False},{'key': 84, 'left': None, 'right': None, 'is_root': False},
]def test_bst_tree():bst = BST.build_from(NODE_LIST)for node_dict in NODE_LIST:key = node_dict['key']assert bst.get(key) == keyassert bst.size == len(NODE_LIST)assert bst.get(-1) is None    # 单例的 None 我们用 is 来比较assert bst.bst_min() == 1bst.add(0, 0)assert bst.bst_min() == 0bst.remove(12)assert bst.get(12) is Nonebst.remove(1)assert bst.get(1) is Nonebst.remove(29)assert bst.get(29) is None

练习题:

  • 请你实现查找 BST 最大值的函数

延伸阅读

  • 《Data Structures and Algorithms in Python》14 章,树的概念和算法还有很多,我们这里介绍最基本的帮你打个基础
  • 了解红黑树。普通二叉查找树有个很大的问题就是难以保证树的平衡,极端情况下某些节点可能会非常深,导致查找复杂度大幅退化。而平衡二叉树就是为了解决这个问题。请搜索对应资料了解下。
  • 了解 mysql 索引使用的 B-Tree 结构(多路平衡查找树),这个是后端面试数据库的常考点。想想为什么?当元素非常多的时候,二叉树的深度会很深,导致多次磁盘查找。从B树、B+树、B*树谈到R 树

Leetcode

验证是否是合法二叉搜索树 [validate-binary-search-tree](https://leetcode.com/problems/validate-binary-search-tree/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/164050.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亚马逊卖家不想被平台限制,应如何脱离平台,建立自己的跨境独立站?

随着跨境电商的快速发展&#xff0c;越来越多的卖家选择在亚马逊等电商平台上销售自己的产品。然而&#xff0c;这些平台往往会限制卖家的经营行为&#xff0c;收取高额的佣金和费用&#xff0c;给卖家带来了很大的压力和风险。因此&#xff0c;一些卖家开始考虑脱离电商平台&a…

Flink之状态TTL机制内容详解

1 状态TTL机制 状态的 TTL机制就是Flink提供的自动化删除状态中的过期数据,配置 TTL的 API可以做到对状态中的数据进行冷热数据分离,将热数据一直保存在状态存储器中,将冷数据进行定期删除. 1.1 API简介 TTL常用API如下: API注解setTtl(Time.seconds(…))配置过期时长,当状态…

Docker可视化管理界面工具Portainer安装

Portainer是Docker容器管理界面工具&#xff0c;可以直观的管理Docker。 部署也很简单&#xff1a; 官方安装文档地址 1、创建数据卷 docker volume create portainer_data2、下载允许容器 docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restartalways -v /v…

放弃无谓的「技术氛围」幻想,准备战斗

大型科技公司每年都招聘大量研发人才&#xff0c;这给了很多人一种错觉&#xff0c;认为是「技术」导致了这些公司的成功&#xff0c;其实他们的成功是技术推动的市场战略的成功&#xff0c;是市场需要某项服务&#xff0c;才需要研发人员夜以继日的埋头苦干。资本绝不会做亏本…

vue2 element el-transfer穿梭框组件支持拖拽及排序 已封装,随取随用

项目场景&#xff1a; 项目中有个功能用到穿梭框组件&#xff0c;新版本需要支持穿梭框组件排序&#xff0c;由于element2版本中的穿梭框组件本身不支持排序功能 在此不仅需要支持随意更换顺序&#xff0c;还支持从一侧拖拽至另一侧&#xff0c;具体功能效果图如下&#xff1…

为什么JSX只能在函数的返回语句中使用

JSX只能在函数的返回语句中使用&#xff0c;因为JSX本质上是一种声明式的语法&#xff0c;用于描述React组件的结构和外观。在函数的返回语句中使用JSX&#xff0c;可以将JSX表达式嵌入到组件的输出中。 当我们编写一个React组件时&#xff0c;我们通常需要定义一个Render函数…

消息中间件——RabbitMQ(五)快速入门生产者与消费者,SpringBoot整合RabbitMQ!

前言 本章我们来一次快速入门RabbitMQ——生产者与消费者。需要构建一个生产端与消费端的模型。什么意思呢&#xff1f;我们的生产者发送一条消息&#xff0c;投递到RabbitMQ集群也就是Broker。 我们的消费端进行监听RabbitMQ&#xff0c;当发现队列中有消息后&#xff0c;就进…

森利威尔SL4010 升压恒压 12V升压24V 12V升压36V 12V升压48V

在当今的电子设备中&#xff0c;电源管理系统的设计是非常重要的。为了保证设备的稳定运行&#xff0c;升压和恒压电源的应用已经成为不可或缺的一部分。在这篇文章中&#xff0c;我们将介绍森利威尔SL4010升压恒压电源&#xff0c;它可以实现12V升压24V、12V升压36V、12V升压4…

c 在文本终端中显示yuv图片

把yuv422 转为rgb32 &#xff0c;利用framebuffer 显示 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <lin…

vue2.6源码分析

vue相关文档 vue-cli官方文档 vuex官方文档 vue-router 官方文档 vue2.6源码地址 如何调试源码 package.json 添加了--sourcemap "scripts": {"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap" }新增…

linux apt update错误提示修复

错误提示&#xff1a; E: Release file for http://security.debian.org/dists/bullseye-security/InRelease is expired (invalid since 15d 14h 45min 26s). Updates for this repository will not be applied. E: Release file for http://ftp.jp.debian.org/debian/dists/b…

【Hello Go】Go语言并发编程

并发编程 概述基本概念go语言的并发优势 goroutinegoroutine是什么创建goroutine如果主goroutine退出runtime包GoschedGoexitGOMAXPROCS channel无缓冲的channel有缓冲的channelrange和close单向channel 定时器TimerTicker Select超时 概述 基本概念 并行和并发概念 并行 &…

CVE-2023-6099:优卡特脸爱云一脸通智慧管理平台SystemMng.ashx接口未授权漏洞复现

文章目录 优卡特脸爱云一脸通智慧管理平台未授权SystemMng.ashx接口漏洞复现&#xff08;CVE-2023-6099&#xff09; [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 优卡特脸爱云一脸通智慧管理平台未授权…

mysql字符串转为数字的三种方法、字符串转日期

隐式转换 在MySQL中&#xff0c;使用0运算符可以将一个非数字的值隐式地转换为数字。这在进行数学运算或比较操作时非常有用。 需要注意的是&#xff0c;在使用0进行隐式转换时&#xff0c;MySQL会尽可能将字符串转换为数字。如果字符串不能转换为数字&#xff0c;则会返回0。…

【解决】HDFS JournalNode启动慢问题排查

文章目录 一. 问题描述二. 问题分析1. 排查机器性能2. DNS的问题 三. 问题解决 一句话&#xff1a;因为dns的问题导致journalnode启动时很慢&#xff0c;通过修复dns对0.0.0.0域名解析&#xff0c;修复此问题。 一. 问题描述 从journalnode启动到服务可用&#xff0c;完成RPC…

使用Python将图片转换为PDF

将图片转为 PDF 的主要原因之一是为了方便共享和传输。此外&#xff0c;将多张图片合并成一个 PDF 文件还可以简化文件管理。之前文章详细介绍过如何使用第三方库Spire.PDF for Python将PDF文件转为图片&#xff0c;那么本文介绍使用同样工具在Python中实现图片转PDF文件的功能…

【OpenCV+OCR】计算机视觉:识别图像验证码中指定颜色文字

文章目录 1. 写在前面2. 读取验证码图像3. 生成颜色掩码4. 生成黑白结果图5. OCR文字识别6. 测试结果 【作者主页】&#xff1a;吴秋霖 【作者介绍】&#xff1a;Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作&#xff01; 【…

Spring Security(安全框架,必须登录成功才能访问指定资源)

一、背景知识 1、Spring Security 是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean&#xff0c;充分利用了Spring IoC&#xff0c;DI&#xff08;IOC: 控制反转Inversion of Control ,DI:D…

24路电磁锁控板的特点和主要参数

智能快递柜、智能生鲜柜、电子存储柜、超市寄存柜、智能送餐柜、电子更衣柜、档案柜等物联网终端设备&#xff0c;都是采用电磁锁控制&#xff0c;这种电磁锁控制板俗称锁控板。锁控板可以远程控制储物柜的开关以及远程监控并提供锁的反馈信号。沐渥开发的24路电磁锁控板可以控…

AI:87-基于深度学习的街景图像地理位置识别

🚀 本文选自专栏:人工智能领域200例教程专栏 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的代码,详细讲解供大家学习,希望可以帮到大家。欢迎订阅支持,正在不断更新中,…