数据结构 树1

目录

前言

一,树的引论

 二,二叉树

三,二叉树的详细理解

四,二叉搜索树

五,二分法与二叉搜索树的效率

六,二叉搜索树的实现

七,查找最大值和最小值

指针传递 vs 传引用

为什么指针按值传递不会修改 root->left?

查到最值的功能

总结


前言

当我们运用链表,栈,队列等线性数据结构来进行搜索数据的时候,他们的时间复杂度都是O(n),我们不妨做一个小小的测试
假设          我们的一个机器可以做一百万次比较每秒
                 则我们的机器可以执行10^7比较
                 也就是每次的比较时间为10^-7秒

以上是我们假设一个机器可以做出查找操作的效率,接下来我们用这个机器放入到日常工作中,我们来看看情况,如今我们的数据量是十分惊人的,有着1亿或者10亿的数据量,我们计算一下这个机器处理10亿个数据的效率,运用链表,栈,队列这种数据结构来进行,时间复杂度为O(n)

计算:10亿个数据,我们最差的为10^10*10^-7=10^3s=16.7min,当然我们如果出现了16.7min,肯定是不行的,因为我们都想快点找到自己的数据,所以我们接下来就讲讲树这个结构


一,树的引论

目前我们所学习了链表,栈,队列这些都是线性结构或者说顺序结构

数据结构的选取

有了这么多的数据结构,我们该怎么选择呢?我们应当考虑以下几个方面

1,我们要选择什么样的数据类型

2,操作的成本

3,内存的消耗

4,数据结构实现的难度

树用于表示一种层次的数据结构
(tree)

树的基本结构介绍 

 

树也可以被称为一个递归的数据结构
   (树的基本实现方法就是递归)

这里的子树可以看成递归的深度,这个root为入口

树边的计算

一棵树有N个节点,那么这个树有N-1个节点(图中的每一个线可以表示一个链接或者一个边),除了根节点外,图中的每一个节点都有一个边进行对应

深度与高度

深度

节点到根节点的路径长度

高度节点到子叶节点的路径长度

7高度为0,深度为2
5高度为1,深度为2
13高度为0,深度为3

                           总的来说,深度为节点往根节点数,高度为节点往子叶节点数

 二,二叉树

二叉树的概念

树上的节点最多只可以有两个子叶节点
(不会超过两个小孩,为左孩和右孩)

我们把我们的树换一个角度思考,换成我们所熟悉的样子来看待问题,这样我们就可以构想出一棵树是如何用我们所学过的知识来转化的,我们树中的孩子节点可以这样设置

struct BstNode {int data;BstNode* left;BstNode* right;
};

基于树的应用

1,存储天然具备的层级的数据结构----电脑上面的磁盘驱动器上的文件系统

2,组织数据----在一个结构里面进行快速的查找与删除,二叉搜索树

3,Trie树----用于字典,用于一个动态的拼写检查

三,二叉树的详细理解

树的类型与属性

二叉树的通用属性:二叉搜索树的每一个节点的子节点不可以超过两个节点

严格二叉树: 每一个二叉树的子节点只可以为0个或者2个

完全二叉树:除了最后一层,其他层填满,并且最后一层的节点全部都是向左靠

 这个就是一个完全二叉树的例子

完美二叉树:所有层都填满

二叉树里的计算

1,节点的最大数量:2^0+2^1+2^2+……+2^n=2^(n+1)-1

2,n个节点的完美二叉树的高度:n=2^(h+1)+1    h=\log_{2}(n+1) -1

3,n个节点的完全二叉树的高度:h=\log_{2}n  也就是说时间复杂度为O(\log_{2}n)

4,n个节点的最大高度为(n-1)

所以我们一般把树的高度控制到最小,然后如果只有根节点高度为0,如果没有节点的话就是-1

5,diff=|lhight - rhight|     lhight为左子树,rhight为右子树

实现可以用数组也可以用链表

四,二叉搜索树

二叉搜索树是一种可以高效进行搜索和更新的数据结构

之前数据结构的效率

抛问:你该使用什么数据结构进行存储一个可以修改的集合

搜索插入删除类型
O(1)O(1)数组末尾O(n)数组

O(n)

O(1)链表头部

O(n)链表

根据我们的前言可以知道,我们利用这两个类型不用什么好的方法的话,那么时间会花费的非常长,这个时候我们可以考虑用二分法,在两边分别放一个指针,这样我们的时间复杂度就是O(\log_{2}n)了,我们利用这个来看看我们在实际的时候,这个时间复杂度可以减多少呢?

