【Go 数据结构】树与哈希表的具体实现

树是一种由 n 个有限节点组成的具有层次关系的集合。

树的定义:

  1. 节点之间有层次关系,分为父节点和子节点
  2. 有唯一一个的根节点,该节点没有父节点
  3. 除了根节点,每个节点有且只有一个父节点
  4. 每一个节点本身以及它的后代也是一棵树,是一个递归结构
  5. 没有后代的节点成为叶子节点,没有节点的树称为空树

二叉树:每个节点最多只有两个儿子节点的树

满二叉树:叶子节点与叶子节点之间的高度差为 0 的二叉树。即整棵树是满的。树形呈现出满三角形结构。

完全二叉树:完全二叉树是由满二叉树而引出来的。这里我们设二叉树的深度为 k,除了第 k 层以外,其他各层节点树都达到了最大值,且第 k 层所有的节点都连续集中在最左侧。

树常见的数学特征:

  1. 高度为 h 的二叉树至少 h + 1 个节点
  2. 高度为 h 的二叉树至少 2 ^ h + 1 个节点
  3. 含有 n 个节点的二叉树的高度至多为 n - 1
  4. 含有 n 个节点的二叉树的高度至少为 log n
  5. 在二叉树的第 i 层,至多有 2 ^ (i - 1) 个节点

链表实现二叉树

// TreeNode is a tree node
type TreeNode struct {Data  string    // dataLeft  *TreeNode // left childRight *TreeNode // right child
}

当然了,我们也可以使用 数组 来表示二叉树,但是一般用来表示完全二叉树

对于一棵有 n 个节点的完全二叉树,从上到下,从左到右进行序号编号,对于一个任意节点,编号 i = 0表示树根节点,编号 i 的节点的左右儿子节点编号分别是:2 * i2 * i + 1, 父节点的编号为 i / 2

树的遍历

对于一棵树的遍历,我们有如下四种遍历方法:

  • 先序遍历:先访问根节点,再访问左子树,最后访问右子树
  • 后序遍历:先访问左子树,再访问右子树,最后访问根节点
  • 中序遍历:先访问左子树,再访问根节点,最后访问右子树
  • 层序遍历:每一层从左到右地访问每一个节点

实现树的前三种遍历打印结果:

// PreOrder 先序遍历 根左右
func PreOrder(tree *Node) {if tree == nil {return}fmt.Println(tree.Data, " ")PreOrder(tree.Left)PreOrder(tree.Right)
}// MidOrder 中序遍历 左根右
func MidOrder(tree *Node) {if tree == nil {return}MidOrder(tree.Left)fmt.Println(tree.Data, " ")MidOrder(tree.Right)
}// PostOrder 后续遍历 左右根
func PostOrder(tree *Node) {if tree == nil {return}PostOrder(tree.Left)PostOrder(tree.Right)fmt.Println(tree.Data, " ")
}

在实现树的层序遍历的时候,我们一般会使用队列作为辅助数据结构来实现。

  1. 首先将树的树根节点放入到队列中。
  2. 从队列中 Remove 出节点,先打印节点值,如果该节点有左子树,左子树入队,如果该节点有右子树,右子树入队。
  3. 重复2,直到队列中再无其他元素。

在实现之前我们先实现一些辅助函数,此处的函数是基于我们上一次的链式队列的修改。

// LinkNode 定义链表节点
type LinkNode struct {Next  *LinkNodeValue *Node
}// LinkQueue 定义链表队列
type LinkQueue struct {root *LinkNodesize intlock sync.Mutex
}// Add 入队
func (q *LinkQueue) Add(v *Node) {q.lock.Lock()defer q.lock.Unlock()// 如果队列为空,我们将新节点作为队列的根节点if q.root == nil {q.root = new(LinkNode)q.root.Value = v} else {// 队列不为空,新建一个节点,采用尾插法实现newNode := new(LinkNode)newNode.Value = v// 找到尾节点nowNode := q.rootif nowNode.Next != nil {nowNode = nowNode.Next}nowNode.Next = newNode}q.size++
}// Remove 出队
func (q *LinkQueue) Remove() *Node {q.lock.Lock()defer q.lock.Unlock()if q.size == 0 {return nil}// 找到队头节点top := q.rootv := top.Value// 将对头元素出队q.root = top.Nextq.size--return v
}// Size 队列大小
func (q *LinkQueue) Size() int {return q.size
}

