GeoHash之存储篇

简介: GeoHash之存储篇

前言:

在上一篇文章GeoHash——滴滴打车如何找出方圆一千米内的乘客主要介绍了GeoHash的应用是如何的,本篇文章我想要带大家探索一下使用什么样的数据结构去存储这些Base32编码的经纬度能够节省内存并且提高查询的效率。


前缀树、跳表介绍:

什么是前缀树:

针对于没有接触过前缀树或者不熟悉前缀树的同学,我先简单介绍一下其基本原理。

前缀树 其主要就是分为两个部分 前缀 + 树

树大家肯定不陌生,比如二叉搜索树这样的数据结构就可以将查询效率降低至O(logn),,
而前缀树不同之处在于它的节点的核心数据结构是这样的:

`

type Trie struct {
   
   
    child [26]*Trie
    isEnd bool
}

首先child [26]*Trie主要作用就是存放子节点的,而isEnd作用就是去判断当前节点是否存在有一个完整的元素的结尾。光说原理比较枯燥,举例图示说明:

不知道大家是否了解过web后端路由是有哪些存储方式的,在golang语言中gin框架就是基于前缀树去存储路由的,比如:

假设我们要使用前缀树去存储
/ /ag /c /e这四个路由

那么存储过程就是应该这样的

image.png

每一个节点是一个Trie数据结构的节点,每个数组节点对应的是需要存储数据的单个字符,这样做的好处就是当我们需要存放的数据如果有相同前缀那么就不需要重复存储,节省空间,例如app、approach。那么app就只需要存储一次即可。

为了更方便理解,这里放一下插入元素、搜寻元素是否存在的代码:

func (this *Trie) Insert(word string)  {
   
   
    cur:=this
    for i:=0;i<len(word);i++{
   
   
        idx:=word[i]-'a'
        if cur.child[idx]==nil{
   
   
            cur.child[idx]=&Trie{
   
   }
            }
            cur=cur.child[idx]
    }
    cur.isEnd=true
}


func (this *Trie) Search(word string) bool {
   
   
    cur:=this
    for i:=0;i<len(word);i++{
   
   
        if cur.child[word[i]-'a']==nil{
   
   
                return false
            }
        cur=cur.child[word[i]-'a']
    }
    return cur.isEnd
}

而GeoHash得到的字符串其实正好满足大量相同前缀的特性,因此使用前缀树去存储GeoHash是相对比较合适的。


对于前缀树的补充

上述讲的其实是最基础版的前缀树,我们还可以对此进行一些魔改来优化存储与查询。

比如在Go/gin框架中的路由存储就是用的压缩前缀树

首先该树中当一个节点它仅有一个子节点时就会对树的结构进行一个压缩

image.png

/egg这个节点,e下子节点只有g,g下子节点就只有g,因此它们都会被合并到一起

其次句柄数量更多的 child node 摆放在 children 数组更靠前的位置.

如egg句柄数量更多,那么它就将会更靠前,以便于更早被遍历到


跳表原理简单介绍

其实用上述数据结构已经非常合适了,但是我为什么还要介绍一下SkipList这种数据结构呢,因为Redis中GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。而Sorted Set底层其实就是跳表,那么就简单介绍一下。


链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。

如图所示

image.png

  • L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
  • L1 层级共有 3 个节点,分别是节点 2、3、5;
  • L2 层级只有 1 个节点,也就是节点 3 。

如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。

可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)


想要自己简单动手去实现一下跳表可以刷一下对应的题(力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台)

这里贴一下自己写的跳表代码

type Node struct {
   
   
    Val  int
    Next *Node
    Down *Node
}

type Skiplist struct {
   
   
    head *Node
}

func Constructor() Skiplist {
   
   
    return Skiplist{
   
   head:&Node{
   
   Val:-1,Next:nil,Down:nil} }
}

func (this *Skiplist) Search(target int) bool {
   
   
    curr:=this.head
    for curr!=nil{
   
   
        for curr.Next!=nil&&curr.Next.Val<target{
   
   
            curr=curr.Next
        }
        if curr.Next != nil&&curr.Next.Val==target{
   
   
            return true
        }
        curr=curr.Down
    }
    return false
}

func (this *Skiplist) Add(num int) {
   
   
    curr:=this.head
    isInsert:=true
    down:=&Node{
   
   Val:-1,Next:nil,Down:nil}
    deque:=[]*Node{
   
   }
    for curr!=nil{
   
   
        for curr.Next!=nil&&curr.Next.Val<num{
   
   
            curr=curr.Next
        }
        deque=append(deque,curr)
        curr=curr.Down
    }
    for len(deque)>0&&isInsert{
   
   
        curr=deque[len(deque)-1]
        deque=deque[:len(deque)-1]
        if down.Val==-1{
   
   
            curr.Next=&Node{
   
   Val:num,Next:curr.Next,Down:nil}
        }else{
   
   
            curr.Next=&Node{
   
   Val:num,Next:curr.Next,Down:down}
        }
        down=curr.Next
        isInsert=rand.Float64()>0.5
    }
    if isInsert{
   
   
        this.head=&Node{
   
   Val:-1,Next:nil,Down:this.head}
    }
}

func (this *Skiplist) Erase(num int) bool {
   
   
    curr, isFound := this.head, false
    for curr != nil {
   
   
        for curr.Next != nil && curr.Next.Val < num {
   
   
            curr = curr.Next
        }
        if curr.Next != nil && curr.Next.Val == num {
   
   
            isFound = true
            curr.Next = curr.Next.Next
        }
        curr = curr.Down
    }
    return isFound

}
相关文章
|
9月前
|
分布式计算 Shell MaxCompute
odps测试表及大量数据构建测试
odps测试表及大量数据构建测试
|
监控 JavaScript 前端开发
JavaScript与HTML关系及其嵌入方式:新手常犯错误与规避策略
【4月更文挑战第1天】本文介绍了JavaScript与HTML的关系,强调了理解它们的分工和协作对于初学者的重要性。文中列举了新手在嵌入JavaScript时常见的错误,如嵌入位置不当、异步与延迟属性混淆、内联脚本与HTML混杂、忽略浏览器兼容性以及缺乏错误处理。提供了避免这些错误的策略,包括合理安排script标签、使用事件监听器、关注浏览器兼容性、学习调试技巧,并提倡遵循“结构-样式-行为”分离原则和使用错误处理机制。遵循这些最佳实践,有助于提高代码质量和开发效率。
352 1
|
Kubernetes Cloud Native API
【云原生】kubernetes v1.18部署Metrics-Server:v0.3.6
【云原生】kubernetes v1.18部署Metrics-Server:v0.3.6
492 1
|
11月前
|
存储 NoSQL 算法
Redis地理散列GeoHash
GeoHash作为一种高效的地理位置编码算法,在Redis中得到了很好的支持。通过使用Redis的GeoHash命令,可以方便地进行地理位置的存储、查询和计算。GeoHash在位置存储、附近位置搜索、距离计算和实时定位等场景中有着广泛的应用。掌握GeoHash及其在Redis中的使用方法,可以极大地提高地理位置相关应用的开发效率和性能。
177 5
|
Web App开发 JavaScript 前端开发
Python 自动化 - 浏览器chrome打开F12开发者工具自动Paused in debugger调试导致无法查看网站资源问题原因及解决方法,javascript反调试问题处理实例演示
Python 自动化 - 浏览器chrome打开F12开发者工具自动Paused in debugger调试导致无法查看网站资源问题原因及解决方法,javascript反调试问题处理实例演示
957 0
Python 自动化 - 浏览器chrome打开F12开发者工具自动Paused in debugger调试导致无法查看网站资源问题原因及解决方法,javascript反调试问题处理实例演示
|
消息中间件 Java 网络架构
|
分布式计算 MaxCompute 计算机视觉
ODPS问题之odps.sql.mapper.split.size属性有什么作用,以及如何根据场景调整它
ODPS问题之odps.sql.mapper.split.size属性有什么作用,以及如何根据场景调整它
884 1
|
12月前
|
安全 NoSQL 关系型数据库
阿里云数据库:构建高性能与安全的数据管理系统
在企业数字化转型过程中,数据库是支撑企业业务运转的核心。随着数据量的急剧增长和数据处理需求的不断增加,企业需要一个既能提供高性能又能保障数据安全的数据库解决方案。阿里云数据库产品为企业提供了一站式的数据管理服务,涵盖关系型、非关系型、内存数据库等多种类型,帮助企业构建高效的数据基础设施。
555 2
|
机器学习/深度学习 数据采集
深度学习中的模型优化:策略与实践
【9月更文挑战第9天】本文深入探讨了在深度学习领域,如何通过一系列精心挑选的策略来提升模型性能。从数据预处理到模型架构调整,再到超参数优化,我们将逐一剖析每个环节的关键因素。文章不仅分享了实用的技巧和方法,还提供了代码示例,帮助读者更好地理解和应用这些优化技术。无论你是深度学习的初学者还是有经验的研究者,这篇文章都将为你提供宝贵的参考和启示。
|
前端开发 小程序
新版校园跑腿外卖独立版+APP+小程序前端外卖配送平台源码
同城校园跑腿外卖配送平台源码,支持自定义diy 你可以设计你的页面,设计你自己的风格,支持多校园,独立版本,多商户,有用户端,骑手端,商家端,强大的功能
501 3