例子

假设我们有2^31的数据量,这个数据量以及超过了20亿的数据量

n=2^31     则时间为31*10^-6秒,这个时间相比较上面那一个以及少量非常多的时间了

二叉树的查找成本为O(\log_{2}n)但是也有很差的情况O(n),但是这种情况是可以避免的,可以利用平衡二叉树

平衡二叉树的概念

平衡二叉树是一种自平衡的二叉搜索树其中每个节点的左右子树的高度差(平衡因子)不超过1,这意味着树在任何时候都保持相对平衡的状态,避免了二叉树退化为链表的情况,从而确保了操作的时间复杂度

二叉搜索树
核心规则:
左子树的节点值小于当前节点的值
右子树的节点值大于当前节点的值
左右子树本身也必须是二叉搜索树

这个是一个二叉搜索树,我们来详细分析一下

5比7小    9比7大,符合
3比5小,比7小    6比5大,比7小    8比9小,比7大    10比9大,比7小符合
我们看这些是要进行比较的,而不是单单比较5和9,左右子树本身也必须是二叉搜索树

五,二分法与二叉搜索树的效率

我们不妨想想,二分法的时间复杂度是多少呢?如果忘记了二分法也没事,我写了一个很简单的二分法代码

二分法

#include<iostream>using namespace std;int search(int arr[], int sign, int n, int left, int right) {while (left <= right) {int mid = left + (right - left) / 2;if (arr[mid] == sign) {return 1;}else if (arr[mid] > sign) {right = mid - 1;}else {left = mid + 1;}}return -1;
}int main() {int arr[9] = { 1,2,3,4,5,6,7,8,9 };int sign = 8;int n = sizeof(arr) / sizeof(arr[0]);int result = search(arr, sign, n, 0, n - 1);if (result == -1) {cout << "未找到" << endl;}else {cout << "找到了" << endl;}return 0;
}

我们在使用二分法的时候,都是利用空间减半的方法,n->n/2->n/4->……,最后找到我们所想要的最终元素,如果没有找到则会left>right的情况,这个就可以判断是否要终止程序

n/2^k   =>   2^k = n   =>      k = \log_{2}n

二叉搜索树

其实你可以发现二叉搜索树也利用了这样的原理,我们可以来研究一下

我们要寻找3这个元素, 首先在根部进去进行寻找,然后进行分区,是比7大还是比7小,然后进行对空间的对半分,这不就是二分法么,基于这个思想,我们都很喜欢完美二叉树的出现,为什么呢?这不就是每一次分开的时候,都是对半分,这样效率特别高

我们这里思考了查找,但是我们还要有插入,删除的操作,那我们就需要思考怎么插入或者删除之后,把树平衡起来

六,二叉搜索树的实现

基本实现

#include<iostream>using namespace std;struct BstNode {BstNode* left;int data;BstNode* right;
};BstNode* GetNode(int x) {BstNode* newNode = new BstNode();newNode->data = x;newNode->left = NULL;newNode->right = NULL;return newNode;
}BstNode* Insert(BstNode* root, int x) {if (root == NULL) {root = GetNode(x);return root;}else if (x > root->data) {Insert(root->right, x);}else if (x < root->data) {Insert(root->left, x);}return root;
}bool search(BstNode* root,int x) {if (root == NULL) {return false;}else if (root->data == x) {return true;}else if (root->data >= x) {return search(root->left, x);}else {return search(root->right, x);}
}int main() {BstNode* root = NULL;root = Insert(root, 10);root = Insert(root, 6);root = Insert(root, 7);bool key = search(root, 10);if (key == true) {cout << "找到了" << endl;}else {cout << "未找到" << endl;}
}

这里是用写了插入和搜索,这个main函数里面一定要用root来接收,因为root在其他地方为形参,改变不了形参,如果不想这么麻烦也可以设置一个全局变量,我们重点来理解一下这个递归,我们理解了插入的递归,这个搜索也是可以迎刃而解

细讲递归操作

BstNode* Insert(BstNode* root, int x) {if (root == NULL) {root = GetNode(x);return root;}else if (x > root->data) {Insert(root->right, x);}else if (x < root->data) {Insert(root->left, x);}return root;
}

这里一开始root肯定是为空的,所以我们要给根节点赋予一个节点,然后下一次的时候就是判断这个节点是放到左子树还是右子树,我们这里用递归来进行树的深入