接下来,实现我们的层序遍历:

// LayerOrder 层序遍历
func LayerOrder(tree *Node) {if tree == nil {return}// 借助队列实现层序遍历queue := new(LinkQueue)// 将根节点入队queue.Add(tree)// 层序遍历for queue.Size() > 0 {// 获取队列头元素element := queue.Remove()// 输出fmt.Println(element.Data, " ")// 将左右子树入队if element.Left != nil {queue.Add(element.Left)}if element.Right != nil {queue.Add(element.Right)}}
}

哈希表

首先,我们来理清楚些概念:

线性查找

线性查找,也被称作顺序查找,是一种非常基础且直观的查找算法。顾名思义,线性查找会按照顺序查找数据,直到找到所需要的数据为止。

线性查找的步骤如下:

  1. 从数据集合的第一个元素开始。
  2. 将当前元素与所查找的目标元素进行比较。
  3. 如果当前元素和目标元素相等,那么返回当前元素的位置,查找结束。
  4. 如果当前元素和目标元素不相等,则继续检查下一个元素。
  5. 如果已经检查完所有元素但还没有找到目标元素,那么返回一个表示“未找到”的结果。

线性查找的优势在于它不需要预先对数据进行排序,这在一些需要频繁插入和删除的场景中会非常有用。此外,对于较小的数据集,线性查找是足够有效的。

但是,对于大规模数据集,线性查找的效率并不高,因为在最坏的情况下,线性查找可能需要检查集合中的每一个元素。

散列查找

哈希查找(也称为散列查找)是一种使用哈希哈希表存储数据,通过哈希函数快速查找数据的方法。

散列查找的步骤如下:

  1. 选择一个哈希函数:哈希函数会接受一个输入(或者叫键),并返回一个整数,这个整数就是在哈希表中存放数据的位置。
  2. 创建哈希表:创建一个可以存放数据的哈希表,通常是一个数组。大小可以视实际情况而定。
  3. 插入数据:当你有一份数据需要插入哈希表时,会把这份数据的键放入哈希函数,得到一个哈希值,然后把数据存放到这个哈希值对应的位置。
  4. 查找数据:当你需要查找一份数据时,也是把这份数据的键放入哈希函数,得到一个哈希值,然后去这个哈希值对应的位置取出数据。由于哈希值的计算速度非常快,所以查找的速度也非常快。

虽然散列查找的速度很快。但是在实际应用中,还需要处理一些复杂的问题,如碰撞问题。当两个键的哈希值相同(这称为哈希碰撞),就需要有一种方法来处理,最常见的处理方法包括开放寻址法链地址法

接下来我们将已经引入开放寻址法和链地址法。

开放寻址法

开放寻址法是解决哈希冲突的一种方法。它的基本思想是如果哈希函数返回的位置已经有数据了,即发生了冲突,那么就从当前位置起,依据某种探查规则,在哈希表中找到另一个位置,直到找到一个空的位置或者达到查找上限。

常见的探查规则有以下三种:

  • 线性探查:线性探查的步骤是,如果哈希函数返回的位置已经有数据了,就顺序往下查找,直到找到一个空的位置。比如初始位置是 i ,那么就依次查找 i+1 , i+2 , i+3 …,直到找到空的位置。这种方法简单,但可能导致数据在哈希表中的分布不均匀,产生一种叫做“聚集”的现象。
  • 二次探查:二次探查的步骤是,如果哈希函数返回的位置已经有数据了,那么就按照平方的规则往下查找,直到找到一个空的位置。比如初始位置是i,那么就依次查找i+1², i+2², i+3²…,直到找到空的位置。这种方法相对于线性探查能更好地防止聚集问题。
  • 双重哈希:双重哈希的步骤是,使用一个额外的哈希函数来解决哈希冲突。比如初始哈希函数返回位置 i ,如果 i 位置已经有数据了,那么就按照另一个哈希函数的规则进行探查,直到找到一个空的位置。这种方法可以避免聚集,但需要计算额外的哈希函数,增加了一些计算复杂性。

