diff --git a/blog/数据结构STL——golang实现一致性哈希consistentHash.md b/blog/数据结构STL——golang实现一致性哈希consistentHash.md new file mode 100644 index 0000000..8a1c3be --- /dev/null +++ b/blog/数据结构STL——golang实现一致性哈希consistentHash.md @@ -0,0 +1,330 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 一致性哈希(consistent hash),与一致性哈希相对的是不一致性哈希,但常见的所有的哈希几乎都是不一致的,即哈希桶的容量的不固定的,可以根据需求进行扩容和缩容,不一致性哈希可以提高空间的利用率,但相应的,当进行扩容和缩容操作时需要对桶内存储的所有元素重新计算哈希值,这在某些情况是十分麻烦的事情,特别是在分布式存储的环境下,此时每个哈希结点就相当于一个机器,文件分布在哪台机器由哈希算法来决定,这个系统想要加一台机器时就需要停下来等所有文件重新分布一次才能对外提供服务,而当一台机器掉线的时候尽管只掉了一部分数据,但所有数据访问路由都会出问题。这样整个服务就无法平滑的扩缩容,成为了有状态的服务。 + +​ 为了避免分布式存储出现的状态化问题,就需要利用一致性哈希去解决,一致性哈希是固定了桶的容量,即2^32,然后将结点的哈希值计算后放入其中,当要从一致性哈希中寻找机器结点的时候,可以将hash桶视为一个大小为2^32的环状结构,要查找的结点也计算出hash并放入其中,然后沿顺时针寻找直到遇到第一个机器结点即可。 + +​ 而当增删机器结点的时候,只需要去调度该机器节点临近的一些结点即可,避免了对所有节点的全部调度。 + +​ 当机器节点的hash值比较密集的集中在环上的一部分的时候,就有可能出现**数据倾斜**的问题,即有较多存储结点会映射到其中一个机器结点上,造成该节点的负担较大。对于该问题一般采用增加**虚拟结点**的方案去解决,即对于每一个机器节点,可以凭空创造一些结点作为该虚拟节点的等价替代结点并放入hash环内,以此来充盈整个hash环,从而尽可能减少数据倾斜堆的概率。**虚拟节点数量一般采用32**,主要由于hash环的大小为2^32,采用32个虚拟节点可以近似使得转换出的32bit的每一位都可以存在一个虚拟节点从而实现充盈hash环的目的。 + +### 原理 + +​ 对于一致性hash来说,它需要做的一方面是对机器结点和其对应的虚拟节点的key进行hash计算,然后将计算机插入到hash环上,随后将hash环进行排序;另一方面是计算用于查找的结点的hash值并利用hash环找到其对应的机器节点或机器节点的虚拟节点,然后映射到机器节点并返回。 + +​ 考虑到存储的值仅仅只有机器节点及其虚拟结点,同时机器节点也可以视为其虚拟节点,所以主要需要解决的问题就变成了虚拟节点之间的hash冲突问题。 + +​ 对于虚拟节点的hash冲突来说,由于一致性hash的寻找不应当出现一次找到两个不同的结点,即hash冲突出现时不应该通过拓展的方式去解决。所以可以考虑采用再次hash法,即当一个虚拟节点与之前的结点发送冲突时则废弃该结点,然后重新生成一个虚拟节点在进行计算比较即可。 + +​ 同时,由于虚拟节点可以通过map映射到机器节点上,所以理论上生成的虚拟节点都可以进行直接映射,而没有太多的额外限制,故重新生成虚拟节点放入hash环内一方面可以解决hash冲突,另一方面也不会出现找不到对应的机器节点的情况。 + +​ 除此之外,考虑到再hash也需要一定的时间,所以在进行hash计算的时候,一方面利用**素数做逐位城际累加**,另一方面利用虚拟结点的**id做平方法**去从素数表中找到对应的素数做计算。从而尽可能降低出现hash冲突的概率。 + +### 实现 + +##### hash + +```go +//最大虚拟节点数量 +const ( + maxReplicas = 32 +) + +//素数表 +//用以减少hash冲突 +var primes = []uint32{3, 5, 7, 11, 13, 17, 19, 23, 29, 31, + 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, + 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, + 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, + 121, 191, 193, 197, 199, 211, 223, 227, 229, 233, + 239, 241, 251, +} +``` + +​ 传入一个虚拟节点id和实际结点,计算出它的hash值,先利用id从素数表中找到对应的素数,然后将id,素数和实际结点转化为[]byte,逐层访问并利用素数计算其hash值随后返回。 + +```go +func hash(id int, v interface{}) (h uint32) { + prime := primes[(id*id+len(primes))%len(primes)] + h = uint32(0) + s := fmt.Sprintf("%d-%v-%d", id*int(prime), v, prime) + bs := []byte(s) + for i := range bs { + h += uint32(bs[i]) * prime + } + return h +} +``` + +#### 结构体 + +​ 一致性hash结构体,该实例了一致性hash在创建时设定的最小虚拟节点数,同时保存了所有虚拟节点的hash值,建立了虚拟节点hash值与实际结点之间的映射表,每个实际结点也可以映射其所有的虚拟节点的hash值,并发控制锁用以保证线程安全。 + +```go +type ConsistentHash struct { + minReplicas int //最小的虚拟节点数 + keys []uint32 //存储的结点和虚拟结点的集合 + hashMap map[uint32]interface{} //hash与结点之间的映射 + nodeMap map[interface{}][]uint32 //结点所对应的虚拟节点的hash值 + mutex sync.Mutex //并发控制锁 +} +``` + +#### 接口 + +```go +type consistentHasher interface { + Size() (num int) //返回一致性hash中的虚拟节点数量 + Clear() //清空一致性hash的所有结点 + Empty() (b bool) //返回该一致性hash是否有结点存在 + Insert(keys ...interface{}) (nums []int) //向该一致性hash中插入一组结点,同时返回其生成的虚拟节点的数量 + Erase(key interface{}) (ok bool) //删除结点key + Get(key interface{}) (ans interface{}) //查找key对应的结点 +} +``` + +#### New + +​ 新建一个一致性hash结构体指针并返回,传入设定的最少虚拟节点数量,不可小于1也不可大于最大虚拟节点数。 + +```go +func New(minReplicas int) (ch *ConsistentHash) { + if minReplicas > maxReplicas { + //超过最大虚拟节点数 + minReplicas = maxReplicas + } + if minReplicas < 1 { + //最少虚拟节点数量不得小于1 + minReplicas = 1 + } + ch = &ConsistentHash{ + minReplicas: minReplicas, + keys: make([]uint32, 0, 0), + hashMap: make(map[uint32]interface{}), + nodeMap: make(map[interface{}][]uint32), + mutex: sync.Mutex{}, + } + return ch +} +``` + +#### Size + +​ 以一致性hash做接收者,返回该容器当前含有的虚拟结点数量,如果容器为nil返回0。 + +```go +func (ch *ConsistentHash) Size() (num int) { + if ch == nil { + return 0 + } + return len(ch.keys) +} +``` + +#### Clear + +​ 以一致性hash做接收者,将该容器中所承载的所有结点清除,被清除结点包括实际结点和虚拟节点,重建映射表。 + +```go +func (ch *ConsistentHash) Clear() { + if ch == nil { + return + } + ch.mutex.Lock() + //重建vector并扩容到16 + ch.keys = make([]uint32, 0, 0) + ch.hashMap = make(map[uint32]interface{}) + ch.nodeMap = make(map[interface{}][]uint32) + ch.mutex.Unlock() +} +``` + +#### Empty + +​ 以一致性hash做接收者,判断该一致性hash中是否含有元素,如果含有结点则不为空,返回false,如果不含有结点则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (ch *ConsistentHash) Empty() (b bool) { + if ch == nil { + return false + } + return len(ch.keys) > 0 +} +``` + +#### Insert + +​ 以一致性hash做接收者,向一致性hash中插入结点,同时返回每一个结点插入的数量,插入每个结点后将其对应的虚拟节点的hash放入映射表内和keys内,对keys做排序,利用二分排序算法。 + +```go +func (ch *ConsistentHash) Insert(keys ...interface{}) (nums []int) { + nums = make([]int, 0, len(keys)) + ch.mutex.Lock() + //遍历所有待插入的结点 + for _, key := range keys { + num := 0 + //判断结点是否已经存在 + _, exist := ch.nodeMap[key] + if !exist { + //结点不存在,开始插入 + for i := 0; i < maxReplicas || num < ch.minReplicas; i++ { + //生成每个虚拟节点的hash值 + h := uint32(hash(i, key)) + //判断生成的hash值是否存在,存在则不插入 + _, ok := ch.hashMap[h] + if !ok { + //hash值不存在,进行插入 + num++ + ch.keys = append(ch.keys, h) + //同时建立hash值和结点之间的映射关系 + ch.hashMap[h] = key + ch.nodeMap[key] = append(ch.nodeMap[key], h) + } + } + } + nums = append(nums, num) + } + //对keys进行排序,以方便后续查找 + ch.sort(0, len(ch.keys)-1) + ch.mutex.Unlock() + return nums +} +``` + +##### sort + +​ 以一致性hash做接收者,二分排序,主要用于一致性hash的keys的排序,以保证其有序性,同时方便后续使用二分查找。 + +```go +func (ch *ConsistentHash) sort(L, R int) { + if L >= R { + //左下标大于右下标,结束 + return + } + //找到中间结点,从左右两侧以双指针形式向中间靠近 + l, r, m := L-1, R+1, ch.keys[(L+R)/2] + for l < r { + //左侧出现不小于中间结点时停下 + l++ + for ch.keys[l] < m { + l++ + } + //右侧出现不大于中间结点时停下 + r-- + for ch.keys[r] > m { + r-- + } + if l < r { + //左节点仍在右结点左方,交换结点的值 + ch.keys[l], ch.keys[r] = ch.keys[r], ch.keys[l] + } + } + //递归排序左右两侧保证去有序性 + ch.sort(L, l-1) + ch.sort(r+1, R) +} +``` + +#### Erase + +​ 以一致性hash做接收者,删除以key为索引的结点,先利用结点映射表判断该key是否存在于一致性hash内,若存在则从映射表内删除,同时找到其所有的虚拟节点的hash值,遍历keys进行删除。 + +```go +func (ch *ConsistentHash) Erase(key interface{}) (ok bool) { + ch.mutex.Lock() + hs, ok := ch.nodeMap[key] + if ok { + //该结点存在于一致性hash内 + //删除该结点 + delete(ch.nodeMap, key) + //删除所有该结点的虚拟节点与该结点的映射关系 + for i := range hs { + delete(ch.hashMap, hs[i]) + } + //将待删除的虚拟结点删除即可 + arr := make([]uint32, 0, len(ch.keys)-len(hs)) + for i := range ch.keys { + h := ch.keys[i] + flag := true + for j := range hs { + if hs[j] == h { + flag = false + } + } + if flag { + arr = append(arr, h) + } + } + ch.keys = arr + } + ch.mutex.Unlock() + return ok +} +``` + +#### Get + +​ 以一致性hash做接收者,从一致性hash中获取以key为索引的下一个结点的索引,只要一致性hash内存储了结点则必然可以找到,将keys看成一个环,要找的key的计算出hash后放入环中,按顺时针向下找直到遇到第一个虚拟节点,寻找过程利用二分,若二分找到的结点为末尾结点的下一个,即为首个虚拟节点。 + +```go +func (ch *ConsistentHash) Get(key interface{}) (ans interface{}) { + if len(ch.keys) == 0 { + return nil + } + ch.mutex.Lock() + //计算key的hash值 + h := hash(0, key) + //从现存的所有虚拟结点中找到该hash值对应的下一个虚拟节点对应的结点的索引 + idx := ch.search(h) + ans = ch.hashMap[ch.keys[idx]] + ch.mutex.Unlock() + return ans +} +``` + +##### search + +​ 以一致性hash做接收者,二分查找,找到最近的不小于该值的hash值,如果不存在则返回0,即进行取模运算即可。 + +```go +func (ch *ConsistentHash) search(h uint32) (idx uint32) { + //二分查找,寻找不小于该hash值的下一个值的下标 + l, m, r := uint32(0), uint32(len(ch.keys)/2), uint32(len(ch.keys)) + for l < r { + m = (l + r) / 2 + if ch.keys[m] >= h { + r = m + } else { + l = m + 1 + } + } + //当找到的下标等同于keys的长度时即为0 + return l % uint32(len(ch.keys)) +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/consistentHash" +) + +func main() { + ch:=consistentHash.New(32) + fmt.Println(ch.Insert("http://localhost:8001","http://localhost:8002","http://localhost:8003")) + fmt.Println(ch.Size()) + fmt.Println(ch.Get("group")) + fmt.Println(ch.Get("http://localhost:8002")) +} +``` + +> [32 32 32] +> 96 +> http://localhost:8001 +> http://localhost:8002 diff --git a/blog/数据结构STL——golang实现二叉搜索树Binary Search Tree.md b/blog/数据结构STL——golang实现二叉搜索树Binary Search Tree.md new file mode 100644 index 0000000..c5c1350 --- /dev/null +++ b/blog/数据结构STL——golang实现二叉搜索树Binary Search Tree.md @@ -0,0 +1,504 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 二叉搜索树(Binary Search Tree)不同于之前使用的线性结构,它是一种通过离散的多个点以指针的形式连接起来的**树形结构**。 + +​ 二叉树由一个根节点和根节点下属的多层次的子结点构成,任意一个结点最多只能拥有两个子结点,即左右子结点。基于此种特性,在实现二叉搜索树时,可以仅持有根节点,然后通过根节点去递归访问其子结点以实现寻找到所有结点的。 + +​ 对于二叉搜索树,由于其中缀表达式是有序的,即从小到大(**本文章描述中普遍采用此方案**),所以对于任意一个结点来说,其左子树必然是小于它的,其右子树必然是大于它的。 + +### 原理 + +​ 对于一个二叉搜索树而言,由于其任意结点和其左右子树之间的关系来看,在插入结点时可以通过遍历找到对应的位置,然后插入新结点即可,当然,对于允许重复的情况来说,可以让结点存储的num+1即可,这样就不需要太多的空间去存储,以达到节约空间的目的。 + +​ 而对于删除的情况来说,也可以通过根节点去找到要删除的结点的位置,然后将该结点删除,随后,可以自行选择左子树的结点或右子树的结点替代该结点,对于用于替代的结点,需要去寻找到它的**前缀节点或后继节点**,前缀节点指该子树中最大的比它小的结点,即左子树的最右结点,后继节点指该子树中最小的比它大的结点,即右子树的最左结点。 + +​ 当要查找到对应的位置的情况时,通过根结点和给定的值,如果小于则进入左子树递归查找,如果大于进入右子树递归查找。 + +#### 添加策略 + +​ 对于二叉搜索树的添加结点的情况来说,需要考虑的情况主要是该结点是否存在于二叉搜中,如果存在只需要结点num+1即可,否则找到的位置必然是叶子节点,此时只需要插入一个新结点,让其父结点指向它即可。 + +​ 二叉搜索树添加结点的步骤: + +1. 通过先以根节点为父结点,再以递归结点为父结点,比对待插入值和父结点之间的大小,小于递归左子树,大于递归右子树,找到对应插入的位置 +2. 找到位置后,如果该元素已存在,则结点num+1即可,否则新建结点,同时建立父结点与新结点之间的关系 + +#### 删除策略 + +​ 对于二叉搜索树的删除来说,首先需要先找到要删除的结点,如果没找到则直接结束即可,当找到后,判断该结点是否有子树,如果没有,直接删除即可,如果有左子树,则寻找其前缀节点,即左子树的最右结点,如果只有右子树,则寻找其后缀节点,即右子树的最左结点,将寻找到的结点替换为当前父结点,然后删除被替换的结点即可。 + +​ 二叉搜索树删除结点的步骤: + +1. 通过递归比对遍历寻找到要删除的结点 +2. 如果该结点不存在则直接结束,如果有重复则num-1即可结束 +3. 如果该结点无左右子树,删除该结点即可结束 +4. 如果有左子树,寻找到前缀结点替换该结点,随后删除前缀节点即可结束 +5. 如果只有右子树,寻找到后缀结点替换该结点,随后删除后缀节点即可结束 +6. 删除完成 + +### 实现 + +​ bsTree二叉搜索树结构体,该实例存储二叉树的根节点,同时保存该二叉树已经存储了多少个元素,二叉树中排序使用的比较器在创建时传入,若不传入则在插入首个节点时从默认比较器中寻找,创建时传入是否允许该二叉树出现重复值,如果不允许则进行覆盖,允许则对节点数目增加即可。 + +```go +type bsTree struct { + root *node //根节点指针 + size uint64 //存储元素数量 + cmp comparator.Comparator //比较器 + isMulti bool //是否允许重复 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是二叉搜索树的树节点,若该二叉搜索树允许重复则对节点num+1即可,否则对value进行覆盖,二叉搜索树节点不做平衡。 + +```go +type node struct { + value interface{} //节点中存储的元素 + num uint64 //该元素数量 + left *node //左节点指针 + right *node //右节点指针 +} +``` + +#### 接口 + +```go +type bsTreeer interface { + Iterator() (i *Iterator.Iterator) //返回包含该二叉树的所有元素,重复则返回多个 + Size() (num uint64) //返回该二叉树中保存的元素个数 + Clear() //清空该二叉树 + Empty() (b bool) //判断该二叉树是否为空 + Insert(e interface{}) //向二叉树中插入元素e + Erase(e interface{}) //从二叉树中删除元素e + Count(e interface{}) (num uint64) //从二叉树中寻找元素e并返回其个数 +} +``` + +#### New + +​ 新建一个bsTree二叉搜索树容器并返回,初始根节点为nil,传入该二叉树是否为可重复属性,如果为true则保存重复值,否则对原有相等元素进行覆盖,若有传入的比较器,则将传入的第一个比较器设为该二叉树的比较器。 + +```go +func New(isMulti bool, Cmp ...comparator.Comparator) (bs *bsTree) { + //判断是否有传入比较器,若有则设为该二叉树默认比较器 + var cmp comparator.Comparator + if len(Cmp) == 0 { + cmp = nil + } else { + cmp = Cmp[0] + } + return &bsTree{ + root: nil, + size: 0, + cmp: cmp, + isMulti: isMulti, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个二叉搜索树节点并返回,将传入的元素e作为该节点的承载元素,该节点的num默认为1,左右子节点设为nil。 + +```go +func newNode(e interface{}) (n *node) { + return &node{ + value: e, + num: 1, + left: nil, + right: nil, + } +} +``` + +#### Iterator + +​ 以bsTree二叉搜索树做接收者,将该二叉树中所有保存的元素将从根节点开始以中缀序列的形式放入迭代器中,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (bs *bsTree) Iterator() (i *Iterator.Iterator) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + bs.mutex.Lock() + es := bs.root.inOrder() + i = Iterator.New(&es) + bs.mutex.Unlock() + return i +} +``` + +​ 以node二叉搜索树节点做接收者,以中缀序列返回节点集合,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (n *node) inOrder() (es []interface{}) { + if n == nil { + return es + } + if n.left != nil { + es = append(es, n.left.inOrder()...) + } + for i := uint64(0); i < n.num; i++ { + es = append(es, n.value) + } + if n.right != nil { + es = append(es, n.right.inOrder()...) + } + return es +} +``` + +#### Size + +​ 以bsTree二叉搜索树做接收者,返回该容器当前含有元素的数量,如果容器为nil则创建一个并返回其承载的元素个数。 + +```go +func (bs *bsTree) Size() (num uint64) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + return bs.size +} +``` + +#### Clear + +​ 以bsTree二叉搜索树做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (bs *bsTree) Clear() { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + bs.mutex.Lock() + bs.root = nil + bs.size = 0 + bs.mutex.Unlock() +} +``` + +#### Empty + +​ 以bsTree二叉搜索树做接收者,判断该二叉搜索树是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (bs *bsTree) Empty() (b bool) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + return bs.size == 0 +} +``` + +#### Insert + +​ 以bsTree二叉搜索树做接收者,向二叉树插入元素e,若不允许重复则对相等元素进行覆盖,如果二叉树为空则之间用根节点承载元素e,否则以根节点开始进行查找,不做平衡。 + +```go +func (bs *bsTree) Insert(e interface{}) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + bs.mutex.Lock() + if bs.Empty() { + //二叉树为空,用根节点承载元素e + if bs.cmp == nil { + bs.cmp = comparator.GetCmp(e) + } + if bs.cmp == nil { + bs.mutex.Unlock() + return + } + bs.root = newNode(e) + bs.size++ + bs.mutex.Unlock() + return + } + //二叉树不为空,从根节点开始查找添加元素e + if bs.root.insert(e, bs.isMulti, bs.cmp) { + bs.size++ + } + bs.mutex.Unlock() +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中插入元素e,如果n节点中承载元素与e不同则根据大小从左右子树插入该元素,如果n节点与该元素相等,且允许重复值,则将num+1否则对value进行覆盖,插入成功返回true,插入失败或不允许重复插入返回false。 + +```go +func (n *node) insert(e interface{}, isMulti bool, cmp comparator.Comparator) (b bool) { + if n == nil { + return false + } + //n中承载元素小于e,从右子树继续插入 + if cmp(n.value, e) < 0 { + if n.right == nil { + //右子树为nil,直接插入右子树即可 + n.right = newNode(e) + return true + } else { + return n.right.insert(e, isMulti, cmp) + } + } + //n中承载元素大于e,从左子树继续插入 + if cmp(n.value, e) > 0 { + if n.left == nil { + //左子树为nil,直接插入左子树即可 + n.left = newNode(e) + return true + } else { + return n.left.insert(e, isMulti, cmp) + } + } + //n中承载元素等于e + if isMulti { + //允许重复 + n.num++ + return true + } + //不允许重复,直接进行覆盖 + n.value = e + return false +} +``` + +#### Erase + +​ 以bsTree二叉搜索树做接收者,从搜素二叉树中删除元素e,若允许重复记录则对承载元素e的节点中数量记录减一即可,若不允许重复记录则删除该节点同时将前缀节点或后继节点更换过来以保证二叉树的不发送断裂,如果该二叉树仅持有一个元素且根节点等价于待删除元素,则将二叉树根节点置为nil。 + +```go +func (bs *bsTree) Erase(e interface{}) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + if bs.size == 0 { + return + } + bs.mutex.Lock() + if bs.size == 1 && bs.cmp(bs.root.value, e) == 0 { + //二叉树仅持有一个元素且根节点等价于待删除元素,将二叉树根节点置为nil + bs.root = nil + bs.size = 0 + bs.mutex.Unlock() + return + } + //从根节点开始删除元素e + //如果删除成功则将size-1 + if bs.root.delete(e, bs.isMulti, bs.cmp) { + bs.size-- + } + bs.mutex.Unlock() +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中删除元素e,如果n节点中承载元素与e不同则根据大小从左右子树删除该元素,如果n节点与该元素相等,且允许重复值,则将num-1否则直接删除该元素,删除时先寻找该元素的前缀节点,若不存在则寻找其后继节点进行替换,替换后删除该节点。 + +```go +func (n *node) delete(e interface{}, isMulti bool, cmp comparator.Comparator) (b bool) { + if n == nil { + return false + } + //n中承载元素小于e,从右子树继续删除 + if cmp(n.value, e) < 0 { + if n.right == nil { + //右子树为nil,删除终止 + return false + } + if cmp(e, n.right.value) == 0 && (!isMulti || n.right.num == 1) { + if n.right.left == nil && n.right.right == nil { + //右子树可直接删除 + n.right = nil + return true + } + } + //从右子树继续删除 + return n.right.delete(e, isMulti, cmp) + } + //n中承载元素大于e,从左子树继续删除 + if cmp(n.value, e) > 0 { + if n.left == nil { + //左子树为nil,删除终止 + return false + } + if cmp(e, n.left.value) == 0 && (!isMulti || n.left.num == 1) { + if n.left.left == nil && n.left.right == nil { + //左子树可直接删除 + n.left = nil + return true + } + } + //从左子树继续删除 + return n.left.delete(e, isMulti, cmp) + } + //n中承载元素等于e + if (*n).num > 1 && isMulti { + //允许重复且个数超过1,则减少num即可 + (*n).num-- + return true + } + if n.left == nil && n.right == nil { + //该节点无前缀节点和后继节点,删除即可 + *(&n) = nil + return true + } + if n.left != nil { + //该节点有前缀节点,找到前缀节点进行交换并删除即可 + ln := n.left + if ln.right == nil { + n.value = ln.value + n.num = ln.num + n.left = ln.left + } else { + for ln.right.right != nil { + ln = ln.right + } + n.value = ln.right.value + n.num = ln.right.num + ln.right = ln.right.left + } + } else if (*n).right != nil { + //该节点有后继节点,找到后继节点进行交换并删除即可 + tn := n.right + if tn.left == nil { + n.value = tn.value + n.num = tn.num + n.right = tn.right + } else { + for tn.left.left != nil { + tn = tn.left + } + n.value = tn.left.value + n.num = tn.left.num + tn.left = tn.left.right + } + return true + } + return true +} +``` + +#### Count + +​ 以bsTree二叉搜索树做接收者,从搜素二叉树中查找元素e的个数,如果找到则返回该二叉树中和元素e相同元素的个数,如果不允许重复则最多返回1,如果未找到则返回0。 + +```go +func (bs *bsTree) Count(e interface{}) (num uint64) { + if bs == nil { + //二叉树不存在,返回0 + return 0 + } + if bs.Empty() { + //二叉树为空,返回0 + return 0 + } + bs.mutex.Lock() + //从根节点开始查找并返回查找结果 + num = bs.root.search(e, bs.isMulti, bs.cmp) + bs.mutex.Unlock() + return num +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中查找元素e并返回存储的个数,如果n节点中承载元素与e不同则根据大小从左右子树查找该元素,如果n节点与该元素相等,则直接返回其个数。 + +```go +func (n *node) search(e interface{}, isMulti bool, cmp comparator.Comparator) (num uint64) { + if n == nil { + return 0 + } + //n中承载元素小于e,从右子树继续查找并返回结果 + if cmp(n.value, e) < 0 { + return n.right.search(e, isMulti, cmp) + } + //n中承载元素大于e,从左子树继续查找并返回结果 + if cmp(n.value, e) > 0 { + return n.left.search(e, isMulti, cmp) + } + //n中承载元素等于e,直接返回结果 + return n.num +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/bsTree" + "sync" +) + +func main() { + bs := bsTree.New(true) + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + bs.Insert(i) + go func() { + bs.Insert(i) + wg.Done() + }() + } + wg.Wait() + fmt.Println("遍历输出所有插入的元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("删除一次二叉搜索树中存在的元素,存在重复的将会被剩下") + for i := 0; i < 10; i++ { + bs.Erase(i) + } + fmt.Println("输出剩余的重复元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 遍历输出所有插入的元素 +> 0 +> 1 +> 2 +> 3 +> 3 +> 4 +> 5 +> 6 +> 6 +> 6 +> 7 +> 7 +> 7 +> 7 +> 7 +> 8 +> 9 +> 10 +> 10 +> 10 +> 删除一次二叉搜索树中存在的元素,存在重复的将会被剩下 +> 输出剩余的重复元素 +> 3 +> 6 +> 6 +> 7 +> 7 +> 7 +> 7 +> 10 +> 10 +> 10 +> + diff --git a/blog/数据结构STL——golang实现优先队列priority_queue.md b/blog/数据结构STL——golang实现优先队列priority_queue.md new file mode 100644 index 0000000..d7eae2f --- /dev/null +++ b/blog/数据结构STL——golang实现优先队列priority_queue.md @@ -0,0 +1,352 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 优先队列(priority_queue)它虽然名字上是被称之为队列,单它底层其实是以堆的方式实现的,而堆这个数据结构,它是通过建立一棵**完全二叉树**来进行实现的。它在逻辑上并非是一个线性结构,但由于**二叉树可以用数组表示**的特性,**本次实现采用数组的形式实现**,后续会再使用完全二叉搜实现一次。 + +​ 堆或者说优先队列的主要特点为:它的**父结点必然是小于或等于左右子结点**(该文章叙述中使用**小顶堆**,大顶堆大小情况相反),同时,考虑到用数组来模拟完全二叉搜的情况,它的所有结点必然在数组上是连续的,不会存在上一个结点和下一个结点间存在一段间隔的情况。 + +### 原理 + +##### 数组模拟完全二叉树 + +​ 对于使用数组来模拟完全二叉树的情况来说,首先,完全二叉树除开底部的一层可能存在不满的情况外,其他上层必然是满的,同时,对于完全二叉搜的任意一个结点来说,假设该结点的下标为**p(从0开始)**,使用数组来模拟的情况下,其左子结点的下标则必然是**2p+1**,而其右子结点的下标则必然是**2p+2**,当然,也可能出现不存在右子结点或不存在左子结点的情况。 + +​ 对于数组来说,可以将数组根据2^n来进行划分,n从0开始,即0位为第一层,1~2为第二层,3~6为第三层,7~17为第四层······以此可有 +$$ +2^{(n-1)}-1——2^{n}-2 +$$ +为第n层,同时,由于任意结点的左右子结点必然在其下一层,所以通过计算也可得知其左右子结点的下标也符合上文所述。当然,如果给定一个结点下标为**p**(除首结点,毕竟首节点无父结点),则它父结点的下标必然为**p/2** + +##### 优先队列实现 + +​ 对于一个优先队列来说,它是建立来完全二叉搜的基础上,同时其任意结点的值必然不大于其左右子结点,所以可知道,当插入节点时,可以利用尾部结点和父结点进行比较交换来实现,对于删除节点时,可以利用顶部结点与左右子结点的值进行比较进行下降即可。 + +#### 添加策略 + +​ 当插入一个结点的时候,可以先将其放入二叉树的末尾,然后根据插入的值和它父结点进行比较,如果插入值小于父节点时则交换两结点值,随后在用交换后的结点与其父结点比较,重复该过程直到抵达顶点或不满足条件即可。当然,添加的过程中,由于底层使用动态数组来存储,所以可能出现扩容的情况,由于其情况和vector类似,则不在赘述,仅在下方列出即可。 + +##### 添加步骤 + +1. 先判断动态数组中是否有冗余空间,有则直接放入尾部,否则利用扩容策略进行扩容,扩容后再放入尾部即可 +2. 结点放入尾部后,通过比较该结点与父结点的值进行交换 +3. 满足交换条件,重复回到2过程 +4. 到达顶部或不满足交换条件,添加结束,插入完成 + +##### 扩容策略 + +1. 固定扩容:固定的去增加一定量的空间,该方案时候在数组较大时添加空间,可以避免直接翻倍导致的**冗余过量**问题,到由于增加量是固定的,如果需要一次扩容很多量的话就会比较缓慢。 +2. 翻倍扩容:将原有容量直接翻倍,该方案适合在数组不太大的适合添加空间,可以提高扩容量,增加扩容效率,但数组太大时使用的话会导致一次性增加太多空间,进而造成空间的浪费。 + +#### 删除策略 + +​ 不同于添加时直接利用尾结点的情况,由于删除的是首结点,而同时删除需要减少一个空间,所以可以考虑将首位结点进行交换,或者其实直接**用尾结点覆写首节点**即可,这样就实现了首节点的删除,同时减小一位删去移到首位的尾结点实现实际的删除。同时,在删除后,需要判断实际使用空间是否满足缩容策略的情况,如果满足则需要使用缩容策略进行缩容,缩容策略同vector,已在下方列出。 + +​ 上一过程完成后,首节点以及被删除了,但移到首节点的新首节点可能并不满足优先队列的父结点必然小于或等于左右子结点的情况,所以要通过比较父结点和其左右子结点来进行下将操作,即当存在一个在数组范围内的结点且大于父结点时则交换两结点,然后递归该过程,直到触底或不满足条件。 + +​ 比较过程中,先比较左结点与父结点的情况,然后再比较右结点的情况,找到最小的一侧进行下降即可。 + +##### 删除步骤 + +1. 将尾结点覆盖首结点 +2. 删除首结点,进行缩容判断,并可能利用缩容策略进行缩容 +3. 以首节点为父结点,对其左右结点进行下降判断:左右节点是否存在,且存在的左右节点是否存在小于父结点的情况, +4. 比对左右两侧,找到满足小于父结点且最小的一侧进行下降即可,同时返回3过程进行递归下降 +5. 触底或不满足条件时下降结束,删除完成。 + +##### 缩容策略 + +1. 固定缩容:释放一个固定的空间,该方案适合在当前数组较大的时候进行,可以减缓需要缩小的量,当空间较大的时候如果采取折半缩减的话;将可能在绝大多数时间内都不会被采用,从而造成空间浪费。 +2. 折半缩容:将当前容量进行折半,该方案适合数组不太大的适合进行缩容,可以更快的缩小容量,但对于较大的空间来说并不会频繁使用到。 + +### 实现 + +​ priority_queue优先队列集合结构体,包含动态数组和比较器,同时包含其实际使用长度和实际占用空间容量,该数据结构可以存储多个相同的元素,并不会产生冲突,增删节点后会使用比较器保持该动态数组的相对有序性。 + +```go +type priority_queue struct { + data []interface{} //动态数组 + len uint64 //实际使用长度 + cap uint64 //实际占用的空间的容量 + cmp comparator.Comparator //该优先队列的比较器 + mutex sync.Mutex //并发控制锁 +} +``` + +#### 接口 + +```go +type priority_queueer interface { + Size() (num uint64) //返回该容器存储的元素数量 + Clear() //清空该容器 + Empty() (b bool) //判断该容器是否为空 + Push(e interface{}) //将元素e插入该容器 + Pop() //弹出顶部元素 + Top() (e interface{}) //返回顶部元素 +} +``` + +#### New + +​ 新建一个priority_queue优先队列容器并返回,初始priority_queue的切片数组为空,如果有传入比较器,则将传入的第一个比较器设为可重复集合默认比较器,如果不传入比较器,在后续的增删过程中将会去寻找默认比较器。 + +```go +func New(cmps ...comparator.Comparator) (pq *priority_queue) { + var cmp comparator.Comparator + if len(cmps) == 0 { + cmp = nil + } else { + cmp = cmps[0] + } + //比较器为nil时后续的增删将会去寻找默认比较器 + return &priority_queue{ + data: make([]interface{}, 1, 1), + len: 0, + cap: 1, + cmp: cmp, + mutex: sync.Mutex{}, + } +} +``` + +#### Size + +​ 以priority_queue容器做接收者,返回该容器当前含有元素的数量,由于其数值必然是非负整数,所以选用了**uint64**。 + +```go +func (pq *priority_queue) Size() (num uint64) { + if pq == nil { + pq = New() + } + return pq.len +} +``` + +#### Clear + +​ 以priority_queue容器做接收者,将该容器中所承载的元素清空。 + +```go +func (pq *priority_queue) Clear() { + if pq == nil { + pq = New() + } + pq.mutex.Lock() + //清空已分配的空间 + pq.data = make([]interface{}, 1, 1) + pq.len = 0 + pq.cap = 1 + pq.mutex.Unlock() +} +``` + +#### Empty + +​ 以priority_queue容器做接收者,判断该priority_queue容器是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true,该判断过程通过含有元素个数进行判断。 + +```go +func (pq *priority_queue) Empty() bool { + if pq == nil { + pq = New() + } + return pq.len == 0 +} +``` + +#### Push + +​ 以priority_queue容器做接收者,在该优先队列中插入元素e,利用比较器和交换使得优先队列保持相对有序状态,插入时,首先将该元素放入末尾,然后通过比较其逻辑上的父结点选择是否上移,扩容策略同vector,先进行翻倍扩容,在进行固定扩容,界限为2^16。 + +```go +func (pq *priority_queue) Push(e interface{}) { + if pq == nil { + pq = New() + } + pq.mutex.Lock() + //判断是否存在比较器,不存在则寻找默认比较器,若仍不存在则直接结束 + if pq.cmp == nil { + pq.cmp = comparator.GetCmp(e) + } + if pq.cmp == nil { + pq.mutex.Unlock() + return + } + //先判断是否需要扩容,同时使用和vector相同的扩容策略 + //即先翻倍扩容再固定扩容,随后在末尾插入元素e + if pq.len < pq.cap { + //还有冗余,直接添加 + pq.data[pq.len] = e + } else { + //冗余不足,需要扩容 + if pq.cap <= 65536 { + //容量翻倍 + if pq.cap == 0 { + pq.cap = 1 + } + pq.cap *= 2 + } else { + //容量增加2^16 + pq.cap += 65536 + } + //复制扩容前的元素 + tmp := make([]interface{}, pq.cap, pq.cap) + copy(tmp, pq.data) + pq.data = tmp + pq.data[pq.len] = e + } + pq.len++ + //到此时,元素以插入到末尾处,同时插入位的元素的下标为pq.len-1,随后将对该位置的元素进行上升 + //即通过比较它逻辑上的父结点进行上升 + pq.up(pq.len - 1) + pq.mutex.Unlock() +} +``` + +##### up + +​ 以priority_queue容器做接收者,用于递归判断任意子结点和其父结点之间的关系,满足上升条件则递归上升,从而保证父节点必然都大于或都小于子节点。 + +```go +func (pq *priority_queue) up(p uint64) { + if p == 0 { + //以及上升到顶部,直接结束即可 + return + } + if pq.cmp(pq.data[(p-1)/2], pq.data[p]) > 0 { + //判断该结点和其父结点的关系 + //满足给定的比较函数的关系则先交换该结点和父结点的数值,随后继续上升即可 + pq.data[p], pq.data[(p-1)/2] = pq.data[(p-1)/2], pq.data[p] + pq.up((p - 1) / 2) + } +} +``` + +#### Pop + +​ 以priority_queue容器做接收者,在该优先队列中删除顶部元素,利用比较器和交换使得优先队列保持相对有序状态,删除时首先将首结点移到最后一位进行交换,随后删除最后一位即可,然后对首节点进行下降即可,缩容时同vector一样,先进行固定缩容在进行折半缩容,界限为2^16。 + +```go +func (pq *priority_queue) Pop() { + if pq == nil { + pq = New() + } + if pq.Empty() { + return + } + pq.mutex.Lock() + //将最后一位移到首位,随后删除最后一位,即删除了首位,同时判断是否需要缩容 + pq.data[0] = pq.data[pq.len-1] + pq.len-- + //缩容判断,缩容策略同vector,即先固定缩容在折半缩容 + if pq.cap-pq.len >= 65536 { + //容量和实际使用差值超过2^16时,容量直接减去2^16 + pq.cap -= 65536 + tmp := make([]interface{}, pq.cap, pq.cap) + copy(tmp, pq.data) + pq.data = tmp + } else if pq.len*2 < pq.cap { + //实际使用长度是容量的一半时,进行折半缩容 + pq.cap /= 2 + tmp := make([]interface{}, pq.cap, pq.cap) + copy(tmp, pq.data) + pq.data = tmp + } + //判断是否为空,为空则直接结束 + if pq.Empty() { + pq.mutex.Unlock() + return + } + //对首位进行下降操作,即对比其逻辑上的左右结点判断是否应该下降,再递归该过程即可 + pq.down(0) + pq.mutex.Unlock() +} +``` + +##### down + +​ 以priority_queue容器做接收者,判断待下沉节点与其左右子节点的大小关系以确定是否进行递归上升,从而保证父节点必然都大于或都小于子节点。 + +```go +func (pq *priority_queue) down(p uint64) { + q := p + //先判断其左结点是否在范围内,然后在判断左结点是否满足下降条件 + if 2*p+1 <= pq.len-1 && pq.cmp(pq.data[p], pq.data[2*p+1]) > 0 { + q = 2*p + 1 + } + //在判断右结点是否在范围内,同时若判断右节点是否满足下降条件 + if 2*p+2 <= pq.len-1 && pq.cmp(pq.data[q], pq.data[2*p+2]) > 0 { + q = 2*p + 2 + } + //根据上面两次判断,从最小一侧进行下降 + if p != q { + //进行交互,递归下降 + pq.data[p], pq.data[q] = pq.data[q], pq.data[p] + pq.down(q) + } +} +``` + +#### Top + +​ 以priority_queue容器做接收者,返回该优先队列容器的顶部元素,如果容器不存在或容器为空,返回nil。 + +```go +func (pq *priority_queue) Top() (e interface{}) { + if pq == nil { + pq = New() + } + if pq.Empty() { + return nil + } + pq.mutex.Lock() + e = pq.data[0] + pq.mutex.Unlock() + return e +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/priority_queue" + "sync" +) + +func main() { + pq := priority_queue.New() + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func(num int) { + pq.Push(num) + wg.Done() + }(i) + } + wg.Wait() + fmt.Println("遍历所有元素同时弹出:") + size:=pq.Size() + for i := uint64(0); i < size; i++ { + fmt.Println(pq.Top()) + pq.Pop() + } +} + +``` + +注:虽然添加过程是随机的,但由于其本身是相对有序的,所以不论怎么添加都是一个输出结果 + +> 遍历所有元素同时弹出: +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 7 +> 8 +> 9 diff --git a/blog/数据结构STL——golang实现位图bitmap.md b/blog/数据结构STL——golang实现位图bitmap.md new file mode 100644 index 0000000..5dccdd1 --- /dev/null +++ b/blog/数据结构STL——golang实现位图bitmap.md @@ -0,0 +1,304 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此可以**极大的节省存储空间**。 + +### 原理 + +​ 对于计算机来说,它可以分配的最小单位是**一个字节即8位**,一位有且仅有**1和0**两种表示可能。而如果要表示0~7八个数字在某一个集合内是否存在,比如表示{0,3,5}在集合中是否存在,可以使用一个字节进行存储,即对应位为1的表示存在,为0的表示不存在,即: + + + +| 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | + +而如果要表示超过8个的元素是否存在的话,仅仅使用一个字节是远远不够的。 + +​ 为了解决这个问题,可以选择使用包含更多字节的基本元素,比如**uint64**,选用uint64而不是int64的原因是因为,在计算机内部存储数字采用补码的形式,对于一个数字如果不是非负整数的话,它需要有一位去标注这个数字是否为负数,而在使用bitmap的时候,使用基本类型仅仅是为了使用它的位,至于是否是负数并不重要,既然如此,舍弃掉正负数标志位就可以多利用一位进行表示。 + +​ 除此之外,还可以选择多个基本元素进行叠加,即多组混合表示,以表示{0,3,5,8,14,15}为例: + +| **1** | **0** | **0** | **1** | **0** | **1** | **0** | **0** | +| ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | +| **1** | **0** | **0** | **0** | **0** | **0** | **1** | **1** | + +​ 即一第一行为0~7,第二行及以后为n*8+(0~7)进行表示,而对于golang实现来说,可以选择用uint64的切片进行表示,即在换算时候对于一个数字num,它所属的行(从0开始)为: +$$ +num/64 +$$ +​ 它所属的列(从0开始)为: +$$ +num%64 +$$ + +### 用处 + +​ bitmap由于其本身的有序性和唯一性,一来可以实现**快速排序**:即对于一组无需且**稠密**的数字,可以将其放入bitmap中,然后再遍历获取出来,从而使得其元素是单调递增的;而来可以实现**去重**:由于bitmap是使用bit进行表示一个元素是否存在其中,而bit有且仅有1和0两种情况,所以在一个位置插入多个元素时,它表示的都是1,从而可以实现插入多个保留一个的去重;三来可以极大的**节省存储空间**:由于对基本类型数组的下标赋予了一定的含义,导致可以利用其含义来表示一个数字,而不是去存储一个数字,即要表示一个2^16的数字时候,原本需要16位,现在只需要1位即可实现。 + +### 实现 + +#### 结构定义 + +​ 由上文可知,所要使用的bitmap内部其实是存储的uint64的切片,同时增加一个zero,,即 + +```go +type bitmap struct { + bits []uint64 +} +``` + +##### 接口集合 + +```go +type bitmaper interface { + Insert(num uint) //在num位插入元素 + Delete(num uint) //删除第num位 + Check(num uint) (b bool) //检查第num位是否有元素 + All() (nums []uint) //返回所有存储的元素的下标 + Clear() //清空 +} +``` + +#### New + +创建部分主要做的就是返回一个初始化后的bitmap指针 + +```go +func New() (bm *bitmap) { + return &bitmap{ + bits: make([]uint64, 0, 0), + } +} +``` + +#### Insert + +​ 对于bitmap的插入来说,会出现两种情况,第一种是要插入的位恰好可以在位图中容纳,此时将对应位设为1即可,并不需要对位图进行扩容,实现起来十分简单,找到对应位即可,第二章就是要插入的位超过了位图可容纳的范围,次数就需要一个扩容策略对其进行扩容以保证插入的值在位图内可以存储。 + +##### 扩容策略 + +​ 对于bitmap扩容来说,它的扩容主要是由于新要插入的位超出了位图所能表示的位,从而需要对位图进行扩增以满足其范围,而扩容则可以采取**两种策略**: + +1. **固定扩容**:对于要扩容的位图,进行一个固定的量进行扩容,该方案的好处实现简单,但坏处也明显,如果固定扩容后还是不足以覆盖还需要再次扩容,同时如果扩容太大也会需要更多的空间; +2. **按需扩容**:对于要扩容的位图,按照它需要扩容的范围来进行扩容,该方案的好处是几乎可以恰好容纳需要存储的位,但如果存在连续的几个较短的扩容出现时,即先扩容到64->128>256>512>1024这种情况时,需要连续的扩容复制,会浪费一定的性能。 + +对此,我们可以考虑将两种方案结合起来,即当要增加的范围并不太大的时候,**牺牲一定量的空间**进行固定扩容,从而避免连续的小范围扩容多次出现降低性能,而对于大范围扩容时,则使用按需扩容,以此来提高单次扩容速度。 + +​ 本次实现中,固定扩容采用一次增加**2^10个uint64**。 + +```go +func (bm *bitmap) Insert(num uint) { + //bm不存在时直接结束 + if bm == nil { + return + } + //开始插入 + if num/64+1 > uint(len(bm.bits)) { + //当前冗余量小于num位,需要扩增 + var tmp []uint64 + //通过冗余扩增减少扩增次数 + if num/64+1 < uint(len(bm.bits)+1024) { + //入的位比冗余的多不足2^16即1024*64时,则新增1024个uint64 + tmp = make([]uint64, len(bm.bits)+1024) + } else { + //直接增加到可以容纳第num位的位置 + tmp = make([]uint64, num/64+1) + } + //将原有元素复制到新增的切片内,并将bm所指向的修改为扩增后的 + copy(tmp, bm.bits) + bm.bits = tmp + } + //将第num位设为1即实现插入 + bm.bits[num/64] ^= 1 << (num % 64) +} +``` + +#### Delete + +​ 对于位图的删除,会出现三种情况,第一种是要删除的范围不在位图范围内,直接忽略即可;第二种是在位图范围内,且位图尾部存在连续为0的uint64时,此时就需要执行**缩容策略**。 + +##### 缩容策略 + +​ 对于缩容策略,同样采取三种方案: + +1. **固定缩容**:从尾部删除固定数量的为0的组,该方案删除的量是固定的,优点在于尾部量很多时可以慢慢减少,剩下的可当作冗余量避免多次增加,缺点是缓慢减少需要一定时间开销 +2. **按需缩容**:当存在需要删除的组时,直接进行删除,优点的空间利用率更高,缺点是冗余基本没有,同时删除过于频繁 +3. **折半删除**:当需要删除的组超过一半时,删掉一半即可,该方案删除切合slice底层实现,但在普遍场景来说,可能并不会经常使用。 + +为此,可以选择将三种结合起来,当总组数很长时,且需要删除的量过多,可以使用固定缩容,留出一定余量,当删除量不多时,且需要删除的量超过了总组数的一半时,在进行按需删除,即把要删除的全部删完。 + +​ 为了避免固定扩容和固定缩容循环出现,对两者数值进行错位,由于固定扩容选择的是1024,所以固定缩容选为256。 + +```go +func (bm *bitmap) Delete(num uint) { + //bm不存在时直接结束 + if bm == nil { + return + } + //num超出范围,直接结束 + if num/64+1 > uint(len(bm.bits)) { + return + } + //将第num位设为0 + bm.bits[num/64] &^= 1 << (num % 64) + if bm.bits[len(bm.bits)-1] == 0 { + //最后一组为0,可能进行缩容 + //从后往前遍历判断可缩容内容是否小于总组数 + i := len(bm.bits) - 1 + for ; i >= 0; i-- { + if bm.bits[i] == 0 && i!=len(bm.bits)-1024{ + continue + } else { + //不为0或到1024个时即可返回 + break + } + } + if i <= len(bm.bits)/2 || i==len(bm.bits)-1024 { + //小于总组数一半或超过1023个,进行缩容 + bm.bits = bm.bits[:i+1] + } + } else { + return + } +} +``` + +#### Check + +检查第num位是否存在于位图种,不存在返回false,存在返回true,超出范围也返回false。 + +```go +func (bm *bitmap) Check(num uint) (b bool) { + //bm不存在时直接返回false并结束 + if bm == nil { + return false + } + //num超出范围,直接返回false并结束 + if num/64+1 > uint(len(bm.bits)) { + return false + } + //判断第num是否为1,为1返回true,否则为false + if bm.bits[num/64]&(1< 0 { + return true + } + return false +} +``` + +#### All + +返回所有在位图种存在的元素的下标,同时保证其为单调递增序列。 + +```go +func (bm *bitmap) All() (nums []uint) { + //对要返回的集合进行初始化,以避免返回nil + nums=make([]uint,0,0) + //bm不存在时直接返回并结束 + if bm == nil { + return nums + } + //分组遍历判断某下标的元素是否存在于位图中,即其值是否为1 + for j := 0; j < len(bm.bits); j++ { + for i := 0; i < 64; i++ { + if bm.bits[j]&(1< 0 { + //该元素存在,添加入结果集合内 + nums = append(nums, uint(j*64+i)) + } + } + } + return nums +} +``` + +#### Clear + +清空初始化位图。 + +```go +func (bm *bitmap) Clear() { + if bm == nil { + return + } + bm.bits = make([]uint64, 0, 0) +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/bitmap" +) +func main() { + var nums []uint + bm:=bitmap.New() + bm.Insert(1) + bm.Insert(2) + bm.Insert(3) + bm.Insert(64) + bm.Insert(128) + bm.Insert(256) + bm.Insert(320) + nums=bm.All() + for i:=0;i< len(nums);i++{ + fmt.Println(nums[i]) + } + bm.Delete(320) + fmt.Println() + nums=bm.All() + for i:=0;i< len(nums);i++{ + fmt.Println(nums[i]) + } + bm.Delete(256) + fmt.Println() + nums=bm.All() + for i:=0;i< len(nums);i++{ + fmt.Println(nums[i]) + } + bm.Delete(128) + fmt.Println() + nums=bm.All() + for i:=0;i< len(nums);i++{ + fmt.Println(nums[i]) + } + bm.Clear() + fmt.Println() + nums=bm.All() + for i:=0;i< len(nums);i++{ + fmt.Println(nums[i]) + } +} + +``` + +#### 结果 + +> 1 +> 2 +> 3 +> 64 +> 128 +> 256 +> 320 +> +> 1 +> 2 +> 3 +> 64 +> 128 +> 256 +> +> 1 +> 2 +> 3 +> 64 +> 128 +> +> 1 +> 2 +> 3 +> 64 diff --git a/blog/数据结构STL——golang实现前缀基数树radix.md b/blog/数据结构STL——golang实现前缀基数树radix.md new file mode 100644 index 0000000..b536572 --- /dev/null +++ b/blog/数据结构STL——golang实现前缀基数树radix.md @@ -0,0 +1,605 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 前缀基数树(Radix),又叫基数树,是前缀树的一种变种。 + +​ 它和前缀树不同的地方在于,它前缀树是将一个string按char进行分段保存,而基数树是将**多个char设为一层**,然后将string进行**分层保存**,一般利用**‘/’**作为分层标识。 + +​ 它可用于string的存储和索引,当加上**模糊匹配**时也可用于实现**动态路由**。 + +### 原理 + +​ 本次实现的前缀基数树的每个结点分别存储一层的string,终结点存储整个的string以作为终结点标识。 + +​ string按以‘/’作为分层标识,在一组‘/’之间的string为一层,若该层string为空忽略不记。 + +​ 同时,每一层的string如果首字符为‘:'则视为动态匹配层,即该层可以匹配任意的string,而当该层首字符为'*'时也是动态匹配,且可匹配后方的所有内容。 + +​ 为了加快查找速度,每一个结点的子结点利用map对子结点对应层的string进行一个映射。 + +​ 对于一个基数树来说,它需要满足的特征有三条: + +- 父结点的前缀必然是子结点的前缀 +- 根节点不包含字符,除根节点以外每个节点包含一串char +- 终结点保存整个string +- 每一层的首字符为‘:'时为动态匹配,即任意的不可分层string皆可 +- 每一层的首字符为‘*'时为动态匹配,可匹配后续所有的string,不论是否可以分蹭 + +#### 添加策略 + +​ 从根节点开始插入,将string按’/‘进行分段,每层插入一个,当插入到最后时结点存储的string存在时则说明之前已经插入过,故插入失败,否则插入成功。 + +​ 当中间结点在原Radix树中不存在时创建即可。 + +​ 若插入失败则需要将原Radix树种不存在的结点删除并从map中删除。 + +#### 删除策略 + +​ 从根节点开始删除,将string按'/'进行分段,逐层往下遍历寻找到最终点,如果此时有存储的元素则删除同时表示删除成功,随后逐层返回将对应结点的num-1即可,当num=0时表示无后续结点,将该结点删除即可。如果在逐层下推的过程中发现结点不存在,可视为删除失败。之间返回即可。 + +#### 匹配策略 + +​ 匹配策略主要用于从radix中进行动态匹配,即从中找到一个存储的可以用于进行模糊匹配的string,然后将其和待匹配的s进行匹配,同时将模糊匹配层的name作为key,将待匹配层的string作为value放入map中并返回。 + +​ 于是要解决问题就变成了如何找到一个可以对当前待匹配的string进行模糊匹配的存储结点的string。 + +- 将待匹配的s进行分层处理 +- 从radix树的根节点开始匹配 + - 匹配时遍历每一个子结点,如果不支持动态匹配则判断同层是否相等,相等则加入可匹配的子结点数组 + - 如果支持动态匹配也加入可匹配的子结点数组 + - 继续匹配所有支持匹配的子结点直到找到第一个满足匹配情况的结点并返回 +- 从找到的结点获取它存储的string然后进行分层 +- 将分层结果中的首字符为’:'或‘*'的层即支持模糊匹配的层与待匹配的string的对应层建立映射关系 +- 返回映射关系和匹配是否成功 + +### 实现 + +​ radix前缀基数树结构体,该实例存储前缀基数树的根节点,同时保存该树已经存储了多少个元素。 + +```go +type radix struct { + root *node //前缀基数树的根节点指针 + size int //当前已存放的元素数量 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是radix的树节点,结点存储到此时的string的前缀数量,son存储其下属分叉的子结点指针,该节点同时存储其元素。 + +```go +type node struct { + pattern string //到终点时不为"",其他都为"" + part string //以当前结点的string内容 + num int //以当前结点为前缀的数量 + sons map[string]*node //该结点下属结点的指针 + fuzzy bool //模糊匹配?该结点首字符为':'或'*'为模糊匹配 +} +``` + +#### 接口 + +```go +type radixer interface { + Iterator() (i *Iterator.Iterator) //返回包含该radix的所有string + Size() (num int) //返回该radix中保存的元素个数 + Clear() //清空该radix + Empty() (b bool) //判断该radix是否为空 + Insert(s string) (b bool) //向radix中插入string + Erase(s string) (b bool) //从radix中删除string + Delete(s string) (num int) //从radix中删除以s为前缀的所有string + Count(s string) (num int) //从radix中寻找以s为前缀的string单词数 + Mate(s string) (m map[string]string, ok bool) //利用radix树中的string对s进行模糊匹配,':'可模糊匹配该层,'*'可模糊匹配后面所有 +} +``` + +#### New + +​ 新建一个radix前缀基数树容器并返回,初始根节点为nil。 + +```go +func New() (r *radix) { + return &radix{ + root: newNode(""), + size: 0, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个前缀基数树节点并返回,将传入的元素e作为该节点的承载元素。 + +```go +func newNode(part string) (n *node) { + fuzzy := false + if len(part) > 0 { + fuzzy = part[0] == ':' || part[0] == '*' + } + return &node{ + pattern: "", + part: part, + num: 0, + sons: make(map[string]*node), + fuzzy: fuzzy, + } +} +``` + +##### analysis + +​ 将string按'/'进行分段解析,为""部分直接舍弃,返回解析结果,同时按规则重组用以解析是string并返回。 + +```go +func analysis(s string) (ss []string, newS string) { + vs := strings.Split(s, "/") + ss = make([]string, 0) + newS = "/" + for _, item := range vs { + if item != "" { + ss = append(ss, item) + newS = newS + "/" + item + if item[0] == '*' { + break + } + } + } + return ss, newS +} +``` + +#### Iterator + +​ 以radix前缀基数树做接收者,将该radix中所有存放的string放入迭代器中并返回。 + +```go +func (r *radix) Iterator() (i *Iterator.Iterator) { + if r == nil { + return nil + } + r.mutex.Lock() + es := r.root.inOrder("") + i = Iterator.New(&es) + r.mutex.Unlock() + return i +} +``` + +​ 以node前缀基数树节点做接收者,遍历其分叉以找到其存储的所有string。 + +```go +func (n *node) inOrder(s string) (es []interface{}) { + if n == nil { + return es + } + if n.pattern != "" { + es = append(es, s+n.part) + } + for _, son := range n.sons { + es = append(es, son.inOrder(s+n.part+"/")...) + } + return es +} +``` + +#### Size + +​ 以radix前缀基数树做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (r *radix) Size() (num int) { + if r == nil { + return 0 + } + if r.root == nil { + return 0 + } + return r.size +} +``` + +#### Clear + +​ 以radix前缀基数树做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (r *radix) Clear() { + if r == nil { + return + } + r.mutex.Lock() + r.root = newNode("") + r.size = 0 + r.mutex.Unlock() +} +``` + +#### Empty + +​ 以radix前缀基数树做接收者,判断该radix是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (r *radix) Empty() (b bool) { + if r == nil { + return true + } + return r.size == 0 +} +``` + +#### Insert + +​ 以radix前缀基数树做接收者,向radix插入string,将对string进行解析,按'/'进行分层,':'为首则为模糊匹配该层,'*'为首则为模糊匹配后面所有,已经存在则无法重复插入。 + +```go +func (r *radix) Insert(s string) (b bool) { + if r == nil { + return false + } + //解析s并按规则重构s + ss, s := analysis(s) + r.mutex.Lock() + if r.root == nil { + //避免根节点为nil + r.root = newNode("") + } + //从根节点开始插入 + b = r.root.insert(s, ss, 0) + if b { + //插入成功,size+1 + r.size++ + } + r.mutex.Unlock() + return b +} +``` + +​ 以node前缀基数树节点做接收者,从n节点中继续插入以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行插入,如果此时node承载了string则插入失败,否则成功,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,当插入失败且对应子结点为新建节点时则需要删除该子结点。 + +```go +func (n *node) insert(pattern string, ss []string, p int) (b bool) { + if p == len(ss) { + if n.pattern != "" { + //该节点承载了string + return false + } + //成功插入 + n.pattern = pattern + n.num++ + return true + } + //找到该层的string + s := ss[p] + //从其子结点的map中找到对应的方向 + son, ok := n.sons[s] + if !ok { + //不存在,新建并放入map中 + son = newNode(s) + n.sons[s] = son + } + //从子结点对应方向继续插入 + b = son.insert(pattern, ss, p+1) + if b { + n.num++ + } else { + if !ok { + //插入失败且该子节点为新建结点则需要删除该子结点 + delete(n.sons, s) + } + } + return b +} +``` + +#### Erase + +​ 以radix前缀基数树做接收者,从radix树中删除元素string。 + +```go +func (r *radix) Erase(s string) (b bool) { + if r.Empty() { + return false + } + if len(s) == 0 { + return false + } + if r.root == nil { + //根节点为nil即无法删除 + return false + } + //解析s并按规则重构s + ss, _ := analysis(s) + r.mutex.Lock() + //从根节点开始删除 + b = r.root.erase(ss, 0) + if b { + //删除成功,size-1 + r.size-- + if r.size == 0 { + //所有string都被删除,根节点置为nil + r.root = nil + } + } + r.mutex.Unlock() + return b +} +``` + +​ 以node前缀基数树节点做接收者,从n节点中继续删除以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行删除,如果此时node未承载元素则删除失败,否则成功,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,若其分叉为nil则直接失败。 + +```go +func (n *node) erase(ss []string, p int) (b bool) { + if p == len(ss) { + if n.pattern != "" { + //该结点承载是string是,删除成功 + n.pattern = "" + n.num-- + return true + } + return false + } + //从map中找到对应下子结点位置并递归进行删除 + s := ss[p] + son, ok := n.sons[s] + if !ok || son == nil { + //未找到或son不存在,删除失败 + return false + } + b = son.erase(ss, p+1) + if b { + n.num-- + if son.num <= 0 { + //删除后子结点的num<=0即该节点无后续存储元素,可以销毁 + delete(n.sons, s) + } + } + return b +} +``` + +#### Delete + +​ 以radix前缀基数树做接收者,从radix树中删除以s为前缀的所有string。 + +```go +func (r *radix) Delete(s string) (num int) { + if r.Empty() { + return 0 + } + if len(s) == 0 { + return 0 + } + if r.root == nil { + return 0 + } + //解析s并按规则重构s + ss, _ := analysis(s) + r.mutex.Lock() + //从根节点开始删除 + num = r.root.delete(ss, 0) + if num > 0 { + //删除成功 + r.size -= num + if r.size <= 0 { + //所有string都被删除,根节点置为nil + r.root = nil + } + } + r.mutex.Unlock() + return num +} +``` + +​ 以node前缀基数树节点做接收者,从n节点中继续删除以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行删除,删除所有后续元素,并返回其后续元素的数量,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,若其分叉为nil则直接返回0。 + +```go +func (n *node) delete(ss []string, p int) (num int) { + if p == len(ss) { + return n.num + } + //从map中找到对应下子结点位置并递归进行删除 + s := ss[p] + son, ok := n.sons[s] + if !ok || son == nil { + return 0 + } + num = son.delete(ss, p+1) + if num > 0 { + son.num -= num + if son.num <= 0 { + //删除后子结点的num<=0即该节点无后续存储元素,可以销毁 + delete(n.sons, s) + } + } + return num +} +``` + +#### Count + +​ 以radix前缀基数树做接收者,从radix中查找以s为前缀的所有string的个数,如果存在以s为前缀的则返回大于0的值即其数量,如果未找到则返回0。 + +```go +func (r *radix) Count(s string) (num int) { + if r.Empty() { + return 0 + } + if r.root == nil { + return 0 + } + if len(s) == 0 { + return 0 + } + //解析s并按规则重构s + ss, _ := analysis(s) + r.mutex.Lock() + num = r.root.count(ss, 0) + r.mutex.Unlock() + return num +} +``` + +​ 以node前缀基数树节点做接收者,从n节点中继续查找以s为前缀索引的元素e,且当前抵达的string位置为p,当到达s终点时返回其值即可,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,当其分叉为nil则直接返回0。 + +```go +func (n *node) count(ss []string, p int) (num int) { + if p == len(ss) { + return n.num + } + //从map中找到对应下子结点位置并递归进行查找 + s := ss[p] + son, ok := n.sons[s] + if !ok || son == nil { + return 0 + } + return son.count(ss, p+1) +} +``` + +#### Mate + +​ 以radix前缀基数树做接收者,从radix中查找以s为信息的第一个可以模糊匹配到的key和value的映射表,key是radix树中的段名,value是s中的段名,如果未找到则返回nil和false,否则返回一个映射表和true。 + +```go +func (r *radix) Mate(s string) (m map[string]string, ok bool) { + if r.Empty() { + return nil, false + } + if len(s) == 0 { + return nil, false + } + if r.root == nil { + return nil, false + } + //将s按'/'进行分割,并去掉第一个即去掉"",随后一次按照分层结果进行查找 + m, ok = r.root.mate(s, 0) + return m, ok +} +``` + +​ 以node前缀基数树节点做接收者,先从radix树的根节点开始找到第一个可以满足该模糊匹配方案的string结点,随后将s和结点的pattern进行模糊映射,将模糊查找的值和匹配值进行映射并返回即可,若该结点未找到则直接返回nil和false即可。 + +```go +func (n *node) mate(s string, p int) (m map[string]string, ok bool) { + //解析url + searchParts, _ := analysis(s) + //从该请求类型中寻找对应的路由结点 + q := n.find(searchParts, 0) + if q != nil { + //解析该结点的pattern + parts, _ := analysis(q.pattern) + //动态参数映射表 + params := make(map[string]string) + for index, part := range parts { + if part[0] == ':' { + //动态匹配,将参数名和参数内容的映射放入映射表内 + params[part[1:]] = searchParts[index] + } + if part[0] == '*' && len(part) > 1 { + //通配符,将后续所有内容全部添加到映射表内同时结束遍历 + params[part[1:]] = strings.Join(searchParts[index:], "/") + break + } + } + return params, true + } + return nil, false +} +``` + +​ 以node前缀基数树节点做接收者,从radix树的根节点开始找到第一个可以满足该模糊匹配方案的string结点,若该结点未找到则直接返回nil。 + +```go +func (n *node) find(parts []string, height int) (q *node) { + //根据长度和局部string的首字符进行判断 + if len(parts) == height || strings.HasPrefix(n.part, "*") { + if n.pattern == "" { + //匹配失败,该结点处无匹配的信息 + return nil + } + //匹配成功,返回该结点 + return n + } + //从该结点的所有子结点中查找可用于递归查找的结点 + //当局部string信息和当前层string相同时可用于递归查找 + //当该子结点是动态匹配时也可以用于递归查找 + part := parts[height] + //从所有子结点中找到可用于递归查找的结点 + children := make([]*node, 0, 0) + for _, child := range n.sons { + if child.part == part || child.fuzzy { + //局部string相同或动态匹配 + children = append(children, child) + } + } + for _, child := range children { + //递归查询,并根据结果进行判断 + result := child.find(parts, height+1) + if result != nil { + //存在一个满足时就可以返回 + return result + } + } + return nil +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/radix" +) + +func main() { + radix := radix.New() + radix.Insert("/test/:name/:key") + radix.Insert("/hlccd/:name/:key") + radix.Insert("/hlccd/1") + radix.Insert("/hlccd/a/*name") + fmt.Println("分层匹配") + m, _ := radix.Mate("/hlccd/test/abc") + for k, v := range m { + fmt.Println(k, v) + } + fmt.Println("匹配全部") + m, _ = radix.Mate("/hlccd/a/abc") + for k, v := range m { + fmt.Println(k, v) + } + fmt.Println("利用迭代器遍历") + for i := radix.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + radix.Erase("/hlccd/a/*name") + fmt.Println("利用迭代器遍历定向删除后的结果") + for i := radix.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + radix.Delete("/hlccd/") + fmt.Println("利用迭代器遍历删除前缀的结果") + for i := radix.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } +} +``` + +> 分层匹配 +> name test +> key abc +> 匹配全部 +> name a +> key abc +> 利用迭代器遍历 +> /test/:name/:key +> /hlccd/:name/:key +> /hlccd/1 +> /hlccd/a/*name +> 利用迭代器遍历定向删除后的结果 +> /test/:name/:key +> /hlccd/:name/:key +> /hlccd/1 +> 利用迭代器遍历删除前缀的结果 +> /test/:name/:key diff --git a/blog/数据结构STL——golang实现双向队列deque.md b/blog/数据结构STL——golang实现双向队列deque.md new file mode 100644 index 0000000..90583ad --- /dev/null +++ b/blog/数据结构STL——golang实现双向队列deque.md @@ -0,0 +1,539 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 双向队列(deque)是一个封装了动态大小数组的顺序容器。它同其他数据结构一样都可以承载任意类型的元素,但相比于队列来说,它不同的点主要在于,队列只能首部出尾部入,而双向队列可以在首部和尾部都进行出入操作。 + +​ 对于deque的实现来说,虽然它也是一个线性容器,但考虑到它将会在首部和尾部同时实现增删操作,如果仍然使用动态数组的形式去实现,虽然也可以做出来,但在空间扩容缩容的时候,将会进行大量的数据复制的过程,而这一过程其实也是可以省去的。方法就是**使用链表的形式对它进行存储**,这样对新元素的增删就可以不需要考虑中间仍然存在的元素的复制过程,从而免除复制导致的时间浪费。 + +### 原理 + +​ 对于deque来说,考虑到完全使用链表的形式:即一个结点承载一个元素,除了需要较多的空间分配操作的时间开销外,还需要更多的指针去记录该结点的前后结点的位置,即:既要更多的空间分配导致的时间开销,也有记录前后节点指针的空间开销。对于纯粹的链表实现来说,也并不是特别良好的策略。 + +​ 于是,可以考虑将**链表和数组两者结合起来**,不采用动态数组的形式,而是将一个固定容量的数组放入一个链表结点中,这样即利用了链表可以减少复制数据从而减少时间开销的优势,也可以减少记录太多指针导致的空间浪费,同时由于一次分配固定量的空间大小,也可以留作一定的冗余量,更进一步的减少分配空间导致的时间开销。 + +​ 因此,它的扩容和缩容策略相对于队列容器来说有着较大的不同: + +#### 添加策略 + +​ 考虑到上文所说,deque的扩容方案其实就只有一种了,即新增链表的首节点,将之前结点不能承载的元素放入新结点中,同时将新结点和链表建立连接。由新的结点称为首节点/尾结点。 + +​ 对于deque添加元素的过程中,只会出现以下三种情况: + +1. deque中无结点:新增一个结点同时作为首尾结点即可,并将元素放入其中,具体放入首部还是尾部根据添加方向来确定; +2. 首/尾结点无空间容纳:新增一个结点,将新结点设为首/尾结点,并将元素放弃新结点中 +3. 首/尾结点仍有空间存放:直接放入即可 + +#### 删除策略 + +​ 同上文一样,由于采用链表结合固定数组的形式进行存储,所以它的添加和删除策略是十分类似的,即在删除的时候只需要删除一个结点即可,同时需要将**被删除的结点和链表之间的联系全部断开**,这样才能让被删除的结点被回收。 + +​ 对于deque删除结点的过程中,只会出现以下四种情况: + +1. 链表不存在:无需删除操作 +2. 对应结点仍有承载元素:将结点begin/end进行移动即可 +3. 对应节点无承载元素了:释放该结点,同时解除该结点与链表的连接 +4. 所有元素都被删除完成:销毁链表 + +### 实现 + +**注:整体实现过程中将deque和链表的node进行了分离** + +​ deque双向队列结构体,包含链表的头尾节点指针,当删除节点时通过头尾节点指针进入链表进行删除,当一个节点全部被删除后则释放该节点,同时首尾节点做相应调整,当添加节点时若未占满节点空间时移动下标并做覆盖即可,当添加节点时空间已使用完毕时,根据添加位置新建一个新节点补充上去。 + +```go +type deque struct { + first *node //链表首节点指针 + last *node //链表尾节点指针 + size uint64 //当前存储的元素个数 + mutex sync.Mutex //并发控制锁 +} +``` + +​ deque双向队列中链表的node节点结构体,包含一个2^10空间的固定数组用以承载元素,使用begin和end两个下标用以表示新增的元素的下标,由于begin可能出现-1所以不选用uint16,pre和next是该节点的前后两个节点,用以保证链表整体是相连的。 + +```go +type node struct { + data [1024]interface{} //用于承载元素的股东数组 + begin int16 //该结点在前方添加结点的下标 + end int16 //该结点在后方添加结点的下标 + pre *node //该结点的前一个结点 + next *node //该节点的后一个结点 +} +``` + +#### 接口 + +```go +type dequer interface { + Iterator() (i *Iterator.Iterator) //返回包含双向队列中所有元素的迭代器 + Size() (size uint64) //返回该双向队列中元素的使用空间大小 + Clear() //清空该双向队列 + Empty() (b bool) //判断该双向队列是否为空 + PushFront(e interface{}) //将元素e添加到该双向队列的首部 + PushBack(e interface{}) //将元素e添加到该双向队列的尾部 + PopFront() //将该双向队列首元素弹出 + PopBack() //将该双向队列首元素弹出 + Front() (e interface{}) //获取该双向队列首部元素 + Back() (e interface{}) //获取该双向队列尾部元素 +} +``` + +```go +type noder interface { + nextNode() (m *node) //返回下一个结点 + preNode() (m *node) //返回上一个结点 + value() (es []interface{}) //返回该结点所承载的所有元素 + pushFront(e interface{}) (first *node) //在该结点头部添加一个元素,并返回新首结点 + pushBack(e interface{}) (last *node) //在该结点尾部添加一个元素,并返回新尾结点 + popFront() (first *node) //弹出首元素并返回首结点 + popBack() (last *node) //弹出尾元素并返回尾结点 + front() (e interface{}) //返回首元素 + back() (e interface{}) //返回尾元素 +} +``` + +#### New + +​ 创建一个deque容器并初始化,同时返回其指针。 + +```go +func New() (q *queue) { + return &queue{ + data: make([]interface{}, 1, 1), + begin: 0, + end: 0, + cap: 1, + mutex: sync.Mutex{}, + } +} +``` + +​ 创建一个首结点并初始化,同时返回其指针。 + +```go +func createFirst() (n *node) { + return &node{ + data: [1024]interface{}{}, + begin: 1023, + end: 1024, + pre: nil, + next: nil, + } +} +``` + +​ 创建一个尾结点并初始化,同时返回其指针。 + +```go +func createLast() (n *node) { + return &node{ + data: [1024]interface{}{}, + begin: -1, + end: 0, + pre: nil, + next: nil, + } +} +``` + +#### Iterator + +​ 将双向队列中承载元素传入迭代器中,通过遍历链表中所有结点进行获取,不清除双向队列中的冗余量。返回迭代器指针,用于遍历双向队列。 + +```go +func (d *deque) Iterator() (i *Iterator.Iterator) { + if d == nil { + d = New() + } + tmp := make([]interface{}, 0, d.size) + //遍历链表的所有节点,将其中承载的元素全部复制出来 + for m := d.first; m != nil; m = m.nextNode() { + tmp = append(tmp, m.value()...) + } + return Iterator.New(&tmp) +} +``` + +​ 返回当前结点的下一个结点。 + +```go +func (n *node) nextNode() (m *node) { + if n == nil { + return nil + } + return n.next +} +``` + +​ 返回当前结点的上一个结点。 + +```go +func (n *node) preNode() (m *node) { + if n == nil { + return nil + } + return n.pre +} +``` + +​ 返回当前结点所承载的所有元素。 + +```go +func (n *node) value() (es []interface{}) { + es = make([]interface{}, 0, 0) + if n == nil { + return es + } + if n.begin > n.end { + return es + } + es = n.data[n.begin+1 : n.end] + return es +} +``` + +#### Size + +​ 返回deque中当前所包含的元素个数,由于其数值必然是非负整数,所以选用了**uint64**。 + +```go +func (d *deque) Size() (size uint64) { + if d == nil { + d = New() + } + return d.size +} +``` + +#### Clear + +​ 清空了deque中所承载的所有元素,同时销毁链表的首尾结点以实现销毁链表。 + +```go +func (d *deque) Clear() { + if d == nil { + d = New() + return + } + d.mutex.Lock() + d.first = nil + d.last = nil + d.size = 0 + d.mutex.Unlock() +} +``` + +#### Empty + +​ 判断deque中是否为空,通过Size()是否为0进行判断。 + +```go +func (d *deque) Empty() (b bool) { + if d == nil { + d = New() + } + return d.Size() == 0 +} +``` + +#### PushFront + +​ 以deque为接收者,向其首部添加一个元素。 + +​ 通过链表的首结点进行实现,当链表不存在时则创建一个首结点并以此作为链表首尾结点。 + +```go +func (d *deque) PushFront(e interface{}) { + if d == nil { + d = New() + } + d.mutex.Lock() + d.size++ + //通过首节点进行添加 + if d.first == nil { + d.first = createFirst() + d.last = d.first + } + d.first = d.first.pushFront(e) + d.mutex.Unlock() +} +``` + +​ 以node结点做接收者,向该节点前方添加元素e,当该结点空间已经使用完毕后,新建一个结点并将新结点设为首结点,将插入元素放入新结点并返回新结点作为新的首结点,否则插入当前结点并返回当前结点,首结点不变。 + +```go +func (n *node) pushFront(e interface{}) (first *node) { + if n == nil { + return n + } + if n.begin >= 0 { + //该结点仍有空间可用于承载元素 + n.data[n.begin] = e + n.begin-- + return n + } + //该结点无空间承载,创建新的首结点用于存放 + m := createFirst() + m.data[m.begin] = e + m.next = n + n.pre = m + m.begin-- + return m +} +``` + +#### PushBack + +​ 以deque为接收者,向其尾部添加一个元素。 + +​ 通过链表的尾结点进行实现,当链表不存在时则创建一个尾结点并以此作为链表首尾结点。 + +```go +func (d *deque) PushBack(e interface{}) { + if d == nil { + d = New() + } + d.mutex.Lock() + d.size++ + //通过尾节点进行添加 + if d.last == nil { + d.last = createLast() + d.first = d.last + } + d.last = d.last.pushBack(e) + d.mutex.Unlock() +} +``` + +​ 以node结点做接收者,向该节点后方添加元素e,当该结点空间已经使用完毕后,新建一个结点并将新结点设为尾结点,将插入元素放入新结点并返回新结点作为新的尾结点,否则插入当前结点并返回当前结点,尾结点不变。 + +```go +func (n *node) pushBack(e interface{}) (last *node) { + if n == nil { + return n + } + if n.end < int16(len(n.data)) { + //该结点仍有空间可用于承载元素 + n.data[n.end] = e + n.end++ + return n + } + //该结点无空间承载,创建新的尾结点用于存放 + m := createLast() + m.data[m.end] = e + m.pre = n + n.next = m + m.end++ + return m +} +``` + +#### PopFront + +​ 以deque双向队列容器做接收者,利用首节点进行弹出元素,可能存在首节点全部释放要进行首节点后移的情况,当元素全部删除后,释放全部空间,将首尾节点都设为nil。 + +```go +func (d *deque) PopFront() { + if d == nil { + d = New() + } + if d.size == 0 { + return + } + d.mutex.Lock() + //利用首节点删除首元素 + //返回新的首节点 + d.first = d.first.popFront() + d.size-- + if d.size == 0 { + //全部删除完成,释放空间,并将首尾节点设为nil + d.first = nil + d.last = nil + } + d.mutex.Unlock() +} +``` + +​ 以node结点做接收者,利用首节点进行弹出元素,可能存在首节点全部释放要进行首节点后移的情况, 当发生首结点后移后将会返回新首结点,否则返回当前结点。 + +```go +func (n *node) popFront() (first *node) { + if n == nil { + return nil + } + if n.begin < int16(len(n.data))-2 { + //该结点仍有承载元素 + n.begin++ + n.data[n.begin] = nil + return n + } + if n.next != nil { + //清除该结点下一节点的前结点指针 + n.next.pre = nil + } + return n.next +} +``` + +#### PopBack + +​ 以deque双向队列容器做接收者,利用尾节点进行弹出元素,可能存在尾节点全部释放要进行尾节点前移的情况,当元素全部删除后,释放全部空间,将首尾节点都设为nil。 + +```go +func (d *deque) PopBack() { + if d == nil { + d = New() + } + if d.size == 0 { + return + } + d.mutex.Lock() + //利用尾节点删除首元素 + //返回新的尾节点 + d.last = d.last.popBack() + d.size-- + if d.size == 0 { + //全部删除完成,释放空间,并将首尾节点设为nil + d.first = nil + d.last = nil + } + d.mutex.Unlock() +} +``` + +​ 以node结点做接收者,利用尾节点进行弹出元素,可能存在尾节点全部释放要进行尾节点前移的情况,当发生尾结点前移后将会返回新尾结点,否则返回当前结点。 + +```go +func (n *node) popBack() (last *node) { + if n == nil { + return nil + } + if n.end > 1 { + //该结点仍有承载元素 + n.end-- + n.data[n.end] = nil + return n + } + if n.pre != nil { + //清除该结点上一节点的后结点指针 + n.pre.next = nil + } + return n.pre +} +``` + +#### Front + +​ 以deque双向队列容器做接收者,返回该容器的第一个元素,利用首节点进行寻找,若该容器当前为空,则返回nil。 + +```go +func (d *deque) Front() (e interface{}) { + if d == nil { + d = New() + } + return d.first.front() +} +``` + +​ 以node结点做接收者,返回该结点的第一个元素,利用首节点和begin进行查找,若该结点为nil,则返回nil。 + +```go +func (n *node) front() (e interface{}) { + if n == nil { + return nil + } + return n.data[n.begin+1] +} +``` + +#### Back + +​ 以deque双向队列容器做接收者,返回该容器的最后一个元素,利用尾节点进行寻找,若该容器当前为空,则返回nil。 + +```go +func (d *deque) Back() (e interface{}) { + if d == nil { + d = New() + } + return d.last.back() +} +``` + +​ 以node结点做接收者,返回该结点的最后一个元素,利用尾节点和end进行查找,若该结点为nil,则返回nil。 + +```go +func (n *node) back() (e interface{}) { + if n == nil { + return nil + } + return n.data[n.end-1] +} +``` + +#### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/deque" + "sync" +) + +func main() { + d:=deque.New() + wg:=sync.WaitGroup{} + for i := 0; i >=-4; i-- { + wg.Add(1) + go func(num int) { + d.PushFront(num) + wg.Done() + }(i) + } + for i := 0; i <= 4; i++ { + wg.Add(1) + go func(num int) { + d.PushBack(num) + wg.Done() + }(i) + } + wg.Wait() + fmt.Println("使用迭代器遍历:") + for i:=d.Iterator();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + len:=d.Size() + for i := uint64(0); i < len; i += 2 { + fmt.Println("当前首元素:",d.Front()) + fmt.Println("当前尾元素:",d.Back()) + d.PopFront() + d.PopBack() + } +} +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 使用迭代器遍历: +> 0 +> -1 +> -4 +> -3 +> -2 +> 4 +> 0 +> 1 +> 2 +> 3 +> 当前首元素: 0 +> 当前尾元素: 3 +> 当前首元素: -1 +> 当前尾元素: 2 +> 当前首元素: -4 +> 当前尾元素: 1 +> 当前首元素: -3 +> 当前尾元素: 0 +> 当前首元素: -2 +> 当前尾元素: 4 diff --git a/blog/数据结构STL——golang实现向量vector.md b/blog/数据结构STL——golang实现向量vector.md new file mode 100644 index 0000000..0b3d586 --- /dev/null +++ b/blog/数据结构STL——golang实现向量vector.md @@ -0,0 +1,571 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 向量(Vector)是一个封装了动态大小数组的顺序容器。跟任意其它类型容器一样,它能够存放**各种类型的对象**。可以简单的认为,向量是一个能够存放任意类型的动态数组。 + +​ 对于vector的实现,考虑到它是一个线性容器,其中的元素可以通过下标在O(1)的时间复杂度直接获取访问,可以考虑**底层使用数组**的形式实现。由于vector可以动态的添加和删除元素,所以需要对数组进行动态分配以满足需求。 + +​ 对于golang语言来说,官方并未设计泛型,但由于interface可以充当任意类型的特性,可以使用interface类型作为泛型类型。 + +### 原理 + +​ 对于一个vector来说,它是可以动态的对元素进行添加和修改的,而如果说每一次增删元素都使得数组恰好可以容纳所有元素的话,那它在以后的每一次增删的过程中都需要去**重新分配空间**,并将原有的元素复制到新数组中,如果按照这样来操作的话,每次删减都会需要花费大量的时间去进行,将会极大的降低其性能。 + +​ 为了提高效率,可以选择牺牲一定的空间作为冗余量,即**时空权衡**,通过每次扩容时多分配一定的空间,这样在后续添加时就可以减少扩容次数,而在删除元素时,如果冗余量不多时,可以选择不进行空间分配,仅将后续元素前移一位,同理,在中间插入元素也仅仅将其后移一位,这样可以极大减少的由于容量不足或超出导致的扩容缩容造成的时间开销。 + +#### 扩容策略 + +​ 对于动态数组扩容来说,由于其添加是线性且连续的,即每一次只会增加一个,不像bitmap一样可能需要一次添加很多的空间才能保证在其范围内,所以它的扩容策略相对于bitmap有所不同。 + +​ vector的扩容主要由两种方案: + +1. 固定扩容:固定的去增加一定量的空间,该方案时候在数组较大时添加空间,可以避免直接翻倍导致的**冗余过量**问题,到由于增加量是固定的,如果需要一次扩容很多量的话就会比较缓慢。 +2. 翻倍扩容:将原有容量直接翻倍,该方案适合在数组不太大的适合添加空间,可以提高扩容量,增加扩容效率,但数组太大时使用的话会导致一次性增加太多空间,进而造成空间的浪费。 + +#### 缩容策略 + +​ 对于动态数组的缩容来说,同扩容类似,由于元素的减少只会是线性且连续的,即每一次只会减少一个,不会存在由于一个元素被删去导致其他已使用的空间也需要被释放。所以vector的缩容方案和vector的扩容方案十分类似,即是它的逆过程: + +1. 固定缩容:释放一个固定的空间,该方案适合在当前数组较大的时候进行,可以减缓需要缩小的量,当空间较大的时候如果采取折半缩减的话;将可能在绝大多数时间内都不会被采用,从而造成空间浪费。 +2. 折半缩容:将当前容量进行折半,该方案适合数组不太大的适合进行缩容,可以更快的缩小容量,但对于较大的空间来说并不会频繁使用到。 + +### 实现 + +​ 由于vector底层由动态数组实现,所以其必然包含一个数组指针,同时,利用时空权衡去减少时间开销导致实际使用空间和实际占用空间不一致,所以分别利用len和cap进行存储。同时,为了解决在高并发情况下的数据不一致问题,引入了并发控制锁。 + +```go +type vector struct { + data []interface{} //动态数组 + len uint64 //当前已用数量 + cap uint64 //可容纳元素数量 + mutex sync.Mutex //并发控制锁 +} +``` + +#### 接口 + +```go +type vectorer interface { + Iterator() (i *Iterator.Iterator) //返回一个包含vector所有元素的迭代器 + Sort(Cmp ...comparator.Comparator) //利用比较器对其进行排序 + Size() (num uint64) //返回vector的长度 + Clear() //清空vector + Empty() (b bool) //返回vector是否为空,为空则返回true反之返回false + PushBack(e interface{}) //向vector末尾插入一个元素 + PopBack() //弹出vector末尾元素 + Insert(idx uint64, e interface{}) //向vector第idx的位置插入元素e,同时idx后的其他元素向后退一位 + Erase(idx uint64) //删除vector的第idx个元素 + Reverse() //逆转vector中的数据顺序 + At(idx uint64) (e interface{}) //返回vector的第idx的元素 + Front() (e interface{}) //返回vector的第一个元素 + Back() (e interface{}) //返回vector的最后一个元素 +} +``` + +#### New + +​ 创建一个vector容器并初始化,同时返回其指针。 + +```go +func New() (v *vector) { + return &vector{ + data: make([]interface{}, 1, 1), + len: 0, + cap: 1, + mutex: sync.Mutex{}, + } +} +``` + +#### Iterator + +​ 以vector为接受器,返回一个包含vector中所有元素的迭代器,同时将vector中暂未使用的空间进行释放。 + +```go +func (v *vector) Iterator() (i *Iterator.Iterator) { + if v == nil { + v = New() + } + v.mutex.Lock() + if v.data == nil { + //data不存在,新建一个 + v.data = make([]interface{}, 1, 1) + v.len = 0 + v.cap = 1 + } else if v.len < v.cap { + //释放未使用的空间 + tmp := make([]interface{}, v.len, v.len) + copy(tmp, v.data) + v.data = tmp + } + //创建迭代器 + i = Iterator.New(&v.data) + v.mutex.Unlock() + return i +} +``` + +#### Sort + +​ 以vector为接受器,重载了比较器中的Sort,实现了让vector利用比较器中的Sort进行排序,使用过程中将会释放vector中暂未使用的空间。 + +```go +func (v *vector) Sort(Cmp ...comparator.Comparator) { + if v == nil { + v = New() + } + v.mutex.Lock() + if v.data == nil { + //data不存在,新建一个 + v.data = make([]interface{}, 1, 1) + v.len = 0 + v.cap = 1 + } else if v.len < v.cap { + //释放未使用空间 + tmp := make([]interface{}, v.len, v.len) + copy(tmp, v.data) + v.data = tmp + v.cap = v.len + } + //调用比较器的Sort进行排序 + if len(Cmp) == 0 { + comparator.Sort(&v.data) + } else { + comparator.Sort(&v.data, Cmp[0]) + } + v.mutex.Unlock() +} +``` + +#### Size + +​ 返回vector中当前所包含的元素个数,由于其数值必然是非负整数,所以选用了**uint64**。 + +```go +func (v *vector) Size() (num uint64) { + if v == nil { + v = New() + } + return v.len +} +``` + +#### Clear + +​ 清空了vector中所承载的所有元素。 + +```go +func (v *vector) Clear() { + if v == nil { + v = New() + } + v.mutex.Lock() + //清空data + v.data = make([]interface{}, 1, 1) + v.len = 0 + v.cap = 1 + v.mutex.Unlock() +} +``` + +#### Empty + +​ 判断vector中是否为空,通过Size()是否为0进行判断。 + +```go +func (v *vector) Empty() (b bool) { + if v == nil { + v = New() + } + return v.Size() <= 0 +} +``` + +#### PushBack + +​ 以vector为接受器,向vector尾部添加一个元素,添加元素会出现两种情况,第一种是还有冗余量,此时**直接覆盖**以len为下标指向的位置即可,另一种情况是没有冗余量了,需要对动态数组进行**扩容**,此时就需要利用扩容策略。 + +​ 扩容策略上文已做描述,可返回参考,该实现过程种将两者进行了结合使用,可参考下方注释。 + +​ 固定扩容值设为2^16,翻倍扩容上限也为2^16。 + +```go +func (v *vector) PushBack(e interface{}) { + if v == nil { + v = New() + } + v.mutex.Lock() + if v.len < v.cap { + //还有冗余,直接添加 + v.data[v.len] = e + } else { + //冗余不足,需要扩容 + if v.cap <= 65536 { + //容量翻倍 + if v.cap == 0 { + v.cap = 1 + } + v.cap *= 2 + } else { + //容量增加2^16 + v.cap += 65536 + } + //复制扩容前的元素 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + v.data[v.len] = e + } + v.len++ + v.mutex.Unlock() +} +``` + +#### PopBack + +​ 以vector向量容器做接收者,弹出容器最后一个元素,同时长度--即可,若容器为空,则不进行弹出,当弹出元素后,可能进行缩容,当容量和实际使用差值超过2^16时,容量直接减去2^16,否则,当实际使用长度是容量的一半时,进行折半缩容。其缩容策略可参考前方解释。 + +```go +func (v *vector) PopBack() { + if v == nil { + v = New() + } + if v.Empty() { + return + } + v.mutex.Lock() + v.len-- + if v.cap-v.len >= 65536 { + //容量和实际使用差值超过2^16时,容量直接减去2^16 + v.cap -= 65536 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + } else if v.len*2 < v.cap { + //实际使用长度是容量的一半时,进行折半缩容 + v.cap /= 2 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + } + v.mutex.Unlock() +} +``` + +#### Insert + +​ 向vector中任意位置插入一个元素,由于其位置必然是非负整数,所以选用了**uint64**进行表示,同时,当插入位置**超出了数组范围**时,将在最近的位置进行插入,由于不可能是负数,所以超出范围的情况只有可能是插入下标大于len,此时只需要插入尾部即可,等同于PushBack的操作。 + +​ 对于插入的下标在范围中时,只需要将后续的一些元素后移一位即可。 + +​ 是否需要扩容需要在开始进行判断,扩容策略同上。 + +```go +func (v *vector) Insert(idx uint64, e interface{}) { + if v == nil { + v = New() + } + v.mutex.Lock() + if v.len >= v.cap { + //冗余不足,进行扩容 + if v.cap <= 65536 { + //容量翻倍 + if v.cap == 0 { + v.cap = 1 + } + v.cap *= 2 + } else { + //容量增加2^16 + v.cap += 65536 + } + //复制扩容前的元素 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + } + //从后往前复制,即将idx后的全部后移一位即可 + var p uint64 + for p = v.len; p > 0 && p > uint64(idx); p-- { + v.data[p] = v.data[p-1] + } + v.data[p] = e + v.len++ + v.mutex.Unlock() +} +``` + +#### Erase + +​ 删除下标为idx的元素,idx不在数组范围内时,则删除距离其最近的元素,即下标不大于0删除首部,不小于len删除尾部。 + +​ 删除后进行缩容判断,缩容操作同上方介绍的缩容策略。 + +```go +func (v *vector) Erase(idx uint64) { + if v == nil { + v = New() + } + if v.Empty() { + return + } + v.mutex.Lock() + for p := idx; p < v.len-1; p++ { + v.data[p] = v.data[p+1] + } + v.len-- + if v.cap-v.len >= 65536 { + //容量和实际使用差值超过2^16时,容量直接减去2^16 + v.cap -= 65536 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + } else if v.len*2 < v.cap { + //实际使用长度是容量的一半时,进行折半缩容 + v.cap /= 2 + tmp := make([]interface{}, v.cap, v.cap) + copy(tmp, v.data) + v.data = tmp + } + v.mutex.Unlock() +} +``` + +#### Reverse + +​ 以vector为接受器,将vector中所承载的元素**逆转**,同时,释放掉所有未使用的空间。 + +```go +func (v *vector) Reverse() { + if v == nil { + v = New() + } + v.mutex.Lock() + if v.data == nil { + //data不存在,新建一个 + v.data = make([]interface{}, 1, 1) + v.len = 0 + v.cap = 1 + } else if v.len < v.cap { + //释放未使用的空间 + tmp := make([]interface{}, v.len, v.len) + copy(tmp, v.data) + v.data = tmp + v.cap=v.len + } + for i := uint64(0); i < v.len/2; i++ { + v.data[i], v.data[v.len-i-1] = v.data[v.len-i-1], v.data[i] + } + v.mutex.Unlock() +} +``` + +#### At + +​ 返回下标为idx的元素,当idx不在数组范围内时返回nil,当idx在范围内时**返回对应元素**。 + +```go +func (v *vector) At(idx uint64) (e interface{}) { + if v == nil { + v=New() + return nil + } + v.mutex.Lock() + if idx < 0 && idx >= v.Size() { + v.mutex.Unlock() + return nil + } + if v.Size() > 0 { + e = v.data[idx] + v.mutex.Unlock() + return e + } + v.mutex.Unlock() + return nil +} +``` + +#### Front + +​ 以vector为接受器,返回vector所承载的元素中位于**首部**的元素,如果vector为nil或者元素数组为nil或为空,则返回nil。 + +```go +func (v *vector) Front() (e interface{}) { + if v == nil { + v=New() + return nil + } + v.mutex.Lock() + if v.Size() > 0 { + e = v.data[0] + v.mutex.Unlock() + return e + } + v.mutex.Unlock() + return nil +} +``` + +#### Back + +​ 以vector为接受器,返回vector所承载的元素中位于**尾部**的元素,如果vector为nil或者元素数组为nil或为空,则返回nil。 + +```go +func (v *vector) Back() (e interface{}) { + if v == nil { + v=New() + return nil + } + v.mutex.Lock() + if v.Size() > 0 { + e = v.data[v.len-1] + v.mutex.Unlock() + return e + } + v.mutex.Unlock() + return nil +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/vector" + "sync" +) + +func main() { + v:=vector.New() + wg:=sync.WaitGroup{} + for i:=0;i<10;i++{ + wg.Add(1) + go func(num int) { + v.PushBack(num) + v.Insert(uint64(num),num) + wg.Done() + }(i) + } + wg.Wait() + fmt.Println("随机插入后的结果:") + for i := v.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + v.Sort() + fmt.Println("排序后的结果:") + for i := v.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("随机删除一半的结果:") + for i:=0;i<5;i++{ + wg.Add(1) + go func(num int) { + v.PopBack() + v.Erase(uint64(i)) + wg.Wait() + }(i) + } + wg.Done() + for i:=uint64(0); i 随机插入后的结果: +> 0 +> 1 +> 0 +> 2 +> 3 +> 5 +> 6 +> 7 +> 4 +> 8 +> 1 +> 9 +> 2 +> 3 +> 4 +> 5 +> 6 +> 9 +> 8 +> 7 +> 排序后的结果: +> 0 +> 0 +> 1 +> 1 +> 2 +> 2 +> 3 +> 3 +> 4 +> 4 +> 5 +> 5 +> 6 +> 6 +> 7 +> 7 +> 8 +> 8 +> 9 +> 9 +> 随机删除一半的结果: +> 0 +> 0 +> 1 +> 3 +> 3 +> 5 +> 5 +> 6 +> 6 +> 7 +> 逆转后的结果: +> 7 +> 6 +> 6 +> 5 +> 5 +> 3 +> 3 +> 1 +> 0 +> 0 +> 在下标5处插入元素-1的结果: +> 7 +> 6 +> 6 +> 5 +> 5 +> -1 +> 3 +> 3 +> 1 +> 0 +> 0 +> 首元素: 7 +> 尾元素: 0 + diff --git a/blog/数据结构STL——golang实现哈希映射hashMap.md b/blog/数据结构STL——golang实现哈希映射hashMap.md new file mode 100644 index 0000000..a4ce9d5 --- /dev/null +++ b/blog/数据结构STL——golang实现哈希映射hashMap.md @@ -0,0 +1,532 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 哈希映射(hash map),它是一个两层结构,即第一层以动态数组作为桶去存储元素,第二层存储hash值冲突的元素。 + +​ 对于插入其中的任意一个元素来说,都可以计算其key的hash值然后将其映射到桶内对应位置,随后再插入即可。 + +​ hash映射最大的特点在于其查找、插入和删除都是O(1)的,但可能存在扩容和缩容的问题,此时其时间开销会增加。 + +### 原理 + +​ 对于哈希映射来说,它需要做的主要是对key进行hash计算,将计算值映射到hash桶的对应位置,插入其中。 + +​ 本次实现中,动态数组即hash桶复用之前实现过的vector,而当出现hash冲突时候以avl树存储存在冲突的值。 + +​ 对于其桶的扩容和缩容来说,都可以依靠vector来完成,扩容缩容后重新将所有key-value插入即可。 + +​ 当然,对于hash映射来说,最优的情况就是所有hash都不会冲突,即avl树仅有根节点,这样对于插入和删除都会更快一些,所以就需要尽可能的避免出现hash冲突的情况,即设计一个良好的hash函数。 + +​ 一般来说,hash函数可以采用**平方折半法、逐位累加法**,考虑到数值类的遍历在诸位累计会消耗大量时间,故对于数值类型采用平方折半法,对于string类型采用逐位累加法。 + +​ vector的初始容量和最小容量为**16**,该值为经验值,设为16的时候hash冲突情况最小。 + +​ 而对于扩容和缩容来说,需要一个**扩容因子**,即当前存储容量达到总容量的扩容因子的倍数时候需要扩容,反之当当前存储容量小于缩容后的扩容因子的倍数时则需要缩容。对于扩容因子来说,若设置的过小,虽然冲突的概率降低,但空间利用率也急剧下降,而当设置过大时,空间利用率提高了,但hash冲突的情况增多,需要的时间成本提高。本次实现中将扩容因子设为**0.75**。该值是根据**泊松分布和指数分布**计算出的出现冲突的可能性最小的最优解。 + +### 实现 + +​ hashMap哈希映射结构体,该实例存储第一层vector的指针,同时保存hash函数,哈希映射中的hash函数在创建时传入,若不传入则在插入首个key-value时从默认hash函数中寻找。 + +```go +type hashMap struct { + arr *vector.Vector //第一层的vector + hash algorithm.Hasher //hash函数 + size uint64 //当前存储数量 + cap uint64 //vector的容量 + mutex sync.Mutex //并发控制锁 +} +``` + +​ 索引结构体,存储key-value结构。 + +```go +type node struct { + value interface{} //节点中存储的元素 + num int //该元素数量 + depth int //该节点的深度 + left *node //左节点指针 + right *node //右节点指针 +} +``` + +#### 接口 + +```go +type hashMaper interface { + Iterator() (i *Iterator.Iterator) //返回一个包含hashMap容器中所有value的迭代器 + Size() (num uint64) //返回hashMap已存储的元素数量 + Cap() (num uint64) //返回hashMap中的存放空间的容量 + Clear() //清空hashMap + Empty() (b bool) //返回hashMap是否为空 + Insert(key, value interface{}) (b bool) //向hashMap插入以key为索引的value,若存在会覆盖 + Erase(key interface{}) (b bool) //删除hashMap中以key为索引的value + GetKeys() (keys []interface{}) //返回hashMap中所有的keys + Get(key interface{}) (value interface{}) //以key为索引寻找vlue +} +``` + +#### New + +​ 新建一个hashMap哈希映射容器并返回,初始vector长度为16,若有传入的hash函数,则将传入的第一个hash函数设为该hash映射的hash函数。 + +```go +func New(hash ...algorithm.Hasher) (hm *hashMap) { + var h algorithm.Hasher + if len(hash) == 0 { + h = nil + } else { + h = hash[0] + } + cmp := func(a, b interface{}) int { + ka, kb := a.(*indexes), b.(*indexes) + return comparator.GetCmp(ka.key)(ka.key, kb.key) + } + //新建vector并将其扩容到16 + v := vector.New() + for i := 0; i < 16; i++ { + //vector中嵌套avl树 + v.PushBack(avlTree.New(false, cmp)) + } + return &hashMap{ + arr: v, + hash: h, + size: 0, + cap: 16, + mutex: sync.Mutex{}, + } +} +``` + +#### Iterator + +​ 以hashMap哈希映射做接收者,将该hashMap中所有保存的索引指针的value放入迭代器中。 + +```go +func (hm *hashMap) Iterator() (i *Iterator.Iterator) { + if hm == nil { + return nil + } + if hm.arr == nil { + return nil + } + hm.mutex.Lock() + //取出hashMap中存放的所有value + values := make([]interface{}, 0, 1) + for i := uint64(0); i < hm.arr.Size(); i++ { + avl := hm.arr.At(i).(*avlTree.AvlTree) + ite := avl.Iterator() + es := make([]interface{}, 0, 1) + for j := ite.Begin(); j.HasNext(); j.Next() { + idx := j.Value().(*indexes) + es = append(es, idx.value) + } + values = append(values, es...) + } + //将所有value放入迭代器中 + i = Iterator.New(&values) + hm.mutex.Unlock() + return i +} +``` + +#### Size + +​ 以hashMap哈希映射做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (hm *hashMap) Size() (num uint64) { + if hm == nil { + return 0 + } + return hm.size +} +``` + +#### Cap + +​ 以hashMap哈希映射做接收者,返回该容器当前容量,如果容器为nil返回0。 + +```go +func (hm *hashMap) Cap() (num uint64) { + if hm == nil { + return 0 + } + return hm.cap +} +``` + +#### Clear + +​ 以hashMap哈希映射做接收者,将该容器中所承载的元素清空,将该容器的size置0,容量置为16,vector重建并扩容到16。 + +```go +func (hm *hashMap) Clear() { + if hm == nil { + return + } + hm.mutex.Lock() + //重建vector并扩容到16 + v := vector.New() + cmp := func(a, b interface{}) int { + ka, kb := a.(*indexes), b.(*indexes) + return comparator.GetCmp(ka.key)(ka.key, kb.key) + } + for i := 0; i < 16; i++ { + v.PushBack(avlTree.New(false, cmp)) + } + hm.arr = v + hm.size = 0 + hm.cap = 16 + hm.mutex.Unlock() +} +``` + +#### Empty + +​ 以hashMap哈希映射做接收者,判断该哈希映射中是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (hm *hashMap) Empty() (b bool) { + if hm == nil { + return false + } + return hm.size > 0 +} +``` + +#### Insert + +​ 以hashMap哈希映射做接收者,向哈希映射入元素e,若已存在相同key则进行覆盖,覆盖仍然视为插入成功,若不存在相同key则直接插入即可。 + +```go +func (hm *hashMap) Insert(key, value interface{}) (b bool) { + if hm == nil { + return false + } + if hm.arr == nil { + return false + } + if hm.hash == nil { + hm.hash = algorithm.GetHash(key) + } + if hm.hash == nil { + return false + } + hm.mutex.Lock() + //计算hash值并找到对应的avl树 + hash := hm.hash(key) % hm.cap + avl := hm.arr.At(hash).(*avlTree.AvlTree) + idx := &indexes{ + key: key, + value: value, + } + //判断是否存在该avl树中 + if avl.Count(idx) == 0 { + //avl树中不存在相同key,插入即可 + avl.Insert(idx) + hm.size++ + if hm.size >= hm.cap/4*3 { + //当达到扩容条件时候进行扩容 + hm.expend() + } + } else { + //覆盖 + avl.Insert(idx) + } + hm.mutex.Unlock() + return true +} +``` + +##### expend + +​ 以hashMap哈希映射做接收者,对原vector进行扩容,将所有的key-value取出,让vector自行扩容并清空原有结点,扩容后将所有的key-value重新插入vector中。 + +```go +func (hm *hashMap) expend() { + //取出所有的key-value + idxs := make([]*indexes, 0, hm.size) + for i := uint64(0); i < hm.arr.Size(); i++ { + avl := hm.arr.At(i).(*avlTree.AvlTree) + ite := avl.Iterator() + for j := ite.Begin(); j.HasNext(); j.Next() { + idxs = append(idxs, j.Value().(*indexes)) + } + } + cmp := func(a, b interface{}) int { + ka, kb := a.(*indexes), b.(*indexes) + return comparator.GetCmp(ka.key)(ka.key, kb.key) + } + //对vector进行扩容,扩容到其容量上限即可 + hm.arr.PushBack(avlTree.New(false, cmp)) + for i := uint64(0); i < hm.arr.Size()-1; i++ { + hm.arr.At(i).(*avlTree.AvlTree).Clear() + } + for i := hm.arr.Size(); i < hm.arr.Cap(); i++ { + hm.arr.PushBack(avlTree.New(false, cmp)) + } + //将vector容量设为hashMap容量 + hm.cap = hm.arr.Cap() + //重新将所有的key-value插入到hashMap中去 + for i := 0; i < len(idxs); i++ { + key, value := idxs[i].key, idxs[i].value + hash := hm.hash(key) % hm.cap + avl := hm.arr.At(hash).(*avlTree.AvlTree) + idx := &indexes{ + key: key, + value: value, + } + avl.Insert(idx) + } +} +``` + +#### Erase + +​ 以hashMap哈希映射做接收者,从hashMap中删除以key为索引的value,若存在则删除,否则直接结束,删除成功后size-1,删除后可能出现size<0.75/2*cap且大于16,此时要缩容。 + +```go +func (hm *hashMap) Erase(key interface{}) (b bool) { + if hm == nil { + return false + } + if hm.arr == nil { + return false + } + if hm.hash == nil { + return false + } + hm.mutex.Lock() + //计算该key的hash值 + hash := hm.hash(key) % hm.cap + avl := hm.arr.At(hash).(*avlTree.AvlTree) + idx := &indexes{ + key: key, + value: nil, + } + //从对应的avl树中删除该key-value + b = avl.Erase(idx) + if b { + //删除成功,此时size-1,同时进行缩容判断 + hm.size-- + if hm.size < hm.cap/8*3 && hm.cap > 16 { + hm.shrink() + } + } + hm.mutex.Unlock() + return b +} +``` + +##### shrink + +​ 以hashMap哈希映射做接收者,对原vector进行缩容,将所有的key-value取出,让vector自行缩容并清空所有结点,当vector容量与缩容开始时不同时则视为缩容结束,随容后将所有的key-value重新插入vector中。 + +```go +func (hm *hashMap) shrink() { + //取出所有key-value + idxs := make([]*indexes, 0, hm.size) + for i := uint64(0); i < hm.arr.Size(); i++ { + avl := hm.arr.At(i).(*avlTree.AvlTree) + ite := avl.Iterator() + for j := ite.Begin(); j.HasNext(); j.Next() { + idxs = append(idxs, j.Value().(*indexes)) + } + } + //进行缩容,当vector的cap与初始不同时,说明缩容结束 + cap := hm.arr.Cap() + for ; cap == hm.arr.Cap(); { + hm.arr.PopBack() + } + hm.cap = hm.arr.Cap() + //将所有的key-value重新放入hashMap中 + for i := 0; i < len(idxs); i++ { + key, value := idxs[i].key, idxs[i].value + hash := hm.hash(key) % hm.cap + avl := hm.arr.At(hash).(*avlTree.AvlTree) + idx := &indexes{ + key: key, + value: value, + } + avl.Insert(idx) + } +} +``` + +#### GetKeys + +​ 以hashMap哈希映射做接收者,返回该hashMap中所有的key。 + +```go +func (hm *hashMap) GetKeys() (keys []interface{}) { + if hm == nil { + return nil + } + if hm.arr == nil { + return nil + } + hm.mutex.Lock() + keys = make([]interface{}, 0, 1) + for i := uint64(0); i < hm.arr.Size(); i++ { + avl := hm.arr.At(i).(*avlTree.AvlTree) + ite := avl.Iterator() + es := make([]interface{}, 0, 1) + for j := ite.Begin(); j.HasNext(); j.Next() { + idx := j.Value().(*indexes) + es = append(es, idx.key) + } + keys = append(keys, es...) + } + hm.mutex.Unlock() + return keys +} +``` + +#### Get + +​ 以hashMap哈希映射做接收者,以key寻找到对应的value并返回。 + +```go +func (hm *hashMap) Get(key interface{}) (value interface{}) { + if hm == nil { + return + } + if hm.arr == nil { + return + } + if hm.hash == nil { + hm.hash = algorithm.GetHash(key) + } + if hm.hash == nil { + return + } + hm.mutex.Lock() + //计算hash值 + hash := hm.hash(key) % hm.cap + //从avl树中找到对应该hash值的key-value + info := hm.arr.At(hash).(*avlTree.AvlTree).Find(&indexes{key: key, value: nil}) + hm.mutex.Unlock() + if info == nil { + return nil + } + return info.(*indexes).value +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/hashMap" +) + +func main() { + m:=hashMap.New() + for i:=1;i<=17;i++{ + m.Insert(i,i) + } + fmt.Println("size=",m.Size()) + keys:=m.GetKeys() + fmt.Println(keys) + for i:=0;i< len(keys);i++{ + fmt.Println(m.Get(keys[i])) + } + for i:=m.Iterator().Begin();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + for i:=0;i< len(keys);i++{ + m.Erase(keys[i]) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> size= 17 +> [1 8 16 2 14 3 4 9 12 5 15 17 6 10 13 7 11] +> 1 +> 8 +> 16 +> 2 +> 14 +> 3 +> 4 +> 9 +> 12 +> 5 +> 15 +> 17 +> 6 +> 10 +> 13 +> 7 +> 11 +> 1 +> 8 +> 16 +> 2 +> 14 +> 3 +> 4 +> 9 +> 12 +> 5 +> 15 +> 17 +> 6 +> 10 +> 13 +> 7 +> 11 + +#### 时间开销 + +注:插入的是随机数,即可能出现重复插入覆盖的问题,所以数值会≤max + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/hashMap" + "math/rand" + "time" +) + +func main() { + max := 3000000 + num := 0 + r := rand.New(rand.NewSource(time.Now().UnixNano())) + tm := time.Now() + m := make(map[int]bool) + for i := 0; i < max; i++ { + num=r.Intn(4294967295) + m[num] = true + } + fmt.Println("map消耗时间:", time.Since(tm)) + thm := time.Now() + hm := hashMap.New() + for i := 0; i < max; i++ { + num=r.Intn(4294967295) + hm.Insert(uint64(num), true) + } + fmt.Println("hashmap添加消耗时间:", time.Since(thm)) + thmd := time.Now() + keys:=hm.GetKeys() + for i := 0; i < len(keys); i++ { + hm.Get(keys[i]) + } + fmt.Println("size=",hm.Size()) + fmt.Println("hashmap遍历消耗时间:", time.Since(thmd)) +} +``` + +> map消耗时间: 484.6992ms +> hashmap添加消耗时间: 4.6206379s +> size= 2999043 +> hashmap遍历消耗时间: 2.2020792s diff --git a/blog/数据结构STL——golang实现堆heap(完全二叉树).md b/blog/数据结构STL——golang实现堆heap(完全二叉树).md new file mode 100644 index 0000000..7caa609 --- /dev/null +++ b/blog/数据结构STL——golang实现堆heap(完全二叉树).md @@ -0,0 +1,493 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 本次采用**完全二叉树Complete Binary Tree**的形式实现堆。 + +​ 堆(heap)是一类特殊的数据结构的统称,堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质: + +- 堆中某个结点的值总是不大于或不小于其父结点的值; +- 堆总是一棵完全二叉树。 + +​ 堆的主要特点为:它的**父结点必然是小于或等于左右子结点**(该文章叙述中使用**小顶堆**,大顶堆大小情况相反)。 + +​ 在本次实践过程中使用**完全二叉树**实现。 + +### 原理 + +##### 完全二叉树实现堆 + +​ 对于一个完全二叉树来说,首先,完全二叉树除开底部的一层可能存在不满的情况外,其他上层必然是满的。对于完全二叉树的一个节点来说,它的左节点和右节点不使用下标来进行取得,而是使用其指针进行寻找。 + +​ 同时,在实现中,需要使用**size**来记录该堆中累计存储了多少元素。 + +​ 在插入和删除过程中,同数组实现类似,插入需要在最后一个节点插入,随后上升即可,删除需要将首节点替换为尾节点,随后下降首届点。而这两个过程中,核心点就在于如何**查找尾节点**。 + +##### 查找尾节点 + +​ 在查找尾节点时,可以参考数组实现过程中的下标,同时由于只需要查找尾节点,而尾节点的下标,恰好是**size-1**的值。 + +​ 所以,我们可以利用堆当前存储的元素数量去寻找最后一个节点和其父节点。 + +​ 同数组类似,“下标”从0开始,观察前一组插入的节点:(size+1)且为二进制 + +- 第一个节点->10 + +- 第二个节点->11 + +- 第三个节点->100 + +- 第四个节点->101 + +- 第五个节点->110 + +- 第六个节点->111 + +- 第七个节点->1000 + +- 第八个节点->1001 + +- 第九个节点->1010 + +- ······ + + + + 同时观察其在二叉树的位置可知,其可以根据其下标转化为二进制的值及1010101串来找到插入节点的父节点,例: + + - 第11个节点:**1100**该节点从根节点获取的路径为:**右左左** + - 第15给节点:**10000**该节点从根节点获取的路径为:**左左左左** + - 第18给节点:**10011**该节点从根节点获取的路径为:**左左右右** + - ··· + +观察可知,对于添加节点来说,可以通过其下标即当前的size+1转化为二进制,随后删掉其第一个bit,在根据后续的bit位,将1看作转入右节点,将0转入左节点,即可寻找到对应的节点,而若要找到其父节点只需要减少一步跳转即可。 + +​ 而对于删除节点来说,则不需要先减少size,直接找到末尾节点即可,方法同上。 + +#### 添加策略 + +​ 在解决了查找尾节点和其父节点的问题之后,对于添加策略其实和数组形式实现是基本一样的。 + +​ 即:当插入一个结点的时候,可以先将其放入完全二叉搜的末尾,然后根据插入的值和它父结点进行比较,如果插入值小于父节点时则交换两结点值,随后在用交换后的结点与其父结点比较,重复该过程直到抵达顶点或不满足条件即可。 + +##### 添加步骤 + +1. 通过查找尾节点的算法找到对应尾节点和其父节点,利用父节点插入 +2. 结点放入尾部后,通过比较该结点与父结点的值进行交换 +3. 满足交换条件,重复回到2过程 +4. 到达顶部或不满足交换条件,添加结束,插入完成 + +#### 删除策略 + +​ 完全二叉树实现堆的方式的删除策略类似于数组形式实现堆的删除策略,区别只是在需要用特殊的查找尾节点及其父节点的方法(上文已做介绍)。 + +​ 即:不同于添加时直接利用尾结点的情况,由于删除的是首结点,而同时删除需要减少一个空间,所以可以考虑将首位结点进行交换,或者其实直接**用尾结点覆写首节点**即可,这样就实现了首节点的删除,同时利用之前查找到的尾节点的父节点,将其对应侧的指针设为nil,即实现了尾节点的删除。 + +​ 上一过程完成后,首节点以及被删除了,但移到首节点的新首节点可能并不满足优先队列的父结点必然小于或等于左右子结点的情况,所以要通过比较父结点和其左右子结点来进行下将操作,即当存在一个在数组范围内的结点且大于父结点时则交换两结点,然后递归该过程,直到触底或不满足条件。 + +​ 比较过程中,先比较左结点与父结点的情况,然后再比较右结点的情况,找到最小的一侧进行下降即可。 + +##### 删除步骤 + +1. 通过查找尾节点的方法找到尾节点的父节点,并用尾节点覆盖首节点。 +2. 删除尾节点 +3. 以首节点为父结点,对其左右结点进行下降判断:左右节点是否存在,且存在的左右节点是否存在小于父结点的情况, +4. 比对左右两侧,找到满足小于父结点且最小的一侧进行下降即可,同时返回3过程进行递归下降 +5. 触底或不满足条件时下降结束,删除完成。 + +### 实现 + +​ cbTree完全二叉树树结构体,该实例存储二叉树的根节点,同时保存该二叉树已经存储了多少个元素,二叉树中排序使用的比较器在创建时传入,若不传入则在插入首个节点时从默认比较器中寻找。 + +```go +type cbTree struct { + root *node //根节点指针 + size uint64 //存储元素数量 + cmp comparator.Comparator //比较器 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是完全二叉树的树节点,该节点除了保存承载元素外,还将保存父节点、左右子节点的指针。 + +```go +type node struct { + value interface{} //节点中存储的元素 + parent *node //父节点指针 + left *node //左节点指针 + right *node //右节点指针 +} +``` + +#### 接口 + +```go +type cbTreer interface { + Iterator() (i *Iterator.Iterator) //返回包含该二叉树的所有元素 + Size() (num uint64) //返回该二叉树中保存的元素个数 + Clear() //清空该二叉树 + Empty() (b bool) //判断该二叉树是否为空 + Push(e interface{}) //向二叉树中插入元素e + Pop() //从二叉树中弹出顶部元素 + Top() (e interface{}) //返回该二叉树的顶部元素 +} +``` + +#### New + +​ 新建一个cbTree完全二叉树容器并返回,初始根节点为nil,若有传入的比较器,则将传入的第一个比较器设为该二叉树的比较器。 + +```go +func New(Cmp ...comparator.Comparator) (cb *cbTree) { + //判断是否有传入比较器,若有则设为该二叉树默认比较器 + var cmp comparator.Comparator + if len(Cmp) > 0 { + cmp = Cmp[0] + } + return &cbTree{ + root: nil, + size: 0, + cmp: cmp, + mutex: sync.Mutex{}, + } +} +``` + +##### newNode + +​ 新建一个完全二叉树节点并返回,将传入的元素e作为该节点的承载元素,将传入的parent节点作为其父节点,左右节点设为nil。 + +```go +func newNode(parent *node, e interface{}) (n *node) { + return &node{ + value: e, + parent: parent, + left: nil, + right: nil, + } +} +``` + +#### Iterator + +​ 以cbTree完全二叉树做接收者,将该二叉树中所有保存的元素将从根节点开始以前缀序列的形式放入迭代器中。 + +```go +func (cb *cbTree) Iterator() (i *Iterator.Iterator) { + if cb == nil { + cb = New() + } + cb.mutex.Lock() + es := cb.root.frontOrder() + i = Iterator.New(&es) + cb.mutex.Unlock() + return i +} +``` + +##### frontOrder + +​ 以node节点做接收者,以前缀序列返回节点集合。 + +```go +func (n *node) frontOrder() (es []interface{}) { + if n == nil { + return es + } + es = append(es, n.value) + if n.left != nil { + es = append(es, n.left.frontOrder()...) + } + if n.right != nil { + es = append(es, n.right.frontOrder()...) + } + return es +} +``` + +#### Size + +​ 以cbTree完全二叉树做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (cb *cbTree) Size() (num uint64) { + if cb == nil { + cb = New() + } + return cb.size +} +``` + +#### Clear + +​ 以cbTree完全二叉树做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (cb *cbTree) Clear() { + if cb == nil { + cb = New() + } + cb.mutex.Lock() + cb.root = nil + cb.size = 0 + cb.mutex.Unlock() +} +``` + +#### Empty + +​ 以cbTree完全二叉树做接收者,判断该完全二叉树树是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (cb *cbTree) Empty() (b bool) { + if cb == nil { + cb = New() + } + return cb.size == 0 +} +``` + +#### lastParent + +​ 以node节点做接收者,根据传入数值通过转化为二进制的方式模拟查找最后一个父节点,由于查找父节点的路径等同于转化为二进制后除开首位的中间值,故该方案是可行的。 + +```go +func (n *node) lastParent(num uint64) (ans *node) { + if num > 3 { + //去掉末尾的二进制值 + arr := make([]byte, 0, 64) + ans = n + for num > 0 { + //转化为二进制 + arr = append(arr, byte(num%2)) + num /= 2 + } + //去掉首位的二进制值 + for i := len(arr) - 2; i >= 1; i-- { + if arr[i] == 1 { + ans = ans.right + } else { + ans = ans.left + } + } + return ans + } + return n +} +``` + +#### Push + +​ 以cbTree完全二叉树做接收者,向二叉树插入元素e,将其放入完全二叉树的最后一个位置,随后进行元素上升,如果二叉树本身为空,则直接将根节点设为插入节点元素即可。 + +```go +func (cb *cbTree) Push(e interface{}) { + if cb == nil { + cb=New() + } + cb.mutex.Lock() + if cb.Empty() { + if cb.cmp == nil { + cb.cmp = comparator.GetCmp(e) + } + if cb.cmp == nil { + cb.mutex.Unlock() + return + } + cb.root = newNode(nil, e) + cb.size++ + } else { + cb.size++ + cb.root.insert(cb.size, e, cb.cmp) + } + cb.mutex.Unlock() +} +``` + +##### insert + +​ 以node节点做接收者,从该节点插入元素e,并根据传入的num寻找最后一个父节点用于插入最后一位值,随后对插入值进行上升处理。 + +```go +func (n *node) insert(num uint64, e interface{}, cmp comparator.Comparator) { + if n == nil { + return + } + //寻找最后一个父节点 + n = n.lastParent(num) + //将元素插入最后一个节点 + if num%2 == 0 { + n.left = newNode(n, e) + n = n.left + } else { + n.right = newNode(n, e) + n = n.right + } + //对插入的节点进行上升 + n.up(cmp) +} +``` + +##### up + +​ 以node节点做接收者,对该节点进行上升,当该节点存在且父节点存在时,若该节点小于夫节点,则在交换两个节点值后继续上升即可。 + +```go +func (n *node) up(cmp comparator.Comparator) { + if n == nil { + return + } + if n.parent == nil { + return + } + //该节点和父节点都存在 + if cmp(n.parent.value, n.value) > 0 { + //该节点值小于父节点值,交换两节点值,继续上升 + n.parent.value, n.value = n.value, n.parent.value + n.parent.up(cmp) + } +} +``` + +#### Pop + +​ 以cbTree完全二叉树做接收者,从完全二叉树中删除顶部元素e,将该顶部元素于最后一个元素进行交换,随后删除最后一个元素,再将顶部元素进行下沉处理即可。 + +```go +func (cb *cbTree) Pop() { + if cb == nil { + return + } + if cb.Empty() { + return + } + cb.mutex.Lock() + if cb.size == 1 { + //该二叉树仅剩根节点,直接删除即可 + cb.root = nil + } else { + //该二叉树删除根节点后还有其他节点可生为跟节点 + cb.root.delete(cb.size, cb.cmp) + } + cb.size-- + cb.mutex.Unlock() +} +``` + +##### delete + +​ 以node节点做接收者,从删除该,并根据传入的num寻找最后一个父节点用于替换删除,随后对替换后的值进行下沉处理即可 + +```go +func (n *node) delete(num uint64, cmp comparator.Comparator) { + if n == nil { + return + } + //寻找最后一个父节点 + ln := n.lastParent(num) + if num%2 == 0 { + n.value = ln.left.value + ln.left = nil + } else { + n.value = ln.right.value + ln.right = nil + } + //对交换后的节点进行下沉 + n.down(cmp) +} +``` + +##### down + +​ 以node节点做接收者,对该节点进行下沉,当该存在右节点且小于自身元素时,与右节点进行交换并继续下沉,否则当该存在左节点且小于自身元素时,与左节点进行交换并继续下沉,当左右节点都不存在或都大于自身时下沉停止。 + +```go +func (n *node) down(cmp comparator.Comparator) { + if n == nil { + return + } + if n.right != nil && cmp(n.left.value, n.right.value) >= 0 { + n.right.value, n.value = n.value, n.right.value + n.right.down(cmp) + return + } + if n.left != nil && cmp(n.value, n.left.value) >= 0 { + n.left.value, n.value = n.value, n.left.value + n.left.down(cmp) + return + } +} +``` + +#### Top + +​ 以cbTree完全二叉树做接收者,返回该完全二叉树的顶部元素,当该完全二叉树不存在或根节点不存在时返回nil。 + +```go +func (cb *cbTree) Top() (e interface{}) { + if cb == nil { + cb=New() + } + cb.mutex.Lock() + e = cb.root.value + cb.mutex.Unlock() + return e +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/heap" + "sync" +) + +func main() { + h := heap.New() + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func(num int) { + h.Push(num) + }(i) + } + fmt.Println("利用迭代器输出堆中存储的所有元素:") + for i := h.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("依次输出顶部元素:") + for !h.Empty() { + fmt.Println(h.Top()) + h.Pop() + } +} +``` + +注:虽然添加过程是随机的,但由于其本身是相对有序的,所以不论怎么添加都是一个输出结果 + +> 利用迭代器输出堆中存储的所有元素: +> 0 +> 3 +> 4 +> 6 +> 8 +> 5 +> 9 +> 1 +> 2 +> 7 +> 依次输出顶部元素: +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 7 +> 8 +> 9 diff --git a/blog/数据结构STL——golang实现字典查找树trie.md b/blog/数据结构STL——golang实现字典查找树trie.md new file mode 100644 index 0000000..e0d8d1c --- /dev/null +++ b/blog/数据结构STL——golang实现字典查找树trie.md @@ -0,0 +1,582 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 单词查找树(Tire),又叫前缀树,字典树,是一种有序多叉树。不同于之前实现的二叉搜,它是一个具有多个分叉的树的结构,同时,树结点和其子结点之间并无大小关系,只存在前缀关系,即其**父结点是其子结点的前缀**,一般用于存储string类型,对于string类型的增删查效率极高,其**增删查时间等价于string的长度**。 + +### 原理 + +​ 本次实现的单词查找树的每个结点共有64个分叉,即‘a'~'z','A'~'Z','0'~'9','+','/'一共64个字符,对应base64的64个字符,可用于存储base64。 + +​ 对于一个前缀树来说,它需要满足的特征有三条: + +- 父结点的前缀必然是子结点的前缀 +- 根节点不包含字符,除根节点以外每个节点只包含一个字符 +- 每个节点的所有子节点包含的字符串不相同。 + +​ 它的核心策略是以空间换时间,即将一个string类型拆开,分层保存其byte或叫char,使得每次增删查都只需要其长度的时间,最坏的查找时间比hash表更好。同时也不会出现冲突,并且也必然是满足字典序,即按其中序遍历得到的结果必然是有序的。 + +​ 但同时,如果出现了一个较长的string,就会让整个链条变得很长,造成较多的空间开销。 + +#### 添加策略 + +​ 从根节点开始插入,将string按byte进行分段,每层插入一个,当插入到最后时该string指向的value存在时则说明之前已经插入过,故插入失败,否则插入成功。 + +​ 插入不允许覆盖。 + +​ 当中间结点在原Trie树中不存在时创建即可。 + +#### 删除策略 + +​ 从根节点开始删除,将string按byte进行分段,逐层往下遍历寻找到最终点,如果此时有存储的元素则删除同时表示删除成功,随后逐层返回将对应结点的num-1即可,当num=0时表示无后续结点,将该结点删除即可。如果在逐层下推的过程中发现结点不存在,可视为删除失败。之间返回即可。 + +### 实现 + +​ trie单词查找树结构体,该实例存储单词查找树的根节点,同时保存该树已经存储了多少个元素,整个树不允许重复插入,若出现重复插入则直接失败。 + +```go +type trie struct { + root *node //根节点指针 + size int //存放的元素数量 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是trie的树节点,结点存储到此时的string的前缀数量,以son为分叉存储下属的string,该节点同时存储其元素。 + +```go +type node struct { + num int //以当前结点为前缀的string的数量 + son [64]*node //分叉 + value interface{} //当前结点承载的元素 +} +``` + +#### 接口 + +```go +type trieer interface { + Iterator() (i *Iterator.Iterator) //返回包含该trie的所有string + Size() (num int) //返回该trie中保存的元素个数 + Clear() //清空该trie + Empty() (b bool) //判断该trie是否为空 + Insert(s string, e interface{}) (b bool) //向trie中插入string并携带元素e + Erase(s string) (b bool) //从trie中删除以s为索引的元素e + Delete(s string) (num int) //从trie中删除以s为前缀的所有元素 + Count(s string) (num int) //从trie中寻找以s为前缀的string单词数 + Find(s string) (e interface{}) //从trie中寻找以s为索引的元素e +} +``` + +#### New + +​ 新建一个trie单词查找树容器并返回,初始根节点为nil。 + +```go +func New() (t *trie) { + return &trie{ + root: newNode(nil), + size: 0, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个单词查找树节点并返回,将传入的元素e作为该节点的承载元素。 + +```go +func newNode(e interface{}) (n *node) { + return &node{ + num: 0, + value: e, + } +} +``` + +#### Iterator + +​ 以trie单词查找树做接收者,将该trie中所有存放的string放入迭代器中并返回。 + +```go +func (t *trie) Iterator() (i *Iterator.Iterator) { + if t == nil { + return nil + } + t.mutex.Lock() + //找到trie中存在的所有string + es := t.root.inOrder("") + i = Iterator.New(&es) + t.mutex.Unlock() + return i +} +``` + +​ 以node单词查找树节点做接收者,遍历其分叉以找到其存储的所有string。 + +```go +func (n *node) inOrder(s string) (es []interface{}) { + if n == nil { + return es + } + if n.value != nil { + es = append(es, s) + } + for i, p := 0, 0; i < 62 && p < n.num; i++ { + if n.son[i] != nil { + if i < 26 { + es = append(es, n.son[i].inOrder(s+string(i+'a'))...) + } else if i < 52 { + es = append(es, n.son[i].inOrder(s+string(i-26+'A'))...) + } else { + es = append(es, n.son[i].inOrder(s+string(i-52+'0'))...) + } + p++ + } + } + return es +} +``` + +#### Size + +​ 以trie单词查找树做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (t *trie) Size() (num int) { + if t == nil { + return 0 + } + return t.size +} +``` + +#### Clear + +​ 以trie单词查找树做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (t *trie) Clear() { + if t == nil { + return + } + t.mutex.Lock() + t.root = newNode(nil) + t.size = 0 + t.mutex.Unlock() +} +``` + +#### Empty + +​ 以trie单词查找树做接收者,判断该trie是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (t *trie) Empty() (b bool) { + if t == nil { + return true + } + return t.size == 0 +} +``` + +##### getIdx + +​ 传入一个byte并根据其值返回其映射到分叉的值,当不属于'a'~'z','A'~'Z','0'~'9','+','/'时返回-1。 + +```go +func getIdx(c byte) (idx int) { + if c >= 'a' && c <= 'z' { + idx = int(c - 'a') + } else if c >= 'A' && c <= 'Z' { + idx = int(c-'A') + 26 + } else if c >= '0' && c <= '9' { + idx = int(c-'0') + 52 + } else if c == '+' { + idx = 62 + } else if c == '/' { + idx = 63 + } else { + idx = -1 + } + return idx +} +``` + +#### Insert + +​ 以trie单词查找树做接收者,向trie插入以string类型的s为索引的元素e,若存在重复的s则插入失败,不允许覆盖,否则插入成功。 + +```go +func (t *trie) Insert(s string, e interface{}) (b bool) { + if t == nil { + return + } + if len(s) == 0 { + return false + } + t.mutex.Lock() + if t.root == nil { + //避免根节点为nil + t.root = newNode(nil) + } + //从根节点开始插入 + b = t.root.insert(s, 0, e) + if b { + //插入成功,size+1 + t.size++ + } + t.mutex.Unlock() + return b +} +``` + +​ 以node单词查找树节点做接收者,从n节点中继续插入以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行插入,如果此时node承载了元素则插入失败,否则成功,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可。 + +```go +func (n *node) insert(s string, p int, e interface{}) (b bool) { + if p == len(s) { + if n.value != nil { + return false + } + n.value = e + n.num++ + return true + } + idx := getIdx(s[p]) + if idx == -1 { + return false + } + if n.son[idx] == nil { + n.son[idx] = newNode(nil) + } + b = n.son[idx].insert(s, p+1, e) + if b { + n.num++ + } + return b +} +``` + +#### Erase + +​ 以trie单词查找树做接收者,从trie树中删除元素以s为索引的元素e。 + +```go +func (t *trie) Erase(s string) (b bool) { + if t == nil { + return false + } + if t.Empty() { + return false + } + if len(s) == 0 { + //长度为0无法删除 + return false + } + if t.root == nil { + //根节点为nil即无法删除 + return false + } + t.mutex.Lock() + //从根节点开始删除 + b = t.root.erase(s, 0) + if b { + //删除成功,size-1 + t.size-- + if t.size == 0 { + //所有string都被删除,根节点置为nil + t.root = nil + } + } + t.mutex.Unlock() + return b +} +``` + +​ 以node单词查找树节点做接收者,从n节点中继续删除以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行删除,如果此时node未承载元素则删除失败,否则成功,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,若其分叉为nil则直接失败。 + +```go +func (n *node) erase(s string, p int) (b bool) { + if p == len(s) { + if n.value != nil { + n.value = nil + n.num-- + return true + } + return false + } + idx := getIdx(s[p]) + if idx == -1 { + return false + } + if n.son[idx] == nil { + return false + } + b = n.son[idx].erase(s, p+1) + if b { + n.num-- + if n.son[idx].num == 0 { + n.son[idx] = nil + } + } + return b +} +``` + +#### Delete + +​ 以trie单词查找树做接收者,从trie树中删除以s为前缀的所有元素。 + +```go +func (t *trie) Delete(s string) (num int) { + if t == nil { + return 0 + } + if t.Empty() { + return 0 + } + if len(s) == 0 { + //长度为0无法删除 + return 0 + } + if t.root == nil { + //根节点为nil即无法删除 + return 0 + } + t.mutex.Lock() + //从根节点开始删除 + num = t.root.delete(s, 0) + if num > 0 { + //删除成功 + t.size -= num + if t.size <= 0 { + //所有string都被删除,根节点置为nil + t.root = nil + } + } + t.mutex.Unlock() + return num +} +``` + +​ 以node单词查找树节点做接收者,从n节点中继续删除以s为索引的元素e,且当前抵达的string位置为p,当到达s终点时进行删除,删除所有后续元素,并返回其后续元素的数量,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,若其分叉为nil则直接返回0。 + +```go +func (n *node) delete(s string, p int) (num int) { + if p == len(s) { + return n.num + } + idx := getIdx(s[p]) + if idx == -1 { + return 0 + } + if n.son[idx] == nil { + return 0 + } + num = n.son[idx].delete(s, p+1) + if num>0 { + n.num-=num + if n.son[idx].num <= 0 { + n.son[idx] = nil + } + } + return num +} +``` + +#### Count + +​ 以trie单词查找树做接收者,从trie中查找以s为前缀的所有string的个数,如果存在以s为前缀的则返回大于0的值即其数量,如果未找到则返回0。 + +```go +func (t *trie) Count(s string) (num int) { + if t == nil { + return 0 + } + if t.Empty() { + return 0 + } + if t.root == nil { + return 0 + } + t.mutex.Lock() + //统计所有以s为前缀的string的数量并返回 + num = int(t.root.count(s, 0)) + t.mutex.Unlock() + return num +} +``` + +​ 以node单词查找树节点做接收者,从n节点中继续查找以s为前缀索引的元素e,且当前抵达的string位置为p,当到达s终点时返回其值即可,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,当其分叉为nil则直接返回0。 + +```go +func (n *node) count(s string, p int) (num int) { + if p == len(s) { + return n.num + } + idx := getIdx(s[p]) + if idx == -1 { + return 0 + } + if n.son[idx] == nil { + return 0 + } + return n.son[idx].count(s, p+1) +} +``` + +#### Find + +​ 以trie单词查找树做接收者,从trie中查找以s为索引的元素e,找到则返回e,如果未找到则返回nil。 + +```go +func (t *trie) Find(s string) (e interface{}) { + if t == nil { + return nil + } + if t.Empty() { + return nil + } + if t.root == nil { + return nil + } + t.mutex.Lock() + //从根节点开始查找以s为索引的元素e + e = t.root.find(s, 0) + t.mutex.Unlock() + return e +} +``` + +​ 以node单词查找树节点做接收者,从n节点中继续查找以s为前缀索引的元素e,且当前抵达的string位置为p,当到达s终点时返回其承载的元素即可,当未到达终点时,根据当前抵达的位置去寻找其子结点继续遍历即可,当其分叉为nil则直接返回nil。 + +```go +func (n *node) find(s string, p int) (e interface{}) { + if p == len(s) { + return n.value + } + idx := getIdx(s[p]) + if idx == -1 { + return nil + } + if n.son[idx] == nil { + return nil + } + return n.son[idx].find(s, p+1) +} +``` + + + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/trie" +) + +func main() { + t:=trie.New() + t.Insert("hlccd","hlccd") + t.Insert("ha","ha") + t.Insert("hb","hb") + t.Insert("hc","hc") + t.Insert("hd","hd") + t.Insert("he","he") + t.Insert("hl","hl") + t.Insert("hlccd1","hlccd1") + t.Insert("hlccd2","hlccd2") + t.Insert("hlccd3","hlccd3") + t.Insert("hlccd+","hlccd") + t.Insert("hlccd/","hlccd") + fmt.Println("当前插入的所有string:") + for i:=t.Iterator().Begin();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + t.Erase("h") + t.Erase("ha") + t.Erase("hb") + t.Erase("hc") + t.Erase("hd") + t.Erase("he") + fmt.Println("定向删除后剩余的string:") + for i:=t.Iterator().Begin();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + t.Delete("h") + fmt.Println("删除以'h'为前缀的所有元素后剩余的数量:") + for i:=t.Iterator().Begin();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } +} +``` + +> 当前插入的所有string: +> ha +> hb +> hc +> hd +> he +> hl +> hlccd +> hlccd1 +> hlccd2 +> hlccd3 +> 定向删除后剩余的string: +> hl +> hlccd +> hlccd1 +> hlccd2 +> hlccd3 +> 删除以'h'为前缀的所有元素后剩余的数量: + +#### 时间开销 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/trie" + "math/rand" + "time" +) + +func main() { + max := 3000000 + ss := "" + r := rand.New(rand.NewSource(time.Now().UnixNano())) + ts := time.Now() + s := make([]string, 0, 0) + for i := 0; i < max; i++ { + ss = fmt.Sprintf("%d", r.Intn(4294967295)) + s = append(s, ss) + } + fmt.Println("slice消耗时间:", time.Since(ts)) + tm := time.Now() + m := make(map[string]bool) + for i := 0; i < max; i++ { + ss = fmt.Sprintf("%d", r.Intn(4294967295)) + m[ss] = true + } + fmt.Println("map消耗时间:", time.Since(tm)) + tt := time.Now() + t := trie.New() + for i := 0; i < max; i++ { + ss = fmt.Sprintf("%d", r.Intn(4294967295)) + t.Insert(ss, true) + } + fmt.Println("trie消耗时间:", time.Since(tt)) + tt1 := time.Now() + t.Iterator() + fmt.Println("trie遍历消耗的时间:", time.Since(tt1)) +} +``` + +> slice消耗时间: 586.3899ms +> map消耗时间: 1.192838s +> trie消耗时间: 6.4676663s +> trie遍历消耗的时间: 4.3793102s diff --git a/blog/数据结构STL——golang实现平衡二叉查找树avlTree.md b/blog/数据结构STL——golang实现平衡二叉查找树avlTree.md new file mode 100644 index 0000000..e97b3f0 --- /dev/null +++ b/blog/数据结构STL——golang实现平衡二叉查找树avlTree.md @@ -0,0 +1,683 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 自平衡二叉查找树(Self-Balancing Binary Search Tree),简称为平衡二叉树,一般以其发明者的名称缩写命名为avl树。 + +​ 对于一颗平衡二叉树来说,一方面它需要**满足二叉搜索树的性质**,即父结点大于左结点小于右结点,另一方面,**该树中每个结点的左右子结点的高度差不能超过1**,即其平衡因子最大为1,当插入或删除结点后导致平衡因子超过1时,则需要通过旋转的方式对其进行调节。 + +### 原理 + +​ 对于平衡二叉搜来说,它的搜索同二叉搜索树一样,只需要定向遍历树结点即可,而在增加和删除的过程中,第一步可以先按照二叉搜索树的方式进行增加和删除,即直接插入增加、删除前缀or后继结点即可,增删完成后,则需要对增删结点的父结点进行平衡因子的判断,即重新计算其左右子树的高度差,当期高度差超过1时则需要进行旋转,旋转策略如下: + +1. 左旋:将该结点向其左结点方向旋转,使得右结点放在原结点的位置,右节点的左子树结点即为该结点。 +2. 右旋:将该结点向其右结点方向旋转,使得左结点放在原结点的位置,左节点的右子树结点即为该结点。 +3. 右左旋:将原节点的右节点先进行右旋再将原节点进行左旋 +4. 左右旋:将原节点的左结点先进行左旋再将原结点进行右旋 + +#### 情况分析 + +​ 以插入结点为例,删除节点可视为插入结点的逆运算,即**删除左结点等于插入右结点,删除右结点等于插入左结点**。 + +##### 单左旋转 + +​ 按照{4,5,6}的顺序插入树内,在插入6时会出现4结点的不平衡情况,即单侧不平衡,对于此类情况,只需要将4结点进行左旋即可,即将5结点放到4结点的位置,4结点设为5节点的左结点。 + +##### 单右旋转 + +​ 在刚刚的基础上,插入{3,2}结点,在插入2结点的时候会出现4结点的不平衡,并且也是单侧不平衡,对于此类情况,只需要将4结点进行右旋即可,即将3结点放到4结点的位置上,4结点设为3结点的右结点。 + +##### 右左旋转 + +​ 在此基础上继续插入{8,7}结点,在插入7结点的时候会出现6结点的不平衡,同时,由于8结点存在左结点,故只能先对8结点进行右旋,使得7结点处于8结点的位置,8结点设为7结点的右结点,然后就变成了单左旋转的情况,再进行一次对6结点的单左旋转即可。 + +##### 左右旋转 + +​ 在此基础上继续插入{0,1}结点,在插入1结点的时候会出现2结点的不平衡,考虑到0结点存在右结点,故需要先对0结点进行一次左旋,将1结点放到0结点的位置,0结点设为1结点的左结点,然后就变成了单右旋转的情况,再对2结点进行一次单右旋转即可。 + +### 实现 + +​ avlTree平衡二叉树结构体,该实例存储平衡二叉树的根节点,同时保存该二叉树已经存储了多少个元素,二叉树中排序使用的比较器在创建时传入,若不传入则在插入首个节点时从默认比较器中寻找,创建时传入是否允许该二叉树出现重复值,如果不允许则进行覆盖,允许则对节点数目增加即可。 + +```go +type avlTree struct { + root *node //根节点指针 + size int //存储元素数量 + cmp comparator.Comparator //比较器 + isMulti bool //是否允许重复 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是平衡二叉树的树节点,若该平衡二叉树允许重复则对节点num+1即可,否则对value进行覆盖,平衡二叉树节点当左右子节点深度差超过1时进行左右旋转以实现平衡。 + +```go +type node struct { + value interface{} //节点中存储的元素 + num int //该元素数量 + depth int //该节点的深度 + left *node //左节点指针 + right *node //右节点指针 +} +``` + +#### 接口 + +```go +type avlTreer interface { + Iterator() (i *Iterator.Iterator) //返回包含该二叉树的所有元素,重复则返回多个 + Size() (num int) //返回该二叉树中保存的元素个数 + Clear() //清空该二叉树 + Empty() (b bool) //判断该二叉树是否为空 + Insert(e interface{}) //向二叉树中插入元素e + Erase(e interface{}) //从二叉树中删除元素e + Count(e interface{}) (num int) //从二叉树中寻找元素e并返回其个数 +} +``` + +#### New + +​ 新建一个avlTree平衡二叉树容器并返回,初始根节点为nil,传入该二叉树是否为可重复属性,如果为true则保存重复值,否则对原有相等元素进行覆盖,若有传入的比较器,则将传入的第一个比较器设为该二叉树的比较器。 + +```go +func New(isMulti bool, cmps ...comparator.Comparator) (avl *avlTree) { + //判断是否有传入比较器,若有则设为该二叉树默认比较器 + var cmp comparator.Comparator + if len(cmps) == 0 { + cmp = nil + } else { + cmp = cmps[0] + } + return &avlTree{ + root: nil, + size: 0, + cmp: cmp, + isMulti: isMulti, + } +} +``` + +​ 新建一个平衡二叉树节点并返回,将传入的元素e作为该节点的承载元素,该节点的num和depth默认为1,左右子节点设为nil。 + +```go +func newNode(e interface{}) (n *node) { + return &node{ + value: e, + num: 1, + depth: 1, + left: nil, + right: nil, + } +} +``` + +#### Iterator + +​ 以avlTree平衡二叉树做接收者,将该二叉树中所有保存的元素将从根节点开始以中缀序列的形式放入迭代器中,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (avl *avlTree) Iterator() (i *Iterator.Iterator) { + if avl == nil { + return nil + } + avl.mutex.Lock() + es:=avl.root.inOrder() + i = Iterator.New(&es) + avl.mutex.Unlock() + return i +} +``` + +​ 以node平衡二叉树节点做接收者,以中缀序列返回节点集合,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (n *node) inOrder() (es []interface{}) { + if n == nil { + return es + } + if n.left != nil { + es = append(es, n.left.inOrder()...) + } + for i := 0; i < n.num; i++ { + es = append(es, n.value) + } + if n.right != nil { + es = append(es, n.right.inOrder()...) + } + return es +} +``` + +#### Size + +​ 以avlTree平衡二叉树做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (bs *bsTree) Size() (num uint64) { + if bs == nil { + //创建一个允许插入重复值的二叉搜 + bs = New(true) + } + return bs.size +} +``` + +#### Clear + +​ 以avlTree平衡二叉树做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (avl *avlTree) Clear() { + if avl == nil { + return + } + avl.mutex.Lock() + avl.root = nil + avl.size = 0 + avl.mutex.Unlock() +} +``` + +#### Empty + +​ 以avlTree平衡二叉树做接收者,判断该二叉搜索树是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (avl *avlTree) Empty() (b bool) { + if avl == nil { + return true + } + if avl.size > 0 { + return false + } + return true +} +``` + +#### 旋转 + +##### getDepth + +​ 以node平衡二叉树节点做接收者,返回该节点的深度,节点不存在返回0。 + +```go +func (n *node) getDepth() (depth int) { + if n == nil { + return 0 + } + return n.depth +} +``` + +##### max + +​ 返回a和b中较大的值 + +```go +func max(a, b int) (m int) { + if a > b { + return a + } else { + return b + } +} +``` + +##### leftRotate + +​ 以node平衡二叉树节点做接收者,将该节点向左节点方向转动,使右节点作为原来节点,并返回右节点,同时将右节点的左节点设为原节点的右节点。 + +```go +func (n *node) leftRotate() (m *node) { + //左旋转 + headNode := n.right + n.right = headNode.left + headNode.left = n + //更新结点高度 + n.depth = max(n.left.getDepth(), n.right.getDepth()) + 1 + headNode.depth = max(headNode.left.getDepth(), headNode.right.getDepth()) + 1 + return headNode +} +``` + +##### rightRotate + +​ 以node平衡二叉树节点做接收者,将该节点向右节点方向转动,使左节点作为原来节点,并返回左节点,同时将左节点的右节点设为原节点的左节点。 + +```go +func (n *node) rightRotate() (m *node) { + //右旋转 + headNode := n.left + n.left = headNode.right + headNode.right = n + //更新结点高度 + n.depth = max(n.left.getDepth(), n.right.getDepth()) + 1 + headNode.depth = max(headNode.left.getDepth(), headNode.right.getDepth()) + 1 + return headNode +} +``` + +##### rightLeftRotate + +​ 以node平衡二叉树节点做接收者,将原节点的右节点先进行右旋再将原节点进行左旋。 + +```go +func (n *node) rightLeftRotate() (m *node) { + //右旋转,之后左旋转 + //以失衡点右结点先右旋转 + sonHeadNode := n.right.rightRotate() + n.right = sonHeadNode + //再以失衡点左旋转 + return n.leftRotate() +} +``` + +##### leftRightRotate + +​ 以node平衡二叉树节点做接收者,将原节点的左节点先进行左旋再将原节点进行右旋。 + +```go +func (n *node) leftRightRotate() (m *node) { + //左旋转,之后右旋转 + //以失衡点左结点先左旋转 + sonHeadNode := n.left.leftRotate() + n.left = sonHeadNode + //再以失衡点左旋转 + return n.rightRotate() +} +``` + +##### getMin + +​ 以node平衡二叉树节点做接收者,返回n节点的最小元素的承载的元素和数量,即该节点父节点的后缀节点的元素和数量。 + +```go +func (n *node) getMin() (e interface{}, num int) { + if n == nil { + return nil, 0 + } + if n.left == nil { + return n.value, n.num + } else { + return n.left.getMin() + } +} +``` + +##### adjust + +​ 以node平衡二叉树节点做接收者,对n节点进行旋转以保持节点左右子树平衡。 + +```go +func (n *node) adjust() (m *node) { + if n.right.getDepth()-n.left.getDepth() >= 2 { + //右子树高于左子树且高度差超过2,此时应当对n进行左旋 + if n.right.right.getDepth() > n.right.left.getDepth() { + //由于右右子树高度超过右左子树,故可以直接左旋 + n = n.leftRotate() + } else { + //由于右右子树不高度超过右左子树 + //所以应该先右旋右子树使得右子树高度不超过左子树 + //随后n节点左旋 + n = n.rightLeftRotate() + } + } else if n.left.getDepth()-n.right.getDepth() >= 2 { + //左子树高于右子树且高度差超过2,此时应当对n进行右旋 + if n.left.left.getDepth() > n.left.right.getDepth() { + //由于左左子树高度超过左右子树,故可以直接右旋 + n = n.rightRotate() + } else { + //由于左左子树高度不超过左右子树 + //所以应该先左旋左子树使得左子树高度不超过右子树 + //随后n节点右旋 + n = n.leftRightRotate() + } + } + return n +} +``` + +#### Insert + +​ 以avlTree平衡二叉树做接收者,向二叉树插入元素e,若不允许重复则对相等元素进行覆盖,如果二叉树为空则之间用根节点承载元素e,否则以根节点开始进行查找,当节点左右子树高度差超过1时将进行旋转以保持平衡。 + +```go +func (avl *avlTree) Insert(e interface{}) { + if avl == nil { + return + } + avl.mutex.Lock() + if avl.Empty() { + if avl.cmp == nil { + avl.cmp = comparator.GetCmp(e) + } + if avl.cmp == nil { + return + } + //二叉树为空,用根节点承载元素e + avl.root = newNode(e) + avl.size = 1 + avl.mutex.Unlock() + return + } + //从根节点进行插入,并返回节点,同时返回是否插入成功 + var b bool + avl.root, b = avl.root.insert(e, avl.isMulti, avl.cmp) + if b { + //插入成功,数量+1 + avl.size++ + } + avl.mutex.Unlock() +} +``` + +​ 以node平衡二叉树节点做接收者,从n节点中插入元素e,如果n节点中承载元素与e不同则根据大小从左右子树插入该元素,如果n节点与该元素相等,且允许重复值,则将num+1否则对value进行覆盖,插入成功返回true,插入失败或不允许重复插入返回false。 + +```go +func (n *node) insert(e interface{}, isMulti bool, cmp comparator.Comparator) (m *node, b bool) { + //节点不存在,应该创建并插入二叉树中 + if n == nil { + return newNode(e), true + } + if cmp(e, n.value) < 0 { + //从左子树继续插入 + n.left, b = n.left.insert(e, isMulti, cmp) + if b { + //插入成功,对该节点进行平衡 + n = n.adjust() + } + n.depth = max(n.left.getDepth(), n.right.getDepth()) + 1 + return n, b + } + if cmp(e, n.value) > 0 { + //从右子树继续插入 + n.right, b = n.right.insert(e, isMulti, cmp) + if b { + //插入成功,对该节点进行平衡 + n = n.adjust() + } + n.depth = max(n.left.getDepth(), n.right.getDepth()) + 1 + return n, b + } + //该节点元素与待插入元素相同 + if isMulti { + //允许重复,数目+1 + n.num++ + return n, true + } + //不允许重复,对值进行覆盖 + n.value = e + return n, false +} +``` + +#### Erase + +​ 以avlTree平衡二叉树做接收者,从平衡二叉树中删除元素e,若允许重复记录则对承载元素e的节点中数量记录减一即可,若不允许重复记录则删除该节点同时将前缀节点或后继节点更换过来以保证二叉树的不发送断裂,如果该二叉树仅持有一个元素且根节点等价于待删除元素,则将二叉树根节点置为nil。 + +```go +func (avl *avlTree) Erase(e interface{}) { + if avl == nil { + return + } + if avl.Empty() { + return + } + avl.mutex.Lock() + if avl.size == 1 && avl.cmp(avl.root.value, e) == 0 { + //二叉树仅持有一个元素且根节点等价于待删除元素,将二叉树根节点置为nil + avl.root = nil + avl.size = 0 + avl.mutex.Unlock() + return + } + //从根节点进行插入,并返回节点,同时返回是否删除成功 + var b bool + avl.root, b = avl.root.erase(e, avl.cmp) + if b { + avl.size-- + } + avl.mutex.Unlock() +} +``` + +​ 以node平衡二叉树节点做接收者,从n节点中删除元素e,如果n节点中承载元素与e不同则根据大小从左右子树删除该元素,如果n节点与该元素相等,且允许重复值,则将num-1否则直接删除该元素,删除时先寻找该元素的前缀节点,若不存在则寻找其后继节点进行替换,替换后删除该节点。 + +```go +func (n *node) erase(e interface{}, cmp comparator.Comparator) (m *node, b bool) { + if n == nil { + //待删除值不存在,删除失败 + return n, false + } + if cmp(e, n.value) < 0 { + //从左子树继续删除 + n.left, b = n.left.erase(e, cmp) + } else if cmp(e, n.value) > 0 { + //从右子树继续删除 + n.right, b = n.right.erase(e, cmp) + } else if cmp(e, n.value) == 0 { + //存在相同值,从该节点删除 + b = true + if n.num > 1 { + //有重复值,节点无需删除,直接-1即可 + n.num-- + } else { + //该节点需要被删除 + if n.left != nil && n.right != nil { + //找到该节点后继节点进行交换删除 + n.value, n.num = n.right.getMin() + //从右节点继续删除,同时可以保证删除的节点必然无左节点 + n.right, b = n.right.erase(n.value, cmp) + } else if n.left != nil { + n = n.left + } else { + n = n.right + } + } + } + //当n节点仍然存在时,对其进行调整 + if n != nil { + n.depth = max(n.left.getDepth(), n.right.getDepth()) + 1 + n = n.adjust() + } + return n, b +} +``` + +#### Count + +​ 以avlTree平衡二叉树做接收者,从搜素二叉树中查找元素e的个数,如果找到则返回该二叉树中和元素e相同元素的个数,如果不允许重复则最多返回1,如果未找到则返回0。 + +```go +func (avl *avlTree) Count(e interface{}) (num int) { + if avl == nil { + //二叉树为空,返回0 + return 0 + } + if avl.Empty() { + return 0 + } + avl.mutex.Lock() + num = avl.root.count(e, avl.isMulti, avl.cmp) + avl.mutex.Unlock() + return num +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中查找元素e并返回存储的个数,如果n节点中承载元素与e不同则根据大小从左右子树查找该元素,如果n节点与该元素相等,则直接返回其个数。 + +```go +func (n *node) count(e interface{}, isMulti bool, cmp comparator.Comparator) (num int) { + if n == nil { + return 0 + } + //n中承载元素小于e,从右子树继续查找并返回结果 + if cmp(n.value, e) < 0 { + return n.right.count(e, isMulti, cmp) + } + //n中承载元素大于e,从左子树继续查找并返回结果 + if cmp(n.value, e) > 0 { + return n.left.count(e, isMulti, cmp) + } + //n中承载元素等于e,直接返回结果 + return n.num +} +``` + +#### Find + +​ 以avlTree平衡二叉树做接收者,从搜素二叉树中查找以元素e为索引信息的全部信息,如果找到则返回该二叉树中和索引元素e相同的元素的全部信息,如果未找到则返回nil。 + +```go +func (avl *avlTree) Find(e interface{}) (ans interface{}) { + if avl == nil { + //二叉树为空,返回0 + return 0 + } + if avl.Empty() { + return 0 + } + avl.mutex.Lock() + ans = avl.root.find(e, avl.isMulti, avl.cmp) + avl.mutex.Unlock() + return ans +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中查找元素e并返回其存储的全部信息,如果n节点中承载元素与e不同则根据大小从左右子树查找该元素,如果n节点与该元素相等,则直接返回其信息即可,若未找到该索引信息则返回nil。 + +```go +func (n *node) find(e interface{}, isMulti bool, cmp comparator.Comparator) (ans interface{}) { + if n == nil { + return nil + } + //n中承载元素小于e,从右子树继续查找并返回结果 + if cmp(n.value, e) < 0 { + return n.right.count(e, isMulti, cmp) + } + //n中承载元素大于e,从左子树继续查找并返回结果 + if cmp(n.value, e) > 0 { + return n.left.count(e, isMulti, cmp) + } + //n中承载元素等于e,直接返回结果 + return n.value +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/avlTree" + "sync" +) + +func main() { + bs := avlTree.New(true) + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + bs.Insert(i) + go func() { + bs.Insert(i) + wg.Done() + }() + } + wg.Wait() + fmt.Println("遍历输出所有插入的元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("删除一次平衡二叉树中存在的元素,存在重复的将会被剩下") + for i := 0; i < 10; i++ { + bs.Erase(i) + } + fmt.Println("输出剩余的重复元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 遍历输出所有插入的元素 +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 7 +> 7 +> 8 +> 8 +> 8 +> 8 +> 9 +> 9 +> 9 +> 9 +> 9 +> 10 +> 10 +> 删除一次平衡二叉树中存在的元素,存在重复的将会被剩下 +> 输出剩余的重复元素 +> 7 +> 8 +> 8 +> 8 +> 9 +> 9 +> 9 +> 9 +> 10 +> 10 + +#### 时间开销 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/avlTree" + "time" +) + +func main() { + max:=10000000 + tv := time.Now() + v := make([]interface{},max,max) + num:=0 + for i := 0; i < max; i++ { + if i%2==1{ + num=max-i + }else{ + num=i + } + v[num]=num + } + fmt.Println("插入切片的消耗时间:",time.Since(tv)) + tt := time.Now() + t := avlTree.New(false) + for i := 0; i < max; i++ { + if i%2==1{ + num=max-i + }else{ + num=i + } + t.Insert(num) + } + fmt.Println("插入平衡二叉树的消耗时间:",time.Since(tt)) +} +``` + +> 插入切片的消耗时间: 195.5018ms +> 插入平衡二叉树的消耗时间: 5.6738274s + diff --git a/blog/数据结构STL——golang实现最近最少使用链lru.md b/blog/数据结构STL——golang实现最近最少使用链lru.md new file mode 100644 index 0000000..9d3ed8e --- /dev/null +++ b/blog/数据结构STL——golang实现最近最少使用链lru.md @@ -0,0 +1,262 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ LRU-Least Recently Used,最近最少使用链结构,相对于仅考虑时间因素的FIFO和仅考虑访问频率的LFU,LRU算法可以认为是相对平衡的一种淘汰算法,LRU认为,如果数据最近被访问过,那么将来被访问的概率也会更高,LRU 算法的实现非常简单,维护一个队列,如果某条记录被访问了,则移动到队尾,那么队首则是最近最少访问的数据,淘汰该条记录即可。 + +​ 它适用于作为一个cache缓存,去保存带有索引的数据,对于其要存储的数据值来说,通过实现一个Len()的函数的泛型来表示。 + +### 原理 + +​ 本次实现过程中,采用双向链表的形式做为用于存储值的队列,同时利用map建立key和存储值的链表结点之间的联系。 + +​ 该方案可以加快从队列中找到对应的值并做修改,同时也便于将最近访问的value移到队首。 + +​ 本次实现不做定时淘汰,若要实现定时淘汰可自行创建一个协程同时添加一个map去保存其上次访问时间去完成定时淘汰即可。 + +​ 对于LRU的修改策略来说: + +- 初次添加/访问,直接放到队首 +- 添加后若容量超过上限值则移除最远访问的value,即队尾元素,移除过程直至使用容量低于上限值时才结束 +- 再次访问时从map中找到刚刚访问的结点随后将其移动到队首 +- 删除时直接利用map找到对应的结点进行删除即可 + +### 实现 + +​ LRU链结构体,包含了该LRU结构中能承载的byte数上限和当前已承载的数量,以链表的形式存储承载元素,使用map建立key索引和链表结点之间的联系,链表结点中存放其value,onRemove函数是用于在删除时的执行,可用于将数据持久化,使用并发控制所保证线程安全。 + +```go +type LRU struct { + maxBytes int64 //所能承载的最大byte数量 + nowBytes int64 //当前承载的byte数 + ll *list.List //用于存储的链表 + cache map[string]*list.Element //链表元素与key的映射表 + onRemove func(key string, value Value) //删除元素时的执行函数 + mutex sync.Mutex //并发控制锁 +} +``` + +​ 索引结构体,保存了一个待存储值的索引和值,value值为一个interface{},需要实现它的长度函数即Len()。 + +```go +type indexes struct { + key string //索引 + value Value //存储值 +} +``` + +​ 存储值的函数。 + +```go +type Value interface { + Len() int //用于计算该存储值所占用的长度 +} +``` + +#### 接口 + +```go +type lruer interface { + Size() (num int64) //返回lru中当前存放的byte数 + Cap() (num int64) //返回lru能存放的byte树的最大值 + Clear() //清空lru,将其中存储的所有元素都释放 + Empty() (b bool) //判断该lru中是否存储了元素 + Insert(key string, value Value) //向lru中插入以key为索引的value + Erase(key string) //从lru中删除以key为索引的值 + Get(key string) (value Value, ok bool) //从lru中获取以key为索引的value和是否获取成功? +} +``` + +#### New + +​ 以LRU链容器做接收者,返回该LRU链当前所存储的byte数。 + +```go +func (l *LRU) Size() (num int64) { + if l == nil { + return 0 + } + return l.nowBytes +} +``` + +#### Cap + +​ 以LRU链容器做接收者,返回该LRU链能存储的最大byte数。 + +```go +func (l *LRU) Cap() (num int64) { + if l == nil { + return 0 + } + return l.maxBytes +} +``` + +#### Clear + +​ 以LRU链容器做接收者,将LRU链中的所有承载的元素清除,同时将map清空,将当前存储的byte数清零。 + +```go +func (l *LRU) Clear() { + if l == nil { + return + } + l.mutex.Lock() + l.ll = list.New() + l.cache = make(map[string]*list.Element) + l.nowBytes = 0 + l.mutex.Unlock() +} +``` + +#### Empty + +​ 以LRU链容器做接收者,判断该LRU链中是否存储了元素,没存储则返回true,LRU不存在也返回true,否则返回false。 + +```go +func (l *LRU) Empty() (b bool) { + if l == nil { + return true + } + return l.nowBytes <= 0 +} +``` + +#### Insert + +​ 以LRU链容器做接收者,向该LRU中插入以key为索引的value,若已经存在则将其放到队尾,若空间充足且不存在则直接插入队尾,若空间不足则淘汰队首元素再将其插入队尾,插入完成后将当前已存的byte数增加。 + +```go +func (l *LRU) Insert(key string, value Value) { + l.mutex.Lock() + //利用map从已存的元素中寻找 + if ele, ok := l.cache[key]; ok { + //该key已存在,直接替换即可 + l.ll.MoveToFront(ele) + kv := ele.Value.(*indexes) + //此处是一个替换,即将cache中的value替换为新的value,同时根据实际存储量修改其当前存储的实际大小 + l.nowBytes += int64(value.Len()) - int64(kv.value.Len()) + kv.value = value + } else { + //该key不存在,需要进行插入 + ele := l.ll.PushFront(&indexes{key, value}) + l.cache[key] = ele + //此处是一个增加操作,即原本不存在,所以直接插入即可,同时在当前数值范围内增加对应的占用空间 + l.nowBytes += int64(len(key)) + int64(value.Len()) + } + //添加完成后根据删除掉尾部一部分数据以保证实际使用空间小于设定的空间上限 + for l.maxBytes != 0 && l.maxBytes < l.nowBytes { + //删除队尾元素 + //删除后执行创建时传入的删除执行函数 + ele := l.ll.Back() + if ele != nil { + l.ll.Remove(ele) + kv := ele.Value.(*indexes) + //删除最末尾的同时将其占用空间减去 + delete(l.cache, kv.key) + l.nowBytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if l.onRemove != nil { + //删除后的回调函数,可用于持久化该部分数据 + l.onRemove(kv.key, kv.value) + } + } + } + l.mutex.Unlock() +} +``` + +#### Erase + +​ 以LRU链容器做接收者,从LRU链中删除以key为索引的value,如果不存在则直接结束,删除完成后将其占用的byte数减去,删除后执行函数回调函数,一般用于持久化。 + +```go +func (l *LRU) Erase(key string) { + l.mutex.Lock() + if ele, ok := l.cache[key]; ok { + l.ll.Remove(ele) + kv := ele.Value.(*indexes) + //删除的同时将其占用空间减去 + delete(l.cache, kv.key) + l.nowBytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if l.onRemove != nil { + //删除后的回调函数,可用于持久化该部分数据 + l.onRemove(kv.key, kv.value) + } + } + l.mutex.Unlock() +} +``` + +#### Get + +​ 以LRU链容器做接收者,从LRU链中寻找以key为索引的value,找到对应的value后将其移到首部,随后返回其value,如果没找到以key为索引的value,直接结束即可。 + +```go +func (l *LRU) Get(key string) (value Value, ok bool) { + l.mutex.Lock() + if ele, ok := l.cache[key]; ok { + //找到了value,将其移到链表首部 + l.ll.MoveToFront(ele) + kv := ele.Value.(*indexes) + l.mutex.Unlock() + return kv.value, true + } + l.mutex.Unlock() + return nil, false +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + lru2 "github.com/hlccd/goSTL/data_structure/lru" +) + +type Value struct { + Bytes []byte +} + +func (v Value) Len() int { + return len(v.Bytes) +} +func main() { + space :=0 + lru := lru2.New(2<<10, nil) + for i := 0; i < 10000; i++ { + v := Value{Bytes: []byte(string(i))} + lru.Insert(string(i), v) + space +=v.Len() + } + fmt.Println("应该占用空间:",space) + fmt.Println("LRU中存放的byte数量:",lru.Size()) + fmt.Println("LRU的byte数量上限:",lru.Cap()) + lru.Erase(string(9999)) + fmt.Println("删除后的LRU中存放的byte数量:",lru.Size()) + fmt.Println("从LRU中找9998") + if v,ok:=lru.Get(string(9998));ok{ + fmt.Println(string(v.(Value).Bytes)) + fmt.Println(string(9998)) + } + fmt.Println("从LRU中找1") + if v,ok:=lru.Get(string(1));ok{ + fmt.Println(string(v.(Value).Bytes)) + fmt.Println(string(1)) + } +} +``` + +> 应该占用空间: 27824 +> LRU中存放的byte数量: 2046 +> LRU的byte数量上限: 2048 +> 删除后的LRU中存放的byte数量: 2040 +> 从LRU中找9998 +> ✎ +> ✎ +> 从LRU中找1 +> +> diff --git a/blog/数据结构STL——golang实现栈stack.md b/blog/数据结构STL——golang实现栈stack.md new file mode 100644 index 0000000..80bc6ab --- /dev/null +++ b/blog/数据结构STL——golang实现栈stack.md @@ -0,0 +1,298 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 栈(stack)是一个**线性容器**,但不同于其他容器的特点在于,栈是仅仅支持从顶部插入和从顶部删除的操作,即**单向删除和添加**。 + +​ 对于stack的实现,考虑到它是一个线性容器,并且其中的元素仅可通过顶部添加和删除即单项增删,所以可以考虑其**底层使用动态数组**的形式实现,考虑到动态数组需要自行对分配空间进行操作,同时它也类似于vector进行单项操作的情况,所以它的扩容缩容策略同vector一样,但不同点在于它只能使用单侧增删,所以不需要考虑从中间插入的一些情况。 + +### 原理 + +​ 对于一个栈来说,可以将其看作是一个**木桶**,你要在这个容器里放入一些元素,就只能从已有元素的顶部放入,而不是插入的原有的元素之间去,当你要删除元素的时候,也只能从顶部一个一个删除。 + +​ 它是可以动态的对元素进行添加和修改的,而如果说每一次增删元素都使得数组恰好可以容纳所有元素的话,那它在以后的每一次增删的过程中都需要去**重新分配空间**,并将原有的元素复制到新数组中,如果按照这样来操作的话,每次删减都会需要花费大量的时间去进行,将会极大的降低其性能。 + +​ 为了提高效率,可以选择牺牲一定的空间作为冗余量,即**时空权衡**,通过每次扩容时多分配一定的空间,这样在后续添加时就可以减少扩容次数,而在删除元素时,如果冗余量不多时,可以选择不进行缩容,仅将顶部指针下移一位即可,,这样可以极大减少的由于容量不足或超出导致的扩容缩容造成的时间开销。 + +#### 扩容策略 + +​ 对于动态数组扩容来说,由于其添加是线性且连续的,虽然stack一次只会添加一个,但为了减少频繁分配空间造成的时间浪费,选择多保留一些冗余空间以方便后续增删操作。 + +​ stack的扩容主要由两种方案: + +**同vector的扩容策略** + +1. 固定扩容:固定的去增加一定量的空间,该方案时候在数组较大时添加空间,可以避免直接翻倍导致的**冗余过量**问题,到由于增加量是固定的,如果需要一次扩容很多量的话就会比较缓慢。 +2. 翻倍扩容:将原有容量直接翻倍,该方案适合在数组不太大的适合添加空间,可以提高扩容量,增加扩容效率,但数组太大时使用的话会导致一次性增加太多空间,进而造成空间的浪费。 + +#### 缩容策略 + +​ 对于动态数组的缩容来说,同扩容类似,由于元素的减少只会是线性且连续的,即每一次只会减少一个,不会存在由于一个元素被删去导致其他已使用的空间也需要被释放。所以stack的缩容方案和stack的扩容方案十分类似,即是它的逆过程: + +**同vector的缩容策略** + +1. 固定缩容:释放一个固定的空间,该方案适合在当前数组较大的时候进行,可以减缓需要缩小的量,当空间较大的时候如果采取折半缩减的话;将可能在绝大多数时间内都不会被采用,从而造成空间浪费。 +2. 折半缩容:将当前容量进行折半,该方案适合数组不太大的适合进行缩容,可以更快的缩小容量,但对于较大的空间来说并不会频繁使用到。 + +### 实现 + +​ stack栈结构体,包含动态数组和该数组的顶部指针,顶部指针指向实际顶部元素的下一位置,当删除节点时仅仅需要下移顶部指针一位即可,当新增结点时优先利用冗余空间,当冗余空间不足时先倍增空间至2^16,超过后每次增加2^16的空间,删除结点后如果冗余超过2^16,则释放掉,删除后若冗余量超过使用量,也释放掉冗余空间。 + +```go +type stack struct { + data []interface{} //用于存储元素的动态数组 + top uint64 //顶部指针 + cap uint64 //动态数组的实际空间 + mutex sync.Mutex //并发控制锁 +} +``` + +#### 接口 + +```go +type stacker interface { + Iterator() (i *Iterator.Iterator) //返回一个包含栈中所有元素的迭代器 + Size() (num uint64) //返回该栈中元素的使用空间大小 + Clear() //清空该栈容器 + Empty() (b bool) //判断该栈容器是否为空 + Push(e interface{}) //将元素e添加到栈顶 + Pop() //弹出栈顶元素 + Top() (e interface{}) //返回栈顶元素 +} +``` + +#### New + +​ 新建一个stack栈容器并返回,初始stack的动态数组容量为1,初始stack的顶部指针置0,容量置1。 + +```go +func New() (s *stack) { + return &stack{ + data: make([]interface{}, 1, 1), + top: 0, + cap: 1, + mutex: sync.Mutex{}, + } +} +``` + +#### Iterator + +​ 以stack栈容器做接收者,将stack栈容器中不使用空间释放掉,返回一个包含容器中所有使用元素的迭代器。 + +```go +func (s *stack) Iterator() (i *Iterator.Iterator) { + if s == nil { + s = New() + } + s.mutex.Lock() + if s.data == nil { + //data不存在,新建一个 + s.data = make([]interface{}, 1, 1) + s.top = 0 + s.cap = 1 + } else if s.top < s.cap { + //释放未使用的空间 + tmp := make([]interface{}, s.top, s.top) + copy(tmp, s.data) + s.data = tmp + } + //创建迭代器 + i = Iterator.New(&s.data) + s.mutex.Unlock() + return i +} +``` + +#### Size + +​ 以stack栈容器做接收者吗,返回该容器当前含有元素的数量。 + +```go +func (s *stack) Size() (num uint64) { + if s == nil { + s = New() + } + return s.top +} +``` + +#### Clear + +​ 以stack栈容器做接收者,将该容器中所承载的元素清空,将该容器的尾指针置0。 + +```go +func (s *stack) Clear() { + if s == nil { + s = New() + } + s.mutex.Lock() + s.data = make([]interface{}, 0, 0) + s.top = 0 + s.cap = 1 + s.mutex.Unlock() +} +``` + +#### Empty + +​ 以stack栈容器做接收者,判断该stack栈容器是否含有元素,如果含有元素则不为空,返回false, 如果不含有元素则说明为空,返回true,如果容器不存在,返回true,该判断过程通过顶部指针数值进行判断,当顶部指针数值为0时说明不含有元素,当顶部指针数值大于0时说明含有元素。 + +```go +func (s *stack) Empty() (b bool) { + if s == nil { + return true + } + return s.Size() == 0 +} +``` + +#### Push + +​ 以stack栈容器做接收者,在容器顶部插入元素,若存储冗余空间,则在顶部指针位插入元素,随后上移顶部指针,否则进行扩容,扩容后获得冗余空间重复上一步即可。 + +​ 固定扩容值设为2^16,翻倍扩容上限也为2^16。 + +```go +func (s *stack) Push(e interface{}) { + if s == nil { + s = New() + } + s.mutex.Lock() + if s.top < s.cap { + //还有冗余,直接添加 + s.data[s.top] = e + } else { + //冗余不足,需要扩容 + if s.cap <= 65536 { + //容量翻倍 + if s.cap == 0 { + s.cap = 1 + } + s.cap *= 2 + } else { + //容量增加2^16 + s.cap += 65536 + } + //复制扩容前的元素 + tmp := make([]interface{}, s.cap, s.cap) + copy(tmp, s.data) + s.data = tmp + s.data[s.top] = e + } + s.top++ + s.mutex.Unlock() +} +``` + +#### Pop + +​ 以stack栈容器做接收者,弹出容器顶部元素,同时顶部指针下移一位,当顶部指针小于容器切片实际使用空间的一半时,重新分配空间释放未使用部分,若容器为空,则不进行弹出。 + +```go +func (s *stack) Pop() { + if s == nil { + s = New() + return + } + if s.Empty() { + return + } + s.mutex.Lock() + s.top-- + if s.cap-s.top >= 65536 { + //容量和实际使用差值超过2^16时,容量直接减去2^16 + s.cap -= 65536 + tmp := make([]interface{}, s.cap, s.cap) + copy(tmp, s.data) + s.data = tmp + } else if s.top*2 < s.cap { + //实际使用长度是容量的一半时,进行折半缩容 + s.cap /= 2 + tmp := make([]interface{}, s.cap, s.cap) + copy(tmp, s.data) + s.data = tmp + } + s.mutex.Unlock() +} +``` + +#### Top + +​ 以stack栈容器做接收者,返回该容器的顶部元素,若该容器当前为空,则返回nil。 + +```go +func (s *stack) Top() (e interface{}) { + if s == nil { + return nil + } + if s.Empty() { + return nil + } + s.mutex.Lock() + e = s.data[s.top-1] + s.mutex.Unlock() + return e +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/stack" + "sync" +) + +func main() { + s := stack.New() + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func(num int) { + s.Push(num) + wg.Done() + }(i) + } + wg.Wait() + fmt.Println("使用迭代器遍历全部:") + for i := s.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("使用size变删除边遍历:") + size:=s.Size() + for i := uint64(0); i < size; i++ { + fmt.Println(s.Top()) + s.Pop() + } +} +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 使用迭代器遍历全部: +> 9 +> 3 +> 4 +> 5 +> 6 +> 7 +> 8 +> 0 +> 1 +> 2 +> 使用size变删除边遍历: +> 2 +> 1 +> 0 +> 8 +> 7 +> 6 +> 5 +> 4 +> 3 +> 9 + diff --git a/blog/数据结构STL——golang实现树堆Treap.md b/blog/数据结构STL——golang实现树堆Treap.md new file mode 100644 index 0000000..69eb43a --- /dev/null +++ b/blog/数据结构STL——golang实现树堆Treap.md @@ -0,0 +1,648 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 树堆(Treap)是一个比较特殊的结构,它同之前实现的二叉搜索树和完全二叉搜有着类似的性质,它**即满足二叉搜索树的查找性质,又满足完全二叉树的极值处于堆顶的性质**。 + +​ 树堆由一个根节点和根节点下属的多层次的子结点构成,任意一个结点最多只能拥有两个子结点,即左右子结点。它的这一特性**同二叉搜索树完全一致**,但除此之外,它还有另一个特质:给每一个结点赋予一个随机的优先级,使得其在插入的时候,通过**左右旋转**的方式,保证其满足二叉搜索树的性质,同时使得优先级更大的结点旋转至更靠近顶部的方式,即**满足完全二叉树的性质**。 + +​ 除开以上两点之外,它还有一个比较特殊的优势,考虑到所有结点的优先级都是随机生成的,即其优先级和其结点值之前完全没有关系,故只要随机算法合理的情况下,整个树堆的形状就会更加扁平,从概率上来说,当插入结点数量够多的情况下,**增加和删除结点的时间复杂的仅为O(logn)**,即实现了**依概率平衡**。 + +### 原理 + +​ 对于一个树堆来说,它需要满足的条件主要有两条: + +1. 父结点的值必然大于左结点小于右结点 +2. 父结点的优先级必然大于左右两结点 + +​ 而为了实现这两个情况,我们可以考虑先实现分别实现第一点和第二点。对于第一点来说其实就是指的**二叉搜索树**的性质,第二点指的是完全二叉树的,考虑到之前的文章已经提及,便不在此处赘述。 + +​ 于是,现在的核心问题就变成了,如何在满足二叉搜索树的性质的情况下,使得它同时也满足完全二叉树的性质。 + +​ 我们考虑这样一种情况,对于一个二叉搜索树来说,父结点的左结点的值必然是小于父结点同时也小于右结点的,对应的,父结点的右结点的值必然也是大于父结点同时也大于左结点的,所以,对于前一个情况来说,将父结点设为右结点的左结点,同时将父结点的右结点设为右节点的左节点,其大小情况是保持不变的;对应的,将父结点设为左结点的右结点,同时将父结点的左结点设为左节点的右节点,其大小情况仍然是满足二叉搜索树的条件的。 + +​ 以上两种情况分别是**右旋和左旋**,到此时可以知道,可以通过左旋或右旋的方式,保持该结构仍然满足二叉搜索树的特征的同时,转换结点的位置。 + +​ 同时我们可以知道,对于一个完全二叉树来说,即对于一个堆来说,它的核心特征是父结点的优先级必然大于左右两个结点,所以只需要通过旋转的方式满足该特征即可,即当父结点的左结点的优先级大于父结点的时候,进行左旋,当父结点的右节点的优先级大于父结点的时候进行右旋,当左右结点的优先级都大于父结点时,选择更大的一侧旋转即可。 + +​ 到此,即可根据左右旋转的方式同时满足二叉搜索树和完全二叉树的性质了,只不过不同点在于,满足二叉搜索树的值是结点承载的值,而满足完全二叉树的优先级并非承载值,而是随机生成的值,同时由于其是随机生成的,从概率上来说,当数量足够大的情况下,可以实现O(logn)级别的增删查。 + +#### 添加策略 + +​ 对于树堆的添加元素的情况来说,需要的情况主要可以分为该元素是否已经存在于树堆中,如果已经存在,则只需要将结点的num+1即可,否则则需要新增结点,同时赋予一个随机的优先级,再根据情况左右旋转以满足性质。 + +​ 步骤: + +1. 先以根节点为父结点,递归找到等于该元素的结点位置,若插入元素小于当前父结点元素,则转入左子树,否则转入右子树,以此方式找到对应的插入位置 +2. 找到后,如果该元素已经存在,则结点num+1即可,否则新建结点,同时建立父结点和新结点之间的关系,此时,整个结构是满足于二叉搜索树的性质的 +3. 随后,根据新增结点的优先级和其父结点的优先级进行判断,如果大于父结点优先级则进行对应方向的旋转即可。 + +#### 删除策略 + +​ 对于树堆的元素删除来说,首先要做的还是先找到要删除的结点,如果没找到则可以之间结束。找到后需要判断该节点是否存在子结点,如果不存在则可以直接删除,否则,如果右左子树则找左子树的前缀节点即左子树的最右结点,如果只有右子树则朝气后缀结点即右子树的最右结点,将寻找到的结点替换为当前父结点,然后删除被替换的结点即可。但对于左右两个结点都存在的情况来说,则找到优先级更小的一侧进行旋转,然后将结点转入优先级更小的一侧结点进行持续递归,直到找到一个只有左节点或只有右节点或无结点的情况,对于存在结点的情况来说,把其子结点替换为自己即可,对于无结点的情况来说,删除即可。 + +​ 树堆删除结点的步骤: + +1. 通过递归比对遍历寻找到要删除的结点 +2. 如果该结点不存在则直接结束,如果有重复则num-1即可结束 +3. 如果同时有左右子树,转换为优先级更小的一侧,然后进行递归直到不同时存在左右子树为止 +4. 如果有左子树,将左子树替换为自己即可 +5. 如果只有右子树,将右子树替换为自己即可 +6. 如果无子树,直接删除即可 + +### 实现 + +​ treap树堆结构体,该实例存储树堆的根节点,同时保存该树堆中已经存储了多少个元素,二叉树中排序使用的比较器在创建时传入,若不传入则在插入首个节点时从默认比较器中寻找,该树堆实例中存储随机数生成器,用于后续新建节点时生成随机数,创建时传入是否允许该树堆出现重复值,如果不允许则进行覆盖,允许则对节点数目增加即可。 + +```go +type treap struct { + root *node //根节点指针 + size int //存储元素数量 + cmp comparator.Comparator //比较器 + rand *rand.Rand //随机数生成器 + isMulti bool //是否允许重复 + mutex sync.Mutex //并发控制锁 +} +``` + +​ node树节点结构体,该节点是树堆的树节点,若该树堆允许重复则对节点num+1即可,否则对value进行覆盖,树堆节点将针对堆的性质通过左右旋转的方式做平衡。 + +```go +type node struct { + value interface{} //节点中存储的元素 + priority uint32 //该节点的优先级,随机生成 + num int //该节点中存储的数量 + left *node //左节点指针 + right *node //右节点指针 +} +``` + +#### 接口 + +```go +type treaper interface { + Iterator() (i *Iterator.Iterator) //返回包含该树堆的所有元素,重复则返回多个 + Size() (num int) //返回该树堆中保存的元素个数 + Clear() //清空该树堆 + Empty() (b bool) //判断该树堆是否为空 + Insert(e interface{}) //向树堆中插入元素e + Erase(e interface{}) //从树堆中删除元素e + Count(e interface{}) (num int) //从树堆中寻找元素e并返回其个数 +} +``` + +#### New + +​ 新建一个treap树堆容器并返回,初始根节点为nil,传入该树堆是否为可重复属性,如果为true则保存重复值,否则对原有相等元素进行覆盖,若有传入的比较器,则将传入的第一个比较器设为该树堆的比较器。 + +```go +func New(isMulti bool, Cmp ...comparator.Comparator) (t *treap) { + //设置默认比较器 + var cmp comparator.Comparator + if len(Cmp) > 0 { + cmp = Cmp[0] + } + //创建随机数生成器 + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return &treap{ + root: nil, + size: 0, + cmp: cmp, + rand: r, + isMulti: isMulti, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个树堆节点并返回,将传入的元素e作为该节点的承载元素,该节点的num默认为1,左右子节点设为nil,该节点优先级随机生成,范围在0~2^16内。 + +```go +func newNode(e interface{}, rand *rand.Rand) (n *node) { + return &node{ + value: e, + priority: uint32(rand.Intn(4294967295)), + num: 1, + left: nil, + right: nil, + } +} +``` + +#### Iterator + +​ 以treap树堆做接收者,将该树堆中所有保存的元素将从根节点开始以中缀序列的形式放入迭代器中,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (t *treap) Iterator() (i *Iterator.Iterator) { + if t == nil { + return nil + } + t.mutex.Lock() + es := t.root.inOrder() + i = Iterator.New(&es) + t.mutex.Unlock() + return i +} +``` + +​ 以node树堆节点做接收者,以中缀序列返回节点集合,若允许重复存储则对于重复元素进行多次放入。 + +```go +func (n *node) inOrder() (es []interface{}) { + if n == nil { + return es + } + if n.left != nil { + es = append(es, n.left.inOrder()...) + } + for i := 0; i < n.num; i++ { + es = append(es, n.value) + } + if n.right != nil { + es = append(es, n.right.inOrder()...) + } + return es +} +``` + +#### Size + +​ 以treap树堆做接收者,返回该容器当前含有元素的数量,如果容器为nil返回0。 + +```go +func (t *treap) Size() (num int) { + if t == nil { + return 0 + } + return t.size +} +``` + +#### Clear + +​ 以treap树堆做接收者,将该容器中所承载的元素清空,将该容器的size置0。 + +```go +func (t *treap) Clear() { + if t == nil { + return + } + t.mutex.Lock() + t.root = nil + t.size = 0 + t.mutex.Unlock() +} +``` + +#### Empty + +​ 以treap树堆做接收者,判断该二叉搜索树是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true。 + +```go +func (t *treap) Empty() (b bool) { + if t == nil { + return true + } + if t.size > 0 { + return false + } + return true +} +``` + +#### rightRotate + +​ 以node树堆节点做接收者,新建一个节点作为n节点的右节点,同时将n节点的数值放入新建节点中作为右转后的n节点,右转后的n节点的左节点是原n节点左节点的右节点,右转后的右节点保持不变,原n节点改为原n节点的左节点,同时右节点指向新建的节点即右转后的n节点,该右转方式可以保证n节点的双亲节点不用更换节点指向。 + +```go +func (n *node) rightRotate() { + if n == nil { + return + } + if n.left == nil { + return + } + //新建节点作为更换后的n节点 + tmp := &node{ + value: n.value, + priority: n.priority, + num: n.num, + left: n.left.right, + right: n.right, + } + //原n节点左节点上移到n节点位置 + n.right = tmp + n.value = n.left.value + n.priority = n.left.priority + n.num = n.left.num + n.left = n.left.left +} +``` + +#### leftRotate + +​ 以node树堆节点做接收者,新建一个节点作为n节点的左节点,同时将n节点的数值放入新建节点中作为左转后的n节点,左转后的n节点的右节点是原n节点右节点的左节点,左转后的左节点保持不变,原n节点改为原n节点的右节点,同时左节点指向新建的节点即左转后的n节点,该左转方式可以保证n节点的双亲节点不用更换节点指向。 + +```go +func (n *node) leftRotate() { + if n == nil { + return + } + if n.right == nil { + return + } + //新建节点作为更换后的n节点 + tmp := &node{ + value: n.value, + priority: n.priority, + num: n.num, + left: n.left, + right: n.right.left, + } + //原n节点右节点上移到n节点位置 + n.left = tmp + n.value = n.right.value + n.priority = n.right.priority + n.num = n.right.num + n.right = n.right.right +} +``` + +#### Insert + +​ 以treap树堆做接收者,向二叉树插入元素e,若不允许重复则对相等元素进行覆盖,如果二叉树为空则之间用根节点承载元素e,否则以根节点开始进行查找,对于该树堆来说,通过赋予随机的优先级根据堆的性质来实现平衡。 + +```go +func (t *treap) Insert(e interface{}) { + //判断容器是否存在 + if t == nil { + return + } + t.mutex.Lock() + if t.Empty() { + //判断比较器是否存在 + if t.cmp == nil { + t.cmp = comparator.GetCmp(e) + } + if t.cmp == nil { + t.mutex.Unlock() + return + } + //插入到根节点 + t.root = newNode(e, t.rand) + t.size = 1 + t.mutex.Unlock() + return + } + //从根节点向下插入 + if t.root.insert(newNode(e, t.rand), t.isMulti, t.cmp) { + t.size++ + } + t.mutex.Unlock() +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中插入元素e,如果n节点中承载元素与e不同则根据大小从左右子树插入该元素,如果n节点与该元素相等,且允许重复值,则将num+1否则对value进行覆盖,插入成功返回true,插入失败或不允许重复插入返回false。 + +```go +func (n *node) insert(e *node, isMulti bool, cmp comparator.Comparator) (b bool) { + if cmp(n.value, e.value) > 0 { + if n.left == nil { + //将左节点直接设为e + n.left = e + b = true + } else { + //对左节点进行递归插入 + b = n.left.insert(e, isMulti, cmp) + } + if n.priority > e.priority { + //对n节点进行右转 + n.rightRotate() + } + return b + } else if cmp(n.value, e.value) < 0 { + if n.right == nil { + //将右节点直接设为e + n.right = e + b = true + } else { + //对右节点进行递归插入 + b = n.right.insert(e, isMulti, cmp) + } + if n.priority > e.priority { + //对n节点进行左转 + n.leftRotate() + } + return b + } + if isMulti { + //允许重复 + n.num++ + return true + } + //不允许重复,对值进行覆盖 + n.value = e.value + return false +} +``` + +#### Erase + +​ 以treap树堆做接收者,从树堆中删除元素e,若允许重复记录则对承载元素e的节点中数量记录减一即可,若不允许重复记录则删除该节点同时将前缀节点或后继节点更换过来以保证树堆的不发送断裂,交换后根据优先级进行左右旋转以保证符合堆的性质,如果该树堆仅持有一个元素且根节点等价于待删除元素,则将根节点置为nil。 + +```go +func (t *treap) Erase(e interface{}) { + if t == nil { + return + } + if t.Empty() { + return + } + t.mutex.Lock() + if t.size == 1 && t.cmp(t.root.value, e) == 0 { + //该树堆仅持有一个元素且根节点等价于待删除元素,则将根节点置为nil + t.root = nil + t.size = 0 + t.mutex.Unlock() + return + } + //从根节点开始删除元素 + if t.root.delete(e, t.isMulti, t.cmp) { + //删除成功 + t.size-- + } + t.mutex.Unlock() +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中删除元素e,如果n节点中承载元素与e不同则根据大小从左右子树删除该元素,如果n节点与该元素相等,且允许重复值,则将num-1否则直接删除该元素,删除时先寻找该元素的前缀节点,若不存在则寻找其后继节点进行替换,替换后删除该节点。 + +```go +func (n *node) delete(e interface{}, isMulti bool, cmp comparator.Comparator) (b bool) { + if n == nil { + return false + } + //n中承载元素小于e,从右子树继续删除 + if cmp(n.value, e) < 0 { + if n.right == nil { + //右子树为nil,删除终止 + return false + } + if cmp(e, n.right.value) == 0 && (!isMulti || n.right.num == 1) { + //待删除节点无子节点,直接删除即可 + if n.right.left == nil && n.right.right == nil { + //右子树可直接删除 + n.right = nil + return true + } + } + //从右子树继续删除 + return n.right.delete(e, isMulti, cmp) + } + //n中承载元素大于e,从左子树继续删除 + if cmp(n.value, e) > 0 { + if n.left == nil { + //左子树为nil,删除终止 + return false + } + if cmp(e, n.left.value) == 0 && (!isMulti || n.left.num == 1) { + //待删除节点无子节点,直接删除即可 + if n.left.left == nil && n.left.right == nil { + //左子树可直接删除 + n.left = nil + return true + } + } + //从左子树继续删除 + return n.left.delete(e, isMulti, cmp) + } + if isMulti && n.num > 1 { + //允许重复且数量超过1 + n.num-- + return true + } + //删除该节点 + tmp := n + //左右子节点都存在则选择优先级较小一个进行旋转 + for tmp.left != nil && tmp.right != nil { + if tmp.left.priority < tmp.right.priority { + tmp.rightRotate() + if tmp.right.left == nil && tmp.right.right == nil { + tmp.right = nil + return false + } + tmp = tmp.right + } else { + tmp.leftRotate() + if tmp.left.left == nil && tmp.left.right == nil { + tmp.left = nil + return false + } + tmp = tmp.left + } + } + if tmp.left == nil && tmp.right != nil { + //到左子树为nil时直接换为右子树即可 + tmp.value = tmp.right.value + tmp.num = tmp.right.num + tmp.priority = tmp.right.priority + tmp.left = tmp.right.left + tmp.right = tmp.right.right + } else if tmp.right == nil && tmp.left != nil { + //到右子树为nil时直接换为左子树即可 + tmp.value = tmp.left.value + tmp.num = tmp.left.num + tmp.priority = tmp.left.priority + tmp.right = tmp.left.right + tmp.left = tmp.left.left + } + //当左右子树都为nil时直接结束 + return true +} +``` + +#### Count + +​ 以treap树堆做接收者,从树堆中查找元素e的个数,如果找到则返回该树堆中和元素e相同元素的个数,如果不允许重复则最多返回1,如果未找到则返回0。 + +```go +func (t *treap) Count(e interface{}) (num int) { + if t == nil { + //树堆不存在,直接返回0 + return 0 + } + if t.Empty() { + return + } + t.mutex.Lock() + num = t.root.search(e, t.cmp) + t.mutex.Unlock() + //树堆存在,从根节点开始查找该元素 + return num +} +``` + +​ 以node二叉搜索树节点做接收者,从n节点中查找元素e并返回存储的个数,如果n节点中承载元素与e不同则根据大小从左右子树查找该元素,如果n节点与该元素相等,则直接返回其个数。 + +```go +func (n *node) search(e interface{}, cmp comparator.Comparator) (num int) { + if n == nil { + return 0 + } + if cmp(n.value, e) > 0 { + return n.left.search(e, cmp) + } else if cmp(n.value, e) < 0 { + return n.right.search(e, cmp) + } + return n.num +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/treap" + "sync" +) + +func main() { + bs := treap.New(true) + wg := sync.WaitGroup{} + for i := 0; i < 20; i++ { + wg.Add(1) + bs.Insert(i) + go func() { + bs.Insert(i) + wg.Done() + }() + } + wg.Wait() + fmt.Println("遍历输出所有插入的元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + fmt.Println("删除一次树堆中存在的元素,存在重复的将会被剩下") + for i := 0; i < 20; i++ { + bs.Erase(i) + } + fmt.Println("输出剩余的重复元素") + for i := bs.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 遍历输出所有插入的元素 +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 7 +> 8 +> 8 +> 9 +> 10 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 12 +> 12 +> 13 +> 14 +> 15 +> 16 +> 16 +> 16 +> 16 +> 16 +> 17 +> 17 +> 18 +> 19 +> 20 +> 20 +> 20 +> 删除一次树堆中存在的元素,存在重复的将会被剩下 +> 输出剩余的重复元素 +> 8 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 11 +> 12 +> 16 +> 16 +> 16 +> 16 +> 17 +> 20 +> 20 +> 20 + +#### 时间开销 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/treap" + "time" +) + +func main() { + max:=10000000 + tv := time.Now() + v := make([]interface{},max,max) + num:=0 + for i := 0; i < max; i++ { + if i%2==1{ + num=max-i + }else{ + num=i + } + v[num]=num + } + fmt.Println("插入切片的消耗时间:",time.Since(tv)) + tt := time.Now() + t := treap.New(false) + for i := 0; i < max; i++ { + if i%2==1{ + num=max-i + }else{ + num=i + } + t.Insert(num) + } + fmt.Println("插入树堆的消耗时间:",time.Since(tt)) +} + +``` + +> 插入切片的消耗时间: 201.4934ms +> 插入树堆的消耗时间: 7.0841462s diff --git a/blog/数据结构STL——golang实现比较器comparator.md b/blog/数据结构STL——golang实现比较器comparator.md new file mode 100644 index 0000000..9f35968 --- /dev/null +++ b/blog/数据结构STL——golang实现比较器comparator.md @@ -0,0 +1,727 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### Comparator + +#### 概述 + +​ 对于某些存在大小比较的数据结构,如果每次都要特定的实现一些大小比较是十分繁琐的,特别是对于一些官方已经设定的类型,如果将基本类型引入数据结构中时需要实现其元素的比较会简单。 + +​ 同时,对于一些常用的函数,比如排序、查找、排序第n个以及寻找上下边界的函数,这些函数需要通过比较器进行配合实现,为了更进一步简化使用,可以在比较器中实现。 + +#### 定义 + +对于一个比较器,除开基本类型外,必须传入比较函数,当然,基本数据类型也可以传入自定的比较函数进行覆盖,对于待使用的比较函数,需要传入两个元素a和b(有先后),同时返回一个int,其中**0表示相等,正数表示a>b,负数表示a b.(int) { + return 1 + } else if a.(int) < b.(int) { + return -1 + } + return 0 +} +func int8Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(int8) > b.(int8) { + return 1 + } else if a.(int8) < b.(int8) { + return -1 + } + return 0 +} +func uint8Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(uint8) > b.(uint8) { + return 1 + } else if a.(uint8) < b.(uint8) { + return -1 + } + return 0 +} +func int16Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(int16) > b.(int16) { + return 1 + } else if a.(int16) < b.(int16) { + return -1 + } + return 0 +} +func uint16Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(uint16) > b.(uint16) { + return 1 + } else if a.(uint16) < b.(uint16) { + return -1 + } + return 0 +} +func int32Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(int32) > b.(int32) { + return 1 + } else if a.(int32) < b.(int32) { + return -1 + } + return 0 +} +func uint32Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(uint32) > b.(uint32) { + return 1 + } else if a.(uint32) < b.(uint32) { + return -1 + } + return 0 +} +func int64Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(int64) > b.(int64) { + return 1 + } else if a.(int64) < b.(int64) { + return -1 + } + return 0 +} +func uint64Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(uint64) > b.(uint64) { + return 1 + } else if a.(uint64) < b.(uint64) { + return -1 + } + return 0 +} +func float32Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(float32) > b.(float32) { + return 1 + } else if a.(float32) < b.(float32) { + return -1 + } + return 0 +} +func float64Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if a.(float64) > b.(float64) { + return 1 + } else if a.(float64) < b.(float64) { + return -1 + } + return 0 +} +func complex64Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if real(a.(complex64)) > real(b.(complex64)) { + return 1 + } else if real(a.(complex64)) < real(b.(complex64)) { + return -1 + } else { + if imag(a.(complex64)) > imag(b.(complex64)) { + return 1 + } else if imag(a.(complex64)) < imag(b.(complex64)) { + return -1 + } + } + return 0 +} +func complex128Cmp(a, b interface{}) int { + if a == b { + return 0 + } + if real(a.(complex128)) > real(b.(complex128)) { + return 1 + } else if real(a.(complex128)) < real(b.(complex128)) { + return -1 + } else { + if imag(a.(complex128)) > imag(b.(complex128)) { + return 1 + } else if imag(a.(complex128)) < imag(b.(complex128)) { + return -1 + } + } + return 0 +} +func stringCmp(a, b interface{}) int { + if a == b { + return 0 + } + if len(a.(string)) > len(b.(string)) { + return 1 + } else if len(a.(string)) < len(b.(string)) { + return -1 + } else { + if a.(string) > b.(string) { + return 1 + } else if a.(string) < b.(string) { + return -1 + } + } + return 0 +} +``` + +#### GetEqual + +获取默认的相等器。 + +```go +func GetEqual() (equ Equaler) { + return basicEqual +} +``` + +##### basicEqual + +​ 默认的相等器,即两个元素完全相等。 + +```go +func basicEqual(a, b interface{}) (B bool) { + return a == b +} +``` + +#### Sort + +​ 排序,对于传入的数组,通过传入的比较函数(基本类型可通过GetCmp直接获取,不需要传入)进行排序。 + +​ 为了简化实现,可以将待比较元素组限定为线性数组,而对于一些非线性结构,可以通过将其指针列为线性表,再通过传入的比较函数进行比较即可,但一般不建议非线性结构使用比较器,其结构最好自己去维护大小分布,这样可以更好的提高效率。 + +​ 排序中分别实现了**二分排序**和**归并排序**,由于二分排序本身不稳定,所以更加适合数据量不太大的数组,而对于归并排序,其性能是十分稳定的,更加适合用于数据量较大的数组,所以对待排序数组根据长度进行了区分,以根据情况使用合适的排序方式。 + +​ 传入的数组是其**指针**,传入指针可以减少复制的情况,以节约时间。 + +```go +func Sort(arr *[]interface{}, Cmp ...Comparator) { + //如果传入一个空数组或nil,则直接结束 + if arr==nil || (*arr)==nil || len((*arr)) == 0 { + return + } + var cmp Comparator + cmp = nil + if len(Cmp) > 0 { + cmp = Cmp[0] + } else { + cmp = GetCmp((*arr)[0]) + } + if cmp == nil { + //未传入比较比较函数且并非默认类型导致未找到默认比较器则直接终止排序 + return + } + //根据数组长度进行分类选择排序函数 + if len((*arr)) < 2^26 { + //当长度小于2^16时使用二分排序 + binary(arr,0,len((*arr))-1, cmp) + } else { + merge(arr,0,len((*arr))-1, cmp) + } +} +``` + +##### Binary + +​ 对于二分排序,其原理主要是将一个无须数组,通过寻找一个中间量(一般是数组中间点的值)作为参考,通过比较和交换使得数组中间量左侧始终不大于中间量,右侧始终不小于中间量,即**相对有序的状态**,再对两侧的情况进行递归排序,从而使得每一部分都是相对有序的,依次保证整体的有序性。 + +​ 当中间值取的较差甚至是极值的时候,二分排序将会退化为冒泡排序,该排序方案并不稳定,但由于不需要额外空间进行存储,所以可以在较小的数组中进行使用。 + +```go +func binary(arr *[]interface{},l,r int, cmp Comparator) { + //对当前部分进行预排序,使得两侧都大于或小于中间值 + if l >= r { + return + } + m := (*arr)[(r + l) / 2] + i, j := l-1, r+1 + for i < j { + i++ + for cmp((*arr)[i], m) < 0 { + i++ + } + j-- + for cmp((*arr)[j],m) > 0 { + j-- + } + if i < j { + (*arr)[i],(*arr)[j]=(*arr)[j],(*arr)[i] + } + } + //对分好的两侧进行迭代二分排序 + binary(arr,l,j, cmp) + binary(arr,j+1,r, cmp) +} +``` + +##### Merge + +​ 对于归并排序,其原理主要是将一个无须数组,先通过对数组的左右拆分,随后从最小部分(一般是仅存在两个或一个元素)进行初步排序,然后依次向上合并,由于合并的两个小数组的内部都是有序的,所以只需要依次遍历比较其大小即可,与此同时,需要一个临时数组去存储比较结果,随后再将临时数组中的值放入待排序的数组中去,依次合并到整个数组来保证其有序性。 + +​ 由于该方案是将数组逐步拆分为最小单元在进行合并,其情况必然是相对稳定的,虽然需要一定的额外空间进行存储,但相较于其稳定性来说也是值得的空间成本。 + +```go +func merge(arr *[]interface{},l,r int, cmp Comparator) { + //对当前部分进行分组排序,将该部分近似平均的拆为两部分进行比较排序 + if l >= r { + return + } + m := (r + l) / 2 + //对待排序内容进行二分 + merge(arr,l,m, cmp) + merge(arr,m+1,r, cmp) + //二分结束后依次比较进行归并 + i, j := l, m+1 + var tmp []interface{}=make([]interface{},0,r-l+1) + for i <= m && j <= r { + if cmp((*arr)[i], (*arr)[j]) <= 0 { + tmp = append(tmp, (*arr)[i]) + i++ + } else { + tmp = append(tmp, (*arr)[j]) + j++ + } + } + //当一方比较到头时将另一方剩余内容全部加入进去 + for ; i <= m; i++ { + tmp = append(tmp, (*arr)[i]) + } + for ; j <= r; j++ { + tmp = append(tmp, (*arr)[j]) + } + //将局部排序结果放入迭代器中 + for i, j = l, 0; i <= r; i, j = i+1, j+1 { + (*arr)[i]=tmp[j] + } +} +``` + +##### 示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/utils/comparator" +) + +func main() { + var arr =make([]interface{},0,0) + arr=append(arr,5) + arr=append(arr,3) + arr=append(arr,2) + arr=append(arr,4) + arr=append(arr,1) + arr=append(arr,4) + arr=append(arr,3) + arr=append(arr,1) + arr=append(arr,5) + arr=append(arr,2) + comparator.Sort(&arr) + for i:=0;i< len(arr);i++{ + println(arr[i].(int)) + } +} +``` + +#### Search + +​ 对于一个**有序线性表**来说,如果要从中查找某个元素,可以通过二分的方式进行查找,即通过比对该元素和当前区间的中值的大小,从而判断去掉左侧或右侧区间,然后继续进行比较直至剩下一个元素即可,此时只需要比较该元素和待查找元素是否相等,相等则返回该元素下标,不等返回-1表示未找到该元素。 + +​ 先对传入是参数进行判断,以保证在查找时不会出现error,判断我完成后再调用search函数进行查找 + +```go +func Search(arr *[]interface{}, e interface{}, Cmp ...Comparator) (idx int) { + if arr==nil || (*arr)==nil || len((*arr)) == 0 { + return + } + //判断比较函数是否有效,若无效则寻找默认比较器 + var cmp Comparator + cmp = nil + if len(Cmp) == 0 { + cmp = GetCmp(e) + } else { + cmp = Cmp[0] + } + if cmp == nil { + //若并非默认类型且未传入比较比较函数则直接结束 + return -1 + } + //查找开始 + return search(arr, e, cmp) +} +``` + +##### search + +​ 二分查找函数: + +```go +func search(arr *[]interface{}, e interface{}, cmp Comparator) (idx int) { + //通过二分查找的方式寻找该元素 + l, m, r := 0, (len((*arr))-1)/2, len((*arr)) + for l < r { + m = (l + r) / 2 + if cmp((*arr)[m], e) < 0 { + l = m + 1 + } else { + r = m + } + } + //查找结束 + if (*arr)[l] == e { + //该元素存在,返回下标 + return l + } + //该元素不存在,返回-1 + return -1 +} +``` + +##### 示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/utils/comparator" +) + +func main() { + var arr =make([]interface{},0,0) + arr=append(arr,5) + arr=append(arr,3) + arr=append(arr,2) + arr=append(arr,4) + arr=append(arr,1) + arr=append(arr,4) + arr=append(arr,3) + arr=append(arr,1) + arr=append(arr,5) + arr=append(arr,2) + comparator.Sort(&arr) + for i:=0;i< len(arr);i++{ + println(arr[i].(int)) + } + fmt.Println("search:",comparator.Search(&arr,3)) +} +``` + +#### NthElement + +​ 对于要找的第n个元素**(下标从0开始)**,可以使用和二分排序类似的方式,只不过,由于只需要将第n个元素放到第n位,所以只需要排序第n位所在的区间,即对利用中间值进行二分之后所获得的两个区间,**只需要对包含n的区间进行二分即可**,另一个区间可以直接不管。 + +​ 该函数结果将会返回位于n的元素,该过程分两部分进行,第一部分验证其可执行情况,即指针是否为nil,数组是否为nil,n是否超出数组范围之类的情况,若出现类似情况,则直接返回nil即可。否则则对数组进行有限排序。 + +```go +func NthElement(arr *[]interface{}, n int, Cmp ...Comparator) (value interface{}){ + if arr==nil || (*arr)==nil || len((*arr)) == 0 { + return nil + } + //判断比较函数是否有效 + var cmp Comparator + cmp = nil + if len(Cmp) > 0 { + cmp = Cmp[0] + } else { + cmp = GetCmp((*arr)[0]) + } + if cmp == nil { + return nil + } + //判断待确认的第n位是否在该集合范围内 + if len((*arr)) < n || n<0 { + return nil + } + //进行查找 + nthElement(arr,0,len((*arr))-1, n, cmp) + return (*arr)[n] +} +``` + +##### nthElement + +实现部分不返回任何值,仅仅是进行有限排序,即仅排序包含n的区间。 + +```go +func nthElement(arr *[]interface{},l,r int, n int, cmp Comparator){ + //二分该区域并对此进行预排序 + if l >= r { + return + } + m := (*arr)[(r + l) / 2] + i, j := l-1, r+1 + for i < j { + i++ + for cmp((*arr)[i], m) < 0 { + i++ + } + j-- + for cmp((*arr)[j], m) > 0 { + j-- + } + if i < j { + (*arr)[i],(*arr)[j]=(*arr)[j],(*arr)[i] + } + } + //确认第n位的范围进行局部二分 + if n-1 >= i { + nthElement(arr,j+1,r, n, cmp) + } else { + nthElement(arr,l,j, n, cmp) + } +} +``` + +##### 示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/utils/comparator" +) + +func main() { + var arr =make([]interface{},0,0) + arr=append(arr,5) + arr=append(arr,3) + arr=append(arr,2) + arr=append(arr,4) + arr=append(arr,1) + arr=append(arr,4) + arr=append(arr,3) + arr=append(arr,1) + arr=append(arr,5) + arr=append(arr,2) + for i:=0;i< len(arr);i++{ + fmt.Println("n:",comparator.NthElement(&arr,i)) + } +} +``` + +#### Bound + +​ 对于一组有序线性表,可以通过二分查找的方式,获得它的上下界,当待查找元素不存在于该线性表内时,则返回的上届是小于它的最大值,返回的下届是大于它的最小值。当元素存在于线性表内时,返回的上界是该元素最右侧的下标,下届是该元素最左侧的下标 + +​ 查找方法是二分查找的变形,返回查找值的边界。 + +##### UpperBound + +```go +func UpperBound(arr *[]interface{}, e interface{}, Cmp ...Comparator) (idx int) { + if arr==nil || (*arr)==nil || len((*arr)) == 0 { + return -1 + } + //判断比较函数是否有效 + var cmp Comparator + cmp = nil + if len(Cmp) == 0 { + cmp = GetCmp(e) + } else { + cmp = Cmp[0] + } + if cmp == nil { + return -1 + } + //寻找该元素的上界 + return upperBound(arr, e, cmp) +} +``` + +##### upperBound + +```go +func upperBound(arr *[]interface{}, e interface{}, cmp Comparator) (idx int) { + l, m, r := 0, len((*arr)) / 2, len((*arr))-1 + for l < r { + m = (l + r + 1) / 2 + if cmp((*arr)[m], e) <= 0 { + l = m + } else { + r = m - 1 + } + } + return l +} +``` + +##### LowerBound + +```go +func LowerBound(arr *[]interface{}, e interface{}, Cmp ...Comparator) (idx int) { + if arr==nil || (*arr)==nil || len((*arr)) == 0 { + return -1 + } + //判断比较函数是否有效 + var cmp Comparator + cmp = nil + if len(Cmp) == 0 { + cmp = GetCmp(e) + } else { + cmp = Cmp[0] + } + if cmp == nil { + return -1 + } + //寻找该元素的下界 + return lowerBound(arr, e, cmp) +} +``` + +##### lowerBound + +```go +func lowerBound(arr *[]interface{}, e interface{}, cmp Comparator) (idx int) { + l, m, r := 0, len((*arr)) / 2, len((*arr)) + for l < r { + m = (l + r) / 2 + if cmp((*arr)[m], e) >= 0 { + r = m + } else { + l = m + 1 + } + } + return l +} +``` + +##### 示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/utils/comparator" +) + +func main() { + var arr =make([]interface{},0,0) + arr=append(arr,5) + arr=append(arr,3) + arr=append(arr,2) + arr=append(arr,4) + arr=append(arr,1) + arr=append(arr,4) + arr=append(arr,3) + arr=append(arr,1) + arr=append(arr,5) + arr=append(arr,2) + comparator.Sort(&arr) + for i:=0;i< len(arr);i++{ + fmt.Println(i,"=",arr[i]) + } + fmt.Println("\n\n\n") + for i:=0;i< len(arr);i++{ + fmt.Println(i) + fmt.Println("upper:",comparator.UpperBound(&arr,i)) + fmt.Println("lower:",comparator.LowerBound(&arr,i)) + fmt.Println() + } +} +``` + diff --git a/blog/数据结构STL——golang实现环ring.md b/blog/数据结构STL——golang实现环ring.md new file mode 100644 index 0000000..be65e30 --- /dev/null +++ b/blog/数据结构STL——golang实现环ring.md @@ -0,0 +1,461 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 环(ring),是一种**离散的环状线性结构**,即**首尾连接的线性结构**,它是又多个分布在不同物理空间的结点,通过指针链接建立逻辑连接而形成的线性结构。 + +​ 但不同的地方在于环本身没有首尾结点之分,甚至说,它没有首尾结点,它可以看作是**将链表的首尾结点连接起来**,即任何一个结点都可以看作是首节点也可以看作是尾结点,可以通过对环中任意一个结点向前或向后遍历得到全部结点。 + +​ 同链表一样,它的所有结点之间都是相互分离的,基本不存在在物理上临接的可能(不绝对),所以它要增加或删除结点的过程也是十分简单的,在添加时建立待添加结点和前后节点的连接即可,删除时建立前后节点的连接即可。适用于频繁删减的情况,或者循环遍历的情况。 + +### 原理 + +​ 对于一个环状线性结构来说,主要是要建立其它的连接,相比较于用数组的形式实现来说,数组实现需要分配一段连续的空间用以存储数据,但当数组分配的空间不足时,则需要再重新分配空间并将原有的元素复制过去,考虑到复制数据的时间开销也是不容忽略的,所以需要找到另一个结构去尽量减少复制元素的时间开销。 + +​ 同时,对于使用数组去实现环状结构来说,它需要通过逻辑上去建立首尾结点的连接,而不是将这两个点连起来,直观上并不是十分友好,特别是在扩容缩容的时候需要做的事情太多了,时间开销太大了,所以本次**实现并不采用数组形式实现**,但**数组形式仍然是可行的**。 + +#### 添加策略 + +​ 对于环来说,由于它的结点之间的物理间隔并不是连续的,而是根据需要随机的分布在整个内存中的,同时,对于一个环来说,**环并不存在一个核心结点**,它是任意一个结点都可以当作核心结点去使用。 + +​ 对于此种特性,ring在添加结点时只会出现两种情况: + +1. 环不存在:创造一个自环的结点并持有即可,自环指自己前后节点均指向自己 +2. 环存在:在当前持有的结点后面插入一个结点即可,同时建立新结点和前后节点之间的关系,此时持有结点不变 + +#### 删除策略 + +​ 同添加策略类似,考虑到环本身无核心的特性,只需要判断销毁该结点后是否仍有结点即可,有则更换持有结点,否则销毁环即可。 + +​ ring删除主要会有以下三种情况: + +1. 环不存在:结束 +2. 环仅有一个结点:销毁环 +3. 环有多个结点:销毁当前持有结点,并将持有结点切换为原持有节点的**下一个结点**,同时通过在新持有结点前插入原持有结点的前结点实现连接。 + +### 实现 + +​ ring环结构体,包含环的头尾节点指针,当增删结点时只需要移动到对应位置进行操作即可,当一个节点进行增删时需要同步修改其临接结点的前后指针,结构体中记录该环中当前所持有的结点的指针即可,同时记录该环中存在多少元素即size,使用并发控制锁以保证数据一致性。 + +```go +type ring struct { + now *node //环当前持有的结点指针 + size uint64 //当前存储的元素个数 + mutex sync.Mutex //并发控制锁 +} +``` + +​ 环的node节点结构体,pre和next是该节点的前后两个节点的指针,用以保证环整体是相连的。 + +```go +type node struct { + data interface{} //结点所承载的元素 + pre *node //前结点指针 + next *node //后结点指针 +} +``` + +#### 接口 + +```go +type ringer interface { + Iterator() (i *Iterator.Iterator) //创建一个包含环中所有元素的迭代器并返回其指针 + Size() (size uint64) //返回环所承载的元素个数 + Clear() //清空该环 + Empty() (b bool) //判断该环是否位空 + Insert(e interface{}) //向环当前位置后方插入元素e + Erase() //删除当前结点并持有下一结点 + Value() (e interface{}) //返回当前持有结点的元素 + Set(e interface{}) //在当前结点设置其承载的元素为e + Next() //持有下一节点 + Pre() //持有上一结点 +} +``` + +```go +type noder interface { + preNode() (m *node) //返回前结点指针 + nextNode() (m *node) //返回后结点指针 + insertPre(pre *node) //在该结点前插入结点并建立连接 + insertNext(next *node) //在该结点后插入结点并建立连接 + erase() //删除该结点,并使该结点前后两结点建立连接 + value() (e interface{}) //返回该结点所承载的元素 + setValue(e interface{}) //修改该结点承载元素为e +} +``` + +#### New + +​ 新建一个ring环容器并返回,初始持有的结点不存在,即为nil,初始size为0。 + +```go +func New() (r *ring) { + return &ring{ + now: nil, + size: 0, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个自环结点并返回其指针,初始首结点的前后结点指针都为自身。 + +```go +func newNode(e interface{}) (n *node) { + n = &node{ + data: e, + pre: nil, + next: nil, + } + n.pre = n + n.next = n + return n +} +``` + +#### Iterator + +​ 以ring环容器做接收者,将ring环容器中所承载的元素放入迭代器中,从该结点开始向后遍历获取全部承载的元素。 + +```go +func (r *ring) Iterator() (i *Iterator.Iterator) { + if r == nil { + r = New() + } + r.mutex.Lock() + //将所有元素复制出来放入迭代器中 + tmp := make([]interface{}, r.size, r.size) + //从当前结点开始向后遍历 + for n, idx := r.now, uint64(0); n != nil && idx < r.size; n, idx = n.nextNode(), idx+1 { + tmp[idx] = n.value() + } + i = Iterator.New(&tmp) + r.mutex.Unlock() + return i +} +``` + +​ 以node结点做接收者,返回该结点的前结点。 + +```go +func (n *node) preNode() (pre *node) { + if n == nil { + return + } + return n.pre +} +``` + +​ 以node结点做接收者,返回该结点的后结点。 + +```go +func (n *node) nextNode() (next *node) { + if n == nil { + return + } + return n.next +} +``` + +#### Size + +​ 以ring环容器做接收者,返回该容器当前含有元素的数量。 + +```go +func (r *ring) Size() (size uint64) { + if r == nil { + r = New() + } + return r.size +} +``` + +#### Clear + +​ 以ring环容器做接收者,将该容器中所承载的元素清空,将该容器的当前持有的结点置为nil,长度初始为0。 + +```go +func (r *ring) Clear() { + if r == nil { + r = New() + } + r.mutex.Lock() + //销毁环 + r.now = nil + r.size = 0 + r.mutex.Unlock() +} +``` + +#### Empty + +​ 以ring环容器做接收者,判断该ring环容器是否含有元素,该判断过程通过size进行判断,size为0则为true,否则为false。 + +```go +func (r *ring) Empty() (b bool) { + if r == nil { + r = New() + } + return r.size == 0 +} +``` + +#### Insert + +​ 以ring环容器做接收者,通过环中当前持有的结点进行添加,如果环为建立,则新建一个自环结点设为环,存在持有的结点,则在其后方添加即可。 + +```go +func (r *ring) Insert(e interface{}) { + if r == nil { + r = New() + } + r.mutex.Lock() + //新建自环结点 + n := newNode(e) + if r.size == 0 { + //原本无环,设为新环 + r.now = n + } else { + //持有结点,在后方插入 + r.now.insertNext(n) + } + r.size++ + r.mutex.Unlock() +} +``` + +​ 以node结点做接收者,对该结点插入前结点,并建立前结点和该结点之间的连接。 + +```go +func (n *node) insertPre(pre *node) { + if n == nil || pre == nil { + return + } + pre.next = n + pre.pre = n.pre + if n.pre != nil { + n.pre.next = pre + } + n.pre = pre +} +``` + +​ 以node结点做接收者,对该结点插入后结点,并建立后结点和该结点之间的连接。 + +```go +func (n *node) insertNext(next *node) { + if n == nil || next == nil { + return + } + next.pre = n + next.next = n.next + if n.next != nil { + n.next.pre = next + } + n.next = next +} +``` + +#### Erase + +​ 以ring环容器做接收者,先判断是否仅持有一个结点,若仅有一个结点,则直接销毁环,否则将当前持有结点设为下一节点,并前插原持有结点的前结点即可。 + +```go +func (r *ring) Erase() { + if r == nil { + r = New() + } + if r.size == 0 { + return + } + r.mutex.Lock() + //删除开始 + if r.size == 1 { + //环内仅有一个结点,销毁环即可 + r.now = nil + } else { + //环内还有其他结点,将持有结点后移一位 + //后移后将当前结点前插原持有结点的前结点 + r.now = r.now.nextNode() + r.now.insertPre(r.now.preNode().preNode()) + } + r.size-- + r.mutex.Unlock() +} +``` + +​ 以node结点做接收者,销毁该结点,同时建立该节点前后节点之间的连接。 + +```go +func (n *node) erase() { + if n == nil { + return + } + if n.pre == nil && n.next == nil { + return + } else if n.pre == nil { + n.next.pre = nil + } else if n.next == nil { + n.pre.next = nil + } else { + n.pre.next = n.next + n.next.pre = n.pre + } + n = nil +} +``` + +#### Value + +​ 以ring环容器做接收者,获取环中当前持有节点所承载的元素,若环中持有的结点不存在,直接返回nil。 + +```go +func (r *ring) Value() (e interface{}) { + if r == nil { + r = New() + } + if r.now == nil { + //无持有结点,直接返回nil + return nil + } + return r.now.value() +} +``` + +​ 以node结点做接收者,返回该结点所要承载的元素。 + +```go +func (n *node) value() (e interface{}) { + if n == nil { + return nil + } + return n.data +} +``` + +#### Set + +​ 以ring环容器做接收者,修改当前持有结点所承载的元素,若未持有结点,直接结束即可。 + +```go +func (r *ring) Set(e interface{}) { + if r == nil { + r = New() + } + if r.now == nil { + return + } + r.mutex.Lock() + r.now.setValue(e) + r.mutex.Unlock() +} +``` + +​ 以node结点做接收者,对该结点设置其承载的元素。 + +```go +func (n *node) setValue(e interface{}) { + if n == nil { + return + } + n.data = e +} +``` + +#### Next + +​ 以ring环容器做接收者,将当前持有的结点后移一位,若当前无持有结点,则直接结束。 + +```go +func (r *ring) Next() { + if r == nil { + r = New() + } + if r.now == nil { + return + } + r.mutex.Lock() + r.now = r.now.nextNode() + r.mutex.Unlock() +} +``` + +#### Pre + +​ 以ring环容器做接收者,将当前持有的结点前移一位,若当前无持有结点,则直接结束。 + +```go +func (r *ring) Pre() { + if r == nil { + r = New() + } + if r.size == 0 { + return + } + r.mutex.Lock() + r.now = r.now.preNode() + r.mutex.Unlock() +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/ring" + "sync" +) + +func main() { + r:=ring.New() + wg:=sync.WaitGroup{} + for i:=0;i<10;i++{ + wg.Add(1) + go func(num int) { + r.Insert(num) + wg.Done() + }(i) + } + wg.Wait() + for i:=uint64(0);i 2 +> 0 +> 1 +> 5 +> 3 +> 4 +> 7 +> 6 +> 8 +> 9 +> ring当前持有结点的元素: 2 +> ring修改有持有的结点元素: -1 +> 删除后 +> 9 +> 8 +> 6 +> 7 +> 4 +> 3 +> 5 +> 1 +> 0 diff --git a/blog/数据结构STL——golang实现迭代器iterator.md b/blog/数据结构STL——golang实现迭代器iterator.md new file mode 100644 index 0000000..b401c08 --- /dev/null +++ b/blog/数据结构STL——golang实现迭代器iterator.md @@ -0,0 +1,351 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### Iterator + +#### 概述 + +iterator模式:提供一种方法,使之能依次访问容器内的各个元素,而又不暴露该聚合物内部的表述方式。 +STL的中心思想是将算法与数据结构分离,彼此独立设计,最后在用iterator将他们结合在一起,获得最大的适配性。 + +由于golang官方未实现泛型,而interface存在可以替换为任意结构的特性,故而可以**使用interface实现泛型**。 + +一个迭代器需要包括其存储的元素序列以及内部存储一个该迭代器当前指向的元素下标。 + +```go +//Iterator迭代器 +//包含泛型切片和该迭代器当前指向元素的下标 +//可通过下标和泛型切片长度来判断是否可以前移或后移 +//当index不小于0时迭代器可前移 +//当index小于data的长度时可后移 +type Iterator struct { + data *[]interface{} //该迭代器中存放的元素集合的指针 + index int //该迭代器当前指向的元素下标,-1即不存在元素 +} +``` + +对于一个迭代器,它需要执行的主要方法有:返回该首尾迭代器,获取指向某位的迭代器,获取指定位置的元素,判断能否前后移动以及进行前后移动。 + +```go +type Iteratorer interface { + Begin() (I *Iterator) //将该迭代器设为位于首节点并返回新迭代器 + End() (I *Iterator) //将该迭代器设为位于尾节点并返回新迭代器 + Get(idx int) (I *Iterator) //将该迭代器设为位于第idx节点并返回该迭代器 + Value() (e interface{}) //返回该迭代器下标所指元素 + HasNext() (b bool) //判断该迭代器是否可以后移 + Next() (b bool) //将该迭代器后移一位 + HasPre() (b bool) //判罚该迭代器是否可以前移 + Pre() (b bool) //将该迭代器前移一位 +} +``` + +#### 接口实现 + +##### New + +对于迭代器的初始化,需要传入一个所要承载的元素集合的指针,可以自己选择性传入一个index下标。该函数会返回一个承载了该元素集合的迭代器。 + +```go +func New(data *[]interface{}, Idx ...int) (i *Iterator) { + //迭代器下标 + var idx int + if len(Idx) <= 0 { + //没有传入下标,则将下标设为0 + idx = 0 + } else { + //有传入下标,则将传入下标第一个设为迭代器下标 + idx = Idx[0] + } + if len((*data)) > 0 { + //如果元素集合非空,则判断下标是否超过元素集合范围 + if idx >= len((*data)) { + //如果传入下标超过元素集合范围则寻找最近的下标值 + idx = len((*data)) - 1 + } + } else { + //如果元素集合为空则将下标设为-1 + idx = -1 + } + //新建并返回迭代器 + return &Iterator{ + data: data, + index: idx, + } +} +``` + +##### Bgein + +以原有的迭代器做接受器,返回一个承载相同元素但下标位于首位的迭代器。 + +```go +func (i *Iterator) Begin() (I *Iterator) { + if i == nil { + //迭代器为空,直接结束 + return nil + } + if len((*i.data)) == 0 { + //迭代器元素集合为空,下标设为-1 + i.index = -1 + } else { + //迭代器元素集合非空,下标设为0 + i.index = 0 + } + //返回修改后的新指针 + return &Iterator{ + data: i.data, + index: i.index, + } +} +``` + +##### End + +以原有的迭代器做接受器,返回一个承载相同元素但下标位于末尾的迭代器。 + +```go +func (i *Iterator) End() (I *Iterator) { + if i == nil { + //迭代器为空,直接返回 + return nil + } + if len((*i.data)) == 0 { + //元素集合为空,下标设为-1 + i.index = -1 + } else { + //元素集合非空,下标设为最后一个元素的下标 + i.index = len((*i.data)) - 1 + } + //返回修改后的该指针 + return &Iterator{ + data: i.data, + index: i.index, + } +} +``` + +##### Get + +​ 以原有的迭代器做接受器,返回一个承载相同元素但下标为自己传入的idx的迭代器,当idx不在元素集合的范围内时,下标设为距离idx最近的值,即小于0设为首位,大于元素集合长度则设为尾部,其他情况设为idx。 + +```go +func (i *Iterator) Get(idx int) (I *Iterator) { + if i == nil { + //迭代器为空,直接返回 + return nil + } + if idx <= 0 { + //预设下标超过元素集合范围,将下标设为最近元素的下标,此状态下为首元素下标 + idx = 0 + } else if idx >= len((*i.data))-1 { + //预设下标超过元素集合范围,将下标设为最近元素的下标,此状态下为尾元素下标 + idx = len((*i.data)) - 1 + } + if len((*i.data)) > 0 { + //元素集合非空,迭代器下标设为预设下标 + i.index = idx + } else { + //元素集合为空,迭代器下标设为-1 + i.index = -1 + } + //返回修改后的迭代器指针 + return i +} +``` + +##### Value + +以原有的迭代器做接受器,返回该迭代器当前下标指向的元素 + +```go +func (i *Iterator) Value() (e interface{}) { + if i == nil { + //迭代器为nil,返回nil + return nil + } + if len((*i.data)) == 0 { + //元素集合为空,返回nil + return nil + } + if i.index <= 0 { + //下标超过元素集合范围下限,最近元素为首元素 + i.index = 0 + } + if i.index >= len((*i.data)) { + //下标超过元素集合范围上限,最近元素为尾元素 + i.index = len((*i.data)) - 1 + } + //返回下标指向元素 + return (*i.data)[i.index] +} +``` + +##### HasNext + +以原有的迭代器做接受器,判断该迭代器是否可以进行后移,可以则返回true否则返回false。 + +```go +func (i *Iterator) HasNext() (b bool) { + if i == nil { + //迭代器为nil时不能后移 + return false + } + if len((*i.data)) == 0 { + //元素集合为空时不能后移 + return false + } + //下标到达元素集合上限时不能后移,否则可以后移 + return i.index < len((*i.data)) +} +``` + +##### Next + +​ 以原有的迭代器做接受器,将迭代器下标后移,当满足后移条件时进行后移同时返回true,当不满足后移条件时将下标设为尾元素下标同时返回false,当迭代器为nil时返回false。 + +```go +func (i *Iterator) Next() (b bool) { + if i == nil { + //迭代器为nil时返回false + return false + } + if i.HasNext() { + //满足后移条件时进行后移 + i.index++ + return true + } + if len((*i.data)) == 0 { + //元素集合为空时下标设为-1同时返回false + i.index = -1 + return false + } + //不满足后移条件时将下标设为尾元素下标并返回false + i.index = len((*i.data)) - 1 + return false +} +``` + +##### HasPre + +以原有的迭代器做接受器,判断该迭代器是否可以进行前移,可以则返回true否则返回false。 + +```go +func (i *Iterator) End() (I *Iterator) { + if i == nil { + //迭代器为空,直接返回 + return nil + } + if len((*i.data)) == 0 { + //元素集合为空,下标设为-1 + i.index = -1 + } else { + //元素集合非空,下标设为最后一个元素的下标 + i.index = len((*i.data)) - 1 + } + //返回修改后的该指针 + return &Iterator{ + data: i.data, + index: i.index, + } +} +``` + +##### Pre + +​ 以原有的迭代器做接受器,将迭代器下标前移,当满足前移条件时进行前移同时返回true,当不满足前移条件时将下标设为首元素下标同时返回false,当迭代器为nil时返回false,当元素集合为空时下标设为-1同时返回false。 + +```go +func (i *Iterator) Pre() (b bool) { + if i == nil { + //迭代器为nil时返回false + return false + } + if i.HasPre() { + //满足后移条件时进行前移 + i.index-- + return true + } + if len((*i.data)) == 0 { + //元素集合为空时下标设为-1同时返回false + i.index = -1 + return false + } + //不满足后移条件时将下标设为尾元素下标并返回false + i.index = 0 + return false +} +``` + +#### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/utils/iterator" +) + +func main() { + var arr =make([]interface{},0,0) + arr=append(arr,5) + arr=append(arr,3) + arr=append(arr,2) + arr=append(arr,4) + arr=append(arr,1) + arr=append(arr,4) + arr=append(arr,3) + arr=append(arr,1) + arr=append(arr,5) + arr=append(arr,2) + i:=Iterator.New(&arr) + fmt.Println("begin") + for i:=i.Begin();i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + fmt.Println() + fmt.Println("end") + for i:=i.End();i.HasPre();i.Pre(){ + fmt.Println(i.Value()) + } + fmt.Println() + fmt.Println("get4") + for i:=i.Get(4);i.HasNext();i.Next(){ + fmt.Println(i.Value()) + } + fmt.Println() +} +``` + +##### 示例结果 + +> begin +> 5 +> 3 +> 2 +> 4 +> 1 +> 4 +> 3 +> 1 +> 5 +> 2 +> +> end +> 2 +> 5 +> 1 +> 3 +> 4 +> 1 +> 4 +> 2 +> 3 +> 5 +> +> get4 +> 1 +> 4 +> 3 +> 1 +> 5 +> 2 \ No newline at end of file diff --git a/blog/数据结构STL——golang实现链表list.md b/blog/数据结构STL——golang实现链表list.md new file mode 100644 index 0000000..8143d74 --- /dev/null +++ b/blog/数据结构STL——golang实现链表list.md @@ -0,0 +1,611 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 链表(list),是一种**离散的线性结构**,它是又多个分布在不同物理空间的结点,通过指针链接建立逻辑连接而形成的线性结构。 + +​ 由于它的一个个结点相互之间是分离开的,所以它增加和删除结点的过程就会变得十分简单,只需要找到对应节点并将其增加/删除即可,同时修改该结点前后结点的指针以保证整个链表不断开即可,对整个链表的大多数元素来说几乎没有影响,适用于频繁增删的情况,但它也会频繁的分配空间。 + +### 原理 + +​ 对于一个线性结构来说,主要是要建立其它的连接,相比较于用数组的形式实现来说,数组实现需要分配一段连续的空间用以存储数据,但当数组分配的空间不足时,则需要再重新分配空间并将原有的元素复制过去,考虑到复制数据的时间开销也是不容忽略的,所以需要找到另一个结构去尽量减少复制元素的时间开销。 + +​ 对此,可以选择使用链表,它是**将多个分离的点通过指针连接起来**,对于增删元素都只需要修改对应的结点和它前后连接的结点即可,对于链表中存储的其他结点不需要做修改,即可以极大的省去频繁复制元素的时间开销,但相应的,由于每个结点是离散的,每次增删结点都需要分配空间,增加了分配空间的时间开销。 + +​ 链表和数组两种数据结构各有千秋,当在需要承载的元素过多的时候,可以选择使用链表以减少频繁复制的时间开销,当元素不多时可以选择之间分配一个较大的数组去存储以减少分配空间的时间开销。 + +#### 添加策略 + +​ 对于链表来说,由于它的结点之间的物理间隔并不是连续的,而是根据需要随机的分布在整个内存中的,同时,对于一个链表来说,核心的主要是它的首尾结点,可以通过首尾结点进行元素的添加,添加时需要先遍历到对应的位置再进行添加,可以根据位置和总承载元素的情况选择从前或从后进行遍历以减少时间开销。 + +​ list添加主要会有以下四种情况: + +1. 链表不存在:创建新的结点设为头尾结点即可 +2. 添加在首部:从头结点添加并修改链表的头结点即可 +3. 添加在尾部:从尾结点添加并修改链表的尾结点即可 +4. 添加的中部:从最近的首尾结点开始遍历找到对应位置后插入即可,同时要**修改前后结点之间的指针以指向自身**,保持链表的连续性。 + +#### 删除策略 + +​ 同添加策略一样,链表的删除特性和添加是类似的,只需要删除一个结点即可,同时修改前后连接结点的指针以保证链表的连续性,当删除的是头尾结点时在修改记录的头尾结点即可。当所有元素删除完毕后,需要**销毁链表**。 + +​ list删除主要会有以下五种情况: + +1. 链表不存在:结束 +2. 删除首结点:将首节点设为首节点的下一结点,同时销毁首结点 +3. 删除尾结点:将尾节点设为尾节点的上一结点,同时销毁尾结点 +4. 删除中间结点:从最近的首尾结点开始遍历找到对应位置后删除即可,同时**将前后结点连接起来**,以保证链表的连续性。 +5. 删除后链表中不存在元素:销毁链表 + +### 实现 + +​ list链表结构体,包含链表的头尾节点指针,当增删结点时只需要找到对应位置进行操作即可,当一个节点进行增删时需要同步修改其临接结点的前后指针,结构体中记录整个链表的首尾指针,同时记录其当前已承载的元素,使用并发控制锁以保证数据一致性。 + +```go +type list struct { + first *node //链表首节点指针 + last *node //链表尾节点指针 + size uint64 //当前存储的元素个数 + mutex sync.Mutex //并发控制锁 +} +``` + +​ 链表的node节点结构体,pre和next是该节点的前后两个节点的指针,用以保证链表整体是相连的。 + +```go +type node struct { + data interface{} //结点所承载的元素 + pre *node //前结点指针 + next *node //后结点指针 +} +``` + +#### 接口 + +```go +type lister interface { + Iterator() (i *Iterator.Iterator) //创建一个包含链表中所有元素的迭代器并返回其指针 + Sort(Cmp ...comparator.Comparator) //将链表中所承载的所有元素进行排序 + Size() (size uint64) //返回链表所承载的元素个数 + Clear() //清空该链表 + Empty() (b bool) //判断该链表是否位空 + Insert(idx uint64, e interface{}) //向链表的idx位(下标从0开始)插入元素组e + Erase(idx uint64) //删除第idx位的元素(下标从0开始) + Get(idx uint64) (e interface{}) //获得下标为idx的元素 + Set(idx uint64, e interface{}) //在下标为idx的位置上放置元素e + IndexOf(e interface{}, Equ ...comparator.Equaler) (idx uint64) //返回和元素e相同的第一个下标 + SubList(begin, num uint64) (newList *list) //从begin开始复制最多num个元素以形成新的链表 +} +``` + +```go +type noder interface { + preNode() (m *node) //返回前结点指针 + nextNode() (m *node) //返回后结点指针 + insertPre(pre *node) //在该结点前插入结点并建立连接 + insertNext(next *node) //在该结点后插入结点并建立连接 + erase() //删除该结点,并使该结点前后两结点建立连接 + value() (e interface{}) //返回该结点所承载的元素 + setValue(e interface{}) //修改该结点承载元素为e +} +``` + +#### New + +​ 新建一个list链表容器并返回,初始链表首尾节点为nil,初始size为0。 + +```go +func New() (l *list) { + return &list{ + first: nil, + last: nil, + size: 0, + mutex: sync.Mutex{}, + } +} +``` + +​ 新建一个结点并返回其指针, 初始首结点的前后结点指针都为nil。 + +```go +func newNode(e interface{}) (n *node) { + return &node{ + data: e, + pre: nil, + next: nil, + } +} +``` + +#### Iterator + +​ 以list链表容器做接收者,将list链表容器中所承载的元素放入迭代器中。 + +```go +func (l *list) Iterator() (i *Iterator.Iterator) { + if l == nil { + l = New() + } + l.mutex.Lock() + //将所有元素复制出来放入迭代器中 + tmp := make([]interface{}, l.size, l.size) + for n, idx := l.first, uint64(0); n != nil && idx < l.size; n, idx = n.nextNode(), idx+1 { + tmp[idx] = n.value() + } + i = Iterator.New(&tmp) + l.mutex.Unlock() + return i +} +``` + +​ 以node结点做接收者,返回该结点的前结点。 + +```go +func (n *node) preNode() (pre *node) { + if n == nil { + return + } + return n.pre +} +``` + +​ 以node结点做接收者,返回该结点的后结点。 + +```go +func (n *node) nextNode() (next *node) { + if n == nil { + return + } + return n.next +} +``` + +#### Sort + +​ 以list链表容器做接收者,将list链表容器中所承载的元素利用比较器进行排序,可以自行传入比较函数,否则将调用默认比较函数。 + +```go +func (l *list) Sort(Cmp ...comparator.Comparator) { + if l == nil { + l = New() + } + l.mutex.Lock() + //将所有元素复制出来用于排序 + tmp := make([]interface{}, l.size, l.size) + for n, idx := l.first, uint64(0); n != nil && idx < l.size; n, idx = n.nextNode(), idx+1 { + tmp[idx] = n.value() + } + if len(Cmp) > 0 { + comparator.Sort(&tmp, Cmp[0]) + } else { + comparator.Sort(&tmp) + } + //将排序结果再放入链表中 + for n, idx := l.first, uint64(0); n != nil && idx < l.size; n, idx = n.nextNode(), idx+1 { + n.setValue(tmp[idx]) + } + l.mutex.Unlock() +} +``` + +#### Size + +​ 以list链表容器做接收者,返回该容器当前含有元素的数量。 + +```go +func (l *list) Size() (size uint64) { + if l == nil { + l = New() + } + return l.size +} +``` + +#### Clear + +​ 以list链表容器做接收者,将该容器中所承载的元素清空,将该容器的首尾指针均置nil,将size重置为0。 + +```go +func (l *list) Clear() { + if l == nil { + l = New() + } + l.mutex.Lock() + //销毁链表 + l.first = nil + l.last = nil + l.size = 0 + l.mutex.Unlock() +} +``` + +#### Empty + +​ 以list链表容器做接收者,判断该list链表容器是否含有元素,如果含有元素则不为空,返回false,如果不含有元素则说明为空,返回true,如果容器不存在,返回true,该判断过程通过size进行判断,为0则为true,否则为false。 + +```go +func (l *list) Empty() (b bool) { + if l == nil { + l = New() + } + return l.size == 0 +} +``` + +#### Insert + +​ 以list链表容器做接收者,通过链表的首尾结点进行元素插入,插入的元素可以有很多个,通过判断idx。 + +```go +func (l *list) Insert(idx uint64, e interface{}) { + if l == nil { + l = New() + } + l.mutex.Lock() + n := newNode(e) + if l.size == 0 { + //链表中原本无元素,新建链表 + l.first = n + l.last = n + } else { + //链表中存在元素 + if idx == 0 { + //插入头节点 + n.insertNext(l.first) + l.first = n + } else if idx >= l.size { + //插入尾节点 + l.last.insertNext(n) + l.last = n + } else { + //插入中间节点 + //根据插入的位置选择从前或从后寻找 + if idx < l.size/2 { + //从首节点开始遍历寻找 + m := l.first + for i := uint64(0); i < idx-1; i++ { + m = m.nextNode() + } + m.insertNext(n) + } else { + //从尾节点开始遍历寻找 + m := l.last + for i := l.size - 1; i > idx; i-- { + m = m.preNode() + } + m.insertPre(n) + } + } + } + l.size++ + l.mutex.Unlock() +} +``` + +​ 以node结点做接收者,对该结点插入前结点,并建立前结点和该结点之间的连接。 + +```go +func (n *node) insertPre(pre *node) { + if n == nil || pre == nil { + return + } + pre.next = n + pre.pre = n.pre + if n.pre != nil { + n.pre.next = pre + } + n.pre = pre +} +``` + +​ 以node结点做接收者,对该结点插入后结点,并建立后结点和该结点之间的连接。 + +```go +func (n *node) insertNext(next *node) { + if n == nil || next == nil { + return + } + next.pre = n + next.next = n.next + if n.next != nil { + n.next.pre = next + } + n.next = next +} +``` + +#### Erase + +​ 以list链表容器做接收者,先判断是否为首尾结点,如果是首尾结点,在删除后将设置新的首尾结点,当链表所承载的元素全部删除后则销毁链表,删除时通过idx与总元素数量选择从前或从后进行遍历以找到对应位置,删除后,将该位置的前后结点连接起来,以保证链表不断裂。 + +```go +func (l *list) Erase(idx uint64) { + if l == nil { + l = New() + } + l.mutex.Lock() + if l.size > 0 && idx < l.size { + //链表中存在元素,且要删除的点在范围内 + if idx == 0 { + //删除头节点 + l.first = l.first.next + } else if idx == l.size-1 { + //删除尾节点 + l.last = l.last.pre + } else { + //删除中间节点 + //根据删除的位置选择从前或从后寻找 + if idx < l.size/2 { + //从首节点开始遍历寻找 + m := l.first + for i := uint64(0); i < idx; i++ { + m = m.nextNode() + } + m.erase() + } else { + //从尾节点开始遍历寻找 + m := l.last + for i := l.size - 1; i > idx; i-- { + m = m.preNode() + } + m.erase() + } + } + l.size-- + if l.size == 0 { + //所有节点都被删除,销毁链表 + l.first = nil + l.last = nil + } + } + l.mutex.Unlock() +} +``` + +​ 以node结点做接收者,销毁该结点,同时建立该节点前后节点之间的连接。 + +```go +func (n *node) erase() { + if n == nil { + return + } + if n.pre == nil && n.next == nil { + return + } else if n.pre == nil { + n.next.pre = nil + } else if n.next == nil { + n.pre.next = nil + } else { + n.pre.next = n.next + n.next.pre = n.pre + } + n = nil +} +``` + +#### Get + +​ 以list链表容器做接收者,获取第idx位结点所承载的元素,若不在链表范围内则返回nil + +```go +func (l *list) Get(idx uint64) (e interface{}) { + if l == nil { + l = New() + } + if idx >= l.size { + return nil + } + l.mutex.Lock() + if idx < l.size/2 { + //从首节点开始遍历寻找 + m := l.first + for i := uint64(0); i < idx; i++ { + m = m.nextNode() + } + e = m.value() + } else { + //从尾节点开始遍历寻找 + m := l.last + for i := l.size - 1; i > idx; i-- { + m = m.preNode() + } + e = m.value() + } + l.mutex.Unlock() + return e +} +``` + +​ 以node结点做接收者,返回该结点所要承载的元素。 + +```go +func (n *node) value() (e interface{}) { + if n == nil { + return nil + } + return n.data +} +``` + +#### Set + +​ 以list链表容器做接收者,修改第idx为结点所承载的元素,超出范围则不修改。 + +```go +func (l *list) Set(idx uint64, e interface{}) { + if l == nil { + l = New() + } + if idx >= l.size { + return + } + l.mutex.Lock() + if idx < l.size/2 { + //从首节点开始遍历寻找 + m := l.first + for i := uint64(0); i < idx; i++ { + m = m.nextNode() + } + m.setValue(e) + } else { + //从尾节点开始遍历寻找 + m := l.last + for i := l.size - 1; i > idx; i-- { + m = m.preNode() + } + m.setValue(e) + } + l.mutex.Unlock() +} +``` + +​ 以node结点做接收者,对该结点设置其承载的元素。 + +```go +func (n *node) setValue(e interface{}) { + if n == nil { + return + } + n.data = e +} +``` + +#### IndexOf + +​ 以list链表容器做接收者,返回与e相同的元素的首个位置,可以自行传入用于判断相等的相等器进行处理,遍历从头至尾,如果不存在则返回l.size。 + +```go +func (l *list) IndexOf(e interface{}, Equ ...comparator.Equaler) (idx uint64) { + if l == nil { + l = New() + } + l.mutex.Lock() + var equ comparator.Equaler + if len(Equ) > 0 { + equ = Equ[0] + } else { + equ = comparator.GetEqual() + } + n := l.first + //从头寻找直到找到相等的两个元素即可返回 + for idx = 0; idx < l.size && n != nil; idx++ { + if equ(n.value(), e) { + break + } + n = n.nextNode() + } + l.mutex.Unlock() + return idx +} +``` + +#### SubList + +​ 以list链表容器做接收者,以begin为起点(包含),最多复制num个元素进入新链表,并返回**新链表指针**。 + +```go +func (l *list) SubList(begin, num uint64) (newList *list) { + if l == nil { + l = New() + } + newList = New() + l.mutex.Lock() + if begin < l.size { + //起点在范围内,可以复制 + n := l.first + for i := uint64(0); i < begin; i++ { + n = n.nextNode() + } + m := newNode(n.value()) + newList.first = m + newList.size++ + for i := uint64(0); i < num-1 && i+begin < l.size-1; i++ { + n = n.nextNode() + m.insertNext(newNode(n.value())) + m = m.nextNode() + newList.size++ + } + newList.last = m + } + l.mutex.Unlock() + return newList +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/list" + "sync" +) + +func main() { + l := list.New() + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func(idx uint64) { + l.Insert(idx, idx) + wg.Done() + }(uint64(i)) + } + wg.Wait() + fmt.Println("使用迭代器访问全部元素;") + for i := l.Iterator(); i.HasNext(); i.Next() { + fmt.Println(i.Value()) + } + l.Set(5, "测试") + fmt.Println("输出刚设定的测试元素的位置:", l.IndexOf("测试")) + fmt.Println("从测试位生产新list,长度上限为10,并从头部输出:") + newList := l.SubList(l.IndexOf("测试"), 10) + len := newList.Size() + fmt.Println("新链表的长度:", len) + for i := uint64(0); i < len; i++ { + fmt.Println(newList.Get(0)) + newList.Erase(0) + } + fmt.Println("从结尾向首部输出原链表:") + for i := l.Size(); i > 0; i-- { + fmt.Println(l.Get(i - 1)) + l.Erase(i - 1) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 使用迭代器访问全部元素; +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 7 +> 9 +> 8 +> 输出刚设定的测试元素的位置: 5 +> 从测试位生产新list,长度上限为10,并从头部输出: +> 新链表的长度: 5 +> 测试 +> 6 +> 7 +> 9 +> 8 +> 从结尾向首部输出原链表: +> 8 +> 9 +> 7 +> 6 +> 测试 +> 4 +> 3 +> 2 +> 1 +> 0 diff --git a/blog/数据结构STL——golang实现队列queue.md b/blog/数据结构STL——golang实现队列queue.md new file mode 100644 index 0000000..76a84ce --- /dev/null +++ b/blog/数据结构STL——golang实现队列queue.md @@ -0,0 +1,327 @@ +github仓库存储地址:https://github.com/hlccd/goSTL + +### 概述 + +​ 队列(queue)是一个封装了动态大小数组的顺序容器。除了可以包含任意类型的元素外,更主要是的它满足**FIFO**的先进先出模式,对于一些排队问题可以考虑使用队列来存储。 + +​ 对于queue的实现,由于它也是一个线性容器,底层依然可以考虑使用动态数组来实现,但它和vector仍有一定的不同,vector的冗余量主要是在尾部,毕竟vector要实现随机读取的话中间和首部不能有空余量,而对于queue来说,它的添加只在尾部,而首部仅仅只做删除,所以除了在尾部留有一定的空间做添加之外,也可以在首部删除后留有不多的余量以**避免多次分配空间**。 + +### 原理 + +​ 对于一个queue来说,它可以**在尾部添加元素**,**在首部删除元素**。而同vector一样,如果每次增删都要重新分配空间,将会极大的降低效率,所以可以考虑对前后都留有一定的冗余,以此来减少分配空间和复制的次数,从而减少时间开销。 + +​ 对于首部的冗余来说,它的存在主要是为了减少删除后就重新分配空间并复制,同时对于尾部添加时,如果尾部冗余不足可以将使用的元素整体前移到首部,即将首部冗余挪到尾部去,从而减少了空间分配的次数。 + +​ 对于尾部的冗余来说,它存在的目的和vector类似,仅仅是为了后续元素时更快而已。 + +​ 和vector一样,都是通过设置一定量的冗余来换取操作次数的减少,从而提高效率减少时间损耗。 + +#### 扩容策略 + +​ 对于queue的动态数组扩容来说,由于其添加是线性且连续的,即每一次只会增加一个,并且每一次都只会在它的尾部,不像vector一样可能在数组的任意位置进行添加,同时由于首部也有冗余,所以它的扩容也需要考虑首部的情况。 + +​ queue的扩容主要由三种方案: + +1. 利用首部:**严格来说其实并没有进行扩容**,而是将承载的元素前移到首部,即将首部的冗余平移到最后进行利用。 +2. 固定扩容:固定的去增加一定量的空间,该方案时候在数组较大时添加空间,可以避免直接翻倍导致的**冗余过量**问题,到由于增加量是固定的,如果需要一次扩容很多量的话就会比较缓慢。(同vector) +3. 翻倍扩容:将原有容量直接翻倍,该方案适合在数组不太大的适合添加空间,可以提高扩容量,增加扩容效率,但数组太大时使用的话会导致一次性增加太多空间,进而造成空间的浪费。(同vector) + +#### 缩容策略 + +​ 对于queue的数组来说,它的缩容不同于扩容,由于删除元素只会在首部进行,所以缩容其实也只会在首部进行,考虑到首部并不会进行添加,所以也不需要冗余太多的量进行减缓,即可以将设定上限减少一些,如(2^10): + +1. 固定缩容:释放一个固定的空间,该方案适合在当前数组较大的时候进行,可以减缓需要缩小的量,当首部冗余超过上限时进行缩容,**一次性全部释放即可**。 +2. 折半缩容:当**首部冗余超过了实际承载的元素的数量**时,需要对首部进行缩容,同前者一样,不需要考虑首部添加的问题,即采用对首部冗余全部释放的方式。 + +### 实现 + +​ queue底层同样使用动态数组实现,同时,由于首部只做删除,尾部只做添加,而首尾两侧都有一定的冗余,所以需要对两侧都进行记录,也可以根据尾部下标减首部下标得出实际承载元素的量,与此同时,需要引入cap以记录实际分配的空间大小,也可以根据cap和end计算出尾部冗余量。同时,为了解决在高并发情况下的数据不一致问题,引入了并发控制锁。 + +```go +type queue struct { + data []interface{} //泛型切片 + begin uint64 //首节点下标 + end uint64 //尾节点下标 + cap uint64 //容量 + mutex sync.Mutex //并发控制锁 +} +``` + +#### 接口 + +```go +type queuer interface { + Iterator() (i *Iterator.Iterator) //返回包含队列中所有元素的迭代器 + Size() (num uint64) //返回该队列中元素的使用空间大小 + Clear() //清空该队列 + Empty() (b bool) //判断该队列是否为空 + Push(e interface{}) //将元素e添加到该队列末尾 + Pop() (e interface{}) //将该队列首元素弹出并返回 + Front() (e interface{}) //获取该队列首元素 + Back() (e interface{}) //获取该队列尾元素 +} +``` + +#### New + +​ 创建一个queue容器并初始化,同时返回其指针。 + +```go +func New() (q *queue) { + return &queue{ + data: make([]interface{}, 1, 1), + begin: 0, + end: 0, + cap: 1, + mutex: sync.Mutex{}, + } +} +``` + +#### Iterator + +​ 将队列中承载元素传入迭代器中,不清除队列中的冗余量。返回迭代器指针,用于遍历队列。 + +```go +func (q *queue) Iterator() (i *Iterator.Iterator) { + if q == nil { + q=New() + } + q.mutex.Lock() + tmp:=make([]interface{},q.end-q.begin,q.end-q.begin) + copy(tmp, q.data[q.begin:q.end]) + i = Iterator.New(&tmp) + q.mutex.Unlock() + return i +} +``` + +#### Size + +​ 返回queue中当前所包含的元素个数,由于其数值必然是非负整数,所以选用了**uint64**。 + +```go +func (q *queue) Size() (num uint64) { + if q == nil { + q = New() + } + return q.end - q.begin +} +``` + +#### Clear + +​ 清空了queue中所承载的所有元素。 + +```go +func (q *queue) Clear() { + if q == nil { + q = New() + } + q.mutex.Lock() + q.data = make([]interface{}, 1, 1) + q.begin = 0 + q.end = 0 + q.cap = 1 + q.mutex.Unlock() +} +``` + +#### Empty + +​ 判断queue中是否为空,通过Size()是否为0进行判断。 + +```go +func (q *queue) Empty() (b bool) { + if q == nil { + q = New() + } + return q.Size() <= 0 +} +``` + +#### Push + +​ 以queue为接受器,向queue尾部添加一个元素,添加元素会出现两种情况,第三种是还有冗余量,此时**直接覆盖**以len为下标指向的位置即可,另一种情况是没有冗余量了,需要对动态数组进行**扩容**,此时就需要利用扩容策略。另一种是尾部没有冗余量,但首部仍有冗余量,此时可以将承载元素前移,把首部冗余量”借“过来使用。 + +​ 扩容策略上文已做描述,可返回参考,该实现过程种将两者进行了结合使用,可参考下方注释。 + +​ 固定扩容值设为2^16,翻倍扩容上限也为2^16。 + +```go +func (q *queue) Push(e interface{}) { + if q == nil { + q = New() + } + q.mutex.Lock() + if q.end < q.cap { + //不需要扩容 + q.data[q.end] = e + } else { + //需要扩容 + if q.begin > 0 { + //首部有冗余,整体前移 + for i := uint64(0); i < q.end-q.begin; i++ { + q.data[i] = q.data[i+q.begin] + } + q.end -= q.begin + q.begin = 0 + } else { + //冗余不足,需要扩容 + if q.cap <= 65536 { + //容量翻倍 + if q.cap == 0 { + q.cap = 1 + } + q.cap *= 2 + } else { + //容量增加2^16 + q.cap += 2 ^ 16 + } + //复制扩容前的元素 + tmp := make([]interface{}, q.cap, q.cap) + copy(tmp, q.data) + q.data = tmp + } + q.data[q.end] = e + } + q.end++ + q.mutex.Unlock() +} +``` + +#### Pop + +​ 以queue队列容器做接收者,弹出容器首部元素,同时begin++即可,若容器为空,则不进行弹出,当弹出元素后,可能进行缩容,由于首部不会进行添加,所以不需要太多的冗余,即将首部冗余上限设为2^10,固定缩容和折半缩容都参考此值执行,具体缩容策略可参考上文介绍。 + +```go +func (q *queue) Pop() (e interface{}) { + if q == nil { + q = New() + return nil + } + if q.Empty() { + q.Clear() + return nil + } + q.mutex.Lock() + e = q.data[q.begin] + q.begin++ + if q.begin >= 1024 || q.begin*2>q.end { + //首部冗余超过2^10或首部冗余超过实际使用 + q.cap -= q.begin + q.end -= q.begin + tmp := make([]interface{}, q.cap, q.cap) + copy(tmp, q.data[q.begin:]) + q.data = tmp + q.begin=0 + } + q.mutex.Unlock() + return e +} +``` + +#### Front + +​ 以queue为接受器,返回vector所承载的元素中位于**首部**的元素,如果queue为nil或者元素数组为nil或为空,则返回nil。考虑到仅仅只是读取元素,故不对该过程进行加锁操作。 + +```go +func (q *queue) Front() (e interface{}) { + if q == nil { + q=New() + return nil + } + if q.Empty() { + q.Clear() + return nil + } + return q.data[q.begin] +} +``` + +#### Back + +​ 以queue为接受器,返回queue所承载的元素中位于**尾部**的元素,如果queue为nil或者元素数组为nil或为空,则返回nil。考虑到仅仅只是读取元素,故不对该过程进行加锁操作。 + +```go +func (q *queue) Back() (e interface{}) { + if q == nil { + q=New() + return nil + } + if q.Empty() { + q.Clear() + return nil + } + return q.data[q.end-1] +} +``` + +### 使用示例 + +```go +package main + +import ( + "fmt" + "github.com/hlccd/goSTL/data_structure/queue" + "sync" +) + +func main() { + q := queue.New() + wg := sync.WaitGroup{} + //随机插入队列中 + for i := 0; i < 8; i++ { + wg.Add(1) + go func(num int) { + fmt.Println(num) + q.Push(num) + wg.Done() + }(i) + } + wg.Wait() + fmt.Println("输出首部:", q.Front()) + fmt.Println("输出尾部:", q.Back()) + fmt.Println("弹出并输出前4个:") + for i := uint64(0); i < q.Size()-1; i++ { + fmt.Println(q.Pop()) + } + //在尾部再添加4个,从10开始以做区分 + for i := 10; i < 14; i++ { + q.Push(i) + } + fmt.Println("从头输出全部:") + for ;!q.Empty();{ + fmt.Println(q.Pop()) + } +} + +``` + +注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同 + +> 0 +> 7 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 +> 输出首部: 0 +> 输出尾部: 6 +> 弹出并输出前4个: +> 0 +> 7 +> 1 +> 2 +> 从头输出全部: +> 3 +> 4 +> 5 +> 6 +> 10 +> 11 +> 12 +> 13