到了下一次,我们就会到左子树还是右子树,注意此时进入递归了,这个root不是你根节点了,是左子树或者右子树的根节点,然后当我找到left或者right为NULL的时候,我可以用第一个if语句给这个节点赋予一个节点了,重中之重此时的root不是根节点,然后赋予完返回根节点


然后进入到上一次的递归进行返回,最终递归逐个破解,返回的是这个树的根节点 

很好这里讲的是错误的,这是作者初代代码,后面有正确的改进,也可以给读者看看错误的递归思路,确实有点绕,此代码也有细小的问题,读者可以先自己思考哪里错误,后面再看正确的思路

放到堆和栈的策略

这个就涉及大到一个变量的生命周期的问题了,大致就是你想临时的就放到栈里,你想长期的就放到堆里

七,查找最大值和最小值

这个十分的简单哈,真的,你会发现这个最大值不就是最右边的那个么,这个最小值不就是最左边那个么,我们来实现一下

#include<iostream>using namespace std;struct BstNode {BstNode* left;int data;BstNode* right;
};BstNode* GetNode(int x) {BstNode* newNode = new BstNode();newNode->data = x;newNode->left = NULL;newNode->right = NULL;return newNode;
}BstNode* Insert(BstNode* root, int x) {if (root == NULL) {root = GetNode(x);return root;}else if (x > root->data) {root->right = Insert(root->right, x);}else if (x < root->data) {root->left = Insert(root->left, x);}return root;
}bool search(BstNode* root,int x) {if (root == NULL) {return false;}else if (root->data == x) {return true;}else if (root->data >= x) {return search(root->left, x);}else {return search(root->right, x);}
}BstNode* MAX(BstNode* root) {if (root == NULL) {cout << "未找到" << endl;return NULL;}BstNode* current = root;while (current -> right != NULL) {current = current->right;}return current;
}int main() {BstNode* root = NULL;root = Insert(root, 10);root = Insert(root, 6);root = Insert(root, 7);root = Insert(root, 15);root = Insert(root, 14);root = Insert(root, 20);bool key = search(root, 7);if (key == true) {cout << "找到了" << endl;}else {cout << "未找到" << endl;}cout << MAX(root)->data << endl;
}

这里是既有修正的代码和寻找最值的代码,我会一 一讲述

错误的点

BstNode* Insert(BstNode* root, int x) {if (root == NULL) {root = GetNode(x);return root;}else if (x > root->data) {root->right = Insert(root->right, x);}else if (x < root->data) {root->left = Insert(root->left, x);}return root;
}

这里为什么不可以利用root = GetNode(x);来给左边和右边进行赋值呢?

假设你插入 6 到如下 BST:

    10/  \NULL  NULL

执行:

root = Insert(root, 6);
  1. Insert(root, 6);

    • root = 10
    • 6 < 10,调用 Insert(root->left, 6);
  2. Insert(root->left, 6);

    • root->left == NULL,进入:
      if (root == NULL) {root = GetNode(x);  // root 现在指向了新节点 6return root;
      }
      
    • root局部变量,修改 root 不会修改 root->left,这里不可以进行对于左边的赋值操作
    • 这一步返回了新节点 6,但 Insert(root->left, 6); 的返回值被丢弃了,没有赋值给 root->left
  3. 递归结束后,原来的 root->left 仍然是 NULL,导致插入失败

但是又想,奇怪,我传的是指针呀,为什么是值操作嘞?

指针传递 vs 传引用

在 C++ 里,指针本身是 按值传递
换句话说,Insert(root->left, x); 传递的 root->left 是它的拷贝,而不是原始变量的引用

Insert(root->left, x);  // 你以为 root->left 会被修改

我觉得 root->left 传递给 Insert 后,在 Insert 内部的 root = GetNode(x); 可以直接修改 root->left 

实际发生的情况

void Insert(BstNode* root, int x);
  • root->left 的值(即 NULL)被传递给 Insert
  • 进入 Insert(root->left, x);,这里的 root 只是 root->left 的拷贝
  • root = GetNode(x); 只是修改了 拷贝的 root不会修改 root->left
  • 递归结束后,root->left 仍然是 NULL

为什么指针按值传递不会修改 root->left

来看一个简单的示例

void ChangePointer(int* ptr) {ptr = new int(10);  // 只修改了 ptr 的拷贝
}int main() {int* p = NULL;ChangePointer(p);cout << p << endl;  // 还是 NULL,没有变!
}