开放寻址法的主要优点是实现简单,结构紧凑,不需要额外的链表或数组结构。缺点是可能会有较差的缓存性能,并且需要处理较复杂的删除操作。

链地址法

链地址法也叫做链式哈希,是一种用来解决哈希碰撞问题的方法。当哈希函数返回的位置已经有数据了,即发生哈希碰撞时,链地址法是将这些哈希值相同的元素,放到同一个链表中。

链地址法的步骤如下:

  • 首先,初始化哈希表,每个位置都链接到一个链表,一开始这些链表都是空的。
  • 当我们要插入一个元素时,首先计算这个元素的哈希值,然后找到对应的链表,我们把这个元素插入到链表尾部。
  • 当我们要查找一个元素时,也是首先计算这个元素的哈希值,找到对应的链表,然后在链表中进行顺序查找。

链地址法的优点是处理哈希碰撞简单,不会出现表满的情况;并且在哈希表的大小固定,且哈希值分布均匀时,查找效果较好。它的缺点是需要额外的存储空间来存放指向链表的指针,并且可能存在比较长的链表,会降低查找的效率。

哈希函数

哈希函数(Hash function)是任意长度的输入(也叫做预映射,pre-image),通过散列算法,变换成固定长度的输出,该输出就是哈希值。

哈希函数的构造规则主要基于以下几个目标:

  • 确定性:对于同一个输入,无论执行多少次哈希函数,输出的哈希值始终不变。也就是说,如果 a=b,那么 hash(a)=hash(b)
  • 快速计算:哈希函数需要能快速地计算出哈希值。给定一个输入,进行哈希运算的效率应该很高。
  • 雪崩效应:即使只是微小的输入变化,也会产生巨大的输出变化。换句话说,如果 a≠b,那么 hash(a)hash(b) 的值应该差别很大。
  • 散列均匀:哈希函数应该能保证散列值在哈希表中均匀分布,避免哈希冲突。

哈希函数在很多不同的场合都有应用,例如在数据结构中的哈希表,而在密码学中哈希函数通常用来验证数据的完整性,比如MD5,SHA1,SHA2等。

在目前计算哈希速度最快的哈希算法是 xxhash

说完了一些基础的概念,接下来我们来实现一下简单的链式哈希表。

实现链式哈希表

在介绍先介绍一个小知识点,防止大家疑惑。

我们在实现时,使用到了一个加载因子 factor 这个变量主要用来控制哈希表的扩容与缩容。

我们设定当加载因子 factor <= 0.125 时进行数组缩容,每次将容量对半砍。当加载因子 factor >= 0.75 进行数组扩容,每次将容量翻倍。

定义数据

const (// 扩容因子expandFactor = 0.75
)// 键值对
type keyPairs struct {key   stringvalue interface{}next  *keyPairs
}// HashMap 哈希表
type HashMap struct {array        []*keyPairslen          intcapacity     intcapacityMask intlock         sync.Mutex
}

初始化

// NewHashMap 初始化哈希表
func NewHashMap(capacity int) *HashMap {// 默认容积为2的幂defaultCapacity := 1 << 4if capacity <= defaultCapacity {capacity = defaultCapacity} else {capacity = 1 << int(math.Ceil(math.Log2(float64(capacity))))}// 新建一个哈希表hashtable := new(HashMap)hashtable.capacity = capacityhashtable.capacityMask = capacity - 1return hashtable
}

获取长度

// Len 返回哈希表中键值对的个数
func (hashtable *HashMap) Len() int {return hashtable.len
}

计算哈希值

// value 计算哈希值
var value = func(key []byte) uint64 {h := xxhash.New()h.Write(key)return h.Sum64()
}

获取下标

// hashIndex 计算哈希值并获取下标
func (hashtable *HashMap) hashIndex(key string, mask int) int {// 计算哈希值hash := value([]byte(key))index := hash & uint64(mask)return int(index)
}

插入元素

// Put 插入键值对
func (hashtable *HashMap) Put(key string, value interface{}) {hashtable.lock.Lock()defer hashtable.lock.Unlock()// 获取下标index := hashtable.hashIndex(key, hashtable.capacityMask)// 此下标在哈希表中的值element := hashtable.array[index]if element == nil {// 此下标没有元素,则插入hashtable.array[index] = &keyPairs{key:   key,value: value,}} else {// 此下标已经有元素,则插入到上一个元素的后面var lastPairs *keyPairsfor element != nil {if element.key == key {element.value = valuereturn}lastPairs = elementelement = element.next}// 找不到元素,则插入到最后lastPairs.next = &keyPairs{key:   key,value: value,}}// 长度加一newLen := hashtable.len + 1// 计算扩容因子,如果长度大于容积的75%,则扩容if float64(newLen)/float64(hashtable.capacity) >= expandFactor {// 新建一个原来两倍大小的哈希表newhashtable := new(HashMap)newhashtable.array = make([]*keyPairs, hashtable.capacity*2)newhashtable.capacity = hashtable.capacity * 2newhashtable.capacityMask = newhashtable.capacity*2 - 1// 遍历原哈希表,将元素插入到新哈希表for _, pairs := range hashtable.array {for pairs != nil {newhashtable.Put(pairs.key, pairs.value)pairs = pairs.next}}hashtable.array = newhashtable.arrayhashtable.capacity = newhashtable.capacityhashtable.capacityMask = newhashtable.capacityMask}hashtable.len = newLen
}

获取元素

// Get 获取键值对
func (hashtable *HashMap) Get(key string) (value interface{}, ok bool) {hashtable.lock.Lock()defer hashtable.lock.Unlock()// 获取下标index := hashtable.hashIndex(key, hashtable.capacityMask)// 此下标在哈希表中的值element := hashtable.array[index]// 遍历元素,如果元素的key等于key,则返回for element != nil {if element.key == key {return element.value, true}element = element.next}return nil, false
}

删除元素

// Delete 删除键值对
func (hashtable *HashMap) Delete(key string) {hashtable.lock.Lock()defer hashtable.lock.Unlock()// 获取下标index := hashtable.hashIndex(key, hashtable.capacityMask)// 此下标在哈希表中的值element := hashtable.array[index]// 如果为空链表,则直接返回if element == nil {return}// 如果第一个元素的key等于key,则删除if element.key == key {hashtable.array[index] = element.nexthashtable.len--return}// 下一个键值对nextElement := element.nextfor nextElement != nil {if nextElement.key == key {element.next = nextElement.nexthashtable.len--return}element = nextElementnextElement = nextElement.next}
}

遍历哈希表

// Range 遍历哈希表
func (hashtable *HashMap) Range() {hashtable.lock.Lock()defer hashtable.lock.Unlock()for _, pairs := range hashtable.array {for pairs != nil {fmt.Println(pairs.key, pairs.value)pairs = pairs.next}}fmt.Println("len:", hashtable.len)
}

哈希表总结

哈希查找总的来说是一种用空间去换时间的查找算法,时间复杂度达到 O ( 1 ) {O(1)} O(1)级别。

总结

本次我们介绍使用Go语言实现数据结构中的树和哈希表,并且详细介绍了哈希表的具体实现。数据结构这一系列我们没有涉及到具体的细节的讲解,适合有一定数据结构基础的童鞋,本系列代码已经上传至Github,欢迎大家 Star。

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

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