这里的 ChangePointer(p); 不会修改 p,因为 p 作为参数传进去时,只是它的拷贝,所以 ptr = new int(10); 不会影响外部的 p

这里也就是我们很常见的指针问题,我们这放了个形参,但不是全局变量,是局部的,这里确实再堆里面创建了一个节点,但是没有东西接收,所以会为NULL,上面这个简单的例子就阐明了

查到最值的功能

BstNode* MAX(BstNode* root) {if (root == NULL) {cout << "未找到" << endl;return NULL;}BstNode* current = root;while (current -> right != NULL) {current = current->right;}return current;
}

我们这里只需要一直往右边就可以找到最大值了  


总结

我们总结一下我们所学的东西

1,树的概念和树的基本知识

2,树的高度为节点到子叶的距离,深度为节点到根部的距离

3,二叉树的概念,需注意核心规则

4,二叉树的分类:严格二叉树,完全二叉树,完美二叉树,平衡二叉树

5,二叉树与二分法的关系和两个的具体实现

6,实现的时候遇到了一个bug为指针传递和传引用的区别,两个是不一样的传指针其实也就是一个形参罢了,引用不一样,这个是再原来的地方做法,也就是再常量区,这个需要学过C++才知道

7,查找最值的方法

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

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

相关文章

利用metaGPT多智能体框架实现智能体-1

1.metaGPT简介 MetaGPT 是一个基于大语言模型&#xff08;如 GPT-4&#xff09;的多智能体协作框架&#xff0c;旨在通过模拟人类团队的工作模式&#xff0c;让多个 AI 智能体分工合作&#xff0c;共同完成复杂的任务。它通过赋予不同智能体特定的角色&#xff08;如产品经理、…

嵌入式系统|DMA和SPI

文章目录 DMA&#xff08;直接内存访问&#xff09;DMA底层原理1. 关键组件2. 工作机制3. DMA传输模式 SPI&#xff08;串行外设接口&#xff09;SPI的基本原理SPI连接示例 DMA与SPI的共同作用 DMA&#xff08;直接内存访问&#xff09; 类型&#xff1a;DMA是一种数据传输接口…

【MySQL】--- 复合查询 内外连接

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; MySQL &#x1f3e0; 基本查询回顾 假设有以下表结构&#xff1a; 查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的姓名首字母为…

2 MapReduce

2 MapReduce 1. MapReduce 介绍1.1 MapReduce 设计构思 2. MapReduce 编程规范3. Mapper以及Reducer抽象类介绍1.Mapper抽象类的基本介绍2.Reducer抽象类基本介绍 4. WordCount示例编写5. MapReduce程序运行模式6. MapReduce的运行机制详解6.1 MapTask 工作机制6.2 ReduceTask …

【memgpt】letta 课程6: 多agent编排

Lab 6: Multi-Agent Orchestration 多代理协作 letta 是作为一个服务存在的,app通过restful api 通信 多智能体之间如何协调与沟通? 相互发送消息共享内存块,让代理同步到不同的服务的内存块

cmd命令行无法进入D:盘怎么办

我找到了一个方法就是 增加一个/d cd /d d: 如下图,我不仅可以进入d盘符下&#xff0c;还可以访问盘符下的文件夹

【机器学习】自定义数据集 ,使用朴素贝叶斯对其进行分类

一、贝叶斯原理 贝叶斯算法是基于贝叶斯公式的&#xff0c;其公式为&#xff1a; 其中叫做先验概率&#xff0c;叫做条件概率&#xff0c;叫做观察概率&#xff0c;叫做后验概率&#xff0c;也是我们求解的结果&#xff0c;通过比较后验概率的大小&#xff0c;将后验概率最大的…

2025年人工智能技术:Prompt与Agent的发展趋势与机遇

文章目录 一、Prompt与Agent的定义与区别(一)定义(二)区别二、2025年Prompt与Agent的应用场景(一)Prompt的应用场景(二)Agent的应用场景三、2025年Prompt与Agent的适合群体(一)Prompt适合的群体(二)Agent适合的群体四、2025年Prompt与Agent的发展机遇(一)Prompt的…

2025_1_31 C语言中关于数组和指针

1.数组作为指针传递 数组作为指针传递可以&#xff1a; 加一个数减一个数两个指针相减自增自减 int main() {int arr[] { 1,2,3,4,5,6,7,8,9 };printf("%d\n", arr[0] 2);printf("%d\n", arr[2] - 2);printf("%d\n", arr[0] arr[2]);int* …

Baklib推动企业知识管理创新与效率提升的全面探讨