相关文章

补一 继承的使用

继承的关键词为extends 模型为 public class 子类 extends 父类

中国热门高端dating约会交友软件有哪些?国内权威Dating App红黑排行榜推荐

在dating 软件刷了无数个男人后终于脱单啦&#xff0c;跟大家分享一些我的个人感受 1、二狗 颜值⭐️⭐️⭐️ 真实性 ⭐️⭐️⭐️⭐️⭐️ 用户质量⭐️⭐️⭐️⭐️ ⭕️优点&#xff1a;整体用户质量较高&#xff0c;用户集中在金融、互联网和体制内行业。用户需进行学历、…

速来get!多微信聚合聊天功能大揭秘!

随着网络时代的发展&#xff0c;微信成为了职场中不可或缺的沟通工具&#xff0c;很多人都有着多个微信号&#xff0c;而要想高效管理这些账号&#xff0c;那就少不了工具的帮忙。 通过微信管理系统&#xff0c;可以轻松实现多个微信号聚合聊天&#xff0c;提高沟通效率。 1、…

抓取Google时被屏蔽怎么办?如何避免?

在当今数字化时代&#xff0c;数据采集和网络爬取已成为许多企业和个人必不可少的业务活动。对于爬取搜索引擎数据&#xff0c;特别是Google&#xff0c;使用代理IP是常见的手段。然而&#xff0c;使用代理抓取Google并不是一件轻松的事情&#xff0c;有许多常见的误区可能会导…

顶顶通呼叫中心中间件电话黑名单系统介绍

黑名单 有显示成功和失败导入数&#xff0c;可以禁用也可以启用&#xff0c;如果禁用状态就是不使用这一组黑名单&#xff0c;多个号码核验就是验证号码存不存在。黑名单只有管理员和操作员可以配置&#xff0c;租户是看不到黑名单的。但是黑名单跟租户是互通的。 可以单个号码…

【智能算法应用】基于麻雀搜索算法的二维最大熵图像阈值分割

目录 1.算法原理2.数学模型3.结果展示4.参考文献5.代码获取 1.算法原理 【智能算法】麻雀搜索算法&#xff08;SSA&#xff09;原理及实现 2.数学模型 最大熵法是由 Kapur 于 1985 年所提出的&#xff0c; 该方法的阈值选取标准取决于图像中最大化分 割的目标区域和背景区域…

STM32F1#1(入门了解)

一、STM32开发平台和工具 1.1 STM32芯片介绍 典型微控制器由CPU&#xff08;运算器、控制器&#xff09;、RAM、ROM和输入输出组成。 1.2 STM32核心板 STM32核心板配件&#xff1a; ①JTAG/SWD仿真-下载器 ②通信-下载模块 ③OLED显示屏 1&#xff09; 通信-下载模…

spring的高阶使用技巧1——ApplicationListener注册监听器的使用

Spring中的监听器&#xff0c;高阶开发工作者应该都耳熟能详。在 Spring 框架中&#xff0c;这个接口允许开发者注册监听器来监听应用程序中发布的事件。Spring的事件处理机制提供了一种观察者模式的实现&#xff0c;允许应用程序组件之间进行松耦合的通信。 更详细的介绍和使…

海外市场成 ISV 新掘金地?生成式 AI 如何加速业务创新实践?Zilliz 有话说

期望了解 Zilliz 最新动态&#xff1f;想要与 Zilliz 线下探讨 AI 时代向量数据库的全球化布局思考及典型实践&#xff1f; 机会来啦&#xff01;5 月 10 日&#xff0c;Zilliz 将闪现亚马逊云科技的两场活动现场&#xff08;苏州、西安&#xff09;&#xff0c;与大家共话行业…

Ubuntu将软件图标添加到应用列表

一.简介snap snap和yum&#xff0c;apt一样都是安装包工具&#xff0c;但是snap里的软件源是自动更新到最新版本&#xff0c;最好用 比如Ubuntu的软件商城就是使用的snap软件包 二. Ubuntu软件商城更新 1.ps -ef | grep snap-store 查询并kill snap-store的所有进程 2.sudo …

【仪酷LabVIEW AI工具包案例】使用LabVIEW AI工具包+YOLOv5结合Dobot机械臂实现智能垃圾分类

‍‍&#x1f3e1;博客主页&#xff1a; virobotics(仪酷智能)&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『仪酷LabVIEW AI工具包案例』 &#x1f4d1;上期文章&#xff1a;『【YOLOv9】实战二&#xff1a;手把手教你使用TensorRT实现YOLOv…

C++之STL-priority_queue和仿函数的讲解

目录 一、priority_queue的介绍和使用 1.1 priority_queue的介绍 1.2 priority_queue的基本接口 二、仿函数的介绍 2.1 基本概念 2.2 适用场景 三、模拟实现priority_queue 3.1 向上调整算法 3.2 向下调整算法 3.3 整体框架 一、priority_queue的介绍和使用 1.1 prio…

基于卷积神经网络的一维信号降噪(简单版,MATLAB)

简单演示一下基于卷积神经网络的一维信号降噪&#xff0c;有个大致印象即可。 %% Plot the previous training CNN. set_plot_defaults(on) load(denoiser_sparse.mat); h1{1} double(conv1); h1{2} double(conv2); h1{3} reshape(double(conv3),[8,1,17]); figure(1) [r,c…

Java_异常

介绍 编译时异常&#xff1a; 除RuntimeException和他的子类&#xff0c;其他都是编译时异常。编译阶段需要进行处理&#xff0c;作用在于提醒程序眼 运行时异常&#xff1a; RuntimeException本身和其所有子类&#xff0c;都是运行时异常。编译阶段不报错&#xff0c;是程序…

Java17 --- SpringCloud之Nacos

目录 一、下载nacos并运行 1.1、创建9001微服务作为提供者 1.2、创建80微服务作为消费者 二、naocs配置中心规则与动态刷新 2.1、创建3377微服务 2.2、在nacos中创建配置文件 三、nacos数据模型之Namespace-Group-Datald 3.1、DatalD方案 3.2、Group方案 3.3、Name…

python软件开发遇到的坑-相对路径文件读写异常,不稳定

1. os.chdir()会影响那些使用相对路径读写文件的程序&#xff0c;使其变得不稳定&#xff0c;默认情况下&#xff0c;当前工作目录是主程序所在目录&#xff0c;使用os.chdir会将当前工作目录修改到其他路径。 资料&#xff1a; python相对路径写对了却报错是什么原因呢&#…

蜜蜂收卡系统 加油卡充值卡礼品卡自定义回收系统源码 前后端开源uniapp可打包app

本文来自&#xff1a;蜜蜂收卡系统 加油卡充值卡礼品卡自定义回收系统源码 前后端开源uniapp可打包app - 源码1688 卡券绿色循环计划—— 一项旨在构建卡券价值再利用生态的社会责任感项目。在当前数字化消费日益普及的背景下&#xff0c;大量礼品卡、优惠券因各种原因未能有效…

mib browser读取mib文件的oid(飞塔防火墙为例)

在配置zabbix监控的时候,配置监控项最为麻烦,一般我们都会套用模板,这种方式比较简单,但是有些设备就是没有现成的zabbix模板,怎么办? 今天我们使用MIB Browser来获取设备SNMP的OID,然后加入zabbix 。 1.什么是MIB Browser SNMP客户端工具MIB Browser, 全名iReasonin…

ARP详解

2.4 ARP 如图2-10所示&#xff0c;当网络设备有数据要发送给另一台网络设备时&#xff0c;必须要知道对方的网络层地址&#xff08;即IP地址&#xff09;。IP地址由网络层来提供&#xff0c;但是仅有IP地址是不够的&#xff0c;IP数据报文必须封装成帧才能通过数据链路进行发送…