内容概要 在当今数字化转型的背景下&#xff0c;有效的知识管理显得尤为重要。知识是企业的核心资产&#xff0c;而传统的管理方式往往无法充分发挥这些知识的价值。因此&#xff0c;企业亟需一种高效、灵活的解决方案来应对这一挑战。Baklib作为一款先进的企业级知识管理平台…

JAVA实战开源项目:网上购物商城(Vue+SpringBoot) 附源码

本文项目编号 T 041 &#xff0c;文末自助获取源码 \color{red}{T041&#xff0c;文末自助获取源码} T041&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

访问CMOS RAM

实验内容、程序清单及运行结果 访问CMOS RAM&#xff08;课本实验14&#xff09; 代码如下&#xff1a; assume cs:code data segment time db yy/mm/dd hh:mm:ss$ ;int 21h 显示字符串&#xff0c;要求以$结尾 table db 9,8,7,4,2,0 ;各时间量的存放单元 data ends cod…

Visual Studio使用GitHub Copilot提高.NET开发工作效率

GitHub Copilot介绍 GitHub Copilot 是一款 AI 编码助手&#xff0c;可帮助你更快、更省力地编写代码&#xff0c;从而将更多精力集中在问题解决和协作上。 GitHub Copilot Free包含哪些功能&#xff1f; 每月 2000 代码补全&#xff0c;帮助开发者快速完成代码编写。 每月 …

socket实现HTTP请求,参考HttpURLConnection源码解析

背景 有台服务器&#xff0c;网卡绑定有2个ip地址&#xff0c;分别为&#xff1a; A&#xff1a;192.168.111.201 B&#xff1a;192.168.111.202 在这台服务器请求目标地址 C&#xff1a;192.168.111.203 时必须使用B作为源地址才能访问目标地址C&#xff0c;在这台服务器默认…

Spring Boot 日志:项目的“行车记录仪”

一、什么是Spring Boot日志 &#xff08;一&#xff09;日志引入 在正式介绍日志之前&#xff0c;我们先来看看上篇文章中&#xff08;Spring Boot 配置文件&#xff09;中的验证码功能的一个代码片段&#xff1a; 这是一段校验用户输入的验证码是否正确的后端代码&#xff0c…

Go学习:Go语言中if、switch、for语句与其他编程语言中相应语句的格式区别

Go语言中的流程控制语句逻辑结构与其他编程语言类似&#xff0c;格式有些不同。Go语言的流程控制中&#xff0c;包括if、switch、for、range、goto等语句&#xff0c;没有while循环。 1. if 语句 语法格式&#xff1a; &#xff08;1&#xff09;单分支&#xff1a; if 条件语句…

想品客老师的第九天:原型和继承

原型与继承前置看这里 原型 原型都了解了&#xff0c;但是不是所有对象都有对象原型 let obj1 {}console.log(obj1)let obj2 Object.create(null, {name: {value: 荷叶饭}})console.log(obj2) obj2为什么没有对象原型&#xff1f;obj2是完全的数据字典对象&#xff0c;没有…

SpringBoot--基本使用(配置、整合SpringMVC、Druid、Mybatis、基础特性)

这里写目录标题 一.介绍1.为什么依赖不需要写版本&#xff1f;2.启动器(Starter)是何方神圣&#xff1f;3.SpringBootApplication注解的功效&#xff1f;4.启动源码5.如何学好SpringBoot 二.SpringBoot3配置文件2.1属性配置文件使用2.2 YAML配置文件使用2.3 YAML配置文件使用2.…

98.1 AI量化开发:长文本AI金融智能体(Qwen-Long)对金融研报大批量处理与智能分析的实战应用

目录 0. 承前1. 简介1.1 通义千问(Qwen-Long)的长文本处理能力 2. 基础功能实现2.1 文件上传2.2 单文件分析2.3 多文件分析 3. 汇总代码&运行3.1 封装的工具函数3.2 主要功能特点3.3 使用示例3.4 首次运行3.5 运行结果展示 4. 注意事项4.1 文件要求4.2 错误处理机制4.3 最佳…

数据结构实战之线性表(一)

一.线性表的定义和特点 线性表的定义 线性表是一种数据结构&#xff0c;它包含了一系列具有相同特性的数据元素&#xff0c;数据元素之间存在着顺序关系。例如&#xff0c;26个英文字母的字符表 ( (A, B, C, ....., Z) ) 就是一个线性表&#xff0c;其中每个字母就是一个数据…